@bobbykim/manguito-cms-api 0.2.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,6 +30,931 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
32
 
33
+ // src/relations.ts
34
+ function quoteIdent(name) {
35
+ if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
36
+ return `"${name}"`;
37
+ }
38
+ function paragraphMediaFieldNames(pType) {
39
+ return new Set(
40
+ pType.fields.filter((f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file").map((f) => f.name)
41
+ );
42
+ }
43
+ async function fetchParagraphMediaIds(db, pType, itemId, fieldName) {
44
+ const mediaColumns = pType.fields.filter((f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file").map((f) => f.db_column.column_name);
45
+ if (mediaColumns.length === 0) return [];
46
+ const tbl = import_drizzle_orm2.sql.raw(quoteIdent(pType.db.table_name));
47
+ const cols = import_drizzle_orm2.sql.join(mediaColumns.map((c) => import_drizzle_orm2.sql.raw(quoteIdent(c))), import_drizzle_orm2.sql`, `);
48
+ const result = await db.execute(
49
+ import_drizzle_orm2.sql`SELECT ${cols} FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`
50
+ );
51
+ const ids = [];
52
+ for (const row of result.rows) {
53
+ for (const col of mediaColumns) {
54
+ const v = row[col];
55
+ if (typeof v === "string" && v !== "") ids.push(v);
56
+ }
57
+ }
58
+ return ids;
59
+ }
60
+ function nestedParagraphFields(pType, registry) {
61
+ if (!registry) return [];
62
+ const out = [];
63
+ for (const f of pType.fields) {
64
+ if (f.db_column !== null) continue;
65
+ const comp = f.ui_component;
66
+ if (comp.component !== "paragraph-embed" || !comp.ref) continue;
67
+ const nType = registry.paragraph_types[comp.ref];
68
+ if (nType) out.push({ fieldName: f.name, nType });
69
+ }
70
+ return out;
71
+ }
72
+ async function fetchParagraphRowIds(db, pType, itemId, fieldName) {
73
+ const tbl = import_drizzle_orm2.sql.raw(quoteIdent(pType.db.table_name));
74
+ const result = await db.execute(
75
+ import_drizzle_orm2.sql`SELECT id FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`
76
+ );
77
+ return result.rows.map((r) => r["id"]).filter((id) => typeof id === "string");
78
+ }
79
+ async function persistParagraphField(db, itemId, parentType, fieldName, pType, items, registry) {
80
+ const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName);
81
+ const nested = nestedParagraphFields(pType, registry);
82
+ if (nested.length > 0) {
83
+ const staleIds = await fetchParagraphRowIds(db, pType, itemId, fieldName);
84
+ for (const rowId of staleIds) {
85
+ for (const n of nested) {
86
+ const d = await deleteParagraphField(db, rowId, n.fieldName, n.nType, registry);
87
+ removed.push(...d.removed);
88
+ }
89
+ }
90
+ }
91
+ const tbl = import_drizzle_orm2.sql.raw(quoteIdent(pType.db.table_name));
92
+ await db.execute(import_drizzle_orm2.sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
93
+ const mediaFieldNames = paragraphMediaFieldNames(pType);
94
+ const added = [];
95
+ for (let i = 0; i < items.length; i++) {
96
+ const pItem = items[i];
97
+ const rowId = import_node_crypto.default.randomUUID();
98
+ const data = {
99
+ id: rowId,
100
+ parent_id: itemId,
101
+ parent_type: parentType,
102
+ parent_field: fieldName,
103
+ order: i,
104
+ created_at: /* @__PURE__ */ new Date(),
105
+ updated_at: /* @__PURE__ */ new Date()
106
+ };
107
+ for (const pf of pType.fields) {
108
+ if (!pf.db_column || pf.db_column.junction) continue;
109
+ data[pf.db_column.column_name] = pItem[pf.name] ?? null;
110
+ }
111
+ const cols = import_drizzle_orm2.sql.join(Object.keys(data).map((k) => import_drizzle_orm2.sql.raw(quoteIdent(k))), import_drizzle_orm2.sql`, `);
112
+ const vals = import_drizzle_orm2.sql.join(Object.values(data).map((v) => import_drizzle_orm2.sql`${v}`), import_drizzle_orm2.sql`, `);
113
+ await db.execute(import_drizzle_orm2.sql`INSERT INTO ${tbl} (${cols}) VALUES (${vals})`);
114
+ for (const name of mediaFieldNames) {
115
+ const val = pItem[name];
116
+ if (typeof val === "string" && val !== "") added.push(val);
117
+ }
118
+ for (const n of nested) {
119
+ const nestedItems = Array.isArray(pItem[n.fieldName]) ? pItem[n.fieldName] : [];
120
+ const d = await persistParagraphField(
121
+ db,
122
+ rowId,
123
+ pType.db.table_name,
124
+ n.fieldName,
125
+ n.nType,
126
+ nestedItems,
127
+ registry
128
+ );
129
+ added.push(...d.added);
130
+ removed.push(...d.removed);
131
+ }
132
+ }
133
+ return { added, removed };
134
+ }
135
+ async function deleteParagraphField(db, itemId, fieldName, pType, registry) {
136
+ const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName);
137
+ const nested = nestedParagraphFields(pType, registry);
138
+ if (nested.length > 0) {
139
+ const rowIds = await fetchParagraphRowIds(db, pType, itemId, fieldName);
140
+ for (const rowId of rowIds) {
141
+ for (const n of nested) {
142
+ const d = await deleteParagraphField(db, rowId, n.fieldName, n.nType, registry);
143
+ removed.push(...d.removed);
144
+ }
145
+ }
146
+ }
147
+ const tbl = import_drizzle_orm2.sql.raw(quoteIdent(pType.db.table_name));
148
+ await db.execute(import_drizzle_orm2.sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
149
+ return { added: [], removed };
150
+ }
151
+ async function persistJunctionField(db, itemId, junction, relatedIds) {
152
+ const tbl = import_drizzle_orm2.sql.raw(quoteIdent(junction.table_name));
153
+ const left = import_drizzle_orm2.sql.raw(quoteIdent(junction.left_column));
154
+ const right = import_drizzle_orm2.sql.raw(quoteIdent(junction.right_column));
155
+ await db.execute(import_drizzle_orm2.sql`DELETE FROM ${tbl} WHERE ${left} = ${itemId}`);
156
+ for (const rid of relatedIds) {
157
+ await db.execute(import_drizzle_orm2.sql`INSERT INTO ${tbl} (${left}, ${right}) VALUES (${itemId}, ${rid})`);
158
+ }
159
+ }
160
+ function buildRelationsMap(fields, registry) {
161
+ const relations = {};
162
+ for (const field of fields) {
163
+ if (field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
164
+ if (!field.db_column) continue;
165
+ relations[field.name] = { type: "media", fk_column: field.db_column.column_name };
166
+ } else if (field.field_type === "paragraph") {
167
+ const ref = field.ui_component.ref;
168
+ const pType = ref ? registry.paragraph_types[ref] : void 0;
169
+ if (!pType) continue;
170
+ relations[field.name] = { type: "paragraph", table: pType.db.table_name };
171
+ } else if (field.field_type === "reference") {
172
+ if (!field.db_column) continue;
173
+ if (field.db_column.junction) {
174
+ const j = field.db_column.junction;
175
+ relations[field.name] = {
176
+ type: "junction",
177
+ table: j.right_table,
178
+ junction_table: j.table_name,
179
+ left_column: j.left_column,
180
+ right_column: j.right_column,
181
+ order_column: j.order_column
182
+ };
183
+ } else if (field.db_column.foreign_key) {
184
+ relations[field.name] = {
185
+ type: "reference",
186
+ table: field.db_column.foreign_key.table,
187
+ fk_column: field.db_column.column_name
188
+ };
189
+ }
190
+ }
191
+ }
192
+ return relations;
193
+ }
194
+ function groupBy(items, key) {
195
+ const result = {};
196
+ for (const item of items) {
197
+ const k = String(item[key]);
198
+ if (!result[k]) result[k] = [];
199
+ result[k].push(item);
200
+ }
201
+ return result;
202
+ }
203
+ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOnly = false) {
204
+ if (rows.length === 0) return;
205
+ if (rel.type === "paragraph") {
206
+ const parentIds = rows.map((r) => r["id"]);
207
+ const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
208
+ const result = await db.execute(
209
+ import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY "order" ASC`
210
+ );
211
+ const byParent = groupBy(result.rows, "parent_id");
212
+ for (const row of rows) {
213
+ row[fieldName] = byParent[row["id"]] ?? [];
214
+ }
215
+ } else if (rel.type === "reference") {
216
+ const fkValues = rows.map((r) => r[rel.fk_column]).filter(Boolean);
217
+ if (fkValues.length === 0) {
218
+ for (const row of rows) row[fieldName] = null;
219
+ return;
220
+ }
221
+ const unique = [...new Set(fkValues)];
222
+ const uncached = unique.filter((id) => !cache.has(`${rel.table}:${id}`));
223
+ if (uncached.length > 0) {
224
+ const inList = import_drizzle_orm2.sql.join(uncached.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
225
+ const publishedCond = publishedOnly ? import_drizzle_orm2.sql` AND published = true` : import_drizzle_orm2.sql``;
226
+ const result = await db.execute(
227
+ import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE id IN (${inList})${publishedCond}`
228
+ );
229
+ for (const item of result.rows) {
230
+ cache.set(`${rel.table}:${item["id"]}`, item);
231
+ }
232
+ }
233
+ for (const row of rows) {
234
+ const fkVal = row[rel.fk_column];
235
+ row[fieldName] = fkVal ? cache.get(`${rel.table}:${fkVal}`) ?? null : null;
236
+ }
237
+ } else if (rel.type === "junction") {
238
+ const parentIds = rows.map((r) => r["id"]);
239
+ const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
240
+ const orderBy = rel.order_column ? import_drizzle_orm2.sql` ORDER BY "order" ASC` : import_drizzle_orm2.sql``;
241
+ const junctionResult = await db.execute(
242
+ import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.junction_table))} WHERE ${import_drizzle_orm2.sql.raw(quoteIdent(rel.left_column))} IN (${inList})${orderBy}`
243
+ );
244
+ const jRows = junctionResult.rows;
245
+ const rightIds = [...new Set(jRows.map((r) => r[rel.right_column]))];
246
+ const uncached = rightIds.filter((id) => !cache.has(`${rel.table}:${id}`));
247
+ if (uncached.length > 0) {
248
+ const rightInList = import_drizzle_orm2.sql.join(uncached.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
249
+ const publishedCond = publishedOnly ? import_drizzle_orm2.sql` AND published = true` : import_drizzle_orm2.sql``;
250
+ const entitiesResult = await db.execute(
251
+ import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE id IN (${rightInList})${publishedCond}`
252
+ );
253
+ for (const item of entitiesResult.rows) {
254
+ cache.set(`${rel.table}:${item["id"]}`, item);
255
+ }
256
+ }
257
+ const byLeft = groupBy(jRows, rel.left_column);
258
+ for (const row of rows) {
259
+ const jEntries = byLeft[row["id"]] ?? [];
260
+ row[fieldName] = jEntries.map((jr) => cache.get(`${rel.table}:${jr[rel.right_column]}`)).filter(Boolean);
261
+ }
262
+ } else if (rel.type === "media") {
263
+ const fkValues = rows.map((r) => r[rel.fk_column]).filter(Boolean);
264
+ if (fkValues.length === 0) {
265
+ for (const row of rows) row[fieldName] = null;
266
+ return;
267
+ }
268
+ const unique = [...new Set(fkValues)];
269
+ const uncached = unique.filter((id) => !cache.has(`media:${id}`));
270
+ if (uncached.length > 0) {
271
+ const inList = import_drizzle_orm2.sql.join(uncached.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
272
+ const result = await db.execute(import_drizzle_orm2.sql`SELECT * FROM "media" WHERE id IN (${inList})`);
273
+ for (const item of result.rows) {
274
+ cache.set(`media:${item["id"]}`, item);
275
+ }
276
+ }
277
+ for (const row of rows) {
278
+ const fkVal = row[rel.fk_column];
279
+ row[fieldName] = fkVal ? cache.get(`media:${fkVal}`) ?? null : null;
280
+ }
281
+ }
282
+ }
283
+ async function resolveRelationBareIds(db, rows, fieldName, rel) {
284
+ if (rows.length === 0) return;
285
+ if (rel.type === "paragraph") {
286
+ const parentIds = rows.map((r) => r["id"]);
287
+ const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
288
+ const result = await db.execute(
289
+ import_drizzle_orm2.sql`SELECT id, parent_id FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY "order" ASC`
290
+ );
291
+ const byParent = groupBy(result.rows, "parent_id");
292
+ for (const row of rows) {
293
+ row[fieldName] = (byParent[row["id"]] ?? []).map((r) => r["id"]);
294
+ }
295
+ } else if (rel.type === "junction") {
296
+ const parentIds = rows.map((r) => r["id"]);
297
+ const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
298
+ const result = await db.execute(
299
+ import_drizzle_orm2.sql`SELECT ${import_drizzle_orm2.sql.raw(quoteIdent(rel.left_column))} AS left_id, ${import_drizzle_orm2.sql.raw(quoteIdent(rel.right_column))} AS right_id FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.junction_table))} WHERE ${import_drizzle_orm2.sql.raw(quoteIdent(rel.left_column))} IN (${inList})`
300
+ );
301
+ const byLeft = groupBy(result.rows, "left_id");
302
+ for (const row of rows) {
303
+ row[fieldName] = (byLeft[row["id"]] ?? []).map((r) => r["right_id"]);
304
+ }
305
+ }
306
+ }
307
+ var import_node_crypto, import_drizzle_orm2;
308
+ var init_relations = __esm({
309
+ "src/relations.ts"() {
310
+ "use strict";
311
+ import_node_crypto = __toESM(require("crypto"));
312
+ import_drizzle_orm2 = require("drizzle-orm");
313
+ }
314
+ });
315
+
316
+ // src/graphql/scalars.ts
317
+ var import_graphql, DateTimeScalar, JSONScalar;
318
+ var init_scalars = __esm({
319
+ "src/graphql/scalars.ts"() {
320
+ "use strict";
321
+ import_graphql = require("graphql");
322
+ DateTimeScalar = new import_graphql.GraphQLScalarType({
323
+ name: "DateTime",
324
+ description: "ISO-8601 date-time string",
325
+ serialize(value) {
326
+ if (value instanceof Date) return value.toISOString();
327
+ if (typeof value === "string") return value;
328
+ if (typeof value === "number") return new Date(value).toISOString();
329
+ throw new TypeError(`DateTime cannot serialize value: ${String(value)}`);
330
+ },
331
+ parseValue(value) {
332
+ if (typeof value !== "string") throw new TypeError("DateTime must be a string");
333
+ return value;
334
+ },
335
+ parseLiteral(ast) {
336
+ if (ast.kind !== import_graphql.Kind.STRING) throw new TypeError("DateTime must be a string");
337
+ return ast.value;
338
+ }
339
+ });
340
+ JSONScalar = new import_graphql.GraphQLScalarType({
341
+ name: "JSON",
342
+ description: "Arbitrary JSON value",
343
+ serialize: (value) => value,
344
+ parseValue: (value) => value,
345
+ parseLiteral: (ast, variables) => (0, import_graphql.valueFromASTUntyped)(ast, variables ?? void 0)
346
+ });
347
+ }
348
+ });
349
+
350
+ // src/graphql/type-mapping.ts
351
+ function scalarOutputType(fieldType) {
352
+ switch (fieldType) {
353
+ case "text/plain":
354
+ case "text/rich":
355
+ return import_graphql2.GraphQLString;
356
+ case "integer":
357
+ return import_graphql2.GraphQLInt;
358
+ case "float":
359
+ return import_graphql2.GraphQLFloat;
360
+ case "boolean":
361
+ return import_graphql2.GraphQLBoolean;
362
+ case "date":
363
+ return DateTimeScalar;
364
+ case "programmatic":
365
+ return JSONScalar;
366
+ default:
367
+ return null;
368
+ }
369
+ }
370
+ var import_graphql2;
371
+ var init_type_mapping = __esm({
372
+ "src/graphql/type-mapping.ts"() {
373
+ "use strict";
374
+ import_graphql2 = require("graphql");
375
+ init_scalars();
376
+ }
377
+ });
378
+
379
+ // src/graphql/naming.ts
380
+ function schemaSegment(machineName) {
381
+ const idx = machineName.indexOf("--");
382
+ return idx === -1 ? machineName : machineName.slice(idx + 2);
383
+ }
384
+ function words(input) {
385
+ return input.split(/[_\s-]+/).filter(Boolean);
386
+ }
387
+ function toPascalCase(input) {
388
+ return words(input).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
389
+ }
390
+ function toCamelCase(input) {
391
+ const pascal = toPascalCase(input);
392
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
393
+ }
394
+ function pluralize(word) {
395
+ if (/[^aeiou]y$/.test(word)) return word.slice(0, -1) + "ies";
396
+ if (/(s|x|z|ch|sh)$/.test(word)) return word + "es";
397
+ return word + "s";
398
+ }
399
+ function isValidGraphQLName(name) {
400
+ return GRAPHQL_NAME.test(name);
401
+ }
402
+ function graphqlTypeName(machineName) {
403
+ return toPascalCase(schemaSegment(machineName));
404
+ }
405
+ function singleQueryName(machineName) {
406
+ return toCamelCase(schemaSegment(machineName));
407
+ }
408
+ function collectionQueryName(machineName) {
409
+ return pluralize(singleQueryName(machineName));
410
+ }
411
+ function buildFieldNameMap(schemaNames) {
412
+ const toG = /* @__PURE__ */ new Map();
413
+ const toS = /* @__PURE__ */ new Map();
414
+ for (const name of schemaNames) {
415
+ const g = toCamelCase(name);
416
+ toG.set(name, g);
417
+ toS.set(g, name);
418
+ }
419
+ return {
420
+ toGraphql: (schemaName) => toG.get(schemaName) ?? toCamelCase(schemaName),
421
+ toSchema: (graphqlName) => toS.get(graphqlName) ?? graphqlName
422
+ };
423
+ }
424
+ var GRAPHQL_NAME;
425
+ var init_naming = __esm({
426
+ "src/graphql/naming.ts"() {
427
+ "use strict";
428
+ GRAPHQL_NAME = /^[_A-Za-z][_0-9A-Za-z]*$/;
429
+ }
430
+ });
431
+
432
+ // src/graphql/filters.ts
433
+ function buildSortFieldEnum(typeName) {
434
+ return new import_graphql3.GraphQLEnumType({
435
+ name: `${typeName}SortField`,
436
+ values: Object.fromEntries(SORTABLE.map((s) => [s.gql, { value: s.column }]))
437
+ });
438
+ }
439
+ function comparableFilterInput(name, scalar) {
440
+ return new import_graphql3.GraphQLInputObjectType({
441
+ name,
442
+ fields: {
443
+ eq: { type: scalar },
444
+ in: { type: new import_graphql3.GraphQLList(new import_graphql3.GraphQLNonNull(scalar)) },
445
+ gt: { type: scalar },
446
+ gte: { type: scalar },
447
+ lt: { type: scalar },
448
+ lte: { type: scalar }
449
+ }
450
+ });
451
+ }
452
+ function equalityFilterInput(name, scalar) {
453
+ return new import_graphql3.GraphQLInputObjectType({
454
+ name,
455
+ fields: {
456
+ eq: { type: scalar },
457
+ in: { type: new import_graphql3.GraphQLList(new import_graphql3.GraphQLNonNull(scalar)) }
458
+ }
459
+ });
460
+ }
461
+ function filterInputForField(fieldType) {
462
+ const scalar = scalarOutputType(fieldType);
463
+ if (fieldType === "boolean") return BooleanFilter;
464
+ if (fieldType === "date") return DateTimeFilter;
465
+ if (fieldType === "integer") return IntFilter;
466
+ if (fieldType === "float") return FloatFilter;
467
+ if (fieldType === "reference") return IDFilter;
468
+ if (scalar === null) return null;
469
+ return StringFilter;
470
+ }
471
+ function buildFilterInputType(type) {
472
+ const fields = {};
473
+ fields["id"] = { type: IDFilter };
474
+ fields["published"] = { type: BooleanFilter };
475
+ fields["createdAt"] = { type: DateTimeFilter };
476
+ fields["updatedAt"] = { type: DateTimeFilter };
477
+ if (type.schema_type === "content-type") fields["slug"] = { type: StringFilter };
478
+ for (const f of type.fields) {
479
+ if (f.field_type === "programmatic" || f.field_type === "paragraph") continue;
480
+ if (f.field_type === "image" || f.field_type === "video" || f.field_type === "file") continue;
481
+ const input = filterInputForField(f.field_type);
482
+ if (input) fields[buildFieldNameMap([f.name]).toGraphql(f.name)] = { type: input };
483
+ }
484
+ if (Object.keys(fields).length === 0) return null;
485
+ return new import_graphql3.GraphQLInputObjectType({ name: `${graphqlTypeName(type.name)}Filter`, fields });
486
+ }
487
+ function translateFilters(input, nameMap) {
488
+ const out = {};
489
+ if (!input) return out;
490
+ for (const [gqlField, raw] of Object.entries(input)) {
491
+ if (raw === null || typeof raw !== "object") continue;
492
+ const column = nameMap.toSchema(gqlField);
493
+ const spec = raw;
494
+ if (spec["eq"] !== void 0) {
495
+ out[column] = spec["eq"];
496
+ } else if (Array.isArray(spec["in"])) {
497
+ out[column] = spec["in"];
498
+ } else {
499
+ const op = {};
500
+ for (const k of ["gt", "gte", "lt", "lte"]) {
501
+ if (spec[k] !== void 0) op[k] = spec[k];
502
+ }
503
+ if (Object.keys(op).length > 0) out[column] = op;
504
+ }
505
+ }
506
+ return out;
507
+ }
508
+ var import_graphql3, SortOrderEnum, SORTABLE, StringFilter, BooleanFilter, DateTimeFilter, IDFilter, IntFilter, FloatFilter;
509
+ var init_filters = __esm({
510
+ "src/graphql/filters.ts"() {
511
+ "use strict";
512
+ import_graphql3 = require("graphql");
513
+ init_naming();
514
+ init_type_mapping();
515
+ init_scalars();
516
+ SortOrderEnum = new import_graphql3.GraphQLEnumType({
517
+ name: "SortOrder",
518
+ values: { ASC: { value: "asc" }, DESC: { value: "desc" } }
519
+ });
520
+ SORTABLE = [
521
+ { gql: "title", column: "title" },
522
+ { gql: "createdAt", column: "created_at" },
523
+ { gql: "updatedAt", column: "updated_at" }
524
+ ];
525
+ StringFilter = equalityFilterInput("StringFilter", import_graphql3.GraphQLString);
526
+ BooleanFilter = new import_graphql3.GraphQLInputObjectType({
527
+ name: "BooleanFilter",
528
+ fields: { eq: { type: import_graphql3.GraphQLBoolean } }
529
+ });
530
+ DateTimeFilter = comparableFilterInput("DateTimeFilter", DateTimeScalar);
531
+ IDFilter = equalityFilterInput("IDFilter", import_graphql3.GraphQLString);
532
+ IntFilter = comparableFilterInput("IntFilter", import_graphql3.GraphQLInt);
533
+ FloatFilter = comparableFilterInput("FloatFilter", import_graphql3.GraphQLFloat);
534
+ }
535
+ });
536
+
537
+ // src/graphql/resolvers.ts
538
+ function codeError2(code, message) {
539
+ return new import_graphql4.GraphQLError(message, { extensions: { code } });
540
+ }
541
+ function isPublished2(item) {
542
+ return !!item && item["published"] === true;
543
+ }
544
+ function scalarFieldResolver(schemaName) {
545
+ return (parent) => parent[schemaName];
546
+ }
547
+ function relationFieldResolver(typeName, schemaFieldName) {
548
+ return (parent, _args, ctx) => ctx.loaders.load(typeName, schemaFieldName, parent);
549
+ }
550
+ function programmaticFieldResolver(typeName, schemaFieldName, mediaFieldNames = []) {
551
+ return async (parent, _args, ctx) => {
552
+ let p = ctx.programmaticMemo.get(parent);
553
+ if (!p) {
554
+ p = resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames);
555
+ ctx.programmaticMemo.set(parent, p);
556
+ }
557
+ return (await p)[schemaFieldName];
558
+ };
559
+ }
560
+ async function resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames) {
561
+ if (mediaFieldNames.length === 0) return ctx.resolver.resolveItem(typeName, parent);
562
+ const enriched = { ...parent };
563
+ await Promise.all(mediaFieldNames.map((name) => ctx.loaders.load(typeName, name, enriched)));
564
+ return ctx.resolver.resolveItem(typeName, enriched);
565
+ }
566
+ function collectionResolver(typeName, nameMap) {
567
+ return async (_root, args, ctx) => {
568
+ const page = args.page ?? 1;
569
+ const perPage = args.perPage ?? 10;
570
+ if (!Number.isInteger(page) || page < 1) {
571
+ throw codeError2("INVALID_PAGINATION", "page must be \u2265 1");
572
+ }
573
+ if (!Number.isInteger(perPage) || perPage < 1 || perPage > 100) {
574
+ throw codeError2("INVALID_PAGINATION", "perPage must be between 1 and 100");
575
+ }
576
+ const repo = ctx.repos[typeName];
577
+ const result = await repo.findMany({
578
+ published_only: true,
579
+ page,
580
+ per_page: perPage,
581
+ sort_by: args.sortBy ?? "created_at",
582
+ sort_order: args.sortOrder ?? "asc",
583
+ filters: translateFilters(args.filter, nameMap)
584
+ });
585
+ return { data: result.data, meta: result.meta };
586
+ };
587
+ }
588
+ function singleBySlugResolver(typeName) {
589
+ return async (_root, args, ctx) => {
590
+ const repo = ctx.repos[typeName];
591
+ const item = await repo.findBySlug(args.slug);
592
+ return isPublished2(item) ? item : null;
593
+ };
594
+ }
595
+ function singletonResolver(typeName) {
596
+ return async (_root, _args, ctx) => {
597
+ const repo = ctx.repos[typeName];
598
+ const result = await repo.findMany({ published_only: true, page: 1, per_page: 1 });
599
+ return result.data[0] ?? null;
600
+ };
601
+ }
602
+ function taxonomySingleResolver(typeName) {
603
+ return async (_root, args, ctx) => {
604
+ const repo = ctx.repos[typeName];
605
+ const item = await repo.findOne(args.id);
606
+ return isPublished2(item) ? item : null;
607
+ };
608
+ }
609
+ var import_graphql4;
610
+ var init_resolvers = __esm({
611
+ "src/graphql/resolvers.ts"() {
612
+ "use strict";
613
+ import_graphql4 = require("graphql");
614
+ init_filters();
615
+ }
616
+ });
617
+
618
+ // src/graphql/schema.ts
619
+ function buildGraphQLSchema(registry) {
620
+ const objectTypes = /* @__PURE__ */ new Map();
621
+ const enumTypes = /* @__PURE__ */ new Map();
622
+ for (const [name, enumType] of Object.entries(registry.enum_types)) {
623
+ if (enumType.values.every(isValidGraphQLName)) {
624
+ enumTypes.set(
625
+ name,
626
+ new import_graphql5.GraphQLEnumType({
627
+ name: graphqlTypeName(name),
628
+ values: Object.fromEntries(enumType.values.map((v) => [v, { value: v }]))
629
+ })
630
+ );
631
+ } else {
632
+ process.stderr.write(
633
+ `\u26A0 enum '${name}' has values that aren't valid GraphQL identifiers; exposing as String
634
+ `
635
+ );
636
+ }
637
+ }
638
+ function outputTypeForField(field) {
639
+ const scalar = scalarOutputType(field.field_type);
640
+ if (scalar) return field.required ? new import_graphql5.GraphQLNonNull(scalar) : scalar;
641
+ if (field.field_type === "enum") {
642
+ const ref = field.ui_component.component === "select" ? field.ui_component.enum_ref : void 0;
643
+ const et = ref ? enumTypes.get(ref) : void 0;
644
+ const t = et ?? import_graphql5.GraphQLString;
645
+ return field.required ? new import_graphql5.GraphQLNonNull(t) : t;
646
+ }
647
+ if (field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
648
+ return MEDIA;
649
+ }
650
+ if (field.field_type === "paragraph") {
651
+ const ref = field.ui_component.component === "paragraph-embed" ? field.ui_component.ref : void 0;
652
+ const target = ref ? objectTypes.get(ref) : void 0;
653
+ if (ref && !target) {
654
+ process.stderr.write(
655
+ `\u26A0 paragraph field '${field.name}' targets unknown paragraph type '${ref}'; exposing as Media
656
+ `
657
+ );
658
+ }
659
+ return new import_graphql5.GraphQLList(new import_graphql5.GraphQLNonNull(target ?? MEDIA));
660
+ }
661
+ if (field.field_type === "reference") {
662
+ const ref = field.ui_component.component === "typeahead-select" ? field.ui_component.ref : void 0;
663
+ const target = ref ? objectTypes.get(ref) : void 0;
664
+ if (ref && !target) {
665
+ process.stderr.write(`\u26A0 reference field '${field.name}' targets unknown type '${ref}'; exposing as Media
666
+ `);
667
+ }
668
+ const rel = field.ui_component.component === "typeahead-select" ? field.ui_component.rel : void 0;
669
+ const isMany = rel === "many-to-many" || rel === "one-to-many";
670
+ const t = target ?? MEDIA;
671
+ return isMany ? new import_graphql5.GraphQLList(new import_graphql5.GraphQLNonNull(t)) : t;
672
+ }
673
+ return import_graphql5.GraphQLString;
674
+ }
675
+ function buildObjectType(machineName, type) {
676
+ const mediaFieldNames = type.fields.filter((f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file").map((f) => f.name);
677
+ return new import_graphql5.GraphQLObjectType({
678
+ name: graphqlTypeName(machineName),
679
+ fields: () => {
680
+ const fields = {};
681
+ fields["id"] = { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLID), resolve: (p) => p["id"] };
682
+ if (type.schema_type !== "paragraph-type") {
683
+ fields["published"] = { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLBoolean), resolve: (p) => p["published"] };
684
+ }
685
+ if (type.schema_type === "content-type") {
686
+ fields["slug"] = { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLString), resolve: (p) => p["slug"] };
687
+ }
688
+ fields["createdAt"] = { type: new import_graphql5.GraphQLNonNull(DateTimeScalar), resolve: (p) => p["created_at"] };
689
+ fields["updatedAt"] = { type: new import_graphql5.GraphQLNonNull(DateTimeScalar), resolve: (p) => p["updated_at"] };
690
+ for (const field of type.fields) {
691
+ const gqlName = toCamelCase(field.name);
692
+ const outType = outputTypeForField(field);
693
+ let resolve;
694
+ if (field.field_type === "programmatic") {
695
+ resolve = programmaticFieldResolver(machineName, field.name, mediaFieldNames);
696
+ } else if (field.field_type === "reference" || field.field_type === "paragraph" || field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
697
+ resolve = relationFieldResolver(machineName, field.name);
698
+ } else {
699
+ resolve = scalarFieldResolver(field.name);
700
+ }
701
+ fields[gqlName] = { type: outType, resolve };
702
+ }
703
+ return fields;
704
+ }
705
+ });
706
+ }
707
+ const typeNameOwners = /* @__PURE__ */ new Map();
708
+ function registerTypeName(machineName) {
709
+ const gqlName = graphqlTypeName(machineName);
710
+ const owner = typeNameOwners.get(gqlName);
711
+ if (owner !== void 0 && owner !== machineName) {
712
+ throw new Error(
713
+ `GraphQL type name collision: '${gqlName}' is produced by both '${owner}' and '${machineName}'. Rename one schema's machine name segment so their GraphQL type names differ.`
714
+ );
715
+ }
716
+ typeNameOwners.set(gqlName, machineName);
717
+ }
718
+ for (const [name, ct] of Object.entries(registry.content_types)) {
719
+ registerTypeName(name);
720
+ objectTypes.set(name, buildObjectType(name, ct));
721
+ }
722
+ for (const [name, tt] of Object.entries(registry.taxonomy_types)) {
723
+ registerTypeName(name);
724
+ objectTypes.set(name, buildObjectType(name, tt));
725
+ }
726
+ for (const [name, pt] of Object.entries(registry.paragraph_types)) {
727
+ registerTypeName(name);
728
+ objectTypes.set(name, buildObjectType(name, pt));
729
+ }
730
+ const queryFields = {};
731
+ for (const [name, ct] of Object.entries(registry.content_types)) {
732
+ const objType = objectTypes.get(name);
733
+ const nameMap = buildFieldNameMap(ct.fields.map((f) => f.name));
734
+ if (ct.only_one) {
735
+ queryFields[singleQueryName(name)] = { type: objType, resolve: singletonResolver(name) };
736
+ continue;
737
+ }
738
+ const listType = new import_graphql5.GraphQLObjectType({
739
+ name: `${graphqlTypeName(name)}List`,
740
+ fields: {
741
+ data: { type: new import_graphql5.GraphQLNonNull(new import_graphql5.GraphQLList(new import_graphql5.GraphQLNonNull(objType))), resolve: (r) => r.data },
742
+ meta: { type: new import_graphql5.GraphQLNonNull(PAGE_META), resolve: (r) => r.meta }
743
+ }
744
+ });
745
+ const filterType = buildFilterInputType(ct);
746
+ queryFields[collectionQueryName(name)] = {
747
+ type: new import_graphql5.GraphQLNonNull(listType),
748
+ args: {
749
+ page: { type: import_graphql5.GraphQLInt },
750
+ perPage: { type: import_graphql5.GraphQLInt },
751
+ sortBy: { type: buildSortFieldEnum(graphqlTypeName(name)) },
752
+ sortOrder: { type: SortOrderEnum },
753
+ ...filterType ? { filter: { type: filterType } } : {}
754
+ },
755
+ resolve: collectionResolver(name, nameMap)
756
+ };
757
+ queryFields[singleQueryName(name)] = {
758
+ type: objType,
759
+ args: { slug: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLString) } },
760
+ resolve: singleBySlugResolver(name)
761
+ };
762
+ }
763
+ for (const [name, tt] of Object.entries(registry.taxonomy_types)) {
764
+ const objType = objectTypes.get(name);
765
+ const listType = new import_graphql5.GraphQLObjectType({
766
+ name: `${graphqlTypeName(name)}List`,
767
+ fields: {
768
+ data: { type: new import_graphql5.GraphQLNonNull(new import_graphql5.GraphQLList(new import_graphql5.GraphQLNonNull(objType))), resolve: (r) => r.data },
769
+ meta: { type: new import_graphql5.GraphQLNonNull(PAGE_META), resolve: (r) => r.meta }
770
+ }
771
+ });
772
+ queryFields[collectionQueryName(name)] = {
773
+ type: new import_graphql5.GraphQLNonNull(listType),
774
+ args: { page: { type: import_graphql5.GraphQLInt }, perPage: { type: import_graphql5.GraphQLInt } },
775
+ resolve: collectionResolver(name, buildFieldNameMap(tt.fields.map((f) => f.name)))
776
+ };
777
+ queryFields[singleQueryName(name)] = {
778
+ type: objType,
779
+ args: { id: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLID) } },
780
+ resolve: taxonomySingleResolver(name)
781
+ };
782
+ }
783
+ return new import_graphql5.GraphQLSchema({
784
+ query: new import_graphql5.GraphQLObjectType({ name: "Query", fields: queryFields })
785
+ });
786
+ }
787
+ var import_graphql5, PAGE_META, MEDIA;
788
+ var init_schema = __esm({
789
+ "src/graphql/schema.ts"() {
790
+ "use strict";
791
+ import_graphql5 = require("graphql");
792
+ init_scalars();
793
+ init_type_mapping();
794
+ init_naming();
795
+ init_filters();
796
+ init_resolvers();
797
+ PAGE_META = new import_graphql5.GraphQLObjectType({
798
+ name: "PageMeta",
799
+ fields: {
800
+ total: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLInt), resolve: (m) => m.total },
801
+ page: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLInt), resolve: (m) => m.page },
802
+ perPage: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLInt), resolve: (m) => m.per_page },
803
+ totalPages: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLInt), resolve: (m) => m.total_pages },
804
+ hasNext: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLBoolean), resolve: (m) => m.has_next },
805
+ hasPrev: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLBoolean), resolve: (m) => m.has_prev }
806
+ }
807
+ });
808
+ MEDIA = new import_graphql5.GraphQLObjectType({
809
+ name: "Media",
810
+ fields: {
811
+ id: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLID), resolve: (m) => m.id },
812
+ type: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLString), resolve: (m) => m.type },
813
+ url: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLString), resolve: (m) => m.url },
814
+ mimeType: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLString), resolve: (m) => m.mime_type },
815
+ alt: { type: import_graphql5.GraphQLString, resolve: (m) => m.alt },
816
+ fileSize: { type: import_graphql5.GraphQLInt, resolve: (m) => m.file_size },
817
+ width: { type: import_graphql5.GraphQLInt, resolve: (m) => m.width },
818
+ height: { type: import_graphql5.GraphQLInt, resolve: (m) => m.height },
819
+ duration: { type: import_graphql5.GraphQLInt, resolve: (m) => m.duration }
820
+ }
821
+ });
822
+ }
823
+ });
824
+
825
+ // src/graphql/dataloaders.ts
826
+ function createRelationLoaders(db, registry) {
827
+ const relMaps = /* @__PURE__ */ new Map();
828
+ const loaders = /* @__PURE__ */ new Map();
829
+ const cache = /* @__PURE__ */ new Map();
830
+ function relationsFor(typeName) {
831
+ let m = relMaps.get(typeName);
832
+ if (!m) {
833
+ const type = registry.content_types[typeName] ?? registry.taxonomy_types[typeName] ?? registry.paragraph_types[typeName];
834
+ m = type ? buildRelationsMap(type.fields, registry) : {};
835
+ relMaps.set(typeName, m);
836
+ }
837
+ return m;
838
+ }
839
+ function loaderFor(typeName, fieldName, rel) {
840
+ const key = `${typeName}:${fieldName}`;
841
+ let dl = loaders.get(key);
842
+ if (!dl) {
843
+ dl = new import_dataloader.default(
844
+ async (parents) => {
845
+ const rows = parents;
846
+ await resolveRelationField(db, rows, fieldName, rel, cache, true);
847
+ return rows.map((r) => r[fieldName]);
848
+ },
849
+ // Batch within a tick; do not memoize by parent identity (rows are mutated
850
+ // and may recur across nesting levels).
851
+ { cache: false }
852
+ );
853
+ loaders.set(key, dl);
854
+ }
855
+ return dl;
856
+ }
857
+ return {
858
+ async load(typeName, fieldName, parent) {
859
+ const rel = relationsFor(typeName)[fieldName];
860
+ if (!rel) return null;
861
+ return loaderFor(typeName, fieldName, rel).load(parent);
862
+ }
863
+ };
864
+ }
865
+ var import_dataloader;
866
+ var init_dataloaders = __esm({
867
+ "src/graphql/dataloaders.ts"() {
868
+ "use strict";
869
+ import_dataloader = __toESM(require("dataloader"));
870
+ init_relations();
871
+ }
872
+ });
873
+
874
+ // src/graphql/security.ts
875
+ function buildArmorPlugin(options) {
876
+ const armor = new import_graphql_armor.EnvelopArmor({
877
+ maxDepth: { n: options.maxDepth },
878
+ costLimit: { maxCost: options.maxComplexity },
879
+ maxAliases: { n: 15 },
880
+ maxDirectives: { n: 50 },
881
+ blockFieldSuggestion: { enabled: true }
882
+ // maxTokens is intentionally left at Armor's secure default (n=1000) to bound
883
+ // GraphQL document token count at parse time, a layer the depth/cost/alias/directive
884
+ // limits do not cover.
885
+ });
886
+ return armor.protect();
887
+ }
888
+ function introspectionPlugin(enabled) {
889
+ return {
890
+ onValidate({ addValidationRule }) {
891
+ if (!enabled) addValidationRule(import_graphql6.NoSchemaIntrospectionCustomRule);
892
+ }
893
+ };
894
+ }
895
+ var import_graphql_armor, import_graphql6;
896
+ var init_security = __esm({
897
+ "src/graphql/security.ts"() {
898
+ "use strict";
899
+ import_graphql_armor = require("@escape.tech/graphql-armor");
900
+ import_graphql6 = require("graphql");
901
+ }
902
+ });
903
+
904
+ // src/graphql/handler.ts
905
+ var handler_exports = {};
906
+ __export(handler_exports, {
907
+ createGraphQLHandler: () => createGraphQLHandler
908
+ });
909
+ function isClientSafeGraphQLError(error) {
910
+ let current = error;
911
+ while (current != null && typeof current === "object" && current.name === "GraphQLError") {
912
+ if (current.originalError == null) return true;
913
+ current = current.originalError;
914
+ }
915
+ return false;
916
+ }
917
+ function createGraphQLHandler(registry, repos, resolver, db, options) {
918
+ const schema = buildGraphQLSchema(registry);
919
+ const { plugins } = buildArmorPlugin({
920
+ maxDepth: options.maxDepth,
921
+ maxComplexity: options.maxComplexity
922
+ });
923
+ const yoga = (0, import_graphql_yoga.createYoga)({
924
+ schema,
925
+ graphqlEndpoint: "/graphql",
926
+ graphiql: options.graphiql,
927
+ landingPage: false,
928
+ maskedErrors: {
929
+ maskError: (error, message, isDev) => isClientSafeGraphQLError(error) ? error : (0, import_graphql_yoga.maskError)(error, message, isDev)
930
+ },
931
+ // buildArmorPlugin's return type is `unknown[]` (see security.ts) since Armor's
932
+ // plugin shapes aren't part of its public typings; Yoga's `plugins` option only
933
+ // accepts its own `Plugin` type, so the cast bridges the two.
934
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
935
+ plugins: [...plugins, introspectionPlugin(options.introspection)],
936
+ context: () => ({
937
+ db,
938
+ registry,
939
+ repos,
940
+ resolver,
941
+ loaders: createRelationLoaders(db, registry),
942
+ programmaticMemo: /* @__PURE__ */ new WeakMap()
943
+ })
944
+ });
945
+ return (c) => yoga.fetch(c.req.raw, {});
946
+ }
947
+ var import_graphql_yoga;
948
+ var init_handler = __esm({
949
+ "src/graphql/handler.ts"() {
950
+ "use strict";
951
+ import_graphql_yoga = require("graphql-yoga");
952
+ init_schema();
953
+ init_dataloaders();
954
+ init_security();
955
+ }
956
+ });
957
+
30
958
  // src/index.ts
