@bobbykim/manguito-cms-api 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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 });
@@ -15,17 +18,870 @@ var __copyProps = (to, from, except, desc) => {
15
18
  if (!__hasOwnProp.call(to, key) && key !== except)
16
19
  __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
20
  }
18
- return to;
19
- };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
24
+ // If the importer is in node compatibility mode or this is not an ESM
25
+ // file that has been converted to a CommonJS file using a Babel-
26
+ // compatible transform (i.e. "__esModule" has not been set), then set
27
+ // "default" to the CommonJS "module.exports" for node compatibility.
28
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
29
+ mod
30
+ ));
31
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
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
+ async function persistParagraphField(db, itemId, parentType, fieldName, pType, items) {
61
+ const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName);
62
+ const tbl = import_drizzle_orm2.sql.raw(quoteIdent(pType.db.table_name));
63
+ await db.execute(import_drizzle_orm2.sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
64
+ const mediaFieldNames = paragraphMediaFieldNames(pType);
65
+ const added = [];
66
+ for (let i = 0; i < items.length; i++) {
67
+ const pItem = items[i];
68
+ const data = {
69
+ id: import_node_crypto.default.randomUUID(),
70
+ parent_id: itemId,
71
+ parent_type: parentType,
72
+ parent_field: fieldName,
73
+ order: i,
74
+ created_at: /* @__PURE__ */ new Date(),
75
+ updated_at: /* @__PURE__ */ new Date()
76
+ };
77
+ for (const pf of pType.fields) {
78
+ if (!pf.db_column || pf.db_column.junction) continue;
79
+ data[pf.db_column.column_name] = pItem[pf.name] ?? null;
80
+ }
81
+ const cols = import_drizzle_orm2.sql.join(Object.keys(data).map((k) => import_drizzle_orm2.sql.raw(quoteIdent(k))), import_drizzle_orm2.sql`, `);
82
+ const vals = import_drizzle_orm2.sql.join(Object.values(data).map((v) => import_drizzle_orm2.sql`${v}`), import_drizzle_orm2.sql`, `);
83
+ await db.execute(import_drizzle_orm2.sql`INSERT INTO ${tbl} (${cols}) VALUES (${vals})`);
84
+ for (const name of mediaFieldNames) {
85
+ const val = pItem[name];
86
+ if (typeof val === "string" && val !== "") added.push(val);
87
+ }
88
+ }
89
+ return { added, removed };
90
+ }
91
+ async function deleteParagraphField(db, itemId, fieldName, pType) {
92
+ const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName);
93
+ const tbl = import_drizzle_orm2.sql.raw(quoteIdent(pType.db.table_name));
94
+ await db.execute(import_drizzle_orm2.sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
95
+ return { added: [], removed };
96
+ }
97
+ async function persistJunctionField(db, itemId, junction, relatedIds) {
98
+ const tbl = import_drizzle_orm2.sql.raw(quoteIdent(junction.table_name));
99
+ const left = import_drizzle_orm2.sql.raw(quoteIdent(junction.left_column));
100
+ const right = import_drizzle_orm2.sql.raw(quoteIdent(junction.right_column));
101
+ await db.execute(import_drizzle_orm2.sql`DELETE FROM ${tbl} WHERE ${left} = ${itemId}`);
102
+ for (const rid of relatedIds) {
103
+ await db.execute(import_drizzle_orm2.sql`INSERT INTO ${tbl} (${left}, ${right}) VALUES (${itemId}, ${rid})`);
104
+ }
105
+ }
106
+ function buildRelationsMap(fields, registry) {
107
+ const relations = {};
108
+ for (const field of fields) {
109
+ if (field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
110
+ if (!field.db_column) continue;
111
+ relations[field.name] = { type: "media", fk_column: field.db_column.column_name };
112
+ } else if (field.field_type === "paragraph") {
113
+ const ref = field.ui_component.ref;
114
+ const pType = ref ? registry.paragraph_types[ref] : void 0;
115
+ if (!pType) continue;
116
+ relations[field.name] = { type: "paragraph", table: pType.db.table_name };
117
+ } else if (field.field_type === "reference") {
118
+ if (!field.db_column) continue;
119
+ if (field.db_column.junction) {
120
+ const j = field.db_column.junction;
121
+ relations[field.name] = {
122
+ type: "junction",
123
+ table: j.right_table,
124
+ junction_table: j.table_name,
125
+ left_column: j.left_column,
126
+ right_column: j.right_column,
127
+ order_column: j.order_column
128
+ };
129
+ } else if (field.db_column.foreign_key) {
130
+ relations[field.name] = {
131
+ type: "reference",
132
+ table: field.db_column.foreign_key.table,
133
+ fk_column: field.db_column.column_name
134
+ };
135
+ }
136
+ }
137
+ }
138
+ return relations;
139
+ }
140
+ function groupBy(items, key) {
141
+ const result = {};
142
+ for (const item of items) {
143
+ const k = String(item[key]);
144
+ if (!result[k]) result[k] = [];
145
+ result[k].push(item);
146
+ }
147
+ return result;
148
+ }
149
+ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOnly = false) {
150
+ if (rows.length === 0) return;
151
+ if (rel.type === "paragraph") {
152
+ const parentIds = rows.map((r) => r["id"]);
153
+ const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
154
+ const result = await db.execute(
155
+ import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) ORDER BY "order" ASC`
156
+ );
157
+ const byParent = groupBy(result.rows, "parent_id");
158
+ for (const row of rows) {
159
+ row[fieldName] = byParent[row["id"]] ?? [];
160
+ }
161
+ } else if (rel.type === "reference") {
162
+ const fkValues = rows.map((r) => r[rel.fk_column]).filter(Boolean);
163
+ if (fkValues.length === 0) {
164
+ for (const row of rows) row[fieldName] = null;
165
+ return;
166
+ }
167
+ const unique = [...new Set(fkValues)];
168
+ const uncached = unique.filter((id) => !cache.has(`${rel.table}:${id}`));
169
+ if (uncached.length > 0) {
170
+ const inList = import_drizzle_orm2.sql.join(uncached.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
171
+ const publishedCond = publishedOnly ? import_drizzle_orm2.sql` AND published = true` : import_drizzle_orm2.sql``;
172
+ const result = await db.execute(
173
+ import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE id IN (${inList})${publishedCond}`
174
+ );
175
+ for (const item of result.rows) {
176
+ cache.set(`${rel.table}:${item["id"]}`, item);
177
+ }
178
+ }
179
+ for (const row of rows) {
180
+ const fkVal = row[rel.fk_column];
181
+ row[fieldName] = fkVal ? cache.get(`${rel.table}:${fkVal}`) ?? null : null;
182
+ }
183
+ } else if (rel.type === "junction") {
184
+ const parentIds = rows.map((r) => r["id"]);
185
+ const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
186
+ const orderBy = rel.order_column ? import_drizzle_orm2.sql` ORDER BY "order" ASC` : import_drizzle_orm2.sql``;
187
+ const junctionResult = await db.execute(
188
+ 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}`
189
+ );
190
+ const jRows = junctionResult.rows;
191
+ const rightIds = [...new Set(jRows.map((r) => r[rel.right_column]))];
192
+ const uncached = rightIds.filter((id) => !cache.has(`${rel.table}:${id}`));
193
+ if (uncached.length > 0) {
194
+ const rightInList = import_drizzle_orm2.sql.join(uncached.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
195
+ const publishedCond = publishedOnly ? import_drizzle_orm2.sql` AND published = true` : import_drizzle_orm2.sql``;
196
+ const entitiesResult = await db.execute(
197
+ import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE id IN (${rightInList})${publishedCond}`
198
+ );
199
+ for (const item of entitiesResult.rows) {
200
+ cache.set(`${rel.table}:${item["id"]}`, item);
201
+ }
202
+ }
203
+ const byLeft = groupBy(jRows, rel.left_column);
204
+ for (const row of rows) {
205
+ const jEntries = byLeft[row["id"]] ?? [];
206
+ row[fieldName] = jEntries.map((jr) => cache.get(`${rel.table}:${jr[rel.right_column]}`)).filter(Boolean);
207
+ }
208
+ } else if (rel.type === "media") {
209
+ const fkValues = rows.map((r) => r[rel.fk_column]).filter(Boolean);
210
+ if (fkValues.length === 0) {
211
+ for (const row of rows) row[fieldName] = null;
212
+ return;
213
+ }
214
+ const unique = [...new Set(fkValues)];
215
+ const uncached = unique.filter((id) => !cache.has(`media:${id}`));
216
+ if (uncached.length > 0) {
217
+ const inList = import_drizzle_orm2.sql.join(uncached.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
218
+ const result = await db.execute(import_drizzle_orm2.sql`SELECT * FROM "media" WHERE id IN (${inList})`);
219
+ for (const item of result.rows) {
220
+ cache.set(`media:${item["id"]}`, item);
221
+ }
222
+ }
223
+ for (const row of rows) {
224
+ const fkVal = row[rel.fk_column];
225
+ row[fieldName] = fkVal ? cache.get(`media:${fkVal}`) ?? null : null;
226
+ }
227
+ }
228
+ }
229
+ async function resolveRelationBareIds(db, rows, fieldName, rel) {
230
+ if (rows.length === 0) return;
231
+ if (rel.type === "paragraph") {
232
+ const parentIds = rows.map((r) => r["id"]);
233
+ const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
234
+ const result = await db.execute(
235
+ 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`
236
+ );
237
+ const byParent = groupBy(result.rows, "parent_id");
238
+ for (const row of rows) {
239
+ row[fieldName] = (byParent[row["id"]] ?? []).map((r) => r["id"]);
240
+ }
241
+ } else if (rel.type === "junction") {
242
+ const parentIds = rows.map((r) => r["id"]);
243
+ const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
244
+ const result = await db.execute(
245
+ 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})`
246
+ );
247
+ const byLeft = groupBy(result.rows, "left_id");
248
+ for (const row of rows) {
249
+ row[fieldName] = (byLeft[row["id"]] ?? []).map((r) => r["right_id"]);
250
+ }
251
+ }
252
+ }
253
+ var import_node_crypto, import_drizzle_orm2;
254
+ var init_relations = __esm({
255
+ "src/relations.ts"() {
256
+ "use strict";
257
+ import_node_crypto = __toESM(require("crypto"));
258
+ import_drizzle_orm2 = require("drizzle-orm");
259
+ }
260
+ });
261
+
262
+ // src/graphql/scalars.ts
263
+ var import_graphql, DateTimeScalar, JSONScalar;
264
+ var init_scalars = __esm({
265
+ "src/graphql/scalars.ts"() {
266
+ "use strict";
267
+ import_graphql = require("graphql");
268
+ DateTimeScalar = new import_graphql.GraphQLScalarType({
269
+ name: "DateTime",
270
+ description: "ISO-8601 date-time string",
271
+ serialize(value) {
272
+ if (value instanceof Date) return value.toISOString();
273
+ if (typeof value === "string") return value;
274
+ if (typeof value === "number") return new Date(value).toISOString();
275
+ throw new TypeError(`DateTime cannot serialize value: ${String(value)}`);
276
+ },
277
+ parseValue(value) {
278
+ if (typeof value !== "string") throw new TypeError("DateTime must be a string");
279
+ return value;
280
+ },
281
+ parseLiteral(ast) {
282
+ if (ast.kind !== import_graphql.Kind.STRING) throw new TypeError("DateTime must be a string");
283
+ return ast.value;
284
+ }
285
+ });
286
+ JSONScalar = new import_graphql.GraphQLScalarType({
287
+ name: "JSON",
288
+ description: "Arbitrary JSON value",
289
+ serialize: (value) => value,
290
+ parseValue: (value) => value,
291
+ parseLiteral: (ast, variables) => (0, import_graphql.valueFromASTUntyped)(ast, variables ?? void 0)
292
+ });
293
+ }
294
+ });
295
+
296
+ // src/graphql/type-mapping.ts
297
+ function scalarOutputType(fieldType) {
298
+ switch (fieldType) {
299
+ case "text/plain":
300
+ case "text/rich":
301
+ return import_graphql2.GraphQLString;
302
+ case "integer":
303
+ return import_graphql2.GraphQLInt;
304
+ case "float":
305
+ return import_graphql2.GraphQLFloat;
306
+ case "boolean":
307
+ return import_graphql2.GraphQLBoolean;
308
+ case "date":
309
+ return DateTimeScalar;
310
+ case "programmatic":
311
+ return JSONScalar;
312
+ default:
313
+ return null;
314
+ }
315
+ }
316
+ var import_graphql2;
317
+ var init_type_mapping = __esm({
318
+ "src/graphql/type-mapping.ts"() {
319
+ "use strict";
320
+ import_graphql2 = require("graphql");
321
+ init_scalars();
322
+ }
323
+ });
324
+
325
+ // src/graphql/naming.ts
326
+ function schemaSegment(machineName) {
327
+ const idx = machineName.indexOf("--");
328
+ return idx === -1 ? machineName : machineName.slice(idx + 2);
329
+ }
330
+ function words(input) {
331
+ return input.split(/[_\s-]+/).filter(Boolean);
332
+ }
333
+ function toPascalCase(input) {
334
+ return words(input).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
335
+ }
336
+ function toCamelCase(input) {
337
+ const pascal = toPascalCase(input);
338
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
339
+ }
340
+ function pluralize(word) {
341
+ if (/[^aeiou]y$/.test(word)) return word.slice(0, -1) + "ies";
342
+ if (/(s|x|z|ch|sh)$/.test(word)) return word + "es";
343
+ return word + "s";
344
+ }
345
+ function isValidGraphQLName(name) {
346
+ return GRAPHQL_NAME.test(name);
347
+ }
348
+ function graphqlTypeName(machineName) {
349
+ return toPascalCase(schemaSegment(machineName));
350
+ }
351
+ function singleQueryName(machineName) {
352
+ return toCamelCase(schemaSegment(machineName));
353
+ }
354
+ function collectionQueryName(machineName) {
355
+ return pluralize(singleQueryName(machineName));
356
+ }
357
+ function buildFieldNameMap(schemaNames) {
358
+ const toG = /* @__PURE__ */ new Map();
359
+ const toS = /* @__PURE__ */ new Map();
360
+ for (const name of schemaNames) {
361
+ const g = toCamelCase(name);
362
+ toG.set(name, g);
363
+ toS.set(g, name);
364
+ }
365
+ return {
366
+ toGraphql: (schemaName) => toG.get(schemaName) ?? toCamelCase(schemaName),
367
+ toSchema: (graphqlName) => toS.get(graphqlName) ?? graphqlName
368
+ };
369
+ }
370
+ var GRAPHQL_NAME;
371
+ var init_naming = __esm({
372
+ "src/graphql/naming.ts"() {
373
+ "use strict";
374
+ GRAPHQL_NAME = /^[_A-Za-z][_0-9A-Za-z]*$/;
375
+ }
376
+ });
377
+
378
+ // src/graphql/filters.ts
379
+ function buildSortFieldEnum(typeName) {
380
+ return new import_graphql3.GraphQLEnumType({
381
+ name: `${typeName}SortField`,
382
+ values: Object.fromEntries(SORTABLE.map((s) => [s.gql, { value: s.column }]))
383
+ });
384
+ }
385
+ function comparableFilterInput(name, scalar) {
386
+ return new import_graphql3.GraphQLInputObjectType({
387
+ name,
388
+ fields: {
389
+ eq: { type: scalar },
390
+ in: { type: new import_graphql3.GraphQLList(new import_graphql3.GraphQLNonNull(scalar)) },
391
+ gt: { type: scalar },
392
+ gte: { type: scalar },
393
+ lt: { type: scalar },
394
+ lte: { type: scalar }
395
+ }
396
+ });
397
+ }
398
+ function equalityFilterInput(name, scalar) {
399
+ return new import_graphql3.GraphQLInputObjectType({
400
+ name,
401
+ fields: {
402
+ eq: { type: scalar },
403
+ in: { type: new import_graphql3.GraphQLList(new import_graphql3.GraphQLNonNull(scalar)) }
404
+ }
405
+ });
406
+ }
407
+ function filterInputForField(fieldType) {
408
+ const scalar = scalarOutputType(fieldType);
409
+ if (fieldType === "boolean") return BooleanFilter;
410
+ if (fieldType === "date") return DateTimeFilter;
411
+ if (fieldType === "integer") return IntFilter;
412
+ if (fieldType === "float") return FloatFilter;
413
+ if (fieldType === "reference") return IDFilter;
414
+ if (scalar === null) return null;
415
+ return StringFilter;
416
+ }
417
+ function buildFilterInputType(type) {
418
+ const fields = {};
419
+ fields["id"] = { type: IDFilter };
420
+ fields["published"] = { type: BooleanFilter };
421
+ fields["createdAt"] = { type: DateTimeFilter };
422
+ fields["updatedAt"] = { type: DateTimeFilter };
423
+ if (type.schema_type === "content-type") fields["slug"] = { type: StringFilter };
424
+ for (const f of type.fields) {
425
+ if (f.field_type === "programmatic" || f.field_type === "paragraph") continue;
426
+ if (f.field_type === "image" || f.field_type === "video" || f.field_type === "file") continue;
427
+ const input = filterInputForField(f.field_type);
428
+ if (input) fields[buildFieldNameMap([f.name]).toGraphql(f.name)] = { type: input };
429
+ }
430
+ if (Object.keys(fields).length === 0) return null;
431
+ return new import_graphql3.GraphQLInputObjectType({ name: `${graphqlTypeName(type.name)}Filter`, fields });
432
+ }
433
+ function translateFilters(input, nameMap) {
434
+ const out = {};
435
+ if (!input) return out;
436
+ for (const [gqlField, raw] of Object.entries(input)) {
437
+ if (raw === null || typeof raw !== "object") continue;
438
+ const column = nameMap.toSchema(gqlField);
439
+ const spec = raw;
440
+ if (spec["eq"] !== void 0) {
441
+ out[column] = spec["eq"];
442
+ } else if (Array.isArray(spec["in"])) {
443
+ out[column] = spec["in"];
444
+ } else {
445
+ const op = {};
446
+ for (const k of ["gt", "gte", "lt", "lte"]) {
447
+ if (spec[k] !== void 0) op[k] = spec[k];
448
+ }
449
+ if (Object.keys(op).length > 0) out[column] = op;
450
+ }
451
+ }
452
+ return out;
453
+ }
454
+ var import_graphql3, SortOrderEnum, SORTABLE, StringFilter, BooleanFilter, DateTimeFilter, IDFilter, IntFilter, FloatFilter;
455
+ var init_filters = __esm({
456
+ "src/graphql/filters.ts"() {
457
+ "use strict";
458
+ import_graphql3 = require("graphql");
459
+ init_naming();
460
+ init_type_mapping();
461
+ init_scalars();
462
+ SortOrderEnum = new import_graphql3.GraphQLEnumType({
463
+ name: "SortOrder",
464
+ values: { ASC: { value: "asc" }, DESC: { value: "desc" } }
465
+ });
466
+ SORTABLE = [
467
+ { gql: "title", column: "title" },
468
+ { gql: "createdAt", column: "created_at" },
469
+ { gql: "updatedAt", column: "updated_at" }
470
+ ];
471
+ StringFilter = equalityFilterInput("StringFilter", import_graphql3.GraphQLString);
472
+ BooleanFilter = new import_graphql3.GraphQLInputObjectType({
473
+ name: "BooleanFilter",
474
+ fields: { eq: { type: import_graphql3.GraphQLBoolean } }
475
+ });
476
+ DateTimeFilter = comparableFilterInput("DateTimeFilter", DateTimeScalar);
477
+ IDFilter = equalityFilterInput("IDFilter", import_graphql3.GraphQLString);
478
+ IntFilter = comparableFilterInput("IntFilter", import_graphql3.GraphQLInt);
479
+ FloatFilter = comparableFilterInput("FloatFilter", import_graphql3.GraphQLFloat);
480
+ }
481
+ });
482
+
483
+ // src/graphql/resolvers.ts
484
+ function codeError2(code, message) {
485
+ return new import_graphql4.GraphQLError(message, { extensions: { code } });
486
+ }
487
+ function isPublished2(item) {
488
+ return !!item && item["published"] === true;
489
+ }
490
+ function scalarFieldResolver(schemaName) {
491
+ return (parent) => parent[schemaName];
492
+ }
493
+ function relationFieldResolver(typeName, schemaFieldName) {
494
+ return (parent, _args, ctx) => ctx.loaders.load(typeName, schemaFieldName, parent);
495
+ }
496
+ function programmaticFieldResolver(typeName, schemaFieldName) {
497
+ return async (parent, _args, ctx) => {
498
+ let p = ctx.programmaticMemo.get(parent);
499
+ if (!p) {
500
+ p = ctx.resolver.resolveItem(typeName, parent);
501
+ ctx.programmaticMemo.set(parent, p);
502
+ }
503
+ return (await p)[schemaFieldName];
504
+ };
505
+ }
506
+ function collectionResolver(typeName, nameMap) {
507
+ return async (_root, args, ctx) => {
508
+ const page = args.page ?? 1;
509
+ const perPage = args.perPage ?? 10;
510
+ if (!Number.isInteger(page) || page < 1) {
511
+ throw codeError2("INVALID_PAGINATION", "page must be \u2265 1");
512
+ }
513
+ if (!Number.isInteger(perPage) || perPage < 1 || perPage > 100) {
514
+ throw codeError2("INVALID_PAGINATION", "perPage must be between 1 and 100");
515
+ }
516
+ const repo = ctx.repos[typeName];
517
+ const result = await repo.findMany({
518
+ published_only: true,
519
+ page,
520
+ per_page: perPage,
521
+ sort_by: args.sortBy ?? "created_at",
522
+ sort_order: args.sortOrder ?? "asc",
523
+ filters: translateFilters(args.filter, nameMap)
524
+ });
525
+ return { data: result.data, meta: result.meta };
526
+ };
527
+ }
528
+ function singleBySlugResolver(typeName) {
529
+ return async (_root, args, ctx) => {
530
+ const repo = ctx.repos[typeName];
531
+ const item = await repo.findBySlug(args.slug);
532
+ return isPublished2(item) ? item : null;
533
+ };
534
+ }
535
+ function singletonResolver(typeName) {
536
+ return async (_root, _args, ctx) => {
537
+ const repo = ctx.repos[typeName];
538
+ const result = await repo.findMany({ published_only: true, page: 1, per_page: 1 });
539
+ return result.data[0] ?? null;
540
+ };
541
+ }
542
+ function taxonomySingleResolver(typeName) {
543
+ return async (_root, args, ctx) => {
544
+ const repo = ctx.repos[typeName];
545
+ const item = await repo.findOne(args.id);
546
+ return isPublished2(item) ? item : null;
547
+ };
548
+ }
549
+ var import_graphql4;
550
+ var init_resolvers = __esm({
551
+ "src/graphql/resolvers.ts"() {
552
+ "use strict";
553
+ import_graphql4 = require("graphql");
554
+ init_filters();
555
+ }
556
+ });
557
+
558
+ // src/graphql/schema.ts
559
+ function buildGraphQLSchema(registry) {
560
+ const objectTypes = /* @__PURE__ */ new Map();
561
+ const enumTypes = /* @__PURE__ */ new Map();
562
+ for (const [name, enumType] of Object.entries(registry.enum_types)) {
563
+ if (enumType.values.every(isValidGraphQLName)) {
564
+ enumTypes.set(
565
+ name,
566
+ new import_graphql5.GraphQLEnumType({
567
+ name: graphqlTypeName(name),
568
+ values: Object.fromEntries(enumType.values.map((v) => [v, { value: v }]))
569
+ })
570
+ );
571
+ } else {
572
+ process.stderr.write(
573
+ `\u26A0 enum '${name}' has values that aren't valid GraphQL identifiers; exposing as String
574
+ `
575
+ );
576
+ }
577
+ }
578
+ function outputTypeForField(field) {
579
+ const scalar = scalarOutputType(field.field_type);
580
+ if (scalar) return field.required ? new import_graphql5.GraphQLNonNull(scalar) : scalar;
581
+ if (field.field_type === "enum") {
582
+ const ref = field.ui_component.component === "select" ? field.ui_component.enum_ref : void 0;
583
+ const et = ref ? enumTypes.get(ref) : void 0;
584
+ const t = et ?? import_graphql5.GraphQLString;
585
+ return field.required ? new import_graphql5.GraphQLNonNull(t) : t;
586
+ }
587
+ if (field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
588
+ return MEDIA;
589
+ }
590
+ if (field.field_type === "paragraph") {
591
+ const ref = field.ui_component.component === "paragraph-embed" ? field.ui_component.ref : void 0;
592
+ const target = ref ? objectTypes.get(ref) : void 0;
593
+ if (ref && !target) {
594
+ process.stderr.write(
595
+ `\u26A0 paragraph field '${field.name}' targets unknown paragraph type '${ref}'; exposing as Media
596
+ `
597
+ );
598
+ }
599
+ return new import_graphql5.GraphQLList(new import_graphql5.GraphQLNonNull(target ?? MEDIA));
600
+ }
601
+ if (field.field_type === "reference") {
602
+ const ref = field.ui_component.component === "typeahead-select" ? field.ui_component.ref : void 0;
603
+ const target = ref ? objectTypes.get(ref) : void 0;
604
+ if (ref && !target) {
605
+ process.stderr.write(`\u26A0 reference field '${field.name}' targets unknown type '${ref}'; exposing as Media
606
+ `);
607
+ }
608
+ const rel = field.ui_component.component === "typeahead-select" ? field.ui_component.rel : void 0;
609
+ const isMany = rel === "many-to-many" || rel === "one-to-many";
610
+ const t = target ?? MEDIA;
611
+ return isMany ? new import_graphql5.GraphQLList(new import_graphql5.GraphQLNonNull(t)) : t;
612
+ }
613
+ return import_graphql5.GraphQLString;
614
+ }
615
+ function buildObjectType(machineName, type) {
616
+ return new import_graphql5.GraphQLObjectType({
617
+ name: graphqlTypeName(machineName),
618
+ fields: () => {
619
+ const fields = {};
620
+ fields["id"] = { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLID), resolve: (p) => p["id"] };
621
+ if (type.schema_type !== "paragraph-type") {
622
+ fields["published"] = { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLBoolean), resolve: (p) => p["published"] };
623
+ }
624
+ if (type.schema_type === "content-type") {
625
+ fields["slug"] = { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLString), resolve: (p) => p["slug"] };
626
+ }
627
+ fields["createdAt"] = { type: new import_graphql5.GraphQLNonNull(DateTimeScalar), resolve: (p) => p["created_at"] };
628
+ fields["updatedAt"] = { type: new import_graphql5.GraphQLNonNull(DateTimeScalar), resolve: (p) => p["updated_at"] };
629
+ for (const field of type.fields) {
630
+ const gqlName = toCamelCase(field.name);
631
+ const outType = outputTypeForField(field);
632
+ let resolve;
633
+ if (field.field_type === "programmatic") {
634
+ resolve = programmaticFieldResolver(machineName, field.name);
635
+ } else if (field.field_type === "reference" || field.field_type === "paragraph" || field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
636
+ resolve = relationFieldResolver(machineName, field.name);
637
+ } else {
638
+ resolve = scalarFieldResolver(field.name);
639
+ }
640
+ fields[gqlName] = { type: outType, resolve };
641
+ }
642
+ return fields;
643
+ }
644
+ });
645
+ }
646
+ const typeNameOwners = /* @__PURE__ */ new Map();
647
+ function registerTypeName(machineName) {
648
+ const gqlName = graphqlTypeName(machineName);
649
+ const owner = typeNameOwners.get(gqlName);
650
+ if (owner !== void 0 && owner !== machineName) {
651
+ throw new Error(
652
+ `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.`
653
+ );
654
+ }
655
+ typeNameOwners.set(gqlName, machineName);
656
+ }
657
+ for (const [name, ct] of Object.entries(registry.content_types)) {
658
+ registerTypeName(name);
659
+ objectTypes.set(name, buildObjectType(name, ct));
660
+ }
661
+ for (const [name, tt] of Object.entries(registry.taxonomy_types)) {
662
+ registerTypeName(name);
663
+ objectTypes.set(name, buildObjectType(name, tt));
664
+ }
665
+ for (const [name, pt] of Object.entries(registry.paragraph_types)) {
666
+ registerTypeName(name);
667
+ objectTypes.set(name, buildObjectType(name, pt));
668
+ }
669
+ const queryFields = {};
670
+ for (const [name, ct] of Object.entries(registry.content_types)) {
671
+ const objType = objectTypes.get(name);
672
+ const nameMap = buildFieldNameMap(ct.fields.map((f) => f.name));
673
+ if (ct.only_one) {
674
+ queryFields[singleQueryName(name)] = { type: objType, resolve: singletonResolver(name) };
675
+ continue;
676
+ }
677
+ const listType = new import_graphql5.GraphQLObjectType({
678
+ name: `${graphqlTypeName(name)}List`,
679
+ fields: {
680
+ data: { type: new import_graphql5.GraphQLNonNull(new import_graphql5.GraphQLList(new import_graphql5.GraphQLNonNull(objType))), resolve: (r) => r.data },
681
+ meta: { type: new import_graphql5.GraphQLNonNull(PAGE_META), resolve: (r) => r.meta }
682
+ }
683
+ });
684
+ const filterType = buildFilterInputType(ct);
685
+ queryFields[collectionQueryName(name)] = {
686
+ type: new import_graphql5.GraphQLNonNull(listType),
687
+ args: {
688
+ page: { type: import_graphql5.GraphQLInt },
689
+ perPage: { type: import_graphql5.GraphQLInt },
690
+ sortBy: { type: buildSortFieldEnum(graphqlTypeName(name)) },
691
+ sortOrder: { type: SortOrderEnum },
692
+ ...filterType ? { filter: { type: filterType } } : {}
693
+ },
694
+ resolve: collectionResolver(name, nameMap)
695
+ };
696
+ queryFields[singleQueryName(name)] = {
697
+ type: objType,
698
+ args: { slug: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLString) } },
699
+ resolve: singleBySlugResolver(name)
700
+ };
701
+ }
702
+ for (const [name, tt] of Object.entries(registry.taxonomy_types)) {
703
+ const objType = objectTypes.get(name);
704
+ const listType = new import_graphql5.GraphQLObjectType({
705
+ name: `${graphqlTypeName(name)}List`,
706
+ fields: {
707
+ data: { type: new import_graphql5.GraphQLNonNull(new import_graphql5.GraphQLList(new import_graphql5.GraphQLNonNull(objType))), resolve: (r) => r.data },
708
+ meta: { type: new import_graphql5.GraphQLNonNull(PAGE_META), resolve: (r) => r.meta }
709
+ }
710
+ });
711
+ queryFields[collectionQueryName(name)] = {
712
+ type: new import_graphql5.GraphQLNonNull(listType),
713
+ args: { page: { type: import_graphql5.GraphQLInt }, perPage: { type: import_graphql5.GraphQLInt } },
714
+ resolve: collectionResolver(name, buildFieldNameMap(tt.fields.map((f) => f.name)))
715
+ };
716
+ queryFields[singleQueryName(name)] = {
717
+ type: objType,
718
+ args: { id: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLID) } },
719
+ resolve: taxonomySingleResolver(name)
720
+ };
721
+ }
722
+ return new import_graphql5.GraphQLSchema({
723
+ query: new import_graphql5.GraphQLObjectType({ name: "Query", fields: queryFields })
724
+ });
725
+ }
726
+ var import_graphql5, PAGE_META, MEDIA;
727
+ var init_schema = __esm({
728
+ "src/graphql/schema.ts"() {
729
+ "use strict";
730
+ import_graphql5 = require("graphql");
731
+ init_scalars();
732
+ init_type_mapping();
733
+ init_naming();
734
+ init_filters();
735
+ init_resolvers();
736
+ PAGE_META = new import_graphql5.GraphQLObjectType({
737
+ name: "PageMeta",
738
+ fields: {
739
+ total: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLInt), resolve: (m) => m.total },
740
+ page: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLInt), resolve: (m) => m.page },
741
+ perPage: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLInt), resolve: (m) => m.per_page },
742
+ totalPages: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLInt), resolve: (m) => m.total_pages },
743
+ hasNext: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLBoolean), resolve: (m) => m.has_next },
744
+ hasPrev: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLBoolean), resolve: (m) => m.has_prev }
745
+ }
746
+ });
747
+ MEDIA = new import_graphql5.GraphQLObjectType({
748
+ name: "Media",
749
+ fields: {
750
+ id: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLID), resolve: (m) => m.id },
751
+ type: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLString), resolve: (m) => m.type },
752
+ url: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLString), resolve: (m) => m.url },
753
+ mimeType: { type: new import_graphql5.GraphQLNonNull(import_graphql5.GraphQLString), resolve: (m) => m.mime_type },
754
+ alt: { type: import_graphql5.GraphQLString, resolve: (m) => m.alt },
755
+ fileSize: { type: import_graphql5.GraphQLInt, resolve: (m) => m.file_size },
756
+ width: { type: import_graphql5.GraphQLInt, resolve: (m) => m.width },
757
+ height: { type: import_graphql5.GraphQLInt, resolve: (m) => m.height },
758
+ duration: { type: import_graphql5.GraphQLInt, resolve: (m) => m.duration }
759
+ }
760
+ });
761
+ }
762
+ });
763
+
764
+ // src/graphql/dataloaders.ts
765
+ function createRelationLoaders(db, registry) {
766
+ const relMaps = /* @__PURE__ */ new Map();
767
+ const loaders = /* @__PURE__ */ new Map();
768
+ const cache = /* @__PURE__ */ new Map();
769
+ function relationsFor(typeName) {
770
+ let m = relMaps.get(typeName);
771
+ if (!m) {
772
+ const type = registry.content_types[typeName] ?? registry.taxonomy_types[typeName] ?? registry.paragraph_types[typeName];
773
+ m = type ? buildRelationsMap(type.fields, registry) : {};
774
+ relMaps.set(typeName, m);
775
+ }
776
+ return m;
777
+ }
778
+ function loaderFor(typeName, fieldName, rel) {
779
+ const key = `${typeName}:${fieldName}`;
780
+ let dl = loaders.get(key);
781
+ if (!dl) {
782
+ dl = new import_dataloader.default(
783
+ async (parents) => {
784
+ const rows = parents;
785
+ await resolveRelationField(db, rows, fieldName, rel, cache, true);
786
+ return rows.map((r) => r[fieldName]);
787
+ },
788
+ // Batch within a tick; do not memoize by parent identity (rows are mutated
789
+ // and may recur across nesting levels).
790
+ { cache: false }
791
+ );
792
+ loaders.set(key, dl);
793
+ }
794
+ return dl;
795
+ }
796
+ return {
797
+ async load(typeName, fieldName, parent) {
798
+ const rel = relationsFor(typeName)[fieldName];
799
+ if (!rel) return null;
800
+ return loaderFor(typeName, fieldName, rel).load(parent);
801
+ }
802
+ };
803
+ }
804
+ var import_dataloader;
805
+ var init_dataloaders = __esm({
806
+ "src/graphql/dataloaders.ts"() {
807
+ "use strict";
808
+ import_dataloader = __toESM(require("dataloader"));
809
+ init_relations();
810
+ }
811
+ });
812
+
813
+ // src/graphql/security.ts
814
+ function buildArmorPlugin(options) {
815
+ const armor = new import_graphql_armor.EnvelopArmor({
816
+ maxDepth: { n: options.maxDepth },
817
+ costLimit: { maxCost: options.maxComplexity },
818
+ maxAliases: { n: 15 },
819
+ maxDirectives: { n: 50 },
820
+ blockFieldSuggestion: { enabled: true }
821
+ // maxTokens is intentionally left at Armor's secure default (n=1000) to bound
822
+ // GraphQL document token count at parse time, a layer the depth/cost/alias/directive
823
+ // limits do not cover.
824
+ });
825
+ return armor.protect();
826
+ }
827
+ function introspectionPlugin(enabled) {
828
+ return {
829
+ onValidate({ addValidationRule }) {
830
+ if (!enabled) addValidationRule(import_graphql6.NoSchemaIntrospectionCustomRule);
831
+ }
832
+ };
833
+ }
834
+ var import_graphql_armor, import_graphql6;
835
+ var init_security = __esm({
836
+ "src/graphql/security.ts"() {
837
+ "use strict";
838
+ import_graphql_armor = require("@escape.tech/graphql-armor");
839
+ import_graphql6 = require("graphql");
840
+ }
841
+ });
842
+
843
+ // src/graphql/handler.ts
844
+ var handler_exports = {};
845
+ __export(handler_exports, {
846
+ createGraphQLHandler: () => createGraphQLHandler
847
+ });
848
+ function createGraphQLHandler(registry, repos, resolver, db, options) {
849
+ const schema = buildGraphQLSchema(registry);
850
+ const { plugins } = buildArmorPlugin({
851
+ maxDepth: options.maxDepth,
852
+ maxComplexity: options.maxComplexity
853
+ });
854
+ const yoga = (0, import_graphql_yoga.createYoga)({
855
+ schema,
856
+ graphqlEndpoint: "/graphql",
857
+ graphiql: options.graphiql,
858
+ landingPage: false,
859
+ // buildArmorPlugin's return type is `unknown[]` (see security.ts) since Armor's
860
+ // plugin shapes aren't part of its public typings; Yoga's `plugins` option only
861
+ // accepts its own `Plugin` type, so the cast bridges the two.
862
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
863
+ plugins: [...plugins, introspectionPlugin(options.introspection)],
864
+ context: () => ({
865
+ db,
866
+ registry,
867
+ repos,
868
+ resolver,
869
+ loaders: createRelationLoaders(db, registry),
870
+ programmaticMemo: /* @__PURE__ */ new WeakMap()
871
+ })
872
+ });
873
+ return (c) => yoga.fetch(c.req.raw, {});
874
+ }
875
+ var import_graphql_yoga;
876
+ var init_handler = __esm({
877
+ "src/graphql/handler.ts"() {
878
+ "use strict";
879
+ import_graphql_yoga = require("graphql-yoga");
880
+ init_schema();
881
+ init_dataloaders();
882
+ init_security();
883
+ }
884
+ });
29
885
 
