import {z} from 'zod' export type Rule = {field: string, op: string, value: unknown, unknown?: ('exclude' | 'review' | null) | undefined}; export type PreferredRule = {field: string, op: string, value: unknown, weight: number}; export type Cutoffs = {pursue: number, monitor: number}; export type DiscoveryDraft = {markets: Array, must?: Array | undefined, preferred?: Array | undefined, verdict_cutoffs: Cutoffs}; export type DiscoveryQueryRequest = {buy_box: string, draft?: (DiscoveryDraft | null) | undefined, limit?: number | undefined, cursor?: (string | null) | undefined}; export type CreateApiV2DiscoveryQueryArgs = {authorization?: (string | null) | undefined, 'X-Tenant'?: (string | null) | undefined, body: DiscoveryQueryRequest}; export type CreateApiV2DiscoveryQueryOptions = {baseUrl: string, fetch?: typeof fetch | undefined}; export const rule = z.object({field: z.string().min(1).max(100), op: z.string().min(1).max(20), value: z.unknown(), unknown: z.enum(["exclude", "review"]).nullable().optional()}); export const preferredRule = z.object({field: z.string().min(1).max(100), op: z.string().min(1).max(20), value: z.unknown(), weight: z.number()}); export const cutoffs = z.object({pursue: z.number(), monitor: z.number()}); export const discoveryDraft = z.object({markets: z.array(z.string().min(1).max(100)), must: z.array(rule).optional(), preferred: z.array(preferredRule).optional(), verdict_cutoffs: cutoffs}); export const mustEvidence = z.object({field: z.string(), value: z.unknown(), status: z.enum(["pass", "fail", "unknown"])}); export const preferredEvidence = z.object({field: z.string(), value: z.unknown(), weight: z.number(), contribution: z.number()}); export const scoreBreakdown = z.object({must: z.array(mustEvidence), preferred: z.array(preferredEvidence)}); export const discoveryResult = z.object({parcel_key: z.string(), apn: z.string().nullable(), address: z.string().nullable(), lat: z.number().nullable(), lon: z.number().nullable(), acres: z.number().nullable(), developable_acres: z.number().nullable(), geometry: z.record(z.string(), z.unknown()).nullable(), bbox: z.array(z.number()).nullable(), facts: z.record(z.string(), z.unknown()), provenance: z.record(z.string(), z.unknown()), score: z.number(), verdict: z.enum(["PURSUE", "MONITOR", "HOLD"]), review_required: z.boolean(), unknowns: z.array(z.string()), score_breakdown: scoreBreakdown}); export const discoveryQueryResponse = z.object({market: z.string(), market_id: z.string(), vintage_id: z.string(), query_hash: z.string(), applied_spec: discoveryDraft, denominator: z.number().int(), preference_weight_total: z.number(), eligible_total: z.number().int(), results: z.array(discoveryResult), next_cursor: z.string().nullable()}); export const validateResponse = async ( schema: { readonly '~standard': { readonly validate: (value: unknown) => ({ readonly value: Output; readonly issues?: undefined } | { readonly issues: ReadonlyArray<{ readonly message: string; readonly path?: ReadonlyArray | undefined }> }) | Promise<{ readonly value: Output; readonly issues?: undefined } | { readonly issues: ReadonlyArray<{ readonly message: string; readonly path?: ReadonlyArray | undefined }> }> } }, value: unknown ): Promise => { const result = await schema['~standard'].validate(value) if (result.issues !== undefined) { const detail = result.issues .map(({ message, path }) => path?.length ? `${path.map((segment) => String(typeof segment === 'object' ? segment.key : segment)).join('.')}: ${message}` : message ) .join('; ') throw new Error(`Response does not match the schema: ${detail}`, { cause: { issues: result.issues } }) } return result.value }; /** * Discovery Query * * POST /v2/discovery/query */ export const createApiV2DiscoveryQuery = async (args: CreateApiV2DiscoveryQueryArgs, options: CreateApiV2DiscoveryQueryOptions) => { const {baseUrl, fetch: fetchFn = fetch} = options ?? {} const query = new URLSearchParams() const headers = new Headers() headers.set('content-type', "application/json") headers.set('accept', "application/json") if (args.authorization != null) headers.set("authorization", String(args.authorization)) if (args['X-Tenant'] != null) headers.set("X-Tenant", String(args['X-Tenant'])) const url = `${baseUrl.replace(/\/+$/, '')}/v2/discovery/query${query.size > 0 ? `?${query}` : ''}` const res = await fetchFn(url, {method: "POST", headers, body: JSON.stringify(args.body)}) if (!res.ok) throw new Error(`Request failed with status ${res.status}`, {cause: {status: res.status, body: await res.text()}}) return validateResponse(discoveryQueryResponse, await res.json()) };