31
959
  var index_exports = {};
32
960
  __export(index_exports, {
@@ -72,26 +1000,43 @@ function createCorsMiddleware(corsConfig) {
72
1000
  }
73
1001
 
74
1002
  // src/middleware/security-headers.ts
1003
+ var GRAPHIQL_CDN = "https://unpkg.com";
75
1004
  function createSecurityHeadersMiddleware(options = {}) {
76
1005
  const connectSrc = ["'self'", ...options.connectSrc ?? []].join(" ");
77
- const csp = [
78
- "default-src 'self'",
79
- "img-src 'self' data: https:",
80
- "media-src 'self' https:",
81
- "style-src 'self' 'unsafe-inline'",
82
- "script-src 'self'",
83
- "font-src 'self' data:",
84
- `connect-src ${connectSrc}`,
85
- "frame-ancestors 'none'",
86
- "base-uri 'self'",
87
- "form-action 'self'"
88
- ].join("; ");
1006
+ function buildCsp(parts) {
1007
+ return [
1008
+ "default-src 'self'",
1009
+ "img-src 'self' data: https:",
1010
+ "media-src 'self' https:",
1011
+ `style-src ${parts.styleSrc}`,
1012
+ `script-src ${parts.scriptSrc}`,
1013
+ "font-src 'self' data:",
1014
+ `connect-src ${parts.connectSrc}`,
1015
+ ...parts.workerSrc ? [`worker-src ${parts.workerSrc}`] : [],
1016
+ "frame-ancestors 'none'",
1017
+ "base-uri 'self'",
1018
+ "form-action 'self'"
1019
+ ].join("; ");
1020
+ }
1021
+ const csp = buildCsp({
1022
+ scriptSrc: "'self'",
1023
+ styleSrc: "'self' 'unsafe-inline'",
1024
+ connectSrc
1025
+ });
1026
+ const graphiqlCsp = buildCsp({
1027
+ scriptSrc: `'self' 'unsafe-inline' ${GRAPHIQL_CDN}`,
1028
+ styleSrc: `'self' 'unsafe-inline' ${GRAPHIQL_CDN}`,
1029
+ connectSrc: `${connectSrc} ${GRAPHIQL_CDN}`,
1030
+ workerSrc: "'self' blob:"
1031
+ });
1032
+ const graphiqlPath = options.graphiqlPath;
89
1033
  return async function securityHeaders(c, next) {
90
1034
  await next();
91
1035
  c.res.headers.set("X-Content-Type-Options", "nosniff");
92
1036
  c.res.headers.set("X-Frame-Options", "DENY");
93
1037
  c.res.headers.set("Referrer-Policy", "no-referrer");
94
- c.res.headers.set("Content-Security-Policy", csp);
1038
+ const isGraphiql = graphiqlPath !== void 0 && c.req.path === graphiqlPath;
1039
+ c.res.headers.set("Content-Security-Policy", isGraphiql ? graphiqlCsp : csp);
95
1040
  };
96
1041
  }
97
1042
 
@@ -890,304 +1835,101 @@ function registerPublicMediaRoutes(app, mediaRepo, listRateLimit) {
890
1835
  if (typeParam !== void 0 && !VALID_MEDIA_TYPES.has(typeParam)) {
891
1836
  return c.json(
892
1837
  {
893
- ok: false,
894
- error: {
895
- code: "VALIDATION_ERROR",
896
- message: `type must be one of: image, video, file`
897
- }
898
- },
899
- 422
900
- );
901
- }
902
- const opts = {
903
- page: pagination.page,
904
- per_page: pagination.per_page
905
- };
906
- if (typeParam !== void 0) {
907
- opts.type = typeParam;
908
- }
909
- const result = await mediaRepo.findMany(opts);
910
- return c.json(result);
911
- };
912
- if (listRateLimit) {
913
- app.get("/api/media", listRateLimit, mediaListHandler);
914
- } else {
915
- app.get("/api/media", mediaListHandler);
916
- }
917
- app.get("/api/media/:id", async (c) => {
918
- const id = c.req.param("id");
919
- const item = await mediaRepo.findOne(id);
920
- if (!item) {
921
- return c.json(
922
- { ok: false, error: { code: "NOT_FOUND", message: "Media item not found" } },
923
- 404
924
- );
925
- }
926
- return c.json({ ok: true, data: item });
927
- });
928
- }
929
-
930
- // src/routes/admin/content.ts
931
- var import_drizzle_orm3 = require("drizzle-orm");
932
-
933
- // src/media-references.ts
934
- function extractMediaId(value) {
935
- return typeof value === "string" && value !== "" ? value : null;
936
- }
937
- function topLevelMediaDelta(mediaFields, before, after) {
938
- const added = [];
939
- const removed = [];
940
- for (const f of mediaFields) {
941
- if (after !== null && !(f.name in after)) continue;
942
- const oldId = before ? extractMediaId(before[f.name]) : null;
943
- const newId = after ? extractMediaId(after[f.name]) : null;
944
- if (newId === oldId) continue;
945
- if (oldId) removed.push(oldId);
946
- if (newId) added.push(newId);
947
- }
948
- return { added, removed };
949
- }
950
- function mergeMediaDeltas(...deltas) {
951
- return {
952
- added: deltas.flatMap((d) => d.added),
953
- removed: deltas.flatMap((d) => d.removed)
954
- };
955
- }
956
- async function applyMediaReferenceDelta(delta, mediaRepo) {
957
- const added = new Set(delta.added);
958
- const removed = new Set(delta.removed);
959
- const netAdd = [...added].filter((id) => !removed.has(id));
960
- const netRemove = [...removed].filter((id) => !added.has(id));
961
- if (netAdd.length > 0) await mediaRepo.incrementReferenceCount(netAdd);
962
- if (netRemove.length > 0) await mediaRepo.decrementReferenceCount(netRemove);
963
- }
964
-
965
- // src/relations.ts
966
- var import_node_crypto = __toESM(require("crypto"));
967
- var import_drizzle_orm2 = require("drizzle-orm");
968
- function quoteIdent(name) {
969
- if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
970
- return `"${name}"`;
971
- }
972
- function paragraphMediaFieldNames(pType) {
973
- return new Set(
974
- pType.fields.filter((f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file").map((f) => f.name)
975
- );
976
- }
977
- async function fetchParagraphMediaIds(db, pType, itemId, fieldName) {
978
- const mediaColumns = pType.fields.filter((f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file").map((f) => f.db_column.column_name);
979
- if (mediaColumns.length === 0) return [];
980
- const tbl = import_drizzle_orm2.sql.raw(quoteIdent(pType.db.table_name));
981
- const cols = import_drizzle_orm2.sql.join(mediaColumns.map((c) => import_drizzle_orm2.sql.raw(quoteIdent(c))), import_drizzle_orm2.sql`, `);
982
- const result = await db.execute(
983
- import_drizzle_orm2.sql`SELECT ${cols} FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`
984
- );
985
- const ids = [];
986
- for (const row of result.rows) {
987
- for (const col of mediaColumns) {
988
- const v = row[col];
989
- if (typeof v === "string" && v !== "") ids.push(v);
990
- }
991
- }
992
- return ids;
993
- }
994
- async function persistParagraphField(db, itemId, parentType, fieldName, pType, items) {
995
- const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName);
996
- const tbl = import_drizzle_orm2.sql.raw(quoteIdent(pType.db.table_name));
997
- await db.execute(import_drizzle_orm2.sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
998
- const mediaFieldNames = paragraphMediaFieldNames(pType);
999
- const added = [];
1000
- for (let i = 0; i < items.length; i++) {
1001
- const pItem = items[i];
1002
- const data = {
1003
- id: import_node_crypto.default.randomUUID(),
1004
- parent_id: itemId,
1005
- parent_type: parentType,
1006
- parent_field: fieldName,
1007
- order: i,
1008
- created_at: /* @__PURE__ */ new Date(),
1009
- updated_at: /* @__PURE__ */ new Date()
1010
- };
1011
- for (const pf of pType.fields) {
1012
- if (!pf.db_column || pf.db_column.junction) continue;
1013
- data[pf.db_column.column_name] = pItem[pf.name] ?? null;
1014
- }
1015
- const cols = import_drizzle_orm2.sql.join(Object.keys(data).map((k) => import_drizzle_orm2.sql.raw(quoteIdent(k))), import_drizzle_orm2.sql`, `);
1016
- const vals = import_drizzle_orm2.sql.join(Object.values(data).map((v) => import_drizzle_orm2.sql`${v}`), import_drizzle_orm2.sql`, `);
1017
- await db.execute(import_drizzle_orm2.sql`INSERT INTO ${tbl} (${cols}) VALUES (${vals})`);
1018
- for (const name of mediaFieldNames) {
1019
- const val = pItem[name];
1020
- if (typeof val === "string" && val !== "") added.push(val);
1021
- }
1022
- }
1023
- return { added, removed };
1024
- }
1025
- async function deleteParagraphField(db, itemId, fieldName, pType) {
1026
- const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName);
1027
- const tbl = import_drizzle_orm2.sql.raw(quoteIdent(pType.db.table_name));
1028
- await db.execute(import_drizzle_orm2.sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
1029
- return { added: [], removed };
1030
- }
1031
- async function persistJunctionField(db, itemId, junction, relatedIds) {
1032
- const tbl = import_drizzle_orm2.sql.raw(quoteIdent(junction.table_name));
1033
- const left = import_drizzle_orm2.sql.raw(quoteIdent(junction.left_column));
1034
- const right = import_drizzle_orm2.sql.raw(quoteIdent(junction.right_column));
1035
- await db.execute(import_drizzle_orm2.sql`DELETE FROM ${tbl} WHERE ${left} = ${itemId}`);
1036
- for (const rid of relatedIds) {
1037
- await db.execute(import_drizzle_orm2.sql`INSERT INTO ${tbl} (${left}, ${right}) VALUES (${itemId}, ${rid})`);
1038
- }
1039
- }
1040
- function buildRelationsMap(fields, registry) {
1041
- const relations = {};
1042
- for (const field of fields) {
1043
- if (field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
1044
- if (!field.db_column) continue;
1045
- relations[field.name] = { type: "media", fk_column: field.db_column.column_name };
1046
- } else if (field.field_type === "paragraph") {
1047
- const ref = field.ui_component.ref;
1048
- const pType = ref ? registry.paragraph_types[ref] : void 0;
1049
- if (!pType) continue;
1050
- relations[field.name] = { type: "paragraph", table: pType.db.table_name };
1051
- } else if (field.field_type === "reference") {
1052
- if (!field.db_column) continue;
1053
- if (field.db_column.junction) {
1054
- const j = field.db_column.junction;
1055
- relations[field.name] = {
1056
- type: "junction",
1057
- table: j.right_table,
1058
- junction_table: j.table_name,
1059
- left_column: j.left_column,
1060
- right_column: j.right_column,
1061
- order_column: j.order_column
1062
- };
1063
- } else if (field.db_column.foreign_key) {
1064
- relations[field.name] = {
1065
- type: "reference",
1066
- table: field.db_column.foreign_key.table,
1067
- fk_column: field.db_column.column_name
1068
- };
1069
- }
1070
- }
1071
- }
1072
- return relations;
1073
- }
1074
- function groupBy(items, key) {
1075
- const result = {};
1076
- for (const item of items) {
1077
- const k = String(item[key]);
1078
- if (!result[k]) result[k] = [];
1079
- result[k].push(item);
1080
- }
1081
- return result;
1082
- }
1083
- async function resolveRelationField(db, rows, fieldName, rel, cache) {
1084
- if (rows.length === 0) return;
1085
- if (rel.type === "paragraph") {
1086
- const parentIds = rows.map((r) => r["id"]);
1087
- const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
1088
- const result = await db.execute(
1089
- import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) ORDER BY "order" ASC`
1090
- );
1091
- const byParent = groupBy(result.rows, "parent_id");
1092
- for (const row of rows) {
1093
- row[fieldName] = byParent[row["id"]] ?? [];
1094
- }
1095
- } else if (rel.type === "reference") {
1096
- const fkValues = rows.map((r) => r[rel.fk_column]).filter(Boolean);
1097
- if (fkValues.length === 0) {
1098
- for (const row of rows) row[fieldName] = null;
1099
- return;
1100
- }
1101
- const unique = [...new Set(fkValues)];
1102
- const uncached = unique.filter((id) => !cache.has(`${rel.table}:${id}`));
1103
- if (uncached.length > 0) {
1104
- const inList = import_drizzle_orm2.sql.join(uncached.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
1105
- const result = await db.execute(
1106
- import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE id IN (${inList})`
1107
- );
1108
- for (const item of result.rows) {
1109
- cache.set(`${rel.table}:${item["id"]}`, item);
1110
- }
1111
- }
1112
- for (const row of rows) {
1113
- const fkVal = row[rel.fk_column];
1114
- row[fieldName] = fkVal ? cache.get(`${rel.table}:${fkVal}`) ?? null : null;
1115
- }
1116
- } else if (rel.type === "junction") {
1117
- const parentIds = rows.map((r) => r["id"]);
1118
- const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
1119
- const orderBy = rel.order_column ? import_drizzle_orm2.sql` ORDER BY "order" ASC` : import_drizzle_orm2.sql``;
1120
- const junctionResult = await db.execute(
1121
- import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.junction_table))} WHERE ${import_drizzle_orm2.sql.raw(quoteIdent(rel.left_column))} IN (${inList})${orderBy}`
1122
- );
1123
- const jRows = junctionResult.rows;
1124
- const rightIds = [...new Set(jRows.map((r) => r[rel.right_column]))];
1125
- const uncached = rightIds.filter((id) => !cache.has(`${rel.table}:${id}`));
1126
- if (uncached.length > 0) {
1127
- const rightInList = import_drizzle_orm2.sql.join(uncached.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
1128
- const entitiesResult = await db.execute(
1129
- import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE id IN (${rightInList})`
1130
- );
1131
- for (const item of entitiesResult.rows) {
1132
- cache.set(`${rel.table}:${item["id"]}`, item);
1133
- }
1134
- }
1135
- const byLeft = groupBy(jRows, rel.left_column);
1136
- for (const row of rows) {
1137
- const jEntries = byLeft[row["id"]] ?? [];
1138
- row[fieldName] = jEntries.map((jr) => cache.get(`${rel.table}:${jr[rel.right_column]}`)).filter(Boolean);
1139
- }
1140
- } else if (rel.type === "media") {
1141
- const fkValues = rows.map((r) => r[rel.fk_column]).filter(Boolean);
1142
- if (fkValues.length === 0) {
1143
- for (const row of rows) row[fieldName] = null;
1144
- return;
1145
- }
1146
- const unique = [...new Set(fkValues)];
1147
- const uncached = unique.filter((id) => !cache.has(`media:${id}`));
1148
- if (uncached.length > 0) {
1149
- const inList = import_drizzle_orm2.sql.join(uncached.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
1150
- const result = await db.execute(import_drizzle_orm2.sql`SELECT * FROM "media" WHERE id IN (${inList})`);
1151
- for (const item of result.rows) {
1152
- cache.set(`media:${item["id"]}`, item);
1153
- }
1838
+ ok: false,
1839
+ error: {
1840
+ code: "VALIDATION_ERROR",
1841
+ message: `type must be one of: image, video, file`
1842
+ }
1843
+ },
1844
+ 422
1845
+ );
1154
1846
  }
