agent-cms 0.1.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.
@@ -0,0 +1,1952 @@
1
+ import { Data, Effect, ParseResult, Schema } from "effect";
2
+ import { SqlClient, SqlError } from "@effect/sql";
3
+ import * as Either from "effect/Either";
4
+ import { unified } from "unified";
5
+ import remarkParse from "remark-parse";
6
+ import "remark-stringify";
7
+ import remarkGfm from "remark-gfm";
8
+ import { AsyncLocalStorage } from "node:async_hooks";
9
+ //#region src/errors.ts
10
+ /** Model/field/record not found */
11
+ var NotFoundError = class extends Data.TaggedError("NotFoundError") {};
12
+ /** Validation error (field values, DAST, API input) */
13
+ var ValidationError = class extends Data.TaggedError("ValidationError") {};
14
+ /** Reference conflict — trying to delete something that's referenced */
15
+ var ReferenceConflictError = class extends Data.TaggedError("ReferenceConflictError") {};
16
+ /** Duplicate — e.g., apiKey already exists */
17
+ var DuplicateError = class extends Data.TaggedError("DuplicateError") {};
18
+ /** Schema engine error — DDL failed, migration issue */
19
+ var SchemaEngineError = class extends Data.TaggedError("SchemaEngineError") {};
20
+ /** Unauthorized access to a protected API surface */
21
+ var UnauthorizedError = class extends Data.TaggedError("UnauthorizedError") {};
22
+ /** Runtime type guard for CmsError — uses instanceof, no duck-typing */
23
+ function isCmsError(error) {
24
+ return error instanceof NotFoundError || error instanceof ValidationError || error instanceof ReferenceConflictError || error instanceof DuplicateError || error instanceof SchemaEngineError || error instanceof UnauthorizedError;
25
+ }
26
+ /** Map a CMS error to an HTTP status code and JSON body */
27
+ function errorToResponse(error) {
28
+ switch (error._tag) {
29
+ case "NotFoundError": return {
30
+ status: 404,
31
+ body: { error: `${error.entity} not found: ${error.id}` }
32
+ };
33
+ case "ValidationError": return {
34
+ status: 400,
35
+ body: { error: error.message }
36
+ };
37
+ case "ReferenceConflictError": return {
38
+ status: 409,
39
+ body: { error: error.message }
40
+ };
41
+ case "DuplicateError": return {
42
+ status: 409,
43
+ body: { error: error.message }
44
+ };
45
+ case "SchemaEngineError": return {
46
+ status: 500,
47
+ body: { error: error.message }
48
+ };
49
+ case "UnauthorizedError": return {
50
+ status: 401,
51
+ body: { error: error.message }
52
+ };
53
+ }
54
+ }
55
+ //#endregion
56
+ //#region src/field-types.ts
57
+ /**
58
+ * Typed field type registry.
59
+ *
60
+ * Each field type defines its static properties in one place:
61
+ * SQL storage, GraphQL type, filter type, validation schema, etc.
62
+ *
63
+ * The "type sandwich":
64
+ * - Top: system tables (models, fields) are statically typed
65
+ * - Middle: which fields exist on which models is dynamic (runtime)
66
+ * - Bottom: each field type's shape is statically known (this file)
67
+ */
68
+ /** Effect Schemas for composite field type validation */
69
+ const ColorSchema = Schema.Struct({
70
+ red: Schema.Number.pipe(Schema.int(), Schema.between(0, 255)),
71
+ green: Schema.Number.pipe(Schema.int(), Schema.between(0, 255)),
72
+ blue: Schema.Number.pipe(Schema.int(), Schema.between(0, 255)),
73
+ alpha: Schema.optional(Schema.Number.pipe(Schema.int(), Schema.between(0, 255)))
74
+ });
75
+ const LatLonSchema = Schema.Struct({
76
+ latitude: Schema.Number.pipe(Schema.between(-90, 90)),
77
+ longitude: Schema.Number.pipe(Schema.between(-180, 180))
78
+ });
79
+ const SeoSchema = Schema.Struct({
80
+ title: Schema.optional(Schema.String),
81
+ description: Schema.optional(Schema.String),
82
+ image: Schema.optional(Schema.String),
83
+ twitterCard: Schema.optional(Schema.String)
84
+ });
85
+ const MediaFocalPointSchema = Schema.Struct({
86
+ x: Schema.Number.pipe(Schema.between(0, 1)),
87
+ y: Schema.Number.pipe(Schema.between(0, 1))
88
+ });
89
+ const MediaFieldObjectSchema = Schema.Struct({
90
+ upload_id: Schema.String,
91
+ alt: Schema.optional(Schema.NullOr(Schema.String)),
92
+ title: Schema.optional(Schema.NullOr(Schema.String)),
93
+ focal_point: Schema.optional(Schema.NullOr(MediaFocalPointSchema)),
94
+ custom_data: Schema.optional(Schema.NullOr(Schema.Record({
95
+ key: Schema.String,
96
+ value: Schema.Unknown
97
+ })))
98
+ });
99
+ const MediaFieldSchema = Schema.Union(Schema.String, MediaFieldObjectSchema);
100
+ const MediaGalleryFieldSchema = Schema.Array(MediaFieldSchema);
101
+ function isValidIsoDate(value) {
102
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
103
+ const parsed = /* @__PURE__ */ new Date(`${value}T00:00:00.000Z`);
104
+ return !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === value;
105
+ }
106
+ function isValidIsoDateTime(value) {
107
+ if (!/^\d{4}-\d{2}-\d{2}T/.test(value)) return false;
108
+ return !Number.isNaN(Date.parse(value));
109
+ }
110
+ const DateSchema = Schema.String.pipe(Schema.filter(isValidIsoDate, { message: () => "expected ISO date string YYYY-MM-DD" }));
111
+ const DateTimeSchema = Schema.String.pipe(Schema.filter(isValidIsoDateTime, { message: () => "expected ISO datetime string" }));
112
+ /**
113
+ * The field type registry — one entry per field type.
114
+ * Add a new field type here and TypeScript will enforce all properties are defined.
115
+ */
116
+ const FIELD_TYPE_REGISTRY = {
117
+ string: {
118
+ sqliteType: "TEXT",
119
+ graphqlType: "String",
120
+ filterType: "StringFilter",
121
+ localizable: true,
122
+ multiLocaleType: "StringMultiLocaleField",
123
+ inputSchema: null,
124
+ jsonStored: false,
125
+ hasCustomResolver: false
126
+ },
127
+ text: {
128
+ sqliteType: "TEXT",
129
+ graphqlType: "String",
130
+ filterType: "StringFilter",
131
+ localizable: true,
132
+ multiLocaleType: "StringMultiLocaleField",
133
+ inputSchema: null,
134
+ jsonStored: false,
135
+ hasCustomResolver: false
136
+ },
137
+ boolean: {
138
+ sqliteType: "INTEGER",
139
+ graphqlType: "Boolean",
140
+ filterType: "BooleanFilter",
141
+ localizable: true,
142
+ multiLocaleType: "BooleanMultiLocaleField",
143
+ inputSchema: Schema.Boolean,
144
+ jsonStored: false,
145
+ hasCustomResolver: false
146
+ },
147
+ integer: {
148
+ sqliteType: "INTEGER",
149
+ graphqlType: "Int",
150
+ filterType: "IntFilter",
151
+ localizable: true,
152
+ multiLocaleType: "IntMultiLocaleField",
153
+ inputSchema: Schema.Number.pipe(Schema.int()),
154
+ jsonStored: false,
155
+ hasCustomResolver: false
156
+ },
157
+ float: {
158
+ sqliteType: "REAL",
159
+ graphqlType: "Float",
160
+ filterType: "FloatFilter",
161
+ localizable: true,
162
+ multiLocaleType: "FloatMultiLocaleField",
163
+ inputSchema: Schema.Number,
164
+ jsonStored: false,
165
+ hasCustomResolver: false
166
+ },
167
+ slug: {
168
+ sqliteType: "TEXT",
169
+ graphqlType: "String",
170
+ filterType: "StringFilter",
171
+ localizable: true,
172
+ multiLocaleType: "StringMultiLocaleField",
173
+ inputSchema: null,
174
+ jsonStored: false,
175
+ hasCustomResolver: false
176
+ },
177
+ date: {
178
+ sqliteType: "TEXT",
179
+ graphqlType: "String",
180
+ filterType: "StringFilter",
181
+ localizable: true,
182
+ multiLocaleType: "StringMultiLocaleField",
183
+ inputSchema: DateSchema,
184
+ jsonStored: false,
185
+ hasCustomResolver: false
186
+ },
187
+ date_time: {
188
+ sqliteType: "TEXT",
189
+ graphqlType: "String",
190
+ filterType: "StringFilter",
191
+ localizable: true,
192
+ multiLocaleType: "StringMultiLocaleField",
193
+ inputSchema: DateTimeSchema,
194
+ jsonStored: false,
195
+ hasCustomResolver: false
196
+ },
197
+ media: {
198
+ sqliteType: "TEXT",
199
+ graphqlType: "Asset",
200
+ filterType: "LinkFilter",
201
+ localizable: false,
202
+ multiLocaleType: "StringMultiLocaleField",
203
+ inputSchema: MediaFieldSchema,
204
+ jsonStored: false,
205
+ hasCustomResolver: true
206
+ },
207
+ media_gallery: {
208
+ sqliteType: "TEXT",
209
+ graphqlType: "[Asset!]",
210
+ filterType: "LinksFilter",
211
+ localizable: false,
212
+ multiLocaleType: "JsonMultiLocaleField",
213
+ inputSchema: MediaGalleryFieldSchema,
214
+ jsonStored: true,
215
+ hasCustomResolver: true
216
+ },
217
+ link: {
218
+ sqliteType: "TEXT",
219
+ graphqlType: null,
220
+ filterType: "LinkFilter",
221
+ localizable: false,
222
+ multiLocaleType: "StringMultiLocaleField",
223
+ inputSchema: null,
224
+ jsonStored: false,
225
+ hasCustomResolver: true
226
+ },
227
+ links: {
228
+ sqliteType: "TEXT",
229
+ graphqlType: null,
230
+ filterType: "LinksFilter",
231
+ localizable: false,
232
+ multiLocaleType: "JsonMultiLocaleField",
233
+ inputSchema: null,
234
+ jsonStored: true,
235
+ hasCustomResolver: true
236
+ },
237
+ structured_text: {
238
+ sqliteType: "TEXT",
239
+ graphqlType: "StructuredText",
240
+ filterType: "TextFilter",
241
+ localizable: false,
242
+ multiLocaleType: "JsonMultiLocaleField",
243
+ inputSchema: null,
244
+ jsonStored: true,
245
+ hasCustomResolver: true
246
+ },
247
+ seo: {
248
+ sqliteType: "TEXT",
249
+ graphqlType: "SeoField",
250
+ filterType: "ExistsFilter",
251
+ localizable: false,
252
+ multiLocaleType: "SeoMultiLocaleField",
253
+ inputSchema: SeoSchema,
254
+ jsonStored: true,
255
+ hasCustomResolver: true
256
+ },
257
+ json: {
258
+ sqliteType: "TEXT",
259
+ graphqlType: "JSON",
260
+ filterType: "ExistsFilter",
261
+ localizable: false,
262
+ multiLocaleType: "JsonMultiLocaleField",
263
+ inputSchema: null,
264
+ jsonStored: true,
265
+ hasCustomResolver: false
266
+ },
267
+ color: {
268
+ sqliteType: "TEXT",
269
+ graphqlType: "ColorField",
270
+ filterType: "ExistsFilter",
271
+ localizable: false,
272
+ multiLocaleType: "JsonMultiLocaleField",
273
+ inputSchema: ColorSchema,
274
+ jsonStored: true,
275
+ hasCustomResolver: true
276
+ },
277
+ lat_lon: {
278
+ sqliteType: "TEXT",
279
+ graphqlType: "LatLonField",
280
+ filterType: "LatLonFilter",
281
+ localizable: false,
282
+ multiLocaleType: "JsonMultiLocaleField",
283
+ inputSchema: LatLonSchema,
284
+ jsonStored: true,
285
+ hasCustomResolver: true
286
+ },
287
+ video: {
288
+ sqliteType: "TEXT",
289
+ graphqlType: "VideoField",
290
+ filterType: "ExistsFilter",
291
+ localizable: false,
292
+ multiLocaleType: "JsonMultiLocaleField",
293
+ inputSchema: null,
294
+ jsonStored: false,
295
+ hasCustomResolver: true
296
+ }
297
+ };
298
+ /** Get the registry definition for a field type */
299
+ function getFieldTypeDef(fieldType) {
300
+ return FIELD_TYPE_REGISTRY[fieldType];
301
+ }
302
+ //#endregion
303
+ //#region src/json.ts
304
+ const UnknownJson = Schema.parseJson();
305
+ const JsonRecord = Schema.Record({
306
+ key: Schema.String,
307
+ value: Schema.Unknown
308
+ });
309
+ const JsonRecordString = Schema.parseJson(JsonRecord);
310
+ function encodeJson(value) {
311
+ return Schema.encodeSync(UnknownJson)(value);
312
+ }
313
+ function decodeJsonString(input) {
314
+ return Schema.decodeUnknownSync(UnknownJson)(input);
315
+ }
316
+ function tryDecodeJsonString(input) {
317
+ const result = Schema.decodeUnknownEither(UnknownJson)(input);
318
+ return Either.isRight(result) ? {
319
+ ok: true,
320
+ value: result.right
321
+ } : { ok: false };
322
+ }
323
+ function decodeJsonStringOr(input, fallback) {
324
+ const parsed = tryDecodeJsonString(input);
325
+ return parsed.ok ? parsed.value : fallback;
326
+ }
327
+ function decodeJsonRecordStringOr(input, fallback) {
328
+ const result = Schema.decodeUnknownEither(JsonRecordString)(input);
329
+ return Either.isRight(result) ? result.right : fallback;
330
+ }
331
+ function decodeJsonIfString(value) {
332
+ if (typeof value !== "string") return value;
333
+ return decodeJsonStringOr(value, value);
334
+ }
335
+ //#endregion
336
+ //#region src/media-field.ts
337
+ function parseMediaFieldReference(value) {
338
+ const parsed = decodeJsonIfString(value);
339
+ if (typeof parsed === "string") return parsed.length > 0 ? { uploadId: parsed } : null;
340
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
341
+ const objectValue = parsed;
342
+ const uploadId = typeof objectValue.upload_id === "string" ? objectValue.upload_id : null;
343
+ if (!uploadId) return null;
344
+ return {
345
+ uploadId,
346
+ alt: typeof objectValue.alt === "string" || objectValue.alt === null ? objectValue.alt : void 0,
347
+ title: typeof objectValue.title === "string" || objectValue.title === null ? objectValue.title : void 0,
348
+ focalPoint: isFocalPoint(objectValue.focal_point) || objectValue.focal_point === null ? objectValue.focal_point ?? null : void 0,
349
+ customData: isJsonRecord(objectValue.custom_data) || objectValue.custom_data === null ? objectValue.custom_data ?? null : void 0
350
+ };
351
+ }
352
+ function parseMediaGalleryReferences(value) {
353
+ const parsed = decodeJsonIfString(value);
354
+ if (!Array.isArray(parsed)) return [];
355
+ return parsed.map((entry) => parseMediaFieldReference(entry)).filter((entry) => entry !== null);
356
+ }
357
+ function mergeAssetWithMediaReference(asset, reference, assetUrl) {
358
+ const defaultCustomData = asset.custom_data ? decodeJsonStringOr(asset.custom_data, null) : null;
359
+ const defaultFocalPoint = asset.focal_point ? decodeJsonStringOr(asset.focal_point, null) : null;
360
+ return {
361
+ id: asset.id,
362
+ filename: asset.filename,
363
+ mimeType: asset.mime_type,
364
+ size: asset.size,
365
+ width: asset.width,
366
+ height: asset.height,
367
+ alt: reference?.alt ?? asset.alt,
368
+ title: reference?.title ?? asset.title,
369
+ blurhash: asset.blurhash ?? null,
370
+ focalPoint: isFocalPoint(reference?.focalPoint) ? reference.focalPoint : isFocalPoint(defaultFocalPoint) ? defaultFocalPoint : null,
371
+ customData: isJsonRecord(reference?.customData) ? reference.customData : isJsonRecord(defaultCustomData) ? defaultCustomData : null,
372
+ tags: Array.isArray(decodeJsonStringOr(asset.tags, [])) ? decodeJsonStringOr(asset.tags, []).filter((value) => typeof value === "string") : [],
373
+ url: assetUrl(asset.r2_key),
374
+ _createdAt: asset.created_at,
375
+ _updatedAt: asset.updated_at,
376
+ _createdBy: asset.created_by,
377
+ _updatedBy: asset.updated_by
378
+ };
379
+ }
380
+ function isJsonRecord(value) {
381
+ return typeof value === "object" && value !== null && !Array.isArray(value);
382
+ }
383
+ function isFocalPoint(value) {
384
+ return isJsonRecord(value) && typeof value.x === "number" && typeof value.y === "number";
385
+ }
386
+ //#endregion
387
+ //#region src/db/validators.ts
388
+ /**
389
+ * Typed access to field validator properties.
390
+ * Instead of casting with `as`, these functions safely extract
391
+ * known validator properties with runtime checks.
392
+ */
393
+ /** Safely get the slug source field from validators */
394
+ function getSlugSource(validators) {
395
+ const v = validators.slug_source;
396
+ return typeof v === "string" ? v : void 0;
397
+ }
398
+ /** Safely get the structured_text_blocks whitelist */
399
+ function getBlockWhitelist(validators) {
400
+ const v = validators.structured_text_blocks;
401
+ return Array.isArray(v) && v.every((x) => typeof x === "string") ? v : void 0;
402
+ }
403
+ /** Safely get the blocks_only flag */
404
+ function getBlocksOnly(validators) {
405
+ return validators.blocks_only === true;
406
+ }
407
+ /** Safely check if field is required */
408
+ function isRequired(validators) {
409
+ return validators.required === true;
410
+ }
411
+ /** Safely check if field must be unique */
412
+ function isUnique(validators) {
413
+ return validators.unique === true;
414
+ }
415
+ /** Safely get link target model types (for `link` fields) */
416
+ function getLinkTargets(validators) {
417
+ const v = validators.item_item_type;
418
+ return Array.isArray(v) && v.every((x) => typeof x === "string") ? v : void 0;
419
+ }
420
+ /** Safely get links target model types (for `links` fields) */
421
+ function getLinksTargets(validators) {
422
+ const v = validators.items_item_type;
423
+ return Array.isArray(v) && v.every((x) => typeof x === "string") ? v : void 0;
424
+ }
425
+ /** Check if field is searchable (default: true — opt out with {"searchable": false}) */
426
+ function isSearchable(validators) {
427
+ return validators.searchable !== false;
428
+ }
429
+ /** Field types where exact-value uniqueness is supported */
430
+ function supportsUniqueValidation(fieldType) {
431
+ return [
432
+ "string",
433
+ "text",
434
+ "slug",
435
+ "integer",
436
+ "float",
437
+ "boolean",
438
+ "date",
439
+ "date_time",
440
+ "link",
441
+ "media"
442
+ ].includes(fieldType);
443
+ }
444
+ /**
445
+ * Compute whether a record is valid (all required fields have values).
446
+ * For localized required fields, checks the default locale key in the JSON map.
447
+ * When allLocales is provided, checks all locale keys (for all_locales_required models).
448
+ * Returns { valid, missingFields } where missingFields lists api_keys that are missing.
449
+ */
450
+ function computeIsValid(record, fields, defaultLocale, allLocales) {
451
+ const missingFields = [];
452
+ for (const field of fields) {
453
+ const value = record[field.api_key];
454
+ let fieldInvalid = false;
455
+ if (field.localized && defaultLocale) {
456
+ let localeMap = value;
457
+ if (typeof localeMap === "string") try {
458
+ localeMap = JSON.parse(localeMap);
459
+ } catch {
460
+ missingFields.push(field.api_key);
461
+ continue;
462
+ }
463
+ if (typeof localeMap !== "object" || localeMap === null) {
464
+ missingFields.push(field.api_key);
465
+ continue;
466
+ }
467
+ const map = localeMap;
468
+ const localesToCheck = allLocales ?? [defaultLocale];
469
+ for (const locale of localesToCheck) {
470
+ const locValue = map[locale];
471
+ if (!isValueValidForField(locValue, field.field_type, field.validators)) {
472
+ fieldInvalid = true;
473
+ break;
474
+ }
475
+ }
476
+ } else fieldInvalid = !isValueValidForField(value, field.field_type, field.validators);
477
+ if (fieldInvalid) missingFields.push(field.api_key);
478
+ }
479
+ return {
480
+ valid: missingFields.length === 0,
481
+ missingFields
482
+ };
483
+ }
484
+ function isValueValidForField(value, fieldType, validators) {
485
+ if (isRequired(validators) && !hasMeaningfulValue(value)) return false;
486
+ if (!hasMeaningfulValue(value)) return true;
487
+ if (!passesEnumValidation(value, validators)) return false;
488
+ if (!passesLengthValidation(value, fieldType, validators)) return false;
489
+ if (!passesNumberRangeValidation(value, fieldType, validators)) return false;
490
+ if (!passesFormatValidation(value, fieldType, validators)) return false;
491
+ if (!passesDateRangeValidation(value, fieldType, validators)) return false;
492
+ return true;
493
+ }
494
+ function passesEnumValidation(value, validators) {
495
+ const enumValues = validators.enum;
496
+ if (!Array.isArray(enumValues) || !enumValues.every((entry) => typeof entry === "string")) return true;
497
+ return typeof value === "string" && enumValues.includes(value);
498
+ }
499
+ function passesLengthValidation(value, fieldType, validators) {
500
+ if (![
501
+ "string",
502
+ "text",
503
+ "slug"
504
+ ].includes(fieldType)) return true;
505
+ const lengthConfig = validators.length;
506
+ if (typeof lengthConfig !== "object" || lengthConfig === null || Array.isArray(lengthConfig)) return true;
507
+ const length = lengthConfig;
508
+ if (typeof value !== "string") return false;
509
+ const min = typeof length.min === "number" ? length.min : void 0;
510
+ const max = typeof length.max === "number" ? length.max : void 0;
511
+ if (min !== void 0 && value.length < min) return false;
512
+ if (max !== void 0 && value.length > max) return false;
513
+ return true;
514
+ }
515
+ function passesNumberRangeValidation(value, fieldType, validators) {
516
+ if (!["integer", "float"].includes(fieldType)) return true;
517
+ const rangeConfig = validators.number_range;
518
+ if (typeof rangeConfig !== "object" || rangeConfig === null || Array.isArray(rangeConfig)) return true;
519
+ const range = rangeConfig;
520
+ if (typeof value !== "number") return false;
521
+ const min = typeof range.min === "number" ? range.min : void 0;
522
+ const max = typeof range.max === "number" ? range.max : void 0;
523
+ if (min !== void 0 && value < min) return false;
524
+ if (max !== void 0 && value > max) return false;
525
+ return true;
526
+ }
527
+ function passesFormatValidation(value, fieldType, validators) {
528
+ if (![
529
+ "string",
530
+ "text",
531
+ "slug"
532
+ ].includes(fieldType) || typeof value !== "string") return true;
533
+ const format = validators.format;
534
+ if (!format) return true;
535
+ if (format === "email") return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
536
+ if (format === "url") try {
537
+ const parsed = new URL(value);
538
+ return parsed.protocol === "http:" || parsed.protocol === "https:";
539
+ } catch {
540
+ return false;
541
+ }
542
+ if (typeof format === "object" && !Array.isArray(format) && typeof format.custom_pattern === "string") try {
543
+ return new RegExp(format.custom_pattern).test(value);
544
+ } catch {
545
+ return false;
546
+ }
547
+ return true;
548
+ }
549
+ function passesDateRangeValidation(value, fieldType, validators) {
550
+ if (!["date", "date_time"].includes(fieldType) || typeof value !== "string") return true;
551
+ const rangeConfig = validators.date_range;
552
+ if (typeof rangeConfig !== "object" || rangeConfig === null || Array.isArray(rangeConfig)) return true;
553
+ const range = rangeConfig;
554
+ const valueTime = parseDateValue(value);
555
+ if (valueTime === null) return false;
556
+ const minTime = parseDateBoundary(range.min);
557
+ const maxTime = parseDateBoundary(range.max);
558
+ if (minTime !== null && valueTime < minTime) return false;
559
+ if (maxTime !== null && valueTime > maxTime) return false;
560
+ return true;
561
+ }
562
+ function parseDateValue(value) {
563
+ const time = Date.parse(value);
564
+ return Number.isNaN(time) ? null : time;
565
+ }
566
+ function parseDateBoundary(value) {
567
+ if (value === void 0) return null;
568
+ if (value === "now") return Date.now();
569
+ if (typeof value !== "string") return null;
570
+ return parseDateValue(value);
571
+ }
572
+ function findUniqueConstraintViolations(options) {
573
+ return Effect.gen(function* () {
574
+ const sql = yield* SqlClient.SqlClient;
575
+ const invalidFields = /* @__PURE__ */ new Set();
576
+ for (const field of options.fields) {
577
+ if (!isUnique(field.validators) || !supportsUniqueValidation(field.field_type)) continue;
578
+ if (options.onlyFieldApiKeys && !options.onlyFieldApiKeys.has(field.api_key)) continue;
579
+ const value = options.record[field.api_key];
580
+ if (field.localized) {
581
+ const localeMap = parseLocaleMap(value);
582
+ for (const [localeCode, localeValue] of Object.entries(localeMap)) {
583
+ if (!hasMeaningfulValue(localeValue)) continue;
584
+ const path = `$."${localeCode.replace(/"/g, "\\\"")}"`;
585
+ if ((yield* sql.unsafe(`SELECT id FROM "${options.tableName}" WHERE json_extract("${field.api_key}", ?) = ?${options.excludeId ? " AND id != ?" : ""} LIMIT 1`, options.excludeId ? [
586
+ path,
587
+ serializeUniqueValue(localeValue),
588
+ options.excludeId
589
+ ] : [path, serializeUniqueValue(localeValue)])).length > 0) {
590
+ invalidFields.add(field.api_key);
591
+ break;
592
+ }
593
+ }
594
+ continue;
595
+ }
596
+ if (!hasMeaningfulValue(value)) continue;
597
+ const comparableValue = field.field_type === "media" ? parseMediaFieldReference(value)?.uploadId ?? value : value;
598
+ if ((yield* sql.unsafe(`SELECT id FROM "${options.tableName}" WHERE (CASE WHEN json_valid("${field.api_key}") AND json_type("${field.api_key}") = 'object' THEN json_extract("${field.api_key}", '$.upload_id') ELSE "${field.api_key}" END) = ?${options.excludeId ? " AND id != ?" : ""} LIMIT 1`, options.excludeId ? [serializeUniqueValue(comparableValue), options.excludeId] : [serializeUniqueValue(comparableValue)])).length > 0) invalidFields.add(field.api_key);
599
+ }
600
+ return [...invalidFields];
601
+ });
602
+ }
603
+ function parseLocaleMap(value) {
604
+ if (value === null || value === void 0) return {};
605
+ return typeof value === "string" ? decodeJsonRecordStringOr(value, {}) : value;
606
+ }
607
+ function hasMeaningfulValue(value) {
608
+ return value !== null && value !== void 0 && value !== "";
609
+ }
610
+ function serializeUniqueValue(value) {
611
+ if (typeof value === "boolean") return value ? 1 : 0;
612
+ return value;
613
+ }
614
+ //#endregion
615
+ //#region src/db/row-types.ts
616
+ function isContentRow(row) {
617
+ return typeof row === "object" && row !== null && "id" in row && "_status" in row;
618
+ }
619
+ /** Runtime check that a value is a plain object record */
620
+ function isRecord(value) {
621
+ return typeof value === "object" && value !== null && !Array.isArray(value);
622
+ }
623
+ /** Safely parse JSON to a Record, returning empty object on failure */
624
+ function parseJsonRecord(json) {
625
+ if (!json) return {};
626
+ try {
627
+ const parsed = JSON.parse(json);
628
+ if (isRecord(parsed)) return parsed;
629
+ } catch {}
630
+ return {};
631
+ }
632
+ /** Parse a FieldRow's validators from JSON string to object */
633
+ function parseFieldValidators(field) {
634
+ return {
635
+ ...field,
636
+ validators: parseJsonRecord(field.validators)
637
+ };
638
+ }
639
+ //#endregion
640
+ //#region src/dast/schema.ts
641
+ /**
642
+ * Effect Schema definitions for DAST documents.
643
+ * Comprehensive recursive schemas for full validation of DAST nodes.
644
+ */
645
+ const MarkSchema = Schema.Literal("strong", "emphasis", "underline", "strikethrough", "code", "highlight");
646
+ const LinkMetaEntry = Schema.Struct({
647
+ id: Schema.String,
648
+ value: Schema.String
649
+ });
650
+ const SpanNodeSchema = Schema.Struct({
651
+ type: Schema.Literal("span"),
652
+ value: Schema.String,
653
+ marks: Schema.optionalWith(Schema.Array(MarkSchema), { exact: true })
654
+ });
655
+ const LinkNodeSchema = Schema.Struct({
656
+ type: Schema.Literal("link"),
657
+ url: Schema.NonEmptyString,
658
+ meta: Schema.optionalWith(Schema.Array(LinkMetaEntry), { exact: true }),
659
+ children: Schema.Array(SpanNodeSchema)
660
+ });
661
+ const ItemLinkNodeSchema = Schema.Struct({
662
+ type: Schema.Literal("itemLink"),
663
+ item: Schema.NonEmptyString,
664
+ meta: Schema.optionalWith(Schema.Array(LinkMetaEntry), { exact: true }),
665
+ children: Schema.Array(SpanNodeSchema)
666
+ });
667
+ const InlineItemNodeSchema = Schema.Struct({
668
+ type: Schema.Literal("inlineItem"),
669
+ item: Schema.NonEmptyString
670
+ });
671
+ const InlineBlockNodeSchema = Schema.Struct({
672
+ type: Schema.Literal("inlineBlock"),
673
+ item: Schema.NonEmptyString
674
+ });
675
+ const InlineNodeSchema = Schema.Union(SpanNodeSchema, LinkNodeSchema, ItemLinkNodeSchema, InlineItemNodeSchema, InlineBlockNodeSchema);
676
+ const ParagraphNodeSchema = Schema.Struct({
677
+ type: Schema.Literal("paragraph"),
678
+ style: Schema.optionalWith(Schema.String, { exact: true }),
679
+ children: Schema.Array(InlineNodeSchema)
680
+ });
681
+ const HeadingLevel = Schema.Number.pipe(Schema.int(), Schema.greaterThanOrEqualTo(1), Schema.lessThanOrEqualTo(6));
682
+ const HeadingNodeSchema = Schema.Struct({
683
+ type: Schema.Literal("heading"),
684
+ level: HeadingLevel,
685
+ style: Schema.optionalWith(Schema.String, { exact: true }),
686
+ children: Schema.Array(InlineNodeSchema)
687
+ });
688
+ const ListNodeSchema = Schema.suspend(() => Schema.Struct({
689
+ type: Schema.Literal("list"),
690
+ style: Schema.Literal("bulleted", "numbered"),
691
+ children: Schema.Array(ListItemNodeSchema)
692
+ }));
693
+ const ListItemNodeSchema = Schema.suspend(() => Schema.Struct({
694
+ type: Schema.Literal("listItem"),
695
+ children: Schema.Array(Schema.Union(ParagraphNodeSchema, ListNodeSchema))
696
+ }));
697
+ const BlockquoteNodeSchema = Schema.Struct({
698
+ type: Schema.Literal("blockquote"),
699
+ attribution: Schema.optionalWith(Schema.String, { exact: true }),
700
+ children: Schema.Array(ParagraphNodeSchema)
701
+ });
702
+ const CodeNodeSchema = Schema.Struct({
703
+ type: Schema.Literal("code"),
704
+ code: Schema.String,
705
+ language: Schema.optionalWith(Schema.String, { exact: true }),
706
+ highlight: Schema.optionalWith(Schema.Array(Schema.Number), { exact: true })
707
+ });
708
+ const ThematicBreakNodeSchema = Schema.Struct({ type: Schema.Literal("thematicBreak") });
709
+ const BlockRefNodeSchema = Schema.Struct({
710
+ type: Schema.Literal("block"),
711
+ item: Schema.NonEmptyString
712
+ });
713
+ const TableCellNodeSchema = Schema.Struct({
714
+ type: Schema.Literal("tableCell"),
715
+ children: Schema.Array(Schema.Union(ParagraphNodeSchema, InlineNodeSchema))
716
+ });
717
+ const TableRowNodeSchema = Schema.Struct({
718
+ type: Schema.Literal("tableRow"),
719
+ children: Schema.NonEmptyArray(TableCellNodeSchema)
720
+ });
721
+ const TableNodeSchema = Schema.Struct({
722
+ type: Schema.Literal("table"),
723
+ children: Schema.NonEmptyArray(TableRowNodeSchema)
724
+ });
725
+ const BlockLevelNodeSchema = Schema.Union(ParagraphNodeSchema, HeadingNodeSchema, ListNodeSchema, BlockquoteNodeSchema, CodeNodeSchema, ThematicBreakNodeSchema, BlockRefNodeSchema, TableNodeSchema);
726
+ const RootNodeSchema = Schema.Struct({
727
+ type: Schema.Literal("root"),
728
+ children: Schema.Array(BlockLevelNodeSchema)
729
+ });
730
+ const DastDocumentSchema = Schema.Struct({
731
+ schema: Schema.Literal("dast"),
732
+ document: RootNodeSchema
733
+ });
734
+ /** Decode a StructuredText write payload */
735
+ const StructuredTextWriteInput = Schema.Struct({
736
+ value: DastDocumentSchema,
737
+ blocks: Schema.optionalWith(Schema.Record({
738
+ key: Schema.String,
739
+ value: Schema.Unknown
740
+ }), { default: () => ({}) })
741
+ });
742
+ //#endregion
743
+ //#region src/dast/validate.ts
744
+ /**
745
+ * Validate that a DAST document only contains block nodes at root level.
746
+ */
747
+ function validateBlocksOnly(doc) {
748
+ const decoded = Schema.decodeUnknownEither(DastDocumentSchema)(doc);
749
+ if (decoded._tag === "Left") return [];
750
+ return decoded.right.document.children.flatMap((child, index) => child.type === "block" ? [] : [{
751
+ path: `document.children[${index}]`,
752
+ message: `Only block nodes are allowed at root level in a blocks-only field. Found "${child.type}" node.`
753
+ }]);
754
+ }
755
+ function visitDastNode(node, visit) {
756
+ visit(node);
757
+ if ("children" in node) for (const child of node.children) visitDastNode(child, visit);
758
+ }
759
+ /**
760
+ * Extract all block-level block IDs (type "block") from a DAST document.
761
+ */
762
+ function extractBlockIds(doc) {
763
+ const ids = [];
764
+ visitDastNode({
765
+ type: "root",
766
+ children: doc.document.children
767
+ }, (node) => {
768
+ if (node.type === "block") ids.push(node.item);
769
+ });
770
+ return ids;
771
+ }
772
+ /**
773
+ * Extract all inline block IDs (type "inlineBlock") from a DAST document.
774
+ */
775
+ function extractInlineBlockIds(doc) {
776
+ const ids = [];
777
+ visitDastNode({
778
+ type: "root",
779
+ children: doc.document.children
780
+ }, (node) => {
781
+ if (node.type === "inlineBlock") ids.push(node.item);
782
+ });
783
+ return ids;
784
+ }
785
+ /**
786
+ * Extract ALL block IDs (both "block" and "inlineBlock") from a DAST document.
787
+ * Used for write orchestration where both types need to be stored.
788
+ */
789
+ function extractAllBlockIds(doc) {
790
+ const ids = [];
791
+ visitDastNode({
792
+ type: "root",
793
+ children: doc.document.children
794
+ }, (node) => {
795
+ if (node.type === "block" || node.type === "inlineBlock") ids.push(node.item);
796
+ });
797
+ return ids;
798
+ }
799
+ /**
800
+ * Extract all record link IDs referenced in a DAST document.
801
+ */
802
+ function extractLinkIds(doc) {
803
+ const ids = [];
804
+ visitDastNode({
805
+ type: "root",
806
+ children: doc.document.children
807
+ }, (node) => {
808
+ if (node.type === "itemLink" || node.type === "inlineItem") ids.push(node.item);
809
+ });
810
+ return ids;
811
+ }
812
+ /**
813
+ * Remove block/inlineBlock nodes whose item ID is in the given set.
814
+ * Returns a deep-cloned document with those nodes pruned from the tree.
815
+ */
816
+ function pruneBlockNodes(doc, blockIdsToRemove) {
817
+ function isSpanOnlyChildren(node) {
818
+ return node.type === "link" || node.type === "itemLink";
819
+ }
820
+ function pruneInlineNode(node) {
821
+ if (node.type === "inlineBlock" && blockIdsToRemove.has(node.item)) return null;
822
+ if (isSpanOnlyChildren(node)) return node;
823
+ return node;
824
+ }
825
+ function pruneParagraphNode(node) {
826
+ return {
827
+ ...node,
828
+ children: node.children.map((child) => pruneInlineNode(child)).filter((child) => child !== null)
829
+ };
830
+ }
831
+ function pruneBlockLevelNode(node) {
832
+ if (node.type === "block" && blockIdsToRemove.has(node.item)) return null;
833
+ switch (node.type) {
834
+ case "paragraph": return pruneParagraphNode(node);
835
+ case "heading": return {
836
+ ...node,
837
+ children: node.children.map((child) => pruneInlineNode(child)).filter((child) => child !== null)
838
+ };
839
+ case "list": return {
840
+ ...node,
841
+ children: node.children.map((child) => ({
842
+ ...child,
843
+ children: child.children.map((nested) => pruneBlockLevelNode(nested)).filter((nested) => nested !== null && (nested.type === "paragraph" || nested.type === "list"))
844
+ }))
845
+ };
846
+ case "blockquote": return {
847
+ ...node,
848
+ children: node.children.map((child) => pruneParagraphNode(child))
849
+ };
850
+ case "table": return {
851
+ ...node,
852
+ children: node.children.map((row) => ({
853
+ ...row,
854
+ children: row.children.map((cell) => ({
855
+ ...cell,
856
+ children: cell.children.map((child) => "item" in child || "url" in child || "value" in child ? pruneInlineNode(child) : pruneBlockLevelNode(child)).filter((child) => child !== null)
857
+ }))
858
+ }))
859
+ };
860
+ default: return node;
861
+ }
862
+ }
863
+ return {
864
+ schema: "dast",
865
+ document: {
866
+ type: "root",
867
+ children: doc.document.children.map((child) => pruneBlockLevelNode(child)).filter((child) => child !== null)
868
+ }
869
+ };
870
+ }
871
+ //#endregion
872
+ //#region src/dast/markdown.ts
873
+ /**
874
+ * Editable DAST ↔ Markdown projection.
875
+ *
876
+ * This is NOT a generic serializer. Markdown is a projection of DAST for
877
+ * editing text structure. DAST remains the source of truth.
878
+ *
879
+ * Contract:
880
+ * - Block refs, inline refs survive as stable sentinels
881
+ * - Deleting a sentinel deletes the ref; moving it reorders it
882
+ * - Unsupported DAST metadata (link.meta, paragraph.style, heading.style,
883
+ * blockquote.attribution, code.highlight) is preserved in a sidecar map
884
+ * and re-attached on round-trip if the node is untouched
885
+ * - No new blocks can be authored from markdown
886
+ * - No block payloads are editable from markdown
887
+ *
888
+ * API:
889
+ * dastToEditableMarkdown(doc) → { markdown, preservation }
890
+ * editableMarkdownToDast(markdown, preservation) → DastDocument
891
+ *
892
+ * Legacy wrappers dastToMarkdown/markdownToDast are preserved for
893
+ * non-editing use cases (export, display).
894
+ */
895
+ const BLOCK_SENTINEL_RE = /^<!--\s*cms:block:(\S+)\s*-->$/;
896
+ const NODE_SENTINEL_RE = /^<!--\s*cms:n(\d+)\s*-->$/;
897
+ const INLINE_ITEM_SENTINEL_RE = /^<!--\s*cms:inlineItem:(\S+)\s*-->$/;
898
+ const INLINE_BLOCK_SENTINEL_RE = /^<!--\s*cms:inlineBlock:(\S+)\s*-->$/;
899
+ const LINK_META_SENTINEL_RE = /^<!--\s*cms:linkMeta:(\S+)\s*-->$/;
900
+ const ITEM_LINK_META_SENTINEL_RE = /^<!--\s*cms:itemLinkMeta:(\S+)\s*-->$/;
901
+ const ITEM_LINK_PREFIX = "itemLink:";
902
+ const MARK_RE = /^<mark>([\s\S]*)<\/mark>$/;
903
+ const UNDERLINE_RE = /^<u>([\s\S]*)<\/u>$/;
904
+ function decodeSentinelValue(value) {
905
+ try {
906
+ return decodeURIComponent(value);
907
+ } catch {
908
+ return value;
909
+ }
910
+ }
911
+ function escapeHtmlText(value) {
912
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
913
+ }
914
+ function decodeHtmlText(value) {
915
+ return value.replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
916
+ }
917
+ /** Map of opening HTML tags to DAST marks */
918
+ const HTML_TAG_TO_MARK = {
919
+ "<u>": "underline",
920
+ "<mark>": "highlight"
921
+ };
922
+ const HTML_CLOSING_TAGS = new Set(["</u>", "</mark>"]);
923
+ function isOpeningMarkTag(value) {
924
+ return value in HTML_TAG_TO_MARK;
925
+ }
926
+ function isClosingMarkTag(value) {
927
+ return HTML_CLOSING_TAGS.has(value);
928
+ }
929
+ function mergeHtmlTagSequences(nodes) {
930
+ const result = [];
931
+ let i = 0;
932
+ while (i < nodes.length) {
933
+ const node = nodes[i];
934
+ if (node.type === "html" && isOpeningMarkTag(node.value)) {
935
+ let depth = 1;
936
+ let raw = node.value;
937
+ i++;
938
+ while (i < nodes.length) {
939
+ const current = nodes[i];
940
+ if (current.type === "html") {
941
+ raw += current.value;
942
+ if (isOpeningMarkTag(current.value)) depth++;
943
+ else if (isClosingMarkTag(current.value)) {
944
+ depth--;
945
+ i++;
946
+ if (depth === 0) break;
947
+ continue;
948
+ }
949
+ } else if (current.type === "text") raw += escapeHtmlText(current.value);
950
+ else if (current.type === "inlineCode") raw += escapeHtmlText(current.value);
951
+ else {
952
+ result.push(node);
953
+ i++;
954
+ break;
955
+ }
956
+ i++;
957
+ }
958
+ if (depth === 0) result.push({
959
+ type: "html",
960
+ value: raw
961
+ });
962
+ } else {
963
+ result.push(node);
964
+ i++;
965
+ }
966
+ }
967
+ return result;
968
+ }
969
+ function addMark(inline, mark) {
970
+ if (inline.type !== "span") return inline;
971
+ const existing = inline.marks ?? [];
972
+ if (existing.includes(mark)) return inline;
973
+ return {
974
+ ...inline,
975
+ marks: [...existing, mark]
976
+ };
977
+ }
978
+ function mdastPhrasingToSpans(nodes, pres) {
979
+ return mdastPhrasingToDastInlines(nodes, pres).map((inline) => inline.type === "span" ? inline : {
980
+ type: "span",
981
+ value: extractText(inline)
982
+ });
983
+ }
984
+ function extractLinkMetaId(children, pattern) {
985
+ let metaId = null;
986
+ const remaining = [];
987
+ for (const child of children) {
988
+ if (child.type === "html") {
989
+ const match = pattern.exec(child.value);
990
+ if (match) {
991
+ metaId = decodeSentinelValue(match[1]);
992
+ continue;
993
+ }
994
+ }
995
+ remaining.push(child);
996
+ }
997
+ return {
998
+ metaId,
999
+ children: remaining
1000
+ };
1001
+ }
1002
+ function extractText(node) {
1003
+ switch (node.type) {
1004
+ case "span": return node.value;
1005
+ case "link":
1006
+ case "itemLink": return node.children.map((c) => c.value).join("");
1007
+ case "inlineItem":
1008
+ case "inlineBlock": return "";
1009
+ }
1010
+ }
1011
+ function mdastPhrasingToDastInlines(rawNodes, pres) {
1012
+ const nodes = mergeHtmlTagSequences(rawNodes);
1013
+ const result = [];
1014
+ let pendingLinkMetaId = null;
1015
+ let pendingItemLinkMetaId = null;
1016
+ for (const node of nodes) switch (node.type) {
1017
+ case "text":
1018
+ result.push({
1019
+ type: "span",
1020
+ value: node.value
1021
+ });
1022
+ break;
1023
+ case "inlineCode":
1024
+ result.push({
1025
+ type: "span",
1026
+ value: node.value,
1027
+ marks: ["code"]
1028
+ });
1029
+ break;
1030
+ case "strong":
1031
+ for (const inline of mdastPhrasingToDastInlines(node.children, pres)) result.push(addMark(inline, "strong"));
1032
+ break;
1033
+ case "emphasis":
1034
+ for (const inline of mdastPhrasingToDastInlines(node.children, pres)) result.push(addMark(inline, "emphasis"));
1035
+ break;
1036
+ case "delete":
1037
+ for (const inline of mdastPhrasingToDastInlines(node.children, pres)) result.push(addMark(inline, "strikethrough"));
1038
+ break;
1039
+ case "link":
1040
+ if (node.url.startsWith(ITEM_LINK_PREFIX)) {
1041
+ const itemId = node.url.slice(9);
1042
+ const extracted = extractLinkMetaId(node.children, ITEM_LINK_META_SENTINEL_RE);
1043
+ const children = mdastPhrasingToSpans(extracted.children, pres);
1044
+ const preserved = extracted.metaId ? pres.itemLinks[extracted.metaId] : pendingItemLinkMetaId ? pres.itemLinks[pendingItemLinkMetaId] : void 0;
1045
+ const itemLink = {
1046
+ type: "itemLink",
1047
+ item: itemId,
1048
+ children,
1049
+ ...preserved?.meta ? { meta: preserved.meta } : {}
1050
+ };
1051
+ result.push(itemLink);
1052
+ pendingItemLinkMetaId = null;
1053
+ } else {
1054
+ const extracted = extractLinkMetaId(node.children, LINK_META_SENTINEL_RE);
1055
+ const children = mdastPhrasingToSpans(extracted.children, pres);
1056
+ const preserved = extracted.metaId ? pres.links[extracted.metaId] : pendingLinkMetaId ? pres.links[pendingLinkMetaId] : void 0;
1057
+ const link = {
1058
+ type: "link",
1059
+ url: node.url,
1060
+ children,
1061
+ ...preserved?.meta ? { meta: preserved.meta } : {}
1062
+ };
1063
+ result.push(link);
1064
+ pendingLinkMetaId = null;
1065
+ }
1066
+ break;
1067
+ case "html": {
1068
+ const linkMetaMatch = LINK_META_SENTINEL_RE.exec(node.value);
1069
+ if (linkMetaMatch) {
1070
+ pendingLinkMetaId = decodeSentinelValue(linkMetaMatch[1]);
1071
+ break;
1072
+ }
1073
+ const itemLinkMetaMatch = ITEM_LINK_META_SENTINEL_RE.exec(node.value);
1074
+ if (itemLinkMetaMatch) {
1075
+ pendingItemLinkMetaId = decodeSentinelValue(itemLinkMetaMatch[1]);
1076
+ break;
1077
+ }
1078
+ const m1 = INLINE_ITEM_SENTINEL_RE.exec(node.value);
1079
+ if (m1) {
1080
+ result.push({
1081
+ type: "inlineItem",
1082
+ item: decodeSentinelValue(m1[1])
1083
+ });
1084
+ break;
1085
+ }
1086
+ const m2 = INLINE_BLOCK_SENTINEL_RE.exec(node.value);
1087
+ if (m2) {
1088
+ result.push({
1089
+ type: "inlineBlock",
1090
+ item: decodeSentinelValue(m2[1])
1091
+ });
1092
+ break;
1093
+ }
1094
+ const marked = parseMarkedHtml(node.value);
1095
+ if (marked) {
1096
+ result.push(marked);
1097
+ break;
1098
+ }
1099
+ result.push({
1100
+ type: "span",
1101
+ value: node.value
1102
+ });
1103
+ break;
1104
+ }
1105
+ case "break":
1106
+ result.push({
1107
+ type: "span",
1108
+ value: "\n"
1109
+ });
1110
+ break;
1111
+ default: break;
1112
+ }
1113
+ return result;
1114
+ }
1115
+ function parseMarkedHtml(value) {
1116
+ const marks = [];
1117
+ let current = value;
1118
+ for (;;) {
1119
+ const underlineMatch = UNDERLINE_RE.exec(current);
1120
+ if (underlineMatch) {
1121
+ marks.push("underline");
1122
+ current = underlineMatch[1];
1123
+ continue;
1124
+ }
1125
+ const highlightMatch = MARK_RE.exec(current);
1126
+ if (highlightMatch) {
1127
+ marks.push("highlight");
1128
+ current = highlightMatch[1];
1129
+ continue;
1130
+ }
1131
+ break;
1132
+ }
1133
+ if (marks.length === 0) return null;
1134
+ return {
1135
+ type: "span",
1136
+ value: decodeHtmlText(current),
1137
+ marks
1138
+ };
1139
+ }
1140
+ /**
1141
+ * Parse markdown back into DAST, re-attaching preserved metadata.
1142
+ * The `pendingNodeId` is set when we encounter a `<!-- cms:nX -->` sentinel,
1143
+ * and consumed by the next block-level node.
1144
+ */
1145
+ function mdastBlockToDast(node, pres, nodeId) {
1146
+ const meta = nodeId ? pres.nodes[nodeId] : void 0;
1147
+ switch (node.type) {
1148
+ case "paragraph": return {
1149
+ type: "paragraph",
1150
+ children: mdastPhrasingToDastInlines(node.children, pres),
1151
+ ...meta?.style !== void 0 ? { style: meta.style } : {}
1152
+ };
1153
+ case "heading": return {
1154
+ type: "heading",
1155
+ level: node.depth,
1156
+ children: mdastPhrasingToDastInlines(node.children, pres),
1157
+ ...meta?.style !== void 0 ? { style: meta.style } : {}
1158
+ };
1159
+ case "list": return {
1160
+ type: "list",
1161
+ style: node.ordered ? "numbered" : "bulleted",
1162
+ children: node.children.map((item) => mdastListItemToDast(item, pres))
1163
+ };
1164
+ case "blockquote": return {
1165
+ type: "blockquote",
1166
+ children: node.children.filter((c) => c.type === "paragraph").map((p) => ({
1167
+ type: "paragraph",
1168
+ children: mdastPhrasingToDastInlines(p.children, pres)
1169
+ })),
1170
+ ...meta?.attribution !== void 0 ? { attribution: meta.attribution } : {}
1171
+ };
1172
+ case "code": return {
1173
+ type: "code",
1174
+ code: node.value,
1175
+ ...node.lang ? { language: node.lang } : {},
1176
+ ...meta?.highlight !== void 0 ? { highlight: meta.highlight } : {}
1177
+ };
1178
+ case "thematicBreak": return { type: "thematicBreak" };
1179
+ case "html": {
1180
+ const blockMatch = BLOCK_SENTINEL_RE.exec(node.value);
1181
+ if (blockMatch) return {
1182
+ type: "block",
1183
+ item: decodeSentinelValue(blockMatch[1])
1184
+ };
1185
+ return {
1186
+ type: "paragraph",
1187
+ children: [{
1188
+ type: "span",
1189
+ value: node.value
1190
+ }]
1191
+ };
1192
+ }
1193
+ case "table": return {
1194
+ type: "table",
1195
+ children: node.children.map((row) => mdastTableRowToDast(row, pres))
1196
+ };
1197
+ default: return null;
1198
+ }
1199
+ }
1200
+ function mdastListItemToDast(item, pres) {
1201
+ const children = [];
1202
+ for (const child of item.children) if (child.type === "paragraph") children.push({
1203
+ type: "paragraph",
1204
+ children: mdastPhrasingToDastInlines(child.children, pres)
1205
+ });
1206
+ else if (child.type === "list") children.push({
1207
+ type: "list",
1208
+ style: child.ordered ? "numbered" : "bulleted",
1209
+ children: child.children.map((li) => mdastListItemToDast(li, pres))
1210
+ });
1211
+ return {
1212
+ type: "listItem",
1213
+ children
1214
+ };
1215
+ }
1216
+ function mdastTableRowToDast(row, pres) {
1217
+ return {
1218
+ type: "tableRow",
1219
+ children: row.children.map((cell) => mdastTableCellToDast(cell, pres))
1220
+ };
1221
+ }
1222
+ function mdastTableCellToDast(cell, pres) {
1223
+ const inlines = mdastPhrasingToDastInlines(cell.children, pres);
1224
+ if (inlines.length === 0) return {
1225
+ type: "tableCell",
1226
+ children: [{
1227
+ type: "paragraph",
1228
+ children: [{
1229
+ type: "span",
1230
+ value: ""
1231
+ }]
1232
+ }]
1233
+ };
1234
+ return {
1235
+ type: "tableCell",
1236
+ children: [{
1237
+ type: "paragraph",
1238
+ children: inlines
1239
+ }]
1240
+ };
1241
+ }
1242
+ /**
1243
+ * Parse edited markdown back into DAST, re-attaching preserved metadata
1244
+ * from the sidecar. Sentinels that were deleted are omitted; sentinels
1245
+ * that were reordered are reflected in the output order.
1246
+ */
1247
+ function editableMarkdownToDast(markdown, preservation) {
1248
+ const mdastRoot = unified().use(remarkParse).use(remarkGfm).parse(markdown);
1249
+ const children = [];
1250
+ let pendingNodeId = null;
1251
+ let pendingInlineSentinels = [];
1252
+ for (const child of mdastRoot.children) {
1253
+ if (child.type === "html") {
1254
+ const nodeMatch = NODE_SENTINEL_RE.exec(child.value);
1255
+ if (nodeMatch) {
1256
+ pendingNodeId = `n${nodeMatch[1]}`;
1257
+ continue;
1258
+ }
1259
+ if (LINK_META_SENTINEL_RE.test(child.value) || ITEM_LINK_META_SENTINEL_RE.test(child.value)) {
1260
+ pendingInlineSentinels.push(child);
1261
+ continue;
1262
+ }
1263
+ }
1264
+ const childWithPendingSentinels = pendingInlineSentinels.length > 0 && (child.type === "paragraph" || child.type === "heading") ? {
1265
+ ...child,
1266
+ children: [...pendingInlineSentinels, ...child.children]
1267
+ } : child;
1268
+ pendingInlineSentinels = [];
1269
+ const dastNode = mdastBlockToDast(childWithPendingSentinels, preservation, pendingNodeId);
1270
+ pendingNodeId = null;
1271
+ if (dastNode) children.push(dastNode);
1272
+ }
1273
+ return {
1274
+ schema: "dast",
1275
+ document: {
1276
+ type: "root",
1277
+ children
1278
+ }
1279
+ };
1280
+ }
1281
+ /** Parse a CommonMark markdown string into a DAST document (no preservation). */
1282
+ function markdownToDast(markdown) {
1283
+ return editableMarkdownToDast(markdown, {
1284
+ nodes: {},
1285
+ links: {},
1286
+ itemLinks: {}
1287
+ });
1288
+ }
1289
+ //#endregion
1290
+ //#region src/graphql/sql-metrics.ts
1291
+ const sqlMetricsStorage = new AsyncLocalStorage();
1292
+ function withSqlMetrics(run) {
1293
+ return sqlMetricsStorage.run({
1294
+ statementCount: 0,
1295
+ hopCount: 0,
1296
+ batchHopCount: 0,
1297
+ batchedStatementCount: 0,
1298
+ totalDurationMs: 0,
1299
+ slowestSamplesMs: [],
1300
+ byPhase: /* @__PURE__ */ new Map()
1301
+ }, run);
1302
+ }
1303
+ function recordSqlMetrics(durationMs, options) {
1304
+ const store = sqlMetricsStorage.getStore();
1305
+ if (!store) return;
1306
+ const statementCount = options?.statementCount ?? 1;
1307
+ const hopCount = options?.hopCount ?? 1;
1308
+ const batchHopCount = options?.batchHopCount ?? 0;
1309
+ const batchedStatementCount = options?.batchedStatementCount ?? 0;
1310
+ store.statementCount += statementCount;
1311
+ store.hopCount += hopCount;
1312
+ store.batchHopCount += batchHopCount;
1313
+ store.batchedStatementCount += batchedStatementCount;
1314
+ store.totalDurationMs += durationMs;
1315
+ store.slowestSamplesMs.push(Number(durationMs.toFixed(3)));
1316
+ store.slowestSamplesMs.sort((a, b) => b - a);
1317
+ if (store.slowestSamplesMs.length > 5) store.slowestSamplesMs.length = 5;
1318
+ const phase = options?.phase;
1319
+ if (phase) {
1320
+ const current = store.byPhase.get(phase) ?? {
1321
+ statementCount: 0,
1322
+ hopCount: 0,
1323
+ batchHopCount: 0,
1324
+ batchedStatementCount: 0,
1325
+ totalDurationMs: 0
1326
+ };
1327
+ current.statementCount += statementCount;
1328
+ current.hopCount += hopCount;
1329
+ current.batchHopCount += batchHopCount;
1330
+ current.batchedStatementCount += batchedStatementCount;
1331
+ current.totalDurationMs += durationMs;
1332
+ store.byPhase.set(phase, current);
1333
+ }
1334
+ }
1335
+ function getSqlMetrics() {
1336
+ const store = sqlMetricsStorage.getStore();
1337
+ if (!store) return null;
1338
+ return {
1339
+ statementCount: store.statementCount,
1340
+ hopCount: store.hopCount,
1341
+ batchHopCount: store.batchHopCount,
1342
+ batchedStatementCount: store.batchedStatementCount,
1343
+ totalDurationMs: Number(store.totalDurationMs.toFixed(3)),
1344
+ slowestSamplesMs: store.slowestSamplesMs,
1345
+ byPhase: Object.fromEntries([...store.byPhase.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([phase, value]) => [phase, {
1346
+ statementCount: value.statementCount,
1347
+ hopCount: value.hopCount,
1348
+ batchHopCount: value.batchHopCount,
1349
+ batchedStatementCount: value.batchedStatementCount,
1350
+ totalDurationMs: Number(value.totalDurationMs.toFixed(3))
1351
+ }]))
1352
+ };
1353
+ }
1354
+ //#endregion
1355
+ //#region src/db/run-batched-queries.ts
1356
+ function isD1Database(value) {
1357
+ if (typeof value !== "object" || value === null) return false;
1358
+ return typeof Reflect.get(value, "prepare") === "function" && typeof Reflect.get(value, "batch") === "function";
1359
+ }
1360
+ function isD1ClientLike(value) {
1361
+ if (typeof value !== "object" || value === null) return false;
1362
+ const config = Reflect.get(value, "config");
1363
+ if (typeof config !== "object" || config === null) return false;
1364
+ return isD1Database(Reflect.get(config, "db"));
1365
+ }
1366
+ function runBatchedQueries(queries, options) {
1367
+ return Effect.gen(function* () {
1368
+ if (queries.length === 0) return [];
1369
+ const startedAt = performance.now();
1370
+ const sql = yield* SqlClient.SqlClient;
1371
+ if (isD1ClientLike(sql)) return yield* Effect.tryPromise({
1372
+ try: async () => {
1373
+ const statements = queries.map((query) => sql.config.db.prepare(query.sql).bind(...query.params));
1374
+ const results = await sql.config.db.batch(statements);
1375
+ recordSqlMetrics(performance.now() - startedAt, {
1376
+ statementCount: queries.length,
1377
+ hopCount: 1,
1378
+ batchHopCount: 1,
1379
+ batchedStatementCount: queries.length,
1380
+ phase: options?.phase
1381
+ });
1382
+ return results.map((result) => result.results);
1383
+ },
1384
+ catch: (cause) => new SqlError.SqlError({
1385
+ cause,
1386
+ message: "Failed to execute D1 batch query"
1387
+ })
1388
+ });
1389
+ const results = yield* Effect.all(queries.map((query) => sql.unsafe(query.sql, query.params)), { concurrency: 1 });
1390
+ recordSqlMetrics(performance.now() - startedAt, {
1391
+ statementCount: queries.length,
1392
+ hopCount: 1,
1393
+ batchHopCount: 1,
1394
+ batchedStatementCount: queries.length,
1395
+ phase: options?.phase
1396
+ });
1397
+ return results;
1398
+ });
1399
+ }
1400
+ //#endregion
1401
+ //#region src/types.ts
1402
+ /** Field types supported in v1 */
1403
+ const FIELD_TYPES = [
1404
+ "string",
1405
+ "text",
1406
+ "boolean",
1407
+ "integer",
1408
+ "slug",
1409
+ "media",
1410
+ "media_gallery",
1411
+ "link",
1412
+ "links",
1413
+ "structured_text",
1414
+ "seo",
1415
+ "json",
1416
+ "float",
1417
+ "date",
1418
+ "date_time",
1419
+ "color",
1420
+ "lat_lon",
1421
+ "video"
1422
+ ];
1423
+ /** Type guard for FieldType */
1424
+ function isFieldType(value) {
1425
+ return FIELD_TYPES.includes(value);
1426
+ }
1427
+ //#endregion
1428
+ //#region src/services/structured-text-service.ts
1429
+ function getStructuredTextStorageKey(fieldApiKey, localeCode) {
1430
+ return localeCode ? `${fieldApiKey}:${localeCode}` : fieldApiKey;
1431
+ }
1432
+ function mergeRowMaps(target, source) {
1433
+ for (const [tableName, rows] of source) {
1434
+ const existing = target.get(tableName);
1435
+ if (existing) existing.push(...rows);
1436
+ else target.set(tableName, [...rows]);
1437
+ }
1438
+ }
1439
+ function serializeValue(value) {
1440
+ if (value === void 0 || value === null) return null;
1441
+ if (typeof value === "boolean") return value ? 1 : 0;
1442
+ if (typeof value === "object") return encodeJson(value);
1443
+ return value;
1444
+ }
1445
+ function deserializeValue(value) {
1446
+ if (typeof value === "string" && (value.startsWith("{") || value.startsWith("["))) return decodeJsonStringOr(value, value);
1447
+ return value;
1448
+ }
1449
+ function decodeStructuredTextInput(fieldApiKey, value) {
1450
+ return Schema.decodeUnknown(StructuredTextWriteInput)(value).pipe(Effect.mapError((e) => new ValidationError({
1451
+ message: `Invalid StructuredText for field '${fieldApiKey}': ${e.message}`,
1452
+ field: fieldApiKey
1453
+ })));
1454
+ }
1455
+ function getParsedFields(sql, modelId) {
1456
+ return Effect.gen(function* () {
1457
+ return (yield* sql.unsafe("SELECT * FROM fields WHERE model_id = ? ORDER BY position", [modelId])).map(parseFieldValidators);
1458
+ });
1459
+ }
1460
+ function getBlockModelSchema(sql, blockApiKey) {
1461
+ return Effect.gen(function* () {
1462
+ const rows = yield* sql.unsafe("SELECT id, api_key FROM models WHERE api_key = ? AND is_block = 1", [blockApiKey]);
1463
+ if (rows.length === 0) return yield* new ValidationError({ message: `Block type '${blockApiKey}' does not exist` });
1464
+ const model = rows[0];
1465
+ const fields = yield* getParsedFields(sql, model.id);
1466
+ const structuredTextAllowedBlockApiKeysByField = /* @__PURE__ */ new Map();
1467
+ for (const field of fields) {
1468
+ if (field.field_type !== "structured_text") continue;
1469
+ structuredTextAllowedBlockApiKeysByField.set(field.api_key, getBlockWhitelist(field.validators) ?? []);
1470
+ }
1471
+ return {
1472
+ id: model.id,
1473
+ apiKey: model.api_key,
1474
+ fields,
1475
+ structuredTextAllowedBlockApiKeysByField
1476
+ };
1477
+ });
1478
+ }
1479
+ function getBlockModelSchemaCached(ctx, sql, blockApiKey) {
1480
+ return Effect.gen(function* () {
1481
+ const cached = ctx.blockModelSchemas.get(blockApiKey);
1482
+ if (cached) return cached;
1483
+ const schema = yield* getBlockModelSchema(sql, blockApiKey);
1484
+ ctx.blockModelSchemas.set(blockApiKey, schema);
1485
+ return schema;
1486
+ });
1487
+ }
1488
+ function fetchBlockModelsCached(ctx, sql) {
1489
+ return Effect.gen(function* () {
1490
+ if (ctx.blockModels) return ctx.blockModels;
1491
+ const blockModels = yield* fetchBlockModels(sql);
1492
+ ctx.blockModels = blockModels;
1493
+ return blockModels;
1494
+ });
1495
+ }
1496
+ function getCandidateBlockModelsCached(ctx, blockModels, allowedBlockApiKeys) {
1497
+ const cacheKey = allowedBlockApiKeys && allowedBlockApiKeys.length > 0 ? allowedBlockApiKeys.join(",") : "*";
1498
+ const cached = ctx.candidateBlockModels.get(cacheKey);
1499
+ if (cached) return cached;
1500
+ const candidateBlockModels = allowedBlockApiKeys && allowedBlockApiKeys.length > 0 ? blockModels.filter((model) => allowedBlockApiKeys.includes(model.api_key)) : blockModels;
1501
+ ctx.candidateBlockModels.set(cacheKey, candidateBlockModels);
1502
+ return candidateBlockModels;
1503
+ }
1504
+ function runHotBlockQueries(queries) {
1505
+ return runBatchedQueries(queries, { phase: "st_frontier" });
1506
+ }
1507
+ function formatDastParseErrors(error) {
1508
+ return ParseResult.ArrayFormatter.formatErrorSync(error).map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ");
1509
+ }
1510
+ function validateDastForField(fieldApiKey, value, blocksOnly) {
1511
+ return Effect.gen(function* () {
1512
+ const dast = yield* Schema.decodeUnknown(DastDocumentSchema)(value).pipe(Effect.mapError((e) => new ValidationError({
1513
+ message: `Invalid DAST document: ${formatDastParseErrors(e)}`,
1514
+ field: fieldApiKey
1515
+ })));
1516
+ if (blocksOnly) {
1517
+ const blocksOnlyErrors = validateBlocksOnly(value);
1518
+ if (blocksOnlyErrors.length > 0) return yield* new ValidationError({
1519
+ message: `Blocks-only field '${fieldApiKey}': ${blocksOnlyErrors.map((e) => e.message).join("; ")}`,
1520
+ field: fieldApiKey
1521
+ });
1522
+ }
1523
+ return dast;
1524
+ });
1525
+ }
1526
+ function compileStructuredText(ctx, container, params) {
1527
+ return Effect.gen(function* () {
1528
+ const { sql, seenBlockIds } = ctx;
1529
+ const { fieldApiKey, input, allowedBlockTypes, blocksOnly } = params;
1530
+ const dast = yield* validateDastForField(fieldApiKey, input.value, blocksOnly);
1531
+ const referencedBlockIds = extractAllBlockIds(dast);
1532
+ const providedBlockIds = Object.keys(input.blocks);
1533
+ for (const blockId of referencedBlockIds) if (!input.blocks[blockId]) return yield* new ValidationError({
1534
+ message: `DAST references block '${blockId}' but no block data provided for it`,
1535
+ field: fieldApiKey
1536
+ });
1537
+ for (const blockId of providedBlockIds) if (!referencedBlockIds.includes(blockId)) return yield* new ValidationError({
1538
+ message: `StructuredText field '${fieldApiKey}' includes unreferenced block '${blockId}'`,
1539
+ field: fieldApiKey
1540
+ });
1541
+ const rowsByTable = /* @__PURE__ */ new Map();
1542
+ for (const blockId of referencedBlockIds) {
1543
+ if (seenBlockIds.has(blockId)) return yield* new ValidationError({
1544
+ message: `StructuredText graph reuses block id '${blockId}' multiple times`,
1545
+ field: fieldApiKey
1546
+ });
1547
+ seenBlockIds.add(blockId);
1548
+ const rawBlock = input.blocks[blockId];
1549
+ if (typeof rawBlock !== "object" || rawBlock === null || Array.isArray(rawBlock)) return yield* new ValidationError({
1550
+ message: `Block '${blockId}' must be an object`,
1551
+ field: fieldApiKey
1552
+ });
1553
+ const blockData = rawBlock;
1554
+ if (typeof blockData._type !== "string" || blockData._type.length === 0) return yield* new ValidationError({
1555
+ message: `Block '${blockId}' must have a _type property`,
1556
+ field: fieldApiKey
1557
+ });
1558
+ if (allowedBlockTypes.length > 0 && !allowedBlockTypes.includes(blockData._type)) return yield* new ValidationError({
1559
+ message: `Block type '${blockData._type}' is not allowed in field '${fieldApiKey}'. Allowed: ${allowedBlockTypes.join(", ")}`,
1560
+ field: fieldApiKey
1561
+ });
1562
+ const blockModel = yield* getBlockModelSchema(sql, blockData._type);
1563
+ const row = {
1564
+ id: blockId,
1565
+ _root_record_id: ctx.rootRecordId,
1566
+ _root_field_api_key: ctx.rootFieldApiKey,
1567
+ _parent_container_model_api_key: container.parentContainerModelApiKey,
1568
+ _parent_block_id: container.parentBlockId,
1569
+ _parent_field_api_key: container.parentFieldApiKey,
1570
+ _depth: container.depth
1571
+ };
1572
+ const nestedRows = /* @__PURE__ */ new Map();
1573
+ for (const field of blockModel.fields) {
1574
+ const value = blockData[field.api_key];
1575
+ if (value === void 0) continue;
1576
+ if (field.field_type === "structured_text") {
1577
+ if (value === null) {
1578
+ row[field.api_key] = null;
1579
+ continue;
1580
+ }
1581
+ const nestedInput = yield* decodeStructuredTextInput(field.api_key, value);
1582
+ const nestedCompiled = yield* compileStructuredText(ctx, {
1583
+ parentContainerModelApiKey: blockModel.apiKey,
1584
+ parentBlockId: blockId,
1585
+ parentFieldApiKey: field.api_key,
1586
+ depth: container.depth + 1
1587
+ }, {
1588
+ fieldApiKey: field.api_key,
1589
+ input: nestedInput,
1590
+ allowedBlockTypes: getBlockWhitelist(field.validators) ?? [],
1591
+ blocksOnly: getBlocksOnly(field.validators)
1592
+ });
1593
+ row[field.api_key] = nestedCompiled.dast;
1594
+ mergeRowMaps(nestedRows, nestedCompiled.rowsByTable);
1595
+ continue;
1596
+ }
1597
+ if (isFieldType(field.field_type)) {
1598
+ const fieldDef = getFieldTypeDef(field.field_type);
1599
+ if (fieldDef.inputSchema) yield* Schema.decodeUnknown(fieldDef.inputSchema)(value).pipe(Effect.mapError((e) => new ValidationError({
1600
+ message: `Invalid ${field.field_type} for block field '${field.api_key}': ${e.message}`,
1601
+ field: field.api_key
1602
+ })));
1603
+ }
1604
+ row[field.api_key] = value;
1605
+ }
1606
+ const tableName = `block_${blockModel.apiKey}`;
1607
+ const rows = rowsByTable.get(tableName);
1608
+ if (rows) rows.push(row);
1609
+ else rowsByTable.set(tableName, [row]);
1610
+ mergeRowMaps(rowsByTable, nestedRows);
1611
+ }
1612
+ return {
1613
+ dast,
1614
+ rowsByTable
1615
+ };
1616
+ });
1617
+ }
1618
+ function insertCompiledRows(sql, rowsByTable) {
1619
+ return Effect.gen(function* () {
1620
+ for (const [tableName, rows] of rowsByTable) for (const row of rows) {
1621
+ const columns = Object.keys(row);
1622
+ const colList = columns.map((c) => `"${c}"`).join(", ");
1623
+ const placeholders = columns.map(() => "?").join(", ");
1624
+ const values = columns.map((c) => serializeValue(row[c]));
1625
+ yield* sql.unsafe(`INSERT INTO "${tableName}" (${colList}) VALUES (${placeholders})`, values);
1626
+ }
1627
+ });
1628
+ }
1629
+ function fetchBlockModels(sql) {
1630
+ return sql.unsafe("SELECT api_key FROM models WHERE is_block = 1 ORDER BY api_key");
1631
+ }
1632
+ function collectDescendantBlockIds(sql, startIds) {
1633
+ return Effect.gen(function* () {
1634
+ const blockModels = yield* fetchBlockModels(sql);
1635
+ const allIds = new Set(startIds);
1636
+ let frontier = [...startIds];
1637
+ while (frontier.length > 0) {
1638
+ const next = /* @__PURE__ */ new Set();
1639
+ const placeholders = frontier.map(() => "?").join(", ");
1640
+ for (const model of blockModels) {
1641
+ const rows = yield* sql.unsafe(`SELECT id FROM "block_${model.api_key}" WHERE _parent_block_id IN (${placeholders})`, frontier);
1642
+ for (const row of rows) if (!allIds.has(row.id)) {
1643
+ allIds.add(row.id);
1644
+ next.add(row.id);
1645
+ }
1646
+ }
1647
+ frontier = [...next];
1648
+ }
1649
+ return allIds;
1650
+ });
1651
+ }
1652
+ function writeStructuredText(params) {
1653
+ return Effect.gen(function* () {
1654
+ const sql = yield* SqlClient.SqlClient;
1655
+ const input = yield* decodeStructuredTextInput(params.fieldApiKey, {
1656
+ value: params.value,
1657
+ blocks: params.blocks ?? {}
1658
+ });
1659
+ const compiled = yield* compileStructuredText({
1660
+ sql,
1661
+ rootRecordId: params.rootRecordId,
1662
+ rootFieldApiKey: params.rootFieldStorageKey ?? params.fieldApiKey,
1663
+ rootModelApiKey: params.rootModelApiKey,
1664
+ seenBlockIds: /* @__PURE__ */ new Set()
1665
+ }, {
1666
+ parentContainerModelApiKey: params.rootModelApiKey,
1667
+ parentBlockId: null,
1668
+ parentFieldApiKey: params.fieldApiKey,
1669
+ depth: 0
1670
+ }, {
1671
+ fieldApiKey: params.fieldApiKey,
1672
+ input,
1673
+ allowedBlockTypes: params.allowedBlockTypes ?? [],
1674
+ blocksOnly: params.blocksOnly ?? false
1675
+ });
1676
+ yield* insertCompiledRows(sql, compiled.rowsByTable);
1677
+ return compiled.dast;
1678
+ });
1679
+ }
1680
+ function deleteBlocksForField(params) {
1681
+ return Effect.gen(function* () {
1682
+ const sql = yield* SqlClient.SqlClient;
1683
+ const blockModels = yield* fetchBlockModels(sql);
1684
+ for (const model of blockModels) if (params.includeLocalizedVariants) yield* sql.unsafe(`DELETE FROM "block_${model.api_key}"
1685
+ WHERE _root_record_id = ?
1686
+ AND (_root_field_api_key = ? OR _root_field_api_key LIKE ?)`, [
1687
+ params.rootRecordId,
1688
+ params.fieldApiKey,
1689
+ `${params.fieldApiKey}:%`
1690
+ ]);
1691
+ else yield* sql.unsafe(`DELETE FROM "block_${model.api_key}" WHERE _root_record_id = ? AND _root_field_api_key = ?`, [params.rootRecordId, params.fieldApiKey]);
1692
+ });
1693
+ }
1694
+ function deleteBlockSubtrees(params) {
1695
+ return Effect.gen(function* () {
1696
+ if (params.blockIds.length === 0) return;
1697
+ const sql = yield* SqlClient.SqlClient;
1698
+ const blockModels = yield* fetchBlockModels(sql);
1699
+ const ids = [...yield* collectDescendantBlockIds(sql, params.blockIds)];
1700
+ const placeholders = ids.map(() => "?").join(", ");
1701
+ for (const model of blockModels) yield* sql.unsafe(`DELETE FROM "block_${model.api_key}" WHERE id IN (${placeholders})`, ids);
1702
+ });
1703
+ }
1704
+ function parseMaterializeStructuredTextRequest(request) {
1705
+ const dast = decodeJsonIfString(request.rawValue);
1706
+ if (!dast || typeof dast !== "object") return null;
1707
+ if (!("document" in dast) || typeof dast.document !== "object" || dast.document === null || !("children" in dast.document)) return null;
1708
+ const doc = dast;
1709
+ const blockIds = extractAllBlockIds(doc);
1710
+ return {
1711
+ requestKey: request.requestKey,
1712
+ params: request,
1713
+ doc,
1714
+ blockIds,
1715
+ blockIdSet: new Set(blockIds)
1716
+ };
1717
+ }
1718
+ function getMaterializeBatchGroupKey(params) {
1719
+ const allowed = params.allowedBlockApiKeys?.join(",") ?? "*";
1720
+ const planKey = serializeMaterializePlan(params.selectedNestedFieldsPlan);
1721
+ return [
1722
+ params.parentContainerModelApiKey,
1723
+ params.parentFieldApiKey,
1724
+ params.rootFieldApiKey,
1725
+ params.parentBlockId === null ? "root" : "nested",
1726
+ allowed,
1727
+ planKey
1728
+ ].join(":");
1729
+ }
1730
+ function serializeMaterializePlan(plan) {
1731
+ if (!plan) return "*";
1732
+ return [...plan.fieldsByBlockApiKey.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([blockApiKey, fieldPlans]) => [blockApiKey, [...fieldPlans.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([fieldApiKey, nestedPlan]) => `${fieldApiKey}(${serializeMaterializePlan(nestedPlan)})`).join(",")]).map(([blockApiKey, nested]) => `${blockApiKey}[${nested}]`).join("|");
1733
+ }
1734
+ function buildMaterializeQueries(params) {
1735
+ const rootRecordPlaceholders = params.rootRecordIds.map(() => "?").join(", ");
1736
+ const blockPlaceholders = params.blockIds.map(() => "?").join(", ");
1737
+ const parentBlockPlaceholders = params.parentBlockIds.map(() => "?").join(", ");
1738
+ return params.blockModels.map((model) => {
1739
+ const payloadParts = model.fields.map((field) => `'${field.api_key}', "${field.api_key}"`).join(", ");
1740
+ return {
1741
+ sql: `SELECT id, _root_record_id, _root_field_api_key, _parent_block_id, '${model.apiKey}' AS __block_api_key, json_object(${payloadParts}) AS __payload
1742
+ FROM "block_${model.apiKey}"
1743
+ WHERE _root_record_id IN (${rootRecordPlaceholders})
1744
+ AND _root_field_api_key = ?
1745
+ AND _parent_container_model_api_key = ?
1746
+ AND _parent_field_api_key = ?
1747
+ AND ${params.parentBlockIds.length === 0 ? "_parent_block_id IS NULL" : `_parent_block_id IN (${parentBlockPlaceholders})`}
1748
+ AND id IN (${blockPlaceholders})`,
1749
+ params: [
1750
+ ...params.rootRecordIds,
1751
+ params.rootFieldApiKey,
1752
+ params.parentContainerModelApiKey,
1753
+ params.parentFieldApiKey,
1754
+ ...params.parentBlockIds.length === 0 ? [] : params.parentBlockIds,
1755
+ ...params.blockIds
1756
+ ]
1757
+ };
1758
+ });
1759
+ }
1760
+ function materializeStructuredTextValues(params) {
1761
+ return Effect.gen(function* () {
1762
+ const sql = yield* SqlClient.SqlClient;
1763
+ const materializeContext = params.materializeContext ?? {
1764
+ blockModelSchemas: /* @__PURE__ */ new Map(),
1765
+ candidateBlockModels: /* @__PURE__ */ new Map()
1766
+ };
1767
+ const results = /* @__PURE__ */ new Map();
1768
+ const parsedRequests = [];
1769
+ for (const request of params.requests) {
1770
+ const parsed = parseMaterializeStructuredTextRequest(request);
1771
+ if (!parsed) {
1772
+ results.set(request.requestKey, null);
1773
+ continue;
1774
+ }
1775
+ if (parsed.blockIds.length === 0) {
1776
+ results.set(parsed.requestKey, {
1777
+ value: parsed.doc,
1778
+ blocks: {}
1779
+ });
1780
+ continue;
1781
+ }
1782
+ parsedRequests.push(parsed);
1783
+ }
1784
+ if (parsedRequests.length === 0) return results;
1785
+ const blockModels = yield* fetchBlockModelsCached(materializeContext, sql);
1786
+ const requestsByGroup = /* @__PURE__ */ new Map();
1787
+ for (const request of parsedRequests) {
1788
+ const groupKey = getMaterializeBatchGroupKey(request.params);
1789
+ const group = requestsByGroup.get(groupKey);
1790
+ if (group) group.push(request);
1791
+ else requestsByGroup.set(groupKey, [request]);
1792
+ results.set(request.requestKey, {
1793
+ value: request.doc,
1794
+ blocks: {}
1795
+ });
1796
+ }
1797
+ const nestedRequests = [];
1798
+ const nestedAssignments = [];
1799
+ for (const requests of requestsByGroup.values()) {
1800
+ const sample = requests[0];
1801
+ if (!sample) continue;
1802
+ const requestByParentKey = /* @__PURE__ */ new Map();
1803
+ const requestBlockIds = /* @__PURE__ */ new Map();
1804
+ const allBlockIds = /* @__PURE__ */ new Set();
1805
+ const rootRecordIds = /* @__PURE__ */ new Set();
1806
+ const parentBlockIds = /* @__PURE__ */ new Set();
1807
+ for (const request of requests) {
1808
+ const parentKey = `${request.params.rootRecordId}:${request.params.parentBlockId ?? "root"}`;
1809
+ requestByParentKey.set(parentKey, request);
1810
+ requestBlockIds.set(request.requestKey, request.blockIdSet);
1811
+ rootRecordIds.add(request.params.rootRecordId);
1812
+ if (request.params.parentBlockId !== null) parentBlockIds.add(request.params.parentBlockId);
1813
+ for (const blockId of request.blockIds) allBlockIds.add(blockId);
1814
+ }
1815
+ const candidateBlockModels = getCandidateBlockModelsCached(materializeContext, blockModels, sample.params.allowedBlockApiKeys);
1816
+ const blockModelSchemas = yield* Effect.all(candidateBlockModels.map((model) => getBlockModelSchemaCached(materializeContext, sql, model.api_key)), { concurrency: "unbounded" });
1817
+ const blockModelByApiKey = new Map(blockModelSchemas.map((model) => [model.apiKey, model]));
1818
+ const rootRecordIdList = [...rootRecordIds];
1819
+ const blockIds = [...allBlockIds];
1820
+ const parentBlockIdList = [...parentBlockIds];
1821
+ const rows = (yield* runHotBlockQueries(buildMaterializeQueries({
1822
+ blockModels: blockModelSchemas,
1823
+ rootRecordIds: rootRecordIdList,
1824
+ rootFieldApiKey: sample.params.rootFieldApiKey,
1825
+ parentContainerModelApiKey: sample.params.parentContainerModelApiKey,
1826
+ parentFieldApiKey: sample.params.parentFieldApiKey,
1827
+ parentBlockIds: parentBlockIdList,
1828
+ blockIds
1829
+ }))).flat();
1830
+ if (rows.length === 0) continue;
1831
+ for (const row of rows) {
1832
+ const rootRecordId = String(row._root_record_id);
1833
+ const parentBlockId = typeof row._parent_block_id === "string" ? row._parent_block_id : "root";
1834
+ const request = requestByParentKey.get(`${rootRecordId}:${parentBlockId}`);
1835
+ if (!request) continue;
1836
+ const allowedBlockIds = requestBlockIds.get(request.requestKey);
1837
+ const rowId = String(row.id);
1838
+ if (!allowedBlockIds?.has(rowId)) continue;
1839
+ const blockApiKey = typeof row.__block_api_key === "string" ? row.__block_api_key : null;
1840
+ if (!blockApiKey) continue;
1841
+ const blockModel = blockModelByApiKey.get(blockApiKey);
1842
+ if (!blockModel) continue;
1843
+ const rawPayload = decodeJsonIfString(row.__payload);
1844
+ if (typeof rawPayload !== "object" || rawPayload === null || Array.isArray(rawPayload)) continue;
1845
+ const payload = { _type: blockApiKey };
1846
+ const selectedFieldPlans = request.params.selectedNestedFieldsPlan?.fieldsByBlockApiKey.get(blockApiKey);
1847
+ for (const field of blockModel.fields) {
1848
+ const rawValue = deserializeValue(Reflect.get(rawPayload, field.api_key));
1849
+ if (rawValue === void 0) continue;
1850
+ if (field.field_type === "structured_text" && rawValue !== null) {
1851
+ const nestedPlan = selectedFieldPlans?.get(field.api_key);
1852
+ if (request.params.selectedNestedFieldsPlan && !nestedPlan) continue;
1853
+ const requestKey = `nested:${nestedAssignments.length}`;
1854
+ nestedRequests.push({
1855
+ requestKey,
1856
+ materializeContext,
1857
+ allowedBlockApiKeys: blockModel.structuredTextAllowedBlockApiKeysByField.get(field.api_key) ?? [],
1858
+ selectedNestedFieldsPlan: nestedPlan,
1859
+ parentContainerModelApiKey: blockApiKey,
1860
+ parentBlockId: rowId,
1861
+ parentFieldApiKey: field.api_key,
1862
+ rootRecordId,
1863
+ rootFieldApiKey: String(row._root_field_api_key),
1864
+ rawValue
1865
+ });
1866
+ nestedAssignments.push({
1867
+ requestKey,
1868
+ target: payload,
1869
+ fieldApiKey: field.api_key
1870
+ });
1871
+ continue;
1872
+ }
1873
+ payload[field.api_key] = rawValue;
1874
+ }
1875
+ const envelope = results.get(request.requestKey);
1876
+ if (envelope) envelope.blocks[rowId] = payload;
1877
+ }
1878
+ }
1879
+ if (nestedRequests.length > 0) {
1880
+ const nestedResults = yield* materializeStructuredTextValues({
1881
+ materializeContext,
1882
+ requests: nestedRequests
1883
+ });
1884
+ for (const assignment of nestedAssignments) assignment.target[assignment.fieldApiKey] = nestedResults.get(assignment.requestKey) ?? null;
1885
+ }
1886
+ return results;
1887
+ });
1888
+ }
1889
+ function materializeStructuredTextValue(params) {
1890
+ return Effect.gen(function* () {
1891
+ return (yield* materializeStructuredTextValues({
1892
+ materializeContext: params.materializeContext,
1893
+ requests: [{
1894
+ requestKey: "single",
1895
+ ...params
1896
+ }]
1897
+ })).get("single") ?? null;
1898
+ });
1899
+ }
1900
+ function materializeRecordStructuredTextFields(params) {
1901
+ return Effect.gen(function* () {
1902
+ const materialized = { ...params.record };
1903
+ for (const field of params.fields) {
1904
+ if (field.field_type !== "structured_text") continue;
1905
+ const rawValue = params.record[field.api_key];
1906
+ if (rawValue === null || rawValue === void 0) continue;
1907
+ const materializeContext = {
1908
+ blockModelSchemas: /* @__PURE__ */ new Map(),
1909
+ candidateBlockModels: /* @__PURE__ */ new Map()
1910
+ };
1911
+ if (field.localized) {
1912
+ const localeMap = decodeJsonIfString(rawValue);
1913
+ if (typeof localeMap !== "object" || localeMap === null || Array.isArray(localeMap)) continue;
1914
+ const localized = {};
1915
+ for (const [localeCode, localeValue] of Object.entries(localeMap)) {
1916
+ if (localeValue === null || localeValue === void 0) {
1917
+ localized[localeCode] = localeValue;
1918
+ continue;
1919
+ }
1920
+ localized[localeCode] = yield* materializeStructuredTextValue({
1921
+ allowedBlockApiKeys: getBlockWhitelist(field.validators) ?? [],
1922
+ parentContainerModelApiKey: params.modelApiKey,
1923
+ materializeContext,
1924
+ parentBlockId: null,
1925
+ parentFieldApiKey: field.api_key,
1926
+ rootRecordId: String(params.record.id),
1927
+ rootFieldApiKey: getStructuredTextStorageKey(field.api_key, localeCode),
1928
+ rawValue: localeValue
1929
+ });
1930
+ }
1931
+ materialized[field.api_key] = localized;
1932
+ continue;
1933
+ }
1934
+ const envelope = yield* materializeStructuredTextValue({
1935
+ allowedBlockApiKeys: getBlockWhitelist(field.validators) ?? [],
1936
+ parentContainerModelApiKey: params.modelApiKey,
1937
+ materializeContext,
1938
+ parentBlockId: null,
1939
+ parentFieldApiKey: field.api_key,
1940
+ rootRecordId: String(params.record.id),
1941
+ rootFieldApiKey: field.api_key,
1942
+ rawValue
1943
+ });
1944
+ materialized[field.api_key] = envelope;
1945
+ }
1946
+ return materialized;
1947
+ });
1948
+ }
1949
+ //#endregion
1950
+ export { isSearchable as A, encodeJson as B, findUniqueConstraintViolations as C, getLinksTargets as D, getLinkTargets as E, parseMediaGalleryReferences as F, ReferenceConflictError as G, getFieldTypeDef as H, decodeJsonIfString as I, ValidationError as J, SchemaEngineError as K, decodeJsonRecordStringOr as L, supportsUniqueValidation as M, mergeAssetWithMediaReference as N, getSlugSource as O, parseMediaFieldReference as P, decodeJsonString as R, computeIsValid as S, getBlocksOnly as T, DuplicateError as U, FIELD_TYPE_REGISTRY as V, NotFoundError as W, isCmsError as X, errorToResponse as Y, extractLinkIds as _, materializeStructuredTextValue as a, isContentRow as b, FIELD_TYPES as c, getSqlMetrics as d, recordSqlMetrics as f, extractInlineBlockIds as g, extractBlockIds as h, materializeRecordStructuredTextFields as i, isUnique as j, isRequired as k, isFieldType as l, markdownToDast as m, deleteBlocksForField as n, materializeStructuredTextValues as o, withSqlMetrics as p, UnauthorizedError as q, getStructuredTextStorageKey as r, writeStructuredText as s, deleteBlockSubtrees as t, runBatchedQueries as u, pruneBlockNodes as v, getBlockWhitelist as w, parseFieldValidators as x, StructuredTextWriteInput as y, decodeJsonStringOr as z };
1951
+
1952
+ //# sourceMappingURL=structured-text-service-B4xSlUg_.mjs.map