@propertysimple/client 0.1.0 → 0.2.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 +27 -73
- package/README.md +200 -57
- package/client/src/index.d.ts +257 -2422
- package/client/src/index.js +6 -69
- package/contracts/src/index.d.ts +0 -7
- package/contracts/src/index.js +0 -7
- package/contracts/src/openapi.js +1 -1
- package/contracts/src/operations.d.ts +950 -4678
- package/contracts/src/operations.js +6 -36
- package/contracts/src/opportunity-events.d.ts +2 -1
- package/contracts/src/opportunity-events.js +6 -1
- package/contracts/src/property-features.d.ts +2 -2
- package/contracts/src/property-features.js +62 -19
- package/contracts/src/routes.d.ts +28 -305
- package/contracts/src/routes.js +82 -354
- package/contracts/src/schemas.d.ts +1114 -1355
- package/contracts/src/schemas.js +45 -223
- package/contracts/src/search.d.ts +1366 -253
- package/contracts/src/search.js +117 -2
- package/openapi.json +19288 -38596
- package/package.json +1 -9
- package/client/src/directory-sync.d.ts +0 -123
- package/client/src/directory-sync.js +0 -404
- package/client/src/managed-sync.d.ts +0 -129
- package/client/src/managed-sync.js +0 -410
- package/contracts/src/directory-execution.d.ts +0 -320
- package/contracts/src/directory-execution.js +0 -122
- package/contracts/src/directory-identity.d.ts +0 -1255
- package/contracts/src/directory-identity.js +0 -129
- package/contracts/src/directory-sales.d.ts +0 -333
- package/contracts/src/directory-sales.js +0 -102
- package/contracts/src/directory.d.ts +0 -1088
- package/contracts/src/directory.js +0 -179
- package/contracts/src/intelligence.d.ts +0 -366
- package/contracts/src/intelligence.js +0 -157
- package/contracts/src/professional-intelligence.d.ts +0 -256
- package/contracts/src/professional-intelligence.js +0 -90
- package/contracts/src/publication.d.ts +0 -44
- package/contracts/src/publication.js +0 -42
- package/contracts/src/smart-feed.d.ts +0 -3731
- package/contracts/src/smart-feed.js +0 -109
package/contracts/src/search.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { ListingSchema } from './schemas.js';
|
|
2
|
+
import { ListingSchema, Money } from './schemas.js';
|
|
3
3
|
import { PropertyFeatureRegistrySchema } from './property-features.js';
|
|
4
4
|
import { CatalogInventoryStatusSchema } from './catalog-inventory.js';
|
|
5
5
|
const Id = z.string().min(1).max(256);
|
|
@@ -18,6 +18,11 @@ const Decimal = z
|
|
|
18
18
|
.string()
|
|
19
19
|
.regex(/^\d+(?:\.\d{1,2})?$/)
|
|
20
20
|
.max(20);
|
|
21
|
+
const WholeRange = (max) => z.strictObject({
|
|
22
|
+
min: z.number().int().min(0).max(max).optional(),
|
|
23
|
+
max: z.number().int().min(0).max(max).optional(),
|
|
24
|
+
});
|
|
25
|
+
const PropertyTypes = z.array(z.string().min(1).max(100)).min(1).max(20);
|
|
21
26
|
export const SearchBounds = z
|
|
22
27
|
.tuple([
|
|
23
28
|
z.number().min(-180).max(180),
|
|
@@ -42,6 +47,24 @@ export const SearchFilters = z.strictObject({
|
|
|
42
47
|
region: z.string().min(1).max(100).optional(),
|
|
43
48
|
postal_code: z.string().min(1).max(30).optional(),
|
|
44
49
|
property_type: z.string().min(1).max(100).optional(),
|
|
50
|
+
/** Any of these property types. */
|
|
51
|
+
property_types: PropertyTypes.optional(),
|
|
52
|
+
/** None of these property types; a listing without a type stays eligible. */
|
|
53
|
+
exclude_property_types: PropertyTypes.optional(),
|
|
54
|
+
/** CountyOrParish without a trailing " County" or " Parish". Names repeat across
|
|
55
|
+
* states, so pair it with `region`. */
|
|
56
|
+
county: z.string().min(1).max(200).optional(),
|
|
57
|
+
/** Whole days since the source listing date (listing contract date, else on-market
|
|
58
|
+
* date) at request time. ListHub sends no DaysOnMarket of its own. */
|
|
59
|
+
days_on_market: WholeRange(36500).optional(),
|
|
60
|
+
year_built: WholeRange(3000).optional(),
|
|
61
|
+
/** Lot area, compared in `lot_size_unit`. */
|
|
62
|
+
lot_size: Range.optional(),
|
|
63
|
+
lot_size_unit: z.enum(['acres', 'sqft']).optional(),
|
|
64
|
+
has_photos: z.boolean().optional(),
|
|
65
|
+
/** A branded or unbranded virtual tour URL. false matches listings whose source
|
|
66
|
+
* explicitly has none. */
|
|
67
|
+
has_virtual_tour: z.boolean().optional(),
|
|
45
68
|
must: z.array(SearchFeaturePredicate).max(30).default([]),
|
|
46
69
|
any: z.array(SearchFeaturePredicate).max(30).default([]),
|
|
47
70
|
/** Exclude known matches. Unknown evidence stays eligible; use must eq:false for confirmed absence. */
|
|
@@ -61,6 +84,17 @@ export const SearchRequest = z
|
|
|
61
84
|
km: z.number().positive().max(20000),
|
|
62
85
|
})
|
|
63
86
|
.optional(),
|
|
87
|
+
/** A neighbourhood or drawn area: vertices in order, closed or not. Edges are
|
|
88
|
+
* straight lines on the latitude/longitude plane, as in GeoJSON and PostGIS
|
|
89
|
+
* geometry; edges cannot cross and the polygon cannot cross the antimeridian. */
|
|
90
|
+
polygon: z
|
|
91
|
+
.array(z.strictObject({
|
|
92
|
+
latitude: z.number().min(-90).max(90),
|
|
93
|
+
longitude: z.number().min(-180).max(180),
|
|
94
|
+
}))
|
|
95
|
+
.min(3)
|
|
96
|
+
.max(500)
|
|
97
|
+
.optional(),
|
|
64
98
|
sort: z.enum(['relevance', 'price_asc', 'price_desc', 'newest']).default('relevance'),
|
|
65
99
|
limit: z.number().int().min(1).max(100).default(24),
|
|
66
100
|
cursor: z.string().min(1).max(4096).optional(),
|
|
@@ -69,6 +103,7 @@ export const SearchRequest = z
|
|
|
69
103
|
facet_limit: z.number().int().min(1).max(100).optional(),
|
|
70
104
|
})
|
|
71
105
|
.refine((v) => !(v.bbox && v.radius), 'Choose bbox or radius, not both.')
|
|
106
|
+
.refine((v) => !(v.polygon && (v.bbox || v.radius)), 'Choose one of bbox, radius or polygon.')
|
|
72
107
|
.refine((v) => v.facet_limit === undefined || v.facet_fields !== undefined, 'Facet limit requires facet fields.');
|
|
73
108
|
export const SearchMeta = z.strictObject({
|
|
74
109
|
backend: z.enum(['typesense', 'postgres']),
|
|
@@ -121,10 +156,13 @@ export const SearchResponse = z.strictObject({
|
|
|
121
156
|
has_more: z.boolean(),
|
|
122
157
|
meta: SearchMeta,
|
|
123
158
|
});
|
|
159
|
+
/** Values per field, at most 1,000: enough for every city, county or postal code of
|
|
160
|
+
* one state (a portal's place pages and sitemaps need all of them, not the top 100). */
|
|
161
|
+
export const SEARCH_FACET_LIMIT = 1000;
|
|
124
162
|
export const SearchFacetRequest = z.strictObject({
|
|
125
163
|
query: SearchRequest,
|
|
126
164
|
fields: z.array(z.string().min(1).max(80)).min(1).max(12),
|
|
127
|
-
limit: z.number().int().min(1).max(
|
|
165
|
+
limit: z.number().int().min(1).max(SEARCH_FACET_LIMIT).default(20),
|
|
128
166
|
});
|
|
129
167
|
export const SearchFacetResponse = z.strictObject({
|
|
130
168
|
facets: SearchFacetValues,
|
|
@@ -177,6 +215,10 @@ export const SearchSuggestRequest = z.strictObject({
|
|
|
177
215
|
dataset_id: Id.optional(),
|
|
178
216
|
q: z.string().min(1).max(200),
|
|
179
217
|
limit: z.coerce.number().int().min(1).max(20).default(8),
|
|
218
|
+
/** Also suggest listings by street address (`kind: listing`, `value` is the listing
|
|
219
|
+
* ID) when the text starts with a house number. Only listings whose address may be
|
|
220
|
+
* shown. Off by default: older consumers treat any non-place value as search text. */
|
|
221
|
+
include_listings: z.union([z.boolean(), z.enum(['true', 'false'])]).optional(),
|
|
180
222
|
});
|
|
181
223
|
export const SearchSuggestResponse = z.strictObject({
|
|
182
224
|
suggestions: z.array(z.strictObject({
|
|
@@ -213,10 +255,83 @@ export const SearchInterpretResponse = z.strictObject({
|
|
|
213
255
|
method: z.literal('bounded_rules'),
|
|
214
256
|
});
|
|
215
257
|
export const SearchFeaturesResponse = PropertyFeatureRegistrySchema;
|
|
258
|
+
export const SearchStatsRequest = z.strictObject({ query: SearchRequest });
|
|
259
|
+
/** A compact walk of every listing a query matches, for sitemaps and link indexes:
|
|
260
|
+
* `after` is the previous page's `next`. Ordered by listing position, never by the
|
|
261
|
+
* query's sort; the query's cursor, sort and limit do not apply. */
|
|
262
|
+
export const SEARCH_INDEX_LIMIT = 5000;
|
|
263
|
+
export const SearchIndexRequest = z.strictObject({
|
|
264
|
+
query: SearchRequest,
|
|
265
|
+
after: z
|
|
266
|
+
.string()
|
|
267
|
+
.regex(/^p1\.\d{1,20}$/)
|
|
268
|
+
.optional(),
|
|
269
|
+
limit: z.number().int().min(1).max(SEARCH_INDEX_LIMIT).default(1000),
|
|
270
|
+
});
|
|
271
|
+
export const SearchIndexEntry = z.strictObject({
|
|
272
|
+
id: Id,
|
|
273
|
+
/** Null where the source withholds the address, as listing-v1 withholds it. */
|
|
274
|
+
street: z.string().nullable(),
|
|
275
|
+
city: z.string().nullable(),
|
|
276
|
+
region: z.string().nullable(),
|
|
277
|
+
postal_code: z.string().nullable(),
|
|
278
|
+
price: Money.nullable(),
|
|
279
|
+
property_type: z.string().nullable(),
|
|
280
|
+
photo_count: z.number().int().nonnegative(),
|
|
281
|
+
/** The source listing date, as the days_on_market filter reads it. */
|
|
282
|
+
listed_at: z.iso.datetime({ offset: true }).nullable(),
|
|
283
|
+
/** The map pin, as searches place it: null where the address is withheld, the
|
|
284
|
+
* listing has no usable point, or a geocoded point has expired. */
|
|
285
|
+
latitude: z.number().min(-90).max(90).nullable(),
|
|
286
|
+
longitude: z.number().min(-180).max(180).nullable(),
|
|
287
|
+
});
|
|
288
|
+
export const SearchIndexResponse = z.strictObject({
|
|
289
|
+
data: z.array(SearchIndexEntry),
|
|
290
|
+
next: z.string().nullable(),
|
|
291
|
+
has_more: z.boolean(),
|
|
292
|
+
meta: SearchMeta,
|
|
293
|
+
});
|
|
294
|
+
const PriceStatistic = (unit) => z.strictObject({
|
|
295
|
+
currency: z.string().regex(/^[A-Z]{3}$/),
|
|
296
|
+
...(unit === 'price'
|
|
297
|
+
? { period: z.string().nullable() }
|
|
298
|
+
: { area_unit: z.enum(['sqft', 'sqm']) }),
|
|
299
|
+
/** Matching listings with a value; the statistics describe these. */
|
|
300
|
+
listings: z.number().int().positive(),
|
|
301
|
+
min: Decimal,
|
|
302
|
+
max: Decimal,
|
|
303
|
+
/** Mean, rounded to the cent. */
|
|
304
|
+
avg: Decimal,
|
|
305
|
+
});
|
|
306
|
+
export const SearchStatsResponse = z.strictObject({
|
|
307
|
+
stats: z.strictObject({
|
|
308
|
+
/** Matching listings, as meta.total. */
|
|
309
|
+
count: z.number().int().nonnegative(),
|
|
310
|
+
/** Null unless the query fixes currency and transaction_type (and price_period
|
|
311
|
+
* for rentals), or when no matching listing has a price. */
|
|
312
|
+
price: PriceStatistic('price').nullable(),
|
|
313
|
+
/** The mean of each listing's price / living area, not a ratio of averages. Also
|
|
314
|
+
* needs living_area_unit. */
|
|
315
|
+
price_per_area: PriceStatistic('area').nullable(),
|
|
316
|
+
/** Whole days since the listing date, as the days_on_market filter counts them.
|
|
317
|
+
* Listings dated in the future are left out. */
|
|
318
|
+
days_on_market: z
|
|
319
|
+
.strictObject({
|
|
320
|
+
listings: z.number().int().positive(),
|
|
321
|
+
min: z.number().int().nonnegative(),
|
|
322
|
+
max: z.number().int().nonnegative(),
|
|
323
|
+
avg: z.number().nonnegative(),
|
|
324
|
+
})
|
|
325
|
+
.nullable(),
|
|
326
|
+
}),
|
|
327
|
+
meta: SearchMeta,
|
|
328
|
+
});
|
|
216
329
|
export const searchOperations = {
|
|
217
330
|
searchFeatures: { input: z.strictObject({}), output: SearchFeaturesResponse },
|
|
218
331
|
searchListings: { input: SearchRequest, output: SearchResponse },
|
|
219
332
|
searchFacets: { input: SearchFacetRequest, output: SearchFacetResponse },
|
|
333
|
+
searchStats: { input: SearchStatsRequest, output: SearchStatsResponse },
|
|
334
|
+
searchIndex: { input: SearchIndexRequest, output: SearchIndexResponse },
|
|
220
335
|
searchMap: { input: SearchMapRequest, output: SearchMapResponse },
|
|
221
336
|
searchSuggest: { input: SearchSuggestRequest, output: SearchSuggestResponse },
|
|
222
337
|
searchInterpret: { input: SearchInterpretRequest, output: SearchInterpretResponse },
|