1155
- for (const row of rows) {
1156
- const fkVal = row[rel.fk_column];
1157
- row[fieldName] = fkVal ? cache.get(`media:${fkVal}`) ?? null : null;
1847
+ const opts = {
1848
+ page: pagination.page,
1849
+ per_page: pagination.per_page
1850
+ };
1851
+ if (typeParam !== void 0) {
1852
+ opts.type = typeParam;
1158
1853
  }
1854
+ const result = await mediaRepo.findMany(opts);
1855
+ return c.json(result);
1856
+ };
1857
+ if (listRateLimit) {
1858
+ app.get("/api/media", listRateLimit, mediaListHandler);
1859
+ } else {
1860
+ app.get("/api/media", mediaListHandler);
1159
1861
  }
1160
- }
1161
- async function resolveRelationBareIds(db, rows, fieldName, rel) {
1162
- if (rows.length === 0) return;
1163
- if (rel.type === "paragraph") {
1164
- const parentIds = rows.map((r) => r["id"]);
1165
- const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
1166
- const result = await db.execute(
1167
- import_drizzle_orm2.sql`SELECT id, parent_id FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) ORDER BY "order" ASC`
1168
- );
1169
- const byParent = groupBy(result.rows, "parent_id");
1170
- for (const row of rows) {
1171
- row[fieldName] = (byParent[row["id"]] ?? []).map((r) => r["id"]);
1172
- }
1173
- } else if (rel.type === "junction") {
1174
- const parentIds = rows.map((r) => r["id"]);
1175
- const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
1176
- const result = await db.execute(
1177
- import_drizzle_orm2.sql`SELECT ${import_drizzle_orm2.sql.raw(quoteIdent(rel.left_column))} AS left_id, ${import_drizzle_orm2.sql.raw(quoteIdent(rel.right_column))} AS right_id FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.junction_table))} WHERE ${import_drizzle_orm2.sql.raw(quoteIdent(rel.left_column))} IN (${inList})`
1178
- );
1179
- const byLeft = groupBy(result.rows, "left_id");
1180
- for (const row of rows) {
1181
- row[fieldName] = (byLeft[row["id"]] ?? []).map((r) => r["right_id"]);
1862
+ app.get("/api/media/:id", async (c) => {
1863
+ const id = c.req.param("id");
1864
+ const item = await mediaRepo.findOne(id);
1865
+ if (!item) {
1866
+ return c.json(
1867
+ { ok: false, error: { code: "NOT_FOUND", message: "Media item not found" } },
1868
+ 404
1869
+ );
1182
1870
  }
1871
+ return c.json({ ok: true, data: item });
1872
+ });
1873
+ }
1874
+
1875
+ // src/routes/admin/content.ts
1876
+ var import_drizzle_orm3 = require("drizzle-orm");
1877
+
1878
+ // src/media-references.ts
1879
+ function extractMediaId(value) {
1880
+ return typeof value === "string" && value !== "" ? value : null;
1881
+ }
1882
+ function topLevelMediaDelta(mediaFields, before, after) {
1883
+ const added = [];
1884
+ const removed = [];
1885
+ for (const f of mediaFields) {
1886
+ if (after !== null && !(f.name in after)) continue;
1887
+ const oldId = before ? extractMediaId(before[f.name]) : null;
1888
+ const newId = after ? extractMediaId(after[f.name]) : null;
1889
+ if (newId === oldId) continue;
1890
+ if (oldId) removed.push(oldId);
1891
+ if (newId) added.push(newId);
1183
1892
  }
1893
+ return { added, removed };
1894
+ }
1895
+ function mergeMediaDeltas(...deltas) {
1896
+ return {
1897
+ added: deltas.flatMap((d) => d.added),
1898
+ removed: deltas.flatMap((d) => d.removed)
1899
+ };
1900
+ }
1901
+ async function applyMediaReferenceDelta(delta, mediaRepo) {
1902
+ const added = new Set(delta.added);
1903
+ const removed = new Set(delta.removed);
1904
+ const netAdd = [...added].filter((id) => !removed.has(id));
1905
+ const netRemove = [...removed].filter((id) => !added.has(id));
1906
+ if (netAdd.length > 0) await mediaRepo.incrementReferenceCount(netAdd);
1907
+ if (netRemove.length > 0) await mediaRepo.decrementReferenceCount(netRemove);
1184
1908
  }
1185
1909
 
1186
1910
  // src/routes/admin/content.ts
1911
+ init_relations();
1187
1912
  function quoteIdent2(name) {
1188
1913
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
1189
1914
  return `"${name}"`;
1190
1915
  }
1916
+ async function loadParagraphRows(db, registry, pType, parentId, fieldName) {
1917
+ const result = await db.execute(
1918
+ import_drizzle_orm3.sql`SELECT * FROM ${import_drizzle_orm3.sql.raw(quoteIdent2(pType.db.table_name))} WHERE parent_id = ${parentId} AND parent_field = ${fieldName} ORDER BY "order" ASC`
1919
+ );
1920
+ const rows = result.rows;
1921
+ for (const nf of pType.fields) {
1922
+ if (nf.db_column !== null) continue;
1923
+ const comp = nf.ui_component;
1924
+ if (comp.component !== "paragraph-embed" || !comp.ref) continue;
1925
+ const nType = registry.paragraph_types[comp.ref];
1926
+ if (!nType) continue;
1927
+ for (const row of rows) {
1928
+ row[nf.name] = await loadParagraphRows(db, registry, nType, row["id"], nf.name);
1929
+ }
1930
+ }
1931
+ return rows;
1932
+ }
1191
1933
  async function lookupBasePathId(db, pathOrName) {
1192
1934
  const r = await db.execute(
1193
1935
  import_drizzle_orm3.sql`SELECT id FROM base_paths WHERE path = ${pathOrName} OR name = ${pathOrName} LIMIT 1`
@@ -1339,10 +2081,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1339
2081
  if (comp.component !== "paragraph-embed" || !comp.ref) continue;
1340
2082
  const pType = registry.paragraph_types[comp.ref];
1341
2083
  if (!pType) continue;
1342
- const r = await db.execute(
1343
- import_drizzle_orm3.sql`SELECT * FROM ${import_drizzle_orm3.sql.raw(quoteIdent2(pType.db.table_name))} WHERE parent_id = ${id} AND parent_field = ${f.name} ORDER BY "order" ASC`
1344
- );
1345
- row[f.name] = r.rows;
2084
+ row[f.name] = await loadParagraphRows(db, registry, pType, id, f.name);
1346
2085
  } else if (f.db_column.junction) {
1347
2086
  const j = f.db_column.junction;
1348
2087
  const r = await db.execute(
@@ -1417,82 +2156,85 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1417
2156
  );
1418
2157
  }
1419
2158
  }
1420
- const fieldErrors = checkRequiredFields(requiredFields, body);
1421
- if (fieldErrors.length > 0) {
2159
+ return writeNewItem(c, body);
2160
+ }
2161
+ );
2162
+ const writeNewItem = async (c, body) => {
2163
+ const fieldErrors = checkRequiredFields(requiredFields, body);
2164
+ if (fieldErrors.length > 0) {
2165
+ return c.json(
2166
+ {
2167
+ ok: false,
2168
+ error: {
2169
+ code: "VALIDATION_ERROR",
2170
+ message: "Required fields are missing",
2171
+ details: fieldErrors
2172
+ }
2173
+ },
2174
+ 422
2175
+ );
2176
+ }
2177
+ if (body["published"] === true) {
2178
+ const publishDeny = await requirePermission("content:edit")(c, async () => {
2179
+ });
2180
+ if (publishDeny) return publishDeny;
2181
+ }
2182
+ const paragraphFields = contentType.fields.filter((f) => f.db_column === null);
2183
+ const junctionFields = contentType.fields.filter(
2184
+ (f) => f.db_column !== null && f.db_column.junction !== void 0
2185
+ );
2186
+ const columnFields = contentType.fields.filter(
2187
+ (f) => f.db_column !== null && f.db_column.junction === void 0
2188
+ );
2189
+ const columnData = {
2190
+ published: body["published"] ?? false
2191
+ };
2192
+ if (contentType.only_one) {
2193
+ columnData["slug"] = typeName;
2194
+ } else {
2195
+ columnData["slug"] = body["slug"];
2196
+ }
2197
+ for (const f of columnFields) {
2198
+ const val = body[f.name];
2199
+ columnData[f.db_column.column_name] = val !== void 0 ? val : null;
2200
+ }
2201
+ if (db) {
2202
+ const basePathId = await lookupBasePathId(db, contentType.default_base_path);
2203
+ if (!basePathId) {
1422
2204
  return c.json(
1423
2205
  {
1424
2206
  ok: false,
1425
2207
  error: {
1426
- code: "VALIDATION_ERROR",
1427
- message: "Required fields are missing",
1428
- details: fieldErrors
2208
+ code: "BASE_PATH_NOT_FOUND",
2209
+ message: `Base path '${contentType.default_base_path}' not found \u2014 run manguito migrate`
1429
2210
  }
1430
2211
  },
1431
- 422
2212
+ 500
1432
2213
  );
1433
2214
  }
1434
- if (body["published"] === true) {
1435
- const publishDeny = await requirePermission("content:edit")(c, async () => {
1436
- });
1437
- if (publishDeny) return publishDeny;
1438
- }
1439
- const paragraphFields = contentType.fields.filter((f) => f.db_column === null);
1440
- const junctionFields = contentType.fields.filter(
1441
- (f) => f.db_column !== null && f.db_column.junction !== void 0
1442
- );
1443
- const columnFields = contentType.fields.filter(
1444
- (f) => f.db_column !== null && f.db_column.junction === void 0
1445
- );
1446
- const columnData = {
1447
- published: body["published"] ?? false
1448
- };
1449
- if (contentType.only_one) {
1450
- columnData["slug"] = typeName;
1451
- } else {
1452
- columnData["slug"] = body["slug"];
1453
- }
1454
- for (const f of columnFields) {
1455
- const val = body[f.name];
1456
- columnData[f.db_column.column_name] = val !== void 0 ? val : null;
1457
- }
1458
- if (db) {
1459
- const basePathId = await lookupBasePathId(db, contentType.default_base_path);
1460
- if (!basePathId) {
1461
- return c.json(
1462
- {
1463
- ok: false,
1464
- error: {
1465
- code: "BASE_PATH_NOT_FOUND",
1466
- message: `Base path '${contentType.default_base_path}' not found \u2014 run manguito migrate`
1467
- }
1468
- },
1469
- 500
1470
- );
1471
- }
1472
- columnData["base_path_id"] = basePathId;
2215
+ columnData["base_path_id"] = basePathId;
2216
+ }
2217
+ const item = await repo.create(columnData);
2218
+ const itemId = item["id"];
2219
+ const mediaDeltas = [topLevelMediaDelta(mediaFields, null, body)];
2220
+ if (db) {
2221
+ for (const f of paragraphFields) {
2222
+ const comp = f.ui_component;
2223
+ if (comp.component !== "paragraph-embed" || !comp.ref) continue;
2224
+ const pType = registry.paragraph_types[comp.ref];
2225
+ if (!pType) continue;
2226
+ const items = Array.isArray(body[f.name]) ? body[f.name] : [];
2227
+ mediaDeltas.push(await persistParagraphField(db, itemId, contentType.db.table_name, f.name, pType, items, registry));
1473
2228
  }
1474
- const item = await repo.create(columnData);
1475
- const itemId = item["id"];
1476
- const mediaDeltas = [topLevelMediaDelta(mediaFields, null, body)];
1477
- if (db) {
1478
- for (const f of paragraphFields) {
1479
- const comp = f.ui_component;
1480
- if (comp.component !== "paragraph-embed" || !comp.ref) continue;
1481
- const pType = registry.paragraph_types[comp.ref];
1482
- if (!pType) continue;
1483
- const items = Array.isArray(body[f.name]) ? body[f.name] : [];
1484
- mediaDeltas.push(await persistParagraphField(db, itemId, contentType.db.table_name, f.name, pType, items));
1485
- }
1486
- for (const f of junctionFields) {
1487
- const junction = f.db_column.junction;
1488
- const relatedIds = Array.isArray(body[f.name]) ? body[f.name].filter((v) => typeof v === "string") : [];
1489
- await persistJunctionField(db, itemId, junction, relatedIds);
1490
- }
2229
+ for (const f of junctionFields) {
2230
+ const junction = f.db_column.junction;
2231
+ const relatedIds = Array.isArray(body[f.name]) ? body[f.name].filter((v) => typeof v === "string") : [];
2232
+ await persistJunctionField(db, itemId, junction, relatedIds);
1491
2233
  }
1492
- await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
1493
- return c.json({ ok: true, data: item }, 201);
1494
2234
  }
1495
- );
2235
+ await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
2236
+ return c.json({ ok: true, data: item }, 201);
2237
+ };
1496
2238
  app.patch(
1497
2239
  `/admin/api/${basePath}/:id`,
1498
2240
  requirePermission("content:edit"),
@@ -1547,71 +2289,92 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1547
2289
  );
1548
2290
  }
1549
2291
  }