30
886
  // src/index.ts
31
887
  var index_exports = {};
@@ -72,26 +928,43 @@ function createCorsMiddleware(corsConfig) {
72
928
  }
73
929
 
74
930
  // src/middleware/security-headers.ts
931
+ var GRAPHIQL_CDN = "https://unpkg.com";
75
932
  function createSecurityHeadersMiddleware(options = {}) {
76
933
  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("; ");
934
+ function buildCsp(parts) {
935
+ return [
936
+ "default-src 'self'",
937
+ "img-src 'self' data: https:",
938
+ "media-src 'self' https:",
939
+ `style-src ${parts.styleSrc}`,
940
+ `script-src ${parts.scriptSrc}`,
941
+ "font-src 'self' data:",
942
+ `connect-src ${parts.connectSrc}`,
943
+ ...parts.workerSrc ? [`worker-src ${parts.workerSrc}`] : [],
944
+ "frame-ancestors 'none'",
945
+ "base-uri 'self'",
946
+ "form-action 'self'"
947
+ ].join("; ");
948
+ }
949
+ const csp = buildCsp({
950
+ scriptSrc: "'self'",
951
+ styleSrc: "'self' 'unsafe-inline'",
952
+ connectSrc
953
+ });
954
+ const graphiqlCsp = buildCsp({
955
+ scriptSrc: `'self' 'unsafe-inline' ${GRAPHIQL_CDN}`,
956
+ styleSrc: `'self' 'unsafe-inline' ${GRAPHIQL_CDN}`,
957
+ connectSrc: `${connectSrc} ${GRAPHIQL_CDN}`,
958
+ workerSrc: "'self' blob:"
959
+ });
960
+ const graphiqlPath = options.graphiqlPath;
89
961
  return async function securityHeaders(c, next) {
90
962
  await next();
91
963
  c.res.headers.set("X-Content-Type-Options", "nosniff");
92
964
  c.res.headers.set("X-Frame-Options", "DENY");
93
965
  c.res.headers.set("Referrer-Policy", "no-referrer");
94
- c.res.headers.set("Content-Security-Policy", csp);
966
+ const isGraphiql = graphiqlPath !== void 0 && c.req.path === graphiqlPath;
967
+ c.res.headers.set("Content-Security-Policy", isGraphiql ? graphiqlCsp : csp);
95
968
  };
96
969
  }
