@voyantjs/products-contracts 0.90.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/README.md +31 -0
- package/dist/content-shape.d.ts +217 -0
- package/dist/content-shape.d.ts.map +1 -0
- package/dist/content-shape.js +140 -0
- package/dist/content-shape.test.d.ts +2 -0
- package/dist/content-shape.test.d.ts.map +1 -0
- package/dist/content-shape.test.js +34 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/package.json +51 -0
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# @voyantjs/products-contracts
|
|
2
|
+
|
|
3
|
+
Pure product content contracts for adapter implementers and external
|
|
4
|
+
consumers that need to validate `products/v1` rich content payloads
|
|
5
|
+
without installing the full products runtime package.
|
|
6
|
+
|
|
7
|
+
Use this package for `PRODUCTS_CONTENT_SCHEMA_VERSION`,
|
|
8
|
+
`productContentSchema`, `ProductContent`, nested content types, and
|
|
9
|
+
`validateProductContent`. Use `@voyantjs/products` when you also need
|
|
10
|
+
Drizzle schema, routes, services, booking integration, catalog projection, or
|
|
11
|
+
runtime content resolution (including the `mergeOverlaysIntoProductContent`
|
|
12
|
+
overlay composition).
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm add @voyantjs/products-contracts zod
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import {
|
|
24
|
+
PRODUCTS_CONTENT_SCHEMA_VERSION,
|
|
25
|
+
productContentSchema,
|
|
26
|
+
type ProductContent,
|
|
27
|
+
} from "@voyantjs/products-contracts"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Existing `@voyantjs/products/content-shape` imports remain available for
|
|
31
|
+
applications that already depend on the full runtime package.
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Products content shape — the rich detail-page content shape returned
|
|
3
|
+
* by `getContent` and stored in `products_sourced_content.payload`.
|
|
4
|
+
*
|
|
5
|
+
* Schema versions are managed by this module: the constant
|
|
6
|
+
* `PRODUCTS_CONTENT_SCHEMA_VERSION` stamps every cache write; reads
|
|
7
|
+
* skip rows with an unrecognized version (treated as cache miss). Bump
|
|
8
|
+
* the version when the shape changes; old cache rows are then evicted
|
|
9
|
+
* by a single `DELETE WHERE content_schema_version != current`.
|
|
10
|
+
*
|
|
11
|
+
* This module is the pure content contract: schemas, types, version, and
|
|
12
|
+
* the validator. The `mergeOverlaysIntoProductContent` overlay
|
|
13
|
+
* composition stays in the `@voyantjs/products` runtime package.
|
|
14
|
+
*
|
|
15
|
+
* See `docs/architecture/catalog-sourced-content.md` §3.2, §3.5.4, §3.6.
|
|
16
|
+
*/
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
/**
|
|
19
|
+
* The current content-schema version. Stamped on every cache write.
|
|
20
|
+
* Bump when the `productContentSchema` shape changes incompatibly.
|
|
21
|
+
*/
|
|
22
|
+
export declare const PRODUCTS_CONTENT_SCHEMA_VERSION = "products/v1";
|
|
23
|
+
/**
|
|
24
|
+
* Top-level product summary fields. Maps loosely to the owned `products`
|
|
25
|
+
* table — the read service synthesizes from indexed projection + overlay
|
|
26
|
+
* for thin adapters, or stores adapter-served data for rich ones.
|
|
27
|
+
*/
|
|
28
|
+
export declare const productSummarySchema: z.ZodObject<{
|
|
29
|
+
id: z.ZodString;
|
|
30
|
+
name: z.ZodString;
|
|
31
|
+
status: z.ZodOptional<z.ZodString>;
|
|
32
|
+
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
33
|
+
inclusions_html: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
34
|
+
exclusions_html: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
35
|
+
terms_html: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
36
|
+
contract_template_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
37
|
+
contractTemplateId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
38
|
+
highlights: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
39
|
+
hero_image_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
40
|
+
duration_days: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
41
|
+
start_date: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
42
|
+
end_date: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
43
|
+
sell_currency: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
44
|
+
supplier: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
45
|
+
country: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
46
|
+
departure_city: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
47
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
48
|
+
}, z.core.$strip>;
|
|
49
|
+
export declare const productMediaItemSchema: z.ZodObject<{
|
|
50
|
+
url: z.ZodString;
|
|
51
|
+
type: z.ZodDefault<z.ZodEnum<{
|
|
52
|
+
image: "image";
|
|
53
|
+
video: "video";
|
|
54
|
+
document: "document";
|
|
55
|
+
}>>;
|
|
56
|
+
caption: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
57
|
+
alt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
58
|
+
}, z.core.$strip>;
|
|
59
|
+
export declare const productOptionUnitSchema: z.ZodObject<{
|
|
60
|
+
id: z.ZodString;
|
|
61
|
+
type: z.ZodString;
|
|
62
|
+
label: z.ZodOptional<z.ZodString>;
|
|
63
|
+
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
64
|
+
capacity: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
65
|
+
}, z.core.$strip>;
|
|
66
|
+
export declare const productOptionSchema: z.ZodObject<{
|
|
67
|
+
id: z.ZodString;
|
|
68
|
+
name: z.ZodString;
|
|
69
|
+
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
70
|
+
units: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
71
|
+
id: z.ZodString;
|
|
72
|
+
type: z.ZodString;
|
|
73
|
+
label: z.ZodOptional<z.ZodString>;
|
|
74
|
+
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
75
|
+
capacity: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
76
|
+
}, z.core.$strip>>>>;
|
|
77
|
+
inclusions: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString>>>;
|
|
78
|
+
}, z.core.$strip>;
|
|
79
|
+
export declare const productDaySchema: z.ZodObject<{
|
|
80
|
+
day_number: z.ZodNumber;
|
|
81
|
+
title: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
82
|
+
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
83
|
+
location: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
84
|
+
hero_image_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
85
|
+
services: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString>>>;
|
|
86
|
+
}, z.core.$strip>;
|
|
87
|
+
export declare const productPolicySchema: z.ZodObject<{
|
|
88
|
+
kind: z.ZodEnum<{
|
|
89
|
+
cancellation: "cancellation";
|
|
90
|
+
payment: "payment";
|
|
91
|
+
supplier_notes: "supplier_notes";
|
|
92
|
+
requirements: "requirements";
|
|
93
|
+
}>;
|
|
94
|
+
body: z.ZodString;
|
|
95
|
+
rules: z.ZodOptional<z.ZodUnknown>;
|
|
96
|
+
}, z.core.$strip>;
|
|
97
|
+
/**
|
|
98
|
+
* A single bookable departure / time slot — the "when" surface of the
|
|
99
|
+
* product. ISO 8601 timestamps for `starts_at` / `ends_at` so locale
|
|
100
|
+
* formatting happens at render time, never in the cache.
|
|
101
|
+
*
|
|
102
|
+
* Owned products derive these from `availability_slots`; sourced
|
|
103
|
+
* adapters return them via `getContent`. Empty array = "always-on"
|
|
104
|
+
* product (e.g. an evergreen transfer service) or one whose schedule
|
|
105
|
+
* is on-request.
|
|
106
|
+
*/
|
|
107
|
+
export declare const productDepartureSchema: z.ZodObject<{
|
|
108
|
+
id: z.ZodString;
|
|
109
|
+
starts_at: z.ZodString;
|
|
110
|
+
ends_at: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
111
|
+
status: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
112
|
+
capacity: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
113
|
+
remaining: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
114
|
+
lowest_price_cents: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
115
|
+
currency: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
116
|
+
note: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
117
|
+
}, z.core.$strip>;
|
|
118
|
+
/**
|
|
119
|
+
* The product content payload. Cache writes validate against this
|
|
120
|
+
* schema; cache reads skip rows that don't validate (treated as cache
|
|
121
|
+
* miss to surface adapter integration bugs without corrupting reads).
|
|
122
|
+
*/
|
|
123
|
+
export declare const productContentSchema: z.ZodObject<{
|
|
124
|
+
product: z.ZodObject<{
|
|
125
|
+
id: z.ZodString;
|
|
126
|
+
name: z.ZodString;
|
|
127
|
+
status: z.ZodOptional<z.ZodString>;
|
|
128
|
+
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
129
|
+
inclusions_html: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
130
|
+
exclusions_html: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
131
|
+
terms_html: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
132
|
+
contract_template_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
133
|
+
contractTemplateId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
134
|
+
highlights: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
135
|
+
hero_image_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
136
|
+
duration_days: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
137
|
+
start_date: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
138
|
+
end_date: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
139
|
+
sell_currency: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
140
|
+
supplier: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
141
|
+
country: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
142
|
+
departure_city: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
143
|
+
tags: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
144
|
+
}, z.core.$strip>;
|
|
145
|
+
options: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
146
|
+
id: z.ZodString;
|
|
147
|
+
name: z.ZodString;
|
|
148
|
+
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
149
|
+
units: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
150
|
+
id: z.ZodString;
|
|
151
|
+
type: z.ZodString;
|
|
152
|
+
label: z.ZodOptional<z.ZodString>;
|
|
153
|
+
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
154
|
+
capacity: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
155
|
+
}, z.core.$strip>>>>;
|
|
156
|
+
inclusions: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString>>>;
|
|
157
|
+
}, z.core.$strip>>>;
|
|
158
|
+
days: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
159
|
+
day_number: z.ZodNumber;
|
|
160
|
+
title: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
161
|
+
description: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
162
|
+
location: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
163
|
+
hero_image_url: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
164
|
+
services: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString>>>;
|
|
165
|
+
}, z.core.$strip>>>;
|
|
166
|
+
media: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
167
|
+
url: z.ZodString;
|
|
168
|
+
type: z.ZodDefault<z.ZodEnum<{
|
|
169
|
+
image: "image";
|
|
170
|
+
video: "video";
|
|
171
|
+
document: "document";
|
|
172
|
+
}>>;
|
|
173
|
+
caption: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
174
|
+
alt: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
175
|
+
}, z.core.$strip>>>;
|
|
176
|
+
policies: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
177
|
+
kind: z.ZodEnum<{
|
|
178
|
+
cancellation: "cancellation";
|
|
179
|
+
payment: "payment";
|
|
180
|
+
supplier_notes: "supplier_notes";
|
|
181
|
+
requirements: "requirements";
|
|
182
|
+
}>;
|
|
183
|
+
body: z.ZodString;
|
|
184
|
+
rules: z.ZodOptional<z.ZodUnknown>;
|
|
185
|
+
}, z.core.$strip>>>;
|
|
186
|
+
departures: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
187
|
+
id: z.ZodString;
|
|
188
|
+
starts_at: z.ZodString;
|
|
189
|
+
ends_at: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
190
|
+
status: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
191
|
+
capacity: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
192
|
+
remaining: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
193
|
+
lowest_price_cents: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
194
|
+
currency: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
195
|
+
note: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
196
|
+
}, z.core.$strip>>>;
|
|
197
|
+
}, z.core.$strip>;
|
|
198
|
+
export type ProductContent = z.infer<typeof productContentSchema>;
|
|
199
|
+
export type ProductSummary = z.infer<typeof productSummarySchema>;
|
|
200
|
+
export type ProductMediaItem = z.infer<typeof productMediaItemSchema>;
|
|
201
|
+
export type ProductOption = z.infer<typeof productOptionSchema>;
|
|
202
|
+
export type ProductDeparture = z.infer<typeof productDepartureSchema>;
|
|
203
|
+
export type ProductDay = z.infer<typeof productDaySchema>;
|
|
204
|
+
export type ProductPolicy = z.infer<typeof productPolicySchema>;
|
|
205
|
+
/**
|
|
206
|
+
* Validate a `ProductContent` payload. Returns the parsed result on
|
|
207
|
+
* success or a structured failure on rejection. Used by the cache write
|
|
208
|
+
* path and by `mergeOverlaysIntoProductContent` to gate overlay merges.
|
|
209
|
+
*/
|
|
210
|
+
export declare function validateProductContent(payload: unknown): {
|
|
211
|
+
valid: true;
|
|
212
|
+
content: ProductContent;
|
|
213
|
+
} | {
|
|
214
|
+
valid: false;
|
|
215
|
+
reason: string;
|
|
216
|
+
};
|
|
217
|
+
//# sourceMappingURL=content-shape.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-shape.d.ts","sourceRoot":"","sources":["../src/content-shape.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAA;AAEvB;;;GAGG;AACH,eAAO,MAAM,+BAA+B,gBAAgB,CAAA;AAE5D;;;;GAIG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;iBAoB/B,CAAA;AAEF,eAAO,MAAM,sBAAsB;;;;;;;;;iBAKjC,CAAA;AAEF,eAAO,MAAM,uBAAuB;;;;;;iBAMlC,CAAA;AAEF,eAAO,MAAM,mBAAmB;;;;;;;;;;;;iBAM9B,CAAA;AAEF,eAAO,MAAM,gBAAgB;;;;;;;iBAQ3B,CAAA;AAEF,eAAO,MAAM,mBAAmB;;;;;;;;;iBAK9B,CAAA;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB;;;;;;;;;;iBAejC,CAAA;AAEF;;;;GAIG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAO/B,CAAA;AAEF,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAA;AACjE,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAA;AACjE,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAA;AACrE,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAC/D,MAAM,MAAM,gBAAgB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAA;AACrE,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAA;AACzD,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAA;AAE/D;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,OAAO,GACf;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,cAAc,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAY7E"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Products content shape — the rich detail-page content shape returned
|
|
3
|
+
* by `getContent` and stored in `products_sourced_content.payload`.
|
|
4
|
+
*
|
|
5
|
+
* Schema versions are managed by this module: the constant
|
|
6
|
+
* `PRODUCTS_CONTENT_SCHEMA_VERSION` stamps every cache write; reads
|
|
7
|
+
* skip rows with an unrecognized version (treated as cache miss). Bump
|
|
8
|
+
* the version when the shape changes; old cache rows are then evicted
|
|
9
|
+
* by a single `DELETE WHERE content_schema_version != current`.
|
|
10
|
+
*
|
|
11
|
+
* This module is the pure content contract: schemas, types, version, and
|
|
12
|
+
* the validator. The `mergeOverlaysIntoProductContent` overlay
|
|
13
|
+
* composition stays in the `@voyantjs/products` runtime package.
|
|
14
|
+
*
|
|
15
|
+
* See `docs/architecture/catalog-sourced-content.md` §3.2, §3.5.4, §3.6.
|
|
16
|
+
*/
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
/**
|
|
19
|
+
* The current content-schema version. Stamped on every cache write.
|
|
20
|
+
* Bump when the `productContentSchema` shape changes incompatibly.
|
|
21
|
+
*/
|
|
22
|
+
export const PRODUCTS_CONTENT_SCHEMA_VERSION = "products/v1";
|
|
23
|
+
/**
|
|
24
|
+
* Top-level product summary fields. Maps loosely to the owned `products`
|
|
25
|
+
* table — the read service synthesizes from indexed projection + overlay
|
|
26
|
+
* for thin adapters, or stores adapter-served data for rich ones.
|
|
27
|
+
*/
|
|
28
|
+
export const productSummarySchema = z.object({
|
|
29
|
+
id: z.string(),
|
|
30
|
+
name: z.string(),
|
|
31
|
+
status: z.string().optional(),
|
|
32
|
+
description: z.string().nullable().optional(),
|
|
33
|
+
inclusions_html: z.string().nullable().optional(),
|
|
34
|
+
exclusions_html: z.string().nullable().optional(),
|
|
35
|
+
terms_html: z.string().nullable().optional(),
|
|
36
|
+
contract_template_id: z.string().nullable().optional(),
|
|
37
|
+
contractTemplateId: z.string().nullable().optional(),
|
|
38
|
+
highlights: z.array(z.string()).optional(),
|
|
39
|
+
hero_image_url: z.string().nullable().optional(),
|
|
40
|
+
duration_days: z.number().int().nonnegative().nullable().optional(),
|
|
41
|
+
start_date: z.string().nullable().optional(),
|
|
42
|
+
end_date: z.string().nullable().optional(),
|
|
43
|
+
sell_currency: z.string().nullable().optional(),
|
|
44
|
+
supplier: z.string().nullable().optional(),
|
|
45
|
+
country: z.string().nullable().optional(),
|
|
46
|
+
departure_city: z.string().nullable().optional(),
|
|
47
|
+
tags: z.array(z.string()).optional(),
|
|
48
|
+
});
|
|
49
|
+
export const productMediaItemSchema = z.object({
|
|
50
|
+
url: z.string(),
|
|
51
|
+
type: z.enum(["image", "video", "document"]).default("image"),
|
|
52
|
+
caption: z.string().nullable().optional(),
|
|
53
|
+
alt: z.string().nullable().optional(),
|
|
54
|
+
});
|
|
55
|
+
export const productOptionUnitSchema = z.object({
|
|
56
|
+
id: z.string(),
|
|
57
|
+
type: z.string(),
|
|
58
|
+
label: z.string().optional(),
|
|
59
|
+
description: z.string().nullable().optional(),
|
|
60
|
+
capacity: z.number().int().nonnegative().nullable().optional(),
|
|
61
|
+
});
|
|
62
|
+
export const productOptionSchema = z.object({
|
|
63
|
+
id: z.string(),
|
|
64
|
+
name: z.string(),
|
|
65
|
+
description: z.string().nullable().optional(),
|
|
66
|
+
units: z.array(productOptionUnitSchema).optional().default([]),
|
|
67
|
+
inclusions: z.array(z.string()).optional().default([]),
|
|
68
|
+
});
|
|
69
|
+
export const productDaySchema = z.object({
|
|
70
|
+
day_number: z.number().int().positive(),
|
|
71
|
+
title: z.string().nullable().optional(),
|
|
72
|
+
description: z.string().nullable().optional(),
|
|
73
|
+
location: z.string().nullable().optional(),
|
|
74
|
+
/** Day-level hero image (catalog detail sheet thumbnail). */
|
|
75
|
+
hero_image_url: z.string().nullable().optional(),
|
|
76
|
+
services: z.array(z.string()).optional().default([]),
|
|
77
|
+
});
|
|
78
|
+
export const productPolicySchema = z.object({
|
|
79
|
+
kind: z.enum(["cancellation", "payment", "supplier_notes", "requirements"]),
|
|
80
|
+
body: z.string(),
|
|
81
|
+
/** Optional structured rules — vertical-specific. */
|
|
82
|
+
rules: z.unknown().optional(),
|
|
83
|
+
});
|
|
84
|
+
/**
|
|
85
|
+
* A single bookable departure / time slot — the "when" surface of the
|
|
86
|
+
* product. ISO 8601 timestamps for `starts_at` / `ends_at` so locale
|
|
87
|
+
* formatting happens at render time, never in the cache.
|
|
88
|
+
*
|
|
89
|
+
* Owned products derive these from `availability_slots`; sourced
|
|
90
|
+
* adapters return them via `getContent`. Empty array = "always-on"
|
|
91
|
+
* product (e.g. an evergreen transfer service) or one whose schedule
|
|
92
|
+
* is on-request.
|
|
93
|
+
*/
|
|
94
|
+
export const productDepartureSchema = z.object({
|
|
95
|
+
id: z.string(),
|
|
96
|
+
starts_at: z.string(),
|
|
97
|
+
ends_at: z.string().nullable().optional(),
|
|
98
|
+
/** "open" | "limited" | "sold_out" | "closed" | "on_request" — display only. */
|
|
99
|
+
status: z.string().nullable().optional(),
|
|
100
|
+
/** Total capacity for the slot, when known. */
|
|
101
|
+
capacity: z.number().int().nonnegative().nullable().optional(),
|
|
102
|
+
/** Remaining capacity. Null = unknown / not surfaced; 0 = sold out. */
|
|
103
|
+
remaining: z.number().int().nonnegative().nullable().optional(),
|
|
104
|
+
/** Lowest pricing hint in cents — display only. Real price comes via liveResolve. */
|
|
105
|
+
lowest_price_cents: z.number().int().nonnegative().nullable().optional(),
|
|
106
|
+
currency: z.string().nullable().optional(),
|
|
107
|
+
/** Free-form note (weather caveat, sales window, etc). */
|
|
108
|
+
note: z.string().nullable().optional(),
|
|
109
|
+
});
|
|
110
|
+
/**
|
|
111
|
+
* The product content payload. Cache writes validate against this
|
|
112
|
+
* schema; cache reads skip rows that don't validate (treated as cache
|
|
113
|
+
* miss to surface adapter integration bugs without corrupting reads).
|
|
114
|
+
*/
|
|
115
|
+
export const productContentSchema = z.object({
|
|
116
|
+
product: productSummarySchema,
|
|
117
|
+
options: z.array(productOptionSchema).default([]),
|
|
118
|
+
days: z.array(productDaySchema).default([]),
|
|
119
|
+
media: z.array(productMediaItemSchema).default([]),
|
|
120
|
+
policies: z.array(productPolicySchema).default([]),
|
|
121
|
+
departures: z.array(productDepartureSchema).default([]),
|
|
122
|
+
});
|
|
123
|
+
/**
|
|
124
|
+
* Validate a `ProductContent` payload. Returns the parsed result on
|
|
125
|
+
* success or a structured failure on rejection. Used by the cache write
|
|
126
|
+
* path and by `mergeOverlaysIntoProductContent` to gate overlay merges.
|
|
127
|
+
*/
|
|
128
|
+
export function validateProductContent(payload) {
|
|
129
|
+
const result = productContentSchema.safeParse(payload);
|
|
130
|
+
if (result.success) {
|
|
131
|
+
return { valid: true, content: result.data };
|
|
132
|
+
}
|
|
133
|
+
// Take the first issue's message — that's enough signal for ops; full
|
|
134
|
+
// detail is available on `result.error.issues` if a caller cares.
|
|
135
|
+
const issue = result.error.issues[0];
|
|
136
|
+
return {
|
|
137
|
+
valid: false,
|
|
138
|
+
reason: issue ? `${issue.path.join(".")}: ${issue.message}` : "validation failed",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-shape.test.d.ts","sourceRoot":"","sources":["../src/content-shape.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { PRODUCTS_CONTENT_SCHEMA_VERSION, productContentSchema, validateProductContent, } from "./index.js";
|
|
3
|
+
describe("@voyantjs/products-contracts content shape", () => {
|
|
4
|
+
it("validates the products/v1 rich content payload", () => {
|
|
5
|
+
const content = productContentSchema.parse({
|
|
6
|
+
product: { id: "prod_abc", name: "Sahara Desert Trek" },
|
|
7
|
+
options: [{ id: "opt_std", name: "Standard Departure" }],
|
|
8
|
+
days: [{ day_number: 1, title: "Arrival in Marrakech" }],
|
|
9
|
+
policies: [{ kind: "cancellation", body: "Free cancellation up to 30 days." }],
|
|
10
|
+
});
|
|
11
|
+
expect(PRODUCTS_CONTENT_SCHEMA_VERSION).toBe("products/v1");
|
|
12
|
+
expect(validateProductContent(content)).toMatchObject({ valid: true });
|
|
13
|
+
expect(content.media).toEqual([]);
|
|
14
|
+
expect(content.departures).toEqual([]);
|
|
15
|
+
expect(content.options[0]?.units).toEqual([]);
|
|
16
|
+
});
|
|
17
|
+
it("defaults a media item type to image", () => {
|
|
18
|
+
const content = productContentSchema.parse({
|
|
19
|
+
product: { id: "prod_abc", name: "Sahara Desert Trek" },
|
|
20
|
+
media: [{ url: "https://cdn.example.com/hero.jpg" }],
|
|
21
|
+
});
|
|
22
|
+
expect(content.media[0]?.type).toBe("image");
|
|
23
|
+
});
|
|
24
|
+
it("rejects payloads missing the required product summary", () => {
|
|
25
|
+
expect(validateProductContent({ options: [] })).toMatchObject({ valid: false });
|
|
26
|
+
expect(validateProductContent({ product: { name: "No id" } })).toMatchObject({ valid: false });
|
|
27
|
+
});
|
|
28
|
+
it("rejects unknown policy kinds", () => {
|
|
29
|
+
expect(validateProductContent({
|
|
30
|
+
product: { id: "prod_abc", name: "Sahara Desert Trek" },
|
|
31
|
+
policies: [{ kind: "loyalty", body: "x" }],
|
|
32
|
+
})).toMatchObject({ valid: false });
|
|
33
|
+
});
|
|
34
|
+
});
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./content-shape.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@voyantjs/products-contracts",
|
|
3
|
+
"version": "0.90.0",
|
|
4
|
+
"license": "Apache-2.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts",
|
|
8
|
+
"./content-shape": "./src/content-shape.ts"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"typecheck": "tsc --noEmit",
|
|
12
|
+
"lint": "biome check src/",
|
|
13
|
+
"test": "vitest run --passWithNoTests",
|
|
14
|
+
"build": "tsc -p tsconfig.json",
|
|
15
|
+
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
|
16
|
+
"prepack": "pnpm run build"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"import": "./dist/index.js",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./content-shape": {
|
|
30
|
+
"types": "./dist/content-shape.d.ts",
|
|
31
|
+
"import": "./dist/content-shape.js",
|
|
32
|
+
"default": "./dist/content-shape.js"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"main": "./dist/index.js",
|
|
36
|
+
"types": "./dist/index.d.ts"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"zod": "^4.3.6"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@voyantjs/voyant-typescript-config": "workspace:*",
|
|
43
|
+
"typescript": "^6.0.2",
|
|
44
|
+
"vitest": "^4.1.2"
|
|
45
|
+
},
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "https://github.com/voyantjs/voyant.git",
|
|
49
|
+
"directory": "packages/products-contracts"
|
|
50
|
+
}
|
|
51
|
+
}
|