1550
- if (body["published"] === true) {
1551
- const publishDeny = await requirePermission("content:edit")(c, async () => {
1552
- });
1553
- if (publishDeny) return publishDeny;
1554
- const merged = { ...existing, ...body };
1555
- const fieldErrors = checkRequiredFields(requiredFields, merged);
1556
- if (fieldErrors.length > 0) {
1557
- return c.json(
1558
- {
1559
- ok: false,
1560
- error: {
1561
- code: "PUBLISH_VALIDATION_ERROR",
1562
- message: "Cannot publish \u2014 required fields are missing",
1563
- details: fieldErrors
1564
- }
1565
- },
1566
- 422
1567
- );
1568
- }
2292
+ return writeExistingItem(c, id, existing, body);
2293
+ }
2294
+ );
2295
+ const writeExistingItem = async (c, id, existing, body) => {
2296
+ if (body["published"] === true) {
2297
+ const publishDeny = await requirePermission("content:edit")(c, async () => {
2298
+ });
2299
+ if (publishDeny) return publishDeny;
2300
+ const merged = { ...existing, ...body };
2301
+ const fieldErrors = checkRequiredFields(requiredFields, merged);
2302
+ if (fieldErrors.length > 0) {
2303
+ return c.json(
2304
+ {
2305
+ ok: false,
2306
+ error: {
2307
+ code: "PUBLISH_VALIDATION_ERROR",
2308
+ message: "Cannot publish \u2014 required fields are missing",
2309
+ details: fieldErrors
2310
+ }
2311
+ },
2312
+ 422
2313
+ );
1569
2314
  }
1570
- const patchParagraphFields = contentType.fields.filter((f) => f.db_column === null);
1571
- const patchJunctionFields = contentType.fields.filter(
1572
- (f) => f.db_column !== null && f.db_column.junction !== void 0
1573
- );
1574
- const patchColumnFields = contentType.fields.filter(
1575
- (f) => f.db_column !== null && f.db_column.junction === void 0
2315
+ }
2316
+ const patchParagraphFields = contentType.fields.filter((f) => f.db_column === null);
2317
+ const patchJunctionFields = contentType.fields.filter(
2318
+ (f) => f.db_column !== null && f.db_column.junction !== void 0
2319
+ );
2320
+ const patchColumnFields = contentType.fields.filter(
2321
+ (f) => f.db_column !== null && f.db_column.junction === void 0
2322
+ );
2323
+ const patchData = {};
2324
+ if (!contentType.only_one && "slug" in body) patchData["slug"] = body["slug"];
2325
+ if ("published" in body) patchData["published"] = body["published"];
2326
+ for (const f of patchColumnFields) {
2327
+ if (f.name in body) {
2328
+ const val = body[f.name];
2329
+ patchData[f.db_column.column_name] = val !== void 0 ? val : null;
2330
+ }
2331
+ }
2332
+ const updated = await repo.update(id, patchData);
2333
+ if (!updated) {
2334
+ return c.json(
2335
+ { ok: false, error: { code: "NOT_FOUND", message: "Not found" } },
2336
+ 404
1576
2337
  );
1577
- const patchData = {};
1578
- if (!contentType.only_one && "slug" in body) patchData["slug"] = body["slug"];
1579
- if ("published" in body) patchData["published"] = body["published"];
1580
- for (const f of patchColumnFields) {
1581
- if (f.name in body) {
1582
- const val = body[f.name];
1583
- patchData[f.db_column.column_name] = val !== void 0 ? val : null;
1584
- }
2338
+ }
2339
+ const mediaDeltas = [
2340
+ topLevelMediaDelta(mediaFields, existing, body)
2341
+ ];
2342
+ if (db) {
2343
+ for (const f of patchParagraphFields) {
2344
+ const comp = f.ui_component;
2345
+ if (comp.component !== "paragraph-embed" || !comp.ref) continue;
2346
+ const pType = registry.paragraph_types[comp.ref];
2347
+ if (!pType) continue;
2348
+ const items = Array.isArray(body[f.name]) ? body[f.name] : [];
2349
+ mediaDeltas.push(await persistParagraphField(db, id, contentType.db.table_name, f.name, pType, items, registry));
1585
2350
  }
1586
- const updated = await repo.update(id, patchData);
1587
- if (!updated) {
1588
- return c.json(
1589
- { ok: false, error: { code: "NOT_FOUND", message: "Not found" } },
1590
- 404
1591
- );
2351
+ for (const f of patchJunctionFields) {
2352
+ const junction = f.db_column.junction;
2353
+ const relatedIds = Array.isArray(body[f.name]) ? body[f.name].filter((v) => typeof v === "string") : [];
2354
+ await persistJunctionField(db, id, junction, relatedIds);
1592
2355
  }
1593
- const mediaDeltas = [
1594
- topLevelMediaDelta(mediaFields, existing, body)
1595
- ];
1596
- if (db) {
1597
- for (const f of patchParagraphFields) {
1598
- const comp = f.ui_component;
1599
- if (comp.component !== "paragraph-embed" || !comp.ref) continue;
1600
- const pType = registry.paragraph_types[comp.ref];
1601
- if (!pType) continue;
1602
- const items = Array.isArray(body[f.name]) ? body[f.name] : [];
1603
- mediaDeltas.push(await persistParagraphField(db, id, contentType.db.table_name, f.name, pType, items));
1604
- }
1605
- for (const f of patchJunctionFields) {
1606
- const junction = f.db_column.junction;
1607
- const relatedIds = Array.isArray(body[f.name]) ? body[f.name].filter((v) => typeof v === "string") : [];
1608
- await persistJunctionField(db, id, junction, relatedIds);
2356
+ }
2357
+ await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
2358
+ return c.json({ ok: true, data: updated });
2359
+ };
2360
+ if (contentType.only_one) {
2361
+ app.put(
2362
+ `/admin/api/${basePath}`,
2363
+ requirePermission("content:edit"),
2364
+ async (c) => {
2365
+ const body = await c.req.json();
2366
+ const existingResult = await repo.findMany({ page: 1, per_page: 1 });
2367
+ const existing = existingResult.data[0];
2368
+ if (!existing) {
2369
+ const createDeny = await requirePermission("content:create")(c, async () => {
2370
+ });
2371
+ if (createDeny) return createDeny;
2372
+ return writeNewItem(c, body);
1609
2373
  }
2374
+ return writeExistingItem(c, existing["id"], existing, body);
1610
2375
  }
1611
- await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
1612
- return c.json({ ok: true, data: updated });
1613
- }
1614
- );
2376
+ );
2377
+ }
1615
2378
  app.delete(
1616
2379
  `/admin/api/${basePath}/:id`,
1617
2380
  requirePermission("content:delete"),
@@ -1633,7 +2396,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1633
2396
  if (comp.component !== "paragraph-embed" || !comp.ref) continue;
1634
2397
  const pType = registry.paragraph_types[comp.ref];
1635
2398
  if (!pType) continue;
1636
- mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType));
2399
+ mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType, registry));
1637
2400
  }
