@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.
Files changed (41) hide show
  1. package/API.md +27 -73
  2. package/README.md +200 -57
  3. package/client/src/index.d.ts +257 -2422
  4. package/client/src/index.js +6 -69
  5. package/contracts/src/index.d.ts +0 -7
  6. package/contracts/src/index.js +0 -7
  7. package/contracts/src/openapi.js +1 -1
  8. package/contracts/src/operations.d.ts +950 -4678
  9. package/contracts/src/operations.js +6 -36
  10. package/contracts/src/opportunity-events.d.ts +2 -1
  11. package/contracts/src/opportunity-events.js +6 -1
  12. package/contracts/src/property-features.d.ts +2 -2
  13. package/contracts/src/property-features.js +62 -19
  14. package/contracts/src/routes.d.ts +28 -305
  15. package/contracts/src/routes.js +82 -354
  16. package/contracts/src/schemas.d.ts +1114 -1355
  17. package/contracts/src/schemas.js +45 -223
  18. package/contracts/src/search.d.ts +1366 -253
  19. package/contracts/src/search.js +117 -2
  20. package/openapi.json +19288 -38596
  21. package/package.json +1 -9
  22. package/client/src/directory-sync.d.ts +0 -123
  23. package/client/src/directory-sync.js +0 -404
  24. package/client/src/managed-sync.d.ts +0 -129
  25. package/client/src/managed-sync.js +0 -410
  26. package/contracts/src/directory-execution.d.ts +0 -320
  27. package/contracts/src/directory-execution.js +0 -122
  28. package/contracts/src/directory-identity.d.ts +0 -1255
  29. package/contracts/src/directory-identity.js +0 -129
  30. package/contracts/src/directory-sales.d.ts +0 -333
  31. package/contracts/src/directory-sales.js +0 -102
  32. package/contracts/src/directory.d.ts +0 -1088
  33. package/contracts/src/directory.js +0 -179
  34. package/contracts/src/intelligence.d.ts +0 -366
  35. package/contracts/src/intelligence.js +0 -157
  36. package/contracts/src/professional-intelligence.d.ts +0 -256
  37. package/contracts/src/professional-intelligence.js +0 -90
  38. package/contracts/src/publication.d.ts +0 -44
  39. package/contracts/src/publication.js +0 -42
  40. package/contracts/src/smart-feed.d.ts +0 -3731
  41. package/contracts/src/smart-feed.js +0 -109
@@ -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(100).default(20),
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 },