@bobbykim/manguito-cms-api 0.1.1 → 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 });
@@ -27,12 +30,868 @@ 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
+ 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
+ });
885
+
30
886
  // src/index.ts
31
887
  var index_exports = {};
32
888
  __export(index_exports, {
33
889
  createAPIAdapter: () => createAPIAdapter,
34
890
  createCmsApp: () => createCmsApp,
35
- createServer: () => createServer
891
+ createProgrammaticResolver: () => createProgrammaticResolver,
892
+ createServer: () => createServer,
893
+ resolverKey: () => resolverKey,
894
+ validateResolverBindings: () => validateResolverBindings
36
895
  });
37
896
  module.exports = __toCommonJS(index_exports);
38
897
 
@@ -69,26 +928,43 @@ function createCorsMiddleware(corsConfig) {
69
928
  }
70
929
 
71
930
  // src/middleware/security-headers.ts
931
+ var GRAPHIQL_CDN = "https://unpkg.com";
72
932
  function createSecurityHeadersMiddleware(options = {}) {
73
933
  const connectSrc = ["'self'", ...options.connectSrc ?? []].join(" ");
74
- const csp = [
75
- "default-src 'self'",
76
- "img-src 'self' data: https:",
77
- "media-src 'self' https:",
78
- "style-src 'self' 'unsafe-inline'",
79
- "script-src 'self'",
80
- "font-src 'self' data:",
81
- `connect-src ${connectSrc}`,
82
- "frame-ancestors 'none'",
83
- "base-uri 'self'",
84
- "form-action 'self'"
85
- ].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;
86
961
  return async function securityHeaders(c, next) {
87
962
  await next();
88
963
  c.res.headers.set("X-Content-Type-Options", "nosniff");
89
964
  c.res.headers.set("X-Frame-Options", "DENY");
90
965
  c.res.headers.set("Referrer-Policy", "no-referrer");
91
- 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);
92
968
  };
93
969
  }
94
970
 
@@ -455,12 +1331,146 @@ function validateRoles(roles) {
455
1331
  `Fatal: roles registry failed to build \u2014 duplicate role name "${role.name}". Run \`manguito validate\` to check your roles schema.`
456
1332
  );
457
1333
  }
458
- namesSeen.add(role.name);
1334
+ namesSeen.add(role.name);
1335
+ }
1336
+ }
1337
+ function buildRolesRegistry(roles) {
1338
+ validateRoles(roles);
1339
+ return Object.fromEntries(roles.map((r) => [r.name, r]));
1340
+ }
1341
+
1342
+ // src/programmatic/resolve.ts
1343
+ var DEFAULT_TIMEOUT_MS = 5e3;
1344
+ var LIST_CONCURRENCY = 10;
1345
+ function resolverKey(schema, field) {
1346
+ return `${schema}::${field}`;
1347
+ }
1348
+ function validateResolverBindings(registry, resolvers) {
1349
+ const declared = /* @__PURE__ */ new Set();
1350
+ const schemas = [
1351
+ ...Object.values(registry.content_types),
1352
+ ...Object.values(registry.taxonomy_types)
1353
+ ];
1354
+ for (const schema of schemas) {
1355
+ for (const field of schema.fields) {
1356
+ if (field.field_type === "programmatic") {
1357
+ declared.add(resolverKey(schema.name, field.name));
1358
+ }
1359
+ }
1360
+ }
1361
+ const missing = [];
1362
+ for (const key of declared) if (!resolvers.has(key)) missing.push(key);
1363
+ const orphans = [];
1364
+ for (const key of resolvers.keys()) if (!declared.has(key)) orphans.push(key);
1365
+ if (missing.length === 0 && orphans.length === 0) return;
1366
+ const lines = ["\u2717 Programmatic field resolver bindings are invalid."];
1367
+ if (missing.length > 0) {
1368
+ lines.push("", ' Declared as `type: "programmatic"` but no resolver found:');
1369
+ for (const k of missing) lines.push(` - ${k} (add a programmaticField in src/programmatic)`);
1370
+ }
1371
+ if (orphans.length > 0) {
1372
+ lines.push("", " Resolver has no matching programmatic field in the schema:");
1373
+ for (const k of orphans) lines.push(` - ${k}`);
1374
+ }
1375
+ lines.push("", "Exiting.");
1376
+ throw new Error(lines.join("\n"));
1377
+ }
1378
+ function buildContext(row) {
1379
+ return {
1380
+ get: (name) => row[name],
1381
+ record: row
1382
+ };
1383
+ }
1384
+ async function withTimeout(p, ms) {
1385
+ let timer;
1386
+ const timeout = new Promise((_, reject) => {
1387
+ timer = setTimeout(() => reject(new Error("resolver timeout")), ms);
1388
+ });
1389
+ try {
1390
+ return await Promise.race([p, timeout]);
1391
+ } finally {
1392
+ clearTimeout(timer);
1393
+ }
1394
+ }
1395
+ async function runOne(def, row, onError) {
1396
+ const fallback = def.fallback ?? null;
1397
+ try {
1398
+ const result = await withTimeout(
1399
+ Promise.resolve(def.resolve(buildContext(row))),
1400
+ def.timeout ?? DEFAULT_TIMEOUT_MS
1401
+ );
1402
+ return { value: result === void 0 ? null : result, failed: false };
1403
+ } catch (err) {
1404
+ onError(
1405
+ `\u26A0 programmatic field ${resolverKey(def.schema, def.field)} failed: ${err instanceof Error ? err.message : String(err)}`
1406
+ );
1407
+ return { value: fallback, failed: true };
1408
+ }
1409
+ }
1410
+ async function runPool(items, limit, fn) {
1411
+ let i = 0;
1412
+ async function worker() {
1413
+ while (i < items.length) {
1414
+ const idx = i++;
1415
+ await fn(items[idx]);
1416
+ }
459
1417
  }
1418
+ const workers = Array.from({ length: Math.min(limit, items.length) }, worker);
1419
+ await Promise.all(workers);
460
1420
  }
