@propertysimple/client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/API.md +79 -0
- package/README.md +348 -0
- package/client/src/directory-sync.d.ts +123 -0
- package/client/src/directory-sync.js +404 -0
- package/client/src/index.d.ts +5123 -0
- package/client/src/index.js +285 -0
- package/client/src/managed-sync.d.ts +129 -0
- package/client/src/managed-sync.js +410 -0
- package/contracts/src/catalog-derived.d.ts +7 -0
- package/contracts/src/catalog-derived.js +78 -0
- package/contracts/src/catalog-fields.d.ts +231 -0
- package/contracts/src/catalog-fields.js +194 -0
- package/contracts/src/catalog-inventory.d.ts +14 -0
- package/contracts/src/catalog-inventory.js +10 -0
- package/contracts/src/directory-execution.d.ts +320 -0
- package/contracts/src/directory-execution.js +122 -0
- package/contracts/src/directory-identity.d.ts +1255 -0
- package/contracts/src/directory-identity.js +129 -0
- package/contracts/src/directory-sales.d.ts +333 -0
- package/contracts/src/directory-sales.js +102 -0
- package/contracts/src/directory.d.ts +1088 -0
- package/contracts/src/directory.js +179 -0
- package/contracts/src/index.d.ts +18 -0
- package/contracts/src/index.js +18 -0
- package/contracts/src/intelligence.d.ts +366 -0
- package/contracts/src/intelligence.js +157 -0
- package/contracts/src/location-serving.d.ts +13 -0
- package/contracts/src/location-serving.js +89 -0
- package/contracts/src/location.d.ts +73 -0
- package/contracts/src/location.js +75 -0
- package/contracts/src/openapi.d.ts +22 -0
- package/contracts/src/openapi.js +109 -0
- package/contracts/src/operations.d.ts +8316 -0
- package/contracts/src/operations.js +56 -0
- package/contracts/src/opportunity-events.d.ts +38 -0
- package/contracts/src/opportunity-events.js +69 -0
- package/contracts/src/platform-api.d.ts +12 -0
- package/contracts/src/platform-api.js +1 -0
- package/contracts/src/professional-intelligence.d.ts +256 -0
- package/contracts/src/professional-intelligence.js +90 -0
- package/contracts/src/property-features.d.ts +94 -0
- package/contracts/src/property-features.js +398 -0
- package/contracts/src/publication.d.ts +44 -0
- package/contracts/src/publication.js +42 -0
- package/contracts/src/routes.d.ts +437 -0
- package/contracts/src/routes.js +496 -0
- package/contracts/src/schemas.d.ts +3198 -0
- package/contracts/src/schemas.js +674 -0
- package/contracts/src/search.d.ts +2630 -0
- package/contracts/src/search.js +223 -0
- package/contracts/src/smart-feed.d.ts +3731 -0
- package/contracts/src/smart-feed.js +109 -0
- package/openapi.json +43542 -0
- package/package.json +60 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const Key = z.string().regex(/^[a-zA-Z0-9_-]{1,80}$/);
|
|
3
|
+
const Text = z.string().min(1).max(4000);
|
|
4
|
+
const Span = z.strictObject({ id: Key, text: Text });
|
|
5
|
+
const Spans = z.array(Span).min(1).max(20);
|
|
6
|
+
const Timestamp = z.iso.datetime({ offset: true });
|
|
7
|
+
export const JudgmentTask = z.enum([
|
|
8
|
+
'claim_verification',
|
|
9
|
+
'meaningful_text_change',
|
|
10
|
+
'marketing_angle',
|
|
11
|
+
'preference_relevance',
|
|
12
|
+
'intent_routing',
|
|
13
|
+
'operational_triage',
|
|
14
|
+
]);
|
|
15
|
+
export const JudgmentRequest = z.discriminatedUnion('task', [
|
|
16
|
+
z.strictObject({
|
|
17
|
+
task: z.literal('claim_verification'),
|
|
18
|
+
claims: z.array(Span).min(1).max(10),
|
|
19
|
+
source_spans: Spans,
|
|
20
|
+
}),
|
|
21
|
+
z.strictObject({ task: z.literal('meaningful_text_change'), before: Text, after: Text }),
|
|
22
|
+
z.strictObject({
|
|
23
|
+
task: z.literal('marketing_angle'),
|
|
24
|
+
product_brief: z.string().min(1).max(1000),
|
|
25
|
+
candidates: z.array(Span).min(1).max(10),
|
|
26
|
+
source_spans: Spans,
|
|
27
|
+
previous_messages: z.array(Span).max(20),
|
|
28
|
+
}),
|
|
29
|
+
z.strictObject({
|
|
30
|
+
task: z.literal('preference_relevance'),
|
|
31
|
+
preferences: z
|
|
32
|
+
.array(z.strictObject({
|
|
33
|
+
id: Key,
|
|
34
|
+
text: z.string().min(1).max(1000),
|
|
35
|
+
importance: z.enum(['required', 'preferred']),
|
|
36
|
+
}))
|
|
37
|
+
.min(1)
|
|
38
|
+
.max(10),
|
|
39
|
+
candidates: z.array(Span).min(1).max(20),
|
|
40
|
+
}),
|
|
41
|
+
z.strictObject({ task: z.literal('intent_routing'), text: Text }),
|
|
42
|
+
z.strictObject({
|
|
43
|
+
task: z.literal('operational_triage'),
|
|
44
|
+
component: z.enum(['api', 'worker', 'source', 'model', 'database', 'unknown']),
|
|
45
|
+
sanitized_logs: z.array(Span).min(1).max(20),
|
|
46
|
+
}),
|
|
47
|
+
]);
|
|
48
|
+
const Verdict = z.enum(['supported', 'contradicted', 'not_established', 'conflicting']);
|
|
49
|
+
const Intent = z.enum([
|
|
50
|
+
'billing',
|
|
51
|
+
'listing_correction',
|
|
52
|
+
'access_issue',
|
|
53
|
+
'marketing_question',
|
|
54
|
+
'technical_issue',
|
|
55
|
+
'tour_request',
|
|
56
|
+
'property_details',
|
|
57
|
+
'agent_help',
|
|
58
|
+
'unsubscribe',
|
|
59
|
+
'other',
|
|
60
|
+
'unknown',
|
|
61
|
+
]);
|
|
62
|
+
export const JudgmentDecision = z.discriminatedUnion('task', [
|
|
63
|
+
z.strictObject({
|
|
64
|
+
task: z.literal('claim_verification'),
|
|
65
|
+
claims: z
|
|
66
|
+
.array(z.strictObject({
|
|
67
|
+
id: Key,
|
|
68
|
+
verdict: Verdict,
|
|
69
|
+
evidence_refs: z.array(Key),
|
|
70
|
+
review_required: z.boolean(),
|
|
71
|
+
}))
|
|
72
|
+
.max(10),
|
|
73
|
+
}),
|
|
74
|
+
z.strictObject({
|
|
75
|
+
task: z.literal('meaningful_text_change'),
|
|
76
|
+
change: z.enum(['material', 'editorial', 'unchanged', 'uncertain']),
|
|
77
|
+
review_required: z.boolean(),
|
|
78
|
+
}),
|
|
79
|
+
z.strictObject({
|
|
80
|
+
task: z.literal('marketing_angle'),
|
|
81
|
+
selected_candidate_id: Key.nullable(),
|
|
82
|
+
repeat_status: z.enum(['novel', 'repetition', 'unknown']),
|
|
83
|
+
evidence_refs: z.array(Key),
|
|
84
|
+
review_required: z.boolean(),
|
|
85
|
+
}),
|
|
86
|
+
z.strictObject({
|
|
87
|
+
task: z.literal('preference_relevance'),
|
|
88
|
+
ranked: z
|
|
89
|
+
.array(z.strictObject({
|
|
90
|
+
candidate_id: Key,
|
|
91
|
+
score: z.number().min(0).max(3).nullable(),
|
|
92
|
+
relevance: z.enum(['relevant', 'not_relevant', 'unknown']),
|
|
93
|
+
}))
|
|
94
|
+
.max(20),
|
|
95
|
+
review_required: z.boolean(),
|
|
96
|
+
}),
|
|
97
|
+
z.strictObject({
|
|
98
|
+
task: z.literal('intent_routing'),
|
|
99
|
+
intent: Intent,
|
|
100
|
+
urgent: z.boolean().nullable(),
|
|
101
|
+
review_required: z.boolean(),
|
|
102
|
+
}),
|
|
103
|
+
z.strictObject({
|
|
104
|
+
task: z.literal('operational_triage'),
|
|
105
|
+
category: z.enum(['auth', 'source_format', 'rate_limit', 'transient_dependency', 'unknown']),
|
|
106
|
+
severity: z.enum(['informational', 'degraded', 'blocking', 'critical', 'unknown']),
|
|
107
|
+
recommended_check: z.enum([
|
|
108
|
+
'credential_configuration',
|
|
109
|
+
'source_contract',
|
|
110
|
+
'retry_budget',
|
|
111
|
+
'dependency_health',
|
|
112
|
+
'manual_review',
|
|
113
|
+
]),
|
|
114
|
+
review_required: z.boolean(),
|
|
115
|
+
}),
|
|
116
|
+
]);
|
|
117
|
+
export const JudgmentProvenance = z.strictObject({
|
|
118
|
+
provider: z.enum(['openrouter', 'vercel_gateway', 'fixture', 'disabled']),
|
|
119
|
+
requested_model: z.string(),
|
|
120
|
+
resolved_model: z.string().nullable(),
|
|
121
|
+
model_policy: z.enum(['pinned', 'rolling_alias', 'fixture']),
|
|
122
|
+
policy_revision: z.string(),
|
|
123
|
+
adapter_version: z.string(),
|
|
124
|
+
/** A code-owned result retains the requested route but did not invoke it. */
|
|
125
|
+
execution: z.literal('deterministic_exact_comparison').optional(),
|
|
126
|
+
});
|
|
127
|
+
export const JudgmentSchema = z.strictObject({
|
|
128
|
+
id: z.string(),
|
|
129
|
+
task: JudgmentTask,
|
|
130
|
+
task_version: z.string(),
|
|
131
|
+
state: z.enum(['running', 'succeeded', 'failed', 'expired']),
|
|
132
|
+
input_fingerprint: z.string(),
|
|
133
|
+
source_trust: z.literal('caller_supplied'),
|
|
134
|
+
provenance: JudgmentProvenance,
|
|
135
|
+
result: JudgmentDecision.nullable(),
|
|
136
|
+
error_code: z.string().nullable(),
|
|
137
|
+
created_at: Timestamp,
|
|
138
|
+
completed_at: Timestamp.nullable(),
|
|
139
|
+
valid_until: Timestamp,
|
|
140
|
+
cached: z.boolean(),
|
|
141
|
+
});
|
|
142
|
+
export const IntelligenceCapabilitySchema = z.strictObject({
|
|
143
|
+
id: z.string(),
|
|
144
|
+
description: z.string(),
|
|
145
|
+
status: z.enum(['implemented', 'not_implemented']),
|
|
146
|
+
task_version: z.string().nullable(),
|
|
147
|
+
task_id: JudgmentTask.nullable(),
|
|
148
|
+
input_kind: z.string(),
|
|
149
|
+
limitations: z.array(z.string()),
|
|
150
|
+
});
|
|
151
|
+
export const IntelligenceCapabilitiesSchema = z.strictObject({
|
|
152
|
+
data: z.array(IntelligenceCapabilitySchema),
|
|
153
|
+
provider: JudgmentProvenance,
|
|
154
|
+
effects_enabled: z.boolean(),
|
|
155
|
+
catalog_model: z.string(),
|
|
156
|
+
taxonomy_version: z.string(),
|
|
157
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Listing } from './schemas.js';
|
|
2
|
+
import type { ListingLocation } from './location.js';
|
|
3
|
+
/** A withheld address is still a listing. Apply this to historical payloads too:
|
|
4
|
+
* absent source fields must never preserve an older public or geocoded location. */
|
|
5
|
+
export declare function redactListingAddress<T extends Listing>(listing: T): T;
|
|
6
|
+
export declare function emptyLocation(policyVersion: string, state: ListingLocation['state'], issue?: ListingLocation['issue']): ListingLocation;
|
|
7
|
+
/** The catalog still owns visibility. Enrichment may never undo a source display restriction. */
|
|
8
|
+
export declare function applyLocationProjection<T extends Listing>(listing: T, projection: ListingLocation): T & {
|
|
9
|
+
location: ListingLocation;
|
|
10
|
+
};
|
|
11
|
+
/** Direct reads and managed consumers must apply expiry even before the next
|
|
12
|
+
* publication sweep. Source points remain valid when only their appends expire. */
|
|
13
|
+
export declare function freshLocationProjection<T extends Listing>(listing: T, at: Date): T;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { redactSourceAddress } from './catalog-fields.js';
|
|
2
|
+
import { catalogSearchDocument } from './catalog-derived.js';
|
|
3
|
+
/** A withheld address is still a listing. Apply this to historical payloads too:
|
|
4
|
+
* absent source fields must never preserve an older public or geocoded location. */
|
|
5
|
+
export function redactListingAddress(listing) {
|
|
6
|
+
if (listing.display.address_allowed)
|
|
7
|
+
return listing;
|
|
8
|
+
const redacted = {
|
|
9
|
+
...listing,
|
|
10
|
+
address: { ...listing.address, street: null, unit: null },
|
|
11
|
+
...(listing.source_fields ? { source_fields: redactSourceAddress(listing.source_fields) } : {}),
|
|
12
|
+
...(listing.source_metadata
|
|
13
|
+
? {
|
|
14
|
+
source_metadata: {
|
|
15
|
+
...listing.source_metadata,
|
|
16
|
+
supplied_paths: listing.source_metadata.supplied_paths.filter((path) => !/(^|\.)(UnparsedAddress|UnitNumber|Street\w*|Latitude|Longitude|Location|GeocodeOptions|SubdivisionName|Directions|ParcelNumber|TaxLegalDescription|Neighborhoods)(\.|\[|$)/.test(path)),
|
|
17
|
+
},
|
|
18
|
+
}
|
|
19
|
+
: {}),
|
|
20
|
+
...(listing.property_details
|
|
21
|
+
? { property_details: { ...listing.property_details, subdivision: null } }
|
|
22
|
+
: {}),
|
|
23
|
+
latitude: null,
|
|
24
|
+
longitude: null,
|
|
25
|
+
...(listing.location
|
|
26
|
+
? {
|
|
27
|
+
location: emptyLocation(listing.location.policy_version, 'suppressed', 'display_restricted'),
|
|
28
|
+
}
|
|
29
|
+
: {}),
|
|
30
|
+
};
|
|
31
|
+
if (redacted.search_document)
|
|
32
|
+
redacted.search_document = catalogSearchDocument(redacted);
|
|
33
|
+
return redacted;
|
|
34
|
+
}
|
|
35
|
+
export function emptyLocation(policyVersion, state, issue = null) {
|
|
36
|
+
return {
|
|
37
|
+
state,
|
|
38
|
+
origin: null,
|
|
39
|
+
precision: 'unknown',
|
|
40
|
+
map_eligible: false,
|
|
41
|
+
coordinates: null,
|
|
42
|
+
accuracy_score: null,
|
|
43
|
+
accuracy_type: null,
|
|
44
|
+
match_type: null,
|
|
45
|
+
provider: null,
|
|
46
|
+
provider_version: null,
|
|
47
|
+
policy_version: policyVersion,
|
|
48
|
+
source: null,
|
|
49
|
+
resolved_at: null,
|
|
50
|
+
valid_until: null,
|
|
51
|
+
country_code: null,
|
|
52
|
+
region_code: null,
|
|
53
|
+
timezone: null,
|
|
54
|
+
enrichment: null,
|
|
55
|
+
geography: null,
|
|
56
|
+
issue,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** The catalog still owns visibility. Enrichment may never undo a source display restriction. */
|
|
60
|
+
export function applyLocationProjection(listing, projection) {
|
|
61
|
+
const location = listing.display.address_allowed
|
|
62
|
+
? projection
|
|
63
|
+
: emptyLocation(projection.policy_version, 'suppressed', 'display_restricted');
|
|
64
|
+
return {
|
|
65
|
+
...redactListingAddress(listing),
|
|
66
|
+
latitude: location.map_eligible ? (location.coordinates?.latitude ?? null) : null,
|
|
67
|
+
longitude: location.map_eligible ? (location.coordinates?.longitude ?? null) : null,
|
|
68
|
+
location,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** Direct reads and managed consumers must apply expiry even before the next
|
|
72
|
+
* publication sweep. Source points remain valid when only their appends expire. */
|
|
73
|
+
export function freshLocationProjection(listing, at) {
|
|
74
|
+
listing = redactListingAddress(listing);
|
|
75
|
+
const location = listing.location;
|
|
76
|
+
if (!location)
|
|
77
|
+
return listing;
|
|
78
|
+
if (!listing.display.address_allowed)
|
|
79
|
+
return applyLocationProjection(listing, location);
|
|
80
|
+
if (location.origin === 'geocoded' &&
|
|
81
|
+
(!location.valid_until || Date.parse(location.valid_until) <= at.getTime()))
|
|
82
|
+
return applyLocationProjection(listing, emptyLocation(location.policy_version, 'stale', 'expired'));
|
|
83
|
+
if (location.enrichment && Date.parse(location.enrichment.valid_until) <= at.getTime())
|
|
84
|
+
return {
|
|
85
|
+
...listing,
|
|
86
|
+
location: { ...location, timezone: null, geography: null, enrichment: null },
|
|
87
|
+
};
|
|
88
|
+
return listing;
|
|
89
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** Accuracy describes the evidence available; a match score is not a distance error bound. */
|
|
3
|
+
export declare const LocationSchema: z.ZodObject<{
|
|
4
|
+
state: z.ZodEnum<{
|
|
5
|
+
source: "source";
|
|
6
|
+
pending: "pending";
|
|
7
|
+
resolved: "resolved";
|
|
8
|
+
coarse: "coarse";
|
|
9
|
+
unavailable: "unavailable";
|
|
10
|
+
stale: "stale";
|
|
11
|
+
suppressed: "suppressed";
|
|
12
|
+
}>;
|
|
13
|
+
origin: z.ZodNullable<z.ZodEnum<{
|
|
14
|
+
source: "source";
|
|
15
|
+
geocoded: "geocoded";
|
|
16
|
+
}>>;
|
|
17
|
+
precision: z.ZodEnum<{
|
|
18
|
+
source_unknown: "source_unknown";
|
|
19
|
+
parcel: "parcel";
|
|
20
|
+
street: "street";
|
|
21
|
+
area: "area";
|
|
22
|
+
unknown: "unknown";
|
|
23
|
+
}>;
|
|
24
|
+
map_eligible: z.ZodBoolean;
|
|
25
|
+
coordinates: z.ZodNullable<z.ZodObject<{
|
|
26
|
+
latitude: z.ZodNumber;
|
|
27
|
+
longitude: z.ZodNumber;
|
|
28
|
+
}, z.core.$strip>>;
|
|
29
|
+
accuracy_score: z.ZodNullable<z.ZodNumber>;
|
|
30
|
+
accuracy_type: z.ZodNullable<z.ZodString>;
|
|
31
|
+
match_type: z.ZodNullable<z.ZodString>;
|
|
32
|
+
provider: z.ZodNullable<z.ZodString>;
|
|
33
|
+
provider_version: z.ZodNullable<z.ZodString>;
|
|
34
|
+
policy_version: z.ZodString;
|
|
35
|
+
source: z.ZodNullable<z.ZodString>;
|
|
36
|
+
resolved_at: z.ZodNullable<z.ZodISODateTime>;
|
|
37
|
+
valid_until: z.ZodNullable<z.ZodISODateTime>;
|
|
38
|
+
country_code: z.ZodNullable<z.ZodString>;
|
|
39
|
+
region_code: z.ZodNullable<z.ZodString>;
|
|
40
|
+
timezone: z.ZodNullable<z.ZodString>;
|
|
41
|
+
enrichment: z.ZodNullable<z.ZodObject<{
|
|
42
|
+
provider: z.ZodString;
|
|
43
|
+
provider_version: z.ZodString;
|
|
44
|
+
resolved_at: z.ZodISODateTime;
|
|
45
|
+
valid_until: z.ZodISODateTime;
|
|
46
|
+
fields: z.ZodArray<z.ZodEnum<{
|
|
47
|
+
timezone: "timezone";
|
|
48
|
+
census2025: "census2025";
|
|
49
|
+
}>>;
|
|
50
|
+
}, z.core.$strip>>;
|
|
51
|
+
geography: z.ZodNullable<z.ZodObject<{
|
|
52
|
+
census_year: z.ZodNumber;
|
|
53
|
+
state_fips: z.ZodNullable<z.ZodString>;
|
|
54
|
+
county_fips: z.ZodNullable<z.ZodString>;
|
|
55
|
+
tract_geoid: z.ZodNullable<z.ZodString>;
|
|
56
|
+
block_geoid: z.ZodNullable<z.ZodString>;
|
|
57
|
+
place_fips: z.ZodNullable<z.ZodString>;
|
|
58
|
+
cbsa_code: z.ZodNullable<z.ZodString>;
|
|
59
|
+
source: z.ZodNullable<z.ZodString>;
|
|
60
|
+
}, z.core.$strip>>;
|
|
61
|
+
issue: z.ZodNullable<z.ZodEnum<{
|
|
62
|
+
pending: "pending";
|
|
63
|
+
invalid_source_point: "invalid_source_point";
|
|
64
|
+
insufficient_address: "insufficient_address";
|
|
65
|
+
unsupported_country: "unsupported_country";
|
|
66
|
+
no_match: "no_match";
|
|
67
|
+
low_quality: "low_quality";
|
|
68
|
+
provider_failure: "provider_failure";
|
|
69
|
+
expired: "expired";
|
|
70
|
+
display_restricted: "display_restricted";
|
|
71
|
+
}>>;
|
|
72
|
+
}, z.core.$strip>;
|
|
73
|
+
export type ListingLocation = z.infer<typeof LocationSchema>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/** Accuracy describes the evidence available; a match score is not a distance error bound. */
|
|
3
|
+
export const LocationSchema = z.object({
|
|
4
|
+
state: z.enum(['source', 'pending', 'resolved', 'coarse', 'unavailable', 'stale', 'suppressed']),
|
|
5
|
+
origin: z.enum(['source', 'geocoded']).nullable(),
|
|
6
|
+
precision: z.enum(['source_unknown', 'parcel', 'street', 'area', 'unknown']),
|
|
7
|
+
map_eligible: z.boolean(),
|
|
8
|
+
coordinates: z
|
|
9
|
+
.object({ latitude: z.number().min(-90).max(90), longitude: z.number().min(-180).max(180) })
|
|
10
|
+
.nullable(),
|
|
11
|
+
accuracy_score: z.number().min(0).max(1).nullable(),
|
|
12
|
+
accuracy_type: z.string().nullable(),
|
|
13
|
+
match_type: z.string().nullable(),
|
|
14
|
+
provider: z.string().nullable(),
|
|
15
|
+
provider_version: z.string().nullable(),
|
|
16
|
+
policy_version: z.string(),
|
|
17
|
+
source: z.string().nullable(),
|
|
18
|
+
resolved_at: z.iso.datetime().nullable(),
|
|
19
|
+
valid_until: z.iso.datetime().nullable(),
|
|
20
|
+
country_code: z.string().nullable(),
|
|
21
|
+
region_code: z.string().nullable(),
|
|
22
|
+
timezone: z.string().nullable(),
|
|
23
|
+
enrichment: z
|
|
24
|
+
.object({
|
|
25
|
+
provider: z.string(),
|
|
26
|
+
provider_version: z.string(),
|
|
27
|
+
resolved_at: z.iso.datetime(),
|
|
28
|
+
valid_until: z.iso.datetime(),
|
|
29
|
+
fields: z.array(z.enum(['timezone', 'census2025'])),
|
|
30
|
+
})
|
|
31
|
+
.nullable(),
|
|
32
|
+
geography: z
|
|
33
|
+
.object({
|
|
34
|
+
census_year: z.number().int(),
|
|
35
|
+
state_fips: z
|
|
36
|
+
.string()
|
|
37
|
+
.regex(/^\d{2}$/)
|
|
38
|
+
.nullable(),
|
|
39
|
+
county_fips: z
|
|
40
|
+
.string()
|
|
41
|
+
.regex(/^\d{5}$/)
|
|
42
|
+
.nullable(),
|
|
43
|
+
tract_geoid: z
|
|
44
|
+
.string()
|
|
45
|
+
.regex(/^\d{11}$/)
|
|
46
|
+
.nullable(),
|
|
47
|
+
block_geoid: z
|
|
48
|
+
.string()
|
|
49
|
+
.regex(/^\d{15}$/)
|
|
50
|
+
.nullable(),
|
|
51
|
+
place_fips: z
|
|
52
|
+
.string()
|
|
53
|
+
.regex(/^\d{7}$/)
|
|
54
|
+
.nullable(),
|
|
55
|
+
cbsa_code: z
|
|
56
|
+
.string()
|
|
57
|
+
.regex(/^\d{5}$/)
|
|
58
|
+
.nullable(),
|
|
59
|
+
source: z.string().nullable(),
|
|
60
|
+
})
|
|
61
|
+
.nullable(),
|
|
62
|
+
issue: z
|
|
63
|
+
.enum([
|
|
64
|
+
'invalid_source_point',
|
|
65
|
+
'insufficient_address',
|
|
66
|
+
'unsupported_country',
|
|
67
|
+
'no_match',
|
|
68
|
+
'low_quality',
|
|
69
|
+
'provider_failure',
|
|
70
|
+
'pending',
|
|
71
|
+
'expired',
|
|
72
|
+
'display_restricted',
|
|
73
|
+
])
|
|
74
|
+
.nullable(),
|
|
75
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** Generate at runtime/build from the same validators used at both ends of the wire. */
|
|
2
|
+
export declare function createOpenApiDocument(): {
|
|
3
|
+
openapi: string;
|
|
4
|
+
info: {
|
|
5
|
+
title: string;
|
|
6
|
+
version: string;
|
|
7
|
+
description: string;
|
|
8
|
+
};
|
|
9
|
+
servers: {
|
|
10
|
+
url: string;
|
|
11
|
+
}[];
|
|
12
|
+
paths: Record<string, Record<string, unknown>>;
|
|
13
|
+
components: {
|
|
14
|
+
securitySchemes: {
|
|
15
|
+
bearerAuth: {
|
|
16
|
+
type: string;
|
|
17
|
+
scheme: string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
schemas: Record<string, unknown>;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { operations } from './operations.js';
|
|
3
|
+
import { ProblemSchema } from './schemas.js';
|
|
4
|
+
import { routes } from './routes.js';
|
|
5
|
+
/** Generate at runtime/build from the same validators used at both ends of the wire. */
|
|
6
|
+
export function createOpenApiDocument() {
|
|
7
|
+
const paths = {};
|
|
8
|
+
const schemas = {};
|
|
9
|
+
const register = (name, schema) => {
|
|
10
|
+
// Zod's recursive JSON values use local refs. Once embedded in OpenAPI they must
|
|
11
|
+
// point at this component, not at an unrelated $defs at the document root.
|
|
12
|
+
const rewrite = (value) => {
|
|
13
|
+
if (Array.isArray(value))
|
|
14
|
+
return value.map(rewrite);
|
|
15
|
+
if (value && typeof value === 'object')
|
|
16
|
+
return Object.fromEntries(Object.entries(value)
|
|
17
|
+
.filter(([key]) => key !== '$schema')
|
|
18
|
+
.map(([key, child]) => [
|
|
19
|
+
key,
|
|
20
|
+
key === '$ref' && typeof child === 'string' && child.startsWith('#')
|
|
21
|
+
? `#/components/schemas/${name}${child.slice(1)}`
|
|
22
|
+
: rewrite(child),
|
|
23
|
+
]));
|
|
24
|
+
return value;
|
|
25
|
+
};
|
|
26
|
+
schemas[name] = rewrite(schema);
|
|
27
|
+
return { $ref: `#/components/schemas/${name}` };
|
|
28
|
+
};
|
|
29
|
+
const problem = register('Problem', z.toJSONSchema(ProblemSchema));
|
|
30
|
+
for (const route of routes) {
|
|
31
|
+
const contract = operations[route.operation];
|
|
32
|
+
const input = z.toJSONSchema(contract.input, { io: 'input' });
|
|
33
|
+
const pathNames = [...route.path.matchAll(/\{([^}]+)\}/g)].map((match) => match[1]);
|
|
34
|
+
const properties = (input.properties ?? {});
|
|
35
|
+
const parameters = pathNames.map((name) => ({
|
|
36
|
+
name,
|
|
37
|
+
in: 'path',
|
|
38
|
+
required: true,
|
|
39
|
+
schema: properties[name],
|
|
40
|
+
}));
|
|
41
|
+
let requestBody;
|
|
42
|
+
if (route.method === 'GET') {
|
|
43
|
+
for (const [name, schema] of Object.entries(properties)) {
|
|
44
|
+
if (!pathNames.includes(name))
|
|
45
|
+
parameters.push({
|
|
46
|
+
name,
|
|
47
|
+
in: 'query',
|
|
48
|
+
required: input.required?.includes(name) ?? false,
|
|
49
|
+
schema,
|
|
50
|
+
...(schema && typeof schema === 'object' && 'type' in schema && schema.type === 'array'
|
|
51
|
+
? { style: 'form', explode: false }
|
|
52
|
+
: {}),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
if (!('readOnly' in route && route.readOnly))
|
|
58
|
+
parameters.push({
|
|
59
|
+
name: 'Idempotency-Key',
|
|
60
|
+
in: 'header',
|
|
61
|
+
required: true,
|
|
62
|
+
schema: { type: 'string', minLength: 1, maxLength: 200 },
|
|
63
|
+
});
|
|
64
|
+
// Path identity is never accepted a second time in JSON, where it could disagree.
|
|
65
|
+
const bodyProperties = Object.fromEntries(Object.entries(properties).filter(([name]) => !pathNames.includes(name)));
|
|
66
|
+
const body = register(`${route.operation}Request`, {
|
|
67
|
+
...input,
|
|
68
|
+
properties: bodyProperties,
|
|
69
|
+
required: input.required?.filter((name) => !pathNames.includes(name)),
|
|
70
|
+
});
|
|
71
|
+
requestBody = { required: true, content: { 'application/json': { schema: body } } };
|
|
72
|
+
}
|
|
73
|
+
const status = 'status' in route ? route.status : 200;
|
|
74
|
+
paths[route.path] ??= {};
|
|
75
|
+
paths[route.path][route.method.toLowerCase()] = {
|
|
76
|
+
operationId: route.operation,
|
|
77
|
+
summary: route.summary,
|
|
78
|
+
security: [{ bearerAuth: [] }],
|
|
79
|
+
parameters,
|
|
80
|
+
...('scope' in route ? { 'x-required-scope': route.scope } : {}),
|
|
81
|
+
...(requestBody ? { requestBody } : {}),
|
|
82
|
+
responses: {
|
|
83
|
+
[status]: {
|
|
84
|
+
description: 'Validated response',
|
|
85
|
+
content: {
|
|
86
|
+
'application/json': {
|
|
87
|
+
schema: register(`${route.operation}Response`, z.toJSONSchema(contract.output)),
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
default: {
|
|
92
|
+
description: 'Problem details; code is stable across message changes.',
|
|
93
|
+
content: { 'application/problem+json': { schema: problem } },
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
openapi: '3.1.1',
|
|
100
|
+
info: {
|
|
101
|
+
title: 'Listings Intelligence API',
|
|
102
|
+
version: '1.0.0',
|
|
103
|
+
description: 'Current classified catalog and persisted, authorized opportunity selections. Creating a plan does not send a message.',
|
|
104
|
+
},
|
|
105
|
+
servers: [{ url: '/' }],
|
|
106
|
+
paths,
|
|
107
|
+
components: { securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer' } }, schemas },
|
|
108
|
+
};
|
|
109
|
+
}
|