@voyantjs/catalog-authoring 0.105.0 → 0.106.1
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/clone-content.d.ts +38 -0
- package/dist/clone-content.d.ts.map +1 -0
- package/dist/clone-content.js +367 -0
- package/dist/clone-pricing.d.ts +9 -0
- package/dist/clone-pricing.d.ts.map +1 -0
- package/dist/clone-pricing.js +242 -0
- package/dist/clone.d.ts +45 -0
- package/dist/clone.d.ts.map +1 -0
- package/dist/clone.js +141 -0
- package/dist/extension.d.ts +83 -9
- package/dist/extension.d.ts.map +1 -1
- package/dist/extension.js +57 -10
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/service.d.ts +2 -3
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +2 -3
- package/package.json +8 -4
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
2
|
+
/** Shared, mutable state threaded across the clone copy phases (id remaps). */
|
|
3
|
+
export interface CloneContext {
|
|
4
|
+
tx: PostgresJsDatabase;
|
|
5
|
+
sourceId: string;
|
|
6
|
+
targetId: string;
|
|
7
|
+
copyDepartures: boolean;
|
|
8
|
+
optionIdMap: Map<string, string>;
|
|
9
|
+
unitIdMap: Map<string, string>;
|
|
10
|
+
unitsByNewOption: Map<string, {
|
|
11
|
+
id: string;
|
|
12
|
+
}[]>;
|
|
13
|
+
itineraryIdMap: Map<string, string>;
|
|
14
|
+
dayIdMap: Map<string, string>;
|
|
15
|
+
startTimeIdMap: Map<string, string>;
|
|
16
|
+
ruleIdMap: Map<string, string>;
|
|
17
|
+
slotIdMap: Map<string, string>;
|
|
18
|
+
optionPriceRuleIdMap: Map<string, string>;
|
|
19
|
+
optionUnitPriceRuleIdMap: Map<string, string>;
|
|
20
|
+
pricingCategoryIdMap: Map<string, string>;
|
|
21
|
+
productExtraIdMap: Map<string, string>;
|
|
22
|
+
optionExtraConfigIdMap: Map<string, string>;
|
|
23
|
+
}
|
|
24
|
+
type SystemColumns = {
|
|
25
|
+
id?: unknown;
|
|
26
|
+
createdAt?: unknown;
|
|
27
|
+
updatedAt?: unknown;
|
|
28
|
+
};
|
|
29
|
+
export declare function withoutSystemColumns<T extends SystemColumns>(row: T): Omit<T, "id" | "createdAt" | "updatedAt">;
|
|
30
|
+
/**
|
|
31
|
+
* Copies the product's content graph: per-product settings, options + units
|
|
32
|
+
* (with translations + resource templates), pricing categories (+ dependencies,
|
|
33
|
+
* remapped), itineraries/days/services, media, and extras. Populates the id
|
|
34
|
+
* remaps in {@link CloneContext} for the pricing/availability phase.
|
|
35
|
+
*/
|
|
36
|
+
export declare function copyProductContent(ctx: CloneContext): Promise<void>;
|
|
37
|
+
export {};
|
|
38
|
+
//# sourceMappingURL=clone-content.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clone-content.d.ts","sourceRoot":"","sources":["../src/clone-content.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAEjE,+EAA+E;AAC/E,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,kBAAkB,CAAA;IACtB,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,cAAc,EAAE,OAAO,CAAA;IACvB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC,CAAA;IAC/C,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7B,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACnC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,oBAAoB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACzC,wBAAwB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7C,oBAAoB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACzC,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IACtC,sBAAsB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAC5C;AAED,KAAK,aAAa,GAAG;IACnB,EAAE,CAAC,EAAE,OAAO,CAAA;IACZ,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB,CAAA;AAED,wBAAgB,oBAAoB,CAAC,CAAC,SAAS,aAAa,EAAE,GAAG,EAAE,CAAC,6CAGnE;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA6HzE"}
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { productOptionResourceTemplates } from "@voyantjs/availability";
|
|
2
|
+
import { optionExtraConfigs, productExtras } from "@voyantjs/extras";
|
|
3
|
+
import { pricingCategories, pricingCategoryDependencies } from "@voyantjs/pricing/schema";
|
|
4
|
+
import { optionUnits, optionUnitTranslations, productActivationSettings, productCapabilities, productCategoryProducts, productDayServices, productDays, productDayTranslations, productDeliveryFormats, productDestinations, productFaqs, productFeatures, productItineraries, productLocations, productMedia, productOptions, productOptionTranslations, productTagProducts, productTicketSettings, productTranslations, productVisibilitySettings, } from "@voyantjs/products/schema";
|
|
5
|
+
import { eq, inArray } from "drizzle-orm";
|
|
6
|
+
export function withoutSystemColumns(row) {
|
|
7
|
+
const { id: _id, createdAt: _createdAt, updatedAt: _updatedAt, ...values } = row;
|
|
8
|
+
return values;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Copies the product's content graph: per-product settings, options + units
|
|
12
|
+
* (with translations + resource templates), pricing categories (+ dependencies,
|
|
13
|
+
* remapped), itineraries/days/services, media, and extras. Populates the id
|
|
14
|
+
* remaps in {@link CloneContext} for the pricing/availability phase.
|
|
15
|
+
*/
|
|
16
|
+
export async function copyProductContent(ctx) {
|
|
17
|
+
const { tx, sourceId, targetId } = ctx;
|
|
18
|
+
const activationRows = await tx
|
|
19
|
+
.select()
|
|
20
|
+
.from(productActivationSettings)
|
|
21
|
+
.where(eq(productActivationSettings.productId, sourceId));
|
|
22
|
+
for (const row of activationRows) {
|
|
23
|
+
await tx.insert(productActivationSettings).values({
|
|
24
|
+
...withoutSystemColumns(row),
|
|
25
|
+
productId: targetId,
|
|
26
|
+
activationMode: "manual",
|
|
27
|
+
activateAt: null,
|
|
28
|
+
deactivateAt: null,
|
|
29
|
+
sellAt: null,
|
|
30
|
+
stopSellAt: null,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
const ticketRows = await tx
|
|
34
|
+
.select()
|
|
35
|
+
.from(productTicketSettings)
|
|
36
|
+
.where(eq(productTicketSettings.productId, sourceId));
|
|
37
|
+
for (const row of ticketRows) {
|
|
38
|
+
await tx
|
|
39
|
+
.insert(productTicketSettings)
|
|
40
|
+
.values({ ...withoutSystemColumns(row), productId: targetId });
|
|
41
|
+
}
|
|
42
|
+
const visibilityRows = await tx
|
|
43
|
+
.select()
|
|
44
|
+
.from(productVisibilitySettings)
|
|
45
|
+
.where(eq(productVisibilitySettings.productId, sourceId));
|
|
46
|
+
for (const row of visibilityRows) {
|
|
47
|
+
await tx
|
|
48
|
+
.insert(productVisibilitySettings)
|
|
49
|
+
.values({ ...withoutSystemColumns(row), productId: targetId });
|
|
50
|
+
}
|
|
51
|
+
const capabilityRows = await tx
|
|
52
|
+
.select()
|
|
53
|
+
.from(productCapabilities)
|
|
54
|
+
.where(eq(productCapabilities.productId, sourceId));
|
|
55
|
+
for (const row of capabilityRows) {
|
|
56
|
+
await tx
|
|
57
|
+
.insert(productCapabilities)
|
|
58
|
+
.values({ ...withoutSystemColumns(row), productId: targetId });
|
|
59
|
+
}
|
|
60
|
+
const deliveryFormatRows = await tx
|
|
61
|
+
.select()
|
|
62
|
+
.from(productDeliveryFormats)
|
|
63
|
+
.where(eq(productDeliveryFormats.productId, sourceId));
|
|
64
|
+
for (const row of deliveryFormatRows) {
|
|
65
|
+
await tx
|
|
66
|
+
.insert(productDeliveryFormats)
|
|
67
|
+
.values({ ...withoutSystemColumns(row), productId: targetId });
|
|
68
|
+
}
|
|
69
|
+
const featureRows = await tx
|
|
70
|
+
.select()
|
|
71
|
+
.from(productFeatures)
|
|
72
|
+
.where(eq(productFeatures.productId, sourceId));
|
|
73
|
+
for (const row of featureRows) {
|
|
74
|
+
await tx.insert(productFeatures).values({ ...withoutSystemColumns(row), productId: targetId });
|
|
75
|
+
}
|
|
76
|
+
const faqRows = await tx.select().from(productFaqs).where(eq(productFaqs.productId, sourceId));
|
|
77
|
+
for (const row of faqRows) {
|
|
78
|
+
await tx.insert(productFaqs).values({ ...withoutSystemColumns(row), productId: targetId });
|
|
79
|
+
}
|
|
80
|
+
const locationRows = await tx
|
|
81
|
+
.select()
|
|
82
|
+
.from(productLocations)
|
|
83
|
+
.where(eq(productLocations.productId, sourceId));
|
|
84
|
+
for (const row of locationRows) {
|
|
85
|
+
await tx.insert(productLocations).values({ ...withoutSystemColumns(row), productId: targetId });
|
|
86
|
+
}
|
|
87
|
+
const translationRows = await tx
|
|
88
|
+
.select()
|
|
89
|
+
.from(productTranslations)
|
|
90
|
+
.where(eq(productTranslations.productId, sourceId));
|
|
91
|
+
for (const row of translationRows) {
|
|
92
|
+
await tx
|
|
93
|
+
.insert(productTranslations)
|
|
94
|
+
.values({ ...withoutSystemColumns(row), productId: targetId });
|
|
95
|
+
}
|
|
96
|
+
const categoryRows = await tx
|
|
97
|
+
.select()
|
|
98
|
+
.from(productCategoryProducts)
|
|
99
|
+
.where(eq(productCategoryProducts.productId, sourceId));
|
|
100
|
+
for (const row of categoryRows) {
|
|
101
|
+
await tx
|
|
102
|
+
.insert(productCategoryProducts)
|
|
103
|
+
.values({ ...withoutSystemColumns(row), productId: targetId });
|
|
104
|
+
}
|
|
105
|
+
const tagRows = await tx
|
|
106
|
+
.select()
|
|
107
|
+
.from(productTagProducts)
|
|
108
|
+
.where(eq(productTagProducts.productId, sourceId));
|
|
109
|
+
for (const row of tagRows) {
|
|
110
|
+
await tx
|
|
111
|
+
.insert(productTagProducts)
|
|
112
|
+
.values({ ...withoutSystemColumns(row), productId: targetId });
|
|
113
|
+
}
|
|
114
|
+
const destinationRows = await tx
|
|
115
|
+
.select()
|
|
116
|
+
.from(productDestinations)
|
|
117
|
+
.where(eq(productDestinations.productId, sourceId));
|
|
118
|
+
for (const row of destinationRows) {
|
|
119
|
+
await tx
|
|
120
|
+
.insert(productDestinations)
|
|
121
|
+
.values({ ...withoutSystemColumns(row), productId: targetId });
|
|
122
|
+
}
|
|
123
|
+
await copyOptionsAndUnits(ctx);
|
|
124
|
+
await copyPricingCategories(ctx);
|
|
125
|
+
await copyItinerary(ctx);
|
|
126
|
+
await copyMedia(ctx);
|
|
127
|
+
await copyExtras(ctx);
|
|
128
|
+
}
|
|
129
|
+
async function copyOptionsAndUnits(ctx) {
|
|
130
|
+
const { tx, sourceId, targetId, optionIdMap, unitIdMap, unitsByNewOption } = ctx;
|
|
131
|
+
const sourceOptions = await tx
|
|
132
|
+
.select()
|
|
133
|
+
.from(productOptions)
|
|
134
|
+
.where(eq(productOptions.productId, sourceId));
|
|
135
|
+
for (const row of sourceOptions) {
|
|
136
|
+
const [copy] = await tx
|
|
137
|
+
.insert(productOptions)
|
|
138
|
+
.values({ ...withoutSystemColumns(row), productId: targetId })
|
|
139
|
+
.returning();
|
|
140
|
+
if (copy) {
|
|
141
|
+
optionIdMap.set(row.id, copy.id);
|
|
142
|
+
unitsByNewOption.set(copy.id, []);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const sourceOptionIds = sourceOptions.map((row) => row.id);
|
|
146
|
+
if (sourceOptionIds.length === 0)
|
|
147
|
+
return;
|
|
148
|
+
const optionTranslationRows = await tx
|
|
149
|
+
.select()
|
|
150
|
+
.from(productOptionTranslations)
|
|
151
|
+
.where(inArray(productOptionTranslations.optionId, sourceOptionIds));
|
|
152
|
+
for (const row of optionTranslationRows) {
|
|
153
|
+
const targetOptionId = optionIdMap.get(row.optionId);
|
|
154
|
+
if (!targetOptionId)
|
|
155
|
+
continue;
|
|
156
|
+
await tx
|
|
157
|
+
.insert(productOptionTranslations)
|
|
158
|
+
.values({ ...withoutSystemColumns(row), optionId: targetOptionId });
|
|
159
|
+
}
|
|
160
|
+
const sourceUnits = await tx
|
|
161
|
+
.select()
|
|
162
|
+
.from(optionUnits)
|
|
163
|
+
.where(inArray(optionUnits.optionId, sourceOptionIds));
|
|
164
|
+
for (const row of sourceUnits) {
|
|
165
|
+
const targetOptionId = optionIdMap.get(row.optionId);
|
|
166
|
+
if (!targetOptionId)
|
|
167
|
+
continue;
|
|
168
|
+
const [copy] = await tx
|
|
169
|
+
.insert(optionUnits)
|
|
170
|
+
.values({ ...withoutSystemColumns(row), optionId: targetOptionId })
|
|
171
|
+
.returning();
|
|
172
|
+
if (copy) {
|
|
173
|
+
unitIdMap.set(row.id, copy.id);
|
|
174
|
+
unitsByNewOption.get(targetOptionId)?.push({ id: copy.id });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const sourceUnitIds = sourceUnits.map((row) => row.id);
|
|
178
|
+
if (sourceUnitIds.length > 0) {
|
|
179
|
+
const unitTranslationRows = await tx
|
|
180
|
+
.select()
|
|
181
|
+
.from(optionUnitTranslations)
|
|
182
|
+
.where(inArray(optionUnitTranslations.unitId, sourceUnitIds));
|
|
183
|
+
for (const row of unitTranslationRows) {
|
|
184
|
+
const targetUnitId = unitIdMap.get(row.unitId);
|
|
185
|
+
if (!targetUnitId)
|
|
186
|
+
continue;
|
|
187
|
+
await tx
|
|
188
|
+
.insert(optionUnitTranslations)
|
|
189
|
+
.values({ ...withoutSystemColumns(row), unitId: targetUnitId });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const resourceTemplateRows = await tx
|
|
193
|
+
.select()
|
|
194
|
+
.from(productOptionResourceTemplates)
|
|
195
|
+
.where(inArray(productOptionResourceTemplates.productOptionId, sourceOptionIds));
|
|
196
|
+
for (const row of resourceTemplateRows) {
|
|
197
|
+
const targetOptionId = optionIdMap.get(row.productOptionId);
|
|
198
|
+
if (!targetOptionId)
|
|
199
|
+
continue;
|
|
200
|
+
await tx
|
|
201
|
+
.insert(productOptionResourceTemplates)
|
|
202
|
+
.values({ ...withoutSystemColumns(row), productOptionId: targetOptionId });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async function copyPricingCategories(ctx) {
|
|
206
|
+
const { tx, sourceId, targetId, optionIdMap, unitIdMap, pricingCategoryIdMap } = ctx;
|
|
207
|
+
const pricingCategoryRows = await tx
|
|
208
|
+
.select()
|
|
209
|
+
.from(pricingCategories)
|
|
210
|
+
.where(eq(pricingCategories.productId, sourceId));
|
|
211
|
+
for (const row of pricingCategoryRows) {
|
|
212
|
+
const targetOptionId = row.optionId ? (optionIdMap.get(row.optionId) ?? null) : null;
|
|
213
|
+
const targetUnitId = row.unitId ? (unitIdMap.get(row.unitId) ?? null) : null;
|
|
214
|
+
if (row.optionId && !targetOptionId)
|
|
215
|
+
continue;
|
|
216
|
+
if (row.unitId && !targetUnitId)
|
|
217
|
+
continue;
|
|
218
|
+
const [copy] = await tx
|
|
219
|
+
.insert(pricingCategories)
|
|
220
|
+
.values({
|
|
221
|
+
...withoutSystemColumns(row),
|
|
222
|
+
productId: targetId,
|
|
223
|
+
optionId: targetOptionId,
|
|
224
|
+
unitId: targetUnitId,
|
|
225
|
+
})
|
|
226
|
+
.returning();
|
|
227
|
+
if (copy)
|
|
228
|
+
pricingCategoryIdMap.set(row.id, copy.id);
|
|
229
|
+
}
|
|
230
|
+
const sourcePricingCategoryIds = pricingCategoryRows.map((row) => row.id);
|
|
231
|
+
if (sourcePricingCategoryIds.length === 0)
|
|
232
|
+
return;
|
|
233
|
+
const dependencyRows = await tx
|
|
234
|
+
.select()
|
|
235
|
+
.from(pricingCategoryDependencies)
|
|
236
|
+
.where(inArray(pricingCategoryDependencies.pricingCategoryId, sourcePricingCategoryIds));
|
|
237
|
+
for (const row of dependencyRows) {
|
|
238
|
+
const targetPricingCategoryId = pricingCategoryIdMap.get(row.pricingCategoryId);
|
|
239
|
+
const targetMasterPricingCategoryId = pricingCategoryIdMap.get(row.masterPricingCategoryId);
|
|
240
|
+
if (!targetPricingCategoryId || !targetMasterPricingCategoryId)
|
|
241
|
+
continue;
|
|
242
|
+
await tx.insert(pricingCategoryDependencies).values({
|
|
243
|
+
...withoutSystemColumns(row),
|
|
244
|
+
pricingCategoryId: targetPricingCategoryId,
|
|
245
|
+
masterPricingCategoryId: targetMasterPricingCategoryId,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
async function copyItinerary(ctx) {
|
|
250
|
+
const { tx, sourceId, targetId, itineraryIdMap, dayIdMap } = ctx;
|
|
251
|
+
const sourceItineraries = await tx
|
|
252
|
+
.select()
|
|
253
|
+
.from(productItineraries)
|
|
254
|
+
.where(eq(productItineraries.productId, sourceId));
|
|
255
|
+
for (const row of sourceItineraries) {
|
|
256
|
+
const [copy] = await tx
|
|
257
|
+
.insert(productItineraries)
|
|
258
|
+
.values({ ...withoutSystemColumns(row), productId: targetId })
|
|
259
|
+
.returning();
|
|
260
|
+
if (copy)
|
|
261
|
+
itineraryIdMap.set(row.id, copy.id);
|
|
262
|
+
}
|
|
263
|
+
if (sourceItineraries.length === 0) {
|
|
264
|
+
await tx
|
|
265
|
+
.insert(productItineraries)
|
|
266
|
+
.values({ productId: targetId, name: "Default", isDefault: true, sortOrder: 0 });
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const sourceItineraryIds = sourceItineraries.map((row) => row.id);
|
|
270
|
+
const sourceDays = await tx
|
|
271
|
+
.select()
|
|
272
|
+
.from(productDays)
|
|
273
|
+
.where(inArray(productDays.itineraryId, sourceItineraryIds));
|
|
274
|
+
for (const row of sourceDays) {
|
|
275
|
+
const targetItineraryId = itineraryIdMap.get(row.itineraryId);
|
|
276
|
+
if (!targetItineraryId)
|
|
277
|
+
continue;
|
|
278
|
+
const [copy] = await tx
|
|
279
|
+
.insert(productDays)
|
|
280
|
+
.values({ ...withoutSystemColumns(row), itineraryId: targetItineraryId })
|
|
281
|
+
.returning();
|
|
282
|
+
if (copy)
|
|
283
|
+
dayIdMap.set(row.id, copy.id);
|
|
284
|
+
}
|
|
285
|
+
const sourceDayIds = sourceDays.map((row) => row.id);
|
|
286
|
+
if (sourceDayIds.length === 0)
|
|
287
|
+
return;
|
|
288
|
+
const dayServiceRows = await tx
|
|
289
|
+
.select()
|
|
290
|
+
.from(productDayServices)
|
|
291
|
+
.where(inArray(productDayServices.dayId, sourceDayIds));
|
|
292
|
+
for (const row of dayServiceRows) {
|
|
293
|
+
const targetDayId = dayIdMap.get(row.dayId);
|
|
294
|
+
if (!targetDayId)
|
|
295
|
+
continue;
|
|
296
|
+
await tx.insert(productDayServices).values({ ...withoutSystemColumns(row), dayId: targetDayId });
|
|
297
|
+
}
|
|
298
|
+
const dayTranslationRows = await tx
|
|
299
|
+
.select()
|
|
300
|
+
.from(productDayTranslations)
|
|
301
|
+
.where(inArray(productDayTranslations.dayId, sourceDayIds));
|
|
302
|
+
for (const row of dayTranslationRows) {
|
|
303
|
+
const targetDayId = dayIdMap.get(row.dayId);
|
|
304
|
+
if (!targetDayId)
|
|
305
|
+
continue;
|
|
306
|
+
await tx
|
|
307
|
+
.insert(productDayTranslations)
|
|
308
|
+
.values({ ...withoutSystemColumns(row), dayId: targetDayId });
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async function copyMedia(ctx) {
|
|
312
|
+
const { tx, sourceId, targetId, dayIdMap } = ctx;
|
|
313
|
+
const mediaRows = await tx.select().from(productMedia).where(eq(productMedia.productId, sourceId));
|
|
314
|
+
for (const row of mediaRows) {
|
|
315
|
+
if (row.isBrochure)
|
|
316
|
+
continue;
|
|
317
|
+
const targetDayId = row.dayId ? dayIdMap.get(row.dayId) : null;
|
|
318
|
+
if (row.dayId && !targetDayId)
|
|
319
|
+
continue;
|
|
320
|
+
await tx.insert(productMedia).values({
|
|
321
|
+
...withoutSystemColumns(row),
|
|
322
|
+
productId: targetId,
|
|
323
|
+
dayId: targetDayId,
|
|
324
|
+
isBrochure: false,
|
|
325
|
+
isBrochureCurrent: false,
|
|
326
|
+
brochureVersion: null,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
async function copyExtras(ctx) {
|
|
331
|
+
const { tx, sourceId, targetId, optionIdMap, productExtraIdMap, optionExtraConfigIdMap } = ctx;
|
|
332
|
+
const extraRows = await tx
|
|
333
|
+
.select()
|
|
334
|
+
.from(productExtras)
|
|
335
|
+
.where(eq(productExtras.productId, sourceId));
|
|
336
|
+
for (const row of extraRows) {
|
|
337
|
+
const [copy] = await tx
|
|
338
|
+
.insert(productExtras)
|
|
339
|
+
.values({ ...withoutSystemColumns(row), productId: targetId })
|
|
340
|
+
.returning();
|
|
341
|
+
if (copy)
|
|
342
|
+
productExtraIdMap.set(row.id, copy.id);
|
|
343
|
+
}
|
|
344
|
+
const sourceOptionIds = [...optionIdMap.keys()];
|
|
345
|
+
if (sourceOptionIds.length === 0)
|
|
346
|
+
return;
|
|
347
|
+
const extraConfigRows = await tx
|
|
348
|
+
.select()
|
|
349
|
+
.from(optionExtraConfigs)
|
|
350
|
+
.where(inArray(optionExtraConfigs.optionId, sourceOptionIds));
|
|
351
|
+
for (const row of extraConfigRows) {
|
|
352
|
+
const targetOptionId = optionIdMap.get(row.optionId);
|
|
353
|
+
const targetProductExtraId = productExtraIdMap.get(row.productExtraId);
|
|
354
|
+
if (!targetOptionId || !targetProductExtraId)
|
|
355
|
+
continue;
|
|
356
|
+
const [copy] = await tx
|
|
357
|
+
.insert(optionExtraConfigs)
|
|
358
|
+
.values({
|
|
359
|
+
...withoutSystemColumns(row),
|
|
360
|
+
optionId: targetOptionId,
|
|
361
|
+
productExtraId: targetProductExtraId,
|
|
362
|
+
})
|
|
363
|
+
.returning();
|
|
364
|
+
if (copy)
|
|
365
|
+
optionExtraConfigIdMap.set(row.id, copy.id);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type CloneContext } from "./clone-content.js";
|
|
2
|
+
/**
|
|
3
|
+
* Copies availability (when `copyDepartures`) and the full pricing-rule graph —
|
|
4
|
+
* option price rules → unit price rules (with remapped pricing categories) →
|
|
5
|
+
* tiers, plus start-time / pickup / dropoff / extra rules and departure
|
|
6
|
+
* overrides. Relies on the id remaps populated by {@link copyProductContent}.
|
|
7
|
+
*/
|
|
8
|
+
export declare function copyPricingAndAvailability(ctx: CloneContext): Promise<void>;
|
|
9
|
+
//# sourceMappingURL=clone-pricing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clone-pricing.d.ts","sourceRoot":"","sources":["../src/clone-pricing.ts"],"names":[],"mappings":"AAgBA,OAAO,EAAE,KAAK,YAAY,EAAwB,MAAM,oBAAoB,CAAA;AAE5E;;;;;GAKG;AACH,wBAAsB,0BAA0B,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAQjF"}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { availabilityRules, availabilitySlots, availabilityStartTimes, } from "@voyantjs/availability";
|
|
2
|
+
import { departurePriceOverrides, dropoffPriceRules, extraPriceRules, optionPriceRules, optionStartTimeRules, optionUnitPriceRules, optionUnitTiers, pickupPriceRules, } from "@voyantjs/pricing/schema";
|
|
3
|
+
import { eq, inArray } from "drizzle-orm";
|
|
4
|
+
import { withoutSystemColumns } from "./clone-content.js";
|
|
5
|
+
/**
|
|
6
|
+
* Copies availability (when `copyDepartures`) and the full pricing-rule graph —
|
|
7
|
+
* option price rules → unit price rules (with remapped pricing categories) →
|
|
8
|
+
* tiers, plus start-time / pickup / dropoff / extra rules and departure
|
|
9
|
+
* overrides. Relies on the id remaps populated by {@link copyProductContent}.
|
|
10
|
+
*/
|
|
11
|
+
export async function copyPricingAndAvailability(ctx) {
|
|
12
|
+
if (ctx.copyDepartures) {
|
|
13
|
+
await copyAvailability(ctx);
|
|
14
|
+
}
|
|
15
|
+
await copyPriceRules(ctx);
|
|
16
|
+
if (ctx.copyDepartures && ctx.slotIdMap.size > 0) {
|
|
17
|
+
await copyDepartureOverrides(ctx);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function copyAvailability(ctx) {
|
|
21
|
+
const { tx, sourceId, targetId, optionIdMap, itineraryIdMap, startTimeIdMap, ruleIdMap, slotIdMap, } = ctx;
|
|
22
|
+
const startTimeRows = await tx
|
|
23
|
+
.select()
|
|
24
|
+
.from(availabilityStartTimes)
|
|
25
|
+
.where(eq(availabilityStartTimes.productId, sourceId));
|
|
26
|
+
for (const row of startTimeRows) {
|
|
27
|
+
const [copy] = await tx
|
|
28
|
+
.insert(availabilityStartTimes)
|
|
29
|
+
.values({
|
|
30
|
+
...withoutSystemColumns(row),
|
|
31
|
+
productId: targetId,
|
|
32
|
+
optionId: row.optionId ? (optionIdMap.get(row.optionId) ?? null) : null,
|
|
33
|
+
})
|
|
34
|
+
.returning();
|
|
35
|
+
if (copy)
|
|
36
|
+
startTimeIdMap.set(row.id, copy.id);
|
|
37
|
+
}
|
|
38
|
+
const availabilityRuleRows = await tx
|
|
39
|
+
.select()
|
|
40
|
+
.from(availabilityRules)
|
|
41
|
+
.where(eq(availabilityRules.productId, sourceId));
|
|
42
|
+
for (const row of availabilityRuleRows) {
|
|
43
|
+
const [copy] = await tx
|
|
44
|
+
.insert(availabilityRules)
|
|
45
|
+
.values({
|
|
46
|
+
...withoutSystemColumns(row),
|
|
47
|
+
productId: targetId,
|
|
48
|
+
optionId: row.optionId ? (optionIdMap.get(row.optionId) ?? null) : null,
|
|
49
|
+
})
|
|
50
|
+
.returning();
|
|
51
|
+
if (copy)
|
|
52
|
+
ruleIdMap.set(row.id, copy.id);
|
|
53
|
+
}
|
|
54
|
+
const slotRows = await tx
|
|
55
|
+
.select()
|
|
56
|
+
.from(availabilitySlots)
|
|
57
|
+
.where(eq(availabilitySlots.productId, sourceId));
|
|
58
|
+
for (const row of slotRows) {
|
|
59
|
+
const targetItineraryId = row.itineraryId ? (itineraryIdMap.get(row.itineraryId) ?? null) : null;
|
|
60
|
+
const targetOptionId = row.optionId ? (optionIdMap.get(row.optionId) ?? null) : null;
|
|
61
|
+
const targetRuleId = row.availabilityRuleId
|
|
62
|
+
? (ruleIdMap.get(row.availabilityRuleId) ?? null)
|
|
63
|
+
: null;
|
|
64
|
+
const targetStartTimeId = row.startTimeId ? (startTimeIdMap.get(row.startTimeId) ?? null) : null;
|
|
65
|
+
const [copy] = await tx
|
|
66
|
+
.insert(availabilitySlots)
|
|
67
|
+
.values({
|
|
68
|
+
...withoutSystemColumns(row),
|
|
69
|
+
productId: targetId,
|
|
70
|
+
itineraryId: targetItineraryId,
|
|
71
|
+
optionId: targetOptionId,
|
|
72
|
+
availabilityRuleId: targetRuleId,
|
|
73
|
+
startTimeId: targetStartTimeId,
|
|
74
|
+
remainingPax: row.initialPax,
|
|
75
|
+
remainingPickups: row.initialPickups,
|
|
76
|
+
})
|
|
77
|
+
.returning();
|
|
78
|
+
if (copy)
|
|
79
|
+
slotIdMap.set(row.id, copy.id);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function copyPriceRules(ctx) {
|
|
83
|
+
const { tx, targetId, optionIdMap, unitIdMap, startTimeIdMap, optionPriceRuleIdMap, optionUnitPriceRuleIdMap, pricingCategoryIdMap, productExtraIdMap, optionExtraConfigIdMap, } = ctx;
|
|
84
|
+
const sourceOptionIds = [...optionIdMap.keys()];
|
|
85
|
+
if (sourceOptionIds.length === 0)
|
|
86
|
+
return;
|
|
87
|
+
const optionPriceRuleRows = await tx
|
|
88
|
+
.select()
|
|
89
|
+
.from(optionPriceRules)
|
|
90
|
+
.where(inArray(optionPriceRules.optionId, sourceOptionIds));
|
|
91
|
+
for (const row of optionPriceRuleRows) {
|
|
92
|
+
const targetOptionId = optionIdMap.get(row.optionId);
|
|
93
|
+
if (!targetOptionId)
|
|
94
|
+
continue;
|
|
95
|
+
const [copy] = await tx
|
|
96
|
+
.insert(optionPriceRules)
|
|
97
|
+
.values({ ...withoutSystemColumns(row), productId: targetId, optionId: targetOptionId })
|
|
98
|
+
.returning();
|
|
99
|
+
if (copy)
|
|
100
|
+
optionPriceRuleIdMap.set(row.id, copy.id);
|
|
101
|
+
}
|
|
102
|
+
const sourceOptionPriceRuleIds = optionPriceRuleRows.map((row) => row.id);
|
|
103
|
+
if (sourceOptionPriceRuleIds.length === 0)
|
|
104
|
+
return;
|
|
105
|
+
const optionUnitPriceRuleRows = await tx
|
|
106
|
+
.select()
|
|
107
|
+
.from(optionUnitPriceRules)
|
|
108
|
+
.where(inArray(optionUnitPriceRules.optionPriceRuleId, sourceOptionPriceRuleIds));
|
|
109
|
+
for (const row of optionUnitPriceRuleRows) {
|
|
110
|
+
const targetPriceRuleId = optionPriceRuleIdMap.get(row.optionPriceRuleId);
|
|
111
|
+
const targetOptionId = optionIdMap.get(row.optionId);
|
|
112
|
+
const targetUnitId = unitIdMap.get(row.unitId);
|
|
113
|
+
const targetPricingCategoryId = row.pricingCategoryId
|
|
114
|
+
? (pricingCategoryIdMap.get(row.pricingCategoryId) ?? null)
|
|
115
|
+
: null;
|
|
116
|
+
if (!targetPriceRuleId || !targetOptionId || !targetUnitId)
|
|
117
|
+
continue;
|
|
118
|
+
if (row.pricingCategoryId && !targetPricingCategoryId)
|
|
119
|
+
continue;
|
|
120
|
+
const [copy] = await tx
|
|
121
|
+
.insert(optionUnitPriceRules)
|
|
122
|
+
.values({
|
|
123
|
+
...withoutSystemColumns(row),
|
|
124
|
+
optionPriceRuleId: targetPriceRuleId,
|
|
125
|
+
optionId: targetOptionId,
|
|
126
|
+
unitId: targetUnitId,
|
|
127
|
+
pricingCategoryId: targetPricingCategoryId,
|
|
128
|
+
})
|
|
129
|
+
.returning();
|
|
130
|
+
if (copy)
|
|
131
|
+
optionUnitPriceRuleIdMap.set(row.id, copy.id);
|
|
132
|
+
}
|
|
133
|
+
const sourceOptionUnitPriceRuleIds = optionUnitPriceRuleRows.map((row) => row.id);
|
|
134
|
+
if (sourceOptionUnitPriceRuleIds.length > 0) {
|
|
135
|
+
const tierRows = await tx
|
|
136
|
+
.select()
|
|
137
|
+
.from(optionUnitTiers)
|
|
138
|
+
.where(inArray(optionUnitTiers.optionUnitPriceRuleId, sourceOptionUnitPriceRuleIds));
|
|
139
|
+
for (const row of tierRows) {
|
|
140
|
+
const targetUnitPriceRuleId = optionUnitPriceRuleIdMap.get(row.optionUnitPriceRuleId);
|
|
141
|
+
if (!targetUnitPriceRuleId)
|
|
142
|
+
continue;
|
|
143
|
+
await tx
|
|
144
|
+
.insert(optionUnitTiers)
|
|
145
|
+
.values({ ...withoutSystemColumns(row), optionUnitPriceRuleId: targetUnitPriceRuleId });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
const startRuleRows = await tx
|
|
149
|
+
.select()
|
|
150
|
+
.from(optionStartTimeRules)
|
|
151
|
+
.where(inArray(optionStartTimeRules.optionPriceRuleId, sourceOptionPriceRuleIds));
|
|
152
|
+
for (const row of startRuleRows) {
|
|
153
|
+
const targetPriceRuleId = optionPriceRuleIdMap.get(row.optionPriceRuleId);
|
|
154
|
+
const targetOptionId = optionIdMap.get(row.optionId);
|
|
155
|
+
const targetStartTimeId = startTimeIdMap.get(row.startTimeId);
|
|
156
|
+
if (!targetPriceRuleId || !targetOptionId || !targetStartTimeId)
|
|
157
|
+
continue;
|
|
158
|
+
await tx.insert(optionStartTimeRules).values({
|
|
159
|
+
...withoutSystemColumns(row),
|
|
160
|
+
optionPriceRuleId: targetPriceRuleId,
|
|
161
|
+
optionId: targetOptionId,
|
|
162
|
+
startTimeId: targetStartTimeId,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
const pickupPriceRuleRows = await tx
|
|
166
|
+
.select()
|
|
167
|
+
.from(pickupPriceRules)
|
|
168
|
+
.where(inArray(pickupPriceRules.optionPriceRuleId, sourceOptionPriceRuleIds));
|
|
169
|
+
for (const row of pickupPriceRuleRows) {
|
|
170
|
+
const targetPriceRuleId = optionPriceRuleIdMap.get(row.optionPriceRuleId);
|
|
171
|
+
const targetOptionId = optionIdMap.get(row.optionId);
|
|
172
|
+
if (!targetPriceRuleId || !targetOptionId)
|
|
173
|
+
continue;
|
|
174
|
+
await tx.insert(pickupPriceRules).values({
|
|
175
|
+
...withoutSystemColumns(row),
|
|
176
|
+
optionPriceRuleId: targetPriceRuleId,
|
|
177
|
+
optionId: targetOptionId,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
const dropoffPriceRuleRows = await tx
|
|
181
|
+
.select()
|
|
182
|
+
.from(dropoffPriceRules)
|
|
183
|
+
.where(inArray(dropoffPriceRules.optionPriceRuleId, sourceOptionPriceRuleIds));
|
|
184
|
+
for (const row of dropoffPriceRuleRows) {
|
|
185
|
+
const targetPriceRuleId = optionPriceRuleIdMap.get(row.optionPriceRuleId);
|
|
186
|
+
const targetOptionId = optionIdMap.get(row.optionId);
|
|
187
|
+
if (!targetPriceRuleId || !targetOptionId)
|
|
188
|
+
continue;
|
|
189
|
+
await tx.insert(dropoffPriceRules).values({
|
|
190
|
+
...withoutSystemColumns(row),
|
|
191
|
+
optionPriceRuleId: targetPriceRuleId,
|
|
192
|
+
optionId: targetOptionId,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
const extraPriceRuleRows = await tx
|
|
196
|
+
.select()
|
|
197
|
+
.from(extraPriceRules)
|
|
198
|
+
.where(inArray(extraPriceRules.optionPriceRuleId, sourceOptionPriceRuleIds));
|
|
199
|
+
for (const row of extraPriceRuleRows) {
|
|
200
|
+
const targetPriceRuleId = optionPriceRuleIdMap.get(row.optionPriceRuleId);
|
|
201
|
+
const targetOptionId = optionIdMap.get(row.optionId);
|
|
202
|
+
const targetProductExtraId = row.productExtraId
|
|
203
|
+
? (productExtraIdMap.get(row.productExtraId) ?? null)
|
|
204
|
+
: null;
|
|
205
|
+
const targetOptionExtraConfigId = row.optionExtraConfigId
|
|
206
|
+
? (optionExtraConfigIdMap.get(row.optionExtraConfigId) ?? null)
|
|
207
|
+
: null;
|
|
208
|
+
if (!targetPriceRuleId || !targetOptionId)
|
|
209
|
+
continue;
|
|
210
|
+
if (row.productExtraId && !targetProductExtraId)
|
|
211
|
+
continue;
|
|
212
|
+
if (row.optionExtraConfigId && !targetOptionExtraConfigId)
|
|
213
|
+
continue;
|
|
214
|
+
await tx.insert(extraPriceRules).values({
|
|
215
|
+
...withoutSystemColumns(row),
|
|
216
|
+
optionPriceRuleId: targetPriceRuleId,
|
|
217
|
+
optionId: targetOptionId,
|
|
218
|
+
productExtraId: targetProductExtraId,
|
|
219
|
+
optionExtraConfigId: targetOptionExtraConfigId,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
async function copyDepartureOverrides(ctx) {
|
|
224
|
+
const { tx, optionIdMap, unitIdMap, slotIdMap } = ctx;
|
|
225
|
+
const overrideRows = await tx
|
|
226
|
+
.select()
|
|
227
|
+
.from(departurePriceOverrides)
|
|
228
|
+
.where(inArray(departurePriceOverrides.departureId, [...slotIdMap.keys()]));
|
|
229
|
+
for (const row of overrideRows) {
|
|
230
|
+
const targetDepartureId = slotIdMap.get(row.departureId);
|
|
231
|
+
const targetOptionId = optionIdMap.get(row.optionId);
|
|
232
|
+
const targetUnitId = unitIdMap.get(row.optionUnitId);
|
|
233
|
+
if (!targetDepartureId || !targetOptionId || !targetUnitId)
|
|
234
|
+
continue;
|
|
235
|
+
await tx.insert(departurePriceOverrides).values({
|
|
236
|
+
...withoutSystemColumns(row),
|
|
237
|
+
departureId: targetDepartureId,
|
|
238
|
+
optionId: targetOptionId,
|
|
239
|
+
optionUnitId: targetUnitId,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
}
|
package/dist/clone.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { products } from "@voyantjs/products/schema";
|
|
2
|
+
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
3
|
+
type Product = typeof products.$inferSelect;
|
|
4
|
+
export interface CloneProductOptions {
|
|
5
|
+
/** New product name. Defaults to `"{source} (Copy)"` (preserves the UI's no-body call). */
|
|
6
|
+
name?: string;
|
|
7
|
+
/** New product status. Defaults to `draft`. */
|
|
8
|
+
status?: Product["status"];
|
|
9
|
+
/** New product visibility. Defaults to the source's. */
|
|
10
|
+
visibility?: Product["visibility"];
|
|
11
|
+
/**
|
|
12
|
+
* Copy availability slots/rules/start-times + departure price overrides.
|
|
13
|
+
* Defaults to `true` (the operator UI clones a full working copy). The Max AI
|
|
14
|
+
* agent passes `false` per #1493 — departures are date-specific, so the clone
|
|
15
|
+
* starts with none and the agent adds fresh ones.
|
|
16
|
+
*/
|
|
17
|
+
copyDepartures?: boolean;
|
|
18
|
+
/** Author of the initial `product_versions` snapshot. When omitted, no snapshot is taken. */
|
|
19
|
+
userId?: string;
|
|
20
|
+
/** Dedup key; a retried request with the same key returns the first clone. */
|
|
21
|
+
idempotencyKey?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface ClonedOption {
|
|
24
|
+
id: string;
|
|
25
|
+
units: {
|
|
26
|
+
id: string;
|
|
27
|
+
}[];
|
|
28
|
+
}
|
|
29
|
+
export type CloneProductOutcome = {
|
|
30
|
+
status: "ok";
|
|
31
|
+
product: Product;
|
|
32
|
+
options: ClonedOption[];
|
|
33
|
+
reused: boolean;
|
|
34
|
+
} | {
|
|
35
|
+
status: "not_found";
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Deep-clone a product (#1493). Wraps {@link cloneGraph} with idempotency and an
|
|
39
|
+
* optional `product_versions` snapshot. The operator UI calls this with no
|
|
40
|
+
* overrides (full copy, `"{X} (Copy)"`, departures included); the agent passes
|
|
41
|
+
* `name` + `copyDepartures: false` + an `Idempotency-Key`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function cloneProduct(db: PostgresJsDatabase, sourceProductId: string, options?: CloneProductOptions): Promise<CloneProductOutcome>;
|
|
44
|
+
export {};
|
|
45
|
+
//# sourceMappingURL=clone.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clone.d.ts","sourceRoot":"","sources":["../src/clone.ts"],"names":[],"mappings":"AACA,OAAO,EAA+B,QAAQ,EAAE,MAAM,2BAA2B,CAAA;AAEjF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAKjE,KAAK,OAAO,GAAG,OAAO,QAAQ,CAAC,YAAY,CAAA;AAE3C,MAAM,WAAW,mBAAmB;IAClC,2FAA2F;IAC3F,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,+CAA+C;IAC/C,MAAM,CAAC,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC1B,wDAAwD;IACxD,UAAU,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAA;IAClC;;;;;OAKG;IACH,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB,6FAA6F;IAC7F,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,8EAA8E;IAC9E,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CACxB;AAED,MAAM,MAAM,mBAAmB,GAC3B;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,YAAY,EAAE,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAC5E;IAAE,MAAM,EAAE,WAAW,CAAA;CAAE,CAAA;AA+G3B;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,EAAE,EAAE,kBAAkB,EACtB,eAAe,EAAE,MAAM,EACvB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,mBAAmB,CAAC,CAoD9B"}
|
package/dist/clone.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { productsService } from "@voyantjs/products";
|
|
2
|
+
import { optionUnits, productOptions, products } from "@voyantjs/products/schema";
|
|
3
|
+
import { eq, inArray, sql } from "drizzle-orm";
|
|
4
|
+
import { copyProductContent, withoutSystemColumns } from "./clone-content.js";
|
|
5
|
+
import { copyPricingAndAvailability } from "./clone-pricing.js";
|
|
6
|
+
import { productAuthoringRequests } from "./schema.js";
|
|
7
|
+
function copyName(name) {
|
|
8
|
+
return `${name} (Copy)`;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Reads a product's options + units back into the {@link ClonedOption} shape.
|
|
12
|
+
* Used to reconstruct the result for an idempotent retry, so a re-sent clone
|
|
13
|
+
* returns the same option/unit ids a fresh response would (the agent needs them
|
|
14
|
+
* to continue authoring after the exact lost-response case idempotency covers).
|
|
15
|
+
*/
|
|
16
|
+
async function loadClonedOptions(tx, productId) {
|
|
17
|
+
const optionRows = await tx
|
|
18
|
+
.select({ id: productOptions.id })
|
|
19
|
+
.from(productOptions)
|
|
20
|
+
.where(eq(productOptions.productId, productId));
|
|
21
|
+
if (optionRows.length === 0)
|
|
22
|
+
return [];
|
|
23
|
+
const unitRows = await tx
|
|
24
|
+
.select({ id: optionUnits.id, optionId: optionUnits.optionId })
|
|
25
|
+
.from(optionUnits)
|
|
26
|
+
.where(inArray(optionUnits.optionId, optionRows.map((o) => o.id)));
|
|
27
|
+
return optionRows.map((o) => ({
|
|
28
|
+
id: o.id,
|
|
29
|
+
units: unitRows.filter((u) => u.optionId === o.id).map((u) => ({ id: u.id })),
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
32
|
+
function newContext(tx, sourceId, targetId, copyDepartures) {
|
|
33
|
+
return {
|
|
34
|
+
tx,
|
|
35
|
+
sourceId,
|
|
36
|
+
targetId,
|
|
37
|
+
copyDepartures,
|
|
38
|
+
optionIdMap: new Map(),
|
|
39
|
+
unitIdMap: new Map(),
|
|
40
|
+
unitsByNewOption: new Map(),
|
|
41
|
+
itineraryIdMap: new Map(),
|
|
42
|
+
dayIdMap: new Map(),
|
|
43
|
+
startTimeIdMap: new Map(),
|
|
44
|
+
ruleIdMap: new Map(),
|
|
45
|
+
slotIdMap: new Map(),
|
|
46
|
+
optionPriceRuleIdMap: new Map(),
|
|
47
|
+
optionUnitPriceRuleIdMap: new Map(),
|
|
48
|
+
pricingCategoryIdMap: new Map(),
|
|
49
|
+
productExtraIdMap: new Map(),
|
|
50
|
+
optionExtraConfigIdMap: new Map(),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Deep-clone a product graph as a draft (#1493): the new product row, then its
|
|
55
|
+
* content (options/units, pricing categories, itinerary, media, extras) and its
|
|
56
|
+
* pricing/availability — copied with correct id remapping. Availability is
|
|
57
|
+
* copied only when `copyDepartures`. MUST run inside the caller's transaction.
|
|
58
|
+
*/
|
|
59
|
+
async function cloneGraph(tx, sourceId, opts) {
|
|
60
|
+
const [sourceProduct] = await tx.select().from(products).where(eq(products.id, sourceId));
|
|
61
|
+
if (!sourceProduct)
|
|
62
|
+
return null;
|
|
63
|
+
const [targetProduct] = await tx
|
|
64
|
+
.insert(products)
|
|
65
|
+
.values({
|
|
66
|
+
...withoutSystemColumns(sourceProduct),
|
|
67
|
+
name: opts.name ?? copyName(sourceProduct.name),
|
|
68
|
+
status: opts.status ?? "draft",
|
|
69
|
+
...(opts.visibility ? { visibility: opts.visibility } : {}),
|
|
70
|
+
activated: false,
|
|
71
|
+
})
|
|
72
|
+
.returning();
|
|
73
|
+
if (!targetProduct) {
|
|
74
|
+
throw new Error("Failed to duplicate product");
|
|
75
|
+
}
|
|
76
|
+
const ctx = newContext(tx, sourceId, targetProduct.id, opts.copyDepartures);
|
|
77
|
+
await copyProductContent(ctx);
|
|
78
|
+
await copyPricingAndAvailability(ctx);
|
|
79
|
+
const options = [...ctx.optionIdMap.values()].map((id) => ({
|
|
80
|
+
id,
|
|
81
|
+
units: ctx.unitsByNewOption.get(id) ?? [],
|
|
82
|
+
}));
|
|
83
|
+
return { product: targetProduct, options };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Deep-clone a product (#1493). Wraps {@link cloneGraph} with idempotency and an
|
|
87
|
+
* optional `product_versions` snapshot. The operator UI calls this with no
|
|
88
|
+
* overrides (full copy, `"{X} (Copy)"`, departures included); the agent passes
|
|
89
|
+
* `name` + `copyDepartures: false` + an `Idempotency-Key`.
|
|
90
|
+
*/
|
|
91
|
+
export async function cloneProduct(db, sourceProductId, options = {}) {
|
|
92
|
+
const [exists] = await db
|
|
93
|
+
.select({ id: products.id })
|
|
94
|
+
.from(products)
|
|
95
|
+
.where(eq(products.id, sourceProductId))
|
|
96
|
+
.limit(1);
|
|
97
|
+
if (!exists)
|
|
98
|
+
return { status: "not_found" };
|
|
99
|
+
const copyDepartures = options.copyDepartures ?? true;
|
|
100
|
+
const key = options.idempotencyKey;
|
|
101
|
+
const result = await db.transaction(async (tx) => {
|
|
102
|
+
if (key) {
|
|
103
|
+
await tx.execute(sql `SELECT pg_advisory_xact_lock(hashtextextended(${key}, 0))`);
|
|
104
|
+
const [prev] = await tx
|
|
105
|
+
.select({ productId: productAuthoringRequests.productId })
|
|
106
|
+
.from(productAuthoringRequests)
|
|
107
|
+
.where(eq(productAuthoringRequests.idempotencyKey, key))
|
|
108
|
+
.limit(1);
|
|
109
|
+
if (prev) {
|
|
110
|
+
const [product] = await tx
|
|
111
|
+
.select()
|
|
112
|
+
.from(products)
|
|
113
|
+
.where(eq(products.id, prev.productId))
|
|
114
|
+
.limit(1);
|
|
115
|
+
if (product) {
|
|
116
|
+
return { product, options: await loadClonedOptions(tx, product.id), reused: true };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const cloned = await cloneGraph(tx, sourceProductId, {
|
|
121
|
+
name: options.name,
|
|
122
|
+
status: options.status,
|
|
123
|
+
visibility: options.visibility,
|
|
124
|
+
copyDepartures,
|
|
125
|
+
});
|
|
126
|
+
if (!cloned)
|
|
127
|
+
return null;
|
|
128
|
+
if (options.userId) {
|
|
129
|
+
await productsService.createVersion(tx, cloned.product.id, options.userId, {});
|
|
130
|
+
}
|
|
131
|
+
if (key) {
|
|
132
|
+
await tx
|
|
133
|
+
.insert(productAuthoringRequests)
|
|
134
|
+
.values({ idempotencyKey: key, productId: cloned.product.id, operation: "duplicate" });
|
|
135
|
+
}
|
|
136
|
+
return { ...cloned, reused: false };
|
|
137
|
+
});
|
|
138
|
+
if (!result)
|
|
139
|
+
return { status: "not_found" };
|
|
140
|
+
return { status: "ok", ...result };
|
|
141
|
+
}
|
package/dist/extension.d.ts
CHANGED
|
@@ -3,18 +3,17 @@ import type { HonoExtension } from "@voyantjs/hono/module";
|
|
|
3
3
|
import type { PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
|
4
4
|
/**
|
|
5
5
|
* Catalog authoring rides on the `products` admin prefix as a HonoExtension, so
|
|
6
|
-
* its
|
|
6
|
+
* its routes land at `/v1/admin/products/...` without `packages/products`
|
|
7
7
|
* depending on this package (which would cycle, since this package depends on
|
|
8
8
|
* both products and pricing). Same mechanism as `bookingsSupplierExtension`.
|
|
9
9
|
*
|
|
10
|
-
* POST /v1/admin/products/
|
|
10
|
+
* POST /v1/admin/products/{id}/duplicate — deep-clone a product graph (#1493)
|
|
11
|
+
* POST /v1/admin/products/compose — build a new product graph from a spec (#1495)
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* `
|
|
15
|
-
*
|
|
16
|
-
* genuinely new, non-overlapping capability — it covers the cold-start /
|
|
17
|
-
* never-authored-before case that clone cannot.
|
|
13
|
+
* The duplicate route is the canonical product clone (it replaces the operator
|
|
14
|
+
* template's previous local `duplicateProductAsDraft` route). No body → a full
|
|
15
|
+
* copy named `"{X} (Copy)"` with departures (preserves the UI). The agent passes
|
|
16
|
+
* `{ name, copyDepartures: false }` + an `Idempotency-Key`.
|
|
18
17
|
*/
|
|
19
18
|
type Env = {
|
|
20
19
|
Variables: {
|
|
@@ -57,7 +56,82 @@ export declare const catalogAuthoringRoutes: import("hono/hono-base").HonoBase<E
|
|
|
57
56
|
status: 200 | 201;
|
|
58
57
|
};
|
|
59
58
|
};
|
|
60
|
-
}
|
|
59
|
+
} & {
|
|
60
|
+
"/:id/duplicate": {
|
|
61
|
+
$post: {
|
|
62
|
+
input: {
|
|
63
|
+
param: {
|
|
64
|
+
id: string;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
output: {
|
|
68
|
+
error: string;
|
|
69
|
+
};
|
|
70
|
+
outputFormat: "json";
|
|
71
|
+
status: 400;
|
|
72
|
+
} | {
|
|
73
|
+
input: {
|
|
74
|
+
param: {
|
|
75
|
+
id: string;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
output: {
|
|
79
|
+
error: string;
|
|
80
|
+
};
|
|
81
|
+
outputFormat: "json";
|
|
82
|
+
status: 404;
|
|
83
|
+
} | {
|
|
84
|
+
input: {
|
|
85
|
+
param: {
|
|
86
|
+
id: string;
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
output: {
|
|
90
|
+
data: {
|
|
91
|
+
id: string;
|
|
92
|
+
name: string;
|
|
93
|
+
createdAt: string;
|
|
94
|
+
updatedAt: string;
|
|
95
|
+
timezone: string | null;
|
|
96
|
+
sellAmountCents: number | null;
|
|
97
|
+
costAmountCents: number | null;
|
|
98
|
+
description: string | null;
|
|
99
|
+
facilityId: string | null;
|
|
100
|
+
status: "active" | "draft" | "archived";
|
|
101
|
+
startDate: string | null;
|
|
102
|
+
endDate: string | null;
|
|
103
|
+
inclusionsHtml: string | null;
|
|
104
|
+
exclusionsHtml: string | null;
|
|
105
|
+
termsHtml: string | null;
|
|
106
|
+
termsShowOnContract: boolean;
|
|
107
|
+
bookingMode: "other" | "date" | "date_time" | "open" | "stay" | "transfer" | "itinerary";
|
|
108
|
+
capacityMode: "on_request" | "free_sale" | "limited";
|
|
109
|
+
defaultLanguageTag: string | null;
|
|
110
|
+
visibility: "public" | "private" | "hidden";
|
|
111
|
+
activated: boolean;
|
|
112
|
+
reservationTimeoutMinutes: number | null;
|
|
113
|
+
sellCurrency: string;
|
|
114
|
+
marginPercent: number | null;
|
|
115
|
+
supplierId: string | null;
|
|
116
|
+
pax: number | null;
|
|
117
|
+
productTypeId: string | null;
|
|
118
|
+
contractTemplateId: string | null;
|
|
119
|
+
taxClassId: string | null;
|
|
120
|
+
customerPaymentPolicy: import("hono/utils/types").JSONValue;
|
|
121
|
+
tags: string[] | null;
|
|
122
|
+
};
|
|
123
|
+
options: {
|
|
124
|
+
id: string;
|
|
125
|
+
units: {
|
|
126
|
+
id: string;
|
|
127
|
+
}[];
|
|
128
|
+
}[];
|
|
129
|
+
};
|
|
130
|
+
outputFormat: "json";
|
|
131
|
+
status: 200 | 201;
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
}, "/", "/:id/duplicate">;
|
|
61
135
|
export declare const catalogAuthoringExtension: HonoExtension;
|
|
62
136
|
export {};
|
|
63
137
|
//# sourceMappingURL=extension.d.ts.map
|
package/dist/extension.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"extension.d.ts","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAa,MAAM,gBAAgB,CAAA;AAEzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;
|
|
1
|
+
{"version":3,"file":"extension.d.ts","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAa,MAAM,gBAAgB,CAAA;AAEzD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAA;AAO1D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AAQjE;;;;;;;;;;;;;GAaG;AAEH,KAAK,GAAG,GAAG;IACT,SAAS,EAAE;QACT,EAAE,EAAE,kBAAkB,CAAA;QACtB,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,QAAQ,CAAC,EAAE,QAAQ,CAAA;KACpB,CAAA;CACF,CAAA;AAmDD,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;yBA6D/B,CAAA;AAOJ,eAAO,MAAM,yBAAyB,EAAE,aAGvC,CAAA"}
|
package/dist/extension.js
CHANGED
|
@@ -1,37 +1,48 @@
|
|
|
1
1
|
import { parseJsonBody } from "@voyantjs/hono";
|
|
2
|
-
import { appendProductMutationLedgerEntry, emitProductContentChanged } from "@voyantjs/products";
|
|
2
|
+
import { appendProductMutationLedgerEntry, emitProductContentChanged, } from "@voyantjs/products";
|
|
3
|
+
import { productStatusEnum, productVisibilityEnum } from "@voyantjs/products/schema";
|
|
3
4
|
import { Hono } from "hono";
|
|
4
5
|
import { z } from "zod";
|
|
6
|
+
import { cloneProduct } from "./clone.js";
|
|
5
7
|
import { composeProduct } from "./service.js";
|
|
6
8
|
import { productGraphSpecSchema } from "./spec.js";
|
|
7
9
|
const composeBodySchema = z.object({
|
|
8
10
|
spec: productGraphSpecSchema,
|
|
9
11
|
idempotencyKey: z.string().min(1).max(255).optional(),
|
|
10
12
|
});
|
|
13
|
+
const duplicateBodySchema = z.object({
|
|
14
|
+
name: z.string().min(1).max(255).optional(),
|
|
15
|
+
status: z.enum(productStatusEnum.enumValues).optional(),
|
|
16
|
+
visibility: z.enum(productVisibilityEnum.enumValues).optional(),
|
|
17
|
+
copyDepartures: z.boolean().optional(),
|
|
18
|
+
idempotencyKey: z.string().min(1).max(255).optional(),
|
|
19
|
+
});
|
|
11
20
|
/** Header takes precedence over a body-supplied key. */
|
|
12
21
|
function idempotencyKey(c, bodyKey) {
|
|
13
22
|
return c.req.header("Idempotency-Key") ?? bodyKey;
|
|
14
23
|
}
|
|
15
24
|
/**
|
|
16
25
|
* Records the same action-ledger entry + `product.content.changed` event the
|
|
17
|
-
* granular product routes emit, so
|
|
26
|
+
* granular product routes emit, so an authored product is indexed and audited
|
|
18
27
|
* like any other create. Only called for freshly built products (a reused
|
|
19
28
|
* idempotent response created nothing new).
|
|
20
29
|
*/
|
|
21
|
-
async function
|
|
22
|
-
// biome-ignore lint/suspicious/noExplicitAny: bridges this extension's Env to products' ledger Context<Env> (#1495); cast to LedgerContext below
|
|
23
|
-
c, productId) {
|
|
30
|
+
async function recordAuthoring(
|
|
31
|
+
// biome-ignore lint/suspicious/noExplicitAny: bridges this extension's Env to products' ledger Context<Env> (#1493/#1495); cast to LedgerContext below
|
|
32
|
+
c, action, productId) {
|
|
33
|
+
const verb = action === "duplicate" ? "duplicate" : "compose";
|
|
24
34
|
await appendProductMutationLedgerEntry(c, {
|
|
25
|
-
action
|
|
35
|
+
action,
|
|
26
36
|
productId,
|
|
27
37
|
changedFields: [],
|
|
28
38
|
subject: "product",
|
|
29
|
-
actionName:
|
|
30
|
-
routeOrToolName:
|
|
39
|
+
actionName: `product.${verb}`,
|
|
40
|
+
routeOrToolName: `products.${verb}`,
|
|
31
41
|
});
|
|
32
42
|
await emitProductContentChanged(c.get("eventBus"), { id: productId, axis: "product" });
|
|
33
43
|
}
|
|
34
|
-
export const catalogAuthoringRoutes = new Hono()
|
|
44
|
+
export const catalogAuthoringRoutes = new Hono()
|
|
45
|
+
.post("/compose", async (c) => {
|
|
35
46
|
const body = await parseJsonBody(c, composeBodySchema);
|
|
36
47
|
const outcome = await composeProduct(c.get("db"), body.spec, {
|
|
37
48
|
userId: c.get("userId"),
|
|
@@ -41,9 +52,45 @@ export const catalogAuthoringRoutes = new Hono().post("/compose", async (c) => {
|
|
|
41
52
|
return c.json({ error: "invalid_product_graph", issues: outcome.issues }, 422);
|
|
42
53
|
}
|
|
43
54
|
if (!outcome.reused) {
|
|
44
|
-
await
|
|
55
|
+
await recordAuthoring(c, "create", outcome.result.productId);
|
|
45
56
|
}
|
|
46
57
|
return c.json({ data: { id: outcome.result.productId, options: outcome.result.options } }, outcome.reused ? 200 : 201);
|
|
58
|
+
})
|
|
59
|
+
.post("/:id/duplicate", async (c) => {
|
|
60
|
+
// The UI clones with no body; the agent sends overrides. Tolerate both.
|
|
61
|
+
const raw = (await c.req.text()).trim();
|
|
62
|
+
let body = {};
|
|
63
|
+
if (raw.length > 0) {
|
|
64
|
+
let json;
|
|
65
|
+
try {
|
|
66
|
+
json = JSON.parse(raw);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
70
|
+
}
|
|
71
|
+
const parsed = duplicateBodySchema.safeParse(json);
|
|
72
|
+
if (!parsed.success) {
|
|
73
|
+
return c.json({ error: "Invalid body", issues: parsed.error.issues }, 400);
|
|
74
|
+
}
|
|
75
|
+
body = parsed.data;
|
|
76
|
+
}
|
|
77
|
+
const outcome = await cloneProduct(c.get("db"), c.req.param("id"), {
|
|
78
|
+
name: body.name,
|
|
79
|
+
status: body.status,
|
|
80
|
+
visibility: body.visibility,
|
|
81
|
+
copyDepartures: body.copyDepartures,
|
|
82
|
+
userId: c.get("userId"),
|
|
83
|
+
idempotencyKey: idempotencyKey(c, body.idempotencyKey),
|
|
84
|
+
});
|
|
85
|
+
if (outcome.status === "not_found") {
|
|
86
|
+
return c.json({ error: "Product not found" }, 404);
|
|
87
|
+
}
|
|
88
|
+
if (!outcome.reused) {
|
|
89
|
+
await recordAuthoring(c, "duplicate", outcome.product.id);
|
|
90
|
+
}
|
|
91
|
+
// `data` stays the full product row (the UI reads `data.id`); `options`
|
|
92
|
+
// carries the cloned option/unit ids for agent follow-up calls.
|
|
93
|
+
return c.json({ data: outcome.product, options: outcome.options }, outcome.reused ? 200 : 201);
|
|
47
94
|
});
|
|
48
95
|
const catalogAuthoringExtensionDef = {
|
|
49
96
|
name: "catalog-authoring",
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Module } from "@voyantjs/core";
|
|
2
2
|
export declare const catalogAuthoringModule: Module;
|
|
3
3
|
export { type BuildProductGraphOptions, type BuildProductGraphResult, buildProductGraph, } from "./builder.js";
|
|
4
|
+
export { type ClonedOption, type CloneProductOptions, type CloneProductOutcome, cloneProduct, } from "./clone.js";
|
|
4
5
|
export { type AuthoringIssue, AuthoringValidationError } from "./errors.js";
|
|
5
6
|
export { catalogAuthoringExtension, catalogAuthoringRoutes } from "./extension.js";
|
|
6
7
|
export type { NewProductAuthoringRequest, ProductAuthoringRequest } from "./schema.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAE5C,eAAO,MAAM,sBAAsB,EAAE,MAEpC,CAAA;AAED,OAAO,EACL,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,EAC5B,iBAAiB,GAClB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,KAAK,cAAc,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAA;AAC3E,OAAO,EAAE,yBAAyB,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;AAClF,YAAY,EAAE,0BAA0B,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA;AACtF,OAAO,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAA;AACtD,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,cAAc,GACf,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,KAAK,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAA;AACzE,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAA;AAE5C,eAAO,MAAM,sBAAsB,EAAE,MAEpC,CAAA;AAED,OAAO,EACL,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,EAC5B,iBAAiB,GAClB,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,YAAY,GACb,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,KAAK,cAAc,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAA;AAC3E,OAAO,EAAE,yBAAyB,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAA;AAClF,YAAY,EAAE,0BAA0B,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAA;AACtF,OAAO,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAA;AACtD,OAAO,EACL,KAAK,mBAAmB,EACxB,KAAK,qBAAqB,EAC1B,cAAc,GACf,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,KAAK,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,WAAW,CAAA;AACzE,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export const catalogAuthoringModule = {
|
|
|
2
2
|
name: "catalog-authoring",
|
|
3
3
|
};
|
|
4
4
|
export { buildProductGraph, } from "./builder.js";
|
|
5
|
+
export { cloneProduct, } from "./clone.js";
|
|
5
6
|
export { AuthoringValidationError } from "./errors.js";
|
|
6
7
|
export { catalogAuthoringExtension, catalogAuthoringRoutes } from "./extension.js";
|
|
7
8
|
export { productAuthoringRequests } from "./schema.js";
|
package/dist/service.d.ts
CHANGED
|
@@ -21,9 +21,8 @@ export type ComposeProductOutcome = {
|
|
|
21
21
|
* caller can self-correct. Rules without a catalog fall back to the operator
|
|
22
22
|
* default.
|
|
23
23
|
*
|
|
24
|
-
* Cloning an existing product
|
|
25
|
-
*
|
|
26
|
-
* at `POST /v1/admin/products/{id}/duplicate`. See #1493.
|
|
24
|
+
* Cloning an existing product lives in `clone.ts` (`cloneProduct`); this module
|
|
25
|
+
* is the from-scratch path. See #1493 / #1495.
|
|
27
26
|
*/
|
|
28
27
|
export declare function composeProduct(db: PostgresJsDatabase, spec: ProductGraphSpec, options?: AuthoringRunOptions): Promise<ComposeProductOutcome>;
|
|
29
28
|
//# sourceMappingURL=service.d.ts.map
|
package/dist/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AACjE,OAAO,EAAE,KAAK,uBAAuB,EAAqB,MAAM,cAAc,CAAA;AAC9E,OAAO,EAAE,KAAK,cAAc,EAA4B,MAAM,aAAa,CAAA;AAE3E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AAGjD,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,+EAA+E;IAC/E,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,MAAM,qBAAqB,GAC7B;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,uBAAuB,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAClE;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,cAAc,EAAE,CAAA;CAAE,CAAA;AAgDnD
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAA;AACjE,OAAO,EAAE,KAAK,uBAAuB,EAAqB,MAAM,cAAc,CAAA;AAC9E,OAAO,EAAE,KAAK,cAAc,EAA4B,MAAM,aAAa,CAAA;AAE3E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAA;AAGjD,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,+EAA+E;IAC/E,cAAc,CAAC,EAAE,MAAM,CAAA;CACxB;AAED,MAAM,MAAM,qBAAqB,GAC7B;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,uBAAuB,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,GAClE;IAAE,MAAM,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,cAAc,EAAE,CAAA;CAAE,CAAA;AAgDnD;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAClC,EAAE,EAAE,kBAAkB,EACtB,IAAI,EAAE,gBAAgB,EACtB,OAAO,GAAE,mBAAwB,GAChC,OAAO,CAAC,qBAAqB,CAAC,CAmBhC"}
|
package/dist/service.js
CHANGED
|
@@ -44,9 +44,8 @@ async function withIdempotency(tx, key, operation, build) {
|
|
|
44
44
|
* caller can self-correct. Rules without a catalog fall back to the operator
|
|
45
45
|
* default.
|
|
46
46
|
*
|
|
47
|
-
* Cloning an existing product
|
|
48
|
-
*
|
|
49
|
-
* at `POST /v1/admin/products/{id}/duplicate`. See #1493.
|
|
47
|
+
* Cloning an existing product lives in `clone.ts` (`cloneProduct`); this module
|
|
48
|
+
* is the from-scratch path. See #1493 / #1495.
|
|
50
49
|
*/
|
|
51
50
|
export async function composeProduct(db, spec, options = {}) {
|
|
52
51
|
const issues = validateProductGraph(spec);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@voyantjs/catalog-authoring",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.106.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -29,11 +29,13 @@
|
|
|
29
29
|
"drizzle-orm": "^0.45.2",
|
|
30
30
|
"hono": "^4.12.10",
|
|
31
31
|
"zod": "^4.3.6",
|
|
32
|
+
"@voyantjs/availability": "^0.104.1",
|
|
32
33
|
"@voyantjs/core": "^0.104.1",
|
|
33
34
|
"@voyantjs/db": "^0.104.1",
|
|
35
|
+
"@voyantjs/extras": "^0.105.0",
|
|
34
36
|
"@voyantjs/hono": "^0.104.1",
|
|
35
|
-
"@voyantjs/pricing": "^0.
|
|
36
|
-
"@voyantjs/products": "^0.
|
|
37
|
+
"@voyantjs/pricing": "^0.105.0",
|
|
38
|
+
"@voyantjs/products": "^0.105.0"
|
|
37
39
|
},
|
|
38
40
|
"devDependencies": {
|
|
39
41
|
"typescript": "^6.0.2",
|
|
@@ -56,7 +58,9 @@
|
|
|
56
58
|
"requiresSchemas": [
|
|
57
59
|
"@voyantjs/db",
|
|
58
60
|
"@voyantjs/products",
|
|
59
|
-
"@voyantjs/pricing"
|
|
61
|
+
"@voyantjs/pricing",
|
|
62
|
+
"@voyantjs/availability",
|
|
63
|
+
"@voyantjs/extras"
|
|
60
64
|
]
|
|
61
65
|
},
|
|
62
66
|
"scripts": {
|