461
- function buildRolesRegistry(roles) {
462
- validateRoles(roles);
463
- return Object.fromEntries(roles.map((r) => [r.name, r]));
1421
+ function createProgrammaticResolver(resolvers, options) {
1422
+ const onError = options?.onError ?? ((message) => {
1423
+ process.stderr.write(message + "\n");
1424
+ });
1425
+ const bySchema = /* @__PURE__ */ new Map();
1426
+ for (const def of resolvers.values()) {
1427
+ const arr = bySchema.get(def.schema) ?? [];
1428
+ arr.push(def);
1429
+ bySchema.set(def.schema, arr);
1430
+ }
1431
+ const cache = /* @__PURE__ */ new Map();
1432
+ async function resolveField(def, row) {
1433
+ const itemId = row["id"] !== void 0 ? String(row["id"]) : "";
1434
+ if (def.cache && itemId) {
1435
+ const key = `${resolverKey(def.schema, def.field)}::${itemId}`;
1436
+ const now = Date.now();
1437
+ const hit = cache.get(key);
1438
+ if (hit && hit.expires > now) return hit.value;
1439
+ const { value: value2, failed } = await runOne(def, row, onError);
1440
+ if (!failed) cache.set(key, { value: value2, expires: now + def.cache.ttl * 1e3 });
1441
+ return value2;
1442
+ }
1443
+ const { value } = await runOne(def, row, onError);
1444
+ return value;
1445
+ }
1446
+ async function resolveItem(schema, row) {
1447
+ const defs = bySchema.get(schema);
1448
+ if (!defs || defs.length === 0) return { ...row };
1449
+ const out = { ...row };
1450
+ await Promise.all(
1451
+ defs.map(async (def) => {
1452
+ out[def.field] = await resolveField(def, row);
1453
+ })
1454
+ );
1455
+ return out;
1456
+ }
1457
+ async function resolveList(schema, rows) {
1458
+ const defs = (bySchema.get(schema) ?? []).filter((d) => d.on_list === true);
1459
+ if (defs.length === 0) return rows.map((r) => ({ ...r }));
1460
+ const out = rows.map((r) => ({ ...r }));
1461
+ const tasks = [];
1462
+ for (let i = 0; i < rows.length; i++) {
1463
+ for (const def of defs) tasks.push({ rowIndex: i, def });
1464
+ }
1465
+ await runPool(tasks, LIST_CONCURRENCY, async ({ rowIndex, def }) => {
1466
+ out[rowIndex][def.field] = await resolveField(def, rows[rowIndex]);
1467
+ });
1468
+ return out;
1469
+ }
1470
+ function hasSchema(schema) {
1471
+ return bySchema.has(schema);
1472
+ }
1473
+ return { resolveItem, resolveList, hasSchema };
464
1474
  }
465
1475
 
466
1476
  // src/routes/query-params.ts
@@ -514,7 +1524,7 @@ function parseFilters(url, validFields) {
514
1524
  function isPublished(item) {
515
1525
  return item["published"] === true;
516
1526
  }
517
- function registerPublicContentRoutes(app, registry, repos, listRateLimit) {
1527
+ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolver) {
518
1528
  function registerListRoute(path, handler) {
519
1529
  if (listRateLimit) {
520
1530
  app.get(path, listRateLimit, handler);
@@ -541,13 +1551,13 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit) {
541
1551
  const basePath = contentType.default_base_path;
542
1552
  const repo = repos[typeName];
543
1553
  if (!repo) continue;
544
- const schemaFieldNames = /* @__PURE__ */ new Set([
545
- ...contentType.fields.map((f) => f.name),
546
- ...contentType.system_fields.map((f) => f.name)
547
- ]);
548
1554
  const relationFieldNames = new Set(
549
1555
  contentType.fields.filter((f) => RELATION_FIELD_TYPES.has(f.field_type)).map((f) => f.name)
550
1556
  );
1557
+ const filterableFieldNames = /* @__PURE__ */ new Set([
1558
+ ...contentType.fields.filter((f) => f.field_type !== "programmatic").map((f) => f.name),
1559
+ ...contentType.system_fields.map((f) => f.name)
1560
+ ]);
551
1561
  if (contentType.only_one) {
552
1562
  app.get(`/api/${basePath}`, async (c) => {
553
1563
  const result = await repo.findMany({ published_only: true, page: 1, per_page: 1 });
@@ -557,7 +1567,9 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit) {
557
1567
  404
558
1568
  );
559
1569
  }
560
- return c.json({ ok: true, data: result.data[0] });
1570
+ let data = result.data[0];
1571
+ if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
1572
+ return c.json({ ok: true, data });
561
1573
  });
562
1574
  } else {
563
1575
  registerListRoute(`/api/${basePath}`, async (c) => {
@@ -600,7 +1612,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit) {
600
1612
  400
601
1613
  );
602
1614
  }
603
- const filtersResult = parseFilters(c.req.url, schemaFieldNames);
1615
+ const filtersResult = parseFilters(c.req.url, filterableFieldNames);
604
1616
  if (!filtersResult.ok) {
605
1617
  return c.json(
606
1618
  {
@@ -637,6 +1649,10 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit) {
637
1649
  filters: filtersResult.filters,
638
1650
  include
639
1651
  });
1652
+ if (resolver?.hasSchema(typeName)) {
1653
+ const resolved = await resolver.resolveList(typeName, result.data);
1654
+ return c.json({ ...result, data: resolved });
1655
+ }
640
1656
  return c.json(result);
641
1657
  });
642
1658
  app.get(`/api/${basePath}/:slug`, async (c) => {
@@ -666,7 +1682,9 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit) {
666
1682
  404
667
1683
  );
668
1684
  }
669
- return c.json({ ok: true, data: item });
1685
+ let data = item;
1686
+ if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
1687
+ return c.json({ ok: true, data });
670
1688
  });
671
1689
  }
672
1690
  }
@@ -692,6 +1710,10 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit) {
692
1710
  page: pagination.page,
693
1711
  per_page: pagination.per_page
694
1712
  });
1713
+ if (resolver?.hasSchema(typeName)) {
1714
+ const resolved = await resolver.resolveList(typeName, result.data);
1715
+ return c.json({ ...result, data: resolved });
1716
+ }
695
1717
  return c.json(result);
696
1718
  });
697
1719
  app.get(`/api/taxonomy/${typeName}/:id`, async (c) => {
@@ -706,7 +1728,9 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit) {
706
1728
  404
707
1729
  );
708
1730
  }
709
- return c.json({ ok: true, data: item });
1731
+ let data = item;
1732
+ if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
1733
+ return c.json({ ok: true, data });
710
1734
  });
711
1735
  }
712
1736
  }
@@ -811,228 +1835,8 @@ async function applyMediaReferenceDelta(delta, mediaRepo) {
811
1835
  if (netRemove.length > 0) await mediaRepo.decrementReferenceCount(netRemove);
812
1836
  }
813
1837
 
814
- // src/relations.ts
815
- var import_node_crypto = __toESM(require("crypto"));
816
- var import_drizzle_orm2 = require("drizzle-orm");
817
- function quoteIdent(name) {
818
- if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
819
- return `"${name}"`;
820
- }
821
- function paragraphMediaFieldNames(pType) {
822
- return new Set(
823
- pType.fields.filter((f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file").map((f) => f.name)
824
- );
825
- }
826
- async function fetchParagraphMediaIds(db, pType, itemId, fieldName) {
827
- 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);
828
- if (mediaColumns.length === 0) return [];
829
- const tbl = import_drizzle_orm2.sql.raw(quoteIdent(pType.db.table_name));
830
- const cols = import_drizzle_orm2.sql.join(mediaColumns.map((c) => import_drizzle_orm2.sql.raw(quoteIdent(c))), import_drizzle_orm2.sql`, `);
831
- const result = await db.execute(
832
- import_drizzle_orm2.sql`SELECT ${cols} FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`
833
- );
834
- const ids = [];
835
- for (const row of result.rows) {
836
- for (const col of mediaColumns) {
837
- const v = row[col];
838
- if (typeof v === "string" && v !== "") ids.push(v);
839
- }
840
- }
841
- return ids;
842
- }
843
- async function persistParagraphField(db, itemId, parentType, fieldName, pType, items) {
844
- const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName);
845
- const tbl = import_drizzle_orm2.sql.raw(quoteIdent(pType.db.table_name));
846
- await db.execute(import_drizzle_orm2.sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
847
- const mediaFieldNames = paragraphMediaFieldNames(pType);
848
- const added = [];
849
- for (let i = 0; i < items.length; i++) {
850
- const pItem = items[i];
851
- const data = {
852
- id: import_node_crypto.default.randomUUID(),
853
- parent_id: itemId,
854
- parent_type: parentType,
855
- parent_field: fieldName,
856
- order: i,
857
- created_at: /* @__PURE__ */ new Date(),
858
- updated_at: /* @__PURE__ */ new Date()
859
- };
860
- for (const pf of pType.fields) {
861
- if (!pf.db_column || pf.db_column.junction) continue;
862
- data[pf.db_column.column_name] = pItem[pf.name] ?? null;
863
- }
864
- const cols = import_drizzle_orm2.sql.join(Object.keys(data).map((k) => import_drizzle_orm2.sql.raw(quoteIdent(k))), import_drizzle_orm2.sql`, `);
865
- const vals = import_drizzle_orm2.sql.join(Object.values(data).map((v) => import_drizzle_orm2.sql`${v}`), import_drizzle_orm2.sql`, `);
866
- await db.execute(import_drizzle_orm2.sql`INSERT INTO ${tbl} (${cols}) VALUES (${vals})`);
867
- for (const name of mediaFieldNames) {
868
- const val = pItem[name];
869
- if (typeof val === "string" && val !== "") added.push(val);
870
- }
871
- }
872
- return { added, removed };
873
- }
874
- async function deleteParagraphField(db, itemId, fieldName, pType) {
875
- const removed = await fetchParagraphMediaIds(db, pType, itemId, fieldName);
876
- const tbl = import_drizzle_orm2.sql.raw(quoteIdent(pType.db.table_name));
877
- await db.execute(import_drizzle_orm2.sql`DELETE FROM ${tbl} WHERE parent_id = ${itemId} AND parent_field = ${fieldName}`);
878
- return { added: [], removed };
879
- }
880
- async function persistJunctionField(db, itemId, junction, relatedIds) {
881
- const tbl = import_drizzle_orm2.sql.raw(quoteIdent(junction.table_name));
882
- const left = import_drizzle_orm2.sql.raw(quoteIdent(junction.left_column));
883
- const right = import_drizzle_orm2.sql.raw(quoteIdent(junction.right_column));
884
- await db.execute(import_drizzle_orm2.sql`DELETE FROM ${tbl} WHERE ${left} = ${itemId}`);
885
- for (const rid of relatedIds) {
886
- await db.execute(import_drizzle_orm2.sql`INSERT INTO ${tbl} (${left}, ${right}) VALUES (${itemId}, ${rid})`);
887
- }
888
- }
889
- function buildRelationsMap(fields, registry) {
890
- const relations = {};
891
- for (const field of fields) {
892
- if (field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
893
- if (!field.db_column) continue;
894
- relations[field.name] = { type: "media", fk_column: field.db_column.column_name };
895
- } else if (field.field_type === "paragraph") {
896
- const ref = field.ui_component.ref;
897
- const pType = ref ? registry.paragraph_types[ref] : void 0;
898
- if (!pType) continue;
899
- relations[field.name] = { type: "paragraph", table: pType.db.table_name };
900
- } else if (field.field_type === "reference") {
901
- if (!field.db_column) continue;
902
- if (field.db_column.junction) {
903
- const j = field.db_column.junction;
904
- relations[field.name] = {
905
- type: "junction",
906
- table: j.right_table,
907
- junction_table: j.table_name,
908
- left_column: j.left_column,
909
- right_column: j.right_column,
910
- order_column: j.order_column
911
- };
912
- } else if (field.db_column.foreign_key) {
913
- relations[field.name] = {
914
- type: "reference",
915
- table: field.db_column.foreign_key.table,
916
- fk_column: field.db_column.column_name
917
- };
918
- }
919
- }
920
- }
921
- return relations;
922
- }
923
- function groupBy(items, key) {
924
- const result = {};
925
- for (const item of items) {
926
- const k = String(item[key]);
927
- if (!result[k]) result[k] = [];
928
- result[k].push(item);
929
- }
930
- return result;
931
- }
932
- async function resolveRelationField(db, rows, fieldName, rel, cache) {
933
- if (rows.length === 0) return;
934
- if (rel.type === "paragraph") {
935
- const parentIds = rows.map((r) => r["id"]);
936
- const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
937
- const result = await db.execute(
938
- import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) ORDER BY "order" ASC`
939
- );
940
- const byParent = groupBy(result.rows, "parent_id");
941
- for (const row of rows) {
942
- row[fieldName] = byParent[row["id"]] ?? [];
943
- }
944
- } else if (rel.type === "reference") {
945
- const fkValues = rows.map((r) => r[rel.fk_column]).filter(Boolean);
946
- if (fkValues.length === 0) {
947
- for (const row of rows) row[fieldName] = null;
948
- return;
949
- }
950
- const unique = [...new Set(fkValues)];
951
- const uncached = unique.filter((id) => !cache.has(`${rel.table}:${id}`));
952
- if (uncached.length > 0) {
953
- const inList = import_drizzle_orm2.sql.join(uncached.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
954
- const result = await db.execute(
955
- import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE id IN (${inList})`
956
- );
957
- for (const item of result.rows) {
958
- cache.set(`${rel.table}:${item["id"]}`, item);
959
- }
960
- }
961
- for (const row of rows) {
962
- const fkVal = row[rel.fk_column];
963
- row[fieldName] = fkVal ? cache.get(`${rel.table}:${fkVal}`) ?? null : null;
964
- }
965
- } else if (rel.type === "junction") {
966
- const parentIds = rows.map((r) => r["id"]);
967
- const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
968
- const orderBy = rel.order_column ? import_drizzle_orm2.sql` ORDER BY "order" ASC` : import_drizzle_orm2.sql``;
969
- const junctionResult = await db.execute(
970
- 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}`
971
- );
972
- const jRows = junctionResult.rows;
973
- const rightIds = [...new Set(jRows.map((r) => r[rel.right_column]))];
974
- const uncached = rightIds.filter((id) => !cache.has(`${rel.table}:${id}`));
975
- if (uncached.length > 0) {
976
- const rightInList = import_drizzle_orm2.sql.join(uncached.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
977
- const entitiesResult = await db.execute(
978
- import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE id IN (${rightInList})`
979
- );
980
- for (const item of entitiesResult.rows) {
981
- cache.set(`${rel.table}:${item["id"]}`, item);
982
- }
983
- }
984
- const byLeft = groupBy(jRows, rel.left_column);
985
- for (const row of rows) {
986
- const jEntries = byLeft[row["id"]] ?? [];
987
- row[fieldName] = jEntries.map((jr) => cache.get(`${rel.table}:${jr[rel.right_column]}`)).filter(Boolean);
988
- }
989
- } else if (rel.type === "media") {
990
- const fkValues = rows.map((r) => r[rel.fk_column]).filter(Boolean);
991
- if (fkValues.length === 0) {
992
- for (const row of rows) row[fieldName] = null;
993
- return;
994
- }
995
- const unique = [...new Set(fkValues)];
996
- const uncached = unique.filter((id) => !cache.has(`media:${id}`));
997
- if (uncached.length > 0) {
998
- const inList = import_drizzle_orm2.sql.join(uncached.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
999
- const result = await db.execute(import_drizzle_orm2.sql`SELECT * FROM "media" WHERE id IN (${inList})`);
1000
- for (const item of result.rows) {
1001
- cache.set(`media:${item["id"]}`, item);
1002
- }
1003
- }
1004
- for (const row of rows) {
1005
- const fkVal = row[rel.fk_column];
1006
- row[fieldName] = fkVal ? cache.get(`media:${fkVal}`) ?? null : null;
1007
- }
1008
- }
1009
- }
1010
- async function resolveRelationBareIds(db, rows, fieldName, rel) {
1011
- if (rows.length === 0) return;
1012
- if (rel.type === "paragraph") {
1013
- const parentIds = rows.map((r) => r["id"]);
1014
- const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
1015
- const result = await db.execute(
1016
- 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`
1017
- );
1018
- const byParent = groupBy(result.rows, "parent_id");
1019
- for (const row of rows) {
1020
- row[fieldName] = (byParent[row["id"]] ?? []).map((r) => r["id"]);
1021
- }
1022
- } else if (rel.type === "junction") {
1023
- const parentIds = rows.map((r) => r["id"]);
1024
- const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
1025
- const result = await db.execute(
1026
- 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})`
1027
- );
1028
- const byLeft = groupBy(result.rows, "left_id");
1029
- for (const row of rows) {
1030
- row[fieldName] = (byLeft[row["id"]] ?? []).map((r) => r["right_id"]);
1031
- }
1032
- }
1033
- }
1034
-
1035
1838
  // src/routes/admin/content.ts