1638
2401
  }
1639
2402
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
@@ -1748,7 +2511,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1748
2511
  const pType = registry.paragraph_types[comp.ref];
1749
2512
  if (!pType) continue;
1750
2513
  const items = Array.isArray(body[f.name]) ? body[f.name] : [];
1751
- mediaDeltas.push(await persistParagraphField(db, taxItemId, taxonomyType.db.table_name, f.name, pType, items));
2514
+ mediaDeltas.push(await persistParagraphField(db, taxItemId, taxonomyType.db.table_name, f.name, pType, items, registry));
1752
2515
  }
1753
2516
  }
1754
2517
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
@@ -1817,7 +2580,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1817
2580
  const pType = registry.paragraph_types[comp.ref];
1818
2581
  if (!pType) continue;
1819
2582
  const items = Array.isArray(body[f.name]) ? body[f.name] : [];
1820
- mediaDeltas.push(await persistParagraphField(db, id, taxonomyType.db.table_name, f.name, pType, items));
2583
+ mediaDeltas.push(await persistParagraphField(db, id, taxonomyType.db.table_name, f.name, pType, items, registry));
1821
2584
  }
1822
2585
  }
1823
2586
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
@@ -1845,7 +2608,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
1845
2608
  if (comp.component !== "paragraph-embed" || !comp.ref) continue;
