riverbankcms 0.19.0 → 0.20.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 +23 -0
- package/dist/acceptance/matrix.mjs +3 -3
- package/dist/cli/index.mjs +2291 -105791
- package/dist/cli/init-docs/content/workflow-content-types.md +25 -0
- package/dist/entryCommands-8kDjOex0.mjs +111695 -0
- package/dist/{eventUpsertInputSchema-Bkafrrjw.mjs → eventUpsertInputSchema-VVBVGA6x.mjs} +333 -547
- package/dist/load-config-Cut7_6t_.mjs +4 -0
- package/dist/{load-config-CErro6mj.mjs → load-config-gqUEjIyB.mjs} +2 -2
- package/dist/{plan-DW5O751d.mjs → plan-HAEMHc5T.mjs} +1 -1
- package/dist/{sdk-event-upsert-BdvfnCyV.mjs → richTextSchema-DTvrDDF5.mjs} +1300 -165
- package/dist/scaffold/index.mjs +2 -2
- package/dist/{src-yL0DBH7S.mjs → src-Dixcv4PB.mjs} +109 -15
- package/dist/testing.mjs +5 -4
- package/package.json +1 -1
- package/dist/load-config-rPaisrdG.mjs +0 -4
- package/dist/local-sync-CWLEhMZ-.mjs +0 -4635
|
@@ -1,4635 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { Ar as asAppearancePresetId, Nr as asPageFlowPresetId, Pr as asSiteStyleId, Qn as buildMediaIdentifierFromFilename, T as eventBookingOfferIntentSchema, ar as getExtensionForMimeType, cr as normalizeAmbiguousStoragePathForSite, d as stableStringify, dn as footerPreferencesSchema, dr as MAX_MANAGEMENT_MEDIA_UPLOAD_BYTES, en as buttonStyleSelectionSchema, ln as pageDesignBoundaryTreatmentSchema, n as sdkCourseBookingSettingsSchema, nr as slugifyMediaIdentifier, or as getMimeType, r as sdkEventOccurrenceInputSchema, s as eventVisibilitySchema, tr as resolveMediaIdentifier, un as pageDesignIntentSchemaVersion, ur as FILE_SIZE_LIMITS, ut as themeSchema, zn as getDirectImageUrlForSiteScopedPath } from "./sdk-event-upsert-BdvfnCyV.mjs";
|
|
3
|
-
import { A as parseFooterCompositionV1, Dn as ok, En as err, Rt as assertNever, a as canonicalizeConfiguredBookingVerticalConfig, c as resolveSiteVerticalBookingConfig, l as DEFAULT_EVENT_TYPE_PRESETS, o as parseVerticalId, on as unsafeAsEventOfferingId, s as parseVerticalPresetId, sn as unsafeAsEventSeriesId, t as resolveEventCatalogue, u as EVENT_PRESET_VALUES, un as unsafeAsOccurrenceId, y as exposeSecret } from "./src-yL0DBH7S.mjs";
|
|
4
|
-
import * as fs$1 from "fs/promises";
|
|
5
|
-
import * as path$1 from "path";
|
|
6
|
-
import { z } from "zod";
|
|
7
|
-
import { constants } from "node:fs";
|
|
8
|
-
import { basename, dirname as dirname$1, join } from "node:path";
|
|
9
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
10
|
-
import { access, chmod, link, lstat, mkdir, open, rename, unlink as unlink$1 } from "node:fs/promises";
|
|
11
|
-
|
|
12
|
-
//#region ../api/src/bookingVerticals.ts
|
|
13
|
-
/**
|
|
14
|
-
* Transport adapters for the booking-vertical domain.
|
|
15
|
-
*
|
|
16
|
-
* The domain catalog and all pure resolution/selection rules live in core;
|
|
17
|
-
* this module owns only the Zod boundary used by API payloads.
|
|
18
|
-
*/
|
|
19
|
-
function formatRawDiagnosticValue(value) {
|
|
20
|
-
const serialized = JSON.stringify(value);
|
|
21
|
-
return serialized === undefined ? String(value) : serialized;
|
|
22
|
-
}
|
|
23
|
-
function describeSiteVerticalConfigIssue(issue) {
|
|
24
|
-
switch (issue.code) {
|
|
25
|
-
case "unknown_vertical": return `${issue.code}: ${formatRawDiagnosticValue(issue.rawValue)}`;
|
|
26
|
-
case "unknown_preset": return `${issue.code}: ${formatRawDiagnosticValue(issue.rawValue)}`;
|
|
27
|
-
case "missing_vertical":
|
|
28
|
-
case "empty_selected_presets":
|
|
29
|
-
case "preset_vertical_mismatch":
|
|
30
|
-
case "duplicate_preset_id":
|
|
31
|
-
case "duplicate_canonical_behaviour":
|
|
32
|
-
case "future_preset_selected": return issue.code;
|
|
33
|
-
default: return assertNever(issue);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
const VerticalIdSchema = z.string().trim().transform((value, ctx) => {
|
|
37
|
-
const parsed = parseVerticalId(value);
|
|
38
|
-
if (!parsed) {
|
|
39
|
-
ctx.addIssue({
|
|
40
|
-
code: z.ZodIssueCode.custom,
|
|
41
|
-
message: "Unknown booking vertical"
|
|
42
|
-
});
|
|
43
|
-
return z.NEVER;
|
|
44
|
-
}
|
|
45
|
-
return parsed;
|
|
46
|
-
});
|
|
47
|
-
const VerticalPresetIdSchema = z.string().trim().transform((value, ctx) => {
|
|
48
|
-
const parsed = parseVerticalPresetId(value);
|
|
49
|
-
if (!parsed) {
|
|
50
|
-
ctx.addIssue({
|
|
51
|
-
code: z.ZodIssueCode.custom,
|
|
52
|
-
message: "Unknown booking preset"
|
|
53
|
-
});
|
|
54
|
-
return z.NEVER;
|
|
55
|
-
}
|
|
56
|
-
return parsed;
|
|
57
|
-
});
|
|
58
|
-
const BookingVerticalConfigSchema = z.object({
|
|
59
|
-
verticalId: VerticalIdSchema.nullable(),
|
|
60
|
-
selectedPresetIds: z.array(VerticalPresetIdSchema).max(20)
|
|
61
|
-
}).transform((value, ctx) => {
|
|
62
|
-
const resolved = resolveSiteVerticalBookingConfig(value);
|
|
63
|
-
if (resolved.kind === "invalid") {
|
|
64
|
-
ctx.addIssue({
|
|
65
|
-
code: z.ZodIssueCode.custom,
|
|
66
|
-
message: `Invalid booking vertical config: ${resolved.issues.map(describeSiteVerticalConfigIssue).join(", ")}`
|
|
67
|
-
});
|
|
68
|
-
return z.NEVER;
|
|
69
|
-
}
|
|
70
|
-
return resolved.config;
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
//#endregion
|
|
74
|
-
//#region ../api/src/sdkBookingVerticalSchemas.ts
|
|
75
|
-
const sdkConfiguredBookingVerticalConfigSchema = z.object({
|
|
76
|
-
verticalId: VerticalIdSchema,
|
|
77
|
-
selectedPresetIds: z.tuple([VerticalPresetIdSchema]).rest(VerticalPresetIdSchema)
|
|
78
|
-
}).transform((config, context) => {
|
|
79
|
-
const canonical = canonicalizeConfiguredBookingVerticalConfig(config);
|
|
80
|
-
if (canonical.ok) return canonical.config;
|
|
81
|
-
for (const issue of canonical.issues) {
|
|
82
|
-
context.addIssue({
|
|
83
|
-
code: z.ZodIssueCode.custom,
|
|
84
|
-
path: ["selectedPresetIds"],
|
|
85
|
-
message: issue.code
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
return z.NEVER;
|
|
89
|
-
});
|
|
90
|
-
const sdkConfigureBookingVerticalRequestSchema = z.discriminatedUnion("mode", [z.object({
|
|
91
|
-
mode: z.literal("preview"),
|
|
92
|
-
verticalConfig: sdkConfiguredBookingVerticalConfigSchema,
|
|
93
|
-
expectedRevision: z.string().min(1).nullable().optional()
|
|
94
|
-
}).strict(), z.object({
|
|
95
|
-
mode: z.literal("apply"),
|
|
96
|
-
verticalConfig: sdkConfiguredBookingVerticalConfigSchema,
|
|
97
|
-
expectedRevision: z.string().min(1).nullable()
|
|
98
|
-
}).strict()]);
|
|
99
|
-
const sdkVerticalConfigIssueSchema = z.discriminatedUnion("code", [
|
|
100
|
-
z.object({
|
|
101
|
-
code: z.literal("unknown_vertical"),
|
|
102
|
-
rawValue: z.unknown()
|
|
103
|
-
}),
|
|
104
|
-
z.object({
|
|
105
|
-
code: z.literal("unknown_preset"),
|
|
106
|
-
rawValue: z.unknown()
|
|
107
|
-
}),
|
|
108
|
-
z.object({ code: z.literal("missing_vertical") }),
|
|
109
|
-
z.object({ code: z.literal("empty_selected_presets") }),
|
|
110
|
-
z.object({ code: z.literal("preset_vertical_mismatch") }),
|
|
111
|
-
z.object({ code: z.literal("duplicate_preset_id") }),
|
|
112
|
-
z.object({ code: z.literal("duplicate_canonical_behaviour") }),
|
|
113
|
-
z.object({ code: z.literal("future_preset_selected") })
|
|
114
|
-
]);
|
|
115
|
-
const sdkBookingSettingsWarningSchema = z.discriminatedUnion("code", [z.object({
|
|
116
|
-
code: z.literal("booking_module_disabled"),
|
|
117
|
-
module: z.enum(["appointments", "events"])
|
|
118
|
-
}), z.object({
|
|
119
|
-
code: z.literal("replaced_invalid_current_config"),
|
|
120
|
-
issues: z.array(sdkVerticalConfigIssueSchema)
|
|
121
|
-
})]);
|
|
122
|
-
const sdkPullSettingsWarningSchema = z.object({
|
|
123
|
-
code: z.literal("invalid_booking_vertical_config"),
|
|
124
|
-
issues: z.array(sdkVerticalConfigIssueSchema)
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
//#endregion
|
|
128
|
-
//#region ../api/src/sdkCommandResponseSchemas.ts
|
|
129
|
-
const sdkCommandEffectSchema = z.discriminatedUnion("kind", [
|
|
130
|
-
z.object({ kind: z.literal("created") }),
|
|
131
|
-
z.object({ kind: z.literal("updated") }),
|
|
132
|
-
z.object({ kind: z.literal("unchanged") }),
|
|
133
|
-
z.object({ kind: z.literal("deleted") }),
|
|
134
|
-
z.object({
|
|
135
|
-
kind: z.literal("transitioned"),
|
|
136
|
-
transition: z.enum(["published", "unpublished"])
|
|
137
|
-
})
|
|
138
|
-
]);
|
|
139
|
-
function sdkCommandApplicationResultSchema(value) {
|
|
140
|
-
return z.object({
|
|
141
|
-
value,
|
|
142
|
-
effect: sdkCommandEffectSchema
|
|
143
|
-
}).passthrough();
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
//#endregion
|
|
147
|
-
//#region ../api/src/sdkBlockUpsertContracts.ts
|
|
148
|
-
const sdkBlockAppearanceChoiceSchema = z.discriminatedUnion("kind", [z.object({
|
|
149
|
-
kind: z.literal("pinned"),
|
|
150
|
-
appearancePresetId: z.string().min(1).transform(asAppearancePresetId)
|
|
151
|
-
}), z.object({ kind: z.literal("automatic") })]);
|
|
152
|
-
const sdkBlockAppearanceOutcomeSchema = z.discriminatedUnion("kind", [z.object({
|
|
153
|
-
kind: z.literal("applied"),
|
|
154
|
-
choice: sdkBlockAppearanceChoiceSchema,
|
|
155
|
-
effect: z.enum(["updated", "unchanged"])
|
|
156
|
-
}), z.object({
|
|
157
|
-
kind: z.literal("not-applied"),
|
|
158
|
-
code: z.enum(["STALE_PAGE_DESIGN_STATE", "SAVE_PAGE_DESIGN_STATE_FAILED"]),
|
|
159
|
-
message: z.string(),
|
|
160
|
-
retryable: z.literal(true)
|
|
161
|
-
})]);
|
|
162
|
-
const pageBlockDesignSyncWarningSchema = z.object({
|
|
163
|
-
kind: z.enum([
|
|
164
|
-
"blocked",
|
|
165
|
-
"failed",
|
|
166
|
-
"skipped-invalid"
|
|
167
|
-
]),
|
|
168
|
-
operation: z.enum([
|
|
169
|
-
"create",
|
|
170
|
-
"update",
|
|
171
|
-
"delete",
|
|
172
|
-
"reorder"
|
|
173
|
-
]),
|
|
174
|
-
message: z.string(),
|
|
175
|
-
blocker: z.string().optional(),
|
|
176
|
-
source: z.enum(["draft", "published-fallback"]).optional(),
|
|
177
|
-
reason: z.string().optional()
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
//#endregion
|
|
181
|
-
//#region ../api/src/sdkOccurrenceResponseSchemas.ts
|
|
182
|
-
const identifierSchema$1 = z.string().min(1);
|
|
183
|
-
const timestampSchema = z.string().min(1);
|
|
184
|
-
const sdkManagementEventOccurrenceSchema = z.object({
|
|
185
|
-
id: identifierSchema$1.transform(unsafeAsOccurrenceId),
|
|
186
|
-
seriesId: identifierSchema$1.transform(unsafeAsEventSeriesId),
|
|
187
|
-
startsAt: timestampSchema,
|
|
188
|
-
endsAt: timestampSchema,
|
|
189
|
-
timeZone: z.string().min(1),
|
|
190
|
-
status: z.enum([
|
|
191
|
-
"scheduled",
|
|
192
|
-
"cancelled",
|
|
193
|
-
"completed"
|
|
194
|
-
]),
|
|
195
|
-
provenance: z.enum([
|
|
196
|
-
"manual",
|
|
197
|
-
"recurrence_generated",
|
|
198
|
-
"recurrence_exception"
|
|
199
|
-
]),
|
|
200
|
-
capacityOverride: z.number().int().nonnegative().nullable(),
|
|
201
|
-
capacity: z.number().int().nonnegative().nullable(),
|
|
202
|
-
confirmedPlaces: z.number().int().nonnegative().optional(),
|
|
203
|
-
registrations: z.number().int().nonnegative(),
|
|
204
|
-
deletionEligibility: z.discriminatedUnion("kind", [
|
|
205
|
-
z.object({ kind: z.literal("allowed") }),
|
|
206
|
-
z.object({
|
|
207
|
-
kind: z.literal("blocked_by_registrations"),
|
|
208
|
-
registrations: z.number().int().positive()
|
|
209
|
-
}),
|
|
210
|
-
z.object({ kind: z.literal("blocked_generated_occurrence") }),
|
|
211
|
-
z.object({ kind: z.literal("blocked_completed_occurrence") })
|
|
212
|
-
])
|
|
213
|
-
});
|
|
214
|
-
const sdkListEventOccurrencesResponseSchema = z.object({
|
|
215
|
-
occurrences: z.array(sdkManagementEventOccurrenceSchema),
|
|
216
|
-
nextCursor: z.string().nullable()
|
|
217
|
-
});
|
|
218
|
-
const businessCancellationResultSchema = z.object({
|
|
219
|
-
kind: z.enum([
|
|
220
|
-
"finished",
|
|
221
|
-
"unfinished",
|
|
222
|
-
"partial_application"
|
|
223
|
-
]),
|
|
224
|
-
paymentTerms: z.enum([
|
|
225
|
-
"complete",
|
|
226
|
-
"unfinished",
|
|
227
|
-
"unavailable"
|
|
228
|
-
]),
|
|
229
|
-
refunds: z.enum([
|
|
230
|
-
"complete",
|
|
231
|
-
"processing",
|
|
232
|
-
"needs_attention",
|
|
233
|
-
"unavailable"
|
|
234
|
-
]),
|
|
235
|
-
emails: z.enum([
|
|
236
|
-
"complete",
|
|
237
|
-
"queued",
|
|
238
|
-
"needs_attention",
|
|
239
|
-
"unavailable"
|
|
240
|
-
]),
|
|
241
|
-
operationId: identifierSchema$1,
|
|
242
|
-
directContactAttendeeIds: z.array(identifierSchema$1),
|
|
243
|
-
cancelledBookings: z.number().int().nonnegative(),
|
|
244
|
-
refundedBookings: z.number().int().nonnegative(),
|
|
245
|
-
restoredPassCredits: z.number().int().nonnegative(),
|
|
246
|
-
pending: z.object({
|
|
247
|
-
refundAttendeeIds: z.array(identifierSchema$1),
|
|
248
|
-
creditAttendeeIds: z.array(identifierSchema$1),
|
|
249
|
-
emailAttendeeIds: z.array(identifierSchema$1)
|
|
250
|
-
}),
|
|
251
|
-
structural: z.object({
|
|
252
|
-
created: z.number().int().nonnegative(),
|
|
253
|
-
deleted: z.number().int().nonnegative(),
|
|
254
|
-
cancelled: z.number().int().nonnegative(),
|
|
255
|
-
unfinishedOccurrenceIds: z.array(identifierSchema$1),
|
|
256
|
-
unfinishedCreateCount: z.number().int().nonnegative().optional(),
|
|
257
|
-
reason: z.enum([
|
|
258
|
-
"superseded",
|
|
259
|
-
"not_found",
|
|
260
|
-
"restore_blocked",
|
|
261
|
-
"failed",
|
|
262
|
-
"result_unavailable"
|
|
263
|
-
]).optional()
|
|
264
|
-
})
|
|
265
|
-
});
|
|
266
|
-
const sdkEventOccurrenceResponseSchema = z.object({
|
|
267
|
-
occurrence: sdkManagementEventOccurrenceSchema,
|
|
268
|
-
cancellationResult: businessCancellationResultSchema.optional(),
|
|
269
|
-
cancellationResults: z.array(businessCancellationResultSchema).optional()
|
|
270
|
-
});
|
|
271
|
-
const sdkDeleteEventOccurrenceResponseSchema = z.object({ deleted: z.literal(true) });
|
|
272
|
-
const sdkReorderResponseSchema = z.object({
|
|
273
|
-
reordered: z.literal(true),
|
|
274
|
-
effect: sdkCommandEffectSchema,
|
|
275
|
-
designSyncWarning: pageBlockDesignSyncWarningSchema.optional()
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
//#endregion
|
|
279
|
-
//#region ../api/src/sdkPageDesign.ts
|
|
280
|
-
/**
|
|
281
|
-
* The portable page design document (issue 1497).
|
|
282
|
-
*
|
|
283
|
-
* A stored `PageDesignIntent` names blocks by their database ids, so it only
|
|
284
|
-
* makes sense on the page it came from. The portable document is the same
|
|
285
|
-
* intent with every block reference expressed as the block's management
|
|
286
|
-
* identifier (`hero-0`, `body-text-1`), which is what lets a design travel from
|
|
287
|
-
* one page to another page built from the same blocks.
|
|
288
|
-
*
|
|
289
|
-
* A boundary is carried as its identifier pair only. Its stored `boundaryId`
|
|
290
|
-
* is derived from the pair after mapping, so the two can never disagree.
|
|
291
|
-
*
|
|
292
|
-
* The document is parsed once, here, at the JSON boundary. Whether it fits a
|
|
293
|
-
* particular destination page is a separate, semantic decision made by the
|
|
294
|
-
* dashboard against that page's blocks and site style.
|
|
295
|
-
*/
|
|
296
|
-
const unsafeAsBlockIdentifier = (value) => value;
|
|
297
|
-
const blockIdentifierSchema = z.string().min(1).transform(unsafeAsBlockIdentifier);
|
|
298
|
-
const portableBlockLookPinSchema = z.object({
|
|
299
|
-
blockIdentifier: blockIdentifierSchema,
|
|
300
|
-
blockKind: z.string().min(1),
|
|
301
|
-
choice: z.object({
|
|
302
|
-
kind: z.literal("user-pinned"),
|
|
303
|
-
value: z.string().min(1).transform(asAppearancePresetId)
|
|
304
|
-
}).strict()
|
|
305
|
-
}).strict();
|
|
306
|
-
const portableBoundaryPinSchema = z.object({
|
|
307
|
-
previousBlockIdentifier: blockIdentifierSchema,
|
|
308
|
-
nextBlockIdentifier: blockIdentifierSchema,
|
|
309
|
-
choice: z.object({
|
|
310
|
-
kind: z.literal("user-pinned"),
|
|
311
|
-
value: pageDesignBoundaryTreatmentSchema
|
|
312
|
-
}).strict()
|
|
313
|
-
}).strict();
|
|
314
|
-
function isRepeated(items, same) {
|
|
315
|
-
return (item, index) => items.findIndex((other) => same(item, other)) !== index;
|
|
316
|
-
}
|
|
317
|
-
function refineUniquePins(design, ctx) {
|
|
318
|
-
design.blockLookPins.filter(isRepeated(design.blockLookPins, (a, b) => a.blockIdentifier === b.blockIdentifier)).forEach((pin) => ctx.addIssue({
|
|
319
|
-
code: "custom",
|
|
320
|
-
path: ["blockLookPins"],
|
|
321
|
-
message: `The design pins block "${pin.blockIdentifier}" more than once.`
|
|
322
|
-
}));
|
|
323
|
-
design.boundaryPins.filter(isRepeated(design.boundaryPins, (a, b) => a.previousBlockIdentifier === b.previousBlockIdentifier && a.nextBlockIdentifier === b.nextBlockIdentifier)).forEach((pin) => ctx.addIssue({
|
|
324
|
-
code: "custom",
|
|
325
|
-
path: ["boundaryPins"],
|
|
326
|
-
message: `The design pins the break between "${pin.previousBlockIdentifier}" and "${pin.nextBlockIdentifier}" more than once.`
|
|
327
|
-
}));
|
|
328
|
-
}
|
|
329
|
-
const portablePageDesignSchema = z.object({
|
|
330
|
-
schemaVersion: z.literal(pageDesignIntentSchemaVersion),
|
|
331
|
-
siteStyleId: z.string().min(1).transform(asSiteStyleId),
|
|
332
|
-
pageFlowPresetId: z.string().min(1).transform(asPageFlowPresetId),
|
|
333
|
-
blockLookPins: z.array(portableBlockLookPinSchema),
|
|
334
|
-
boundaryPins: z.array(portableBoundaryPinSchema)
|
|
335
|
-
}).strict().superRefine(refineUniquePins);
|
|
336
|
-
const pageDesignRevisionTokenSchema = z.discriminatedUnion("kind", [
|
|
337
|
-
z.object({ kind: z.literal("absent") }).strict(),
|
|
338
|
-
z.object({
|
|
339
|
-
kind: z.literal("draft"),
|
|
340
|
-
contentVersionId: z.string().min(1),
|
|
341
|
-
updatedAt: z.string().min(1)
|
|
342
|
-
}).strict(),
|
|
343
|
-
z.object({
|
|
344
|
-
kind: z.literal("published"),
|
|
345
|
-
contentVersionId: z.string().min(1),
|
|
346
|
-
updatedAt: z.string().min(1)
|
|
347
|
-
}).strict()
|
|
348
|
-
]);
|
|
349
|
-
const sdkSavePageDesignBodySchema = z.object({
|
|
350
|
-
design: portablePageDesignSchema,
|
|
351
|
-
expectedRevision: pageDesignRevisionTokenSchema
|
|
352
|
-
}).strict();
|
|
353
|
-
const omittedPagePinSchema = z.discriminatedUnion("kind", [z.object({
|
|
354
|
-
kind: z.literal("look-on-missing-block"),
|
|
355
|
-
blockId: z.string().min(1)
|
|
356
|
-
}), z.object({
|
|
357
|
-
kind: z.literal("boundary-on-missing-block"),
|
|
358
|
-
previousBlockId: z.string().min(1),
|
|
359
|
-
nextBlockId: z.string().min(1)
|
|
360
|
-
})]);
|
|
361
|
-
const sdkGetPageDesignResponseSchema = z.discriminatedUnion("kind", [z.object({ kind: z.literal("absent") }), z.object({
|
|
362
|
-
kind: z.literal("stored"),
|
|
363
|
-
source: z.enum(["draft", "published"]),
|
|
364
|
-
design: portablePageDesignSchema,
|
|
365
|
-
revision: pageDesignRevisionTokenSchema,
|
|
366
|
-
omitted: z.array(omittedPagePinSchema)
|
|
367
|
-
})]);
|
|
368
|
-
const sdkSavePageDesignResponseSchema = z.object({
|
|
369
|
-
outcome: z.enum(["created", "updated"]),
|
|
370
|
-
revision: pageDesignRevisionTokenSchema
|
|
371
|
-
});
|
|
372
|
-
|
|
373
|
-
//#endregion
|
|
374
|
-
//#region ../api/src/sdkMediaResponseSchemas.ts
|
|
375
|
-
const identifierSchema = z.string().min(1);
|
|
376
|
-
const nullablePathSchema = z.string().nullable();
|
|
377
|
-
const managementMediaAssetSchema = z.object({
|
|
378
|
-
id: identifierSchema,
|
|
379
|
-
siteId: identifierSchema,
|
|
380
|
-
identifier: identifierSchema,
|
|
381
|
-
storageBucket: identifierSchema,
|
|
382
|
-
storagePath: identifierSchema,
|
|
383
|
-
filename: identifierSchema,
|
|
384
|
-
mimeType: identifierSchema,
|
|
385
|
-
width: z.number().int().nonnegative().nullable(),
|
|
386
|
-
height: z.number().int().nonnegative().nullable(),
|
|
387
|
-
bytes: z.number().int().nonnegative().nullable(),
|
|
388
|
-
checksumSha256: z.string().nullable(),
|
|
389
|
-
createdAt: identifierSchema,
|
|
390
|
-
updatedAt: identifierSchema
|
|
391
|
-
});
|
|
392
|
-
const blockLocationFields = {
|
|
393
|
-
siteId: identifierSchema,
|
|
394
|
-
blockId: identifierSchema,
|
|
395
|
-
blockKind: identifierSchema,
|
|
396
|
-
blockIdentifier: z.string().nullable(),
|
|
397
|
-
stage: z.enum(["draft", "published"])
|
|
398
|
-
};
|
|
399
|
-
const newsletterUsageLocationSchemas = [z.object({
|
|
400
|
-
kind: z.literal("newsletterTemplate"),
|
|
401
|
-
siteId: identifierSchema,
|
|
402
|
-
templateId: identifierSchema,
|
|
403
|
-
templateName: z.string()
|
|
404
|
-
}), z.object({
|
|
405
|
-
kind: z.literal("newsletterCampaign"),
|
|
406
|
-
siteId: identifierSchema,
|
|
407
|
-
campaignId: identifierSchema,
|
|
408
|
-
campaignName: z.string()
|
|
409
|
-
})];
|
|
410
|
-
const mediaUsageLocationSchema = z.discriminatedUnion("kind", [
|
|
411
|
-
...newsletterUsageLocationSchemas,
|
|
412
|
-
z.object({
|
|
413
|
-
kind: z.literal("pageBlock"),
|
|
414
|
-
...blockLocationFields,
|
|
415
|
-
pageId: identifierSchema,
|
|
416
|
-
pageTitle: z.string().nullable(),
|
|
417
|
-
pageDraftPath: nullablePathSchema,
|
|
418
|
-
pagePublishedPath: nullablePathSchema
|
|
419
|
-
}),
|
|
420
|
-
z.object({
|
|
421
|
-
kind: z.literal("entrySlotBlock"),
|
|
422
|
-
...blockLocationFields,
|
|
423
|
-
entryId: identifierSchema,
|
|
424
|
-
entryTitle: z.string(),
|
|
425
|
-
typeKey: identifierSchema,
|
|
426
|
-
routeDraftPath: nullablePathSchema,
|
|
427
|
-
routePublishedPath: nullablePathSchema,
|
|
428
|
-
slotKey: identifierSchema
|
|
429
|
-
}),
|
|
430
|
-
z.object({
|
|
431
|
-
kind: z.literal("surfaceSlotBlock"),
|
|
432
|
-
...blockLocationFields,
|
|
433
|
-
pageSurfaceId: identifierSchema,
|
|
434
|
-
surfaceSlotId: identifierSchema,
|
|
435
|
-
slotKey: identifierSchema
|
|
436
|
-
}),
|
|
437
|
-
z.object({
|
|
438
|
-
kind: z.literal("contentEntry"),
|
|
439
|
-
siteId: identifierSchema,
|
|
440
|
-
entryId: identifierSchema,
|
|
441
|
-
entryTitle: z.string(),
|
|
442
|
-
typeKey: identifierSchema,
|
|
443
|
-
routeDraftPath: nullablePathSchema,
|
|
444
|
-
routePublishedPath: nullablePathSchema,
|
|
445
|
-
stage: z.enum(["draft", "published"])
|
|
446
|
-
}),
|
|
447
|
-
z.object({
|
|
448
|
-
kind: z.literal("productFileDownload"),
|
|
449
|
-
siteId: identifierSchema,
|
|
450
|
-
productId: identifierSchema,
|
|
451
|
-
productTitle: z.string()
|
|
452
|
-
})
|
|
453
|
-
]);
|
|
454
|
-
const mediaUsageItemSchema = z.object({
|
|
455
|
-
assetId: identifierSchema,
|
|
456
|
-
filename: z.string().nullable(),
|
|
457
|
-
usedCount: z.number().int().nonnegative(),
|
|
458
|
-
locations: z.array(mediaUsageLocationSchema)
|
|
459
|
-
});
|
|
460
|
-
const mediaDeleteWarningSchema = z.discriminatedUnion("kind", [z.object({
|
|
461
|
-
kind: z.literal("ORPHAN_CLEANUP_FAILED"),
|
|
462
|
-
message: z.string(),
|
|
463
|
-
storageBucket: identifierSchema,
|
|
464
|
-
storagePath: identifierSchema
|
|
465
|
-
}), z.object({
|
|
466
|
-
kind: z.literal("LABEL_CLEANUP_FAILED"),
|
|
467
|
-
message: z.string()
|
|
468
|
-
})]);
|
|
469
|
-
const sdkMediaListResponseSchema = z.object({
|
|
470
|
-
items: z.array(managementMediaAssetSchema),
|
|
471
|
-
total: z.number().int().nonnegative(),
|
|
472
|
-
limit: z.number().int().positive(),
|
|
473
|
-
offset: z.number().int().nonnegative(),
|
|
474
|
-
nextOffset: z.number().int().nonnegative().optional()
|
|
475
|
-
});
|
|
476
|
-
const sdkMediaDownloadResponseSchema = z.object({
|
|
477
|
-
transferUrl: z.string().url(),
|
|
478
|
-
asset: managementMediaAssetSchema
|
|
479
|
-
});
|
|
480
|
-
const sdkMediaDeletePreviewResponseSchema = z.discriminatedUnion("kind", [
|
|
481
|
-
z.object({ kind: z.literal("missing") }),
|
|
482
|
-
z.object({
|
|
483
|
-
kind: z.literal("in_use"),
|
|
484
|
-
usage: mediaUsageItemSchema
|
|
485
|
-
}),
|
|
486
|
-
z.object({
|
|
487
|
-
kind: z.literal("deletable"),
|
|
488
|
-
assetId: identifierSchema
|
|
489
|
-
})
|
|
490
|
-
]);
|
|
491
|
-
const sdkMediaDeleteApplyResponseSchema = z.discriminatedUnion("kind", [
|
|
492
|
-
z.object({ kind: z.literal("missing") }),
|
|
493
|
-
z.object({
|
|
494
|
-
kind: z.literal("in_use"),
|
|
495
|
-
usage: mediaUsageItemSchema
|
|
496
|
-
}),
|
|
497
|
-
z.object({ kind: z.literal("deleted") }),
|
|
498
|
-
z.object({
|
|
499
|
-
kind: z.literal("deleted_with_warnings"),
|
|
500
|
-
warnings: z.tuple([mediaDeleteWarningSchema], mediaDeleteWarningSchema)
|
|
501
|
-
})
|
|
502
|
-
]);
|
|
503
|
-
|
|
504
|
-
//#endregion
|
|
505
|
-
//#region ../blocks/src/system/types/link.ts
|
|
506
|
-
const nullableString = z.union([z.string().min(1), z.null()]);
|
|
507
|
-
const RESOLVED_INTERNAL_LINK_ENTITY_TYPES = [
|
|
508
|
-
"page",
|
|
509
|
-
"content",
|
|
510
|
-
"pageSurface"
|
|
511
|
-
];
|
|
512
|
-
function isResolvedInternalLinkEntityType(value) {
|
|
513
|
-
return typeof value === "string" && RESOLVED_INTERNAL_LINK_ENTITY_TYPES.includes(value);
|
|
514
|
-
}
|
|
515
|
-
const resolvedInternalLinkKeys = [
|
|
516
|
-
"entityId",
|
|
517
|
-
"entityType",
|
|
518
|
-
"href",
|
|
519
|
-
"title",
|
|
520
|
-
"typeLabel"
|
|
521
|
-
];
|
|
522
|
-
const internalRouteOnlyLinkSchema = z.object({
|
|
523
|
-
kind: z.literal("internal"),
|
|
524
|
-
routeId: z.string().min(1)
|
|
525
|
-
}).passthrough().superRefine((value, ctx) => {
|
|
526
|
-
const hasResolvedMetadata = resolvedInternalLinkKeys.some((key) => value[key] != null);
|
|
527
|
-
if (!hasResolvedMetadata) return;
|
|
528
|
-
ctx.addIssue({
|
|
529
|
-
code: "custom",
|
|
530
|
-
message: "Internal links with resolved metadata must include entityId, entityType, href, title, and typeLabel"
|
|
531
|
-
});
|
|
532
|
-
});
|
|
533
|
-
const internalResolvedLinkSchema = z.object({
|
|
534
|
-
kind: z.literal("internal"),
|
|
535
|
-
routeId: z.string().min(1),
|
|
536
|
-
entityId: z.string().min(1),
|
|
537
|
-
entityType: z.enum(RESOLVED_INTERNAL_LINK_ENTITY_TYPES),
|
|
538
|
-
href: z.string().min(1),
|
|
539
|
-
title: z.string().min(1),
|
|
540
|
-
typeLabel: z.string().min(1),
|
|
541
|
-
contentTypeKey: nullableString.optional(),
|
|
542
|
-
contentTypeName: nullableString.optional(),
|
|
543
|
-
updatedAt: nullableString.optional()
|
|
544
|
-
}).passthrough();
|
|
545
|
-
const internalLinkSchema = z.union([internalResolvedLinkSchema, internalRouteOnlyLinkSchema]);
|
|
546
|
-
const externalLinkSchema$1 = z.object({
|
|
547
|
-
kind: z.literal("external"),
|
|
548
|
-
href: z.string().min(1)
|
|
549
|
-
}).passthrough();
|
|
550
|
-
const customLinkSchema$1 = z.object({
|
|
551
|
-
kind: z.literal("url"),
|
|
552
|
-
href: z.string().min(1)
|
|
553
|
-
}).passthrough();
|
|
554
|
-
const pageLinkSchema$1 = z.object({
|
|
555
|
-
kind: z.literal("page"),
|
|
556
|
-
identifier: z.string().min(1)
|
|
557
|
-
}).passthrough();
|
|
558
|
-
const entryLinkSchema$1 = z.object({
|
|
559
|
-
kind: z.literal("entry"),
|
|
560
|
-
contentType: z.string().min(1),
|
|
561
|
-
identifier: z.string().min(1)
|
|
562
|
-
}).passthrough();
|
|
563
|
-
const linkSchema = z.union([
|
|
564
|
-
internalLinkSchema,
|
|
565
|
-
externalLinkSchema$1,
|
|
566
|
-
customLinkSchema$1,
|
|
567
|
-
pageLinkSchema$1,
|
|
568
|
-
entryLinkSchema$1
|
|
569
|
-
]);
|
|
570
|
-
function visitLinkValueReferences(value, visitor) {
|
|
571
|
-
const parsed = linkSchema.safeParse(value);
|
|
572
|
-
if (!parsed.success) {
|
|
573
|
-
return;
|
|
574
|
-
}
|
|
575
|
-
const link$1 = parsed.data;
|
|
576
|
-
switch (link$1.kind) {
|
|
577
|
-
case "internal":
|
|
578
|
-
visitor.routeIds.add(link$1.routeId);
|
|
579
|
-
return;
|
|
580
|
-
case "page":
|
|
581
|
-
visitor.pageIdentifiers.add(link$1.identifier);
|
|
582
|
-
return;
|
|
583
|
-
case "entry":
|
|
584
|
-
visitor.entryIdentifiers.add(link$1.contentType, link$1.identifier);
|
|
585
|
-
return;
|
|
586
|
-
case "external":
|
|
587
|
-
case "url": return;
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
function isInternalResolvedLinkValue(value) {
|
|
591
|
-
return value?.kind === "internal" && "entityId" in value && "entityType" in value && "href" in value && "title" in value && "typeLabel" in value && typeof value.entityId === "string" && isResolvedInternalLinkEntityType(value.entityType) && typeof value.href === "string" && typeof value.title === "string" && typeof value.typeLabel === "string";
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
//#endregion
|
|
595
|
-
//#region ../api/src/navigation/contracts.ts
|
|
596
|
-
/**
|
|
597
|
-
* Canonical navigation/link Zod contracts.
|
|
598
|
-
*
|
|
599
|
-
* Goal: a single source-of-truth for link shapes used across dashboard + SDK.
|
|
600
|
-
* - Types live in `packages/api/src/navigation/types.ts`
|
|
601
|
-
* - Schemas live here and are tied to types via `satisfies z.ZodType<...>`
|
|
602
|
-
*/
|
|
603
|
-
const portalLinkPayloadSchema = z.object({
|
|
604
|
-
kind: z.literal("portal"),
|
|
605
|
-
path: z.string().trim().startsWith("/").optional(),
|
|
606
|
-
labels: z.object({
|
|
607
|
-
loggedOut: z.string().trim().min(1).optional(),
|
|
608
|
-
loggedIn: z.string().trim().min(1).optional()
|
|
609
|
-
}).optional()
|
|
610
|
-
});
|
|
611
|
-
const cartLinkLabelConfigSchema = z.discriminatedUnion("kind", [z.object({ kind: z.literal("icon-only") }), z.object({
|
|
612
|
-
kind: z.literal("icon-with-label"),
|
|
613
|
-
label: z.string().trim().min(1)
|
|
614
|
-
})]);
|
|
615
|
-
const cartLinkPayloadSchema = z.object({
|
|
616
|
-
kind: z.literal("cart"),
|
|
617
|
-
badge: z.enum(["show-count", "hidden"]),
|
|
618
|
-
label: cartLinkLabelConfigSchema
|
|
619
|
-
});
|
|
620
|
-
const internalLinkPayloadSchema = z.object({
|
|
621
|
-
kind: z.literal("internal"),
|
|
622
|
-
routeId: z.string().uuid(),
|
|
623
|
-
entityId: z.string().uuid(),
|
|
624
|
-
entityType: z.enum(RESOLVED_INTERNAL_LINK_ENTITY_TYPES),
|
|
625
|
-
href: z.string().min(1),
|
|
626
|
-
title: z.string().min(1),
|
|
627
|
-
typeLabel: z.string().min(1),
|
|
628
|
-
contentTypeKey: z.string().nullable().optional(),
|
|
629
|
-
contentTypeName: z.string().nullable().optional(),
|
|
630
|
-
updatedAt: z.string().nullable().optional()
|
|
631
|
-
}).passthrough();
|
|
632
|
-
const externalLinkSchema = z.object({
|
|
633
|
-
kind: z.literal("external"),
|
|
634
|
-
href: z.string().url()
|
|
635
|
-
});
|
|
636
|
-
const customLinkSchema = z.object({
|
|
637
|
-
kind: z.literal("url"),
|
|
638
|
-
href: z.string().min(1)
|
|
639
|
-
});
|
|
640
|
-
const pageLinkSchema = z.object({
|
|
641
|
-
kind: z.literal("page"),
|
|
642
|
-
identifier: z.string().min(1)
|
|
643
|
-
});
|
|
644
|
-
const entryLinkSchema = z.object({
|
|
645
|
-
kind: z.literal("entry"),
|
|
646
|
-
contentType: z.string().min(1),
|
|
647
|
-
identifier: z.string().min(1)
|
|
648
|
-
});
|
|
649
|
-
const linkPayloadSchema = z.discriminatedUnion("kind", [
|
|
650
|
-
internalLinkPayloadSchema,
|
|
651
|
-
pageLinkSchema,
|
|
652
|
-
entryLinkSchema,
|
|
653
|
-
externalLinkSchema,
|
|
654
|
-
customLinkSchema,
|
|
655
|
-
portalLinkPayloadSchema,
|
|
656
|
-
cartLinkPayloadSchema
|
|
657
|
-
]);
|
|
658
|
-
const navigationLinkInputSchemaOptions = [
|
|
659
|
-
pageLinkSchema,
|
|
660
|
-
entryLinkSchema,
|
|
661
|
-
externalLinkSchema,
|
|
662
|
-
customLinkSchema,
|
|
663
|
-
portalLinkPayloadSchema,
|
|
664
|
-
cartLinkPayloadSchema
|
|
665
|
-
];
|
|
666
|
-
const navigationLinkInputSchema = z.discriminatedUnion("kind", navigationLinkInputSchemaOptions);
|
|
667
|
-
const dashboardNavigationLinkSchema = linkPayloadSchema;
|
|
668
|
-
const sdkNavigationLinkInputSchema = navigationLinkInputSchema;
|
|
669
|
-
|
|
670
|
-
//#endregion
|
|
671
|
-
//#region ../api/src/sdkThemeWire.ts
|
|
672
|
-
/**
|
|
673
|
-
* Validate a wire value without replacing it with the schema's parsed output.
|
|
674
|
-
* The returned type is deliberately the schema input, not its canonical output.
|
|
675
|
-
*/
|
|
676
|
-
function losslessValidatedSchema(schema) {
|
|
677
|
-
return z.unknown().superRefine((value, ctx) => {
|
|
678
|
-
const parsed = schema.safeParse(value);
|
|
679
|
-
if (parsed.success) return;
|
|
680
|
-
for (const issue of parsed.error.issues) {
|
|
681
|
-
ctx.addIssue({
|
|
682
|
-
code: "custom",
|
|
683
|
-
path: issue.path,
|
|
684
|
-
message: issue.message,
|
|
685
|
-
params: { validationCode: issue.code }
|
|
686
|
-
});
|
|
687
|
-
}
|
|
688
|
-
}).transform((value) => value);
|
|
689
|
-
}
|
|
690
|
-
const sdkThemeWireSchema = losslessValidatedSchema(themeSchema);
|
|
691
|
-
/** Explicitly cross from the lossless wire model into Riverbank's canonical theme. */
|
|
692
|
-
function normalizeSdkThemeWire(theme) {
|
|
693
|
-
return themeSchema.parse(theme);
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
//#endregion
|
|
697
|
-
//#region ../api/src/sdkPullResponseSchemas.ts
|
|
698
|
-
const sdkIdentifierSchema$1 = z.string().min(1);
|
|
699
|
-
const sdkTimestampSchema$2 = z.string().min(1);
|
|
700
|
-
const sdkJsonObjectSchema$1 = z.record(z.string(), z.unknown());
|
|
701
|
-
const sdkEntrySlotBlocksSchema = z.record(z.string(), z.array(z.object({
|
|
702
|
-
identifier: z.string().nullable(),
|
|
703
|
-
kind: sdkIdentifierSchema$1,
|
|
704
|
-
content: sdkJsonObjectSchema$1
|
|
705
|
-
})));
|
|
706
|
-
const sdkPulledEntrySchema = z.object({
|
|
707
|
-
identifier: sdkIdentifierSchema$1,
|
|
708
|
-
title: z.string(),
|
|
709
|
-
data: sdkJsonObjectSchema$1,
|
|
710
|
-
status: z.enum(["draft", "published"]),
|
|
711
|
-
hasUnpublishedChanges: z.boolean(),
|
|
712
|
-
slug: z.string().optional(),
|
|
713
|
-
blocks: sdkEntrySlotBlocksSchema.optional()
|
|
714
|
-
});
|
|
715
|
-
const sdkPullMetaSchema = z.object({ pulledAt: sdkTimestampSchema$2 });
|
|
716
|
-
const sdkPullEntryTimestampsSchema = z.object({
|
|
717
|
-
createdAt: sdkTimestampSchema$2,
|
|
718
|
-
updatedAt: sdkTimestampSchema$2,
|
|
719
|
-
publishedAt: sdkTimestampSchema$2.nullable()
|
|
720
|
-
});
|
|
721
|
-
const sdkPullPaginationSchema = z.object({
|
|
722
|
-
page: z.number().int().nonnegative(),
|
|
723
|
-
limit: z.number().int().positive(),
|
|
724
|
-
total: z.number().int().nonnegative(),
|
|
725
|
-
hasMore: z.boolean()
|
|
726
|
-
});
|
|
727
|
-
const sdkPullTruncationSchema = z.union([
|
|
728
|
-
z.object({
|
|
729
|
-
truncated: z.literal(false),
|
|
730
|
-
returned: z.number().int().nonnegative()
|
|
731
|
-
}).strict(),
|
|
732
|
-
z.object({
|
|
733
|
-
truncated: z.literal(true),
|
|
734
|
-
returned: z.number().int().nonnegative(),
|
|
735
|
-
droppedTypes: z.tuple([sdkIdentifierSchema$1], sdkIdentifierSchema$1),
|
|
736
|
-
capPerType: z.number().int().positive(),
|
|
737
|
-
capTotal: z.number().int().positive().optional()
|
|
738
|
-
}),
|
|
739
|
-
z.object({
|
|
740
|
-
truncated: z.literal(true),
|
|
741
|
-
returned: z.number().int().nonnegative(),
|
|
742
|
-
droppedTypes: z.tuple([sdkIdentifierSchema$1], sdkIdentifierSchema$1),
|
|
743
|
-
capPerType: z.number().int().positive().optional(),
|
|
744
|
-
capTotal: z.number().int().positive()
|
|
745
|
-
})
|
|
746
|
-
]);
|
|
747
|
-
const sdkPullPageSchema = z.object({
|
|
748
|
-
id: sdkIdentifierSchema$1,
|
|
749
|
-
identifier: sdkIdentifierSchema$1,
|
|
750
|
-
title: z.string(),
|
|
751
|
-
path: z.string(),
|
|
752
|
-
status: z.enum(["draft", "published"]),
|
|
753
|
-
hasUnpublishedChanges: z.boolean(),
|
|
754
|
-
updatedAt: sdkTimestampSchema$2,
|
|
755
|
-
createdAt: sdkTimestampSchema$2,
|
|
756
|
-
publishedAt: sdkTimestampSchema$2.nullable(),
|
|
757
|
-
blocks: z.array(z.object({
|
|
758
|
-
id: sdkIdentifierSchema$1,
|
|
759
|
-
identifier: sdkIdentifierSchema$1,
|
|
760
|
-
kind: sdkIdentifierSchema$1,
|
|
761
|
-
position: z.number().int().nonnegative(),
|
|
762
|
-
data: sdkJsonObjectSchema$1,
|
|
763
|
-
createdAt: sdkTimestampSchema$2,
|
|
764
|
-
updatedAt: sdkTimestampSchema$2
|
|
765
|
-
}))
|
|
766
|
-
});
|
|
767
|
-
const sdkPullNavigationItemSchema = z.lazy(() => z.object({
|
|
768
|
-
id: sdkIdentifierSchema$1,
|
|
769
|
-
menuId: sdkIdentifierSchema$1,
|
|
770
|
-
label: z.string(),
|
|
771
|
-
link: linkPayloadSchema.nullable(),
|
|
772
|
-
isCta: z.boolean().optional(),
|
|
773
|
-
position: z.number().int().nonnegative(),
|
|
774
|
-
parentId: sdkIdentifierSchema$1.nullable(),
|
|
775
|
-
children: z.array(sdkPullNavigationItemSchema).optional(),
|
|
776
|
-
resolvedIdentifier: z.string().nullable().optional(),
|
|
777
|
-
resolvedContentType: z.string().nullable().optional()
|
|
778
|
-
}));
|
|
779
|
-
const sdkPullNavigationMenuSchema = z.object({
|
|
780
|
-
id: sdkIdentifierSchema$1,
|
|
781
|
-
siteId: sdkIdentifierSchema$1,
|
|
782
|
-
location: z.enum([
|
|
783
|
-
"main",
|
|
784
|
-
"footer",
|
|
785
|
-
"legal"
|
|
786
|
-
]),
|
|
787
|
-
items: z.array(sdkPullNavigationItemSchema),
|
|
788
|
-
createdAt: sdkTimestampSchema$2,
|
|
789
|
-
updatedAt: sdkTimestampSchema$2
|
|
790
|
-
});
|
|
791
|
-
const siteTitleSchema = z.string().min(1, "Site title is required");
|
|
792
|
-
const sdkSeoDefaultsSchema = z.object({
|
|
793
|
-
title: z.string().optional(),
|
|
794
|
-
description: z.string().optional(),
|
|
795
|
-
ogImage: z.string().optional(),
|
|
796
|
-
allowIndexingInProduction: z.boolean().optional()
|
|
797
|
-
});
|
|
798
|
-
const sdkSettingsUpdateSchema = z.object({
|
|
799
|
-
siteTitle: siteTitleSchema.optional(),
|
|
800
|
-
homepageId: z.string().nullable().optional(),
|
|
801
|
-
seoDefaults: sdkSeoDefaultsSchema.nullable().optional(),
|
|
802
|
-
logoIdentifier: z.string().min(1).nullable().optional(),
|
|
803
|
-
logoDarkBackgroundIdentifier: z.string().min(1).nullable().optional(),
|
|
804
|
-
faviconIdentifier: z.string().min(1).nullable().optional()
|
|
805
|
-
}).strict();
|
|
806
|
-
const sdkPullSettingsSchema = sdkSettingsUpdateSchema.extend({
|
|
807
|
-
homepageId: z.string().nullable(),
|
|
808
|
-
seoDefaults: sdkSeoDefaultsSchema.nullable(),
|
|
809
|
-
verticalConfig: sdkConfiguredBookingVerticalConfigSchema.optional()
|
|
810
|
-
});
|
|
811
|
-
/** A form's schema as the SDK pulls it; the push route extends it. */
|
|
812
|
-
const sdkPullFormSchema = z.object({
|
|
813
|
-
version: z.string().optional(),
|
|
814
|
-
fields: z.array(z.object({
|
|
815
|
-
id: sdkIdentifierSchema$1,
|
|
816
|
-
label: z.string(),
|
|
817
|
-
type: z.string(),
|
|
818
|
-
required: z.boolean().optional(),
|
|
819
|
-
placeholder: z.string().optional(),
|
|
820
|
-
helpText: z.string().optional(),
|
|
821
|
-
layout: z.object({ width: z.enum(["full", "half"]).optional() }).optional(),
|
|
822
|
-
options: z.array(z.object({
|
|
823
|
-
value: z.string(),
|
|
824
|
-
label: z.string()
|
|
825
|
-
})).optional(),
|
|
826
|
-
multiple: z.boolean().optional(),
|
|
827
|
-
minLength: z.number().optional(),
|
|
828
|
-
maxLength: z.number().optional(),
|
|
829
|
-
pattern: z.string().optional(),
|
|
830
|
-
min: z.number().optional(),
|
|
831
|
-
max: z.number().optional()
|
|
832
|
-
}))
|
|
833
|
-
});
|
|
834
|
-
const sdkPullFormSchemaWithId = z.object({
|
|
835
|
-
id: sdkIdentifierSchema$1,
|
|
836
|
-
slug: sdkIdentifierSchema$1,
|
|
837
|
-
name: z.string(),
|
|
838
|
-
schema: sdkPullFormSchema,
|
|
839
|
-
settings: sdkJsonObjectSchema$1,
|
|
840
|
-
updatedAt: sdkTimestampSchema$2,
|
|
841
|
-
createdAt: sdkTimestampSchema$2
|
|
842
|
-
});
|
|
843
|
-
const sdkPulledFormSchema = sdkPullFormSchemaWithId.omit({ id: true });
|
|
844
|
-
const sdkPullVenueSchema = z.object({
|
|
845
|
-
slug: sdkIdentifierSchema$1,
|
|
846
|
-
title: z.string(),
|
|
847
|
-
description: z.string().nullable(),
|
|
848
|
-
timezone: z.string().nullable(),
|
|
849
|
-
capacity: z.number().int().nonnegative().nullable(),
|
|
850
|
-
address: z.object({
|
|
851
|
-
line1: z.string().optional(),
|
|
852
|
-
line2: z.string().optional(),
|
|
853
|
-
city: z.string().optional(),
|
|
854
|
-
region: z.string().optional(),
|
|
855
|
-
postcode: z.string().optional(),
|
|
856
|
-
country: z.string().optional()
|
|
857
|
-
}),
|
|
858
|
-
location: z.object({
|
|
859
|
-
lat: z.number(),
|
|
860
|
-
lng: z.number()
|
|
861
|
-
}).partial().nullable(),
|
|
862
|
-
mapEmbed: z.string().nullable(),
|
|
863
|
-
updatedAt: sdkTimestampSchema$2,
|
|
864
|
-
createdAt: sdkTimestampSchema$2
|
|
865
|
-
});
|
|
866
|
-
const sdkPullEventCategorySchema = z.object({
|
|
867
|
-
identifier: sdkIdentifierSchema$1,
|
|
868
|
-
name: z.string(),
|
|
869
|
-
description: z.string().nullable(),
|
|
870
|
-
color: z.string().nullable(),
|
|
871
|
-
sortOrder: z.number().int(),
|
|
872
|
-
updatedAt: sdkTimestampSchema$2,
|
|
873
|
-
createdAt: sdkTimestampSchema$2
|
|
874
|
-
});
|
|
875
|
-
const sdkEventScheduleSchema = z.object({
|
|
876
|
-
recurrenceRule: z.string().nullable(),
|
|
877
|
-
startDate: z.string().nullable().optional(),
|
|
878
|
-
timezone: z.string().nullable().optional(),
|
|
879
|
-
defaultStartTime: z.string(),
|
|
880
|
-
durationMinutes: z.number().int().nonnegative()
|
|
881
|
-
});
|
|
882
|
-
const sdkPullEventSchema = z.object({
|
|
883
|
-
identifier: sdkIdentifierSchema$1,
|
|
884
|
-
courseBooking: sdkCourseBookingSettingsSchema.optional(),
|
|
885
|
-
administrativeStatus: z.enum(["active", "archived"]),
|
|
886
|
-
title: z.string(),
|
|
887
|
-
slug: sdkIdentifierSchema$1,
|
|
888
|
-
path: z.string().nullable(),
|
|
889
|
-
preset: z.enum(EVENT_PRESET_VALUES),
|
|
890
|
-
categoryRef: z.string().nullable(),
|
|
891
|
-
runRefs: z.tuple([sdkIdentifierSchema$1], sdkIdentifierSchema$1),
|
|
892
|
-
content: sdkJsonObjectSchema$1,
|
|
893
|
-
updatedAt: sdkTimestampSchema$2,
|
|
894
|
-
createdAt: sdkTimestampSchema$2,
|
|
895
|
-
contentUpdatedAt: sdkTimestampSchema$2.nullable(),
|
|
896
|
-
blocks: sdkEntrySlotBlocksSchema.optional()
|
|
897
|
-
});
|
|
898
|
-
const sdkPullEventRunSchema = z.object({
|
|
899
|
-
identifier: sdkIdentifierSchema$1,
|
|
900
|
-
eventRef: sdkIdentifierSchema$1,
|
|
901
|
-
name: z.string().nullable(),
|
|
902
|
-
status: z.enum([
|
|
903
|
-
"draft",
|
|
904
|
-
"active",
|
|
905
|
-
"archived",
|
|
906
|
-
"cancelled",
|
|
907
|
-
"completed"
|
|
908
|
-
]),
|
|
909
|
-
visibility: eventVisibilitySchema.optional(),
|
|
910
|
-
bookingScope: z.enum(["single_occurrence", "whole_series"]),
|
|
911
|
-
occurrences: z.array(sdkEventOccurrenceInputSchema),
|
|
912
|
-
capacity: z.number().int().nonnegative().nullable(),
|
|
913
|
-
bookingOffer: eventBookingOfferIntentSchema,
|
|
914
|
-
venueRef: z.string().nullable(),
|
|
915
|
-
formRef: z.string().nullable(),
|
|
916
|
-
settings: sdkJsonObjectSchema$1,
|
|
917
|
-
presentation: z.unknown().optional(),
|
|
918
|
-
tags: z.array(z.string()),
|
|
919
|
-
updatedAt: sdkTimestampSchema$2,
|
|
920
|
-
createdAt: sdkTimestampSchema$2
|
|
921
|
-
});
|
|
922
|
-
const sdkPullEntriesResponseSchema = z.object({
|
|
923
|
-
entries: z.array(sdkPulledEntrySchema.extend({ contentType: sdkIdentifierSchema$1 })),
|
|
924
|
-
meta: sdkPullMetaSchema.extend({
|
|
925
|
-
entries: z.record(z.string(), sdkPullEntryTimestampsSchema),
|
|
926
|
-
truncation: sdkPullTruncationSchema
|
|
927
|
-
}),
|
|
928
|
-
pagination: sdkPullPaginationSchema
|
|
929
|
-
});
|
|
930
|
-
const sdkPullEntriesByTypeResponseSchema = z.object({
|
|
931
|
-
contentType: sdkIdentifierSchema$1,
|
|
932
|
-
entries: z.array(sdkPulledEntrySchema),
|
|
933
|
-
meta: sdkPullMetaSchema.extend({ entries: z.record(z.string(), sdkPullEntryTimestampsSchema) }),
|
|
934
|
-
pagination: sdkPullPaginationSchema
|
|
935
|
-
});
|
|
936
|
-
const sdkPullEntriesAllResponseSchema = z.object({
|
|
937
|
-
entries: z.record(z.string(), z.array(sdkPulledEntrySchema)),
|
|
938
|
-
meta: sdkPullMetaSchema.extend({
|
|
939
|
-
totalEntries: z.number().int().nonnegative(),
|
|
940
|
-
contentTypes: z.array(sdkIdentifierSchema$1),
|
|
941
|
-
entries: z.record(z.string(), sdkPullEntryTimestampsSchema),
|
|
942
|
-
truncation: sdkPullTruncationSchema
|
|
943
|
-
})
|
|
944
|
-
});
|
|
945
|
-
const sdkPullPagesResponseSchema = z.object({
|
|
946
|
-
pages: z.array(sdkPullPageSchema),
|
|
947
|
-
meta: sdkPullMetaSchema
|
|
948
|
-
});
|
|
949
|
-
const sdkPullNavigationResponseSchema = z.object({
|
|
950
|
-
menus: z.array(sdkPullNavigationMenuSchema),
|
|
951
|
-
meta: sdkPullMetaSchema
|
|
952
|
-
});
|
|
953
|
-
const sdkPullSettingsResponseSchema = z.object({
|
|
954
|
-
settings: sdkPullSettingsSchema,
|
|
955
|
-
warnings: z.array(z.union([z.string(), sdkPullSettingsWarningSchema])).optional(),
|
|
956
|
-
meta: sdkPullMetaSchema.extend({ updatedAt: sdkTimestampSchema$2.nullable() })
|
|
957
|
-
});
|
|
958
|
-
const sdkPullFormsResponseSchema = z.object({
|
|
959
|
-
forms: z.array(sdkPulledFormSchema),
|
|
960
|
-
meta: sdkPullMetaSchema
|
|
961
|
-
});
|
|
962
|
-
const sdkPullThemeResponseSchema = z.object({
|
|
963
|
-
theme: sdkThemeWireSchema.nullable(),
|
|
964
|
-
themeId: sdkIdentifierSchema$1.nullable(),
|
|
965
|
-
selectionId: sdkIdentifierSchema$1.nullable(),
|
|
966
|
-
selectionVersion: z.number().int().nonnegative(),
|
|
967
|
-
meta: sdkPullMetaSchema.extend({ updatedAt: sdkTimestampSchema$2.nullable() })
|
|
968
|
-
}).passthrough();
|
|
969
|
-
const sdkFooterContentSchema = z.object({
|
|
970
|
-
composition: z.unknown().transform((value, ctx) => {
|
|
971
|
-
const parsed = parseFooterCompositionV1(value);
|
|
972
|
-
if (parsed.ok) return parsed.value;
|
|
973
|
-
ctx.addIssue({
|
|
974
|
-
code: "custom",
|
|
975
|
-
message: "Expected a canonical footer composition"
|
|
976
|
-
});
|
|
977
|
-
return z.NEVER;
|
|
978
|
-
}),
|
|
979
|
-
revision: z.number().int().nonnegative()
|
|
980
|
-
}).passthrough();
|
|
981
|
-
const sdkPullFooterResponseSchema = z.object({
|
|
982
|
-
footer: sdkFooterContentSchema.nullable(),
|
|
983
|
-
meta: sdkPullMetaSchema.extend({ updatedAt: sdkTimestampSchema$2.nullable() })
|
|
984
|
-
});
|
|
985
|
-
const sdkPullVenuesResponseSchema = z.object({
|
|
986
|
-
venues: z.array(sdkPullVenueSchema),
|
|
987
|
-
meta: sdkPullMetaSchema
|
|
988
|
-
});
|
|
989
|
-
const sdkPullEventCategoriesResponseSchema = z.object({
|
|
990
|
-
eventCategories: z.array(sdkPullEventCategorySchema),
|
|
991
|
-
meta: sdkPullMetaSchema
|
|
992
|
-
});
|
|
993
|
-
const sdkPullEventsResponseSchema = z.object({
|
|
994
|
-
events: z.array(sdkPullEventSchema),
|
|
995
|
-
eventRuns: z.array(sdkPullEventRunSchema),
|
|
996
|
-
meta: sdkPullMetaSchema
|
|
997
|
-
});
|
|
998
|
-
|
|
999
|
-
//#endregion
|
|
1000
|
-
//#region ../api/src/bookingSettingsSections.ts
|
|
1001
|
-
const BOOKING_SETTINGS_SECTION_LABELS = {
|
|
1002
|
-
basics: "Booking basics",
|
|
1003
|
-
payments: "Payments",
|
|
1004
|
-
events: "Events",
|
|
1005
|
-
appointments: "Appointments",
|
|
1006
|
-
messages: "Messages"
|
|
1007
|
-
};
|
|
1008
|
-
const BOOKING_SETTINGS_SECTION_KEYS = {
|
|
1009
|
-
basics: [
|
|
1010
|
-
"modules",
|
|
1011
|
-
"verticalConfig",
|
|
1012
|
-
"eventTypePresets",
|
|
1013
|
-
"personRecordMode",
|
|
1014
|
-
"defaultTimezone",
|
|
1015
|
-
"calendarDisplay",
|
|
1016
|
-
"icsFeedsEnabled",
|
|
1017
|
-
"waitlists",
|
|
1018
|
-
"teamMemberProfilePages"
|
|
1019
|
-
],
|
|
1020
|
-
payments: [
|
|
1021
|
-
"paymentDefaults",
|
|
1022
|
-
"currency",
|
|
1023
|
-
"refundsIssueCredit"
|
|
1024
|
-
],
|
|
1025
|
-
events: [
|
|
1026
|
-
"defaultVenueId",
|
|
1027
|
-
"eventReminderHours",
|
|
1028
|
-
"eventReminderOverrides",
|
|
1029
|
-
"eventScheduleWindowMonths",
|
|
1030
|
-
"eventOrganizerNotifications",
|
|
1031
|
-
"eventOrganizerEmail",
|
|
1032
|
-
"eventTicketingDefaultEnabled",
|
|
1033
|
-
"collectAllAttendeeDetails",
|
|
1034
|
-
"eventTypeBookingDefaults",
|
|
1035
|
-
"eventBookingPolicies",
|
|
1036
|
-
"defaultEventImage",
|
|
1037
|
-
"eventDestinationPolicy"
|
|
1038
|
-
],
|
|
1039
|
-
appointments: [
|
|
1040
|
-
"appointmentReminderHours",
|
|
1041
|
-
"appointmentReminderOverrides",
|
|
1042
|
-
"appointmentRescheduleHours",
|
|
1043
|
-
"appointmentDefaultMinLeadMinutes",
|
|
1044
|
-
"appointmentDefaultMaxBookingNoticeMinutes",
|
|
1045
|
-
"appointmentBookingPolicies"
|
|
1046
|
-
],
|
|
1047
|
-
messages: [
|
|
1048
|
-
"remindersEnabled",
|
|
1049
|
-
"notificationSettings",
|
|
1050
|
-
"appointmentStaffReminder",
|
|
1051
|
-
"appointmentStaffChangeNotifications",
|
|
1052
|
-
"eventFeedback",
|
|
1053
|
-
"appointmentFollowUp"
|
|
1054
|
-
]
|
|
1055
|
-
};
|
|
1056
|
-
|
|
1057
|
-
//#endregion
|
|
1058
|
-
//#region ../api/src/event-destination-policy.ts
|
|
1059
|
-
const eventDestinationModeValues = ["cms-managed", "system-managed"];
|
|
1060
|
-
const eventDestinationPresetValues = EVENT_PRESET_VALUES;
|
|
1061
|
-
const DEFAULT_EVENT_DESTINATION_POLICY = {
|
|
1062
|
-
defaultMode: "cms-managed",
|
|
1063
|
-
presetOverrides: {}
|
|
1064
|
-
};
|
|
1065
|
-
function isEventDestinationMode(value) {
|
|
1066
|
-
return eventDestinationModeValues.some((mode) => mode === value);
|
|
1067
|
-
}
|
|
1068
|
-
function resolveEventDestinationMode(input) {
|
|
1069
|
-
const policy = input.policy ?? DEFAULT_EVENT_DESTINATION_POLICY;
|
|
1070
|
-
if (input.preset) {
|
|
1071
|
-
const presetOverride = policy.presetOverrides[input.preset];
|
|
1072
|
-
if (presetOverride) return presetOverride;
|
|
1073
|
-
}
|
|
1074
|
-
if (input.publicExperience === "hosted_booking") return "system-managed";
|
|
1075
|
-
return policy.defaultMode;
|
|
1076
|
-
}
|
|
1077
|
-
function macadamiaDefaultEventDestinationPolicy(selectedPresets) {
|
|
1078
|
-
const presetOverrides = {};
|
|
1079
|
-
for (const preset of selectedPresets) {
|
|
1080
|
-
presetOverrides[preset] = preset === "class" ? "system-managed" : "cms-managed";
|
|
1081
|
-
}
|
|
1082
|
-
return {
|
|
1083
|
-
defaultMode: "cms-managed",
|
|
1084
|
-
presetOverrides
|
|
1085
|
-
};
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
//#endregion
|
|
1089
|
-
//#region ../api/src/bookingConfig.ts
|
|
1090
|
-
/** Every option kind; the checkout routes validate the requested kind against it. */
|
|
1091
|
-
const DIRECT_CHECKOUT_COLLECTION_OPTION_KINDS = [
|
|
1092
|
-
"upfront",
|
|
1093
|
-
"deferred_manual",
|
|
1094
|
-
"instalment_plan",
|
|
1095
|
-
"flexible_balance"
|
|
1096
|
-
];
|
|
1097
|
-
const eventOccurrenceRestorationSummarySchema = z.object({
|
|
1098
|
-
restoredOccurrenceIds: z.array(z.string().uuid()),
|
|
1099
|
-
excludedBookings: z.array(z.object({
|
|
1100
|
-
attendeeId: z.string().uuid(),
|
|
1101
|
-
occurrenceId: z.string().uuid(),
|
|
1102
|
-
customerId: z.string().uuid(),
|
|
1103
|
-
customerName: z.string().nullable(),
|
|
1104
|
-
customerEmail: z.string().nullable()
|
|
1105
|
-
}))
|
|
1106
|
-
});
|
|
1107
|
-
const DEFAULT_BOOKING_NOTIFICATION_SEND_RULE = {
|
|
1108
|
-
kind: "send",
|
|
1109
|
-
sendToAssignedStaff: true,
|
|
1110
|
-
extraEmail: null,
|
|
1111
|
-
sendToSiteOwnerIfNoStaff: true
|
|
1112
|
-
};
|
|
1113
|
-
const DEFAULT_BOOKING_NOTIFICATION_SETTINGS = {
|
|
1114
|
-
state: "configured",
|
|
1115
|
-
defaultRule: DEFAULT_BOOKING_NOTIFICATION_SEND_RULE,
|
|
1116
|
-
overrides: {}
|
|
1117
|
-
};
|
|
1118
|
-
const DEFAULT_APPOINTMENT_STAFF_DIGEST_LOCAL_SEND_TIME = "07:00";
|
|
1119
|
-
const BOOKING_SETTINGS_SECTIONS = [
|
|
1120
|
-
"basics",
|
|
1121
|
-
"payments",
|
|
1122
|
-
"events",
|
|
1123
|
-
"appointments",
|
|
1124
|
-
"messages"
|
|
1125
|
-
];
|
|
1126
|
-
const unownedBookingSettingsKeys = {};
|
|
1127
|
-
void unownedBookingSettingsKeys;
|
|
1128
|
-
function pickBookingSettingsSection(settings, section) {
|
|
1129
|
-
const entries = BOOKING_SETTINGS_SECTION_KEYS[section].map((key) => [key, settings[key]]);
|
|
1130
|
-
return Object.fromEntries(entries);
|
|
1131
|
-
}
|
|
1132
|
-
function completeBookingSettingsForSectionUpdate(settings) {
|
|
1133
|
-
return {
|
|
1134
|
-
...settings,
|
|
1135
|
-
eventTypePresets: settings.eventTypePresets ?? DEFAULT_EVENT_TYPE_PRESETS,
|
|
1136
|
-
verticalConfig: settings.verticalConfig ?? {
|
|
1137
|
-
verticalId: null,
|
|
1138
|
-
selectedPresetIds: []
|
|
1139
|
-
},
|
|
1140
|
-
teamMemberProfilePages: settings.teamMemberProfilePages ?? { policy: "selected" },
|
|
1141
|
-
eventDestinationPolicy: settings.eventDestinationPolicy ?? {
|
|
1142
|
-
defaultMode: "cms-managed",
|
|
1143
|
-
presetOverrides: {}
|
|
1144
|
-
},
|
|
1145
|
-
eventTypeBookingDefaults: settings.eventTypeBookingDefaults ?? {}
|
|
1146
|
-
};
|
|
1147
|
-
}
|
|
1148
|
-
function createBookingSettingsSectionUpdate(settings, section) {
|
|
1149
|
-
const completeSettings = completeBookingSettingsForSectionUpdate(settings);
|
|
1150
|
-
switch (section) {
|
|
1151
|
-
case "basics": return {
|
|
1152
|
-
section,
|
|
1153
|
-
settings: pickBookingSettingsSection(completeSettings, section)
|
|
1154
|
-
};
|
|
1155
|
-
case "payments": return {
|
|
1156
|
-
section,
|
|
1157
|
-
settings: pickBookingSettingsSection(completeSettings, section)
|
|
1158
|
-
};
|
|
1159
|
-
case "events": return {
|
|
1160
|
-
section,
|
|
1161
|
-
settings: pickBookingSettingsSection(completeSettings, section)
|
|
1162
|
-
};
|
|
1163
|
-
case "appointments": return {
|
|
1164
|
-
section,
|
|
1165
|
-
settings: pickBookingSettingsSection(completeSettings, section)
|
|
1166
|
-
};
|
|
1167
|
-
case "messages": return {
|
|
1168
|
-
section,
|
|
1169
|
-
settings: pickBookingSettingsSection(completeSettings, section)
|
|
1170
|
-
};
|
|
1171
|
-
}
|
|
1172
|
-
}
|
|
1173
|
-
function createBookingSettingsUpdateRequest(settings, sections = BOOKING_SETTINGS_SECTIONS) {
|
|
1174
|
-
return { updates: sections.map((section) => createBookingSettingsSectionUpdate(settings, section)) };
|
|
1175
|
-
}
|
|
1176
|
-
function applyBookingSettingsSectionUpdate(settings, update) {
|
|
1177
|
-
return {
|
|
1178
|
-
...settings,
|
|
1179
|
-
...update.settings
|
|
1180
|
-
};
|
|
1181
|
-
}
|
|
1182
|
-
function applyBookingSettingsSectionUpdates(settings, updates) {
|
|
1183
|
-
return updates.reduce(applyBookingSettingsSectionUpdate, settings);
|
|
1184
|
-
}
|
|
1185
|
-
/** Normalize optional wire defaults once before projecting operational Event policy. */
|
|
1186
|
-
function resolveBookingSettingsEventCatalogue(settings) {
|
|
1187
|
-
return resolveEventCatalogue({
|
|
1188
|
-
verticalConfig: settings.verticalConfig,
|
|
1189
|
-
eventTypePresets: settings.eventTypePresets ?? DEFAULT_EVENT_TYPE_PRESETS
|
|
1190
|
-
});
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
//#endregion
|
|
1194
|
-
//#region ../api/src/sdkManagementResponseSchemas.ts
|
|
1195
|
-
const sdkIdentifierSchema = z.string().min(1);
|
|
1196
|
-
const sdkTimestampSchema$1 = z.string().min(1);
|
|
1197
|
-
const sdkJsonObjectSchema = z.record(z.string(), z.unknown());
|
|
1198
|
-
const sdkManagementEntrySchema = z.object({
|
|
1199
|
-
id: sdkIdentifierSchema,
|
|
1200
|
-
identifier: sdkIdentifierSchema,
|
|
1201
|
-
contentType: sdkIdentifierSchema,
|
|
1202
|
-
slug: z.string().optional(),
|
|
1203
|
-
title: z.string(),
|
|
1204
|
-
data: sdkJsonObjectSchema,
|
|
1205
|
-
publishedData: sdkJsonObjectSchema.nullable().optional(),
|
|
1206
|
-
status: z.enum(["draft", "published"]),
|
|
1207
|
-
hasUnpublishedChanges: z.boolean(),
|
|
1208
|
-
createdAt: sdkTimestampSchema$1,
|
|
1209
|
-
updatedAt: sdkTimestampSchema$1,
|
|
1210
|
-
publishedAt: sdkTimestampSchema$1.nullable(),
|
|
1211
|
-
manualOrder: z.number(),
|
|
1212
|
-
blocks: sdkEntrySlotBlocksSchema.optional()
|
|
1213
|
-
});
|
|
1214
|
-
const sdkManagementEntryWithoutPublishedDataSchema = sdkManagementEntrySchema.omit({ publishedData: true });
|
|
1215
|
-
const sdkManagementPageSchema = z.object({
|
|
1216
|
-
id: sdkIdentifierSchema,
|
|
1217
|
-
identifier: sdkIdentifierSchema,
|
|
1218
|
-
title: z.string(),
|
|
1219
|
-
path: z.string(),
|
|
1220
|
-
status: z.enum(["draft", "published"]),
|
|
1221
|
-
hasUnpublishedChanges: z.boolean(),
|
|
1222
|
-
createdAt: sdkTimestampSchema$1,
|
|
1223
|
-
updatedAt: sdkTimestampSchema$1,
|
|
1224
|
-
publishedAt: sdkTimestampSchema$1.nullable()
|
|
1225
|
-
});
|
|
1226
|
-
const sdkManagementBlockSchema = z.object({
|
|
1227
|
-
id: sdkIdentifierSchema,
|
|
1228
|
-
identifier: sdkIdentifierSchema,
|
|
1229
|
-
kind: sdkIdentifierSchema,
|
|
1230
|
-
position: z.number().int().nonnegative(),
|
|
1231
|
-
data: sdkJsonObjectSchema,
|
|
1232
|
-
createdAt: sdkTimestampSchema$1,
|
|
1233
|
-
updatedAt: sdkTimestampSchema$1
|
|
1234
|
-
});
|
|
1235
|
-
const sdkManagementNavigationMenuSchema = sdkPullNavigationMenuSchema.omit({ siteId: true });
|
|
1236
|
-
const sdkPublicationOutcomeSchema = (code) => z.union([z.object({ published: z.literal(true) }), z.object({
|
|
1237
|
-
published: z.literal(false),
|
|
1238
|
-
publishError: z.object({
|
|
1239
|
-
code: z.literal(code),
|
|
1240
|
-
message: z.string().min(1)
|
|
1241
|
-
})
|
|
1242
|
-
})]);
|
|
1243
|
-
const sdkUpsertEntryDraftResponseSchema = sdkCommandApplicationResultSchema(sdkManagementEntryWithoutPublishedDataSchema).extend({
|
|
1244
|
-
saved: z.never().optional(),
|
|
1245
|
-
publication: z.never().optional()
|
|
1246
|
-
});
|
|
1247
|
-
const sdkUpsertEntryPublishResponseSchema = sdkCommandApplicationResultSchema(sdkManagementEntryWithoutPublishedDataSchema).extend({
|
|
1248
|
-
saved: z.literal(true),
|
|
1249
|
-
publication: sdkPublicationOutcomeSchema("PUBLISH_ENTRY_FAILED")
|
|
1250
|
-
});
|
|
1251
|
-
const sdkUpsertPageDraftResponseSchema = sdkCommandApplicationResultSchema(sdkManagementPageSchema).extend({
|
|
1252
|
-
saved: z.never().optional(),
|
|
1253
|
-
publication: z.never().optional()
|
|
1254
|
-
});
|
|
1255
|
-
const sdkUpsertPagePublishResponseSchema = sdkCommandApplicationResultSchema(sdkManagementPageSchema).extend({
|
|
1256
|
-
saved: z.literal(true),
|
|
1257
|
-
publication: sdkPublicationOutcomeSchema("PUBLISH_PAGE_FAILED")
|
|
1258
|
-
});
|
|
1259
|
-
const sdkUpsertPageResponseSchema = z.union([sdkUpsertPageDraftResponseSchema, sdkUpsertPagePublishResponseSchema]);
|
|
1260
|
-
const sdkUpsertEntryResponseSchema = z.union([sdkUpsertEntryDraftResponseSchema, sdkUpsertEntryPublishResponseSchema]);
|
|
1261
|
-
const sdkDeleteResponseSchema = sdkCommandApplicationResultSchema(z.object({ deleted: z.literal(true) }));
|
|
1262
|
-
const sdkDeleteBlockResponseSchema = sdkDeleteResponseSchema.extend({ designSyncWarning: pageBlockDesignSyncWarningSchema.optional() });
|
|
1263
|
-
const sdkBlockContentWarningSchema = z.object({
|
|
1264
|
-
field: z.string(),
|
|
1265
|
-
code: z.literal("theme-unavailable-variant"),
|
|
1266
|
-
message: z.string(),
|
|
1267
|
-
value: z.string()
|
|
1268
|
-
});
|
|
1269
|
-
const sdkUpsertBlockResponseSchema = sdkCommandApplicationResultSchema(sdkManagementBlockSchema).extend({
|
|
1270
|
-
warnings: z.array(sdkBlockContentWarningSchema).optional(),
|
|
1271
|
-
appearance: sdkBlockAppearanceOutcomeSchema.optional(),
|
|
1272
|
-
designSyncWarning: pageBlockDesignSyncWarningSchema.optional()
|
|
1273
|
-
});
|
|
1274
|
-
const sdkPatchPageBlockResponseSchema = z.object({
|
|
1275
|
-
block: z.object({
|
|
1276
|
-
id: sdkIdentifierSchema,
|
|
1277
|
-
identifier: sdkIdentifierSchema,
|
|
1278
|
-
kind: sdkIdentifierSchema,
|
|
1279
|
-
position: z.number().int().nonnegative(),
|
|
1280
|
-
data: sdkJsonObjectSchema,
|
|
1281
|
-
contentHash: sdkIdentifierSchema
|
|
1282
|
-
}),
|
|
1283
|
-
warnings: z.array(sdkBlockContentWarningSchema).optional()
|
|
1284
|
-
});
|
|
1285
|
-
const sdkUpsertNavigationMenuResponseSchema = sdkCommandApplicationResultSchema(sdkManagementNavigationMenuSchema);
|
|
1286
|
-
const sdkUpsertSettingsResponseSchema = sdkCommandApplicationResultSchema(sdkPullSettingsSchema);
|
|
1287
|
-
const sdkUpsertFormResponseSchema = sdkCommandApplicationResultSchema(sdkPullFormSchemaWithId);
|
|
1288
|
-
const sdkUpsertVenueResponseSchema = sdkCommandApplicationResultSchema(sdkPullVenueSchema.extend({ id: sdkIdentifierSchema }));
|
|
1289
|
-
const sdkUpsertEventCategoryResponseSchema = sdkCommandApplicationResultSchema(sdkPullEventCategorySchema.extend({ id: sdkIdentifierSchema }));
|
|
1290
|
-
const sdkManagementEventSchema = sdkPullEventSchema.extend({
|
|
1291
|
-
id: sdkIdentifierSchema.transform(unsafeAsEventOfferingId),
|
|
1292
|
-
pageState: z.enum(["online", "offline"]).optional()
|
|
1293
|
-
});
|
|
1294
|
-
const sdkManagementEventRunSchema = sdkPullEventRunSchema.extend({
|
|
1295
|
-
id: sdkIdentifierSchema.transform(unsafeAsEventSeriesId),
|
|
1296
|
-
eventId: sdkIdentifierSchema.transform(unsafeAsEventOfferingId),
|
|
1297
|
-
schedule: sdkEventScheduleSchema
|
|
1298
|
-
});
|
|
1299
|
-
const sdkManagementEventGraphSchema = z.object({
|
|
1300
|
-
event: sdkManagementEventSchema,
|
|
1301
|
-
runs: z.tuple([sdkManagementEventRunSchema], sdkManagementEventRunSchema)
|
|
1302
|
-
});
|
|
1303
|
-
const sdkListEventsResponseSchema = z.object({
|
|
1304
|
-
events: z.array(sdkManagementEventSchema),
|
|
1305
|
-
eventRuns: z.array(sdkManagementEventRunSchema)
|
|
1306
|
-
});
|
|
1307
|
-
const sdkGetEventResponseSchema = sdkManagementEventGraphSchema;
|
|
1308
|
-
const sdkEventSeriesIdSchema = sdkIdentifierSchema.transform(unsafeAsEventSeriesId);
|
|
1309
|
-
/**
|
|
1310
|
-
* The lifecycle outcome for `POST /sdk/{siteId}/events/{identifier}/lifecycle`.
|
|
1311
|
-
* `applied` always changed something: at least one run, or the Event page.
|
|
1312
|
-
*/
|
|
1313
|
-
const sdkTakeEventOfflineResponseSchema = z.union([
|
|
1314
|
-
z.object({
|
|
1315
|
-
kind: z.literal("applied"),
|
|
1316
|
-
changedRunIds: z.tuple([sdkEventSeriesIdSchema], sdkEventSeriesIdSchema),
|
|
1317
|
-
pageTakenOffline: z.boolean()
|
|
1318
|
-
}),
|
|
1319
|
-
z.object({
|
|
1320
|
-
kind: z.literal("applied"),
|
|
1321
|
-
changedRunIds: z.tuple([]),
|
|
1322
|
-
pageTakenOffline: z.literal(true)
|
|
1323
|
-
}),
|
|
1324
|
-
z.object({ kind: z.literal("already-offline") })
|
|
1325
|
-
]);
|
|
1326
|
-
const sdkUpsertEventResponseSchema = z.union([sdkCommandApplicationResultSchema(sdkManagementEventGraphSchema).extend({
|
|
1327
|
-
cancellationResult: businessCancellationResultSchema.optional(),
|
|
1328
|
-
restoration: eventOccurrenceRestorationSummarySchema.optional()
|
|
1329
|
-
}), z.object({
|
|
1330
|
-
kind: z.literal("accepted_unavailable"),
|
|
1331
|
-
id: sdkEventSeriesIdSchema,
|
|
1332
|
-
effect: z.object({ kind: z.literal("updated") }),
|
|
1333
|
-
cancellationResult: businessCancellationResultSchema
|
|
1334
|
-
})]);
|
|
1335
|
-
const sdkUpsertThemeResponseSchema = sdkCommandApplicationResultSchema(z.object({
|
|
1336
|
-
themeId: sdkIdentifierSchema,
|
|
1337
|
-
selectionId: sdkIdentifierSchema,
|
|
1338
|
-
savedAt: sdkTimestampSchema$1
|
|
1339
|
-
}));
|
|
1340
|
-
const sdkApplySiteStyleSelectionResponseSchema = sdkCommandApplicationResultSchema(z.object({
|
|
1341
|
-
success: z.boolean(),
|
|
1342
|
-
themeId: sdkIdentifierSchema,
|
|
1343
|
-
selectionId: sdkIdentifierSchema,
|
|
1344
|
-
selectionVersion: z.number().int().nonnegative(),
|
|
1345
|
-
selectionUpdatedAt: sdkTimestampSchema$1.nullable(),
|
|
1346
|
-
savedAt: sdkTimestampSchema$1,
|
|
1347
|
-
theme: themeSchema,
|
|
1348
|
-
siteStyleId: sdkIdentifierSchema,
|
|
1349
|
-
buttonStyleSelection: buttonStyleSelectionSchema,
|
|
1350
|
-
headerLayoutId: sdkIdentifierSchema,
|
|
1351
|
-
headerStyleId: sdkIdentifierSchema,
|
|
1352
|
-
footerLookId: sdkIdentifierSchema,
|
|
1353
|
-
footerPreferences: footerPreferencesSchema
|
|
1354
|
-
}));
|
|
1355
|
-
const sdkUpsertFooterResponseSchema = sdkCommandApplicationResultSchema(sdkFooterContentSchema.nullable());
|
|
1356
|
-
|
|
1357
|
-
//#endregion
|
|
1358
|
-
//#region ../api/src/sdkResponseSchemas.ts
|
|
1359
|
-
const sdkTimestampSchema = z.string().min(1);
|
|
1360
|
-
const sdkConfigureBookingVerticalResponseSchema = sdkCommandApplicationResultSchema(z.object({
|
|
1361
|
-
verticalConfig: sdkConfiguredBookingVerticalConfigSchema,
|
|
1362
|
-
revision: sdkTimestampSchema.nullable()
|
|
1363
|
-
})).extend({ warnings: z.array(sdkBookingSettingsWarningSchema).optional() });
|
|
1364
|
-
|
|
1365
|
-
//#endregion
|
|
1366
|
-
//#region ../management-client/src/responseValidation.ts
|
|
1367
|
-
function isRecord$3(value) {
|
|
1368
|
-
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
1369
|
-
}
|
|
1370
|
-
function pullSchemaForRequest(path$2) {
|
|
1371
|
-
switch (path$2) {
|
|
1372
|
-
case "/pull/entries/all": return sdkPullEntriesAllResponseSchema;
|
|
1373
|
-
case "/pull/entries": return sdkPullEntriesResponseSchema;
|
|
1374
|
-
case "/pull/pages": return sdkPullPagesResponseSchema;
|
|
1375
|
-
case "/pull/navigation": return sdkPullNavigationResponseSchema;
|
|
1376
|
-
case "/pull/settings": return sdkPullSettingsResponseSchema;
|
|
1377
|
-
case "/pull/forms": return sdkPullFormsResponseSchema;
|
|
1378
|
-
case "/pull/footer": return sdkPullFooterResponseSchema;
|
|
1379
|
-
case "/pull/venues": return sdkPullVenuesResponseSchema;
|
|
1380
|
-
case "/pull/event-categories": return sdkPullEventCategoriesResponseSchema;
|
|
1381
|
-
case "/pull/events": return sdkPullEventsResponseSchema;
|
|
1382
|
-
default: return /^\/pull\/entries\/[^/]+$/.test(path$2) ? sdkPullEntriesByTypeResponseSchema : undefined;
|
|
1383
|
-
}
|
|
1384
|
-
}
|
|
1385
|
-
function rootMutationSchema(method, path$2, body) {
|
|
1386
|
-
if (method === "POST") {
|
|
1387
|
-
switch (path$2) {
|
|
1388
|
-
case "/entries": return isRecord$3(body) && body.publication === "publish" ? sdkUpsertEntryPublishResponseSchema : sdkUpsertEntryDraftResponseSchema;
|
|
1389
|
-
case "/pages": return isRecord$3(body) && body.publication === "publish" ? sdkUpsertPagePublishResponseSchema : sdkUpsertPageDraftResponseSchema;
|
|
1390
|
-
case "/navigation": return sdkUpsertNavigationMenuResponseSchema;
|
|
1391
|
-
case "/forms": return sdkUpsertFormResponseSchema;
|
|
1392
|
-
case "/venues": return sdkUpsertVenueResponseSchema;
|
|
1393
|
-
case "/event-categories": return sdkUpsertEventCategoryResponseSchema;
|
|
1394
|
-
case "/events": return sdkUpsertEventResponseSchema;
|
|
1395
|
-
case "/footer": return sdkUpsertFooterResponseSchema;
|
|
1396
|
-
}
|
|
1397
|
-
}
|
|
1398
|
-
if (method === "PATCH" && path$2 === "/settings") return sdkUpsertSettingsResponseSchema;
|
|
1399
|
-
if (method === "POST" && path$2 === "/settings/booking/vertical") return sdkConfigureBookingVerticalResponseSchema;
|
|
1400
|
-
return undefined;
|
|
1401
|
-
}
|
|
1402
|
-
function isDeleteMutationPath(path$2) {
|
|
1403
|
-
return [
|
|
1404
|
-
/^\/entries\/[^/]+\/[^/]+$/,
|
|
1405
|
-
/^\/pages\/[^/]+$/,
|
|
1406
|
-
/^\/pages\/[^/]+\/blocks\/[^/]+$/,
|
|
1407
|
-
/^\/navigation\/[^/]+$/,
|
|
1408
|
-
/^\/forms\/[^/]+$/,
|
|
1409
|
-
/^\/venues\/[^/]+$/,
|
|
1410
|
-
/^\/event-categories\/[^/]+$/,
|
|
1411
|
-
/^\/events\/[^/]+$/
|
|
1412
|
-
].some((pattern) => pattern.test(path$2));
|
|
1413
|
-
}
|
|
1414
|
-
function blockMutationSchema(method, path$2) {
|
|
1415
|
-
if (method === "POST" && /^\/pages\/[^/]+\/blocks$/.test(path$2)) return sdkUpsertBlockResponseSchema;
|
|
1416
|
-
if (method === "POST" && /^\/pages\/[^/]+\/blocks\/reorder$/.test(path$2)) return sdkReorderResponseSchema;
|
|
1417
|
-
if (method === "PATCH" && /^\/pages\/[^/]+\/blocks\/[^/]+$/.test(path$2)) return sdkPatchPageBlockResponseSchema;
|
|
1418
|
-
if (method === "DELETE" && /^\/pages\/[^/]+\/blocks\/[^/]+$/.test(path$2)) return sdkDeleteBlockResponseSchema;
|
|
1419
|
-
return undefined;
|
|
1420
|
-
}
|
|
1421
|
-
function nestedMutationSchema(method, path$2) {
|
|
1422
|
-
const blockSchema = blockMutationSchema(method, path$2);
|
|
1423
|
-
if (blockSchema) return blockSchema;
|
|
1424
|
-
if (method === "POST" && /^\/entries\/[^/]+\/[^/]+\/(publish|unpublish)$/.test(path$2)) {
|
|
1425
|
-
return sdkCommandApplicationResultSchema(sdkUpsertEntryDraftResponseSchema.shape.value);
|
|
1426
|
-
}
|
|
1427
|
-
if (method === "POST" && /^\/pages\/[^/]+\/(publish|unpublish)$/.test(path$2)) {
|
|
1428
|
-
return sdkCommandApplicationResultSchema(sdkUpsertPageDraftResponseSchema.shape.value);
|
|
1429
|
-
}
|
|
1430
|
-
if (method === "DELETE" && isDeleteMutationPath(path$2)) return sdkDeleteResponseSchema;
|
|
1431
|
-
return undefined;
|
|
1432
|
-
}
|
|
1433
|
-
function occurrenceResponseSchema(method, path$2) {
|
|
1434
|
-
const collection = /^\/events\/[^/]+\/runs\/[^/]+\/occurrences$/;
|
|
1435
|
-
const member = /^\/events\/[^/]+\/runs\/[^/]+\/occurrences\/[^/]+$/;
|
|
1436
|
-
if (method === "GET" && collection.test(path$2)) return sdkListEventOccurrencesResponseSchema;
|
|
1437
|
-
if (method === "POST" && collection.test(path$2)) return sdkEventOccurrenceResponseSchema;
|
|
1438
|
-
if (method === "PATCH" && member.test(path$2)) return sdkEventOccurrenceResponseSchema;
|
|
1439
|
-
if (method === "DELETE" && member.test(path$2)) return sdkDeleteEventOccurrenceResponseSchema;
|
|
1440
|
-
return method === "POST" && /^\/events\/[^/]+\/runs\/[^/]+\/occurrences\/[^/]+\/cancel$/.test(path$2) ? sdkEventOccurrenceResponseSchema : undefined;
|
|
1441
|
-
}
|
|
1442
|
-
function mediaResponseSchema(method, path$2) {
|
|
1443
|
-
if (method === "GET" && path$2 === "/media") return sdkMediaListResponseSchema;
|
|
1444
|
-
if (method === "GET" && /^\/media\/[^/]+\/download$/.test(path$2)) return sdkMediaDownloadResponseSchema;
|
|
1445
|
-
if (method === "POST" && /^\/media\/[^/]+\/delete-preview$/.test(path$2)) return sdkMediaDeletePreviewResponseSchema;
|
|
1446
|
-
if (method === "DELETE" && /^\/media\/[^/]+$/.test(path$2)) return sdkMediaDeleteApplyResponseSchema;
|
|
1447
|
-
return undefined;
|
|
1448
|
-
}
|
|
1449
|
-
function schemaForManagementRequest(method, path$2, body) {
|
|
1450
|
-
if (method === "GET") {
|
|
1451
|
-
return pullSchemaForRequest(path$2) ?? occurrenceResponseSchema(method, path$2) ?? mediaResponseSchema(method, path$2);
|
|
1452
|
-
}
|
|
1453
|
-
return rootMutationSchema(method, path$2, body) ?? occurrenceResponseSchema(method, path$2) ?? mediaResponseSchema(method, path$2) ?? nestedMutationSchema(method, path$2);
|
|
1454
|
-
}
|
|
1455
|
-
function validateManagementResponse(value, schema) {
|
|
1456
|
-
const parsed = schema.safeParse(value);
|
|
1457
|
-
if (parsed.success) return { valid: true };
|
|
1458
|
-
return {
|
|
1459
|
-
valid: false,
|
|
1460
|
-
details: { issues: parsed.error.issues.slice(0, 8).map((issue) => ({
|
|
1461
|
-
path: issue.path.map(String),
|
|
1462
|
-
message: issue.message,
|
|
1463
|
-
...issue.code ? { code: issue.code } : {}
|
|
1464
|
-
})) }
|
|
1465
|
-
};
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
//#endregion
|
|
1469
|
-
//#region ../management-client/src/transportFailure.ts
|
|
1470
|
-
/**
|
|
1471
|
-
* Sanitised transport failures (issue 1496).
|
|
1472
|
-
*
|
|
1473
|
-
* When `fetch` rejects, the caught value can carry anything the platform put
|
|
1474
|
-
* there: a message naming the host and port, a cause chain, a stack, and in
|
|
1475
|
-
* some runtimes echoes of the request. None of that may leave the client. The
|
|
1476
|
-
* caught value is inspected here, and only here, for one thing: whether its
|
|
1477
|
-
* platform code (`cause.code`, else `code`) is one of an allow-listed set of
|
|
1478
|
-
* Node and undici codes. The result is a closed union whose `code` is always
|
|
1479
|
-
* one of those literals, and a fixed message per kind. Nothing else from the caught value is kept.
|
|
1480
|
-
*
|
|
1481
|
-
* Code lists were checked against Node 24.18 (undici bundled with it) by
|
|
1482
|
-
* provoking each failure: DNS lookups fail with `ENOTFOUND`/`EAI_AGAIN`, a
|
|
1483
|
-
* closed port with `ECONNREFUSED` (an `AggregateError` carrying the same code
|
|
1484
|
-
* when happy-eyeballs tries several addresses), an unroutable host with
|
|
1485
|
-
* undici's `UND_ERR_CONNECT_TIMEOUT`, a peer reset with `ECONNRESET`, a peer
|
|
1486
|
-
* closing mid-response with `UND_ERR_SOCKET`, and TLS failures with the
|
|
1487
|
-
* OpenSSL verification codes and `ERR_TLS_CERT_ALTNAME_INVALID`.
|
|
1488
|
-
*
|
|
1489
|
-
* Bun puts its code on the error itself rather than on `cause`, so that is read
|
|
1490
|
-
* as a fallback. Bun 1.3 reports resets and TLS failures with the same codes
|
|
1491
|
-
* as Node; it reports refused connections *and* failed DNS lookups alike as
|
|
1492
|
-
* `ConnectionRefused`, which is deliberately not listed and stays `unknown`.
|
|
1493
|
-
*/
|
|
1494
|
-
const DNS_CODES = ["ENOTFOUND", "EAI_AGAIN"];
|
|
1495
|
-
const CONNECT_CODES = [
|
|
1496
|
-
"ECONNREFUSED",
|
|
1497
|
-
"UND_ERR_CONNECT_TIMEOUT",
|
|
1498
|
-
"ECONNABORTED",
|
|
1499
|
-
"EHOSTUNREACH",
|
|
1500
|
-
"ENETUNREACH"
|
|
1501
|
-
];
|
|
1502
|
-
const RESET_CODES = [
|
|
1503
|
-
"ECONNRESET",
|
|
1504
|
-
"EPIPE",
|
|
1505
|
-
"UND_ERR_SOCKET"
|
|
1506
|
-
];
|
|
1507
|
-
const TLS_CODES = [
|
|
1508
|
-
"CERT_HAS_EXPIRED",
|
|
1509
|
-
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
|
1510
|
-
"SELF_SIGNED_CERT_IN_CHAIN",
|
|
1511
|
-
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
|
1512
|
-
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
|
|
1513
|
-
"ERR_TLS_CERT_ALTNAME_INVALID",
|
|
1514
|
-
"ERR_SSL_WRONG_VERSION_NUMBER"
|
|
1515
|
-
];
|
|
1516
|
-
const TIMEOUT_CODES = [
|
|
1517
|
-
"UND_ERR_HEADERS_TIMEOUT",
|
|
1518
|
-
"UND_ERR_BODY_TIMEOUT",
|
|
1519
|
-
"ETIMEDOUT"
|
|
1520
|
-
];
|
|
1521
|
-
function isOneOf(values, value) {
|
|
1522
|
-
return values.includes(value);
|
|
1523
|
-
}
|
|
1524
|
-
/**
|
|
1525
|
-
* Read one property of an arbitrary value. A throwing getter or a hostile
|
|
1526
|
-
* proxy yields `undefined`, so inspection can never throw past the boundary.
|
|
1527
|
-
*/
|
|
1528
|
-
function readProperty(value, key) {
|
|
1529
|
-
if (typeof value !== "object" || value === null) return undefined;
|
|
1530
|
-
try {
|
|
1531
|
-
return Reflect.get(value, key);
|
|
1532
|
-
} catch {
|
|
1533
|
-
return undefined;
|
|
1534
|
-
}
|
|
1535
|
-
}
|
|
1536
|
-
function stringCode(value) {
|
|
1537
|
-
const code = readProperty(value, "code");
|
|
1538
|
-
return typeof code === "string" ? code : undefined;
|
|
1539
|
-
}
|
|
1540
|
-
/**
|
|
1541
|
-
* The platform code for a rejected `fetch`: Node/undici put it on `cause`;
|
|
1542
|
-
* Bun puts its own on the error itself. Nothing else is read.
|
|
1543
|
-
*/
|
|
1544
|
-
function platformCode(caught) {
|
|
1545
|
-
return stringCode(readProperty(caught, "cause")) ?? stringCode(caught);
|
|
1546
|
-
}
|
|
1547
|
-
/** Map a raw code onto the closed union; anything unlisted is `unknown`. */
|
|
1548
|
-
function transportFailureFromCode(code) {
|
|
1549
|
-
if (code === undefined) return { kind: "unknown" };
|
|
1550
|
-
if (isOneOf(DNS_CODES, code)) return {
|
|
1551
|
-
kind: "dns",
|
|
1552
|
-
code
|
|
1553
|
-
};
|
|
1554
|
-
if (isOneOf(CONNECT_CODES, code)) return {
|
|
1555
|
-
kind: "connect",
|
|
1556
|
-
code
|
|
1557
|
-
};
|
|
1558
|
-
if (isOneOf(RESET_CODES, code)) return {
|
|
1559
|
-
kind: "reset",
|
|
1560
|
-
code
|
|
1561
|
-
};
|
|
1562
|
-
if (isOneOf(TLS_CODES, code)) return {
|
|
1563
|
-
kind: "tls",
|
|
1564
|
-
code
|
|
1565
|
-
};
|
|
1566
|
-
if (isOneOf(TIMEOUT_CODES, code)) return {
|
|
1567
|
-
kind: "timeout",
|
|
1568
|
-
code
|
|
1569
|
-
};
|
|
1570
|
-
return { kind: "unknown" };
|
|
1571
|
-
}
|
|
1572
|
-
/** True when the client's own `AbortSignal.timeout` ended the request. */
|
|
1573
|
-
function isClientTimeout(caught) {
|
|
1574
|
-
return readProperty(caught, "name") === "TimeoutError";
|
|
1575
|
-
}
|
|
1576
|
-
/**
|
|
1577
|
-
* Classify a rejected `fetch` or body read. The caught value is read only for
|
|
1578
|
-
* its `name` and platform code (guarded, see `readProperty`). `timeoutMs` is
|
|
1579
|
-
* the limit the client itself applied, so the timeout message can name it.
|
|
1580
|
-
*/
|
|
1581
|
-
function classifyTransportFailure(caught, timeoutMs) {
|
|
1582
|
-
return isClientTimeout(caught) ? {
|
|
1583
|
-
code: "sdk:timeout",
|
|
1584
|
-
transport: { kind: "timeout" },
|
|
1585
|
-
timeoutMs
|
|
1586
|
-
} : {
|
|
1587
|
-
code: "sdk:network-error",
|
|
1588
|
-
transport: transportFailureFromCode(platformCode(caught))
|
|
1589
|
-
};
|
|
1590
|
-
}
|
|
1591
|
-
/** The fixed message for a classified failure. */
|
|
1592
|
-
function transportErrorMessage(failure) {
|
|
1593
|
-
return failure.code === "sdk:timeout" ? `Request timed out after ${failure.timeoutMs}ms` : transportFailureMessage(failure.transport);
|
|
1594
|
-
}
|
|
1595
|
-
/** The fixed, safe message for each kind of network failure. */
|
|
1596
|
-
function transportFailureMessage(failure) {
|
|
1597
|
-
switch (failure.kind) {
|
|
1598
|
-
case "dns": return "Could not resolve the dashboard hostname";
|
|
1599
|
-
case "connect": return "Could not open a connection to the dashboard";
|
|
1600
|
-
case "reset": return "The connection to the dashboard closed before a response arrived";
|
|
1601
|
-
case "tls": return "The TLS handshake with the dashboard failed";
|
|
1602
|
-
case "timeout": return "The connection to the dashboard timed out";
|
|
1603
|
-
case "unknown": return "Network request failed";
|
|
1604
|
-
}
|
|
1605
|
-
}
|
|
1606
|
-
|
|
1607
|
-
//#endregion
|
|
1608
|
-
//#region ../management-client/src/http.ts
|
|
1609
|
-
/**
|
|
1610
|
-
* Error thrown when management API request fails
|
|
1611
|
-
*/
|
|
1612
|
-
var ManagementApiError = class extends Error {
|
|
1613
|
-
constructor(message, code, details, statusCode, request, tag) {
|
|
1614
|
-
super(message);
|
|
1615
|
-
this.name = "ManagementApiError";
|
|
1616
|
-
this.code = code;
|
|
1617
|
-
this.details = details;
|
|
1618
|
-
this.statusCode = statusCode;
|
|
1619
|
-
this.request = request;
|
|
1620
|
-
this.tag = tag;
|
|
1621
|
-
}
|
|
1622
|
-
};
|
|
1623
|
-
/** A successful HTTP response whose payload violated its runtime contract. */
|
|
1624
|
-
var ManagementInvalidResponseError = class extends ManagementApiError {
|
|
1625
|
-
constructor(details, statusCode, request) {
|
|
1626
|
-
super("Management API returned an invalid response", "invalid_response", details, statusCode, request);
|
|
1627
|
-
this.code = "invalid_response";
|
|
1628
|
-
this.name = "ManagementInvalidResponseError";
|
|
1629
|
-
this.details = details;
|
|
1630
|
-
this.request = request;
|
|
1631
|
-
}
|
|
1632
|
-
};
|
|
1633
|
-
/**
|
|
1634
|
-
* The request never produced a dashboard answer. The message is fixed per
|
|
1635
|
-
* failure kind and nothing from the platform error (its message, cause, stack
|
|
1636
|
-
* or anything echoing the request) is kept: `transport` is the only diagnostic.
|
|
1637
|
-
*/
|
|
1638
|
-
var ManagementTransportError = class extends ManagementApiError {
|
|
1639
|
-
/** The message is derived from the classification; no free text is accepted. */
|
|
1640
|
-
constructor(classified, request) {
|
|
1641
|
-
super(transportErrorMessage(classified), classified.code, undefined, undefined, request);
|
|
1642
|
-
this.details = undefined;
|
|
1643
|
-
this.name = "ManagementTransportError";
|
|
1644
|
-
this.code = classified.code;
|
|
1645
|
-
this.transport = classified.transport;
|
|
1646
|
-
}
|
|
1647
|
-
};
|
|
1648
|
-
/**
|
|
1649
|
-
* Turn a rejected `fetch` or body read into a {@link ManagementTransportError}.
|
|
1650
|
-
* Every catch site around a management `fetch` or response-body read goes
|
|
1651
|
-
* through here so none of them can leak the platform error.
|
|
1652
|
-
*/
|
|
1653
|
-
function managementTransportError(caught, timeoutMs, request) {
|
|
1654
|
-
return new ManagementTransportError(classifyTransportFailure(caught, timeoutMs), request);
|
|
1655
|
-
}
|
|
1656
|
-
/**
|
|
1657
|
-
* Type guard to check if an error is a 404 Not Found response.
|
|
1658
|
-
* Use this to handle "resource not found" scenarios gracefully.
|
|
1659
|
-
*
|
|
1660
|
-
* @example
|
|
1661
|
-
* ```ts
|
|
1662
|
-
* try {
|
|
1663
|
-
* return await http.get<Entry>(`/entries/${type}/${id}`);
|
|
1664
|
-
* } catch (error) {
|
|
1665
|
-
* if (is404Error(error)) {
|
|
1666
|
-
* return null;
|
|
1667
|
-
* }
|
|
1668
|
-
* throw error;
|
|
1669
|
-
* }
|
|
1670
|
-
* ```
|
|
1671
|
-
*/
|
|
1672
|
-
function is404Error(error) {
|
|
1673
|
-
return error instanceof ManagementApiError && error.statusCode === 404;
|
|
1674
|
-
}
|
|
1675
|
-
function parseRuntimeResponse(value, schema, requestInfo, statusCode) {
|
|
1676
|
-
const parsed = schema.safeParse(value);
|
|
1677
|
-
if (parsed.success) return parsed.data;
|
|
1678
|
-
const validation = validateManagementResponse(value, schema);
|
|
1679
|
-
if (validation.valid) throw new Error("Response schema parsing was inconsistent");
|
|
1680
|
-
throw new ManagementInvalidResponseError(validation.details, statusCode, requestInfo);
|
|
1681
|
-
}
|
|
1682
|
-
function isRecord$2(value) {
|
|
1683
|
-
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
1684
|
-
}
|
|
1685
|
-
function isFieldError(value) {
|
|
1686
|
-
return isRecord$2(value) && typeof value.field === "string" && typeof value.code === "string" && typeof value.message === "string";
|
|
1687
|
-
}
|
|
1688
|
-
function parseApiResponse(value) {
|
|
1689
|
-
if (!isRecord$2(value)) return null;
|
|
1690
|
-
if (value.success === true) {
|
|
1691
|
-
return {
|
|
1692
|
-
success: true,
|
|
1693
|
-
data: value.data
|
|
1694
|
-
};
|
|
1695
|
-
}
|
|
1696
|
-
if (value.success === false) {
|
|
1697
|
-
const error = value.error;
|
|
1698
|
-
if (!isRecord$2(error) || typeof error.code !== "string" || typeof error.message !== "string") {
|
|
1699
|
-
return {
|
|
1700
|
-
success: false,
|
|
1701
|
-
error: {
|
|
1702
|
-
code: "sdk:http-error",
|
|
1703
|
-
message: "Request failed",
|
|
1704
|
-
details: error
|
|
1705
|
-
}
|
|
1706
|
-
};
|
|
1707
|
-
}
|
|
1708
|
-
const fieldErrorsRaw = error.fieldErrors;
|
|
1709
|
-
const fieldErrors = Array.isArray(fieldErrorsRaw) ? fieldErrorsRaw.filter(isFieldError) : undefined;
|
|
1710
|
-
return {
|
|
1711
|
-
success: false,
|
|
1712
|
-
error: {
|
|
1713
|
-
code: error.code,
|
|
1714
|
-
message: error.message,
|
|
1715
|
-
details: error.details,
|
|
1716
|
-
...fieldErrors ? { fieldErrors } : {},
|
|
1717
|
-
...typeof error.tag === "string" ? { tag: error.tag } : {}
|
|
1718
|
-
}
|
|
1719
|
-
};
|
|
1720
|
-
}
|
|
1721
|
-
return null;
|
|
1722
|
-
}
|
|
1723
|
-
function buildRequestInfo(options) {
|
|
1724
|
-
return {
|
|
1725
|
-
method: options.method,
|
|
1726
|
-
intent: options.intent,
|
|
1727
|
-
path: options.path,
|
|
1728
|
-
url: options.url,
|
|
1729
|
-
baseUrl: options.baseUrl,
|
|
1730
|
-
dashboardUrl: options.dashboardUrl,
|
|
1731
|
-
siteId: options.siteId
|
|
1732
|
-
};
|
|
1733
|
-
}
|
|
1734
|
-
/**
|
|
1735
|
-
* Read a response body as text. A body that fails mid-read (a reset, undici's
|
|
1736
|
-
* body timeout, the client's own abort) is a transport failure like any other.
|
|
1737
|
-
*/
|
|
1738
|
-
async function readResponseText(response, timeoutMs, requestInfo) {
|
|
1739
|
-
try {
|
|
1740
|
-
return await response.text();
|
|
1741
|
-
} catch (error) {
|
|
1742
|
-
throw managementTransportError(error, timeoutMs, requestInfo);
|
|
1743
|
-
}
|
|
1744
|
-
}
|
|
1745
|
-
async function readApiResponse(response, requestInfo, options) {
|
|
1746
|
-
const contentType = response.headers.get("content-type");
|
|
1747
|
-
const text = await readResponseText(response, options.timeoutMs, requestInfo);
|
|
1748
|
-
if (!contentType?.includes("application/json")) {
|
|
1749
|
-
if (response.ok && options.allowNonJsonOk) return undefined;
|
|
1750
|
-
if (response.ok && options.invalidOkResponse) {
|
|
1751
|
-
throw new ManagementInvalidResponseError({ issues: [{
|
|
1752
|
-
path: [],
|
|
1753
|
-
message: "Expected a JSON response"
|
|
1754
|
-
}] }, response.status, requestInfo);
|
|
1755
|
-
}
|
|
1756
|
-
throw new ManagementApiError(`Request failed with status ${response.status}`, "sdk:http-error", { statusCode: response.status }, response.status, requestInfo);
|
|
1757
|
-
}
|
|
1758
|
-
let parsed;
|
|
1759
|
-
try {
|
|
1760
|
-
parsed = text ? JSON.parse(text) : null;
|
|
1761
|
-
} catch {
|
|
1762
|
-
if (response.ok && options.invalidOkResponse) {
|
|
1763
|
-
throw new ManagementInvalidResponseError({ issues: [{
|
|
1764
|
-
path: [],
|
|
1765
|
-
message: "Response was not valid JSON"
|
|
1766
|
-
}] }, response.status, requestInfo);
|
|
1767
|
-
}
|
|
1768
|
-
throw new ManagementApiError(`Invalid JSON response (status ${response.status})`, "sdk:http-error", { statusCode: response.status }, response.status, requestInfo);
|
|
1769
|
-
}
|
|
1770
|
-
const envelope = parseApiResponse(parsed);
|
|
1771
|
-
if (!envelope) {
|
|
1772
|
-
if (response.ok && options.invalidOkResponse) {
|
|
1773
|
-
throw new ManagementInvalidResponseError({ issues: [{
|
|
1774
|
-
path: [],
|
|
1775
|
-
message: "Response did not match the API envelope"
|
|
1776
|
-
}] }, response.status, requestInfo);
|
|
1777
|
-
}
|
|
1778
|
-
throw new ManagementApiError(`Invalid API response (status ${response.status})`, "sdk:http-error", { statusCode: response.status }, response.status, requestInfo);
|
|
1779
|
-
}
|
|
1780
|
-
return envelope;
|
|
1781
|
-
}
|
|
1782
|
-
function requestUrl(baseUrl, path$2, params) {
|
|
1783
|
-
if (!params || Object.keys(params).length === 0) return `${baseUrl}${path$2}`;
|
|
1784
|
-
const query = new URLSearchParams(params).toString();
|
|
1785
|
-
return `${baseUrl}${path$2}?${query}`;
|
|
1786
|
-
}
|
|
1787
|
-
const DEFAULT_REQUEST_TIMEOUT_MS = 3e4;
|
|
1788
|
-
function requestTimeoutMs(config) {
|
|
1789
|
-
return config.timeout ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
1790
|
-
}
|
|
1791
|
-
function requestFetchOptions(config, method, body) {
|
|
1792
|
-
const options = {
|
|
1793
|
-
method,
|
|
1794
|
-
headers: {
|
|
1795
|
-
Authorization: `Bearer ${exposeSecret(config.managementApiKey)}`,
|
|
1796
|
-
"Content-Type": "application/json"
|
|
1797
|
-
},
|
|
1798
|
-
signal: AbortSignal.timeout(requestTimeoutMs(config))
|
|
1799
|
-
};
|
|
1800
|
-
if (body !== undefined && method !== "GET") options.body = JSON.stringify(body);
|
|
1801
|
-
return options;
|
|
1802
|
-
}
|
|
1803
|
-
async function fetchManagementResponse(config, method, intent, path$2, url, baseUrl, options) {
|
|
1804
|
-
const requestInfo = buildRequestInfo({
|
|
1805
|
-
method,
|
|
1806
|
-
intent,
|
|
1807
|
-
path: path$2,
|
|
1808
|
-
url,
|
|
1809
|
-
baseUrl,
|
|
1810
|
-
dashboardUrl: config.dashboardUrl,
|
|
1811
|
-
siteId: config.siteId
|
|
1812
|
-
});
|
|
1813
|
-
try {
|
|
1814
|
-
return {
|
|
1815
|
-
response: await fetch(url, options),
|
|
1816
|
-
requestInfo
|
|
1817
|
-
};
|
|
1818
|
-
} catch (error) {
|
|
1819
|
-
throw managementTransportError(error, requestTimeoutMs(config), requestInfo);
|
|
1820
|
-
}
|
|
1821
|
-
}
|
|
1822
|
-
function throwApiError(response, json, requestInfo) {
|
|
1823
|
-
if (response.ok && json.success) return;
|
|
1824
|
-
if ("error" in json && json.error) {
|
|
1825
|
-
const details = json.error.fieldErrors ? { fieldErrors: json.error.fieldErrors } : json.error.details;
|
|
1826
|
-
throw new ManagementApiError(json.error.message, json.error.code, details, response.status, requestInfo, json.error.tag);
|
|
1827
|
-
}
|
|
1828
|
-
throw new ManagementApiError(`Request failed with status ${response.status}`, "sdk:http-error", undefined, response.status, requestInfo);
|
|
1829
|
-
}
|
|
1830
|
-
async function requestWithConfig(config, method, path$2, options) {
|
|
1831
|
-
const baseUrl = `${config.dashboardUrl}/api/sdk/${config.siteId}`;
|
|
1832
|
-
const url = requestUrl(baseUrl, path$2, options.params);
|
|
1833
|
-
const fetchOptions = requestFetchOptions(config, method, options.body);
|
|
1834
|
-
const { response, requestInfo } = await fetchManagementResponse(config, method, options.intent, path$2, url, baseUrl, fetchOptions);
|
|
1835
|
-
const responseSchema = options.responseSchema ?? schemaForManagementRequest(method, path$2, options.body);
|
|
1836
|
-
const timeoutMs = requestTimeoutMs(config);
|
|
1837
|
-
const json = responseSchema ? await readApiResponse(response, requestInfo, {
|
|
1838
|
-
timeoutMs,
|
|
1839
|
-
invalidOkResponse: true
|
|
1840
|
-
}) : await readApiResponse(response, requestInfo, {
|
|
1841
|
-
timeoutMs,
|
|
1842
|
-
allowNonJsonOk: true
|
|
1843
|
-
});
|
|
1844
|
-
if (!json) return undefined;
|
|
1845
|
-
throwApiError(response, json, requestInfo);
|
|
1846
|
-
return responseSchema ? parseRuntimeResponse(json.data, responseSchema, requestInfo, response.status) : json.data;
|
|
1847
|
-
}
|
|
1848
|
-
function createHttpClient(config) {
|
|
1849
|
-
const send = (method, path$2, options) => requestWithConfig(config, method, path$2, options);
|
|
1850
|
-
return {
|
|
1851
|
-
get: (path$2, params) => send("GET", path$2, {
|
|
1852
|
-
intent: "read",
|
|
1853
|
-
params
|
|
1854
|
-
}),
|
|
1855
|
-
post: (path$2, body, intent) => send("POST", path$2, {
|
|
1856
|
-
intent,
|
|
1857
|
-
body
|
|
1858
|
-
}),
|
|
1859
|
-
patch: (path$2, body) => send("PATCH", path$2, {
|
|
1860
|
-
intent: "write",
|
|
1861
|
-
body
|
|
1862
|
-
}),
|
|
1863
|
-
delete: (path$2, body) => send("DELETE", path$2, {
|
|
1864
|
-
intent: "write",
|
|
1865
|
-
body
|
|
1866
|
-
}),
|
|
1867
|
-
getValidated: (path$2, responseSchema, params) => send("GET", path$2, {
|
|
1868
|
-
intent: "read",
|
|
1869
|
-
params,
|
|
1870
|
-
responseSchema
|
|
1871
|
-
}),
|
|
1872
|
-
postValidated: (path$2, responseSchema, body, intent) => send("POST", path$2, {
|
|
1873
|
-
intent,
|
|
1874
|
-
body,
|
|
1875
|
-
responseSchema
|
|
1876
|
-
}),
|
|
1877
|
-
patchValidated: (path$2, responseSchema, body) => send("PATCH", path$2, {
|
|
1878
|
-
intent: "write",
|
|
1879
|
-
body,
|
|
1880
|
-
responseSchema
|
|
1881
|
-
}),
|
|
1882
|
-
deleteValidated: (path$2, responseSchema, body) => send("DELETE", path$2, {
|
|
1883
|
-
intent: "write",
|
|
1884
|
-
body,
|
|
1885
|
-
responseSchema
|
|
1886
|
-
})
|
|
1887
|
-
};
|
|
1888
|
-
}
|
|
1889
|
-
|
|
1890
|
-
//#endregion
|
|
1891
|
-
//#region src/media/walk.ts
|
|
1892
|
-
const isRecordValue = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1893
|
-
const walkValueTree = (value, location, visitor) => {
|
|
1894
|
-
visitor(value, location);
|
|
1895
|
-
if (Array.isArray(value)) {
|
|
1896
|
-
value.forEach((entry, index) => {
|
|
1897
|
-
walkValueTree(entry, `${location}[${index}]`, visitor);
|
|
1898
|
-
});
|
|
1899
|
-
return;
|
|
1900
|
-
}
|
|
1901
|
-
if (isRecordValue(value)) {
|
|
1902
|
-
Object.entries(value).forEach(([key, entry]) => {
|
|
1903
|
-
walkValueTree(entry, location ? `${location}.${key}` : key, visitor);
|
|
1904
|
-
});
|
|
1905
|
-
}
|
|
1906
|
-
};
|
|
1907
|
-
|
|
1908
|
-
//#endregion
|
|
1909
|
-
//#region src/media/value.ts
|
|
1910
|
-
const MEDIA_VALUE_TYPES = [
|
|
1911
|
-
"image",
|
|
1912
|
-
"video",
|
|
1913
|
-
"audio",
|
|
1914
|
-
"document",
|
|
1915
|
-
"spreadsheet",
|
|
1916
|
-
"archive"
|
|
1917
|
-
];
|
|
1918
|
-
const MEDIA_VALUE_TYPE_SET = new Set(MEDIA_VALUE_TYPES);
|
|
1919
|
-
const isOptionalString = (value) => value === undefined || typeof value === "string";
|
|
1920
|
-
const isOptionalNumber = (value) => value === undefined || typeof value === "number";
|
|
1921
|
-
const hasValidKnownMediaFields = (value) => (value.identifier === null || isOptionalString(value.identifier)) && isOptionalString(value.assetId) && isOptionalString(value.storagePath) && isOptionalString(value.storageBucket) && isOptionalString(value.src) && isOptionalString(value.mimeType) && isOptionalNumber(value.width) && isOptionalNumber(value.height);
|
|
1922
|
-
const hasMediaReference = (value) => typeof value.identifier === "string" || typeof value.assetId === "string" || typeof value.storagePath === "string" || typeof value.src === "string" || typeof value.mimeType === "string";
|
|
1923
|
-
const isMediaValue = (value) => isRecordValue(value) && !("attrs" in value) && typeof value.type === "string" && MEDIA_VALUE_TYPE_SET.has(value.type) && hasValidKnownMediaFields(value) && hasMediaReference(value);
|
|
1924
|
-
|
|
1925
|
-
//#endregion
|
|
1926
|
-
//#region src/content/media-manifest.ts
|
|
1927
|
-
const MANIFEST_VERSION = 2;
|
|
1928
|
-
const getManifestPath = (contentDir) => path$1.join(contentDir, "media", ".manifest.json");
|
|
1929
|
-
const ensureDir = async (dirPath) => {
|
|
1930
|
-
try {
|
|
1931
|
-
await fs$1.mkdir(dirPath, { recursive: true });
|
|
1932
|
-
} catch (error) {
|
|
1933
|
-
if (error.code !== "EEXIST") {
|
|
1934
|
-
throw error;
|
|
1935
|
-
}
|
|
1936
|
-
}
|
|
1937
|
-
};
|
|
1938
|
-
const writeJsonFile = async (filePath, data) => {
|
|
1939
|
-
const content = JSON.stringify(data, null, 2) + "\n";
|
|
1940
|
-
await fs$1.writeFile(filePath, content, "utf-8");
|
|
1941
|
-
};
|
|
1942
|
-
const isRecord$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1943
|
-
const hasManifestShape = (value) => {
|
|
1944
|
-
if (!isRecord$1(value) || !("version" in value) || !("assets" in value)) {
|
|
1945
|
-
return false;
|
|
1946
|
-
}
|
|
1947
|
-
return isRecord$1(value.assets);
|
|
1948
|
-
};
|
|
1949
|
-
const toManifestEntry = (value) => {
|
|
1950
|
-
if (!isRecord$1(value)) return null;
|
|
1951
|
-
const filename = typeof value.filename === "string" ? value.filename : "";
|
|
1952
|
-
if (!filename) return null;
|
|
1953
|
-
return {
|
|
1954
|
-
filename,
|
|
1955
|
-
checksum: typeof value.checksum === "string" ? value.checksum : undefined,
|
|
1956
|
-
mimeType: typeof value.mimeType === "string" ? value.mimeType : undefined
|
|
1957
|
-
};
|
|
1958
|
-
};
|
|
1959
|
-
const toV2ManifestEntry = (value) => {
|
|
1960
|
-
if (!isRecord$1(value)) return null;
|
|
1961
|
-
if (value.checksum !== undefined && typeof value.checksum !== "string" || value.mimeType !== undefined && typeof value.mimeType !== "string") {
|
|
1962
|
-
return null;
|
|
1963
|
-
}
|
|
1964
|
-
return toManifestEntry(value);
|
|
1965
|
-
};
|
|
1966
|
-
const parseV2Manifest = (parsed, assets) => {
|
|
1967
|
-
if (typeof parsed.updatedAt !== "string") return null;
|
|
1968
|
-
const parsedAssets = {};
|
|
1969
|
-
for (const [identifier, value] of Object.entries(assets)) {
|
|
1970
|
-
const entry = toV2ManifestEntry(value);
|
|
1971
|
-
if (!entry) return null;
|
|
1972
|
-
parsedAssets[identifier] = entry;
|
|
1973
|
-
}
|
|
1974
|
-
return {
|
|
1975
|
-
version: MANIFEST_VERSION,
|
|
1976
|
-
updatedAt: parsed.updatedAt,
|
|
1977
|
-
assets: parsedAssets
|
|
1978
|
-
};
|
|
1979
|
-
};
|
|
1980
|
-
const migrateV1Assets = (assets) => Object.fromEntries(Object.entries(assets).flatMap(([identifier, value]) => {
|
|
1981
|
-
const entry = toManifestEntry(value);
|
|
1982
|
-
return entry ? [[identifier, entry]] : [];
|
|
1983
|
-
}));
|
|
1984
|
-
const migrateV1Manifest = (parsed, assets) => {
|
|
1985
|
-
return {
|
|
1986
|
-
version: MANIFEST_VERSION,
|
|
1987
|
-
updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : new Date().toISOString(),
|
|
1988
|
-
assets: migrateV1Assets(assets)
|
|
1989
|
-
};
|
|
1990
|
-
};
|
|
1991
|
-
const parseManifest = (content) => {
|
|
1992
|
-
const parsed = JSON.parse(content);
|
|
1993
|
-
if (!hasManifestShape(parsed)) return null;
|
|
1994
|
-
const assets = parsed.assets;
|
|
1995
|
-
if (parsed.version === 1) return migrateV1Manifest(parsed, assets);
|
|
1996
|
-
if (parsed.version === MANIFEST_VERSION) {
|
|
1997
|
-
return parseV2Manifest(parsed, assets);
|
|
1998
|
-
}
|
|
1999
|
-
return null;
|
|
2000
|
-
};
|
|
2001
|
-
const createEmptyMediaManifest = () => ({
|
|
2002
|
-
version: MANIFEST_VERSION,
|
|
2003
|
-
updatedAt: new Date().toISOString(),
|
|
2004
|
-
assets: {}
|
|
2005
|
-
});
|
|
2006
|
-
async function readMediaManifest(contentDir) {
|
|
2007
|
-
const manifestPath = getManifestPath(contentDir);
|
|
2008
|
-
try {
|
|
2009
|
-
const content = await fs$1.readFile(manifestPath, "utf-8");
|
|
2010
|
-
return parseManifest(content) ?? createEmptyMediaManifest();
|
|
2011
|
-
} catch (error) {
|
|
2012
|
-
const nodeError = error;
|
|
2013
|
-
if (nodeError.code !== "ENOENT") {
|
|
2014
|
-
console.warn(`Warning: failed to read media manifest: ${nodeError.message}`);
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
return createEmptyMediaManifest();
|
|
2018
|
-
}
|
|
2019
|
-
async function writeMediaManifest(contentDir, manifest) {
|
|
2020
|
-
const mediaDir = path$1.join(contentDir, "media");
|
|
2021
|
-
await ensureDir(mediaDir);
|
|
2022
|
-
const updatedManifest = {
|
|
2023
|
-
...manifest,
|
|
2024
|
-
version: MANIFEST_VERSION,
|
|
2025
|
-
updatedAt: new Date().toISOString()
|
|
2026
|
-
};
|
|
2027
|
-
const manifestPath = getManifestPath(contentDir);
|
|
2028
|
-
await writeJsonFile(manifestPath, updatedManifest);
|
|
2029
|
-
return manifestPath;
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
//#endregion
|
|
2033
|
-
//#region ../management-client/src/settings-branding.ts
|
|
2034
|
-
const SETTINGS_BASE_FIELDS = [
|
|
2035
|
-
"siteTitle",
|
|
2036
|
-
"homepageId",
|
|
2037
|
-
"seoDefaults"
|
|
2038
|
-
];
|
|
2039
|
-
const SETTINGS_BRANDING_FIELDS = [
|
|
2040
|
-
"logoIdentifier",
|
|
2041
|
-
"logoDarkBackgroundIdentifier",
|
|
2042
|
-
"faviconIdentifier"
|
|
2043
|
-
];
|
|
2044
|
-
const SETTINGS_FIELDS = [...SETTINGS_BASE_FIELDS, ...SETTINGS_BRANDING_FIELDS];
|
|
2045
|
-
/** Fields owned by content/settings.json. Booking configuration has its own
|
|
2046
|
-
* mutation operation and must never enter the generic settings update input. */
|
|
2047
|
-
const SETTINGS_FILE_FIELDS = [...SETTINGS_FIELDS, "verticalConfig"];
|
|
2048
|
-
const SETTINGS_BRANDING_LABELS = {
|
|
2049
|
-
logoIdentifier: "logo",
|
|
2050
|
-
logoDarkBackgroundIdentifier: "dark-background logo",
|
|
2051
|
-
faviconIdentifier: "favicon"
|
|
2052
|
-
};
|
|
2053
|
-
function hasOwnSettingsField(settings, field) {
|
|
2054
|
-
return hasOwnField(settings, field);
|
|
2055
|
-
}
|
|
2056
|
-
function hasOwnField(settings, field) {
|
|
2057
|
-
return Object.prototype.hasOwnProperty.call(settings, field);
|
|
2058
|
-
}
|
|
2059
|
-
function buildSettingsUpdateInput(settings) {
|
|
2060
|
-
const update = {};
|
|
2061
|
-
for (const field of SETTINGS_FIELDS) {
|
|
2062
|
-
if (!hasOwnSettingsField(settings, field)) {
|
|
2063
|
-
continue;
|
|
2064
|
-
}
|
|
2065
|
-
switch (field) {
|
|
2066
|
-
case "siteTitle":
|
|
2067
|
-
update.siteTitle = settings.siteTitle;
|
|
2068
|
-
break;
|
|
2069
|
-
case "homepageId":
|
|
2070
|
-
update.homepageId = settings.homepageId;
|
|
2071
|
-
break;
|
|
2072
|
-
case "seoDefaults":
|
|
2073
|
-
update.seoDefaults = settings.seoDefaults;
|
|
2074
|
-
break;
|
|
2075
|
-
case "logoIdentifier":
|
|
2076
|
-
update.logoIdentifier = settings.logoIdentifier;
|
|
2077
|
-
break;
|
|
2078
|
-
case "logoDarkBackgroundIdentifier":
|
|
2079
|
-
update.logoDarkBackgroundIdentifier = settings.logoDarkBackgroundIdentifier;
|
|
2080
|
-
break;
|
|
2081
|
-
case "faviconIdentifier":
|
|
2082
|
-
update.faviconIdentifier = settings.faviconIdentifier;
|
|
2083
|
-
break;
|
|
2084
|
-
}
|
|
2085
|
-
}
|
|
2086
|
-
return update;
|
|
2087
|
-
}
|
|
2088
|
-
function applySettingsBrandingPullResolutions(baseSettings, resolutions) {
|
|
2089
|
-
const settings = {
|
|
2090
|
-
homepageId: baseSettings.homepageId,
|
|
2091
|
-
seoDefaults: baseSettings.seoDefaults
|
|
2092
|
-
};
|
|
2093
|
-
const warnings = [];
|
|
2094
|
-
for (const resolution of resolutions) {
|
|
2095
|
-
switch (resolution.kind) {
|
|
2096
|
-
case "resolved":
|
|
2097
|
-
settings[resolution.field] = resolution.identifier;
|
|
2098
|
-
break;
|
|
2099
|
-
case "missing":
|
|
2100
|
-
settings[resolution.field] = null;
|
|
2101
|
-
break;
|
|
2102
|
-
case "unresolved":
|
|
2103
|
-
warnings.push(`Remote ${SETTINGS_BRANDING_LABELS[resolution.field]} asset ` + `"${resolution.assetId}" has no media identifier, so ${resolution.field} was omitted from content/settings.json.`);
|
|
2104
|
-
break;
|
|
2105
|
-
}
|
|
2106
|
-
}
|
|
2107
|
-
return {
|
|
2108
|
-
settings,
|
|
2109
|
-
warnings
|
|
2110
|
-
};
|
|
2111
|
-
}
|
|
2112
|
-
function getSettingsBrandingLabel(field) {
|
|
2113
|
-
return SETTINGS_BRANDING_LABELS[field];
|
|
2114
|
-
}
|
|
2115
|
-
function collectSettingsBrandingIdentifiers(settings) {
|
|
2116
|
-
if (!settings) {
|
|
2117
|
-
return [];
|
|
2118
|
-
}
|
|
2119
|
-
const identifiers = [];
|
|
2120
|
-
for (const field of SETTINGS_BRANDING_FIELDS) {
|
|
2121
|
-
const identifier = settings[field];
|
|
2122
|
-
if (typeof identifier === "string" && identifier) {
|
|
2123
|
-
identifiers.push({
|
|
2124
|
-
field,
|
|
2125
|
-
identifier
|
|
2126
|
-
});
|
|
2127
|
-
}
|
|
2128
|
-
}
|
|
2129
|
-
return identifiers;
|
|
2130
|
-
}
|
|
2131
|
-
function isImageMimeType(mimeType) {
|
|
2132
|
-
return typeof mimeType === "string" && mimeType.startsWith("image/");
|
|
2133
|
-
}
|
|
2134
|
-
|
|
2135
|
-
//#endregion
|
|
2136
|
-
//#region src/sync/media.ts
|
|
2137
|
-
/**
|
|
2138
|
-
* Media Sync Helper
|
|
2139
|
-
*
|
|
2140
|
-
* Handles syncing media assets between environments during pull/push operations.
|
|
2141
|
-
*
|
|
2142
|
-
* With relative paths, storagePath values in content are like "originals/2024/12/abc.jpg"
|
|
2143
|
-
* (without site prefix). The actual files are stored at "sites/{siteId}/originals/...".
|
|
2144
|
-
*
|
|
2145
|
-
* When syncing:
|
|
2146
|
-
* 1. Extract all relative storagePath values from content
|
|
2147
|
-
* 2. Download files from source: sites/{sourceSiteId}/{relativePath}
|
|
2148
|
-
* 3. Upload files to target: sites/{targetSiteId}/{relativePath}
|
|
2149
|
-
*
|
|
2150
|
-
* No path rewriting in content data is needed since paths are relative!
|
|
2151
|
-
*/
|
|
2152
|
-
/**
|
|
2153
|
-
* Default retry configuration for network operations.
|
|
2154
|
-
*/
|
|
2155
|
-
const DEFAULT_RETRY_CONFIG = {
|
|
2156
|
-
maxRetries: 3,
|
|
2157
|
-
baseDelayMs: 1e3,
|
|
2158
|
-
maxDelayMs: 1e4
|
|
2159
|
-
};
|
|
2160
|
-
/**
|
|
2161
|
-
* Sleep for a given number of milliseconds.
|
|
2162
|
-
*/
|
|
2163
|
-
function sleep(ms) {
|
|
2164
|
-
return new Promise((resolve$2) => setTimeout(resolve$2, ms));
|
|
2165
|
-
}
|
|
2166
|
-
/**
|
|
2167
|
-
* Calculate exponential backoff delay with jitter.
|
|
2168
|
-
* @param attempt - Current attempt number (0-indexed)
|
|
2169
|
-
* @param baseDelayMs - Base delay in milliseconds
|
|
2170
|
-
* @param maxDelayMs - Maximum delay cap
|
|
2171
|
-
*/
|
|
2172
|
-
function getBackoffDelay(attempt, baseDelayMs, maxDelayMs) {
|
|
2173
|
-
const exponentialDelay = baseDelayMs * Math.pow(2, attempt);
|
|
2174
|
-
const jitter = exponentialDelay * (.75 + Math.random() * .5);
|
|
2175
|
-
return Math.min(jitter, maxDelayMs);
|
|
2176
|
-
}
|
|
2177
|
-
/**
|
|
2178
|
-
* Execute a function with exponential backoff retry.
|
|
2179
|
-
* @param fn - Function to execute
|
|
2180
|
-
* @param options - Retry options
|
|
2181
|
-
*/
|
|
2182
|
-
async function withRetry(fn, options = {}) {
|
|
2183
|
-
const { maxRetries, baseDelayMs, maxDelayMs } = {
|
|
2184
|
-
...DEFAULT_RETRY_CONFIG,
|
|
2185
|
-
...options
|
|
2186
|
-
};
|
|
2187
|
-
let lastError;
|
|
2188
|
-
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
2189
|
-
try {
|
|
2190
|
-
return await fn();
|
|
2191
|
-
} catch (error) {
|
|
2192
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
2193
|
-
if (attempt < maxRetries) {
|
|
2194
|
-
const delay = getBackoffDelay(attempt, baseDelayMs, maxDelayMs);
|
|
2195
|
-
options.onRetry?.(attempt + 1, lastError);
|
|
2196
|
-
await sleep(delay);
|
|
2197
|
-
}
|
|
2198
|
-
}
|
|
2199
|
-
}
|
|
2200
|
-
throw lastError;
|
|
2201
|
-
}
|
|
2202
|
-
/**
|
|
2203
|
-
* Extract all unique storagePath values from content data.
|
|
2204
|
-
* Recursively walks through objects and arrays to find all media references.
|
|
2205
|
-
*/
|
|
2206
|
-
function extractMediaPaths(data) {
|
|
2207
|
-
const paths = new Set();
|
|
2208
|
-
walkValueTree(data, "", (value) => {
|
|
2209
|
-
if (!isRecordValue(value)) return;
|
|
2210
|
-
if (typeof value.storagePath === "string" && value.storagePath) {
|
|
2211
|
-
paths.add(value.storagePath);
|
|
2212
|
-
}
|
|
2213
|
-
});
|
|
2214
|
-
return paths;
|
|
2215
|
-
}
|
|
2216
|
-
/**
|
|
2217
|
-
* Extract all unique media identifiers from content data.
|
|
2218
|
-
* Recursively walks through objects and arrays to find media references.
|
|
2219
|
-
*/
|
|
2220
|
-
function extractMediaIdentifiers(data) {
|
|
2221
|
-
const identifiers = new Set();
|
|
2222
|
-
if (isRecordValue(data)) {
|
|
2223
|
-
const rootSettings = isRecordValue(data.settings) ? data.settings : null;
|
|
2224
|
-
for (const { identifier } of collectSettingsBrandingIdentifiers(rootSettings)) {
|
|
2225
|
-
identifiers.add(identifier);
|
|
2226
|
-
}
|
|
2227
|
-
}
|
|
2228
|
-
walkValueTree(data, "", (value) => {
|
|
2229
|
-
if (!isRecordValue(value)) return;
|
|
2230
|
-
if (isMediaValue(value) && value.identifier) {
|
|
2231
|
-
identifiers.add(value.identifier);
|
|
2232
|
-
}
|
|
2233
|
-
if (value.type === "image" && isRecordValue(value.attrs)) {
|
|
2234
|
-
if (typeof value.attrs.identifier === "string" && value.attrs.identifier) {
|
|
2235
|
-
identifiers.add(value.attrs.identifier);
|
|
2236
|
-
}
|
|
2237
|
-
}
|
|
2238
|
-
});
|
|
2239
|
-
return identifiers;
|
|
2240
|
-
}
|
|
2241
|
-
function buildStorageUrl(supabaseUrl, storagePath, siteId, bucket = "media") {
|
|
2242
|
-
const normalized = normalizeAmbiguousStoragePathForSite(siteId, storagePath);
|
|
2243
|
-
if (!normalized.ok) {
|
|
2244
|
-
return {
|
|
2245
|
-
ok: false,
|
|
2246
|
-
error: {
|
|
2247
|
-
tag: "invalid_storage_path",
|
|
2248
|
-
siteId,
|
|
2249
|
-
storagePath,
|
|
2250
|
-
cause: normalized.error
|
|
2251
|
-
}
|
|
2252
|
-
};
|
|
2253
|
-
}
|
|
2254
|
-
return {
|
|
2255
|
-
ok: true,
|
|
2256
|
-
value: getDirectImageUrlForSiteScopedPath(supabaseUrl, normalized.value.fullPath, bucket)
|
|
2257
|
-
};
|
|
2258
|
-
}
|
|
2259
|
-
/**
|
|
2260
|
-
* File extensions that support Supabase image transforms.
|
|
2261
|
-
*/
|
|
2262
|
-
const TRANSFORMABLE_EXTENSIONS = [
|
|
2263
|
-
"jpg",
|
|
2264
|
-
"jpeg",
|
|
2265
|
-
"png",
|
|
2266
|
-
"webp",
|
|
2267
|
-
"gif"
|
|
2268
|
-
];
|
|
2269
|
-
/**
|
|
2270
|
-
* Check if a file path is an image that can be transformed by Supabase.
|
|
2271
|
-
* @param path - The file path or storage path
|
|
2272
|
-
*/
|
|
2273
|
-
function isTransformableImage(path$2) {
|
|
2274
|
-
const ext = path$2.split(".").pop()?.toLowerCase();
|
|
2275
|
-
return TRANSFORMABLE_EXTENSIONS.includes(ext ?? "");
|
|
2276
|
-
}
|
|
2277
|
-
/**
|
|
2278
|
-
* Build a URL using Supabase's image transform endpoint.
|
|
2279
|
-
* Returns a resized version to avoid exceeding upload size limits.
|
|
2280
|
-
*
|
|
2281
|
-
* @param supabaseUrl - The Supabase project URL
|
|
2282
|
-
* @param storagePath - Relative or site-scoped path within the bucket
|
|
2283
|
-
* @param siteId - The site ID context for relative paths
|
|
2284
|
-
* @param options - Transform options (width, quality)
|
|
2285
|
-
*/
|
|
2286
|
-
function buildImageTransformUrl(supabaseUrl, storagePath, siteId, options = {}) {
|
|
2287
|
-
const { width = 1920, quality = 85 } = options;
|
|
2288
|
-
const normalized = normalizeAmbiguousStoragePathForSite(siteId, storagePath);
|
|
2289
|
-
if (!normalized.ok) {
|
|
2290
|
-
return {
|
|
2291
|
-
ok: false,
|
|
2292
|
-
error: {
|
|
2293
|
-
tag: "invalid_storage_path",
|
|
2294
|
-
siteId,
|
|
2295
|
-
storagePath,
|
|
2296
|
-
cause: normalized.error
|
|
2297
|
-
}
|
|
2298
|
-
};
|
|
2299
|
-
}
|
|
2300
|
-
return {
|
|
2301
|
-
ok: true,
|
|
2302
|
-
value: getDirectImageUrlForSiteScopedPath(supabaseUrl, normalized.value.fullPath, "media", {
|
|
2303
|
-
width,
|
|
2304
|
-
quality
|
|
2305
|
-
})
|
|
2306
|
-
};
|
|
2307
|
-
}
|
|
2308
|
-
/**
|
|
2309
|
-
* Download a file from a public Supabase storage URL.
|
|
2310
|
-
* Returns the file data as a Buffer, or null if download fails after retries.
|
|
2311
|
-
*
|
|
2312
|
-
* Uses exponential backoff retry for transient network failures.
|
|
2313
|
-
*/
|
|
2314
|
-
async function downloadMedia(url, options) {
|
|
2315
|
-
try {
|
|
2316
|
-
return await withRetry(async () => {
|
|
2317
|
-
const response = await fetch(url);
|
|
2318
|
-
if (!response.ok) {
|
|
2319
|
-
if (response.status >= 400 && response.status < 500 && response.status !== 429) {
|
|
2320
|
-
throw new DownloadError(`HTTP ${response.status}`, response.status, false);
|
|
2321
|
-
}
|
|
2322
|
-
throw new DownloadError(`HTTP ${response.status}`, response.status, true);
|
|
2323
|
-
}
|
|
2324
|
-
const contentType = response.headers.get("content-type") ?? "application/octet-stream";
|
|
2325
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
2326
|
-
const data = Buffer.from(arrayBuffer);
|
|
2327
|
-
return {
|
|
2328
|
-
data,
|
|
2329
|
-
contentType
|
|
2330
|
-
};
|
|
2331
|
-
}, {
|
|
2332
|
-
maxRetries: options?.maxRetries ?? DEFAULT_RETRY_CONFIG.maxRetries,
|
|
2333
|
-
onRetry: (attempt, error) => {
|
|
2334
|
-
console.warn(`[media-sync] Retry ${attempt} for ${url}: ${error.message}`);
|
|
2335
|
-
}
|
|
2336
|
-
});
|
|
2337
|
-
} catch (error) {
|
|
2338
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
2339
|
-
console.error(`[media-sync] Download failed after retries: ${url} - ${message}`);
|
|
2340
|
-
return null;
|
|
2341
|
-
}
|
|
2342
|
-
}
|
|
2343
|
-
/**
|
|
2344
|
-
* Custom error for download operations with retryability info.
|
|
2345
|
-
*/
|
|
2346
|
-
var DownloadError = class extends Error {
|
|
2347
|
-
constructor(message, status, retryable) {
|
|
2348
|
-
super(message);
|
|
2349
|
-
this.status = status;
|
|
2350
|
-
this.retryable = retryable;
|
|
2351
|
-
this.name = "DownloadError";
|
|
2352
|
-
}
|
|
2353
|
-
};
|
|
2354
|
-
/**
|
|
2355
|
-
* Sync media files from source to target environment.
|
|
2356
|
-
*
|
|
2357
|
-
* With relative paths, content data doesn't need to be modified.
|
|
2358
|
-
* We just copy files from source site folder to target site folder.
|
|
2359
|
-
*
|
|
2360
|
-
* @param content - The content data containing media references
|
|
2361
|
-
* @param client - Client for uploading to target environment
|
|
2362
|
-
* @param options - Sync options
|
|
2363
|
-
* @returns Summary of sync results (content unchanged since paths are relative)
|
|
2364
|
-
*/
|
|
2365
|
-
async function syncMedia(content, client, options) {
|
|
2366
|
-
const { sourceSupabaseUrl, sourceSiteId, bucket = "media", onProgress } = options;
|
|
2367
|
-
const mediaPaths = extractMediaPaths(content);
|
|
2368
|
-
const total = mediaPaths.size;
|
|
2369
|
-
const results = [];
|
|
2370
|
-
let completed = 0;
|
|
2371
|
-
for (const relativePath of mediaPaths) {
|
|
2372
|
-
const filename = relativePath.split("/").pop() ?? "file";
|
|
2373
|
-
onProgress?.(completed, total, filename);
|
|
2374
|
-
try {
|
|
2375
|
-
const exists = await client.mediaExists(relativePath);
|
|
2376
|
-
if (exists) {
|
|
2377
|
-
results.push({
|
|
2378
|
-
relativePath,
|
|
2379
|
-
success: true,
|
|
2380
|
-
skipped: true
|
|
2381
|
-
});
|
|
2382
|
-
completed++;
|
|
2383
|
-
continue;
|
|
2384
|
-
}
|
|
2385
|
-
} catch {}
|
|
2386
|
-
const urlResult = buildStorageUrl(sourceSupabaseUrl, relativePath, sourceSiteId, bucket);
|
|
2387
|
-
if (!urlResult.ok) {
|
|
2388
|
-
results.push({
|
|
2389
|
-
relativePath,
|
|
2390
|
-
success: false,
|
|
2391
|
-
error: `Invalid media storage path: ${urlResult.error.cause.tag}`
|
|
2392
|
-
});
|
|
2393
|
-
completed++;
|
|
2394
|
-
continue;
|
|
2395
|
-
}
|
|
2396
|
-
const url = urlResult.value;
|
|
2397
|
-
const downloaded = await downloadMedia(url);
|
|
2398
|
-
if (!downloaded) {
|
|
2399
|
-
results.push({
|
|
2400
|
-
relativePath,
|
|
2401
|
-
success: false,
|
|
2402
|
-
error: `Failed to download from ${url}`
|
|
2403
|
-
});
|
|
2404
|
-
completed++;
|
|
2405
|
-
continue;
|
|
2406
|
-
}
|
|
2407
|
-
const uploadResult = await client.uploadMedia({
|
|
2408
|
-
data: downloaded.data,
|
|
2409
|
-
filename,
|
|
2410
|
-
contentType: downloaded.contentType,
|
|
2411
|
-
storagePath: relativePath
|
|
2412
|
-
});
|
|
2413
|
-
if ("error" in uploadResult) {
|
|
2414
|
-
results.push({
|
|
2415
|
-
relativePath,
|
|
2416
|
-
success: false,
|
|
2417
|
-
error: uploadResult.error
|
|
2418
|
-
});
|
|
2419
|
-
} else {
|
|
2420
|
-
results.push({
|
|
2421
|
-
relativePath,
|
|
2422
|
-
success: true
|
|
2423
|
-
});
|
|
2424
|
-
}
|
|
2425
|
-
completed++;
|
|
2426
|
-
}
|
|
2427
|
-
onProgress?.(total, total, "complete");
|
|
2428
|
-
const summary = {
|
|
2429
|
-
total,
|
|
2430
|
-
synced: results.filter((r) => r.success && !r.skipped).length,
|
|
2431
|
-
skipped: results.filter((r) => r.skipped).length,
|
|
2432
|
-
failed: results.filter((r) => !r.success).length,
|
|
2433
|
-
results
|
|
2434
|
-
};
|
|
2435
|
-
return {
|
|
2436
|
-
summary,
|
|
2437
|
-
content
|
|
2438
|
-
};
|
|
2439
|
-
}
|
|
2440
|
-
|
|
2441
|
-
//#endregion
|
|
2442
|
-
//#region src/media/identifiers.ts
|
|
2443
|
-
const walkContentMedia = (content, handler) => {
|
|
2444
|
-
const visit = (value, location) => {
|
|
2445
|
-
if (isMediaValue(value)) {
|
|
2446
|
-
handler(value, location);
|
|
2447
|
-
}
|
|
2448
|
-
};
|
|
2449
|
-
for (const [contentType, entries] of content.entries) {
|
|
2450
|
-
for (const entry of entries) {
|
|
2451
|
-
walkValueTree(entry.data, `entries.${contentType}.${entry.identifier}.data`, visit);
|
|
2452
|
-
for (const [slotKey, blocks] of Object.entries(entry.blocks ?? {})) {
|
|
2453
|
-
for (const block of blocks) {
|
|
2454
|
-
walkValueTree(block.content, `entries.${contentType}.${entry.identifier}.blocks.${slotKey}.${block.identifier ?? "unidentified"}.content`, visit);
|
|
2455
|
-
}
|
|
2456
|
-
}
|
|
2457
|
-
}
|
|
2458
|
-
}
|
|
2459
|
-
for (const page of content.pages) {
|
|
2460
|
-
for (const block of page.blocks ?? []) {
|
|
2461
|
-
walkValueTree(block.data, `pages.${page.identifier}.blocks.${block.identifier}.data`, visit);
|
|
2462
|
-
}
|
|
2463
|
-
}
|
|
2464
|
-
walkValueTree(content.navigation, "navigation", visit);
|
|
2465
|
-
for (const { field, identifier } of collectSettingsBrandingIdentifiers(content.settings)) {
|
|
2466
|
-
handler({
|
|
2467
|
-
type: "image",
|
|
2468
|
-
identifier
|
|
2469
|
-
}, `settings.${field}`);
|
|
2470
|
-
}
|
|
2471
|
-
walkValueTree(content.settings, "settings", visit);
|
|
2472
|
-
walkValueTree(content.forms, "forms", visit);
|
|
2473
|
-
walkValueTree(content.footer, "footer", visit);
|
|
2474
|
-
walkValueTree(content.venues, "venues", visit);
|
|
2475
|
-
walkValueTree(content.eventCategories, "eventCategories", visit);
|
|
2476
|
-
for (const event of content.events ?? []) {
|
|
2477
|
-
walkValueTree(event, `events.${event.identifier}`, visit);
|
|
2478
|
-
}
|
|
2479
|
-
};
|
|
2480
|
-
const localContentSections = (content) => ({
|
|
2481
|
-
entries: content.entries?.entries() ?? [],
|
|
2482
|
-
pages: content.pages ?? [],
|
|
2483
|
-
navigation: content.navigation,
|
|
2484
|
-
settings: content.settings,
|
|
2485
|
-
forms: content.forms,
|
|
2486
|
-
footer: content.footer,
|
|
2487
|
-
venues: content.venues,
|
|
2488
|
-
eventCategories: content.eventCategories,
|
|
2489
|
-
events: content.events
|
|
2490
|
-
});
|
|
2491
|
-
const pulledContentSections = (content) => ({
|
|
2492
|
-
entries: Object.entries(content.entries),
|
|
2493
|
-
pages: content.pages,
|
|
2494
|
-
navigation: content.navigation,
|
|
2495
|
-
settings: content.settings,
|
|
2496
|
-
forms: content.forms,
|
|
2497
|
-
venues: content.venues,
|
|
2498
|
-
eventCategories: content.eventCategories,
|
|
2499
|
-
events: content.events
|
|
2500
|
-
});
|
|
2501
|
-
const walkLocalContentMedia = (content, handler) => {
|
|
2502
|
-
walkContentMedia(localContentSections(content), handler);
|
|
2503
|
-
};
|
|
2504
|
-
const walkPulledContentMedia = (content, handler) => {
|
|
2505
|
-
walkContentMedia(pulledContentSections(content), handler);
|
|
2506
|
-
};
|
|
2507
|
-
|
|
2508
|
-
//#endregion
|
|
2509
|
-
//#region src/media/value-utils.ts
|
|
2510
|
-
/**
|
|
2511
|
-
* Normalize a media identifier to the canonical slug form.
|
|
2512
|
-
* Returns null when the raw value is empty or invalid.
|
|
2513
|
-
*/
|
|
2514
|
-
const normalizeMediaIdentifierInput = (raw) => {
|
|
2515
|
-
if (!raw) return null;
|
|
2516
|
-
const trimmed = raw.trim();
|
|
2517
|
-
if (!trimmed) return null;
|
|
2518
|
-
if (/[./\\]/.test(trimmed)) {
|
|
2519
|
-
return null;
|
|
2520
|
-
}
|
|
2521
|
-
const normalized = slugifyMediaIdentifier(trimmed);
|
|
2522
|
-
return normalized || null;
|
|
2523
|
-
};
|
|
2524
|
-
/**
|
|
2525
|
-
* Normalize a media value to the portable identifier-only representation.
|
|
2526
|
-
*
|
|
2527
|
-
* Returns a new media value by:
|
|
2528
|
-
* - setting `identifier` (required)
|
|
2529
|
-
* - removing env-specific fields (`assetId`, `storagePath`, `storageBucket`, `src`, `mimeType`)
|
|
2530
|
-
*/
|
|
2531
|
-
const normalizeMediaValueToIdentifierOnly = (value, identifier, asset) => {
|
|
2532
|
-
const normalized = {
|
|
2533
|
-
...value,
|
|
2534
|
-
identifier
|
|
2535
|
-
};
|
|
2536
|
-
delete normalized.assetId;
|
|
2537
|
-
delete normalized.storageBucket;
|
|
2538
|
-
delete normalized.storagePath;
|
|
2539
|
-
delete normalized.mimeType;
|
|
2540
|
-
delete normalized.src;
|
|
2541
|
-
if (asset) {
|
|
2542
|
-
applyAuthoritativeDimension(normalized, "width", asset.width);
|
|
2543
|
-
applyAuthoritativeDimension(normalized, "height", asset.height);
|
|
2544
|
-
}
|
|
2545
|
-
return normalized;
|
|
2546
|
-
};
|
|
2547
|
-
function applyAuthoritativeDimension(value, key, dimension) {
|
|
2548
|
-
if (dimension === null || dimension === undefined) {
|
|
2549
|
-
delete value[key];
|
|
2550
|
-
return;
|
|
2551
|
-
}
|
|
2552
|
-
value[key] = dimension;
|
|
2553
|
-
}
|
|
2554
|
-
|
|
2555
|
-
//#endregion
|
|
2556
|
-
//#region src/media/rich-text.ts
|
|
2557
|
-
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2558
|
-
/**
|
|
2559
|
-
* Walk any JSON-like payload and collect TipTap rich text image node attrs.
|
|
2560
|
-
*
|
|
2561
|
-
* TipTap nodes look like: { type: 'image', attrs: { src?: string, identifier?: string, ... } }
|
|
2562
|
-
*/
|
|
2563
|
-
function collectRichTextImageAttrs(value, out = [], location = "") {
|
|
2564
|
-
if (Array.isArray(value)) {
|
|
2565
|
-
value.forEach((child, index) => collectRichTextImageAttrs(child, out, `${location}[${index}]`));
|
|
2566
|
-
return out;
|
|
2567
|
-
}
|
|
2568
|
-
if (!isRecord(value)) return out;
|
|
2569
|
-
if (value.type === "image" && isRecord(value.attrs)) {
|
|
2570
|
-
out.push({
|
|
2571
|
-
attrs: value.attrs,
|
|
2572
|
-
location: location ? `${location}.attrs` : "attrs"
|
|
2573
|
-
});
|
|
2574
|
-
}
|
|
2575
|
-
for (const [key, child] of Object.entries(value)) {
|
|
2576
|
-
const next = location ? `${location}.${key}` : key;
|
|
2577
|
-
collectRichTextImageAttrs(child, out, next);
|
|
2578
|
-
}
|
|
2579
|
-
return out;
|
|
2580
|
-
}
|
|
2581
|
-
|
|
2582
|
-
//#endregion
|
|
2583
|
-
//#region src/media/pull-manifest.ts
|
|
2584
|
-
const buildLocalFilename = (identifier, asset) => {
|
|
2585
|
-
const extensionFromFilename = path$1.extname(asset.filename ?? "");
|
|
2586
|
-
const extension = extensionFromFilename || getExtensionForMimeType(asset.mimeType) || "";
|
|
2587
|
-
return extension ? `${identifier}${extension}` : identifier;
|
|
2588
|
-
};
|
|
2589
|
-
const updateManifestEntry = (manifest, identifier, asset, localFile) => {
|
|
2590
|
-
const previous = manifest.assets[identifier];
|
|
2591
|
-
const filename = localFile?.filename ?? previous?.filename ?? buildLocalFilename(identifier, asset);
|
|
2592
|
-
const checksum = localFile?.checksum ?? asset.checksumSha256 ?? previous?.checksum;
|
|
2593
|
-
const mimeType = localFile?.mimeType ?? asset.mimeType ?? previous?.mimeType;
|
|
2594
|
-
const entry = {
|
|
2595
|
-
checksum,
|
|
2596
|
-
filename,
|
|
2597
|
-
mimeType
|
|
2598
|
-
};
|
|
2599
|
-
manifest.assets[identifier] = entry;
|
|
2600
|
-
};
|
|
2601
|
-
const normalizeStoragePath = (storagePath, siteId) => {
|
|
2602
|
-
const prefix = `sites/${siteId}/`;
|
|
2603
|
-
if (storagePath.startsWith(prefix)) {
|
|
2604
|
-
return storagePath.slice(prefix.length);
|
|
2605
|
-
}
|
|
2606
|
-
return storagePath;
|
|
2607
|
-
};
|
|
2608
|
-
|
|
2609
|
-
//#endregion
|
|
2610
|
-
//#region src/media/push-preparation-plan.ts
|
|
2611
|
-
const cloneLocalFile = (file) => ({
|
|
2612
|
-
identifier: file.identifier,
|
|
2613
|
-
filename: file.filename,
|
|
2614
|
-
filePath: file.filePath,
|
|
2615
|
-
mimeType: file.mimeType,
|
|
2616
|
-
checksum: file.checksum,
|
|
2617
|
-
dataBase64: Buffer.from(file.data).toString("base64"),
|
|
2618
|
-
dataLength: file.data.length
|
|
2619
|
-
});
|
|
2620
|
-
const collectLocalMediaReferences = (content) => {
|
|
2621
|
-
const mediaReferences = [];
|
|
2622
|
-
const richTextReferences = [];
|
|
2623
|
-
const warnings = [];
|
|
2624
|
-
const errors = [];
|
|
2625
|
-
const locationsByIdentifier = new Map();
|
|
2626
|
-
walkLocalContentMedia(content, (value, location) => {
|
|
2627
|
-
const rawIdentifier = typeof value.identifier === "string" ? value.identifier : "";
|
|
2628
|
-
const hasLegacyRef = Boolean(value.assetId || value.storagePath || value.src);
|
|
2629
|
-
if (!rawIdentifier) {
|
|
2630
|
-
if (hasLegacyRef) {
|
|
2631
|
-
errors.push(`Media reference at ${location} is missing identifier.`);
|
|
2632
|
-
}
|
|
2633
|
-
return;
|
|
2634
|
-
}
|
|
2635
|
-
if (/[./\\]/.test(rawIdentifier)) {
|
|
2636
|
-
errors.push(`Media identifier "${rawIdentifier}" at ${location} must not include an extension.`);
|
|
2637
|
-
return;
|
|
2638
|
-
}
|
|
2639
|
-
const normalized = normalizeMediaIdentifierInput(rawIdentifier);
|
|
2640
|
-
if (!normalized) {
|
|
2641
|
-
errors.push(`Media identifier "${rawIdentifier}" at ${location} is invalid.`);
|
|
2642
|
-
return;
|
|
2643
|
-
}
|
|
2644
|
-
if (normalized !== rawIdentifier) {
|
|
2645
|
-
warnings.push(`Normalized media identifier "${rawIdentifier}" to "${normalized}" at ${location}.`);
|
|
2646
|
-
value.identifier = normalized;
|
|
2647
|
-
}
|
|
2648
|
-
mediaReferences.push({
|
|
2649
|
-
identifier: normalized,
|
|
2650
|
-
rawIdentifier,
|
|
2651
|
-
value,
|
|
2652
|
-
location
|
|
2653
|
-
});
|
|
2654
|
-
const locations = locationsByIdentifier.get(normalized) ?? [];
|
|
2655
|
-
locations.push(location);
|
|
2656
|
-
locationsByIdentifier.set(normalized, locations);
|
|
2657
|
-
});
|
|
2658
|
-
for (const node of collectRichTextImageAttrs(content)) {
|
|
2659
|
-
const attrs = node.attrs;
|
|
2660
|
-
const rawIdentifier = typeof attrs.identifier === "string" ? attrs.identifier : "";
|
|
2661
|
-
const rawSrc = typeof attrs.src === "string" ? attrs.src : "";
|
|
2662
|
-
if (!rawIdentifier) {
|
|
2663
|
-
if (rawSrc) {
|
|
2664
|
-
errors.push(`Rich text image at ${node.location} is missing identifier.`);
|
|
2665
|
-
}
|
|
2666
|
-
continue;
|
|
2667
|
-
}
|
|
2668
|
-
if (/[./\\]/.test(rawIdentifier)) {
|
|
2669
|
-
errors.push(`Rich text image identifier "${rawIdentifier}" at ${node.location} must not include an extension.`);
|
|
2670
|
-
continue;
|
|
2671
|
-
}
|
|
2672
|
-
const normalized = normalizeMediaIdentifierInput(rawIdentifier);
|
|
2673
|
-
if (!normalized) {
|
|
2674
|
-
errors.push(`Rich text image identifier "${rawIdentifier}" at ${node.location} is invalid.`);
|
|
2675
|
-
continue;
|
|
2676
|
-
}
|
|
2677
|
-
if (normalized !== rawIdentifier) {
|
|
2678
|
-
warnings.push(`Normalized rich text image identifier "${rawIdentifier}" to "${normalized}" at ${node.location}.`);
|
|
2679
|
-
attrs.identifier = normalized;
|
|
2680
|
-
}
|
|
2681
|
-
delete attrs.src;
|
|
2682
|
-
richTextReferences.push({
|
|
2683
|
-
identifier: normalized,
|
|
2684
|
-
rawIdentifier,
|
|
2685
|
-
attrs,
|
|
2686
|
-
location: node.location
|
|
2687
|
-
});
|
|
2688
|
-
const locations = locationsByIdentifier.get(normalized) ?? [];
|
|
2689
|
-
locations.push(node.location);
|
|
2690
|
-
locationsByIdentifier.set(normalized, locations);
|
|
2691
|
-
}
|
|
2692
|
-
return {
|
|
2693
|
-
mediaReferences,
|
|
2694
|
-
richTextReferences,
|
|
2695
|
-
warnings,
|
|
2696
|
-
errors,
|
|
2697
|
-
locationsByIdentifier
|
|
2698
|
-
};
|
|
2699
|
-
};
|
|
2700
|
-
const buildLocationNote = (locationsByIdentifier, identifier) => {
|
|
2701
|
-
const locations = locationsByIdentifier.get(identifier);
|
|
2702
|
-
return locations?.length ? ` Referenced at: ${locations.join(", ")}.` : "";
|
|
2703
|
-
};
|
|
2704
|
-
const locationsForIdentifier = (locationsByIdentifier, identifier) => locationsByIdentifier.get(identifier) ?? [];
|
|
2705
|
-
const operationPriority = (operation) => {
|
|
2706
|
-
switch (operation.kind) {
|
|
2707
|
-
case "upload": return 0;
|
|
2708
|
-
case "conflict":
|
|
2709
|
-
case "missing_local_file":
|
|
2710
|
-
case "reused":
|
|
2711
|
-
case "skipped":
|
|
2712
|
-
case "unchanged": return 1;
|
|
2713
|
-
}
|
|
2714
|
-
};
|
|
2715
|
-
const sortOperations = (operations) => operations.slice().sort((a, b) => {
|
|
2716
|
-
const byIdentifier = a.identifier.localeCompare(b.identifier);
|
|
2717
|
-
return byIdentifier || operationPriority(a) - operationPriority(b);
|
|
2718
|
-
});
|
|
2719
|
-
const emptyPushSummary = (totalReferences, identifiers) => ({
|
|
2720
|
-
totalReferences,
|
|
2721
|
-
identifiers,
|
|
2722
|
-
resolved: 0,
|
|
2723
|
-
reused: 0,
|
|
2724
|
-
uploaded: 0,
|
|
2725
|
-
skipped: 0,
|
|
2726
|
-
pendingUploads: 0
|
|
2727
|
-
});
|
|
2728
|
-
const stableAssetList = (assets) => assets.slice().sort((a, b) => a.id.localeCompare(b.id)).map((asset) => ({ ...asset }));
|
|
2729
|
-
const stableMediaReadSet = (observations) => stableStringify(observations.slice().sort((a, b) => a.identifier.localeCompare(b.identifier)).map((observation) => ({
|
|
2730
|
-
identifier: observation.identifier,
|
|
2731
|
-
localChecksum: observation.localChecksum.kind === "present" && observation.localChecksum.lookup.kind === "read" ? {
|
|
2732
|
-
...observation.localChecksum,
|
|
2733
|
-
lookup: {
|
|
2734
|
-
...observation.localChecksum.lookup,
|
|
2735
|
-
assets: stableAssetList(observation.localChecksum.lookup.assets)
|
|
2736
|
-
}
|
|
2737
|
-
} : observation.localChecksum,
|
|
2738
|
-
resolvedAsset: observation.resolvedAsset,
|
|
2739
|
-
missing: observation.missing
|
|
2740
|
-
})));
|
|
2741
|
-
const buildMediaReadSet = (options) => options.identifiers.map((identifier) => {
|
|
2742
|
-
const checksum = options.localChecksums.get(identifier);
|
|
2743
|
-
const resolvedAsset = options.initialAssets.get(identifier);
|
|
2744
|
-
const localChecksum = checksum === undefined ? { kind: "missing" } : {
|
|
2745
|
-
kind: "present",
|
|
2746
|
-
checksum,
|
|
2747
|
-
lookup: options.checksumAssetsByChecksum.has(checksum) ? {
|
|
2748
|
-
kind: "read",
|
|
2749
|
-
assets: stableAssetList(options.checksumAssetsByChecksum.get(checksum) ?? [])
|
|
2750
|
-
} : { kind: "not-read" }
|
|
2751
|
-
};
|
|
2752
|
-
return {
|
|
2753
|
-
identifier,
|
|
2754
|
-
localChecksum,
|
|
2755
|
-
resolvedAsset: resolvedAsset ? { ...resolvedAsset } : null,
|
|
2756
|
-
missing: options.initialMissing.has(identifier)
|
|
2757
|
-
};
|
|
2758
|
-
});
|
|
2759
|
-
const localFileForOperation = (localFile) => ({
|
|
2760
|
-
filename: localFile.filename,
|
|
2761
|
-
checksum: localFile.checksum,
|
|
2762
|
-
mimeType: localFile.mimeType
|
|
2763
|
-
});
|
|
2764
|
-
const classifyExistingAsset = (options, hasConflict) => {
|
|
2765
|
-
const { identifier, asset, localFile, locations, locationNote } = options;
|
|
2766
|
-
if (asset && !localFile) {
|
|
2767
|
-
return {
|
|
2768
|
-
operations: [{
|
|
2769
|
-
kind: "skipped",
|
|
2770
|
-
identifier,
|
|
2771
|
-
locations,
|
|
2772
|
-
message: "Using existing CMS asset because no local file was found."
|
|
2773
|
-
}],
|
|
2774
|
-
warnings: [`Missing local file for "${identifier}". Using CMS asset instead.${locationNote}`],
|
|
2775
|
-
errors: []
|
|
2776
|
-
};
|
|
2777
|
-
}
|
|
2778
|
-
if (hasConflict && asset) {
|
|
2779
|
-
const message = `Identifier "${identifier}" points to a different file on the CMS. Delete the existing asset first or rename your local identifier.`;
|
|
2780
|
-
return {
|
|
2781
|
-
operations: [{
|
|
2782
|
-
kind: "conflict",
|
|
2783
|
-
identifier,
|
|
2784
|
-
locations,
|
|
2785
|
-
message
|
|
2786
|
-
}],
|
|
2787
|
-
errors: [`${message}${locationNote}`]
|
|
2788
|
-
};
|
|
2789
|
-
}
|
|
2790
|
-
if (asset && localFile && !hasConflict) {
|
|
2791
|
-
return {
|
|
2792
|
-
operations: [{
|
|
2793
|
-
kind: "unchanged",
|
|
2794
|
-
identifier,
|
|
2795
|
-
asset: { ...asset },
|
|
2796
|
-
file: cloneLocalFile(localFile),
|
|
2797
|
-
locations
|
|
2798
|
-
}],
|
|
2799
|
-
manifestAsset: asset,
|
|
2800
|
-
manifestFile: localFileForOperation(localFile),
|
|
2801
|
-
errors: []
|
|
2802
|
-
};
|
|
2803
|
-
}
|
|
2804
|
-
return undefined;
|
|
2805
|
-
};
|
|
2806
|
-
const classifySingularChecksumMatch = (options) => {
|
|
2807
|
-
const { identifier, localFile, locations, initialOperations } = options;
|
|
2808
|
-
const exact = options.checksumMatches.find((candidate) => candidate.identifier === identifier);
|
|
2809
|
-
if (exact) {
|
|
2810
|
-
return {
|
|
2811
|
-
operations: [...initialOperations, {
|
|
2812
|
-
kind: "reused",
|
|
2813
|
-
identifier,
|
|
2814
|
-
asset: { ...exact },
|
|
2815
|
-
file: cloneLocalFile(localFile),
|
|
2816
|
-
locations
|
|
2817
|
-
}],
|
|
2818
|
-
manifestAsset: exact,
|
|
2819
|
-
manifestFile: localFileForOperation(localFile),
|
|
2820
|
-
errors: []
|
|
2821
|
-
};
|
|
2822
|
-
}
|
|
2823
|
-
return undefined;
|
|
2824
|
-
};
|
|
2825
|
-
const classifyChecksumMatches = (options) => {
|
|
2826
|
-
const { identifier, localFile, checksumMatches, locations, initialOperations, locationNote } = options;
|
|
2827
|
-
const singular = classifySingularChecksumMatch(options);
|
|
2828
|
-
if (singular) return singular;
|
|
2829
|
-
if (checksumMatches.length > 0) {
|
|
2830
|
-
const identifiers = checksumMatches.map((asset) => asset.identifier).join(", ");
|
|
2831
|
-
const message = `The same media bytes already exist under ${identifiers}; use that identifier or delete the existing asset first.`;
|
|
2832
|
-
return {
|
|
2833
|
-
operations: [...initialOperations, {
|
|
2834
|
-
kind: "conflict",
|
|
2835
|
-
identifier,
|
|
2836
|
-
locations,
|
|
2837
|
-
message
|
|
2838
|
-
}],
|
|
2839
|
-
errors: [`${message}${locationNote}`]
|
|
2840
|
-
};
|
|
2841
|
-
}
|
|
2842
|
-
return {
|
|
2843
|
-
operations: [...initialOperations, {
|
|
2844
|
-
kind: "upload",
|
|
2845
|
-
identifier,
|
|
2846
|
-
file: cloneLocalFile(localFile),
|
|
2847
|
-
source: "local_file",
|
|
2848
|
-
locations
|
|
2849
|
-
}],
|
|
2850
|
-
errors: []
|
|
2851
|
-
};
|
|
2852
|
-
};
|
|
2853
|
-
const classifyMissingReference = (options) => {
|
|
2854
|
-
const { identifier, localFile, locationNote, initialOperations, locations } = options;
|
|
2855
|
-
if (!localFile) {
|
|
2856
|
-
const message = `Add content/media/${identifier}.* or pull with --local-media first.`;
|
|
2857
|
-
return {
|
|
2858
|
-
operations: [...initialOperations, {
|
|
2859
|
-
kind: "missing_local_file",
|
|
2860
|
-
identifier,
|
|
2861
|
-
locations,
|
|
2862
|
-
message
|
|
2863
|
-
}],
|
|
2864
|
-
errors: [`Identifier "${identifier}" does not exist on the CMS and no local file was found. ${message}${locationNote}`]
|
|
2865
|
-
};
|
|
2866
|
-
}
|
|
2867
|
-
return classifyChecksumMatches({
|
|
2868
|
-
identifier,
|
|
2869
|
-
localFile,
|
|
2870
|
-
checksumMatches: options.checksumMatches,
|
|
2871
|
-
locations,
|
|
2872
|
-
initialOperations,
|
|
2873
|
-
locationNote
|
|
2874
|
-
});
|
|
2875
|
-
};
|
|
2876
|
-
const classifyLocalMediaReference = (options) => {
|
|
2877
|
-
const { asset, localFile, missing, locations } = options;
|
|
2878
|
-
const hasConflict = Boolean(asset && localFile && asset.checksumSha256 && asset.checksumSha256 !== localFile.checksum);
|
|
2879
|
-
const existing = classifyExistingAsset(options, hasConflict);
|
|
2880
|
-
if (existing) return existing;
|
|
2881
|
-
const initialOperations = [];
|
|
2882
|
-
if (!missing && !hasConflict) return {
|
|
2883
|
-
operations: initialOperations,
|
|
2884
|
-
errors: []
|
|
2885
|
-
};
|
|
2886
|
-
return classifyMissingReference({
|
|
2887
|
-
identifier: options.identifier,
|
|
2888
|
-
localFile,
|
|
2889
|
-
checksumMatches: options.checksumMatches,
|
|
2890
|
-
locations,
|
|
2891
|
-
initialOperations,
|
|
2892
|
-
locationNote: options.locationNote
|
|
2893
|
-
});
|
|
2894
|
-
};
|
|
2895
|
-
const applyManifestClassification = (manifest, identifier, classification) => {
|
|
2896
|
-
if (classification.manifestAsset) {
|
|
2897
|
-
updateManifestEntry(manifest, identifier, classification.manifestAsset, classification.manifestFile);
|
|
2898
|
-
}
|
|
2899
|
-
};
|
|
2900
|
-
|
|
2901
|
-
//#endregion
|
|
2902
|
-
//#region src/utils/checksum.ts
|
|
2903
|
-
function sha256(buffer) {
|
|
2904
|
-
return createHash("sha256").update(buffer).digest("hex");
|
|
2905
|
-
}
|
|
2906
|
-
|
|
2907
|
-
//#endregion
|
|
2908
|
-
//#region src/files/stable-binary-file.ts
|
|
2909
|
-
function isNodeError(error) {
|
|
2910
|
-
return error instanceof Error && "code" in error;
|
|
2911
|
-
}
|
|
2912
|
-
function observationsMatch(left, right) {
|
|
2913
|
-
return left.dev === right.dev && left.ino === right.ino && left.size === right.size && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs;
|
|
2914
|
-
}
|
|
2915
|
-
function admitStableBinaryRead(stats, maxBytes) {
|
|
2916
|
-
if (!stats.isFile()) return err({ tag: "not_a_regular_file" });
|
|
2917
|
-
return stats.size > maxBytes ? err({
|
|
2918
|
-
tag: "file_too_large",
|
|
2919
|
-
maxBytes,
|
|
2920
|
-
actualBytes: stats.size
|
|
2921
|
-
}) : ok(undefined);
|
|
2922
|
-
}
|
|
2923
|
-
function mapStableBinaryReadFailure(error) {
|
|
2924
|
-
if (!isNodeError(error)) return { tag: "read_failed" };
|
|
2925
|
-
if (error.code === "ENOENT") return { tag: "file_not_found" };
|
|
2926
|
-
return error.code === "ELOOP" || error.code === "EISDIR" ? { tag: "not_a_regular_file" } : { tag: "read_failed" };
|
|
2927
|
-
}
|
|
2928
|
-
async function readObservedByteLength(handle, byteLength) {
|
|
2929
|
-
const data = Buffer.allocUnsafe(byteLength);
|
|
2930
|
-
let offset = 0;
|
|
2931
|
-
while (offset < byteLength) {
|
|
2932
|
-
const read = await handle.read(data, offset, byteLength - offset, offset);
|
|
2933
|
-
if (read.bytesRead === 0) return null;
|
|
2934
|
-
offset += read.bytesRead;
|
|
2935
|
-
}
|
|
2936
|
-
const growthProbe = Buffer.allocUnsafe(1);
|
|
2937
|
-
const extra = await handle.read(growthProbe, 0, 1, byteLength);
|
|
2938
|
-
return extra.bytesRead === 0 ? data : null;
|
|
2939
|
-
}
|
|
2940
|
-
async function readStableBinaryFile(filePath, maxBytes) {
|
|
2941
|
-
let handle;
|
|
2942
|
-
try {
|
|
2943
|
-
const noFollow = constants.O_NOFOLLOW ?? 0;
|
|
2944
|
-
handle = await open(filePath, constants.O_RDONLY | noFollow);
|
|
2945
|
-
const before = await handle.stat();
|
|
2946
|
-
const admission = admitStableBinaryRead(before, maxBytes);
|
|
2947
|
-
if (!admission.ok) return admission;
|
|
2948
|
-
const data = await readObservedByteLength(handle, before.size);
|
|
2949
|
-
const after = await handle.stat();
|
|
2950
|
-
if (data === null || !observationsMatch(before, after)) {
|
|
2951
|
-
return err({ tag: "file_changed_during_read" });
|
|
2952
|
-
}
|
|
2953
|
-
return ok(data);
|
|
2954
|
-
} catch (error) {
|
|
2955
|
-
return err(mapStableBinaryReadFailure(error));
|
|
2956
|
-
} finally {
|
|
2957
|
-
await handle?.close().catch(() => undefined);
|
|
2958
|
-
}
|
|
2959
|
-
}
|
|
2960
|
-
async function inspectDestination(filePath, overwrite) {
|
|
2961
|
-
try {
|
|
2962
|
-
const stats = await lstat(filePath);
|
|
2963
|
-
if (overwrite === "refuse") return err({ tag: "destination_exists" });
|
|
2964
|
-
return stats.isFile() ? ok(undefined) : err({ tag: "unsafe_destination" });
|
|
2965
|
-
} catch (error) {
|
|
2966
|
-
if (isNodeError(error) && error.code === "ENOENT") return ok(undefined);
|
|
2967
|
-
return err({ tag: "write_failed" });
|
|
2968
|
-
}
|
|
2969
|
-
}
|
|
2970
|
-
/**
|
|
2971
|
-
* Admit an output path before a remote operation while preserving the atomic
|
|
2972
|
-
* writer's second check at finalization time to close the race window.
|
|
2973
|
-
*/
|
|
2974
|
-
async function admitBinaryWriteDestination(filePath, overwrite) {
|
|
2975
|
-
const destination = await inspectDestination(filePath, overwrite);
|
|
2976
|
-
if (!destination.ok) return destination;
|
|
2977
|
-
try {
|
|
2978
|
-
const parent = dirname$1(filePath);
|
|
2979
|
-
const parentStats = await lstat(parent);
|
|
2980
|
-
if (!parentStats.isDirectory()) return err({ tag: "unsafe_destination" });
|
|
2981
|
-
await access(parent, constants.W_OK);
|
|
2982
|
-
return ok(undefined);
|
|
2983
|
-
} catch {
|
|
2984
|
-
return err({ tag: "write_failed" });
|
|
2985
|
-
}
|
|
2986
|
-
}
|
|
2987
|
-
async function syncDirectory(directory) {
|
|
2988
|
-
if (process.platform === "win32") return true;
|
|
2989
|
-
let handle;
|
|
2990
|
-
try {
|
|
2991
|
-
handle = await open(directory, "r");
|
|
2992
|
-
await handle.sync();
|
|
2993
|
-
return true;
|
|
2994
|
-
} catch {
|
|
2995
|
-
return false;
|
|
2996
|
-
} finally {
|
|
2997
|
-
await handle?.close().catch(() => undefined);
|
|
2998
|
-
}
|
|
2999
|
-
}
|
|
3000
|
-
function mapAtomicBinaryWriteFailure(error) {
|
|
3001
|
-
return isNodeError(error) && (error.code === "ELOOP" || error.code === "EISDIR" || error.code === "ENOTDIR") ? { tag: "unsafe_destination" } : { tag: "write_failed" };
|
|
3002
|
-
}
|
|
3003
|
-
async function writeTemporaryBinaryFile(temporaryPath, data, mode) {
|
|
3004
|
-
let handle;
|
|
3005
|
-
try {
|
|
3006
|
-
handle = await open(temporaryPath, "wx", mode);
|
|
3007
|
-
await handle.writeFile(data);
|
|
3008
|
-
await handle.sync();
|
|
3009
|
-
await handle.close();
|
|
3010
|
-
handle = undefined;
|
|
3011
|
-
await chmod(temporaryPath, mode);
|
|
3012
|
-
} finally {
|
|
3013
|
-
await handle?.close().catch(() => undefined);
|
|
3014
|
-
}
|
|
3015
|
-
}
|
|
3016
|
-
async function finalizeTemporaryBinaryFile(temporaryPath, filePath, overwrite) {
|
|
3017
|
-
if (overwrite === "allow") {
|
|
3018
|
-
await rename(temporaryPath, filePath);
|
|
3019
|
-
return ok([]);
|
|
3020
|
-
}
|
|
3021
|
-
try {
|
|
3022
|
-
await link(temporaryPath, filePath);
|
|
3023
|
-
} catch (error) {
|
|
3024
|
-
return isNodeError(error) && error.code === "EEXIST" ? err({ tag: "destination_exists" }) : err(mapAtomicBinaryWriteFailure(error));
|
|
3025
|
-
}
|
|
3026
|
-
try {
|
|
3027
|
-
await unlink$1(temporaryPath);
|
|
3028
|
-
return ok([]);
|
|
3029
|
-
} catch {
|
|
3030
|
-
return ok([{
|
|
3031
|
-
tag: "temporary_cleanup_failed",
|
|
3032
|
-
temporaryPath
|
|
3033
|
-
}]);
|
|
3034
|
-
}
|
|
3035
|
-
}
|
|
3036
|
-
function toAtomicBinaryWriteOutcome(warnings) {
|
|
3037
|
-
const [first, ...rest] = warnings;
|
|
3038
|
-
return first === undefined ? { tag: "written" } : {
|
|
3039
|
-
tag: "written_with_warnings",
|
|
3040
|
-
warnings: [first, ...rest]
|
|
3041
|
-
};
|
|
3042
|
-
}
|
|
3043
|
-
async function writeBinaryFileAtomic(filePath, data, options) {
|
|
3044
|
-
const parent = dirname$1(filePath);
|
|
3045
|
-
const mode = options.mode ?? 384;
|
|
3046
|
-
const suffix = options.randomSuffix ?? (() => `${process.pid}-${Date.now()}-${randomUUID()}`);
|
|
3047
|
-
let temporaryPath;
|
|
3048
|
-
try {
|
|
3049
|
-
if (options.ensureParent === true) await mkdir(parent, { recursive: true });
|
|
3050
|
-
const admitted = await inspectDestination(filePath, options.overwrite);
|
|
3051
|
-
if (!admitted.ok) return admitted;
|
|
3052
|
-
temporaryPath = join(parent, `.${basename(filePath)}.${suffix()}.tmp`);
|
|
3053
|
-
await writeTemporaryBinaryFile(temporaryPath, data, mode);
|
|
3054
|
-
const finalized = await finalizeTemporaryBinaryFile(temporaryPath, filePath, options.overwrite);
|
|
3055
|
-
if (!finalized.ok) return finalized;
|
|
3056
|
-
temporaryPath = undefined;
|
|
3057
|
-
const warnings = await syncDirectory(parent) ? finalized.value : [...finalized.value, { tag: "directory_sync_failed" }];
|
|
3058
|
-
return ok(toAtomicBinaryWriteOutcome(warnings));
|
|
3059
|
-
} catch (error) {
|
|
3060
|
-
return err(mapAtomicBinaryWriteFailure(error));
|
|
3061
|
-
} finally {
|
|
3062
|
-
if (temporaryPath) await unlink$1(temporaryPath).catch(() => undefined);
|
|
3063
|
-
}
|
|
3064
|
-
}
|
|
3065
|
-
|
|
3066
|
-
//#endregion
|
|
3067
|
-
//#region src/media/local-media.ts
|
|
3068
|
-
const MEDIA_DIR_NAME = "media";
|
|
3069
|
-
const fileExists = async (filePath) => {
|
|
3070
|
-
try {
|
|
3071
|
-
await fs$1.access(filePath);
|
|
3072
|
-
return true;
|
|
3073
|
-
} catch {
|
|
3074
|
-
return false;
|
|
3075
|
-
}
|
|
3076
|
-
};
|
|
3077
|
-
const listMediaDirFiles = async (mediaDir) => {
|
|
3078
|
-
try {
|
|
3079
|
-
return await fs$1.readdir(mediaDir);
|
|
3080
|
-
} catch (error) {
|
|
3081
|
-
const nodeError = error;
|
|
3082
|
-
if (nodeError.code === "ENOENT") return [];
|
|
3083
|
-
throw error;
|
|
3084
|
-
}
|
|
3085
|
-
};
|
|
3086
|
-
const findMatchingFilename = async (contentDir, identifier, manifest) => {
|
|
3087
|
-
const mediaDir = path$1.join(contentDir, MEDIA_DIR_NAME);
|
|
3088
|
-
const manifestEntry = manifest.assets[identifier];
|
|
3089
|
-
if (manifestEntry?.filename) {
|
|
3090
|
-
const candidatePath = path$1.join(mediaDir, manifestEntry.filename);
|
|
3091
|
-
if (await fileExists(candidatePath)) {
|
|
3092
|
-
return manifestEntry.filename;
|
|
3093
|
-
}
|
|
3094
|
-
}
|
|
3095
|
-
const files = await listMediaDirFiles(mediaDir);
|
|
3096
|
-
const matches = files.filter((name) => {
|
|
3097
|
-
if (name.startsWith(".")) return false;
|
|
3098
|
-
const parsed = path$1.parse(name);
|
|
3099
|
-
return parsed.name === identifier;
|
|
3100
|
-
});
|
|
3101
|
-
if (matches.length > 1) {
|
|
3102
|
-
throw new Error(`Multiple media files match identifier "${identifier}": ${matches.join(", ")}`);
|
|
3103
|
-
}
|
|
3104
|
-
return matches[0] ?? null;
|
|
3105
|
-
};
|
|
3106
|
-
async function loadLocalMediaFile(contentDir, identifier, manifest) {
|
|
3107
|
-
const filename = await findMatchingFilename(contentDir, identifier, manifest);
|
|
3108
|
-
if (!filename) return null;
|
|
3109
|
-
const mediaDir = path$1.join(contentDir, MEDIA_DIR_NAME);
|
|
3110
|
-
const filePath = path$1.join(mediaDir, filename);
|
|
3111
|
-
const read = await readStableBinaryFile(filePath, MAX_MANAGEMENT_MEDIA_UPLOAD_BYTES);
|
|
3112
|
-
if (!read.ok) {
|
|
3113
|
-
throw new Error(`Failed to read media file ${filePath}: ${read.error.tag}`);
|
|
3114
|
-
}
|
|
3115
|
-
const data = read.value;
|
|
3116
|
-
const checksum = sha256(data);
|
|
3117
|
-
const mimeType = getMimeType(filename);
|
|
3118
|
-
manifest.assets[identifier] = {
|
|
3119
|
-
...manifest.assets[identifier],
|
|
3120
|
-
filename,
|
|
3121
|
-
checksum,
|
|
3122
|
-
mimeType
|
|
3123
|
-
};
|
|
3124
|
-
return {
|
|
3125
|
-
identifier,
|
|
3126
|
-
filename,
|
|
3127
|
-
filePath,
|
|
3128
|
-
mimeType,
|
|
3129
|
-
checksum,
|
|
3130
|
-
data
|
|
3131
|
-
};
|
|
3132
|
-
}
|
|
3133
|
-
async function writeLocalMediaFile(contentDir, filename, data) {
|
|
3134
|
-
const mediaDir = path$1.join(contentDir, MEDIA_DIR_NAME);
|
|
3135
|
-
await fs$1.mkdir(mediaDir, { recursive: true });
|
|
3136
|
-
const filePath = path$1.join(mediaDir, filename);
|
|
3137
|
-
const written = await writeBinaryFileAtomic(filePath, data, {
|
|
3138
|
-
overwrite: "allow",
|
|
3139
|
-
mode: 420
|
|
3140
|
-
});
|
|
3141
|
-
if (!written.ok) {
|
|
3142
|
-
throw new Error(`Failed to write media file ${filePath}: ${written.error.tag}`);
|
|
3143
|
-
}
|
|
3144
|
-
return written.value.tag === "written" ? {
|
|
3145
|
-
kind: "written",
|
|
3146
|
-
filePath
|
|
3147
|
-
} : {
|
|
3148
|
-
kind: "written_with_warnings",
|
|
3149
|
-
filePath,
|
|
3150
|
-
warnings: written.value.warnings
|
|
3151
|
-
};
|
|
3152
|
-
}
|
|
3153
|
-
async function readLocalMediaChecksum(filePath, maxBytes) {
|
|
3154
|
-
const read = await readStableBinaryFile(filePath, maxBytes);
|
|
3155
|
-
if (!read.ok) {
|
|
3156
|
-
throw new Error(`Failed to read media file ${filePath}: ${read.error.tag}`);
|
|
3157
|
-
}
|
|
3158
|
-
return sha256(read.value);
|
|
3159
|
-
}
|
|
3160
|
-
async function localMediaFileExists(filePath) {
|
|
3161
|
-
return fileExists(filePath);
|
|
3162
|
-
}
|
|
3163
|
-
|
|
3164
|
-
//#endregion
|
|
3165
|
-
//#region src/media/pull-naming.ts
|
|
3166
|
-
const GENERIC_MEDIA_NAMES = new Set([
|
|
3167
|
-
"image",
|
|
3168
|
-
"photo",
|
|
3169
|
-
"picture",
|
|
3170
|
-
"pic",
|
|
3171
|
-
"img",
|
|
3172
|
-
"media",
|
|
3173
|
-
"file"
|
|
3174
|
-
]);
|
|
3175
|
-
const buildIdentifierBaseFromLocation = (location) => {
|
|
3176
|
-
const parts = location.split(".");
|
|
3177
|
-
if (parts.length < 2) return null;
|
|
3178
|
-
if (parts[0] === "entries") {
|
|
3179
|
-
const contentType = parts[1];
|
|
3180
|
-
const entryIdentifier = parts[2];
|
|
3181
|
-
const dataIndex = parts.indexOf("data");
|
|
3182
|
-
const fieldName = dataIndex >= 0 ? parts[dataIndex + 1] : "media";
|
|
3183
|
-
if (contentType && entryIdentifier && fieldName) {
|
|
3184
|
-
return `${contentType}-${entryIdentifier}-${fieldName}`;
|
|
3185
|
-
}
|
|
3186
|
-
}
|
|
3187
|
-
if (parts[0] === "pages") {
|
|
3188
|
-
const pageIdentifier = parts[1];
|
|
3189
|
-
const blockIndex = parts.indexOf("blocks");
|
|
3190
|
-
const blockIdentifier = blockIndex >= 0 ? parts[blockIndex + 1] : null;
|
|
3191
|
-
const dataIndex = parts.indexOf("data");
|
|
3192
|
-
const fieldName = dataIndex >= 0 ? parts[dataIndex + 1] : "media";
|
|
3193
|
-
if (pageIdentifier && blockIdentifier && fieldName) {
|
|
3194
|
-
return `${pageIdentifier}-${blockIdentifier}-${fieldName}`;
|
|
3195
|
-
}
|
|
3196
|
-
if (pageIdentifier && fieldName) {
|
|
3197
|
-
return `${pageIdentifier}-${fieldName}`;
|
|
3198
|
-
}
|
|
3199
|
-
}
|
|
3200
|
-
return null;
|
|
3201
|
-
};
|
|
3202
|
-
const chooseIdentifierBase = (location, asset) => {
|
|
3203
|
-
const baseFromFilename = buildMediaIdentifierFromFilename(asset.filename ?? "");
|
|
3204
|
-
const baseFromContext = buildIdentifierBaseFromLocation(location);
|
|
3205
|
-
if (baseFromFilename && !GENERIC_MEDIA_NAMES.has(baseFromFilename)) {
|
|
3206
|
-
return baseFromFilename;
|
|
3207
|
-
}
|
|
3208
|
-
return baseFromContext || baseFromFilename || "media";
|
|
3209
|
-
};
|
|
3210
|
-
const allocateIdentifier = (base, existingIdentifiers) => resolveMediaIdentifier(base, existingIdentifiers);
|
|
3211
|
-
const parseFilenameFromUrl = (src) => {
|
|
3212
|
-
try {
|
|
3213
|
-
const url = new URL(src);
|
|
3214
|
-
const raw = url.pathname.split("/").filter(Boolean).pop() ?? "";
|
|
3215
|
-
const decoded = raw ? decodeURIComponent(raw) : "";
|
|
3216
|
-
const trimmed = decoded.trim();
|
|
3217
|
-
return trimmed.length > 0 ? trimmed : null;
|
|
3218
|
-
} catch {
|
|
3219
|
-
return null;
|
|
3220
|
-
}
|
|
3221
|
-
};
|
|
3222
|
-
const ensureFilenameExtension = (filename, mimeType) => {
|
|
3223
|
-
if (path$1.extname(filename)) return filename;
|
|
3224
|
-
const ext = getExtensionForMimeType(mimeType);
|
|
3225
|
-
return ext ? `${filename}${ext}` : filename;
|
|
3226
|
-
};
|
|
3227
|
-
const chooseRichTextIdentifierBase = (location, src) => {
|
|
3228
|
-
const filename = parseFilenameFromUrl(src) ?? "";
|
|
3229
|
-
const baseFromFilename = filename ? buildMediaIdentifierFromFilename(filename) : null;
|
|
3230
|
-
const baseFromContext = buildIdentifierBaseFromLocation(location);
|
|
3231
|
-
const base = baseFromFilename && !GENERIC_MEDIA_NAMES.has(baseFromFilename) ? baseFromFilename : baseFromContext || baseFromFilename || "rich-text-image";
|
|
3232
|
-
return `${base}-rt`;
|
|
3233
|
-
};
|
|
3234
|
-
|
|
3235
|
-
//#endregion
|
|
3236
|
-
//#region src/media/push-preparation-reads.ts
|
|
3237
|
-
const readMediaManifestAndLocalFiles = async (options) => {
|
|
3238
|
-
const manifest = options.manifest ?? await readMediaManifest(options.contentDir);
|
|
3239
|
-
const errors = [];
|
|
3240
|
-
const localFiles = new Map();
|
|
3241
|
-
for (const identifier of options.identifiers) {
|
|
3242
|
-
try {
|
|
3243
|
-
const localFile = await loadLocalMediaFile(options.contentDir, identifier, manifest);
|
|
3244
|
-
localFiles.set(identifier, localFile);
|
|
3245
|
-
} catch (error) {
|
|
3246
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
3247
|
-
errors.push(`Failed to load media file for "${identifier}": ${message}`);
|
|
3248
|
-
localFiles.set(identifier, null);
|
|
3249
|
-
}
|
|
3250
|
-
}
|
|
3251
|
-
return {
|
|
3252
|
-
manifest,
|
|
3253
|
-
localFiles,
|
|
3254
|
-
errors
|
|
3255
|
-
};
|
|
3256
|
-
};
|
|
3257
|
-
const resolveMediaAssets = async (client, identifiers) => {
|
|
3258
|
-
const resolved = await client.media.resolve({ identifiers: [...identifiers] });
|
|
3259
|
-
const assetsByIdentifier = new Map();
|
|
3260
|
-
for (const asset of resolved.assets) {
|
|
3261
|
-
if (asset.identifier) assetsByIdentifier.set(asset.identifier, asset);
|
|
3262
|
-
}
|
|
3263
|
-
return {
|
|
3264
|
-
assetsByIdentifier,
|
|
3265
|
-
missingIdentifiers: new Set(resolved.missingIdentifiers)
|
|
3266
|
-
};
|
|
3267
|
-
};
|
|
3268
|
-
const readChecksumMatches = async (options) => {
|
|
3269
|
-
const matchesByChecksum = new Map();
|
|
3270
|
-
for (const identifier of options.identifiers) {
|
|
3271
|
-
if (options.preparedIdentifiers.has(identifier)) continue;
|
|
3272
|
-
const localFile = options.localFiles.get(identifier);
|
|
3273
|
-
const asset = options.assetsByIdentifier.get(identifier);
|
|
3274
|
-
const hasConflict = Boolean(asset && localFile && asset.checksumSha256 && asset.checksumSha256 !== localFile.checksum);
|
|
3275
|
-
if (hasConflict) continue;
|
|
3276
|
-
if (!localFile || !options.missingIdentifiers.has(identifier) && !hasConflict) {
|
|
3277
|
-
continue;
|
|
3278
|
-
}
|
|
3279
|
-
const matches = (await options.client.media.findByChecksum([localFile.checksum])).filter((candidate) => candidate.checksumSha256 === localFile.checksum);
|
|
3280
|
-
const stableMatches = matches.slice().sort((a, b) => a.id.localeCompare(b.id)).map((candidate) => ({ ...candidate }));
|
|
3281
|
-
matchesByChecksum.set(localFile.checksum, stableMatches);
|
|
3282
|
-
}
|
|
3283
|
-
return matchesByChecksum;
|
|
3284
|
-
};
|
|
3285
|
-
const readRichTextImageSource = async (options) => {
|
|
3286
|
-
const downloaded = await downloadMedia(options.source);
|
|
3287
|
-
if (!downloaded) {
|
|
3288
|
-
throw new Error(`Failed to download rich text image from ${options.source}`);
|
|
3289
|
-
}
|
|
3290
|
-
const mimeType = downloaded.contentType.split(";")[0]?.trim() || "application/octet-stream";
|
|
3291
|
-
if (!mimeType.startsWith("image/")) {
|
|
3292
|
-
throw new Error(`Unsupported content-type for rich text image import: ${mimeType}`);
|
|
3293
|
-
}
|
|
3294
|
-
const data = Buffer.from(downloaded.data);
|
|
3295
|
-
const checksum = sha256(data);
|
|
3296
|
-
const checksumMatches = (await options.client.media.findByChecksum([checksum])).filter((candidate) => candidate.checksumSha256 === checksum).slice().sort((a, b) => a.id.localeCompare(b.id)).map((candidate) => ({ ...candidate }));
|
|
3297
|
-
const filename = ensureFilenameExtension(parseFilenameFromUrl(options.source) ?? "image", mimeType);
|
|
3298
|
-
return {
|
|
3299
|
-
location: options.location,
|
|
3300
|
-
attrs: options.attrs,
|
|
3301
|
-
source: options.source,
|
|
3302
|
-
mimeType,
|
|
3303
|
-
checksum,
|
|
3304
|
-
file: cloneLocalFile({
|
|
3305
|
-
identifier: "planned-rich-text-image",
|
|
3306
|
-
filename,
|
|
3307
|
-
filePath: options.source,
|
|
3308
|
-
mimeType,
|
|
3309
|
-
checksum,
|
|
3310
|
-
data
|
|
3311
|
-
}),
|
|
3312
|
-
checksumMatches
|
|
3313
|
-
};
|
|
3314
|
-
};
|
|
3315
|
-
/**
|
|
3316
|
-
* Reads remote rich-text image sources without changing content or CMS state.
|
|
3317
|
-
* Identifier allocation and operation selection happen in the pure planner.
|
|
3318
|
-
*/
|
|
3319
|
-
const readRichTextImageImports = async (options) => {
|
|
3320
|
-
const successful = [];
|
|
3321
|
-
const failures = [];
|
|
3322
|
-
for (const node of collectRichTextImageAttrs(options.content)) {
|
|
3323
|
-
const rawIdentifier = typeof node.attrs.identifier === "string" ? node.attrs.identifier.trim() : "";
|
|
3324
|
-
const source = typeof node.attrs.src === "string" ? node.attrs.src.trim() : "";
|
|
3325
|
-
if (rawIdentifier || !source) continue;
|
|
3326
|
-
try {
|
|
3327
|
-
successful.push(await readRichTextImageSource({
|
|
3328
|
-
client: options.client,
|
|
3329
|
-
location: node.location,
|
|
3330
|
-
attrs: node.attrs,
|
|
3331
|
-
source
|
|
3332
|
-
}));
|
|
3333
|
-
} catch (error) {
|
|
3334
|
-
failures.push({
|
|
3335
|
-
location: node.location,
|
|
3336
|
-
error: error instanceof Error ? error.message : String(error)
|
|
3337
|
-
});
|
|
3338
|
-
}
|
|
3339
|
-
}
|
|
3340
|
-
return {
|
|
3341
|
-
successful,
|
|
3342
|
-
failures
|
|
3343
|
-
};
|
|
3344
|
-
};
|
|
3345
|
-
const localFileChecksums = (identifiers, localFiles) => new Map(identifiers.map((identifier) => [identifier, localFiles.get(identifier)?.checksum]));
|
|
3346
|
-
const cloneAssetMap = (assets) => new Map(Array.from(assets.entries()).map(([identifier, asset]) => [identifier, { ...asset }]));
|
|
3347
|
-
const cloneMissingSet = (missing) => new Set(missing);
|
|
3348
|
-
|
|
3349
|
-
//#endregion
|
|
3350
|
-
//#region src/media/push-rich-text-plan.ts
|
|
3351
|
-
const localFileWithIdentifier = (file, identifier) => ({
|
|
3352
|
-
...file,
|
|
3353
|
-
identifier
|
|
3354
|
-
});
|
|
3355
|
-
const withLocation = (locations, identifier, location) => {
|
|
3356
|
-
const current = locations.get(identifier) ?? [];
|
|
3357
|
-
current.push(location);
|
|
3358
|
-
locations.set(identifier, current);
|
|
3359
|
-
};
|
|
3360
|
-
const chooseExistingRichTextAsset = (matches) => matches.slice().sort((a, b) => a.createdAt.localeCompare(b.createdAt))[0];
|
|
3361
|
-
const normalizeRichTextIdentifiers = (options) => {
|
|
3362
|
-
for (const node of collectRichTextImageAttrs(options.content)) {
|
|
3363
|
-
const attrs = node.attrs;
|
|
3364
|
-
const rawIdentifier = typeof attrs.identifier === "string" ? attrs.identifier.trim() : "";
|
|
3365
|
-
if (!rawIdentifier) continue;
|
|
3366
|
-
const normalized = normalizeMediaIdentifierInput(rawIdentifier);
|
|
3367
|
-
if (!normalized) {
|
|
3368
|
-
options.errors.push(`Rich text image identifier "${rawIdentifier}" at ${node.location} is invalid.`);
|
|
3369
|
-
delete attrs.src;
|
|
3370
|
-
continue;
|
|
3371
|
-
}
|
|
3372
|
-
if (normalized !== rawIdentifier) {
|
|
3373
|
-
options.warnings.push(`Normalized rich text image identifier "${rawIdentifier}" to "${normalized}" at ${node.location}.`);
|
|
3374
|
-
}
|
|
3375
|
-
attrs.identifier = normalized;
|
|
3376
|
-
delete attrs.src;
|
|
3377
|
-
options.existingIdentifiers.add(normalized);
|
|
3378
|
-
withLocation(options.locationsByIdentifier, normalized, node.location);
|
|
3379
|
-
}
|
|
3380
|
-
};
|
|
3381
|
-
/**
|
|
3382
|
-
* Converts already-read rich-text sources into identifier-only content and
|
|
3383
|
-
* closed media operations. It performs no filesystem or CMS I/O.
|
|
3384
|
-
*/
|
|
3385
|
-
const planRichTextImageImports = (options) => {
|
|
3386
|
-
const operations = [];
|
|
3387
|
-
const warnings = [];
|
|
3388
|
-
const errors = options.imports.failures.map((failure) => `Failed to import rich text image @ ${failure.location}: ${failure.error}`);
|
|
3389
|
-
const preparedIdentifiers = new Set();
|
|
3390
|
-
const locationsByIdentifier = new Map();
|
|
3391
|
-
const checksumAssetsByChecksum = new Map();
|
|
3392
|
-
normalizeRichTextIdentifiers({
|
|
3393
|
-
content: options.content,
|
|
3394
|
-
existingIdentifiers: options.existingIdentifiers,
|
|
3395
|
-
warnings,
|
|
3396
|
-
errors,
|
|
3397
|
-
locationsByIdentifier
|
|
3398
|
-
});
|
|
3399
|
-
for (const imported of options.imports.successful) {
|
|
3400
|
-
const attrs = imported.attrs;
|
|
3401
|
-
const matches = imported.checksumMatches;
|
|
3402
|
-
checksumAssetsByChecksum.set(imported.checksum, matches);
|
|
3403
|
-
const existing = chooseExistingRichTextAsset(matches);
|
|
3404
|
-
const planned = existing ? {
|
|
3405
|
-
identifier: existing.identifier,
|
|
3406
|
-
operation: {
|
|
3407
|
-
kind: "unchanged",
|
|
3408
|
-
identifier: existing.identifier,
|
|
3409
|
-
asset: { ...existing },
|
|
3410
|
-
file: localFileWithIdentifier(imported.file, existing.identifier),
|
|
3411
|
-
locations: [imported.location]
|
|
3412
|
-
}
|
|
3413
|
-
} : (() => {
|
|
3414
|
-
const base = chooseRichTextIdentifierBase(imported.location, imported.source);
|
|
3415
|
-
const identifier = resolveMediaIdentifier(base, options.existingIdentifiers);
|
|
3416
|
-
options.existingIdentifiers.add(identifier);
|
|
3417
|
-
withLocation(locationsByIdentifier, identifier, imported.location);
|
|
3418
|
-
const file = localFileWithIdentifier(imported.file, identifier);
|
|
3419
|
-
return {
|
|
3420
|
-
identifier,
|
|
3421
|
-
operation: {
|
|
3422
|
-
kind: "upload",
|
|
3423
|
-
identifier,
|
|
3424
|
-
file,
|
|
3425
|
-
source: "rich_text",
|
|
3426
|
-
locations: [imported.location]
|
|
3427
|
-
}
|
|
3428
|
-
};
|
|
3429
|
-
})();
|
|
3430
|
-
attrs.identifier = planned.identifier;
|
|
3431
|
-
delete attrs.src;
|
|
3432
|
-
options.existingIdentifiers.add(planned.identifier);
|
|
3433
|
-
preparedIdentifiers.add(planned.identifier);
|
|
3434
|
-
operations.push(planned.operation);
|
|
3435
|
-
}
|
|
3436
|
-
return {
|
|
3437
|
-
operations,
|
|
3438
|
-
errors,
|
|
3439
|
-
warnings,
|
|
3440
|
-
preparedIdentifiers,
|
|
3441
|
-
locationsByIdentifier,
|
|
3442
|
-
checksumAssetsByChecksum
|
|
3443
|
-
};
|
|
3444
|
-
};
|
|
3445
|
-
|
|
3446
|
-
//#endregion
|
|
3447
|
-
//#region src/media/content-normalization.ts
|
|
3448
|
-
const normalizeRichTextImage = (node, assetsByIdentifier) => {
|
|
3449
|
-
const attrs = node.attrs;
|
|
3450
|
-
if (!isRecordValue(attrs)) return node;
|
|
3451
|
-
const identifier = typeof attrs.identifier === "string" ? attrs.identifier : "";
|
|
3452
|
-
if (!identifier) return node;
|
|
3453
|
-
const normalizedAttrs = { ...attrs };
|
|
3454
|
-
delete normalizedAttrs.src;
|
|
3455
|
-
const asset = assetsByIdentifier.get(identifier);
|
|
3456
|
-
if (asset) {
|
|
3457
|
-
if (asset.width === null || asset.width === undefined) {
|
|
3458
|
-
delete normalizedAttrs.width;
|
|
3459
|
-
} else {
|
|
3460
|
-
normalizedAttrs.width = asset.width;
|
|
3461
|
-
}
|
|
3462
|
-
if (asset.height === null || asset.height === undefined) {
|
|
3463
|
-
delete normalizedAttrs.height;
|
|
3464
|
-
} else {
|
|
3465
|
-
normalizedAttrs.height = asset.height;
|
|
3466
|
-
}
|
|
3467
|
-
}
|
|
3468
|
-
return {
|
|
3469
|
-
...node,
|
|
3470
|
-
attrs: normalizedAttrs
|
|
3471
|
-
};
|
|
3472
|
-
};
|
|
3473
|
-
const normalizeValueTree = (value, assetsByIdentifier) => {
|
|
3474
|
-
if (Array.isArray(value)) {
|
|
3475
|
-
return value.map((item) => normalizeValueTree(item, assetsByIdentifier));
|
|
3476
|
-
}
|
|
3477
|
-
if (!isRecordValue(value)) return value;
|
|
3478
|
-
const normalizedChildren = Object.fromEntries(Object.entries(value).map(([key, child]) => [key, normalizeValueTree(child, assetsByIdentifier)]));
|
|
3479
|
-
if (isMediaValue(value) && typeof value.identifier === "string") {
|
|
3480
|
-
return normalizeMediaValueToIdentifierOnly(value, value.identifier, assetsByIdentifier.get(value.identifier));
|
|
3481
|
-
}
|
|
3482
|
-
const normalized = normalizedChildren;
|
|
3483
|
-
if (value.type === "image") {
|
|
3484
|
-
return normalizeRichTextImage(normalized, assetsByIdentifier);
|
|
3485
|
-
}
|
|
3486
|
-
return normalized;
|
|
3487
|
-
};
|
|
3488
|
-
const normalizeRecord = (value, assetsByIdentifier) => {
|
|
3489
|
-
const normalized = normalizeValueTree(value, assetsByIdentifier);
|
|
3490
|
-
return normalized;
|
|
3491
|
-
};
|
|
3492
|
-
/**
|
|
3493
|
-
* Return local content with media references in the same portable shape that
|
|
3494
|
-
* the push diff and command compiler receive. The input is never mutated.
|
|
3495
|
-
*/
|
|
3496
|
-
const normalizeLocalContentMedia = (content, assetsByIdentifier) => ({
|
|
3497
|
-
...content,
|
|
3498
|
-
entries: new Map(Array.from(content.entries.entries(), ([contentType, entries]) => [contentType, entries.map((entry) => ({
|
|
3499
|
-
...entry,
|
|
3500
|
-
data: normalizeRecord(entry.data, assetsByIdentifier),
|
|
3501
|
-
blocks: entry.blocks ? Object.fromEntries(Object.entries(entry.blocks).map(([slotKey, blocks]) => [slotKey, blocks.map((block) => ({
|
|
3502
|
-
...block,
|
|
3503
|
-
content: normalizeRecord(block.content, assetsByIdentifier)
|
|
3504
|
-
}))])) : entry.blocks
|
|
3505
|
-
}))])),
|
|
3506
|
-
pages: content.pages.map((page) => ({
|
|
3507
|
-
...page,
|
|
3508
|
-
blocks: page.blocks?.map((block) => ({
|
|
3509
|
-
...block,
|
|
3510
|
-
data: normalizeRecord(block.data, assetsByIdentifier)
|
|
3511
|
-
}))
|
|
3512
|
-
})),
|
|
3513
|
-
navigation: normalizeValueTree(content.navigation, assetsByIdentifier),
|
|
3514
|
-
settings: normalizeValueTree(content.settings, assetsByIdentifier),
|
|
3515
|
-
forms: content.forms.map((form) => ({
|
|
3516
|
-
...form,
|
|
3517
|
-
schema: normalizeRecord(form.schema, assetsByIdentifier),
|
|
3518
|
-
settings: normalizeRecord(form.settings, assetsByIdentifier)
|
|
3519
|
-
})),
|
|
3520
|
-
footer: normalizeValueTree(content.footer, assetsByIdentifier),
|
|
3521
|
-
venues: content.venues.map((venue) => normalizeRecord(venue, assetsByIdentifier)),
|
|
3522
|
-
eventCategories: content.eventCategories.map((category) => normalizeRecord(category, assetsByIdentifier)),
|
|
3523
|
-
events: content.events.map((event) => normalizeRecord(event, assetsByIdentifier))
|
|
3524
|
-
});
|
|
3525
|
-
const addAsset = (assetsByIdentifier, identifier, asset) => {
|
|
3526
|
-
assetsByIdentifier.set(identifier, {
|
|
3527
|
-
...asset,
|
|
3528
|
-
identifier
|
|
3529
|
-
});
|
|
3530
|
-
};
|
|
3531
|
-
/** Build the CMS asset lookup available while applying a prepared media plan. */
|
|
3532
|
-
const assetsForPushMediaPlan = (options) => {
|
|
3533
|
-
const assetsByIdentifier = new Map();
|
|
3534
|
-
const observationsByAssetId = new Map();
|
|
3535
|
-
for (const observation of options.observations) {
|
|
3536
|
-
if (observation.resolvedAsset) {
|
|
3537
|
-
addAsset(assetsByIdentifier, observation.identifier, observation.resolvedAsset);
|
|
3538
|
-
observationsByAssetId.set(observation.resolvedAsset.id, observation.resolvedAsset);
|
|
3539
|
-
}
|
|
3540
|
-
const checksumAssets = observation.localChecksum.kind === "present" && observation.localChecksum.lookup.kind === "read" ? observation.localChecksum.lookup.assets : [];
|
|
3541
|
-
for (const asset of checksumAssets) {
|
|
3542
|
-
observationsByAssetId.set(asset.id, asset);
|
|
3543
|
-
}
|
|
3544
|
-
}
|
|
3545
|
-
for (const operation of options.operations) {
|
|
3546
|
-
if (operation.kind === "unchanged" || operation.kind === "reused") {
|
|
3547
|
-
addAsset(assetsByIdentifier, operation.identifier, operation.asset);
|
|
3548
|
-
continue;
|
|
3549
|
-
}
|
|
3550
|
-
}
|
|
3551
|
-
return assetsByIdentifier;
|
|
3552
|
-
};
|
|
3553
|
-
|
|
3554
|
-
//#endregion
|
|
3555
|
-
//#region src/media/push-preparation.ts
|
|
3556
|
-
const cloneLocalContent = (content) => structuredClone(content);
|
|
3557
|
-
const collectExistingIdentifiers = (content) => {
|
|
3558
|
-
const identifiers = new Set();
|
|
3559
|
-
walkLocalContentMedia(content, (value) => {
|
|
3560
|
-
if (typeof value.identifier === "string" && value.identifier.trim()) {
|
|
3561
|
-
identifiers.add(value.identifier.trim());
|
|
3562
|
-
}
|
|
3563
|
-
});
|
|
3564
|
-
return identifiers;
|
|
3565
|
-
};
|
|
3566
|
-
const mergeLocations = (locations, identifier, location) => {
|
|
3567
|
-
const current = locations.get(identifier) ?? [];
|
|
3568
|
-
if (!current.includes(location)) current.push(location);
|
|
3569
|
-
locations.set(identifier, current);
|
|
3570
|
-
};
|
|
3571
|
-
const mergeRichTextLocations = (locations, additional) => {
|
|
3572
|
-
for (const [identifier, identifierLocations] of additional) {
|
|
3573
|
-
for (const location of identifierLocations) {
|
|
3574
|
-
mergeLocations(locations, identifier, location);
|
|
3575
|
-
}
|
|
3576
|
-
}
|
|
3577
|
-
};
|
|
3578
|
-
const initialAssetMap = (assetsByIdentifier) => cloneAssetMap(assetsByIdentifier);
|
|
3579
|
-
const initialMissingSet = (missingIdentifiers) => cloneMissingSet(missingIdentifiers);
|
|
3580
|
-
const appendClassification = (options) => {
|
|
3581
|
-
options.operations.push(...options.classification.operations);
|
|
3582
|
-
options.errors.push(...options.classification.errors);
|
|
3583
|
-
options.warnings.push(...options.classification.warnings ?? []);
|
|
3584
|
-
applyManifestClassification(options.manifest, options.identifier, options.classification);
|
|
3585
|
-
};
|
|
3586
|
-
const selectPushReferences = (options) => {
|
|
3587
|
-
const collected = collectLocalMediaReferences(options.content);
|
|
3588
|
-
const warnings = [...options.richTextPlan.warnings, ...collected.warnings];
|
|
3589
|
-
const errors = [...options.richTextPlan.errors, ...collected.errors];
|
|
3590
|
-
const locationsByIdentifier = new Map();
|
|
3591
|
-
mergeRichTextLocations(locationsByIdentifier, options.richTextPlan.locationsByIdentifier);
|
|
3592
|
-
mergeRichTextLocations(locationsByIdentifier, collected.locationsByIdentifier);
|
|
3593
|
-
const references = [...collected.mediaReferences.map((reference) => ({
|
|
3594
|
-
identifier: reference.identifier,
|
|
3595
|
-
location: reference.location,
|
|
3596
|
-
richText: false
|
|
3597
|
-
})), ...collected.richTextReferences.map((reference) => ({
|
|
3598
|
-
identifier: reference.identifier,
|
|
3599
|
-
location: reference.location,
|
|
3600
|
-
richText: true
|
|
3601
|
-
}))];
|
|
3602
|
-
for (const reference of references) {
|
|
3603
|
-
mergeLocations(locationsByIdentifier, reference.identifier, reference.location);
|
|
3604
|
-
}
|
|
3605
|
-
const identifiers = Array.from(new Set(references.map((reference) => reference.identifier))).sort();
|
|
3606
|
-
return {
|
|
3607
|
-
references,
|
|
3608
|
-
identifiers,
|
|
3609
|
-
locationsByIdentifier,
|
|
3610
|
-
warnings,
|
|
3611
|
-
errors,
|
|
3612
|
-
summary: emptyPushSummary(references.length, identifiers.length)
|
|
3613
|
-
};
|
|
3614
|
-
};
|
|
3615
|
-
const prepareLocalReferenceOperations = async (options) => {
|
|
3616
|
-
const loaded = await readMediaManifestAndLocalFiles({
|
|
3617
|
-
contentDir: options.prepare.contentDir,
|
|
3618
|
-
identifiers: options.selection.identifiers,
|
|
3619
|
-
manifest: options.manifest
|
|
3620
|
-
});
|
|
3621
|
-
const errors = [...loaded.errors];
|
|
3622
|
-
const warnings = [];
|
|
3623
|
-
const resolved = await resolveMediaAssets(options.prepare.client, options.selection.identifiers);
|
|
3624
|
-
const initialAssets = initialAssetMap(resolved.assetsByIdentifier);
|
|
3625
|
-
const initialMissing = initialMissingSet(resolved.missingIdentifiers);
|
|
3626
|
-
const checksumMatches = await readChecksumMatches({
|
|
3627
|
-
client: options.prepare.client,
|
|
3628
|
-
identifiers: options.selection.identifiers,
|
|
3629
|
-
localFiles: loaded.localFiles,
|
|
3630
|
-
assetsByIdentifier: resolved.assetsByIdentifier,
|
|
3631
|
-
missingIdentifiers: resolved.missingIdentifiers,
|
|
3632
|
-
preparedIdentifiers: options.richTextPlan.preparedIdentifiers
|
|
3633
|
-
});
|
|
3634
|
-
const operations = [];
|
|
3635
|
-
for (const identifier of options.selection.identifiers) {
|
|
3636
|
-
if (options.richTextPlan.preparedIdentifiers.has(identifier)) continue;
|
|
3637
|
-
const localFile = loaded.localFiles.get(identifier);
|
|
3638
|
-
const classification = classifyLocalMediaReference({
|
|
3639
|
-
identifier,
|
|
3640
|
-
asset: resolved.assetsByIdentifier.get(identifier),
|
|
3641
|
-
localFile,
|
|
3642
|
-
missing: resolved.missingIdentifiers.has(identifier),
|
|
3643
|
-
checksumMatches: localFile ? checksumMatches.get(localFile.checksum) ?? [] : [],
|
|
3644
|
-
locations: locationsForIdentifier(options.selection.locationsByIdentifier, identifier),
|
|
3645
|
-
locationNote: buildLocationNote(options.selection.locationsByIdentifier, identifier)
|
|
3646
|
-
});
|
|
3647
|
-
appendClassification({
|
|
3648
|
-
identifier,
|
|
3649
|
-
classification,
|
|
3650
|
-
manifest: options.manifest,
|
|
3651
|
-
operations,
|
|
3652
|
-
errors,
|
|
3653
|
-
warnings
|
|
3654
|
-
});
|
|
3655
|
-
}
|
|
3656
|
-
const sortedOperations = sortOperations(operations);
|
|
3657
|
-
const localChecksums = localFileChecksums(options.selection.identifiers, loaded.localFiles);
|
|
3658
|
-
for (const operation of sortedOperations) {
|
|
3659
|
-
if ("file" in operation && operation.file) {
|
|
3660
|
-
localChecksums.set(operation.identifier, operation.file.checksum);
|
|
3661
|
-
}
|
|
3662
|
-
}
|
|
3663
|
-
const checksumAssetsByChecksum = new Map(options.richTextPlan.checksumAssetsByChecksum);
|
|
3664
|
-
for (const [checksum, matches] of checksumMatches) {
|
|
3665
|
-
checksumAssetsByChecksum.set(checksum, matches);
|
|
3666
|
-
}
|
|
3667
|
-
return {
|
|
3668
|
-
operations: sortedOperations,
|
|
3669
|
-
errors,
|
|
3670
|
-
warnings,
|
|
3671
|
-
mediaReadSet: buildMediaReadSet({
|
|
3672
|
-
identifiers: options.selection.identifiers,
|
|
3673
|
-
localChecksums,
|
|
3674
|
-
initialAssets,
|
|
3675
|
-
initialMissing,
|
|
3676
|
-
checksumAssetsByChecksum
|
|
3677
|
-
})
|
|
3678
|
-
};
|
|
3679
|
-
};
|
|
3680
|
-
async function prepareLocalMediaForPush(options) {
|
|
3681
|
-
const content = cloneLocalContent(options.content);
|
|
3682
|
-
const manifest = await readMediaManifest(options.contentDir);
|
|
3683
|
-
const existingIdentifiers = collectExistingIdentifiers(content);
|
|
3684
|
-
const richTextReads = await readRichTextImageImports({
|
|
3685
|
-
content,
|
|
3686
|
-
client: options.client
|
|
3687
|
-
});
|
|
3688
|
-
const richTextPlan = planRichTextImageImports({
|
|
3689
|
-
content,
|
|
3690
|
-
imports: richTextReads,
|
|
3691
|
-
existingIdentifiers
|
|
3692
|
-
});
|
|
3693
|
-
const selection = selectPushReferences({
|
|
3694
|
-
content,
|
|
3695
|
-
richTextPlan
|
|
3696
|
-
});
|
|
3697
|
-
const prepared = selection.identifiers.length > 0 ? await prepareLocalReferenceOperations({
|
|
3698
|
-
prepare: options,
|
|
3699
|
-
manifest,
|
|
3700
|
-
selection,
|
|
3701
|
-
richTextPlan
|
|
3702
|
-
}) : {
|
|
3703
|
-
operations: [],
|
|
3704
|
-
errors: [],
|
|
3705
|
-
warnings: [],
|
|
3706
|
-
mediaReadSet: []
|
|
3707
|
-
};
|
|
3708
|
-
const contentWithPortableMedia = normalizeLocalContentMedia(content, assetsForPushMediaPlan({
|
|
3709
|
-
operations: [...richTextPlan.operations, ...prepared.operations],
|
|
3710
|
-
observations: prepared.mediaReadSet
|
|
3711
|
-
}));
|
|
3712
|
-
return {
|
|
3713
|
-
content: contentWithPortableMedia,
|
|
3714
|
-
manifest,
|
|
3715
|
-
operations: sortOperations([...richTextPlan.operations, ...prepared.operations]),
|
|
3716
|
-
references: selection.references,
|
|
3717
|
-
summary: selection.summary,
|
|
3718
|
-
warnings: [...selection.warnings, ...prepared.warnings],
|
|
3719
|
-
errors: [...selection.errors, ...prepared.errors],
|
|
3720
|
-
targetEnv: options.targetEnv,
|
|
3721
|
-
mediaReadSet: prepared.mediaReadSet
|
|
3722
|
-
};
|
|
3723
|
-
}
|
|
3724
|
-
|
|
3725
|
-
//#endregion
|
|
3726
|
-
//#region src/media/push-failure.ts
|
|
3727
|
-
const includesAny = (value, needles) => needles.some((needle) => value.includes(needle));
|
|
3728
|
-
const managementErrorRules = [
|
|
3729
|
-
{
|
|
3730
|
-
kind: "max_object_size",
|
|
3731
|
-
matches: (error) => {
|
|
3732
|
-
const code = error.code.toLowerCase();
|
|
3733
|
-
const message = error.message.toLowerCase();
|
|
3734
|
-
return error.statusCode === 413 || code.includes("too_large") || message.includes("too large");
|
|
3735
|
-
}
|
|
3736
|
-
},
|
|
3737
|
-
{
|
|
3738
|
-
kind: "unsupported_mime",
|
|
3739
|
-
matches: (error) => {
|
|
3740
|
-
const code = error.code.toLowerCase();
|
|
3741
|
-
const message = error.message.toLowerCase();
|
|
3742
|
-
return error.statusCode === 415 || includesAny(code, ["mime", "content-type"]) || includesAny(message, ["mime", "content-type"]);
|
|
3743
|
-
}
|
|
3744
|
-
},
|
|
3745
|
-
{
|
|
3746
|
-
kind: "quota_or_storage_limit",
|
|
3747
|
-
matches: (error) => {
|
|
3748
|
-
const code = error.code.toLowerCase();
|
|
3749
|
-
const message = error.message.toLowerCase();
|
|
3750
|
-
return error.statusCode === 402 || error.statusCode === 507 || includesAny(code, ["quota", "storage"]) || includesAny(message, ["quota", "storage limit"]);
|
|
3751
|
-
}
|
|
3752
|
-
},
|
|
3753
|
-
{
|
|
3754
|
-
kind: "auth_or_permission",
|
|
3755
|
-
matches: (error) => {
|
|
3756
|
-
const code = error.code.toLowerCase();
|
|
3757
|
-
return error.statusCode === 401 || error.statusCode === 403 || includesAny(code, ["auth", "permission"]);
|
|
3758
|
-
}
|
|
3759
|
-
},
|
|
3760
|
-
{
|
|
3761
|
-
kind: "network",
|
|
3762
|
-
matches: (error) => {
|
|
3763
|
-
const code = error.code.toLowerCase();
|
|
3764
|
-
return code === "sdk:network-error" || code === "sdk:timeout";
|
|
3765
|
-
}
|
|
3766
|
-
}
|
|
3767
|
-
];
|
|
3768
|
-
const genericErrorRules = [
|
|
3769
|
-
{
|
|
3770
|
-
kind: "network",
|
|
3771
|
-
matches: (error) => includesAny(error.message.toLowerCase(), [
|
|
3772
|
-
"network",
|
|
3773
|
-
"timeout",
|
|
3774
|
-
"timed out"
|
|
3775
|
-
])
|
|
3776
|
-
},
|
|
3777
|
-
{
|
|
3778
|
-
kind: "max_object_size",
|
|
3779
|
-
matches: (error) => includesAny(error.message.toLowerCase(), [
|
|
3780
|
-
"too large",
|
|
3781
|
-
"payload too large",
|
|
3782
|
-
"413"
|
|
3783
|
-
])
|
|
3784
|
-
},
|
|
3785
|
-
{
|
|
3786
|
-
kind: "unsupported_mime",
|
|
3787
|
-
matches: (error) => includesAny(error.message.toLowerCase(), [
|
|
3788
|
-
"mime",
|
|
3789
|
-
"content-type",
|
|
3790
|
-
"unsupported media"
|
|
3791
|
-
])
|
|
3792
|
-
},
|
|
3793
|
-
{
|
|
3794
|
-
kind: "quota_or_storage_limit",
|
|
3795
|
-
matches: (error) => includesAny(error.message.toLowerCase(), ["quota", "storage limit"])
|
|
3796
|
-
},
|
|
3797
|
-
{
|
|
3798
|
-
kind: "auth_or_permission",
|
|
3799
|
-
matches: (error) => includesAny(error.message.toLowerCase(), [
|
|
3800
|
-
"unauthorized",
|
|
3801
|
-
"forbidden",
|
|
3802
|
-
"permission"
|
|
3803
|
-
])
|
|
3804
|
-
}
|
|
3805
|
-
];
|
|
3806
|
-
const classifyWithRules = (error, rules) => rules.find((rule) => rule.matches(error))?.kind;
|
|
3807
|
-
const classifyManagementApiError = (error) => {
|
|
3808
|
-
const classified = classifyWithRules(error, managementErrorRules);
|
|
3809
|
-
if (classified) return classified;
|
|
3810
|
-
if (error.code.toLowerCase().startsWith("sdk:") || typeof error.statusCode === "number") {
|
|
3811
|
-
return "cms_api";
|
|
3812
|
-
}
|
|
3813
|
-
return "unknown";
|
|
3814
|
-
};
|
|
3815
|
-
const classifyErrorMessage = (error) => classifyWithRules(error, genericErrorRules) ?? "unknown";
|
|
3816
|
-
function classifyLocalMediaUploadFailure(error) {
|
|
3817
|
-
if (error instanceof ManagementApiError) {
|
|
3818
|
-
return classifyManagementApiError(error);
|
|
3819
|
-
}
|
|
3820
|
-
if (error instanceof Error) {
|
|
3821
|
-
return classifyErrorMessage(error);
|
|
3822
|
-
}
|
|
3823
|
-
return "unknown";
|
|
3824
|
-
}
|
|
3825
|
-
|
|
3826
|
-
//#endregion
|
|
3827
|
-
//#region src/media/push-application.ts
|
|
3828
|
-
const rawErrorMessage$1 = (error) => error instanceof Error ? error.message : String(error);
|
|
3829
|
-
const makeReport = (params) => {
|
|
3830
|
-
const failedIdentifiers = params.items.filter((item) => item.status === "failed").map((item) => item.identifier);
|
|
3831
|
-
const missingIdentifiers = params.items.filter((item) => item.status === "missing_local_file").map((item) => item.identifier);
|
|
3832
|
-
return {
|
|
3833
|
-
targetEnv: params.targetEnv,
|
|
3834
|
-
summary: { ...params.summary },
|
|
3835
|
-
items: params.items,
|
|
3836
|
-
failedIdentifiers,
|
|
3837
|
-
missingIdentifiers
|
|
3838
|
-
};
|
|
3839
|
-
};
|
|
3840
|
-
const reportForOperation = (operation, status, targetEnv, message, failureKind, rawError) => {
|
|
3841
|
-
const file = "file" in operation ? operation.file : undefined;
|
|
3842
|
-
return {
|
|
3843
|
-
identifier: operation.identifier,
|
|
3844
|
-
status,
|
|
3845
|
-
targetEnv,
|
|
3846
|
-
...file ? {
|
|
3847
|
-
filename: file.filename,
|
|
3848
|
-
sizeBytes: file.dataLength,
|
|
3849
|
-
mimeType: file.mimeType
|
|
3850
|
-
} : {},
|
|
3851
|
-
locations: operation.locations,
|
|
3852
|
-
message,
|
|
3853
|
-
failureKind,
|
|
3854
|
-
rawError
|
|
3855
|
-
};
|
|
3856
|
-
};
|
|
3857
|
-
const applySimpleMediaOperation = (operation, state, targetEnv) => {
|
|
3858
|
-
switch (operation.kind) {
|
|
3859
|
-
case "unchanged":
|
|
3860
|
-
state.assetsByIdentifier.set(operation.identifier, operation.asset);
|
|
3861
|
-
state.summary.resolved += 1;
|
|
3862
|
-
state.reportItems.push(reportForOperation(operation, "resolved", targetEnv));
|
|
3863
|
-
return;
|
|
3864
|
-
case "reused":
|
|
3865
|
-
state.assetsByIdentifier.set(operation.identifier, operation.asset);
|
|
3866
|
-
state.summary.resolved += 1;
|
|
3867
|
-
state.summary.reused += 1;
|
|
3868
|
-
state.reportItems.push(reportForOperation(operation, "reused", targetEnv, "Reused existing CMS asset with matching identifier and checksum."));
|
|
3869
|
-
return;
|
|
3870
|
-
case "skipped":
|
|
3871
|
-
state.summary.skipped += 1;
|
|
3872
|
-
state.reportItems.push(reportForOperation(operation, "skipped", targetEnv, operation.message));
|
|
3873
|
-
return;
|
|
3874
|
-
case "missing_local_file":
|
|
3875
|
-
state.reportItems.push(reportForOperation(operation, "missing_local_file", targetEnv, operation.message));
|
|
3876
|
-
return;
|
|
3877
|
-
case "conflict":
|
|
3878
|
-
state.reportItems.push(reportForOperation(operation, "failed", targetEnv, operation.message, "unknown"));
|
|
3879
|
-
return;
|
|
3880
|
-
default: return assertNever(operation);
|
|
3881
|
-
}
|
|
3882
|
-
};
|
|
3883
|
-
const applyUpload = async (operation, context, state) => {
|
|
3884
|
-
const pendingItem = reportForOperation(operation, "pending_upload", context.targetEnv);
|
|
3885
|
-
if (context.dryRun) {
|
|
3886
|
-
state.summary.pendingUploads += 1;
|
|
3887
|
-
state.reportItems.push(pendingItem);
|
|
3888
|
-
return;
|
|
3889
|
-
}
|
|
3890
|
-
context.onEvent?.({
|
|
3891
|
-
type: "upload_started",
|
|
3892
|
-
item: pendingItem
|
|
3893
|
-
});
|
|
3894
|
-
try {
|
|
3895
|
-
const result = await context.client.media.upload({
|
|
3896
|
-
data: Buffer.from(operation.file.dataBase64, "base64"),
|
|
3897
|
-
filename: operation.file.filename,
|
|
3898
|
-
contentType: operation.file.mimeType,
|
|
3899
|
-
identifier: operation.identifier
|
|
3900
|
-
});
|
|
3901
|
-
state.uploadedIdentifiers.push(operation.identifier);
|
|
3902
|
-
if (result.status === "created") state.summary.uploaded += 1;
|
|
3903
|
-
state.summary.resolved += 1;
|
|
3904
|
-
const uploadedItem = reportForOperation(operation, "uploaded", context.targetEnv, result.status === "created" ? "Uploaded local media file." : "CMS reported media already exists.");
|
|
3905
|
-
state.reportItems.push(uploadedItem);
|
|
3906
|
-
context.onEvent?.({
|
|
3907
|
-
type: "upload_succeeded",
|
|
3908
|
-
item: uploadedItem
|
|
3909
|
-
});
|
|
3910
|
-
} catch (error) {
|
|
3911
|
-
const message = rawErrorMessage$1(error);
|
|
3912
|
-
const failedItem = reportForOperation(operation, "failed", context.targetEnv, `Failed to upload "${operation.identifier}": ${message}`, classifyLocalMediaUploadFailure(error), message);
|
|
3913
|
-
state.reportItems.push(failedItem);
|
|
3914
|
-
state.errors.push(`Failed to upload "${operation.identifier}" (${operation.file.filename}, ${operation.file.mimeType}): ${message}`);
|
|
3915
|
-
context.onEvent?.({
|
|
3916
|
-
type: "upload_failed",
|
|
3917
|
-
item: failedItem
|
|
3918
|
-
});
|
|
3919
|
-
}
|
|
3920
|
-
};
|
|
3921
|
-
const applyMediaOperation = async (operation, context, state) => {
|
|
3922
|
-
switch (operation.kind) {
|
|
3923
|
-
case "unchanged":
|
|
3924
|
-
case "reused":
|
|
3925
|
-
case "skipped":
|
|
3926
|
-
case "missing_local_file":
|
|
3927
|
-
case "conflict":
|
|
3928
|
-
applySimpleMediaOperation(operation, state, context.targetEnv);
|
|
3929
|
-
return;
|
|
3930
|
-
case "upload":
|
|
3931
|
-
await applyUpload(operation, context, state);
|
|
3932
|
-
return;
|
|
3933
|
-
default: return assertNever(operation);
|
|
3934
|
-
}
|
|
3935
|
-
};
|
|
3936
|
-
const createApplyState = (plan) => ({
|
|
3937
|
-
summary: { ...plan.summary },
|
|
3938
|
-
errors: [...plan.errors],
|
|
3939
|
-
reportItems: [],
|
|
3940
|
-
assetsByIdentifier: new Map(),
|
|
3941
|
-
uploadedIdentifiers: []
|
|
3942
|
-
});
|
|
3943
|
-
const applyMediaOperations = async (operations, context, state) => {
|
|
3944
|
-
for (const operation of operations) {
|
|
3945
|
-
await applyMediaOperation(operation, context, state);
|
|
3946
|
-
}
|
|
3947
|
-
};
|
|
3948
|
-
const resolveUploadedAssets = async (state, client, dryRun) => {
|
|
3949
|
-
if (dryRun || state.uploadedIdentifiers.length === 0) return;
|
|
3950
|
-
const resolvedUploads = await client.media.resolve({ identifiers: state.uploadedIdentifiers });
|
|
3951
|
-
for (const asset of resolvedUploads.assets) {
|
|
3952
|
-
if (asset.identifier) {
|
|
3953
|
-
state.assetsByIdentifier.set(asset.identifier, asset);
|
|
3954
|
-
}
|
|
3955
|
-
}
|
|
3956
|
-
for (const missing of resolvedUploads.missingIdentifiers) {
|
|
3957
|
-
state.errors.push(`Upload succeeded but identifier "${missing}" was not found on the CMS.`);
|
|
3958
|
-
}
|
|
3959
|
-
};
|
|
3960
|
-
const fileByIdentifier = (operations) => {
|
|
3961
|
-
const files = new Map();
|
|
3962
|
-
for (const operation of operations) {
|
|
3963
|
-
if ("file" in operation && !files.has(operation.identifier)) {
|
|
3964
|
-
files.set(operation.identifier, operation.file);
|
|
3965
|
-
}
|
|
3966
|
-
}
|
|
3967
|
-
return files;
|
|
3968
|
-
};
|
|
3969
|
-
const rewriteAppliedReferences = (plan, state) => {
|
|
3970
|
-
const content = normalizeLocalContentMedia(plan.content, state.assetsByIdentifier);
|
|
3971
|
-
const manifest = structuredClone(plan.manifest);
|
|
3972
|
-
const files = fileByIdentifier(plan.operations);
|
|
3973
|
-
for (const reference of plan.references) {
|
|
3974
|
-
const asset = state.assetsByIdentifier.get(reference.identifier);
|
|
3975
|
-
if (!asset) continue;
|
|
3976
|
-
updateManifestEntry(manifest, reference.identifier, asset, files.get(reference.identifier));
|
|
3977
|
-
}
|
|
3978
|
-
return {
|
|
3979
|
-
content,
|
|
3980
|
-
manifest
|
|
3981
|
-
};
|
|
3982
|
-
};
|
|
3983
|
-
const finalSummary = (summary, dryRun) => ({
|
|
3984
|
-
...summary,
|
|
3985
|
-
pendingUploads: dryRun ? summary.pendingUploads : 0
|
|
3986
|
-
});
|
|
3987
|
-
async function applyLocalMediaPush(options) {
|
|
3988
|
-
const { plan, client, dryRun, onEvent } = options;
|
|
3989
|
-
const state = createApplyState(plan);
|
|
3990
|
-
const context = {
|
|
3991
|
-
client,
|
|
3992
|
-
dryRun,
|
|
3993
|
-
targetEnv: plan.targetEnv,
|
|
3994
|
-
onEvent
|
|
3995
|
-
};
|
|
3996
|
-
await applyMediaOperations(plan.operations, context, state);
|
|
3997
|
-
await resolveUploadedAssets(state, client, dryRun);
|
|
3998
|
-
const rewritten = rewriteAppliedReferences(plan, state);
|
|
3999
|
-
const summary = finalSummary(state.summary, dryRun);
|
|
4000
|
-
const hasChanges = summary.uploaded > 0 || summary.pendingUploads > 0;
|
|
4001
|
-
return {
|
|
4002
|
-
...rewritten,
|
|
4003
|
-
summary,
|
|
4004
|
-
report: makeReport({
|
|
4005
|
-
targetEnv: plan.targetEnv,
|
|
4006
|
-
summary,
|
|
4007
|
-
items: state.reportItems
|
|
4008
|
-
}),
|
|
4009
|
-
warnings: [...plan.warnings],
|
|
4010
|
-
errors: state.errors,
|
|
4011
|
-
hasChanges
|
|
4012
|
-
};
|
|
4013
|
-
}
|
|
4014
|
-
|
|
4015
|
-
//#endregion
|
|
4016
|
-
//#region src/media/pull-assets.ts
|
|
4017
|
-
const buildPullAssetIndexes = (resolved) => {
|
|
4018
|
-
const assetsById = new Map(resolved.assets.map((asset) => [asset.id, asset]));
|
|
4019
|
-
const assetsByIdentifier = new Map();
|
|
4020
|
-
for (const asset of resolved.assets) {
|
|
4021
|
-
assetsByIdentifier.set(asset.identifier, asset);
|
|
4022
|
-
}
|
|
4023
|
-
return {
|
|
4024
|
-
assetsById,
|
|
4025
|
-
assetsByIdentifier
|
|
4026
|
-
};
|
|
4027
|
-
};
|
|
4028
|
-
const resolveReferencedAsset = (reference, indexes) => {
|
|
4029
|
-
const byId = reference.value.assetId ? indexes.assetsById.get(reference.value.assetId) : undefined;
|
|
4030
|
-
if (byId) return byId;
|
|
4031
|
-
return reference.value.identifier ? indexes.assetsByIdentifier.get(reference.value.identifier) : undefined;
|
|
4032
|
-
};
|
|
4033
|
-
const normalizePullIdentifier = (options) => {
|
|
4034
|
-
const { initial, location, existingIdentifiers } = options;
|
|
4035
|
-
if (!initial) return {
|
|
4036
|
-
identifier: null,
|
|
4037
|
-
canonicalized: 0
|
|
4038
|
-
};
|
|
4039
|
-
const normalized = slugifyMediaIdentifier(initial);
|
|
4040
|
-
if (!normalized) {
|
|
4041
|
-
return {
|
|
4042
|
-
identifier: initial,
|
|
4043
|
-
canonicalized: 0,
|
|
4044
|
-
warning: `Could not normalize identifier "${initial}" at ${location}.`
|
|
4045
|
-
};
|
|
4046
|
-
}
|
|
4047
|
-
existingIdentifiers.add(normalized);
|
|
4048
|
-
return {
|
|
4049
|
-
identifier: normalized,
|
|
4050
|
-
canonicalized: normalized === initial ? 0 : 1
|
|
4051
|
-
};
|
|
4052
|
-
};
|
|
4053
|
-
const planPullAssetReference = (options) => {
|
|
4054
|
-
const resolvedAsset = resolveReferencedAsset(options.reference, options.indexes);
|
|
4055
|
-
let canonicalized = 0;
|
|
4056
|
-
let identifier = resolvedAsset?.identifier ?? options.reference.value.identifier ?? null;
|
|
4057
|
-
const normalized = normalizePullIdentifier({
|
|
4058
|
-
initial: identifier,
|
|
4059
|
-
location: options.reference.location,
|
|
4060
|
-
existingIdentifiers: options.existingIdentifiers
|
|
4061
|
-
});
|
|
4062
|
-
canonicalized += normalized.canonicalized;
|
|
4063
|
-
identifier = normalized.identifier;
|
|
4064
|
-
if (normalized.warning && normalized.identifier) {
|
|
4065
|
-
return {
|
|
4066
|
-
plan: {
|
|
4067
|
-
kind: "identified",
|
|
4068
|
-
reference: options.reference,
|
|
4069
|
-
resolvedAsset,
|
|
4070
|
-
identifier: normalized.identifier
|
|
4071
|
-
},
|
|
4072
|
-
canonicalized,
|
|
4073
|
-
warning: normalized.warning
|
|
4074
|
-
};
|
|
4075
|
-
}
|
|
4076
|
-
if (resolvedAsset && identifier) {
|
|
4077
|
-
options.indexes.assetsByIdentifier.set(identifier, resolvedAsset);
|
|
4078
|
-
}
|
|
4079
|
-
return {
|
|
4080
|
-
plan: identifier ? {
|
|
4081
|
-
kind: "identified",
|
|
4082
|
-
reference: options.reference,
|
|
4083
|
-
resolvedAsset,
|
|
4084
|
-
identifier
|
|
4085
|
-
} : {
|
|
4086
|
-
kind: "unidentified",
|
|
4087
|
-
reference: options.reference,
|
|
4088
|
-
resolvedAsset,
|
|
4089
|
-
identifier: null
|
|
4090
|
-
},
|
|
4091
|
-
canonicalized
|
|
4092
|
-
};
|
|
4093
|
-
};
|
|
4094
|
-
const planPullAssetIdentifiers = (options) => {
|
|
4095
|
-
const indexes = buildPullAssetIndexes(options.resolved);
|
|
4096
|
-
const assetsById = new Map(indexes.assetsById);
|
|
4097
|
-
const assetsByIdentifier = new Map(indexes.assetsByIdentifier);
|
|
4098
|
-
const existingIdentifiers = new Set(options.identifiers);
|
|
4099
|
-
for (const asset of options.resolved.assets) {
|
|
4100
|
-
existingIdentifiers.add(asset.identifier);
|
|
4101
|
-
}
|
|
4102
|
-
let referencesCanonicalized = 0;
|
|
4103
|
-
const warnings = [];
|
|
4104
|
-
const references = [];
|
|
4105
|
-
for (const reference of options.references) {
|
|
4106
|
-
const planned = planPullAssetReference({
|
|
4107
|
-
reference,
|
|
4108
|
-
indexes: {
|
|
4109
|
-
assetsById,
|
|
4110
|
-
assetsByIdentifier
|
|
4111
|
-
},
|
|
4112
|
-
existingIdentifiers
|
|
4113
|
-
});
|
|
4114
|
-
referencesCanonicalized += planned.canonicalized;
|
|
4115
|
-
if (planned.warning) warnings.push(planned.warning);
|
|
4116
|
-
references.push(planned.plan);
|
|
4117
|
-
}
|
|
4118
|
-
return {
|
|
4119
|
-
indexes: {
|
|
4120
|
-
assetsById,
|
|
4121
|
-
assetsByIdentifier
|
|
4122
|
-
},
|
|
4123
|
-
references,
|
|
4124
|
-
summary: { referencesCanonicalized },
|
|
4125
|
-
warnings
|
|
4126
|
-
};
|
|
4127
|
-
};
|
|
4128
|
-
const applyPullAssetPlan = (options) => {
|
|
4129
|
-
const assetsById = new Map(options.plan.indexes.assetsById);
|
|
4130
|
-
const assetsByIdentifier = new Map(options.plan.indexes.assetsByIdentifier);
|
|
4131
|
-
const references = [];
|
|
4132
|
-
for (const planned of options.plan.references) {
|
|
4133
|
-
if (planned.kind === "unidentified") {
|
|
4134
|
-
references.push(planned);
|
|
4135
|
-
continue;
|
|
4136
|
-
}
|
|
4137
|
-
planned.reference.value.identifier = planned.identifier;
|
|
4138
|
-
if (planned.resolvedAsset) {
|
|
4139
|
-
updateManifestEntry(options.manifest, planned.identifier, planned.resolvedAsset, null);
|
|
4140
|
-
}
|
|
4141
|
-
references.push(planned);
|
|
4142
|
-
}
|
|
4143
|
-
return {
|
|
4144
|
-
indexes: {
|
|
4145
|
-
assetsById,
|
|
4146
|
-
assetsByIdentifier
|
|
4147
|
-
},
|
|
4148
|
-
references
|
|
4149
|
-
};
|
|
4150
|
-
};
|
|
4151
|
-
|
|
4152
|
-
//#endregion
|
|
4153
|
-
//#region src/media/pull-references.ts
|
|
4154
|
-
/**
|
|
4155
|
-
* Discover ordinary media values and normalize rich-text nodes that already
|
|
4156
|
-
* have an identifier. Rich-text nodes with only a source URL are returned as
|
|
4157
|
-
* explicit import intents; importing is an effectful stage owned by the
|
|
4158
|
-
* workflow, not by content traversal.
|
|
4159
|
-
*/
|
|
4160
|
-
const collectPullMediaReferences = (content) => {
|
|
4161
|
-
const references = [];
|
|
4162
|
-
const identifiers = new Set();
|
|
4163
|
-
const assetIds = new Set();
|
|
4164
|
-
const warnings = [];
|
|
4165
|
-
walkPulledContentMedia(content, (value, location) => {
|
|
4166
|
-
references.push({
|
|
4167
|
-
value,
|
|
4168
|
-
location
|
|
4169
|
-
});
|
|
4170
|
-
if (typeof value.identifier === "string") {
|
|
4171
|
-
identifiers.add(value.identifier);
|
|
4172
|
-
}
|
|
4173
|
-
if (typeof value.assetId === "string") {
|
|
4174
|
-
assetIds.add(value.assetId);
|
|
4175
|
-
}
|
|
4176
|
-
});
|
|
4177
|
-
const richTextImports = [];
|
|
4178
|
-
let referencesCanonicalized = 0;
|
|
4179
|
-
const richTextNodes = collectRichTextImageAttrs(content);
|
|
4180
|
-
for (const node of richTextNodes) {
|
|
4181
|
-
const attrs = node.attrs;
|
|
4182
|
-
const rawIdentifier = typeof attrs.identifier === "string" ? attrs.identifier.trim() : "";
|
|
4183
|
-
const rawSrc = typeof attrs.src === "string" ? attrs.src.trim() : "";
|
|
4184
|
-
if (rawIdentifier) {
|
|
4185
|
-
const normalized = normalizeMediaIdentifierInput(rawIdentifier);
|
|
4186
|
-
if (!normalized) {
|
|
4187
|
-
warnings.push(`Invalid rich text image identifier "${rawIdentifier}" at ${node.location}.`);
|
|
4188
|
-
continue;
|
|
4189
|
-
}
|
|
4190
|
-
if (normalized !== rawIdentifier) {
|
|
4191
|
-
attrs.identifier = normalized;
|
|
4192
|
-
referencesCanonicalized += 1;
|
|
4193
|
-
}
|
|
4194
|
-
delete attrs.src;
|
|
4195
|
-
identifiers.add(normalized);
|
|
4196
|
-
references.push({
|
|
4197
|
-
value: {
|
|
4198
|
-
type: "image",
|
|
4199
|
-
identifier: normalized
|
|
4200
|
-
},
|
|
4201
|
-
location: node.location
|
|
4202
|
-
});
|
|
4203
|
-
continue;
|
|
4204
|
-
}
|
|
4205
|
-
if (rawSrc) {
|
|
4206
|
-
richTextImports.push({
|
|
4207
|
-
attrs,
|
|
4208
|
-
location: node.location,
|
|
4209
|
-
src: rawSrc
|
|
4210
|
-
});
|
|
4211
|
-
}
|
|
4212
|
-
}
|
|
4213
|
-
return {
|
|
4214
|
-
references,
|
|
4215
|
-
identifiers,
|
|
4216
|
-
assetIds,
|
|
4217
|
-
richTextImports,
|
|
4218
|
-
referencesCanonicalized,
|
|
4219
|
-
cmsImportsPlanned: 0,
|
|
4220
|
-
cmsAssetsImported: 0,
|
|
4221
|
-
warnings
|
|
4222
|
-
};
|
|
4223
|
-
};
|
|
4224
|
-
const recordRichTextImport = (collection, intent, identifier) => {
|
|
4225
|
-
intent.attrs.identifier = identifier;
|
|
4226
|
-
delete intent.attrs.src;
|
|
4227
|
-
collection.identifiers.add(identifier);
|
|
4228
|
-
const reference = {
|
|
4229
|
-
value: {
|
|
4230
|
-
type: "image",
|
|
4231
|
-
identifier
|
|
4232
|
-
},
|
|
4233
|
-
location: intent.location
|
|
4234
|
-
};
|
|
4235
|
-
collection.references.push(reference);
|
|
4236
|
-
return {
|
|
4237
|
-
identifier,
|
|
4238
|
-
reference
|
|
4239
|
-
};
|
|
4240
|
-
};
|
|
4241
|
-
|
|
4242
|
-
//#endregion
|
|
4243
|
-
//#region src/media/pull-rich-text.ts
|
|
4244
|
-
const chooseChecksumIdentifier = (candidates) => {
|
|
4245
|
-
const chosen = candidates.slice().sort((a, b) => a.createdAt.localeCompare(b.createdAt))[0];
|
|
4246
|
-
if (!chosen) return null;
|
|
4247
|
-
return chosen.identifier;
|
|
4248
|
-
};
|
|
4249
|
-
const assignOrUploadRichTextIdentifier = async (options) => {
|
|
4250
|
-
for (let attempt = 0; attempt < 10; attempt += 1) {
|
|
4251
|
-
const candidate = resolveMediaIdentifier(options.base, options.existingIdentifiers);
|
|
4252
|
-
options.existingIdentifiers.add(candidate);
|
|
4253
|
-
if (options.dryRun) return {
|
|
4254
|
-
kind: "planned-upload",
|
|
4255
|
-
identifier: candidate
|
|
4256
|
-
};
|
|
4257
|
-
try {
|
|
4258
|
-
const uploaded = await options.client.media.upload({
|
|
4259
|
-
data: options.data,
|
|
4260
|
-
filename: options.filename,
|
|
4261
|
-
contentType: options.mimeType,
|
|
4262
|
-
identifier: candidate
|
|
4263
|
-
});
|
|
4264
|
-
return {
|
|
4265
|
-
kind: "uploaded",
|
|
4266
|
-
identifier: uploaded.identifier
|
|
4267
|
-
};
|
|
4268
|
-
} catch {
|
|
4269
|
-
continue;
|
|
4270
|
-
}
|
|
4271
|
-
}
|
|
4272
|
-
throw new Error("Unable to assign a unique identifier for rich text image import");
|
|
4273
|
-
};
|
|
4274
|
-
const importRichTextImageSrcToIdentifier = async (options) => {
|
|
4275
|
-
const { client, src, location, existingIdentifiers, dryRun } = options;
|
|
4276
|
-
const downloaded = await downloadMedia(src);
|
|
4277
|
-
if (!downloaded) {
|
|
4278
|
-
throw new Error(`Failed to download rich text image from ${src}`);
|
|
4279
|
-
}
|
|
4280
|
-
const mimeType = downloaded.contentType.split(";")[0]?.trim() || "application/octet-stream";
|
|
4281
|
-
if (!mimeType.startsWith("image/")) {
|
|
4282
|
-
throw new Error(`Unsupported content-type for rich text image import: ${mimeType}`);
|
|
4283
|
-
}
|
|
4284
|
-
const checksum = sha256(downloaded.data);
|
|
4285
|
-
const matches = await client.media.findByChecksum([checksum]);
|
|
4286
|
-
const candidates = matches.filter((asset) => asset.checksumSha256 === checksum);
|
|
4287
|
-
const existingIdentifier = chooseChecksumIdentifier(candidates);
|
|
4288
|
-
if (existingIdentifier) return {
|
|
4289
|
-
kind: "existing",
|
|
4290
|
-
identifier: existingIdentifier
|
|
4291
|
-
};
|
|
4292
|
-
return assignOrUploadRichTextIdentifier({
|
|
4293
|
-
client,
|
|
4294
|
-
base: chooseRichTextIdentifierBase(location, src),
|
|
4295
|
-
existingIdentifiers,
|
|
4296
|
-
dryRun,
|
|
4297
|
-
data: downloaded.data,
|
|
4298
|
-
filename: ensureFilenameExtension(parseFilenameFromUrl(src) ?? "image", mimeType),
|
|
4299
|
-
mimeType
|
|
4300
|
-
});
|
|
4301
|
-
};
|
|
4302
|
-
async function importRichTextPullReferences(options) {
|
|
4303
|
-
const { collection, client, dryRun, allowCmsWrites } = options;
|
|
4304
|
-
const richTextExisting = new Set(collection.identifiers);
|
|
4305
|
-
for (const intent of collection.richTextImports) {
|
|
4306
|
-
try {
|
|
4307
|
-
const identifier = await importRichTextImageSrcToIdentifier({
|
|
4308
|
-
client,
|
|
4309
|
-
src: intent.src,
|
|
4310
|
-
location: intent.location,
|
|
4311
|
-
existingIdentifiers: richTextExisting,
|
|
4312
|
-
dryRun: dryRun || !allowCmsWrites
|
|
4313
|
-
});
|
|
4314
|
-
if (identifier.kind === "planned-upload") {
|
|
4315
|
-
collection.cmsImportsPlanned += 1;
|
|
4316
|
-
if (!dryRun && !allowCmsWrites) continue;
|
|
4317
|
-
}
|
|
4318
|
-
if (identifier.kind === "uploaded") {
|
|
4319
|
-
collection.cmsAssetsImported += 1;
|
|
4320
|
-
}
|
|
4321
|
-
recordRichTextImport(collection, intent, identifier.identifier);
|
|
4322
|
-
richTextExisting.add(identifier.identifier);
|
|
4323
|
-
} catch (error) {
|
|
4324
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
4325
|
-
collection.warnings.push(`Failed to import rich text image @ ${intent.location}: ${message}`);
|
|
4326
|
-
}
|
|
4327
|
-
}
|
|
4328
|
-
}
|
|
4329
|
-
|
|
4330
|
-
//#endregion
|
|
4331
|
-
//#region src/media/pull-download.ts
|
|
4332
|
-
const planLocalMediaDownload = (options) => {
|
|
4333
|
-
const { reference, asset, identifier, existingEntry, local } = options;
|
|
4334
|
-
if (!asset) return {
|
|
4335
|
-
kind: "missing-asset",
|
|
4336
|
-
reference
|
|
4337
|
-
};
|
|
4338
|
-
const filename = buildLocalFilename(identifier, asset);
|
|
4339
|
-
if (local.exists) {
|
|
4340
|
-
if (asset.checksumSha256) {
|
|
4341
|
-
if (existingEntry?.checksum === asset.checksumSha256) {
|
|
4342
|
-
return {
|
|
4343
|
-
kind: "skip",
|
|
4344
|
-
reference,
|
|
4345
|
-
asset,
|
|
4346
|
-
identifier,
|
|
4347
|
-
filename
|
|
4348
|
-
};
|
|
4349
|
-
}
|
|
4350
|
-
if (local.checksum === asset.checksumSha256) {
|
|
4351
|
-
return {
|
|
4352
|
-
kind: "skip",
|
|
4353
|
-
reference,
|
|
4354
|
-
asset,
|
|
4355
|
-
identifier,
|
|
4356
|
-
filename
|
|
4357
|
-
};
|
|
4358
|
-
}
|
|
4359
|
-
} else if (existingEntry?.checksum) {
|
|
4360
|
-
return {
|
|
4361
|
-
kind: "skip",
|
|
4362
|
-
reference,
|
|
4363
|
-
asset,
|
|
4364
|
-
identifier,
|
|
4365
|
-
filename
|
|
4366
|
-
};
|
|
4367
|
-
}
|
|
4368
|
-
}
|
|
4369
|
-
return {
|
|
4370
|
-
kind: "download",
|
|
4371
|
-
reference,
|
|
4372
|
-
asset,
|
|
4373
|
-
identifier,
|
|
4374
|
-
filename
|
|
4375
|
-
};
|
|
4376
|
-
};
|
|
4377
|
-
const readExistingLocalMedia = async (options) => {
|
|
4378
|
-
const exists = await localMediaFileExists(options.localFilePath);
|
|
4379
|
-
if (!exists) return { exists: false };
|
|
4380
|
-
if (!options.asset.checksumSha256) {
|
|
4381
|
-
return { exists: true };
|
|
4382
|
-
}
|
|
4383
|
-
const manifestChecksum = options.existingEntry?.checksum;
|
|
4384
|
-
if (manifestChecksum === options.asset.checksumSha256) {
|
|
4385
|
-
return {
|
|
4386
|
-
exists: true,
|
|
4387
|
-
checksum: manifestChecksum
|
|
4388
|
-
};
|
|
4389
|
-
}
|
|
4390
|
-
const localChecksum = manifestChecksum ?? await sha256FromFile(options.localFilePath);
|
|
4391
|
-
return {
|
|
4392
|
-
exists: true,
|
|
4393
|
-
checksum: localChecksum
|
|
4394
|
-
};
|
|
4395
|
-
};
|
|
4396
|
-
const removeStaleLocalFile = async (options) => {
|
|
4397
|
-
if (options.dryRun || !options.previousFilename || options.previousFilename === options.filename) {
|
|
4398
|
-
return;
|
|
4399
|
-
}
|
|
4400
|
-
const previousPath = path$1.join(options.contentDir, "media", options.previousFilename);
|
|
4401
|
-
if (await localMediaFileExists(previousPath)) {
|
|
4402
|
-
await fs$1.unlink(previousPath).catch(() => {});
|
|
4403
|
-
}
|
|
4404
|
-
};
|
|
4405
|
-
const applySkipDecision = (decision, context, local, existingEntry) => {
|
|
4406
|
-
if (decision.asset.checksumSha256 && local.checksum === decision.asset.checksumSha256 && existingEntry?.checksum !== decision.asset.checksumSha256) {
|
|
4407
|
-
context.manifest.assets[decision.identifier] = {
|
|
4408
|
-
...existingEntry,
|
|
4409
|
-
checksum: local.checksum,
|
|
4410
|
-
filename: decision.filename,
|
|
4411
|
-
mimeType: decision.asset.mimeType ?? existingEntry?.mimeType
|
|
4412
|
-
};
|
|
4413
|
-
}
|
|
4414
|
-
context.summary.skipped += 1;
|
|
4415
|
-
updateManifestEntry(context.manifest, decision.identifier, decision.asset, null);
|
|
4416
|
-
};
|
|
4417
|
-
const applyDownloadDecision = async (decision, context) => {
|
|
4418
|
-
context.summary.pendingDownloads += 1;
|
|
4419
|
-
if (context.dryRun) return;
|
|
4420
|
-
const relativePath = normalizeStoragePath(decision.asset.storagePath, context.siteId);
|
|
4421
|
-
const urlResult = buildStorageUrl(context.supabaseUrl, relativePath, context.siteId, decision.asset.storageBucket ?? "media");
|
|
4422
|
-
if (!urlResult.ok) {
|
|
4423
|
-
context.warnings.push(`Invalid storage path for "${decision.identifier}": ${urlResult.error.cause.tag}.`);
|
|
4424
|
-
return;
|
|
4425
|
-
}
|
|
4426
|
-
const downloaded = await downloadMedia(urlResult.value);
|
|
4427
|
-
if (!downloaded) {
|
|
4428
|
-
context.warnings.push(`Failed to download media for "${decision.identifier}" from ${urlResult.value}.`);
|
|
4429
|
-
return;
|
|
4430
|
-
}
|
|
4431
|
-
const written = await writeLocalMediaFile(context.contentDir, decision.filename, downloaded.data);
|
|
4432
|
-
context.warnings.push(...localMediaWriteWarningMessages(decision.identifier, written));
|
|
4433
|
-
context.manifest.assets[decision.identifier] = {
|
|
4434
|
-
checksum: sha256(downloaded.data),
|
|
4435
|
-
filename: decision.filename,
|
|
4436
|
-
mimeType: decision.asset.mimeType
|
|
4437
|
-
};
|
|
4438
|
-
context.summary.downloaded += 1;
|
|
4439
|
-
};
|
|
4440
|
-
const syncPullMediaDownload = async (planned, indexes, context) => {
|
|
4441
|
-
const { reference, identifier } = planned;
|
|
4442
|
-
const byId = reference.value.assetId ? indexes.assetsById.get(reference.value.assetId) : undefined;
|
|
4443
|
-
const asset = byId ?? indexes.assetsByIdentifier.get(identifier);
|
|
4444
|
-
if (!asset) {
|
|
4445
|
-
context.warnings.push(`Unable to resolve asset for "${identifier}" at ${reference.location}.`);
|
|
4446
|
-
return;
|
|
4447
|
-
}
|
|
4448
|
-
const filename = buildLocalFilename(identifier, asset);
|
|
4449
|
-
const localFilePath = path$1.join(context.contentDir, "media", filename);
|
|
4450
|
-
const existingEntry = context.manifest.assets[identifier];
|
|
4451
|
-
await removeStaleLocalFile({
|
|
4452
|
-
contentDir: context.contentDir,
|
|
4453
|
-
previousFilename: existingEntry?.filename,
|
|
4454
|
-
filename,
|
|
4455
|
-
dryRun: context.dryRun
|
|
4456
|
-
});
|
|
4457
|
-
const local = await readExistingLocalMedia({
|
|
4458
|
-
localFilePath,
|
|
4459
|
-
existingEntry,
|
|
4460
|
-
asset
|
|
4461
|
-
});
|
|
4462
|
-
const decision = planLocalMediaDownload({
|
|
4463
|
-
reference,
|
|
4464
|
-
asset,
|
|
4465
|
-
identifier,
|
|
4466
|
-
existingEntry,
|
|
4467
|
-
local
|
|
4468
|
-
});
|
|
4469
|
-
switch (decision.kind) {
|
|
4470
|
-
case "missing-asset":
|
|
4471
|
-
context.warnings.push(`Unable to resolve asset for "${identifier}" at ${reference.location}.`);
|
|
4472
|
-
return;
|
|
4473
|
-
case "skip":
|
|
4474
|
-
applySkipDecision(decision, context, local, existingEntry);
|
|
4475
|
-
return;
|
|
4476
|
-
case "download": await applyDownloadDecision(decision, context);
|
|
4477
|
-
}
|
|
4478
|
-
};
|
|
4479
|
-
const syncPullMediaDownloads = async (options) => {
|
|
4480
|
-
for (const planned of options.references) {
|
|
4481
|
-
if (planned.kind === "identified") {
|
|
4482
|
-
await syncPullMediaDownload(planned, options.indexes, options);
|
|
4483
|
-
}
|
|
4484
|
-
}
|
|
4485
|
-
if (!options.dryRun) options.summary.pendingDownloads = 0;
|
|
4486
|
-
};
|
|
4487
|
-
const sha256FromFile = (filePath) => readLocalMediaChecksum(filePath, FILE_SIZE_LIMITS.rawVideoBeforeCompression);
|
|
4488
|
-
function localMediaWriteWarningMessages(identifier, outcome) {
|
|
4489
|
-
if (outcome.kind === "written") return [];
|
|
4490
|
-
return outcome.warnings.map((warning) => {
|
|
4491
|
-
switch (warning.tag) {
|
|
4492
|
-
case "temporary_cleanup_failed": return `Downloaded media for "${identifier}", but could not remove temporary file ${warning.temporaryPath}.`;
|
|
4493
|
-
case "directory_sync_failed": return `Downloaded media for "${identifier}", but could not confirm the destination directory was synced.`;
|
|
4494
|
-
}
|
|
4495
|
-
});
|
|
4496
|
-
}
|
|
4497
|
-
|
|
4498
|
-
//#endregion
|
|
4499
|
-
//#region src/media/pull-workflow.ts
|
|
4500
|
-
async function syncLocalMediaForPull(options) {
|
|
4501
|
-
const { contentDir, content, client, dryRun, allowCmsWrites = true, supabaseUrl, siteId, downloadFiles } = options;
|
|
4502
|
-
const manifest = await readMediaManifest(contentDir);
|
|
4503
|
-
const collection = collectPullMediaReferences(content);
|
|
4504
|
-
await importRichTextPullReferences({
|
|
4505
|
-
collection,
|
|
4506
|
-
client,
|
|
4507
|
-
dryRun,
|
|
4508
|
-
allowCmsWrites
|
|
4509
|
-
});
|
|
4510
|
-
const summary = {
|
|
4511
|
-
totalReferences: collection.references.length,
|
|
4512
|
-
referencesCanonicalized: collection.referencesCanonicalized,
|
|
4513
|
-
cmsImportsPlanned: collection.cmsImportsPlanned,
|
|
4514
|
-
cmsAssetsImported: collection.cmsAssetsImported,
|
|
4515
|
-
downloaded: 0,
|
|
4516
|
-
skipped: 0,
|
|
4517
|
-
pendingDownloads: 0
|
|
4518
|
-
};
|
|
4519
|
-
const warnings = [...collection.warnings];
|
|
4520
|
-
if (collection.references.length === 0) {
|
|
4521
|
-
return {
|
|
4522
|
-
content,
|
|
4523
|
-
manifest,
|
|
4524
|
-
summary,
|
|
4525
|
-
warnings
|
|
4526
|
-
};
|
|
4527
|
-
}
|
|
4528
|
-
const resolved = await client.media.resolve({
|
|
4529
|
-
...collection.identifiers.size > 0 ? { identifiers: Array.from(collection.identifiers) } : {},
|
|
4530
|
-
...collection.assetIds.size > 0 ? { assetIds: Array.from(collection.assetIds) } : {}
|
|
4531
|
-
});
|
|
4532
|
-
const assetPlan = planPullAssetIdentifiers({
|
|
4533
|
-
references: collection.references,
|
|
4534
|
-
resolved,
|
|
4535
|
-
identifiers: collection.identifiers
|
|
4536
|
-
});
|
|
4537
|
-
warnings.push(...assetPlan.warnings);
|
|
4538
|
-
summary.referencesCanonicalized += assetPlan.summary.referencesCanonicalized;
|
|
4539
|
-
const applied = applyPullAssetPlan({
|
|
4540
|
-
plan: assetPlan,
|
|
4541
|
-
manifest
|
|
4542
|
-
});
|
|
4543
|
-
if (!downloadFiles) {
|
|
4544
|
-
return {
|
|
4545
|
-
content,
|
|
4546
|
-
manifest,
|
|
4547
|
-
summary,
|
|
4548
|
-
warnings
|
|
4549
|
-
};
|
|
4550
|
-
}
|
|
4551
|
-
if (!supabaseUrl || !siteId) {
|
|
4552
|
-
throw new Error("syncLocalMediaForPull requires supabaseUrl and siteId when downloadFiles=true.");
|
|
4553
|
-
}
|
|
4554
|
-
await syncPullMediaDownloads({
|
|
4555
|
-
contentDir,
|
|
4556
|
-
manifest,
|
|
4557
|
-
references: applied.references,
|
|
4558
|
-
indexes: applied.indexes,
|
|
4559
|
-
supabaseUrl,
|
|
4560
|
-
siteId,
|
|
4561
|
-
dryRun,
|
|
4562
|
-
summary,
|
|
4563
|
-
warnings
|
|
4564
|
-
});
|
|
4565
|
-
return {
|
|
4566
|
-
content,
|
|
4567
|
-
manifest,
|
|
4568
|
-
summary,
|
|
4569
|
-
warnings
|
|
4570
|
-
};
|
|
4571
|
-
}
|
|
4572
|
-
|
|
4573
|
-
//#endregion
|
|
4574
|
-
//#region src/media/local-sync.ts
|
|
4575
|
-
const rawErrorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
4576
|
-
async function revalidateLocalMediaPushPlan(plan, client) {
|
|
4577
|
-
const identifiers = plan.mediaReadSet.map((observation) => observation.identifier);
|
|
4578
|
-
if (identifiers.length === 0) return { ok: true };
|
|
4579
|
-
try {
|
|
4580
|
-
const resolved = await client.media.resolve({ identifiers });
|
|
4581
|
-
const assets = new Map(resolved.assets.map((asset) => [asset.identifier, asset]));
|
|
4582
|
-
const localChecksums = new Map(plan.mediaReadSet.map((observation) => [observation.identifier, observation.localChecksum.kind === "present" ? observation.localChecksum.checksum : undefined]));
|
|
4583
|
-
const observedChecksums = Array.from(new Set(plan.mediaReadSet.flatMap((observation) => observation.localChecksum.kind === "present" && observation.localChecksum.lookup.kind === "read" ? [observation.localChecksum.checksum] : [])));
|
|
4584
|
-
const currentChecksumAssets = observedChecksums.length > 0 ? await client.media.findByChecksum(observedChecksums) : [];
|
|
4585
|
-
const checksumAssetsByChecksum = new Map(observedChecksums.map((checksum) => [checksum, []]));
|
|
4586
|
-
for (const asset of currentChecksumAssets) {
|
|
4587
|
-
if (!asset.checksumSha256) continue;
|
|
4588
|
-
const matches = checksumAssetsByChecksum.get(asset.checksumSha256);
|
|
4589
|
-
if (!matches) continue;
|
|
4590
|
-
matches.push(asset);
|
|
4591
|
-
}
|
|
4592
|
-
const current = buildMediaReadSet({
|
|
4593
|
-
identifiers,
|
|
4594
|
-
localChecksums,
|
|
4595
|
-
initialAssets: assets,
|
|
4596
|
-
initialMissing: new Set(resolved.missingIdentifiers),
|
|
4597
|
-
checksumAssetsByChecksum
|
|
4598
|
-
});
|
|
4599
|
-
if (stableMediaReadSet(current) !== stableMediaReadSet(plan.mediaReadSet)) {
|
|
4600
|
-
return {
|
|
4601
|
-
ok: false,
|
|
4602
|
-
error: {
|
|
4603
|
-
kind: "remote-media-drift",
|
|
4604
|
-
message: "Remote media state changed after preflight; re-run the push to refresh its media plan."
|
|
4605
|
-
}
|
|
4606
|
-
};
|
|
4607
|
-
}
|
|
4608
|
-
return { ok: true };
|
|
4609
|
-
} catch (error) {
|
|
4610
|
-
return {
|
|
4611
|
-
ok: false,
|
|
4612
|
-
error: {
|
|
4613
|
-
kind: "remote-media-read-failed",
|
|
4614
|
-
message: `Failed to revalidate remote media state: ${rawErrorMessage(error)}`
|
|
4615
|
-
}
|
|
4616
|
-
};
|
|
4617
|
-
}
|
|
4618
|
-
}
|
|
4619
|
-
async function syncLocalMediaForPush(options) {
|
|
4620
|
-
const plan = await prepareLocalMediaForPush(options);
|
|
4621
|
-
const result = await applyLocalMediaPush({
|
|
4622
|
-
plan,
|
|
4623
|
-
client: options.client,
|
|
4624
|
-
dryRun: options.dryRun,
|
|
4625
|
-
onEvent: options.onEvent
|
|
4626
|
-
});
|
|
4627
|
-
Object.assign(options.content, result.content);
|
|
4628
|
-
return {
|
|
4629
|
-
...result,
|
|
4630
|
-
content: options.content
|
|
4631
|
-
};
|
|
4632
|
-
}
|
|
4633
|
-
|
|
4634
|
-
//#endregion
|
|
4635
|
-
export { sdkUpsertEventResponseSchema as A, isInternalResolvedLinkValue as B, createHttpClient as C, sdkGetEventResponseSchema as D, sdkConfigureBookingVerticalResponseSchema as E, entryLinkSchema$1 as F, sdkGetPageDesignResponseSchema as G, linkSchema as H, externalLinkSchema$1 as I, sdkEventOccurrenceResponseSchema as J, sdkSavePageDesignResponseSchema as K, internalLinkSchema as L, linkPayloadSchema as M, RESOLVED_INTERNAL_LINK_ENTITY_TYPES as N, sdkListEventsResponseSchema as O, customLinkSchema$1 as P, internalResolvedLinkSchema as R, buildRequestInfo as S, managementTransportError as T, pageLinkSchema$1 as U, isResolvedInternalLinkEntityType as V, portablePageDesignSchema as W, sdkReorderResponseSchema as X, sdkListEventOccurrencesResponseSchema as Y, sdkConfiguredBookingVerticalConfigSchema as Z, buildSettingsUpdateInput as _, prepareLocalMediaForPush as a, isMediaValue as b, writeBinaryFileAtomic as c, normalizeMediaValueToIdentifierOnly as d, walkLocalContentMedia as f, SETTINGS_FILE_FIELDS as g, extractMediaIdentifiers as h, applyLocalMediaPush as i, sdkSettingsUpdateSchema as j, sdkTakeEventOfflineResponseSchema as k, sha256 as l, downloadMedia as m, syncLocalMediaForPush as n, admitBinaryWriteDestination as o, buildStorageUrl as p, sdkDeleteEventOccurrenceResponseSchema as q, syncLocalMediaForPull as r, readStableBinaryFile as s, revalidateLocalMediaPushPlan as t, normalizeMediaIdentifierInput as u, hasOwnSettingsField as v, is404Error as w, ManagementApiError as x, writeMediaManifest as y, internalRouteOnlyLinkSchema as z };
|