97
970
 
@@ -962,228 +1835,8 @@ async function applyMediaReferenceDelta(delta, mediaRepo) {
962
1835
  if (netRemove.length > 0) await mediaRepo.decrementReferenceCount(netRemove);
963
1836
  }
964
1837
 
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
- }
1154
- }
1155
- for (const row of rows) {
1156
- const fkVal = row[rel.fk_column];
1157
- row[fieldName] = fkVal ? cache.get(`media:${fkVal}`) ?? null : null;
1158
- }
1159
- }
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"]);
1182
- }
1183
- }
1184
- }
1185
-
1186
1838
  // src/routes/admin/content.ts
1839
+ init_relations();
1187
1840
  function quoteIdent2(name) {
1188
1841
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
1189
1842
  return `"${name}"`;
@@ -2813,7 +3466,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2813
3466
  var import_drizzle_orm6 = require("drizzle-orm");
2814
3467
  var API_VERSION = (() => {
2815
3468
  try {
2816
- return "0.2.0";
3469
+ return "0.3.0";
2817
3470
  } catch {
2818
3471
  return "0.0.0";
2819
3472
  }
@@ -2920,6 +3573,7 @@ function registerSchemaRoute(app, registry, db) {
2920
3573
  // src/repositories/content.ts
2921
3574
  var import_node_crypto3 = __toESM(require("crypto"));
2922
3575
  var import_drizzle_orm8 = require("drizzle-orm");
3576
+ init_relations();
2923
3577
  var SORTABLE_FIELDS2 = /* @__PURE__ */ new Set(["title", "created_at", "updated_at"]);
2924
3578
  function codeError(code, message) {
2925
3579
  const err = new Error(message);
@@ -2975,7 +3629,7 @@ function whereFragment(conds) {
2975
3629
  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
3630
  }
2977
3631
  function createDrizzleContentRepository(db, tableName, options = {}) {
2978
- const { relations = {} } = options;
3632
+ const { relations = {}, publishedRelations = false } = options;
2979
3633
  function tableRaw() {
2980
3634
  return import_drizzle_orm8.sql.raw(quoteIdent3(tableName));
2981
3635
  }
@@ -2988,7 +3642,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
2988
3642
  }
2989
3643
  for (const [fieldName, rel] of Object.entries(relations)) {
2990
3644
  if (rel.type === "media") {
2991
- await resolveRelationField(db, rows, fieldName, rel, cache);
3645
+ await resolveRelationField(db, rows, fieldName, rel, cache, publishedRelations);
2992
3646
  } else if (!include.includes(fieldName)) {
2993
3647
  await resolveRelationBareIds(db, rows, fieldName, rel);
2994
3648
  }
@@ -2996,7 +3650,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
2996
3650
  for (const fieldName of include) {
2997
3651
  const rel = relations[fieldName];
2998
3652
  if (rel.type !== "media") {
2999
- await resolveRelationField(db, rows, fieldName, rel, cache);
3653
+ await resolveRelationField(db, rows, fieldName, rel, cache, publishedRelations);
3000
3654
  }
3001
3655
  }
3002
3656
  return rows;
@@ -3117,6 +3771,9 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
3117
3771
  };
3118
3772
  }
3119
3773
 
3774
+ // src/app.ts
3775
+ init_relations();
3776
+
3120
3777
  // src/repositories/media.ts
3121
3778
  var import_node_crypto4 = __toESM(require("crypto"));
3122
3779
  var import_drizzle_orm9 = require("drizzle-orm");
@@ -3234,7 +3891,14 @@ function createCmsApp(options) {
3234
3891
  const programmaticResolver = createProgrammaticResolver(resolverMap);
3235
3892
  const app = new import_hono.Hono();
3236
3893
  const uploadOrigins = storage.getUploadOrigins?.() ?? [];
3237
- app.use("*", createSecurityHeadersMiddleware({ connectSrc: uploadOrigins }));
3894
+ const graphiqlEnabled = options.graphql?.enabled === true && options.graphql.graphiql === true;
3895
+ app.use(
3896
+ "*",
3897
+ createSecurityHeadersMiddleware({
3898
+ connectSrc: uploadOrigins,
3899
+ ...graphiqlEnabled && { graphiqlPath: "/graphql" }
3900
+ })
3901
+ );
3238
3902
  app.use("*", createCorsMiddleware(cors ?? { origin: "*", enabled: true }));
3239
3903
  app.onError(errorHandler);
3240
3904
  const listRateLimit = resolveListRateLimit(rateLimit);
@@ -3269,7 +3933,8 @@ function createCmsApp(options) {
3269
3933
  Object.entries(registry.content_types).map(([typeName, ct]) => [
3270
3934
  typeName,
3271
3935
  createDrizzleContentRepository(db, ct.db.table_name, {
3272
- relations: buildRelationsMap(ct.fields, registry)
3936
+ relations: buildRelationsMap(ct.fields, registry),
3937
+ publishedRelations: true
3273
3938
  })
3274
3939
  ])
3275
3940
  );
@@ -3277,7 +3942,8 @@ function createCmsApp(options) {
3277
3942
  Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
3278
3943
  typeName,
3279
3944
  createDrizzleContentRepository(db, tt.db.table_name, {
3280
- relations: buildRelationsMap(tt.fields, registry)
3945
+ relations: buildRelationsMap(tt.fields, registry),
3946
+ publishedRelations: true
3281
3947
  })
3282
3948
  ])
3283
3949
  );
@@ -3318,6 +3984,35 @@ function createCmsApp(options) {
3318
3984
  );
3319
3985
  registerPublicContentRoutes(app, registry, publicRepos, listRateLimit, programmaticResolver);
3320
3986
  registerPublicMediaRoutes(app, mediaRepo, listRateLimit);
3987
+ if (options.graphql?.enabled) {
3988
+ const gqlOptions = options.graphql;
3989
+ let gqlHandler = null;
3990
+ let initError = null;
3991
+ const ready = Promise.resolve().then(() => (init_handler(), handler_exports)).then(({ createGraphQLHandler: createGraphQLHandler2 }) => {
3992
+ gqlHandler = createGraphQLHandler2(registry, publicRepos, programmaticResolver, db, gqlOptions);
3993
+ }).catch((err) => {
3994
+ initError = err;
3995
+ const message = err instanceof Error ? err.message : String(err);
3996
+ process.stderr.write(`\u2717 GraphQL schema failed to initialize; /graphql will return 500. ${message}
3997
+ `);
3998
+ });
3999
+ const invokeGraphQL = async (c) => {
4000
+ if (!gqlHandler && !initError) await ready;
4001
+ if (!gqlHandler) {
4002
+ return c.json(
4003
+ { ok: false, error: { code: "GRAPHQL_INIT_FAILED", message: "GraphQL schema failed to initialize" } },
4004
+ 500
4005
+ );
4006
+ }
4007
+ return gqlHandler(c, async () => {
4008
+ });
4009
+ };
4010
+ if (listRateLimit) {
4011
+ app.all("/graphql", listRateLimit, invokeGraphQL);
4012
+ } else {
4013
+ app.all("/graphql", invokeGraphQL);
4014
+ }
4015
+ }
3321
4016
  registerConfigRoute(
3322
4017
  app,
3323
4018
  {
@@ -3364,13 +4059,24 @@ function createServer(options = {}) {
3364
4059
  }
3365
4060
 
3366
4061
  // src/index.ts
4062
+ function resolveGraphQL(cfg) {
4063
+ const devDefault = process.env["NODE_ENV"] !== "production";
4064
+ return {
4065
+ enabled: cfg.enabled ?? false,
4066
+ maxDepth: cfg.maxDepth ?? 8,
4067
+ maxComplexity: cfg.maxComplexity ?? 1e3,
4068
+ graphiql: cfg.graphiql ?? devDefault,
4069
+ introspection: cfg.introspection ?? cfg.graphiql ?? devDefault
4070
+ };
4071
+ }
3367
4072
  function createAPIAdapter(options = {}) {
3368
4073
  const prefix = options.prefix ?? "/api";
3369
4074
  const media = options.media?.max_file_size !== void 0 ? { max_file_size: options.media.max_file_size } : void 0;
3370
4075
  return {
3371
4076
  prefix,
3372
4077
  ...media !== void 0 && { media },
3373
- ...options.rateLimit !== void 0 && { rateLimit: options.rateLimit }
4078
+ ...options.rateLimit !== void 0 && { rateLimit: options.rateLimit },
4079
+ ...options.graphql !== void 0 && { graphql: resolveGraphQL(options.graphql) }
3374
4080
  };
3375
4081
  }
3376
4082
  // Annotate the CommonJS export names for ESM import in node: