@bobbykim/manguito-cms-api 0.3.3 → 0.4.1
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/{chunk-TK4DNRFF.mjs → chunk-4F2CRXPG.mjs} +76 -12
- package/dist/chunk-4F2CRXPG.mjs.map +1 -0
- package/dist/{chunk-GM55HGWA.mjs → chunk-S4NT6FUM.mjs} +39 -25
- package/dist/chunk-S4NT6FUM.mjs.map +1 -0
- package/dist/graphql/index.d.mts +14 -1
- package/dist/graphql/index.d.ts +14 -1
- package/dist/graphql/index.js +77 -33
- package/dist/graphql/index.js.map +1 -1
- package/dist/graphql/index.mjs +2 -2
- package/dist/handler-7LVYEU5C.mjs +8 -0
- package/dist/index.js +348 -142
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +236 -111
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-GM55HGWA.mjs.map +0 -1
- package/dist/chunk-TK4DNRFF.mjs.map +0 -1
- package/dist/handler-CGHZG3J5.mjs +0 -8
- /package/dist/{handler-CGHZG3J5.mjs.map → handler-7LVYEU5C.mjs.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -200,22 +200,34 @@ function groupBy(items, key) {
|
|
|
200
200
|
}
|
|
201
201
|
return result;
|
|
202
202
|
}
|
|
203
|
+
function needsFkResolution(row, fkColumn, fieldName) {
|
|
204
|
+
if (fkColumn !== fieldName && !(fkColumn in row)) return false;
|
|
205
|
+
const current = row[fieldName];
|
|
206
|
+
return !(typeof current === "object" && current !== null);
|
|
207
|
+
}
|
|
203
208
|
async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOnly = false) {
|
|
204
209
|
if (rows.length === 0) return;
|
|
210
|
+
const batch = [...new Set(rows)];
|
|
205
211
|
if (rel.type === "paragraph") {
|
|
206
|
-
const parentIds =
|
|
212
|
+
const parentIds = batch.map((r) => r["id"]);
|
|
207
213
|
const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
|
|
208
214
|
const result = await db.execute(
|
|
209
215
|
import_drizzle_orm2.sql`SELECT * FROM ${import_drizzle_orm2.sql.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY "order" ASC`
|
|
210
216
|
);
|
|
211
217
|
const byParent = groupBy(result.rows, "parent_id");
|
|
212
|
-
for (const row of
|
|
218
|
+
for (const row of batch) {
|
|
213
219
|
row[fieldName] = byParent[row["id"]] ?? [];
|
|
214
220
|
}
|
|
215
221
|
} else if (rel.type === "reference") {
|
|
216
|
-
const
|
|
222
|
+
const dropFk = rel.fk_column !== fieldName;
|
|
223
|
+
const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column, fieldName));
|
|
224
|
+
if (pending.length === 0) return;
|
|
225
|
+
const fkValues = pending.map((r) => r[rel.fk_column]).filter(Boolean);
|
|
217
226
|
if (fkValues.length === 0) {
|
|
218
|
-
for (const row of
|
|
227
|
+
for (const row of pending) {
|
|
228
|
+
row[fieldName] = null;
|
|
229
|
+
if (dropFk) delete row[rel.fk_column];
|
|
230
|
+
}
|
|
219
231
|
return;
|
|
220
232
|
}
|
|
221
233
|
const unique = [...new Set(fkValues)];
|
|
@@ -230,12 +242,13 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
|
|
|
230
242
|
cache.set(`${rel.table}:${item["id"]}`, item);
|
|
231
243
|
}
|
|
232
244
|
}
|
|
233
|
-
for (const row of
|
|
245
|
+
for (const row of pending) {
|
|
234
246
|
const fkVal = row[rel.fk_column];
|
|
235
247
|
row[fieldName] = fkVal ? cache.get(`${rel.table}:${fkVal}`) ?? null : null;
|
|
248
|
+
if (dropFk) delete row[rel.fk_column];
|
|
236
249
|
}
|
|
237
250
|
} else if (rel.type === "junction") {
|
|
238
|
-
const parentIds =
|
|
251
|
+
const parentIds = batch.map((r) => r["id"]);
|
|
239
252
|
const inList = import_drizzle_orm2.sql.join(parentIds.map((id) => import_drizzle_orm2.sql`${id}`), import_drizzle_orm2.sql`, `);
|
|
240
253
|
const orderBy = rel.order_column ? import_drizzle_orm2.sql` ORDER BY "order" ASC` : import_drizzle_orm2.sql``;
|
|
241
254
|
const junctionResult = await db.execute(
|
|
@@ -255,14 +268,20 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
|
|
|
255
268
|
}
|
|
256
269
|
}
|
|
257
270
|
const byLeft = groupBy(jRows, rel.left_column);
|
|
258
|
-
for (const row of
|
|
271
|
+
for (const row of batch) {
|
|
259
272
|
const jEntries = byLeft[row["id"]] ?? [];
|
|
260
273
|
row[fieldName] = jEntries.map((jr) => cache.get(`${rel.table}:${jr[rel.right_column]}`)).filter(Boolean);
|
|
261
274
|
}
|
|
262
275
|
} else if (rel.type === "media") {
|
|
263
|
-
const
|
|
276
|
+
const dropFk = rel.fk_column !== fieldName;
|
|
277
|
+
const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column, fieldName));
|
|
278
|
+
if (pending.length === 0) return;
|
|
279
|
+
const fkValues = pending.map((r) => r[rel.fk_column]).filter(Boolean);
|
|
264
280
|
if (fkValues.length === 0) {
|
|
265
|
-
for (const row of
|
|
281
|
+
for (const row of pending) {
|
|
282
|
+
row[fieldName] = null;
|
|
283
|
+
if (dropFk) delete row[rel.fk_column];
|
|
284
|
+
}
|
|
266
285
|
return;
|
|
267
286
|
}
|
|
268
287
|
const unique = [...new Set(fkValues)];
|
|
@@ -274,9 +293,10 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
|
|
|
274
293
|
cache.set(`media:${item["id"]}`, item);
|
|
275
294
|
}
|
|
276
295
|
}
|
|
277
|
-
for (const row of
|
|
296
|
+
for (const row of pending) {
|
|
278
297
|
const fkVal = row[rel.fk_column];
|
|
279
298
|
row[fieldName] = fkVal ? cache.get(`media:${fkVal}`) ?? null : null;
|
|
299
|
+
if (dropFk) delete row[rel.fk_column];
|
|
280
300
|
}
|
|
281
301
|
}
|
|
282
302
|
}
|
|
@@ -313,6 +333,53 @@ var init_relations = __esm({
|
|
|
313
333
|
}
|
|
314
334
|
});
|
|
315
335
|
|
|
336
|
+
// src/field-keys.ts
|
|
337
|
+
function isColumnBacked(field) {
|
|
338
|
+
const col = field.db_column;
|
|
339
|
+
if (col === null) return false;
|
|
340
|
+
if (col.column_name === "") return false;
|
|
341
|
+
if (col.junction) return false;
|
|
342
|
+
return true;
|
|
343
|
+
}
|
|
344
|
+
function createFieldKeyMap(fields) {
|
|
345
|
+
const labelToColumn = /* @__PURE__ */ new Map();
|
|
346
|
+
const columnToLabel = /* @__PURE__ */ new Map();
|
|
347
|
+
for (const f of fields) {
|
|
348
|
+
if (!isColumnBacked(f)) continue;
|
|
349
|
+
labelToColumn.set(f.name, f.db_column.column_name);
|
|
350
|
+
columnToLabel.set(f.db_column.column_name, f.name);
|
|
351
|
+
}
|
|
352
|
+
for (const f of fields) {
|
|
353
|
+
const columnOwner = columnToLabel.get(f.name);
|
|
354
|
+
if (columnOwner !== void 0 && columnOwner !== f.name) {
|
|
355
|
+
throw new Error(
|
|
356
|
+
`Fatal: field key map failed to build \u2014 field label "${f.name}" collides with the storage column of field "${columnOwner}". A field label may not reuse another field's column name. Rename the field, then run \`manguito validate\` to check your schema.`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const diverges = [...labelToColumn].some(([label, column]) => label !== column);
|
|
361
|
+
function remap(input, lookup) {
|
|
362
|
+
const out = {};
|
|
363
|
+
for (const key of Object.keys(input)) {
|
|
364
|
+
out[lookup.get(key) ?? key] = input[key];
|
|
365
|
+
}
|
|
366
|
+
return out;
|
|
367
|
+
}
|
|
368
|
+
return {
|
|
369
|
+
toStorage: (input) => remap(input, labelToColumn),
|
|
370
|
+
toLabels: (row) => remap(row, columnToLabel),
|
|
371
|
+
columnFor: (label) => labelToColumn.get(label),
|
|
372
|
+
labelFor: (column) => columnToLabel.get(column),
|
|
373
|
+
labels: [...labelToColumn.keys()],
|
|
374
|
+
diverges
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
var init_field_keys = __esm({
|
|
378
|
+
"src/field-keys.ts"() {
|
|
379
|
+
"use strict";
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
316
383
|
// src/graphql/scalars.ts
|
|
317
384
|
var import_graphql, DateTimeScalar, JSONScalar;
|
|
318
385
|
var init_scalars = __esm({
|
|
@@ -433,7 +500,7 @@ var init_naming = __esm({
|
|
|
433
500
|
function buildSortFieldEnum(typeName) {
|
|
434
501
|
return new import_graphql3.GraphQLEnumType({
|
|
435
502
|
name: `${typeName}SortField`,
|
|
436
|
-
values: Object.fromEntries(SORTABLE.map((s) => [s.gql, { value: s.
|
|
503
|
+
values: Object.fromEntries(SORTABLE.map((s) => [s.gql, { value: s.value }]))
|
|
437
504
|
});
|
|
438
505
|
}
|
|
439
506
|
function comparableFilterInput(name, scalar) {
|
|
@@ -484,12 +551,13 @@ function buildFilterInputType(type) {
|
|
|
484
551
|
if (Object.keys(fields).length === 0) return null;
|
|
485
552
|
return new import_graphql3.GraphQLInputObjectType({ name: `${graphqlTypeName(type.name)}Filter`, fields });
|
|
486
553
|
}
|
|
487
|
-
function translateFilters(input, nameMap) {
|
|
554
|
+
function translateFilters(input, nameMap, columnFor) {
|
|
488
555
|
const out = {};
|
|
489
556
|
if (!input) return out;
|
|
490
557
|
for (const [gqlField, raw] of Object.entries(input)) {
|
|
491
558
|
if (raw === null || typeof raw !== "object") continue;
|
|
492
|
-
const
|
|
559
|
+
const label = nameMap.toSchema(gqlField);
|
|
560
|
+
const column = columnFor?.(label) ?? label;
|
|
493
561
|
const spec = raw;
|
|
494
562
|
if (spec["eq"] !== void 0) {
|
|
495
563
|
out[column] = spec["eq"];
|
|
@@ -518,9 +586,9 @@ var init_filters = __esm({
|
|
|
518
586
|
values: { ASC: { value: "asc" }, DESC: { value: "desc" } }
|
|
519
587
|
});
|
|
520
588
|
SORTABLE = [
|
|
521
|
-
{ gql: "title",
|
|
522
|
-
{ gql: "createdAt",
|
|
523
|
-
{ gql: "updatedAt",
|
|
589
|
+
{ gql: "title", value: "title" },
|
|
590
|
+
{ gql: "createdAt", value: "created_at" },
|
|
591
|
+
{ gql: "updatedAt", value: "updated_at" }
|
|
524
592
|
];
|
|
525
593
|
StringFilter = equalityFilterInput("StringFilter", import_graphql3.GraphQLString);
|
|
526
594
|
BooleanFilter = new import_graphql3.GraphQLInputObjectType({
|
|
@@ -541,29 +609,34 @@ function codeError2(code, message) {
|
|
|
541
609
|
function isPublished2(item) {
|
|
542
610
|
return !!item && item["published"] === true;
|
|
543
611
|
}
|
|
544
|
-
function
|
|
545
|
-
|
|
612
|
+
function resolveFieldValue(field, row) {
|
|
613
|
+
const key = isColumnBacked(field) ? field.db_column.column_name : field.name;
|
|
614
|
+
return row[key] ?? null;
|
|
615
|
+
}
|
|
616
|
+
function scalarFieldResolver(field) {
|
|
617
|
+
return (parent) => resolveFieldValue(field, parent);
|
|
546
618
|
}
|
|
547
619
|
function relationFieldResolver(typeName, schemaFieldName) {
|
|
548
620
|
return (parent, _args, ctx) => ctx.loaders.load(typeName, schemaFieldName, parent);
|
|
549
621
|
}
|
|
550
|
-
function programmaticFieldResolver(typeName, schemaFieldName, mediaFieldNames = []) {
|
|
622
|
+
function programmaticFieldResolver(typeName, schemaFieldName, mediaFieldNames = [], fieldKeys) {
|
|
551
623
|
return async (parent, _args, ctx) => {
|
|
552
624
|
let p = ctx.programmaticMemo.get(parent);
|
|
553
625
|
if (!p) {
|
|
554
|
-
p = resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames);
|
|
626
|
+
p = resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames, fieldKeys);
|
|
555
627
|
ctx.programmaticMemo.set(parent, p);
|
|
556
628
|
}
|
|
557
629
|
return (await p)[schemaFieldName];
|
|
558
630
|
};
|
|
559
631
|
}
|
|
560
|
-
async function resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames) {
|
|
561
|
-
|
|
632
|
+
async function resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames, fieldKeys) {
|
|
633
|
+
const toLabels = (row) => fieldKeys ? fieldKeys.toLabels(row) : row;
|
|
634
|
+
if (mediaFieldNames.length === 0) return ctx.resolver.resolveItem(typeName, toLabels(parent));
|
|
562
635
|
const enriched = { ...parent };
|
|
563
636
|
await Promise.all(mediaFieldNames.map((name) => ctx.loaders.load(typeName, name, enriched)));
|
|
564
|
-
return ctx.resolver.resolveItem(typeName, enriched);
|
|
637
|
+
return ctx.resolver.resolveItem(typeName, toLabels(enriched));
|
|
565
638
|
}
|
|
566
|
-
function collectionResolver(typeName, nameMap) {
|
|
639
|
+
function collectionResolver(typeName, nameMap, fieldKeys) {
|
|
567
640
|
return async (_root, args, ctx) => {
|
|
568
641
|
const page = args.page ?? 1;
|
|
569
642
|
const perPage = args.perPage ?? 10;
|
|
@@ -574,13 +647,15 @@ function collectionResolver(typeName, nameMap) {
|
|
|
574
647
|
throw codeError2("INVALID_PAGINATION", "perPage must be between 1 and 100");
|
|
575
648
|
}
|
|
576
649
|
const repo = ctx.repos[typeName];
|
|
650
|
+
const sortBy = args.sortBy ?? "created_at";
|
|
651
|
+
const sortColumn = fieldKeys?.columnFor(sortBy) ?? sortBy;
|
|
577
652
|
const result = await repo.findMany({
|
|
578
653
|
published_only: true,
|
|
579
654
|
page,
|
|
580
655
|
per_page: perPage,
|
|
581
|
-
sort_by:
|
|
656
|
+
sort_by: sortColumn,
|
|
582
657
|
sort_order: args.sortOrder ?? "asc",
|
|
583
|
-
filters: translateFilters(args.filter, nameMap)
|
|
658
|
+
filters: translateFilters(args.filter, nameMap, fieldKeys?.columnFor)
|
|
584
659
|
});
|
|
585
660
|
return { data: result.data, meta: result.meta };
|
|
586
661
|
};
|
|
@@ -612,11 +687,12 @@ var init_resolvers = __esm({
|
|
|
612
687
|
"use strict";
|
|
613
688
|
import_graphql4 = require("graphql");
|
|
614
689
|
init_filters();
|
|
690
|
+
init_field_keys();
|
|
615
691
|
}
|
|
616
692
|
});
|
|
617
693
|
|
|
618
694
|
// src/graphql/schema.ts
|
|
619
|
-
function buildGraphQLSchema(registry) {
|
|
695
|
+
function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
|
|
620
696
|
const objectTypes = /* @__PURE__ */ new Map();
|
|
621
697
|
const enumTypes = /* @__PURE__ */ new Map();
|
|
622
698
|
for (const [name, enumType] of Object.entries(registry.enum_types)) {
|
|
@@ -692,11 +768,16 @@ function buildGraphQLSchema(registry) {
|
|
|
692
768
|
const outType = outputTypeForField(field);
|
|
693
769
|
let resolve;
|
|
694
770
|
if (field.field_type === "programmatic") {
|
|
695
|
-
resolve = programmaticFieldResolver(
|
|
771
|
+
resolve = programmaticFieldResolver(
|
|
772
|
+
machineName,
|
|
773
|
+
field.name,
|
|
774
|
+
mediaFieldNames,
|
|
775
|
+
fieldKeyMaps[machineName]
|
|
776
|
+
);
|
|
696
777
|
} else if (field.field_type === "reference" || field.field_type === "paragraph" || field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
|
|
697
778
|
resolve = relationFieldResolver(machineName, field.name);
|
|
698
779
|
} else {
|
|
699
|
-
resolve = scalarFieldResolver(field
|
|
780
|
+
resolve = scalarFieldResolver(field);
|
|
700
781
|
}
|
|
701
782
|
fields[gqlName] = { type: outType, resolve };
|
|
702
783
|
}
|
|
@@ -752,7 +833,7 @@ function buildGraphQLSchema(registry) {
|
|
|
752
833
|
sortOrder: { type: SortOrderEnum },
|
|
753
834
|
...filterType ? { filter: { type: filterType } } : {}
|
|
754
835
|
},
|
|
755
|
-
resolve: collectionResolver(name, nameMap)
|
|
836
|
+
resolve: collectionResolver(name, nameMap, fieldKeyMaps[name])
|
|
756
837
|
};
|
|
757
838
|
queryFields[singleQueryName(name)] = {
|
|
758
839
|
type: objType,
|
|
@@ -772,7 +853,7 @@ function buildGraphQLSchema(registry) {
|
|
|
772
853
|
queryFields[collectionQueryName(name)] = {
|
|
773
854
|
type: new import_graphql5.GraphQLNonNull(listType),
|
|
774
855
|
args: { page: { type: import_graphql5.GraphQLInt }, perPage: { type: import_graphql5.GraphQLInt } },
|
|
775
|
-
resolve: collectionResolver(name, buildFieldNameMap(tt.fields.map((f) => f.name)))
|
|
856
|
+
resolve: collectionResolver(name, buildFieldNameMap(tt.fields.map((f) => f.name)), fieldKeyMaps[name])
|
|
776
857
|
};
|
|
777
858
|
queryFields[singleQueryName(name)] = {
|
|
778
859
|
type: objType,
|
|
@@ -914,8 +995,8 @@ function isClientSafeGraphQLError(error) {
|
|
|
914
995
|
}
|
|
915
996
|
return false;
|
|
916
997
|
}
|
|
917
|
-
function createGraphQLHandler(registry, repos, resolver, db, options) {
|
|
918
|
-
const schema = buildGraphQLSchema(registry);
|
|
998
|
+
function createGraphQLHandler(registry, repos, fieldKeyMaps, resolver, db, options) {
|
|
999
|
+
const schema = buildGraphQLSchema(registry, fieldKeyMaps);
|
|
919
1000
|
const { plugins } = buildArmorPlugin({
|
|
920
1001
|
maxDepth: options.maxDepth,
|
|
921
1002
|
maxComplexity: options.maxComplexity
|
|
@@ -967,6 +1048,25 @@ __export(index_exports, {
|
|
|
967
1048
|
});
|
|
968
1049
|
module.exports = __toCommonJS(index_exports);
|
|
969
1050
|
|
|
1051
|
+
// src/paths.ts
|
|
1052
|
+
function normalizePrefix(prefix) {
|
|
1053
|
+
if (prefix === void 0) return "/api";
|
|
1054
|
+
const withSlash = prefix.startsWith("/") ? prefix : `/${prefix}`;
|
|
1055
|
+
const trimmed = withSlash.replace(/\/+$/, "");
|
|
1056
|
+
return trimmed === "" ? "/api" : trimmed;
|
|
1057
|
+
}
|
|
1058
|
+
function createPublicPaths(prefix) {
|
|
1059
|
+
return {
|
|
1060
|
+
collection: (basePath) => `${prefix}/${basePath}`,
|
|
1061
|
+
item: (basePath) => `${prefix}/${basePath}/:slug`,
|
|
1062
|
+
taxonomyCollection: (typeName) => `${prefix}/taxonomy/${typeName}`,
|
|
1063
|
+
taxonomyItem: (typeName) => `${prefix}/taxonomy/${typeName}/:id`,
|
|
1064
|
+
mediaCollection: () => `${prefix}/media`,
|
|
1065
|
+
mediaItem: () => `${prefix}/media/:id`,
|
|
1066
|
+
openapi: () => `${prefix}/openapi.json`
|
|
1067
|
+
};
|
|
1068
|
+
}
|
|
1069
|
+
|
|
970
1070
|
// src/app.ts
|
|
971
1071
|
var import_hono = require("hono");
|
|
972
1072
|
var import_drizzle_orm10 = require("drizzle-orm");
|
|
@@ -1545,6 +1645,54 @@ function createProgrammaticResolver(resolvers, options) {
|
|
|
1545
1645
|
return { resolveItem, resolveList, hasSchema };
|
|
1546
1646
|
}
|
|
1547
1647
|
|
|
1648
|
+
// src/projector.ts
|
|
1649
|
+
function isPlainRow(v) {
|
|
1650
|
+
return typeof v === "object" && v !== null && !Array.isArray(v) && !(v instanceof Date);
|
|
1651
|
+
}
|
|
1652
|
+
var MEDIA_FIELD_TYPES = /* @__PURE__ */ new Set(["image", "video", "file"]);
|
|
1653
|
+
function nestedTargets(fields) {
|
|
1654
|
+
const out = [];
|
|
1655
|
+
for (const f of fields) {
|
|
1656
|
+
if (MEDIA_FIELD_TYPES.has(f.field_type)) continue;
|
|
1657
|
+
if (f.field_type !== "paragraph" && f.field_type !== "reference") continue;
|
|
1658
|
+
const ref = f.ui_component.ref;
|
|
1659
|
+
if (!ref) continue;
|
|
1660
|
+
out.push({ label: f.name, target: ref });
|
|
1661
|
+
}
|
|
1662
|
+
return out;
|
|
1663
|
+
}
|
|
1664
|
+
function buildProjectors(registry, fieldKeyMaps) {
|
|
1665
|
+
const projectors = {};
|
|
1666
|
+
const sources = [
|
|
1667
|
+
registry.content_types,
|
|
1668
|
+
registry.taxonomy_types,
|
|
1669
|
+
registry.paragraph_types
|
|
1670
|
+
];
|
|
1671
|
+
for (const source of sources) {
|
|
1672
|
+
for (const [typeName, type] of Object.entries(source)) {
|
|
1673
|
+
const map = fieldKeyMaps[typeName];
|
|
1674
|
+
if (!map) continue;
|
|
1675
|
+
projectors[typeName] = { map, nested: nestedTargets(type.fields) };
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
return projectors;
|
|
1679
|
+
}
|
|
1680
|
+
function projectRow(row, typeName, projectors) {
|
|
1681
|
+
const p = projectors[typeName];
|
|
1682
|
+
if (!p) return row;
|
|
1683
|
+
const out = p.map.toLabels(row);
|
|
1684
|
+
for (const { label, target } of p.nested) {
|
|
1685
|
+
const v = out[label];
|
|
1686
|
+
if (v === null || v === void 0) continue;
|
|
1687
|
+
if (Array.isArray(v)) {
|
|
1688
|
+
out[label] = v.map((item) => isPlainRow(item) ? projectRow(item, target, projectors) : item);
|
|
1689
|
+
} else if (isPlainRow(v)) {
|
|
1690
|
+
out[label] = projectRow(v, target, projectors);
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
return out;
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1548
1696
|
// src/routes/query-params.ts
|
|
1549
1697
|
var SORTABLE_FIELDS = /* @__PURE__ */ new Set(["title", "created_at", "updated_at"]);
|
|
1550
1698
|
var RELATION_FIELD_TYPES = /* @__PURE__ */ new Set([
|
|
@@ -1565,28 +1713,31 @@ function parseInclude(includeParam) {
|
|
|
1565
1713
|
if (!includeParam) return [];
|
|
1566
1714
|
return includeParam.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1567
1715
|
}
|
|
1568
|
-
function parseFilters(url, validFields) {
|
|
1716
|
+
function parseFilters(url, validFields, columnFor) {
|
|
1569
1717
|
const { searchParams } = new URL(url);
|
|
1570
1718
|
const filters = {};
|
|
1719
|
+
const toKey = (label) => columnFor?.(label) ?? label;
|
|
1571
1720
|
for (const [key, value] of searchParams.entries()) {
|
|
1572
1721
|
const simpleMatch = /^filter\[([^\]]+)\]$/.exec(key);
|
|
1573
1722
|
const opMatch = /^filter\[([^\]]+)\]\[([^\]]+)\]$/.exec(key);
|
|
1574
1723
|
if (simpleMatch) {
|
|
1575
1724
|
const field = simpleMatch[1];
|
|
1576
1725
|
if (!validFields.has(field)) return { ok: false, invalidField: field };
|
|
1577
|
-
const
|
|
1726
|
+
const storageKey = toKey(field);
|
|
1727
|
+
const existing = filters[storageKey];
|
|
1578
1728
|
if (existing !== void 0) {
|
|
1579
|
-
filters[
|
|
1729
|
+
filters[storageKey] = Array.isArray(existing) ? [...existing, value] : [existing, value];
|
|
1580
1730
|
} else {
|
|
1581
|
-
filters[
|
|
1731
|
+
filters[storageKey] = value;
|
|
1582
1732
|
}
|
|
1583
1733
|
} else if (opMatch) {
|
|
1584
1734
|
const field = opMatch[1];
|
|
1585
1735
|
const operator = opMatch[2];
|
|
1586
1736
|
if (!validFields.has(field)) return { ok: false, invalidField: field };
|
|
1587
1737
|
if (!["gt", "gte", "lt", "lte"].includes(operator)) continue;
|
|
1588
|
-
const
|
|
1589
|
-
|
|
1738
|
+
const storageKey = toKey(field);
|
|
1739
|
+
const existing = filters[storageKey] ?? {};
|
|
1740
|
+
filters[storageKey] = { ...existing, [operator]: value };
|
|
1590
1741
|
}
|
|
1591
1742
|
}
|
|
1592
1743
|
return { ok: true, filters };
|
|
@@ -1596,7 +1747,7 @@ function parseFilters(url, validFields) {
|
|
|
1596
1747
|
function isPublished(item) {
|
|
1597
1748
|
return item["published"] === true;
|
|
1598
1749
|
}
|
|
1599
|
-
function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolver) {
|
|
1750
|
+
function registerPublicContentRoutes(app, registry, repos, projectors, paths, listRateLimit, resolver) {
|
|
1600
1751
|
function registerListRoute(path, handler) {
|
|
1601
1752
|
if (listRateLimit) {
|
|
1602
1753
|
app.get(path, listRateLimit, handler);
|
|
@@ -1604,7 +1755,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
1604
1755
|
app.get(path, handler);
|
|
1605
1756
|
}
|
|
1606
1757
|
}
|
|
1607
|
-
registerListRoute("
|
|
1758
|
+
registerListRoute(paths.collection("content"), (c) => {
|
|
1608
1759
|
const data = Object.values(registry.content_types).map((ct) => ({
|
|
1609
1760
|
name: ct.name,
|
|
1610
1761
|
label: ct.label,
|
|
@@ -1612,7 +1763,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
1612
1763
|
}));
|
|
1613
1764
|
return c.json({ ok: true, data });
|
|
1614
1765
|
});
|
|
1615
|
-
registerListRoute("
|
|
1766
|
+
registerListRoute(paths.collection("taxonomy"), (c) => {
|
|
1616
1767
|
const data = Object.values(registry.taxonomy_types).map((tt) => ({
|
|
1617
1768
|
name: tt.name,
|
|
1618
1769
|
label: tt.label
|
|
@@ -1631,7 +1782,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
1631
1782
|
...contentType.system_fields.map((f) => f.name)
|
|
1632
1783
|
]);
|
|
1633
1784
|
if (contentType.only_one) {
|
|
1634
|
-
app.get(
|
|
1785
|
+
app.get(paths.collection(basePath), async (c) => {
|
|
1635
1786
|
const result = await repo.findMany({ published_only: true, page: 1, per_page: 1 });
|
|
1636
1787
|
if (result.data.length === 0) {
|
|
1637
1788
|
return c.json(
|
|
@@ -1639,12 +1790,14 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
1639
1790
|
404
|
|
1640
1791
|
);
|
|
1641
1792
|
}
|
|
1642
|
-
let data = result.data[0];
|
|
1793
|
+
let data = projectRow(result.data[0], typeName, projectors);
|
|
1643
1794
|
if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
|
|
1644
1795
|
return c.json({ ok: true, data });
|
|
1645
1796
|
});
|
|
1646
1797
|
} else {
|
|
1647
|
-
registerListRoute(
|
|
1798
|
+
registerListRoute(paths.collection(basePath), async (c) => {
|
|
1799
|
+
const projector = projectors[typeName];
|
|
1800
|
+
const fieldKeys = projector.map;
|
|
1648
1801
|
const pagination = parsePagination(c.req.query("page"), c.req.query("per_page"));
|
|
1649
1802
|
if (!pagination.ok) {
|
|
1650
1803
|
return c.json(
|
|
@@ -1684,7 +1837,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
1684
1837
|
400
|
|
1685
1838
|
);
|
|
1686
1839
|
}
|
|
1687
|
-
const filtersResult = parseFilters(c.req.url, filterableFieldNames);
|
|
1840
|
+
const filtersResult = parseFilters(c.req.url, filterableFieldNames, fieldKeys.columnFor);
|
|
1688
1841
|
if (!filtersResult.ok) {
|
|
1689
1842
|
return c.json(
|
|
1690
1843
|
{
|
|
@@ -1712,22 +1865,23 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
1712
1865
|
);
|
|
1713
1866
|
}
|
|
1714
1867
|
}
|
|
1868
|
+
const sortColumn = fieldKeys.columnFor(sortBy) ?? sortBy;
|
|
1715
1869
|
const result = await repo.findMany({
|
|
1716
1870
|
published_only: true,
|
|
1717
1871
|
page: pagination.page,
|
|
1718
1872
|
per_page: pagination.per_page,
|
|
1719
|
-
sort_by:
|
|
1873
|
+
sort_by: sortColumn,
|
|
1720
1874
|
sort_order: sortOrder,
|
|
1721
1875
|
filters: filtersResult.filters,
|
|
1722
1876
|
include
|
|
1723
1877
|
});
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
return c.json(result);
|
|
1878
|
+
const labeled = result.data.map(
|
|
1879
|
+
(row) => projectRow(row, typeName, projectors)
|
|
1880
|
+
);
|
|
1881
|
+
const data = resolver?.hasSchema(typeName) ? await resolver.resolveList(typeName, labeled) : labeled;
|
|
1882
|
+
return c.json({ ...result, data });
|
|
1729
1883
|
});
|
|
1730
|
-
app.get(
|
|
1884
|
+
app.get(paths.item(basePath), async (c) => {
|
|
1731
1885
|
const slug = c.req.param("slug");
|
|
1732
1886
|
const include = parseInclude(c.req.query("include"));
|
|
1733
1887
|
for (const field of include) {
|
|
@@ -1754,7 +1908,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
1754
1908
|
404
|
|
1755
1909
|
);
|
|
1756
1910
|
}
|
|
1757
|
-
let data = item;
|
|
1911
|
+
let data = projectRow(item, typeName, projectors);
|
|
1758
1912
|
if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
|
|
1759
1913
|
return c.json({ ok: true, data });
|
|
1760
1914
|
});
|
|
@@ -1763,7 +1917,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
1763
1917
|
for (const [typeName] of Object.entries(registry.taxonomy_types)) {
|
|
1764
1918
|
const repo = repos[typeName];
|
|
1765
1919
|
if (!repo) continue;
|
|
1766
|
-
registerListRoute(
|
|
1920
|
+
registerListRoute(paths.taxonomyCollection(typeName), async (c) => {
|
|
1767
1921
|
const pagination = parsePagination(c.req.query("page"), c.req.query("per_page"));
|
|
1768
1922
|
if (!pagination.ok) {
|
|
1769
1923
|
return c.json(
|
|
@@ -1782,13 +1936,13 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
1782
1936
|
page: pagination.page,
|
|
1783
1937
|
per_page: pagination.per_page
|
|
1784
1938
|
});
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
return c.json(result);
|
|
1939
|
+
const labeled = result.data.map(
|
|
1940
|
+
(row) => projectRow(row, typeName, projectors)
|
|
1941
|
+
);
|
|
1942
|
+
const data = resolver?.hasSchema(typeName) ? await resolver.resolveList(typeName, labeled) : labeled;
|
|
1943
|
+
return c.json({ ...result, data });
|
|
1790
1944
|
});
|
|
1791
|
-
app.get(
|
|
1945
|
+
app.get(paths.taxonomyItem(typeName), async (c) => {
|
|
1792
1946
|
const id = c.req.param("id");
|
|
1793
1947
|
const item = await repo.findOne(id);
|
|
1794
1948
|
if (!item || !isPublished(item)) {
|
|
@@ -1800,7 +1954,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
1800
1954
|
404
|
|
1801
1955
|
);
|
|
1802
1956
|
}
|
|
1803
|
-
let data = item;
|
|
1957
|
+
let data = projectRow(item, typeName, projectors);
|
|
1804
1958
|
if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
|
|
1805
1959
|
return c.json({ ok: true, data });
|
|
1806
1960
|
});
|
|
@@ -1816,7 +1970,7 @@ function parsePagination2(pageStr, perPageStr) {
|
|
|
1816
1970
|
if (!Number.isInteger(per_page) || per_page < 1 || per_page > 100) return { ok: false };
|
|
1817
1971
|
return { ok: true, page, per_page };
|
|
1818
1972
|
}
|
|
1819
|
-
function registerPublicMediaRoutes(app, mediaRepo, listRateLimit) {
|
|
1973
|
+
function registerPublicMediaRoutes(app, mediaRepo, paths, listRateLimit) {
|
|
1820
1974
|
const mediaListHandler = async (c) => {
|
|
1821
1975
|
const pagination = parsePagination2(c.req.query("page"), c.req.query("per_page"));
|
|
1822
1976
|
if (!pagination.ok) {
|
|
@@ -1855,11 +2009,11 @@ function registerPublicMediaRoutes(app, mediaRepo, listRateLimit) {
|
|
|
1855
2009
|
return c.json(result);
|
|
1856
2010
|
};
|
|
1857
2011
|
if (listRateLimit) {
|
|
1858
|
-
app.get(
|
|
2012
|
+
app.get(paths.mediaCollection(), listRateLimit, mediaListHandler);
|
|
1859
2013
|
} else {
|
|
1860
|
-
app.get(
|
|
2014
|
+
app.get(paths.mediaCollection(), mediaListHandler);
|
|
1861
2015
|
}
|
|
1862
|
-
app.get(
|
|
2016
|
+
app.get(paths.mediaItem(), async (c) => {
|
|
1863
2017
|
const id = c.req.param("id");
|
|
1864
2018
|
const item = await mediaRepo.findOne(id);
|
|
1865
2019
|
if (!item) {
|
|
@@ -1883,9 +2037,11 @@ function topLevelMediaDelta(mediaFields, before, after) {
|
|
|
1883
2037
|
const added = [];
|
|
1884
2038
|
const removed = [];
|
|
1885
2039
|
for (const f of mediaFields) {
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
2040
|
+
const key = f.db_column?.column_name;
|
|
2041
|
+
if (key === void 0 || key === "") continue;
|
|
2042
|
+
if (after !== null && !(key in after)) continue;
|
|
2043
|
+
const oldId = before ? extractMediaId(before[key]) : null;
|
|
2044
|
+
const newId = after ? extractMediaId(after[key]) : null;
|
|
1889
2045
|
if (newId === oldId) continue;
|
|
1890
2046
|
if (oldId) removed.push(oldId);
|
|
1891
2047
|
if (newId) added.push(newId);
|
|
@@ -1909,6 +2065,7 @@ async function applyMediaReferenceDelta(delta, mediaRepo) {
|
|
|
1909
2065
|
|
|
1910
2066
|
// src/routes/admin/content.ts
|
|
1911
2067
|
init_relations();
|
|
2068
|
+
init_field_keys();
|
|
1912
2069
|
function quoteIdent2(name) {
|
|
1913
2070
|
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
|
|
1914
2071
|
return `"${name}"`;
|
|
@@ -1946,7 +2103,7 @@ function isEmpty(value) {
|
|
|
1946
2103
|
function checkRequiredFields(fields, data) {
|
|
1947
2104
|
return fields.filter((f) => f.required && isEmpty(data[f.name])).map((f) => ({ field: f.name, message: `${f.label} is required` }));
|
|
1948
2105
|
}
|
|
1949
|
-
function parseListQuery(url, schemaFieldNames, relationFieldNames) {
|
|
2106
|
+
function parseListQuery(url, schemaFieldNames, relationFieldNames, columnFor) {
|
|
1950
2107
|
const searchParams = new URL(url).searchParams;
|
|
1951
2108
|
const pagination = parsePagination(
|
|
1952
2109
|
searchParams.get("page") ?? void 0,
|
|
@@ -1981,7 +2138,7 @@ function parseListQuery(url, schemaFieldNames, relationFieldNames) {
|
|
|
1981
2138
|
status: 400
|
|
1982
2139
|
};
|
|
1983
2140
|
}
|
|
1984
|
-
const filtersResult = parseFilters(url, schemaFieldNames);
|
|
2141
|
+
const filtersResult = parseFilters(url, schemaFieldNames, columnFor);
|
|
1985
2142
|
if (!filtersResult.ok) {
|
|
1986
2143
|
return {
|
|
1987
2144
|
ok: false,
|
|
@@ -2013,7 +2170,7 @@ function parseListQuery(url, schemaFieldNames, relationFieldNames) {
|
|
|
2013
2170
|
search
|
|
2014
2171
|
};
|
|
2015
2172
|
}
|
|
2016
|
-
function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePermission, db) {
|
|
2173
|
+
function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo, requirePermission, db) {
|
|
2017
2174
|
for (const [typeName, contentType] of Object.entries(registry.content_types)) {
|
|
2018
2175
|
const basePath = `content/${typeName}`;
|
|
2019
2176
|
const repo = repos[typeName];
|
|
@@ -2025,6 +2182,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2025
2182
|
const relationFieldNames = new Set(
|
|
2026
2183
|
contentType.fields.filter((f) => RELATION_FIELD_TYPES.has(f.field_type)).map((f) => f.name)
|
|
2027
2184
|
);
|
|
2185
|
+
const projector = projectors[typeName];
|
|
2186
|
+
const fieldKeys = projector.map;
|
|
2028
2187
|
const requiredFields = contentType.fields.filter((f) => f.required);
|
|
2029
2188
|
const mediaFields = contentType.fields.filter(
|
|
2030
2189
|
(f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file"
|
|
@@ -2038,17 +2197,18 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2038
2197
|
`/admin/api/${basePath}`,
|
|
2039
2198
|
requirePermission("content:read"),
|
|
2040
2199
|
async (c) => {
|
|
2041
|
-
const parsed = parseListQuery(c.req.url, schemaFieldNames, relationFieldNames);
|
|
2200
|
+
const parsed = parseListQuery(c.req.url, schemaFieldNames, relationFieldNames, fieldKeys.columnFor);
|
|
2042
2201
|
if (!parsed.ok) {
|
|
2043
2202
|
return c.json({ ok: false, error: parsed.response }, parsed.status);
|
|
2044
2203
|
}
|
|
2045
2204
|
const publishedParam = c.req.query("published");
|
|
2046
2205
|
const extraFilters = {};
|
|
2047
2206
|
if (publishedParam === "false") extraFilters["published"] = false;
|
|
2207
|
+
const sortColumn = fieldKeys.columnFor(parsed.sortBy) ?? parsed.sortBy;
|
|
2048
2208
|
const findOpts = {
|
|
2049
2209
|
page: parsed.pagination.page,
|
|
2050
2210
|
per_page: parsed.pagination.per_page,
|
|
2051
|
-
sort_by:
|
|
2211
|
+
sort_by: sortColumn,
|
|
2052
2212
|
sort_order: parsed.sortOrder,
|
|
2053
2213
|
filters: { ...parsed.filters, ...extraFilters },
|
|
2054
2214
|
include: parsed.include
|
|
@@ -2058,7 +2218,10 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2058
2218
|
findOpts.search = { term: parsed.search, columns: searchableColumns };
|
|
2059
2219
|
}
|
|
2060
2220
|
const result = await repo.findMany(findOpts);
|
|
2061
|
-
|
|
2221
|
+
const data = result.data.map(
|
|
2222
|
+
(row) => projectRow(row, typeName, projectors)
|
|
2223
|
+
);
|
|
2224
|
+
return c.json({ ...result, data });
|
|
2062
2225
|
}
|
|
2063
2226
|
);
|
|
2064
2227
|
app.get(
|
|
@@ -2093,7 +2256,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2093
2256
|
}
|
|
2094
2257
|
}
|
|
2095
2258
|
}
|
|
2096
|
-
|
|
2259
|
+
const data = projectRow(item, typeName, projectors);
|
|
2260
|
+
return c.json({ ok: true, data });
|
|
2097
2261
|
}
|
|
2098
2262
|
);
|
|
2099
2263
|
app.post(
|
|
@@ -2160,6 +2324,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2160
2324
|
}
|
|
2161
2325
|
);
|
|
2162
2326
|
const writeNewItem = async (c, body) => {
|
|
2327
|
+
const storageBody = fieldKeys.toStorage(body);
|
|
2163
2328
|
const fieldErrors = checkRequiredFields(requiredFields, body);
|
|
2164
2329
|
if (fieldErrors.length > 0) {
|
|
2165
2330
|
return c.json(
|
|
@@ -2183,9 +2348,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2183
2348
|
const junctionFields = contentType.fields.filter(
|
|
2184
2349
|
(f) => f.db_column !== null && f.db_column.junction !== void 0
|
|
2185
2350
|
);
|
|
2186
|
-
const
|
|
2187
|
-
(f) => f.db_column !== null && f.db_column.junction === void 0
|
|
2188
|
-
);
|
|
2351
|
+
const columnBackedFields = contentType.fields.filter(isColumnBacked);
|
|
2189
2352
|
const columnData = {
|
|
2190
2353
|
published: body["published"] ?? false
|
|
2191
2354
|
};
|
|
@@ -2194,9 +2357,10 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2194
2357
|
} else {
|
|
2195
2358
|
columnData["slug"] = body["slug"];
|
|
2196
2359
|
}
|
|
2197
|
-
for (const f of
|
|
2198
|
-
const
|
|
2199
|
-
|
|
2360
|
+
for (const f of columnBackedFields) {
|
|
2361
|
+
const key = f.db_column.column_name;
|
|
2362
|
+
if (!(key in storageBody)) continue;
|
|
2363
|
+
columnData[key] = storageBody[key];
|
|
2200
2364
|
}
|
|
2201
2365
|
if (db) {
|
|
2202
2366
|
const basePathId = await lookupBasePathId(db, contentType.default_base_path);
|
|
@@ -2216,7 +2380,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2216
2380
|
}
|
|
2217
2381
|
const item = await repo.create(columnData);
|
|
2218
2382
|
const itemId = item["id"];
|
|
2219
|
-
const mediaDeltas = [topLevelMediaDelta(mediaFields, null,
|
|
2383
|
+
const mediaDeltas = [topLevelMediaDelta(mediaFields, null, storageBody)];
|
|
2220
2384
|
if (db) {
|
|
2221
2385
|
for (const f of paragraphFields) {
|
|
2222
2386
|
const comp = f.ui_component;
|
|
@@ -2233,7 +2397,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2233
2397
|
}
|
|
2234
2398
|
}
|
|
2235
2399
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
2236
|
-
|
|
2400
|
+
const data = projectRow(item, typeName, projectors);
|
|
2401
|
+
return c.json({ ok: true, data }, 201);
|
|
2237
2402
|
};
|
|
2238
2403
|
app.patch(
|
|
2239
2404
|
`/admin/api/${basePath}/:id`,
|
|
@@ -2293,11 +2458,12 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2293
2458
|
}
|
|
2294
2459
|
);
|
|
2295
2460
|
const writeExistingItem = async (c, id, existing, body) => {
|
|
2461
|
+
const storageBody = fieldKeys.toStorage(body);
|
|
2296
2462
|
if (body["published"] === true) {
|
|
2297
2463
|
const publishDeny = await requirePermission("content:edit")(c, async () => {
|
|
2298
2464
|
});
|
|
2299
2465
|
if (publishDeny) return publishDeny;
|
|
2300
|
-
const merged = { ...existing, ...body };
|
|
2466
|
+
const merged = { ...fieldKeys.toLabels(existing), ...body };
|
|
2301
2467
|
const fieldErrors = checkRequiredFields(requiredFields, merged);
|
|
2302
2468
|
if (fieldErrors.length > 0) {
|
|
2303
2469
|
return c.json(
|
|
@@ -2317,17 +2483,14 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2317
2483
|
const patchJunctionFields = contentType.fields.filter(
|
|
2318
2484
|
(f) => f.db_column !== null && f.db_column.junction !== void 0
|
|
2319
2485
|
);
|
|
2320
|
-
const
|
|
2321
|
-
(f) => f.db_column !== null && f.db_column.junction === void 0
|
|
2322
|
-
);
|
|
2486
|
+
const patchColumnBackedFields = contentType.fields.filter(isColumnBacked);
|
|
2323
2487
|
const patchData = {};
|
|
2324
2488
|
if (!contentType.only_one && "slug" in body) patchData["slug"] = body["slug"];
|
|
2325
2489
|
if ("published" in body) patchData["published"] = body["published"];
|
|
2326
|
-
for (const f of
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
}
|
|
2490
|
+
for (const f of patchColumnBackedFields) {
|
|
2491
|
+
const key = f.db_column.column_name;
|
|
2492
|
+
if (!(key in storageBody)) continue;
|
|
2493
|
+
patchData[key] = storageBody[key];
|
|
2331
2494
|
}
|
|
2332
2495
|
const updated = await repo.update(id, patchData);
|
|
2333
2496
|
if (!updated) {
|
|
@@ -2337,7 +2500,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2337
2500
|
);
|
|
2338
2501
|
}
|
|
2339
2502
|
const mediaDeltas = [
|
|
2340
|
-
topLevelMediaDelta(mediaFields, existing,
|
|
2503
|
+
topLevelMediaDelta(mediaFields, existing, storageBody)
|
|
2341
2504
|
];
|
|
2342
2505
|
if (db) {
|
|
2343
2506
|
for (const f of patchParagraphFields) {
|
|
@@ -2355,7 +2518,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2355
2518
|
}
|
|
2356
2519
|
}
|
|
2357
2520
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
2358
|
-
|
|
2521
|
+
const data = projectRow(updated, typeName, projectors);
|
|
2522
|
+
return c.json({ ok: true, data });
|
|
2359
2523
|
};
|
|
2360
2524
|
if (contentType.only_one) {
|
|
2361
2525
|
app.put(
|
|
@@ -2415,6 +2579,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2415
2579
|
const relationFieldNames = new Set(
|
|
2416
2580
|
taxonomyType.fields.filter((f) => RELATION_FIELD_TYPES.has(f.field_type)).map((f) => f.name)
|
|
2417
2581
|
);
|
|
2582
|
+
const projector = projectors[typeName];
|
|
2583
|
+
const fieldKeys = projector.map;
|
|
2418
2584
|
const requiredFields = taxonomyType.fields.filter((f) => f.required);
|
|
2419
2585
|
const mediaFields = taxonomyType.fields.filter(
|
|
2420
2586
|
(f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file"
|
|
@@ -2428,17 +2594,18 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2428
2594
|
`/admin/api/taxonomy/${typeName}`,
|
|
2429
2595
|
requirePermission("content:read"),
|
|
2430
2596
|
async (c) => {
|
|
2431
|
-
const parsed = parseListQuery(c.req.url, schemaFieldNames, relationFieldNames);
|
|
2597
|
+
const parsed = parseListQuery(c.req.url, schemaFieldNames, relationFieldNames, fieldKeys.columnFor);
|
|
2432
2598
|
if (!parsed.ok) {
|
|
2433
2599
|
return c.json({ ok: false, error: parsed.response }, parsed.status);
|
|
2434
2600
|
}
|
|
2435
2601
|
const publishedParam = c.req.query("published");
|
|
2436
2602
|
const extraFilters = {};
|
|
2437
2603
|
if (publishedParam === "false") extraFilters["published"] = false;
|
|
2604
|
+
const sortColumn = fieldKeys.columnFor(parsed.sortBy) ?? parsed.sortBy;
|
|
2438
2605
|
const findOpts = {
|
|
2439
2606
|
page: parsed.pagination.page,
|
|
2440
2607
|
per_page: parsed.pagination.per_page,
|
|
2441
|
-
sort_by:
|
|
2608
|
+
sort_by: sortColumn,
|
|
2442
2609
|
sort_order: parsed.sortOrder,
|
|
2443
2610
|
filters: { ...parsed.filters, ...extraFilters },
|
|
2444
2611
|
include: parsed.include
|
|
@@ -2448,7 +2615,10 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2448
2615
|
findOpts.search = { term: parsed.search, columns: searchableColumns };
|
|
2449
2616
|
}
|
|
2450
2617
|
const result = await repo.findMany(findOpts);
|
|
2451
|
-
|
|
2618
|
+
const data = result.data.map(
|
|
2619
|
+
(row) => projectRow(row, typeName, projectors)
|
|
2620
|
+
);
|
|
2621
|
+
return c.json({ ...result, data });
|
|
2452
2622
|
}
|
|
2453
2623
|
);
|
|
2454
2624
|
app.get(
|
|
@@ -2463,7 +2633,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2463
2633
|
404
|
|
2464
2634
|
);
|
|
2465
2635
|
}
|
|
2466
|
-
|
|
2636
|
+
const data = projectRow(item, typeName, projectors);
|
|
2637
|
+
return c.json({ ok: true, data });
|
|
2467
2638
|
}
|
|
2468
2639
|
);
|
|
2469
2640
|
app.post(
|
|
@@ -2471,6 +2642,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2471
2642
|
requirePermission("content:create"),
|
|
2472
2643
|
async (c) => {
|
|
2473
2644
|
const body = await c.req.json();
|
|
2645
|
+
const storageBody = fieldKeys.toStorage(body);
|
|
2474
2646
|
const fieldErrors = checkRequiredFields(requiredFields, body);
|
|
2475
2647
|
if (fieldErrors.length > 0) {
|
|
2476
2648
|
return c.json(
|
|
@@ -2490,20 +2662,19 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2490
2662
|
});
|
|
2491
2663
|
if (publishDeny) return publishDeny;
|
|
2492
2664
|
}
|
|
2493
|
-
const taxColumnFields = taxonomyType.fields.filter(
|
|
2494
|
-
(f) => f.db_column !== null && f.db_column.junction === void 0
|
|
2495
|
-
);
|
|
2496
2665
|
const taxParagraphFields = taxonomyType.fields.filter((f) => f.db_column === null);
|
|
2666
|
+
const taxColumnBackedFields = taxonomyType.fields.filter(isColumnBacked);
|
|
2497
2667
|
const taxColumnData = {
|
|
2498
2668
|
published: body["published"] ?? false
|
|
2499
2669
|
};
|
|
2500
|
-
for (const f of
|
|
2501
|
-
const
|
|
2502
|
-
|
|
2670
|
+
for (const f of taxColumnBackedFields) {
|
|
2671
|
+
const key = f.db_column.column_name;
|
|
2672
|
+
if (!(key in storageBody)) continue;
|
|
2673
|
+
taxColumnData[key] = storageBody[key];
|
|
2503
2674
|
}
|
|
2504
2675
|
const item = await repo.create(taxColumnData);
|
|
2505
2676
|
const taxItemId = item["id"];
|
|
2506
|
-
const mediaDeltas = [topLevelMediaDelta(mediaFields, null,
|
|
2677
|
+
const mediaDeltas = [topLevelMediaDelta(mediaFields, null, storageBody)];
|
|
2507
2678
|
if (db) {
|
|
2508
2679
|
for (const f of taxParagraphFields) {
|
|
2509
2680
|
const comp = f.ui_component;
|
|
@@ -2515,7 +2686,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2515
2686
|
}
|
|
2516
2687
|
}
|
|
2517
2688
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
2518
|
-
|
|
2689
|
+
const data = projectRow(item, typeName, projectors);
|
|
2690
|
+
return c.json({ ok: true, data }, 201);
|
|
2519
2691
|
}
|
|
2520
2692
|
);
|
|
2521
2693
|
app.patch(
|
|
@@ -2524,6 +2696,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2524
2696
|
async (c) => {
|
|
2525
2697
|
const id = c.req.param("id");
|
|
2526
2698
|
const body = await c.req.json();
|
|
2699
|
+
const storageBody = fieldKeys.toStorage(body);
|
|
2527
2700
|
const existing = await repo.findOne(id);
|
|
2528
2701
|
if (!existing) {
|
|
2529
2702
|
return c.json(
|
|
@@ -2535,7 +2708,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2535
2708
|
const publishDeny = await requirePermission("content:edit")(c, async () => {
|
|
2536
2709
|
});
|
|
2537
2710
|
if (publishDeny) return publishDeny;
|
|
2538
|
-
const merged = { ...existing, ...body };
|
|
2711
|
+
const merged = { ...fieldKeys.toLabels(existing), ...body };
|
|
2539
2712
|
const fieldErrors = checkRequiredFields(requiredFields, merged);
|
|
2540
2713
|
if (fieldErrors.length > 0) {
|
|
2541
2714
|
return c.json(
|
|
@@ -2551,17 +2724,14 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2551
2724
|
);
|
|
2552
2725
|
}
|
|
2553
2726
|
}
|
|
2554
|
-
const taxPatchColumnFields = taxonomyType.fields.filter(
|
|
2555
|
-
(f) => f.db_column !== null && f.db_column.junction === void 0
|
|
2556
|
-
);
|
|
2557
2727
|
const taxPatchParagraphFields = taxonomyType.fields.filter((f) => f.db_column === null);
|
|
2728
|
+
const taxPatchColumnBackedFields = taxonomyType.fields.filter(isColumnBacked);
|
|
2558
2729
|
const taxPatchData = {};
|
|
2559
2730
|
if ("published" in body) taxPatchData["published"] = body["published"];
|
|
2560
|
-
for (const f of
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
}
|
|
2731
|
+
for (const f of taxPatchColumnBackedFields) {
|
|
2732
|
+
const key = f.db_column.column_name;
|
|
2733
|
+
if (!(key in storageBody)) continue;
|
|
2734
|
+
taxPatchData[key] = storageBody[key];
|
|
2565
2735
|
}
|
|
2566
2736
|
const updated = await repo.update(id, taxPatchData);
|
|
2567
2737
|
if (!updated) {
|
|
@@ -2571,7 +2741,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2571
2741
|
);
|
|
2572
2742
|
}
|
|
2573
2743
|
const mediaDeltas = [
|
|
2574
|
-
topLevelMediaDelta(mediaFields, existing,
|
|
2744
|
+
topLevelMediaDelta(mediaFields, existing, storageBody)
|
|
2575
2745
|
];
|
|
2576
2746
|
if (db) {
|
|
2577
2747
|
for (const f of taxPatchParagraphFields) {
|
|
@@ -2584,7 +2754,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2584
2754
|
}
|
|
2585
2755
|
}
|
|
2586
2756
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
2587
|
-
|
|
2757
|
+
const data = projectRow(updated, typeName, projectors);
|
|
2758
|
+
return c.json({ ok: true, data });
|
|
2588
2759
|
}
|
|
2589
2760
|
);
|
|
2590
2761
|
app.delete(
|
|
@@ -3576,7 +3747,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
3576
3747
|
var import_drizzle_orm6 = require("drizzle-orm");
|
|
3577
3748
|
var API_VERSION = (() => {
|
|
3578
3749
|
try {
|
|
3579
|
-
return "0.
|
|
3750
|
+
return "0.4.1";
|
|
3580
3751
|
} catch {
|
|
3581
3752
|
return "0.0.0";
|
|
3582
3753
|
}
|
|
@@ -3739,7 +3910,7 @@ function whereFragment(conds) {
|
|
|
3739
3910
|
return conds.length > 0 ? import_drizzle_orm8.sql` WHERE ${import_drizzle_orm8.sql.join(conds, import_drizzle_orm8.sql` AND `)}` : import_drizzle_orm8.sql``;
|
|
3740
3911
|
}
|
|
3741
3912
|
function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
3742
|
-
const { relations = {}, publishedRelations = false } = options;
|
|
3913
|
+
const { relations = {}, publishedRelations = false, sortableColumns } = options;
|
|
3743
3914
|
function tableRaw() {
|
|
3744
3915
|
return import_drizzle_orm8.sql.raw(quoteIdent3(tableName));
|
|
3745
3916
|
}
|
|
@@ -3777,8 +3948,9 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
|
|
|
3777
3948
|
sort_order = "asc",
|
|
3778
3949
|
search
|
|
3779
3950
|
} = opts;
|
|
3780
|
-
|
|
3781
|
-
|
|
3951
|
+
const allowed = sortableColumns ?? SORTABLE_FIELDS2;
|
|
3952
|
+
if (!allowed.has(sort_by)) {
|
|
3953
|
+
throw codeError("INVALID_SORT_FIELD", `'${sort_by}' is not sortable.`);
|
|
3782
3954
|
}
|
|
3783
3955
|
const offset = (page - 1) * per_page;
|
|
3784
3956
|
const conds = buildConditions(published_only, filters, search);
|
|
@@ -3978,6 +4150,7 @@ function createMediaRepository(db) {
|
|
|
3978
4150
|
}
|
|
3979
4151
|
|
|
3980
4152
|
// src/app.ts
|
|
4153
|
+
init_field_keys();
|
|
3981
4154
|
var MISSING_STORAGE_ERROR = `\u2717 api.storage is required but not configured.
|
|
3982
4155
|
Add a storage adapter to your manguito.config.ts:
|
|
3983
4156
|
|
|
@@ -3991,7 +4164,8 @@ function createCmsApp(options) {
|
|
|
3991
4164
|
if (!options.storage) {
|
|
3992
4165
|
throw new Error(MISSING_STORAGE_ERROR);
|
|
3993
4166
|
}
|
|
3994
|
-
const prefix = options.prefix
|
|
4167
|
+
const prefix = normalizePrefix(options.prefix);
|
|
4168
|
+
const publicPaths = createPublicPaths(prefix);
|
|
3995
4169
|
const { storage, registry, db, rateLimit, media, cors } = options;
|
|
3996
4170
|
const cmsName = options.name ?? "Manguito CMS";
|
|
3997
4171
|
const maxFileSize = media?.max_file_size;
|
|
@@ -4025,16 +4199,38 @@ function createCmsApp(options) {
|
|
|
4025
4199
|
return result.rows[0]?.role ?? null;
|
|
4026
4200
|
};
|
|
4027
4201
|
const requireHierarchy = createHierarchyMiddleware(rolesRegistry, getUserRole);
|
|
4202
|
+
const fieldKeyMaps = Object.fromEntries([
|
|
4203
|
+
...Object.entries(registry.content_types).map(([typeName, ct]) => [
|
|
4204
|
+
typeName,
|
|
4205
|
+
createFieldKeyMap(ct.fields)
|
|
4206
|
+
]),
|
|
4207
|
+
...Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
|
|
4208
|
+
typeName,
|
|
4209
|
+
createFieldKeyMap(tt.fields)
|
|
4210
|
+
]),
|
|
4211
|
+
...Object.entries(registry.paragraph_types).map(([typeName, pt]) => [
|
|
4212
|
+
typeName,
|
|
4213
|
+
createFieldKeyMap(pt.fields)
|
|
4214
|
+
])
|
|
4215
|
+
]);
|
|
4216
|
+
const projectors = buildProjectors(registry, fieldKeyMaps);
|
|
4217
|
+
const sortableColumnsFor = (typeName) => new Set(
|
|
4218
|
+
[...SORTABLE_FIELDS].map((label) => fieldKeyMaps[typeName].columnFor(label) ?? label)
|
|
4219
|
+
);
|
|
4028
4220
|
const contentRepos = Object.fromEntries(
|
|
4029
4221
|
Object.entries(registry.content_types).map(([typeName, ct]) => [
|
|
4030
4222
|
typeName,
|
|
4031
|
-
createDrizzleContentRepository(db, ct.db.table_name
|
|
4223
|
+
createDrizzleContentRepository(db, ct.db.table_name, {
|
|
4224
|
+
sortableColumns: sortableColumnsFor(typeName)
|
|
4225
|
+
})
|
|
4032
4226
|
])
|
|
4033
4227
|
);
|
|
4034
4228
|
const taxonomyRepos = Object.fromEntries(
|
|
4035
4229
|
Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
|
|
4036
4230
|
typeName,
|
|
4037
|
-
createDrizzleContentRepository(db, tt.db.table_name
|
|
4231
|
+
createDrizzleContentRepository(db, tt.db.table_name, {
|
|
4232
|
+
sortableColumns: sortableColumnsFor(typeName)
|
|
4233
|
+
})
|
|
4038
4234
|
])
|
|
4039
4235
|
);
|
|
4040
4236
|
const repos = { ...contentRepos, ...taxonomyRepos };
|
|
@@ -4044,7 +4240,8 @@ function createCmsApp(options) {
|
|
|
4044
4240
|
typeName,
|
|
4045
4241
|
createDrizzleContentRepository(db, ct.db.table_name, {
|
|
4046
4242
|
relations: buildRelationsMap(ct.fields, registry),
|
|
4047
|
-
publishedRelations: true
|
|
4243
|
+
publishedRelations: true,
|
|
4244
|
+
sortableColumns: sortableColumnsFor(typeName)
|
|
4048
4245
|
})
|
|
4049
4246
|
])
|
|
4050
4247
|
);
|
|
@@ -4053,7 +4250,8 @@ function createCmsApp(options) {
|
|
|
4053
4250
|
typeName,
|
|
4054
4251
|
createDrizzleContentRepository(db, tt.db.table_name, {
|
|
4055
4252
|
relations: buildRelationsMap(tt.fields, registry),
|
|
4056
|
-
publishedRelations: true
|
|
4253
|
+
publishedRelations: true,
|
|
4254
|
+
sortableColumns: sortableColumnsFor(typeName)
|
|
4057
4255
|
})
|
|
4058
4256
|
])
|
|
4059
4257
|
);
|
|
@@ -4061,33 +4259,41 @@ function createCmsApp(options) {
|
|
|
4061
4259
|
const graphqlRepos = Object.fromEntries([
|
|
4062
4260
|
...Object.entries(registry.content_types).map(([typeName, ct]) => [
|
|
4063
4261
|
typeName,
|
|
4064
|
-
createDrizzleContentRepository(db, ct.db.table_name
|
|
4262
|
+
createDrizzleContentRepository(db, ct.db.table_name, {
|
|
4263
|
+
sortableColumns: sortableColumnsFor(typeName)
|
|
4264
|
+
})
|
|
4065
4265
|
]),
|
|
4066
4266
|
...Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
|
|
4067
4267
|
typeName,
|
|
4068
|
-
createDrizzleContentRepository(db, tt.db.table_name
|
|
4268
|
+
createDrizzleContentRepository(db, tt.db.table_name, {
|
|
4269
|
+
sortableColumns: sortableColumnsFor(typeName)
|
|
4270
|
+
})
|
|
4069
4271
|
])
|
|
4070
4272
|
]);
|
|
4071
4273
|
registerAuthRoutes(app, db);
|
|
4072
4274
|
app.use("/admin/api/*", authMiddleware);
|
|
4073
4275
|
app.use("/admin/api/*", mustChangePasswordCheck);
|
|
4074
|
-
app.get(
|
|
4276
|
+
app.get(publicPaths.openapi(), (c) => {
|
|
4075
4277
|
const paths = {};
|
|
4076
4278
|
for (const ct of Object.values(registry.content_types)) {
|
|
4077
4279
|
const base = ct.default_base_path;
|
|
4078
4280
|
if (ct.only_one) {
|
|
4079
|
-
paths[
|
|
4281
|
+
paths[publicPaths.collection(base)] = { get: { summary: `Get ${ct.label}`, tags: [ct.label] } };
|
|
4080
4282
|
} else {
|
|
4081
|
-
paths[
|
|
4082
|
-
paths[
|
|
4283
|
+
paths[publicPaths.collection(base)] = { get: { summary: `List published ${ct.label}`, tags: [ct.label] } };
|
|
4284
|
+
paths[publicPaths.item(base).replace(":slug", "{slug}")] = {
|
|
4285
|
+
get: { summary: `Get ${ct.label} by slug`, tags: [ct.label] }
|
|
4286
|
+
};
|
|
4083
4287
|
}
|
|
4084
4288
|
}
|
|
4085
4289
|
for (const tt of Object.values(registry.taxonomy_types)) {
|
|
4086
|
-
paths[
|
|
4087
|
-
paths[
|
|
4290
|
+
paths[publicPaths.taxonomyCollection(tt.name)] = { get: { summary: `List published ${tt.label}`, tags: [tt.label] } };
|
|
4291
|
+
paths[publicPaths.taxonomyItem(tt.name).replace(":id", "{id}")] = {
|
|
4292
|
+
get: { summary: `Get ${tt.label} by id`, tags: [tt.label] }
|
|
4293
|
+
};
|
|
4088
4294
|
}
|
|
4089
|
-
paths[
|
|
4090
|
-
paths["
|
|
4295
|
+
paths[publicPaths.mediaCollection()] = { get: { summary: "List media items", tags: ["Media"] } };
|
|
4296
|
+
paths[publicPaths.mediaItem().replace(":id", "{id}")] = { get: { summary: "Get media item by id", tags: ["Media"] } };
|
|
4091
4297
|
return c.json({
|
|
4092
4298
|
openapi: "3.0.3",
|
|
4093
4299
|
info: { title: "Manguito CMS Public API", version: "1.0.0" },
|
|
@@ -4102,14 +4308,14 @@ function createCmsApp(options) {
|
|
|
4102
4308
|
paths: {}
|
|
4103
4309
|
})
|
|
4104
4310
|
);
|
|
4105
|
-
registerPublicContentRoutes(app, registry, publicRepos, listRateLimit, programmaticResolver);
|
|
4106
|
-
registerPublicMediaRoutes(app, mediaRepo, listRateLimit);
|
|
4311
|
+
registerPublicContentRoutes(app, registry, publicRepos, projectors, publicPaths, listRateLimit, programmaticResolver);
|
|
4312
|
+
registerPublicMediaRoutes(app, mediaRepo, publicPaths, listRateLimit);
|
|
4107
4313
|
if (options.graphql?.enabled) {
|
|
4108
4314
|
const gqlOptions = options.graphql;
|
|
4109
4315
|
let gqlHandler = null;
|
|
4110
4316
|
let initError = null;
|
|
4111
4317
|
const ready = Promise.resolve().then(() => (init_handler(), handler_exports)).then(({ createGraphQLHandler: createGraphQLHandler2 }) => {
|
|
4112
|
-
gqlHandler = createGraphQLHandler2(registry, graphqlRepos, programmaticResolver, db, gqlOptions);
|
|
4318
|
+
gqlHandler = createGraphQLHandler2(registry, graphqlRepos, fieldKeyMaps, programmaticResolver, db, gqlOptions);
|
|
4113
4319
|
}).catch((err) => {
|
|
4114
4320
|
initError = err;
|
|
4115
4321
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -4149,7 +4355,7 @@ function createCmsApp(options) {
|
|
|
4149
4355
|
);
|
|
4150
4356
|
registerSchemaRoute(app, registry, db);
|
|
4151
4357
|
registerUserRoutes(app, db, requirePermission, requireHierarchy);
|
|
4152
|
-
registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePermission, db);
|
|
4358
|
+
registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo, requirePermission, db);
|
|
4153
4359
|
registerAdminMediaRoutes(app, mediaRepo, storage, requirePermission, maxFileSize);
|
|
4154
4360
|
return { prefix, app };
|
|
4155
4361
|
}
|
|
@@ -4190,7 +4396,7 @@ function resolveGraphQL(cfg) {
|
|
|
4190
4396
|
};
|
|
4191
4397
|
}
|
|
4192
4398
|
function createAPIAdapter(options = {}) {
|
|
4193
|
-
const prefix = options.prefix
|
|
4399
|
+
const prefix = normalizePrefix(options.prefix);
|
|
4194
4400
|
const media = options.media?.max_file_size !== void 0 ? { max_file_size: options.media.max_file_size } : void 0;
|
|
4195
4401
|
return {
|
|
4196
4402
|
prefix,
|