1839
+ init_relations();
1036
1840
  function quoteIdent2(name) {
1037
1841
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
1038
1842
  return `"${name}"`;
@@ -2662,7 +3466,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2662
3466
  var import_drizzle_orm6 = require("drizzle-orm");
2663
3467
  var API_VERSION = (() => {
2664
3468
  try {
2665
- return "0.1.1";
3469
+ return "0.3.0";
2666
3470
  } catch {
2667
3471
  return "0.0.0";
2668
3472
  }
@@ -2769,6 +3573,7 @@ function registerSchemaRoute(app, registry, db) {
2769
3573
  // src/repositories/content.ts
2770
3574
  var import_node_crypto3 = __toESM(require("crypto"));
2771
3575
  var import_drizzle_orm8 = require("drizzle-orm");
3576
+ init_relations();
2772
3577
  var SORTABLE_FIELDS2 = /* @__PURE__ */ new Set(["title", "created_at", "updated_at"]);
2773
3578
  function codeError(code, message) {
2774
3579
  const err = new Error(message);
@@ -2824,7 +3629,7 @@ function whereFragment(conds) {
2824
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``;
2825
3630
  }
2826
3631
  function createDrizzleContentRepository(db, tableName, options = {}) {
2827
- const { relations = {} } = options;
3632
+ const { relations = {}, publishedRelations = false } = options;
2828
3633
  function tableRaw() {
2829
3634
  return import_drizzle_orm8.sql.raw(quoteIdent3(tableName));
2830
3635
  }
@@ -2837,7 +3642,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
2837
3642
  }
2838
3643
  for (const [fieldName, rel] of Object.entries(relations)) {
2839
3644
  if (rel.type === "media") {
2840
- await resolveRelationField(db, rows, fieldName, rel, cache);
3645
+ await resolveRelationField(db, rows, fieldName, rel, cache, publishedRelations);
2841
3646
  } else if (!include.includes(fieldName)) {
2842
3647
  await resolveRelationBareIds(db, rows, fieldName, rel);
2843
3648
  }
@@ -2845,7 +3650,7 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
2845
3650
  for (const fieldName of include) {
2846
3651
  const rel = relations[fieldName];
2847
3652
  if (rel.type !== "media") {
2848
- await resolveRelationField(db, rows, fieldName, rel, cache);
3653
+ await resolveRelationField(db, rows, fieldName, rel, cache, publishedRelations);
2849
3654
  }
2850
3655
  }
2851
3656
  return rows;
@@ -2966,6 +3771,9 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
2966
3771
  };
2967
3772
  }
2968
3773
 
3774
+ // src/app.ts
3775
+ init_relations();
3776
+
2969
3777
  // src/repositories/media.ts
2970
3778
  var import_node_crypto4 = __toESM(require("crypto"));
2971
3779
  var import_drizzle_orm9 = require("drizzle-orm");
@@ -3078,9 +3886,19 @@ function createCmsApp(options) {
3078
3886
  const cmsName = options.name ?? "Manguito CMS";
3079
3887
  const maxFileSize = media?.max_file_size;
3080
3888
  const rolesRegistry = buildRolesRegistry(registry.roles.roles);
3889
+ const resolverMap = options.resolvers ?? /* @__PURE__ */ new Map();
3890
+ validateResolverBindings(registry, resolverMap);
3891
+ const programmaticResolver = createProgrammaticResolver(resolverMap);
3081
3892
  const app = new import_hono.Hono();
3082
3893
  const uploadOrigins = storage.getUploadOrigins?.() ?? [];
3083
- 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
+ );
3084
3902
  app.use("*", createCorsMiddleware(cors ?? { origin: "*", enabled: true }));
3085
3903
  app.onError(errorHandler);
3086
3904
  const listRateLimit = resolveListRateLimit(rateLimit);
@@ -3115,7 +3933,8 @@ function createCmsApp(options) {
3115
3933
  Object.entries(registry.content_types).map(([typeName, ct]) => [
3116
3934
  typeName,
3117
3935
  createDrizzleContentRepository(db, ct.db.table_name, {
3118
- relations: buildRelationsMap(ct.fields, registry)
3936
+ relations: buildRelationsMap(ct.fields, registry),
3937
+ publishedRelations: true
3119
3938
  })
3120
3939
  ])
3121
3940
  );
@@ -3123,7 +3942,8 @@ function createCmsApp(options) {
3123
3942
  Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
3124
3943
  typeName,
3125
3944
  createDrizzleContentRepository(db, tt.db.table_name, {
3126
- relations: buildRelationsMap(tt.fields, registry)
3945
+ relations: buildRelationsMap(tt.fields, registry),
3946
+ publishedRelations: true
3127
3947
  })
3128
3948
  ])
3129
3949
  );
@@ -3162,8 +3982,37 @@ function createCmsApp(options) {
3162
3982
  paths: {}
3163
3983
  })
3164
3984
  );