1846
2609
  const pType = registry.paragraph_types[comp.ref];
1847
2610
  if (!pType) continue;
1848
- mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType));
2611
+ mediaDeltas.push(await deleteParagraphField(db, id, f.name, pType, registry));
1849
2612
  }
1850
2613
  }
1851
2614
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
@@ -2813,7 +3576,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2813
3576
  var import_drizzle_orm6 = require("drizzle-orm");
2814
3577
  var API_VERSION = (() => {
2815
3578
  try {
2816
- return "0.2.0";
3579
+ return "0.3.2";
2817
3580
  } catch {
2818
3581
  return "0.0.0";
2819
3582
  }
@@ -2920,6 +3683,7 @@ function registerSchemaRoute(app, registry, db) {
2920
3683
  // src/repositories/content.ts
2921
3684
  var import_node_crypto3 = __toESM(require("crypto"));
2922
3685
  var import_drizzle_orm8 = require("drizzle-orm");
3686
+ init_relations();
2923
3687
  var SORTABLE_FIELDS2 = /* @__PURE__ */ new Set(["title", "created_at", "updated_at"]);
2924
3688
  function codeError(code, message) {
2925
3689
  const err = new Error(message);
@@ -2975,7 +3739,7 @@ function whereFragment(conds) {
2975
3739
  return conds.length > 0 ? import_drizzle_orm8.sql` WHERE ${import_drizzle_orm8.sql.join(conds, import_drizzle_orm8.sql` AND `)}` : import_drizzle_orm8.sql``;
2976
3740
  }
2977
3741
  function createDrizzleContentRepository(db, tableName, options = {}) {
2978
- const { relations = {} } = options;
3742
+ const { relations = {}, publishedRelations = false } = options;
2979
3743
  function tableRaw() {
2980
3744
  return import_drizzle_orm8.sql.raw(quoteIdent3(tableName));
2981
3745
  }
@@ -2988,7 +3752,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
2988
3752
  }
2989
3753
  for (const [fieldName, rel] of Object.entries(relations)) {
2990
3754
  if (rel.type === "media") {
2991
- await resolveRelationField(db, rows, fieldName, rel, cache);
3755
+ await resolveRelationField(db, rows, fieldName, rel, cache, publishedRelations);
2992
3756
  } else if (!include.includes(fieldName)) {
2993
3757
  await resolveRelationBareIds(db, rows, fieldName, rel);
2994
3758
  }
@@ -2996,7 +3760,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
2996
3760
  for (const fieldName of include) {
2997
3761
  const rel = relations[fieldName];
2998
3762
  if (rel.type !== "media") {
2999
- await resolveRelationField(db, rows, fieldName, rel, cache);
3763
+ await resolveRelationField(db, rows, fieldName, rel, cache, publishedRelations);
3000
3764
  }
3001
3765
  }
3002
3766
  return rows;
@@ -3117,6 +3881,9 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
3117
3881
  };
3118
3882
  }
3119
3883
 
3884
+ // src/app.ts
3885
+ init_relations();
3886
+
3120
3887
  // src/repositories/media.ts
3121
3888
  var import_node_crypto4 = __toESM(require("crypto"));
3122
3889
  var import_drizzle_orm9 = require("drizzle-orm");
@@ -3234,7 +4001,14 @@ function createCmsApp(options) {
3234
4001
  const programmaticResolver = createProgrammaticResolver(resolverMap);
3235
4002
  const app = new import_hono.Hono();
3236
4003
  const uploadOrigins = storage.getUploadOrigins?.() ?? [];
3237
- app.use("*", createSecurityHeadersMiddleware({ connectSrc: uploadOrigins }));
4004
+ const graphiqlEnabled = options.graphql?.enabled === true && options.graphql.graphiql === true;
4005
+ app.use(
4006
+ "*",
4007
+ createSecurityHeadersMiddleware({
4008
+ connectSrc: uploadOrigins,
4009
+ ...graphiqlEnabled && { graphiqlPath: "/graphql" }
4010
+ })
4011
+ );
3238
4012
  app.use("*", createCorsMiddleware(cors ?? { origin: "*", enabled: true }));
3239
4013
  app.onError(errorHandler);
3240
4014
  const listRateLimit = resolveListRateLimit(rateLimit);
@@ -3269,7 +4043,8 @@ function createCmsApp(options) {
3269
4043
  Object.entries(registry.content_types).map(([typeName, ct]) => [
3270
4044
  typeName,
3271
4045
  createDrizzleContentRepository(db, ct.db.table_name, {
3272
- relations: buildRelationsMap(ct.fields, registry)
4046
+ relations: buildRelationsMap(ct.fields, registry),
4047
+ publishedRelations: true
3273
4048
  })
3274
4049
  ])
3275
4050
  );
@@ -3277,11 +4052,22 @@ function createCmsApp(options) {
3277
4052
  Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
3278
4053
  typeName,
3279
4054
  createDrizzleContentRepository(db, tt.db.table_name, {
3280
- relations: buildRelationsMap(tt.fields, registry)
4055
+ relations: buildRelationsMap(tt.fields, registry),
4056
+ publishedRelations: true
3281
4057
  })
3282
4058
  ])
3283
4059
  );
3284
4060
  const publicRepos = { ...publicContentRepos, ...publicTaxonomyRepos };
4061
+ const graphqlRepos = Object.fromEntries([
4062
+ ...Object.entries(registry.content_types).map(([typeName, ct]) => [
4063
+ typeName,
4064
+ createDrizzleContentRepository(db, ct.db.table_name)
4065
+ ]),
4066
+ ...Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
4067
+ typeName,
4068
+ createDrizzleContentRepository(db, tt.db.table_name)
4069
+ ])
4070
+ ]);
3285
4071
  registerAuthRoutes(app, db);
