@voyantjs/extras 0.19.0 → 0.21.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/dist/catalog-policy.d.ts +30 -0
- package/dist/catalog-policy.d.ts.map +1 -0
- package/dist/catalog-policy.js +305 -0
- package/dist/content-shape.d.ts +118 -0
- package/dist/content-shape.d.ts.map +1 -0
- package/dist/content-shape.js +98 -0
- package/dist/draft-shape.d.ts +34 -0
- package/dist/draft-shape.d.ts.map +1 -0
- package/dist/draft-shape.js +69 -0
- package/dist/routes.d.ts +35 -31
- package/dist/routes.d.ts.map +1 -1
- package/dist/schema-sourced-content.d.ts +254 -0
- package/dist/schema-sourced-content.d.ts.map +1 -0
- package/dist/schema-sourced-content.js +45 -0
- package/dist/schema.d.ts +21 -3
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +6 -0
- package/dist/service-catalog-plane.d.ts +79 -0
- package/dist/service-catalog-plane.d.ts.map +1 -0
- package/dist/service-catalog-plane.js +157 -0
- package/dist/service-content-synthesizer.d.ts +41 -0
- package/dist/service-content-synthesizer.d.ts.map +1 -0
- package/dist/service-content-synthesizer.js +138 -0
- package/dist/service-content.d.ts +48 -0
- package/dist/service-content.d.ts.map +1 -0
- package/dist/service-content.js +253 -0
- package/dist/service.d.ts +29 -25
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +2 -0
- package/dist/validation.d.ts +28 -24
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +2 -0
- package/package.json +7 -6
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog-plane integration for the extras service.
|
|
3
|
+
*
|
|
4
|
+
* Extras are a **partial-adoption vertical** per architecture §3.3.1: they
|
|
5
|
+
* participate in provenance + booking snapshot + catalog event taxonomy,
|
|
6
|
+
* but skip the search index and overlay store. The service-plane integration
|
|
7
|
+
* here reflects that — `getResolvedExtraById` returns a resolved view but
|
|
8
|
+
* the resolver always sees an empty overlay set (extras have no overlay
|
|
9
|
+
* rows; the catalog-policy file declares no merchandisable fields).
|
|
10
|
+
*
|
|
11
|
+
* The value of running through the catalog-plane resolver anyway is
|
|
12
|
+
* uniformity: extras' projection and visibility filtering use the same
|
|
13
|
+
* machinery as every other vertical, and snapshot capture at booking commit
|
|
14
|
+
* (the most important participation surface for extras) reuses
|
|
15
|
+
* `productExtraRowToProjection` to build the frozen payload.
|
|
16
|
+
*
|
|
17
|
+
* See `docs/architecture/catalog-architecture.md` §9.1 + §3.3.1.
|
|
18
|
+
*/
|
|
19
|
+
import { type CaptureSnapshotInput, type DocumentBuilder, type DocumentEmitter, type IndexerDocument, type IndexerSlice, type PricingBasis, type Provenance, type ResolvedView, type ResolverScope } from "@voyantjs/catalog";
|
|
20
|
+
import type { AnyDrizzleDb } from "@voyantjs/db";
|
|
21
|
+
import { productExtras } from "./schema.js";
|
|
22
|
+
/**
|
|
23
|
+
* Maps a product-extra row to a field-keyed projection. Extras almost
|
|
24
|
+
* always inherit their provenance from the parent product they attach to;
|
|
25
|
+
* the caller passes the parent's source kind / ref through, defaulting to
|
|
26
|
+
* `owned` for operator-defined extras.
|
|
27
|
+
*/
|
|
28
|
+
export declare function productExtraRowToProjection(row: typeof productExtras.$inferSelect, context: {
|
|
29
|
+
sellerOperatorId: string;
|
|
30
|
+
sourceKind?: string;
|
|
31
|
+
sourceRef?: string;
|
|
32
|
+
}): ReadonlyMap<string, unknown>;
|
|
33
|
+
export declare function productExtraProvenance(_row: typeof productExtras.$inferSelect, context: {
|
|
34
|
+
sellerOperatorId: string;
|
|
35
|
+
sourceKind?: string;
|
|
36
|
+
sourceRef?: string;
|
|
37
|
+
}): Provenance;
|
|
38
|
+
export interface ProductExtraCatalogContext {
|
|
39
|
+
sellerOperatorId: string;
|
|
40
|
+
scope: ResolverScope;
|
|
41
|
+
sourceKind?: string;
|
|
42
|
+
sourceRef?: string;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Catalog-aware extra fetch. The catalog-policy declares no merchandisable
|
|
46
|
+
* fields for extras (per §3.3.1 partial adoption), so the resolver acts as
|
|
47
|
+
* a pure visibility filter rather than an overlay-merge engine. Useful
|
|
48
|
+
* primarily for snapshot capture at booking time.
|
|
49
|
+
*/
|
|
50
|
+
export declare function getResolvedExtraById(db: AnyDrizzleDb, id: string, context: ProductExtraCatalogContext): Promise<ResolvedView | null>;
|
|
51
|
+
export declare function listResolvedExtras(db: AnyDrizzleDb, rows: ReadonlyArray<typeof productExtras.$inferSelect>, context: ProductExtraCatalogContext): Promise<ResolvedView[]>;
|
|
52
|
+
/**
|
|
53
|
+
* Build a `CaptureSnapshotInput` for a product extra. Extras participate
|
|
54
|
+
* in the snapshot graph (per §3.3.1 partial adoption) so refunds can know
|
|
55
|
+
* exactly what add-on the customer purchased and what selectionType /
|
|
56
|
+
* pricingMode applied at booking time.
|
|
57
|
+
*/
|
|
58
|
+
export declare function buildExtraSnapshotInput(db: AnyDrizzleDb, extraId: string, context: ProductExtraCatalogContext & {
|
|
59
|
+
pricingBasis?: PricingBasis;
|
|
60
|
+
}): Promise<Omit<CaptureSnapshotInput, "bookingId"> | null>;
|
|
61
|
+
/**
|
|
62
|
+
* Note: per architecture §3.3.1, extras opt out of search-index
|
|
63
|
+
* participation — the catalog-policy declares every field with
|
|
64
|
+
* `reindex: "none"`. The emitter is provided for completeness (a deployment
|
|
65
|
+
* may opt extras into the index for ops-side keyword search) but production
|
|
66
|
+
* deployments should not register an extras emitter with the IndexerService.
|
|
67
|
+
*/
|
|
68
|
+
export declare function createExtraDocumentEmitter(context: {
|
|
69
|
+
sellerOperatorId: string;
|
|
70
|
+
sourceKind?: string;
|
|
71
|
+
sourceRef?: string;
|
|
72
|
+
}): DocumentEmitter<typeof productExtras.$inferSelect>;
|
|
73
|
+
export declare function createExtraDocumentBuilder(db: AnyDrizzleDb, context: {
|
|
74
|
+
sellerOperatorId: string;
|
|
75
|
+
sourceKind?: string;
|
|
76
|
+
sourceRef?: string;
|
|
77
|
+
}): DocumentBuilder;
|
|
78
|
+
export type { CaptureSnapshotInput, DocumentBuilder, DocumentEmitter, IndexerDocument, IndexerSlice, PricingBasis, Provenance, ResolvedView, ResolverScope, };
|
|
79
|
+
//# sourceMappingURL=service-catalog-plane.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-catalog-plane.d.ts","sourceRoot":"","sources":["../src/service-catalog-plane.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAGL,KAAK,oBAAoB,EAEzB,KAAK,eAAe,EACpB,KAAK,eAAe,EAEpB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,aAAa,EAEnB,MAAM,mBAAmB,CAAA;AAC1B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAIhD,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAU3C;;;;;GAKG;AACH,wBAAgB,2BAA2B,CACzC,GAAG,EAAE,OAAO,aAAa,CAAC,YAAY,EACtC,OAAO,EAAE;IAAE,gBAAgB,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7E,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,CA6B9B;AAED,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,OAAO,aAAa,CAAC,YAAY,EACvC,OAAO,EAAE;IAAE,gBAAgB,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7E,UAAU,CAMZ;AAED,MAAM,WAAW,0BAA0B;IACzC,gBAAgB,EAAE,MAAM,CAAA;IACxB,KAAK,EAAE,aAAa,CAAA;IACpB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,EAAE,EAAE,YAAY,EAChB,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAW9B;AAED,wBAAsB,kBAAkB,CACtC,EAAE,EAAE,YAAY,EAChB,IAAI,EAAE,aAAa,CAAC,OAAO,aAAa,CAAC,YAAY,CAAC,EACtD,OAAO,EAAE,0BAA0B,GAClC,OAAO,CAAC,YAAY,EAAE,CAAC,CAazB;AAED;;;;;GAKG;AACH,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,YAAY,EAChB,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,0BAA0B,GAAG;IAAE,YAAY,CAAC,EAAE,YAAY,CAAA;CAAE,GACpE,OAAO,CAAC,IAAI,CAAC,oBAAoB,EAAE,WAAW,CAAC,GAAG,IAAI,CAAC,CAUzD;AAMD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE;IAClD,gBAAgB,EAAE,MAAM,CAAA;IACxB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,GAAG,eAAe,CAAC,OAAO,aAAa,CAAC,YAAY,CAAC,CAarD;AAED,wBAAgB,0BAA0B,CACxC,EAAE,EAAE,YAAY,EAChB,OAAO,EAAE;IAAE,gBAAgB,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAC7E,eAAe,CAYjB;AAED,YAAY,EACV,oBAAoB,EACpB,eAAe,EACf,eAAe,EACf,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,aAAa,GACd,CAAA"}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Catalog-plane integration for the extras service.
|
|
3
|
+
*
|
|
4
|
+
* Extras are a **partial-adoption vertical** per architecture §3.3.1: they
|
|
5
|
+
* participate in provenance + booking snapshot + catalog event taxonomy,
|
|
6
|
+
* but skip the search index and overlay store. The service-plane integration
|
|
7
|
+
* here reflects that — `getResolvedExtraById` returns a resolved view but
|
|
8
|
+
* the resolver always sees an empty overlay set (extras have no overlay
|
|
9
|
+
* rows; the catalog-policy file declares no merchandisable fields).
|
|
10
|
+
*
|
|
11
|
+
* The value of running through the catalog-plane resolver anyway is
|
|
12
|
+
* uniformity: extras' projection and visibility filtering use the same
|
|
13
|
+
* machinery as every other vertical, and snapshot capture at booking commit
|
|
14
|
+
* (the most important participation surface for extras) reuses
|
|
15
|
+
* `productExtraRowToProjection` to build the frozen payload.
|
|
16
|
+
*
|
|
17
|
+
* See `docs/architecture/catalog-architecture.md` §9.1 + §3.3.1.
|
|
18
|
+
*/
|
|
19
|
+
import { buildIndexerDocument, buildSnapshotInputFromView, createFieldPolicyRegistry, resolveEntityView, } from "@voyantjs/catalog";
|
|
20
|
+
import { eq } from "drizzle-orm";
|
|
21
|
+
import { extrasCatalogPolicy } from "./catalog-policy.js";
|
|
22
|
+
import { productExtras } from "./schema.js";
|
|
23
|
+
let _registry;
|
|
24
|
+
function getExtrasRegistry() {
|
|
25
|
+
if (!_registry) {
|
|
26
|
+
_registry = createFieldPolicyRegistry(extrasCatalogPolicy);
|
|
27
|
+
}
|
|
28
|
+
return _registry;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Maps a product-extra row to a field-keyed projection. Extras almost
|
|
32
|
+
* always inherit their provenance from the parent product they attach to;
|
|
33
|
+
* the caller passes the parent's source kind / ref through, defaulting to
|
|
34
|
+
* `owned` for operator-defined extras.
|
|
35
|
+
*/
|
|
36
|
+
export function productExtraRowToProjection(row, context) {
|
|
37
|
+
return new Map([
|
|
38
|
+
// Provenance
|
|
39
|
+
["source.kind", context.sourceKind ?? "owned"],
|
|
40
|
+
["source.ref", context.sourceRef],
|
|
41
|
+
["seller.operator_id", context.sellerOperatorId],
|
|
42
|
+
// Identity + cross-module reference
|
|
43
|
+
["id", row.id],
|
|
44
|
+
["code", row.code],
|
|
45
|
+
["productId", row.productId],
|
|
46
|
+
["supplierId", row.supplierId],
|
|
47
|
+
["createdAt", row.createdAt],
|
|
48
|
+
["updatedAt", row.updatedAt],
|
|
49
|
+
// Snapshot-relevant managed fields
|
|
50
|
+
["name", row.name],
|
|
51
|
+
["description", row.description],
|
|
52
|
+
// Selection / pricing structure
|
|
53
|
+
["selectionType", row.selectionType],
|
|
54
|
+
["pricingMode", row.pricingMode],
|
|
55
|
+
["pricedPerPerson", row.pricedPerPerson],
|
|
56
|
+
["minQuantity", row.minQuantity],
|
|
57
|
+
["maxQuantity", row.maxQuantity],
|
|
58
|
+
["defaultQuantity", row.defaultQuantity],
|
|
59
|
+
["active", row.active],
|
|
60
|
+
["sortOrder", row.sortOrder],
|
|
61
|
+
]);
|
|
62
|
+
}
|
|
63
|
+
export function productExtraProvenance(_row, context) {
|
|
64
|
+
return {
|
|
65
|
+
source_kind: context.sourceKind ?? "owned",
|
|
66
|
+
source_freshness: context.sourceKind && context.sourceKind !== "owned" ? "sync" : "static",
|
|
67
|
+
source_ref: context.sourceRef,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Catalog-aware extra fetch. The catalog-policy declares no merchandisable
|
|
72
|
+
* fields for extras (per §3.3.1 partial adoption), so the resolver acts as
|
|
73
|
+
* a pure visibility filter rather than an overlay-merge engine. Useful
|
|
74
|
+
* primarily for snapshot capture at booking time.
|
|
75
|
+
*/
|
|
76
|
+
export async function getResolvedExtraById(db, id, context) {
|
|
77
|
+
const rows = await db.select().from(productExtras).where(eq(productExtras.id, id)).limit(1);
|
|
78
|
+
const row = rows[0];
|
|
79
|
+
if (!row)
|
|
80
|
+
return null;
|
|
81
|
+
const projection = productExtraRowToProjection(row, {
|
|
82
|
+
sellerOperatorId: context.sellerOperatorId,
|
|
83
|
+
sourceKind: context.sourceKind,
|
|
84
|
+
sourceRef: context.sourceRef,
|
|
85
|
+
});
|
|
86
|
+
return resolveEntityView(db, getExtrasRegistry(), "extras", id, projection, context.scope);
|
|
87
|
+
}
|
|
88
|
+
export async function listResolvedExtras(db, rows, context) {
|
|
89
|
+
const registry = getExtrasRegistry();
|
|
90
|
+
const views = [];
|
|
91
|
+
for (const row of rows) {
|
|
92
|
+
const projection = productExtraRowToProjection(row, {
|
|
93
|
+
sellerOperatorId: context.sellerOperatorId,
|
|
94
|
+
sourceKind: context.sourceKind,
|
|
95
|
+
sourceRef: context.sourceRef,
|
|
96
|
+
});
|
|
97
|
+
const view = await resolveEntityView(db, registry, "extras", row.id, projection, context.scope);
|
|
98
|
+
views.push(view);
|
|
99
|
+
}
|
|
100
|
+
return views;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Build a `CaptureSnapshotInput` for a product extra. Extras participate
|
|
104
|
+
* in the snapshot graph (per §3.3.1 partial adoption) so refunds can know
|
|
105
|
+
* exactly what add-on the customer purchased and what selectionType /
|
|
106
|
+
* pricingMode applied at booking time.
|
|
107
|
+
*/
|
|
108
|
+
export async function buildExtraSnapshotInput(db, extraId, context) {
|
|
109
|
+
const view = await getResolvedExtraById(db, extraId, context);
|
|
110
|
+
if (!view)
|
|
111
|
+
return null;
|
|
112
|
+
return buildSnapshotInputFromView(view, {
|
|
113
|
+
entityModule: "extras",
|
|
114
|
+
entityId: extraId,
|
|
115
|
+
sourceKind: context.sourceKind ?? "owned",
|
|
116
|
+
sourceRef: context.sourceRef,
|
|
117
|
+
pricingBasis: context.pricingBasis,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
121
|
+
// Indexer document emission
|
|
122
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
123
|
+
/**
|
|
124
|
+
* Note: per architecture §3.3.1, extras opt out of search-index
|
|
125
|
+
* participation — the catalog-policy declares every field with
|
|
126
|
+
* `reindex: "none"`. The emitter is provided for completeness (a deployment
|
|
127
|
+
* may opt extras into the index for ops-side keyword search) but production
|
|
128
|
+
* deployments should not register an extras emitter with the IndexerService.
|
|
129
|
+
*/
|
|
130
|
+
export function createExtraDocumentEmitter(context) {
|
|
131
|
+
const registry = getExtrasRegistry();
|
|
132
|
+
return {
|
|
133
|
+
vertical: "extras",
|
|
134
|
+
emit(source, slice) {
|
|
135
|
+
const projection = productExtraRowToProjection(source, {
|
|
136
|
+
sellerOperatorId: context.sellerOperatorId,
|
|
137
|
+
sourceKind: context.sourceKind,
|
|
138
|
+
sourceRef: context.sourceRef,
|
|
139
|
+
});
|
|
140
|
+
return buildIndexerDocument(registry, projection, slice, source.id);
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
export function createExtraDocumentBuilder(db, context) {
|
|
145
|
+
const emitter = createExtraDocumentEmitter(context);
|
|
146
|
+
return async (entityId, slice) => {
|
|
147
|
+
const rows = await db
|
|
148
|
+
.select()
|
|
149
|
+
.from(productExtras)
|
|
150
|
+
.where(eq(productExtras.id, entityId))
|
|
151
|
+
.limit(1);
|
|
152
|
+
const row = rows[0];
|
|
153
|
+
if (!row)
|
|
154
|
+
return null;
|
|
155
|
+
return emitter.emit(row, slice);
|
|
156
|
+
};
|
|
157
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extras content synthesizer — fallback for thin adapters that declare
|
|
3
|
+
* `supportsContentFetch: false`.
|
|
4
|
+
*
|
|
5
|
+
* Produces the most complete `ExtraContent` blob we can legitimately
|
|
6
|
+
* synthesize from the durable sourced-entry projection + locale-aware
|
|
7
|
+
* overlays + plane-level provenance. Sub-options render as a typed
|
|
8
|
+
* empty state unless the projection genuinely carries them.
|
|
9
|
+
*
|
|
10
|
+
* Per §3.6: never invents plausible-but-unverified fields, never
|
|
11
|
+
* machine-translates, never mines snapshots, never caches its own
|
|
12
|
+
* output.
|
|
13
|
+
*/
|
|
14
|
+
import { type ProvenanceReadResult } from "@voyantjs/catalog";
|
|
15
|
+
import type { AnyDrizzleDb } from "@voyantjs/db";
|
|
16
|
+
import { type ExtraContent } from "./content-shape.js";
|
|
17
|
+
export interface SynthesizeExtraContentOptions {
|
|
18
|
+
provenance: Extract<ProvenanceReadResult, {
|
|
19
|
+
kind: "sourced";
|
|
20
|
+
}>;
|
|
21
|
+
overlays?: ReadonlyArray<{
|
|
22
|
+
field_path: string;
|
|
23
|
+
value: unknown;
|
|
24
|
+
}>;
|
|
25
|
+
}
|
|
26
|
+
export interface SynthesizedExtraContent {
|
|
27
|
+
content: ExtraContent;
|
|
28
|
+
content_schema_version: string;
|
|
29
|
+
served_locale: string;
|
|
30
|
+
source_kind: string;
|
|
31
|
+
source_provider?: string;
|
|
32
|
+
}
|
|
33
|
+
export declare function synthesizeExtraContent(scope: {
|
|
34
|
+
locale: string;
|
|
35
|
+
}, options: SynthesizeExtraContentOptions): SynthesizedExtraContent;
|
|
36
|
+
export declare function synthesizeExtraContentFromDb(db: AnyDrizzleDb, scope: {
|
|
37
|
+
locale: string;
|
|
38
|
+
}, provenance: Extract<ProvenanceReadResult, {
|
|
39
|
+
kind: "sourced";
|
|
40
|
+
}>): Promise<SynthesizedExtraContent>;
|
|
41
|
+
//# sourceMappingURL=service-content-synthesizer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-content-synthesizer.d.ts","sourceRoot":"","sources":["../src/service-content-synthesizer.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,mBAAmB,CAAA;AAC1B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAEhD,OAAO,EAEL,KAAK,YAAY,EAElB,MAAM,oBAAoB,CAAA;AAE3B,MAAM,WAAW,6BAA6B;IAC5C,UAAU,EAAE,OAAO,CAAC,oBAAoB,EAAE;QAAE,IAAI,EAAE,SAAS,CAAA;KAAE,CAAC,CAAA;IAC9D,QAAQ,CAAC,EAAE,aAAa,CAAC;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,CAAC,CAAA;CACjE;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,YAAY,CAAA;IACrB,sBAAsB,EAAE,MAAM,CAAA;IAC9B,aAAa,EAAE,MAAM,CAAA;IACrB,WAAW,EAAE,MAAM,CAAA;IACnB,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,EACzB,OAAO,EAAE,6BAA6B,GACrC,uBAAuB,CAiCzB;AAED,wBAAsB,4BAA4B,CAChD,EAAE,EAAE,YAAY,EAChB,KAAK,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,EACzB,UAAU,EAAE,OAAO,CAAC,oBAAoB,EAAE;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC,GAC7D,OAAO,CAAC,uBAAuB,CAAC,CAOlC"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extras content synthesizer — fallback for thin adapters that declare
|
|
3
|
+
* `supportsContentFetch: false`.
|
|
4
|
+
*
|
|
5
|
+
* Produces the most complete `ExtraContent` blob we can legitimately
|
|
6
|
+
* synthesize from the durable sourced-entry projection + locale-aware
|
|
7
|
+
* overlays + plane-level provenance. Sub-options render as a typed
|
|
8
|
+
* empty state unless the projection genuinely carries them.
|
|
9
|
+
*
|
|
10
|
+
* Per §3.6: never invents plausible-but-unverified fields, never
|
|
11
|
+
* machine-translates, never mines snapshots, never caches its own
|
|
12
|
+
* output.
|
|
13
|
+
*/
|
|
14
|
+
import { fetchOverlaysForEntity, mergeOverlaysIntoContent, } from "@voyantjs/catalog";
|
|
15
|
+
import { EXTRAS_CONTENT_SCHEMA_VERSION, extraContentSchema, } from "./content-shape.js";
|
|
16
|
+
export function synthesizeExtraContent(scope, options) {
|
|
17
|
+
const projection = options.provenance.projection;
|
|
18
|
+
const extra = pickExtraSummary(projection, options.provenance);
|
|
19
|
+
const media = pickMedia(projection);
|
|
20
|
+
const policies = pickPolicies(projection);
|
|
21
|
+
const baseContent = {
|
|
22
|
+
extra,
|
|
23
|
+
options: [],
|
|
24
|
+
media,
|
|
25
|
+
policies,
|
|
26
|
+
};
|
|
27
|
+
let merged = baseContent;
|
|
28
|
+
if (options.overlays && options.overlays.length > 0) {
|
|
29
|
+
const result = mergeOverlaysIntoContent(baseContent, options.overlays, {
|
|
30
|
+
validate(p) {
|
|
31
|
+
const r = extraContentSchema.safeParse(p);
|
|
32
|
+
return r.success
|
|
33
|
+
? { valid: true }
|
|
34
|
+
: { valid: false, reason: r.error.issues[0]?.message ?? "invalid" };
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
merged = extraContentSchema.parse(result);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
content: merged,
|
|
41
|
+
content_schema_version: EXTRAS_CONTENT_SCHEMA_VERSION,
|
|
42
|
+
served_locale: scope.locale,
|
|
43
|
+
source_kind: options.provenance.provenance.source_kind,
|
|
44
|
+
source_provider: options.provenance.provenance.source_provider,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export async function synthesizeExtraContentFromDb(db, scope, provenance) {
|
|
48
|
+
const entityId = entityIdFromProvenance(provenance);
|
|
49
|
+
const overlays = await fetchOverlaysForEntity(db, "extras", entityId);
|
|
50
|
+
return synthesizeExtraContent(scope, {
|
|
51
|
+
provenance,
|
|
52
|
+
overlays: overlays.map((o) => ({ field_path: o.field_path, value: o.value })),
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function entityIdFromProvenance(provenance) {
|
|
56
|
+
const fromProjection = provenance.projection.id;
|
|
57
|
+
if (typeof fromProjection === "string" && fromProjection.length > 0) {
|
|
58
|
+
return fromProjection;
|
|
59
|
+
}
|
|
60
|
+
return provenance.entry_id;
|
|
61
|
+
}
|
|
62
|
+
function pickExtraSummary(projection, provenance) {
|
|
63
|
+
return {
|
|
64
|
+
id: stringOr(projection.id, "") || provenance.entry_id,
|
|
65
|
+
name: stringOr(projection.name, "") || stringOr(projection.title, "") || "Unnamed extra",
|
|
66
|
+
status: stringOr(projection.status, undefined),
|
|
67
|
+
description: stringOr(projection.description, null),
|
|
68
|
+
selection_type: stringOr(projection.selection_type, "optional"),
|
|
69
|
+
pricing_mode: stringOr(projection.pricing_mode, "per_booking"),
|
|
70
|
+
priced_per_person: typeof projection.priced_per_person === "boolean" ? projection.priced_per_person : undefined,
|
|
71
|
+
category: stringOr(projection.category, null),
|
|
72
|
+
hero_image_url: stringOr(projection.hero_image_url, null),
|
|
73
|
+
highlights: stringArrayOr(projection.highlights, []),
|
|
74
|
+
supplier: stringOr(projection.supplier, null) ??
|
|
75
|
+
stringOr(projection.supplier_name, null) ??
|
|
76
|
+
provenance.provenance.source_provider ??
|
|
77
|
+
null,
|
|
78
|
+
duration_minutes: numberOr(projection.duration_minutes, null),
|
|
79
|
+
requirements_summary: stringOr(projection.requirements_summary, null),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function pickMedia(projection) {
|
|
83
|
+
const heroUrl = stringOr(projection.hero_image_url, null);
|
|
84
|
+
const out = [];
|
|
85
|
+
if (heroUrl) {
|
|
86
|
+
out.push({ url: heroUrl, type: "image", caption: null, alt: null });
|
|
87
|
+
}
|
|
88
|
+
const additional = projection.media;
|
|
89
|
+
if (Array.isArray(additional)) {
|
|
90
|
+
for (const item of additional) {
|
|
91
|
+
if (item && typeof item === "object") {
|
|
92
|
+
const obj = item;
|
|
93
|
+
const url = stringOr(obj.url, null);
|
|
94
|
+
if (!url)
|
|
95
|
+
continue;
|
|
96
|
+
out.push({
|
|
97
|
+
url,
|
|
98
|
+
type: pickMediaType(obj.type),
|
|
99
|
+
caption: stringOr(obj.caption, null),
|
|
100
|
+
alt: stringOr(obj.alt, null),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
function pickMediaType(value) {
|
|
108
|
+
if (value === "video")
|
|
109
|
+
return "video";
|
|
110
|
+
if (value === "document")
|
|
111
|
+
return "document";
|
|
112
|
+
return "image";
|
|
113
|
+
}
|
|
114
|
+
function pickPolicies(projection) {
|
|
115
|
+
const out = [];
|
|
116
|
+
const cancel = stringOr(projection.cancellation_policy, null);
|
|
117
|
+
if (cancel)
|
|
118
|
+
out.push({ kind: "cancellation", body: cancel });
|
|
119
|
+
const payment = stringOr(projection.payment_terms, null);
|
|
120
|
+
if (payment)
|
|
121
|
+
out.push({ kind: "payment", body: payment });
|
|
122
|
+
const requirements = stringOr(projection.requirements, null);
|
|
123
|
+
if (requirements)
|
|
124
|
+
out.push({ kind: "requirements", body: requirements });
|
|
125
|
+
return out;
|
|
126
|
+
}
|
|
127
|
+
function stringOr(value, fallback) {
|
|
128
|
+
return typeof value === "string" && value.length > 0 ? value : fallback;
|
|
129
|
+
}
|
|
130
|
+
function numberOr(value, fallback) {
|
|
131
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
132
|
+
}
|
|
133
|
+
function stringArrayOr(value, fallback) {
|
|
134
|
+
if (!Array.isArray(value))
|
|
135
|
+
return fallback;
|
|
136
|
+
const out = value.filter((v) => typeof v === "string");
|
|
137
|
+
return out.length > 0 ? out : fallback;
|
|
138
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extras content service — `getExtraContent` with locale-resolved
|
|
3
|
+
* cache reads, SWR refresh, and synthesizer fallback.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors `service-content.ts` in the products / cruises / hospitality
|
|
6
|
+
* / charters packages but extras-shaped. The extras content aggregate
|
|
7
|
+
* (§3.2 / §3.6) is `{ extra, options[], media[], policies[] }` — one
|
|
8
|
+
* payload returned by a single getContent. Pricing stays out (volatile-
|
|
9
|
+
* live, flows through `liveResolve`).
|
|
10
|
+
*
|
|
11
|
+
* Extras don't appear in the search index (per the vertical's catalog-
|
|
12
|
+
* policy.ts), but sourced extras still need rich content for the
|
|
13
|
+
* booking-flow's add-on selection UI. The cache layer covers exactly
|
|
14
|
+
* that surface.
|
|
15
|
+
*/
|
|
16
|
+
import { type ContentLocaleResolution, type InvalidateOnDrift, type SourceAdapter, type SourceAdapterContext } from "@voyantjs/catalog";
|
|
17
|
+
import type { SourceAdapterRegistry } from "@voyantjs/catalog/booking-engine";
|
|
18
|
+
import type { AnyDrizzleDb } from "@voyantjs/db";
|
|
19
|
+
import { type ExtraContent } from "./content-shape.js";
|
|
20
|
+
export interface ExtraContentScope {
|
|
21
|
+
preferredLocales: ReadonlyArray<string>;
|
|
22
|
+
market?: string;
|
|
23
|
+
currency?: string;
|
|
24
|
+
acceptMachineTranslated?: boolean;
|
|
25
|
+
}
|
|
26
|
+
export interface GetExtraContentOptions {
|
|
27
|
+
registry: SourceAdapterRegistry;
|
|
28
|
+
buildAdapterContext?: (adapter: SourceAdapter) => SourceAdapterContext;
|
|
29
|
+
onOverlayError?: (event: {
|
|
30
|
+
field_path: string;
|
|
31
|
+
reason: string;
|
|
32
|
+
}) => void;
|
|
33
|
+
}
|
|
34
|
+
export interface ResolvedExtraContent {
|
|
35
|
+
content: ExtraContent;
|
|
36
|
+
resolution: ContentLocaleResolution<{
|
|
37
|
+
locale: string;
|
|
38
|
+
payload: ExtraContent;
|
|
39
|
+
}>;
|
|
40
|
+
source: "sourced-cache" | "sourced-fresh" | "synthesized";
|
|
41
|
+
served_stale: boolean;
|
|
42
|
+
synthesized: boolean;
|
|
43
|
+
machine_translated: boolean;
|
|
44
|
+
}
|
|
45
|
+
export declare function getExtraContent(db: AnyDrizzleDb, entityId: string, scope: ExtraContentScope, options: GetExtraContentOptions): Promise<ResolvedExtraContent | null>;
|
|
46
|
+
/** Drift event consumer for the extras content cache. Per §3.4.1. */
|
|
47
|
+
export declare const invalidateExtraContentOnDrift: InvalidateOnDrift;
|
|
48
|
+
//# sourceMappingURL=service-content.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-content.d.ts","sourceRoot":"","sources":["../src/service-content.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EACL,KAAK,uBAAuB,EAK5B,KAAK,iBAAiB,EAKtB,KAAK,aAAa,EAClB,KAAK,oBAAoB,EAE1B,MAAM,mBAAmB,CAAA;AAC1B,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,kCAAkC,CAAA;AAC7E,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAA;AAGhD,OAAO,EAEL,KAAK,YAAY,EAIlB,MAAM,oBAAoB,CAAA;AAc3B,MAAM,WAAW,iBAAiB;IAChC,gBAAgB,EAAE,aAAa,CAAC,MAAM,CAAC,CAAA;IACvC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,uBAAuB,CAAC,EAAE,OAAO,CAAA;CAClC;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,qBAAqB,CAAA;IAC/B,mBAAmB,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,oBAAoB,CAAA;IACtE,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAA;CACzE;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,YAAY,CAAA;IACrB,UAAU,EAAE,uBAAuB,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,YAAY,CAAA;KAAE,CAAC,CAAA;IAC9E,MAAM,EAAE,eAAe,GAAG,eAAe,GAAG,aAAa,CAAA;IACzD,YAAY,EAAE,OAAO,CAAA;IACrB,WAAW,EAAE,OAAO,CAAA;IACpB,kBAAkB,EAAE,OAAO,CAAA;CAC5B;AAED,wBAAsB,eAAe,CACnC,EAAE,EAAE,YAAY,EAChB,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,iBAAiB,EACxB,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC,CA0FtC;AA2ND,qEAAqE;AACrE,eAAO,MAAM,6BAA6B,EAAE,iBAG3C,CAAA"}
|