3165
- registerPublicContentRoutes(app, registry, publicRepos, listRateLimit);
3985
+ registerPublicContentRoutes(app, registry, publicRepos, listRateLimit, programmaticResolver);
3166
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
+ }
3167
4016
  registerConfigRoute(
3168
4017
  app,
3169
4018
  {
@@ -3210,19 +4059,33 @@ function createServer(options = {}) {
3210
4059
  }
3211
4060
 
3212
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
+ }
3213
4072
  function createAPIAdapter(options = {}) {
3214
4073
  const prefix = options.prefix ?? "/api";
3215
4074
  const media = options.media?.max_file_size !== void 0 ? { max_file_size: options.media.max_file_size } : void 0;
3216
4075
  return {
3217
4076
  prefix,
3218
4077
  ...media !== void 0 && { media },
3219
- ...options.rateLimit !== void 0 && { rateLimit: options.rateLimit }
4078
+ ...options.rateLimit !== void 0 && { rateLimit: options.rateLimit },
4079
+ ...options.graphql !== void 0 && { graphql: resolveGraphQL(options.graphql) }
3220
4080
  };
3221
4081
  }
3222
4082
  // Annotate the CommonJS export names for ESM import in node:
3223
4083
  0 && (module.exports = {
3224
4084
  createAPIAdapter,
3225
4085
  createCmsApp,
3226
- createServer
4086
+ createProgrammaticResolver,
4087
+ createServer,
4088
+ resolverKey,
4089
+ validateResolverBindings
3227
4090
  });
3228
4091
  //# sourceMappingURL=index.js.map