3286
4072
  app.use("/admin/api/*", authMiddleware);
3287
4073
  app.use("/admin/api/*", mustChangePasswordCheck);
@@ -3318,6 +4104,35 @@ function createCmsApp(options) {
3318
4104
  );
3319
4105
  registerPublicContentRoutes(app, registry, publicRepos, listRateLimit, programmaticResolver);
3320
4106
  registerPublicMediaRoutes(app, mediaRepo, listRateLimit);
4107
+ if (options.graphql?.enabled) {
4108
+ const gqlOptions = options.graphql;
4109
+ let gqlHandler = null;
4110
+ let initError = null;
4111
+ const ready = Promise.resolve().then(() => (init_handler(), handler_exports)).then(({ createGraphQLHandler: createGraphQLHandler2 }) => {
4112
+ gqlHandler = createGraphQLHandler2(registry, graphqlRepos, programmaticResolver, db, gqlOptions);
4113
+ }).catch((err) => {
4114
+ initError = err;
4115
+ const message = err instanceof Error ? err.message : String(err);
4116
+ process.stderr.write(`\u2717 GraphQL schema failed to initialize; /graphql will return 500. ${message}
4117
+ `);
4118
+ });
4119
+ const invokeGraphQL = async (c) => {
4120
+ if (!gqlHandler && !initError) await ready;
4121
+ if (!gqlHandler) {
4122
+ return c.json(
4123
+ { ok: false, error: { code: "GRAPHQL_INIT_FAILED", message: "GraphQL schema failed to initialize" } },
4124
+ 500
4125
+ );
4126
+ }
4127
+ return gqlHandler(c, async () => {
4128
+ });
4129
+ };
4130
+ if (listRateLimit) {
4131
+ app.all("/graphql", listRateLimit, invokeGraphQL);
4132
+ } else {
4133
+ app.all("/graphql", invokeGraphQL);
4134
+ }
4135
+ }
3321
4136
  registerConfigRoute(
3322
4137
  app,
3323
4138
  {
@@ -3364,13 +4179,24 @@ function createServer(options = {}) {
3364
4179
  }
3365
4180
 
3366
4181
  // src/index.ts
4182
+ function resolveGraphQL(cfg) {
4183
+ const devDefault = process.env["NODE_ENV"] !== "production";
4184
+ return {
4185
+ enabled: cfg.enabled ?? false,
4186
+ maxDepth: cfg.maxDepth ?? 8,
4187
+ maxComplexity: cfg.maxComplexity ?? 1e3,
4188
+ graphiql: cfg.graphiql ?? devDefault,
4189
+ introspection: cfg.introspection ?? cfg.graphiql ?? devDefault
4190
+ };
4191
+ }
3367
4192
  function createAPIAdapter(options = {}) {
3368
4193
  const prefix = options.prefix ?? "/api";
3369
4194
  const media = options.media?.max_file_size !== void 0 ? { max_file_size: options.media.max_file_size } : void 0;
3370
4195
  return {
3371
4196
  prefix,
3372
4197
  ...media !== void 0 && { media },
3373
- ...options.rateLimit !== void 0 && { rateLimit: options.rateLimit }
4198
+ ...options.rateLimit !== void 0 && { rateLimit: options.rateLimit },
4199
+ ...options.graphql !== void 0 && { graphql: resolveGraphQL(options.graphql) }
3374
4200
  };
3375
4201
  }
3376
4202
  // Annotate the CommonJS export names for ESM import in node: