@bobbykim/manguito-cms-api 0.3.2 → 0.4.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/{chunk-TK4DNRFF.mjs → chunk-4F2CRXPG.mjs} +76 -12
- package/dist/chunk-4F2CRXPG.mjs.map +1 -0
- package/dist/{chunk-GM55HGWA.mjs → chunk-B5LWIR4U.mjs} +32 -20
- package/dist/chunk-B5LWIR4U.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 +70 -28
- package/dist/graphql/index.js.map +1 -1
- package/dist/graphql/index.mjs +2 -2
- package/dist/handler-TPEULQ5W.mjs +8 -0
- package/dist/index.js +253 -125
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +148 -99
- 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-TPEULQ5W.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({
|
|
@@ -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"];
|
|
@@ -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;
|
|
@@ -580,7 +653,7 @@ function collectionResolver(typeName, nameMap) {
|
|
|
580
653
|
per_page: perPage,
|
|
581
654
|
sort_by: args.sortBy ?? "created_at",
|
|
582
655
|
sort_order: args.sortOrder ?? "asc",
|
|
583
|
-
filters: translateFilters(args.filter, nameMap)
|
|
656
|
+
filters: translateFilters(args.filter, nameMap, fieldKeys?.columnFor)
|
|
584
657
|
});
|
|
585
658
|
return { data: result.data, meta: result.meta };
|
|
586
659
|
};
|
|
@@ -612,11 +685,12 @@ var init_resolvers = __esm({
|
|
|
612
685
|
"use strict";
|
|
613
686
|
import_graphql4 = require("graphql");
|
|
614
687
|
init_filters();
|
|
688
|
+
init_field_keys();
|
|
615
689
|
}
|
|
616
690
|
});
|
|
617
691
|
|
|
618
692
|
// src/graphql/schema.ts
|
|
619
|
-
function buildGraphQLSchema(registry) {
|
|
693
|
+
function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
|
|
620
694
|
const objectTypes = /* @__PURE__ */ new Map();
|
|
621
695
|
const enumTypes = /* @__PURE__ */ new Map();
|
|
622
696
|
for (const [name, enumType] of Object.entries(registry.enum_types)) {
|
|
@@ -692,11 +766,16 @@ function buildGraphQLSchema(registry) {
|
|
|
692
766
|
const outType = outputTypeForField(field);
|
|
693
767
|
let resolve;
|
|
694
768
|
if (field.field_type === "programmatic") {
|
|
695
|
-
resolve = programmaticFieldResolver(
|
|
769
|
+
resolve = programmaticFieldResolver(
|
|
770
|
+
machineName,
|
|
771
|
+
field.name,
|
|
772
|
+
mediaFieldNames,
|
|
773
|
+
fieldKeyMaps[machineName]
|
|
774
|
+
);
|
|
696
775
|
} else if (field.field_type === "reference" || field.field_type === "paragraph" || field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
|
|
697
776
|
resolve = relationFieldResolver(machineName, field.name);
|
|
698
777
|
} else {
|
|
699
|
-
resolve = scalarFieldResolver(field
|
|
778
|
+
resolve = scalarFieldResolver(field);
|
|
700
779
|
}
|
|
701
780
|
fields[gqlName] = { type: outType, resolve };
|
|
702
781
|
}
|
|
@@ -752,7 +831,7 @@ function buildGraphQLSchema(registry) {
|
|
|
752
831
|
sortOrder: { type: SortOrderEnum },
|
|
753
832
|
...filterType ? { filter: { type: filterType } } : {}
|
|
754
833
|
},
|
|
755
|
-
resolve: collectionResolver(name, nameMap)
|
|
834
|
+
resolve: collectionResolver(name, nameMap, fieldKeyMaps[name])
|
|
756
835
|
};
|
|
757
836
|
queryFields[singleQueryName(name)] = {
|
|
758
837
|
type: objType,
|
|
@@ -772,7 +851,7 @@ function buildGraphQLSchema(registry) {
|
|
|
772
851
|
queryFields[collectionQueryName(name)] = {
|
|
773
852
|
type: new import_graphql5.GraphQLNonNull(listType),
|
|
774
853
|
args: { page: { type: import_graphql5.GraphQLInt }, perPage: { type: import_graphql5.GraphQLInt } },
|
|
775
|
-
resolve: collectionResolver(name, buildFieldNameMap(tt.fields.map((f) => f.name)))
|
|
854
|
+
resolve: collectionResolver(name, buildFieldNameMap(tt.fields.map((f) => f.name)), fieldKeyMaps[name])
|
|
776
855
|
};
|
|
777
856
|
queryFields[singleQueryName(name)] = {
|
|
778
857
|
type: objType,
|
|
@@ -914,8 +993,8 @@ function isClientSafeGraphQLError(error) {
|
|
|
914
993
|
}
|
|
915
994
|
return false;
|
|
916
995
|
}
|
|
917
|
-
function createGraphQLHandler(registry, repos, resolver, db, options) {
|
|
918
|
-
const schema = buildGraphQLSchema(registry);
|
|
996
|
+
function createGraphQLHandler(registry, repos, fieldKeyMaps, resolver, db, options) {
|
|
997
|
+
const schema = buildGraphQLSchema(registry, fieldKeyMaps);
|
|
919
998
|
const { plugins } = buildArmorPlugin({
|
|
920
999
|
maxDepth: options.maxDepth,
|
|
921
1000
|
maxComplexity: options.maxComplexity
|
|
@@ -967,6 +1046,25 @@ __export(index_exports, {
|
|
|
967
1046
|
});
|
|
968
1047
|
module.exports = __toCommonJS(index_exports);
|
|
969
1048
|
|
|
1049
|
+
// src/paths.ts
|
|
1050
|
+
function normalizePrefix(prefix) {
|
|
1051
|
+
if (prefix === void 0) return "/api";
|
|
1052
|
+
const withSlash = prefix.startsWith("/") ? prefix : `/${prefix}`;
|
|
1053
|
+
const trimmed = withSlash.replace(/\/+$/, "");
|
|
1054
|
+
return trimmed === "" ? "/api" : trimmed;
|
|
1055
|
+
}
|
|
1056
|
+
function createPublicPaths(prefix) {
|
|
1057
|
+
return {
|
|
1058
|
+
collection: (basePath) => `${prefix}/${basePath}`,
|
|
1059
|
+
item: (basePath) => `${prefix}/${basePath}/:slug`,
|
|
1060
|
+
taxonomyCollection: (typeName) => `${prefix}/taxonomy/${typeName}`,
|
|
1061
|
+
taxonomyItem: (typeName) => `${prefix}/taxonomy/${typeName}/:id`,
|
|
1062
|
+
mediaCollection: () => `${prefix}/media`,
|
|
1063
|
+
mediaItem: () => `${prefix}/media/:id`,
|
|
1064
|
+
openapi: () => `${prefix}/openapi.json`
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
|
|
970
1068
|
// src/app.ts
|
|
971
1069
|
var import_hono = require("hono");
|
|
972
1070
|
var import_drizzle_orm10 = require("drizzle-orm");
|
|
@@ -1565,28 +1663,31 @@ function parseInclude(includeParam) {
|
|
|
1565
1663
|
if (!includeParam) return [];
|
|
1566
1664
|
return includeParam.split(",").map((s) => s.trim()).filter(Boolean);
|
|
1567
1665
|
}
|
|
1568
|
-
function parseFilters(url, validFields) {
|
|
1666
|
+
function parseFilters(url, validFields, columnFor) {
|
|
1569
1667
|
const { searchParams } = new URL(url);
|
|
1570
1668
|
const filters = {};
|
|
1669
|
+
const toKey = (label) => columnFor?.(label) ?? label;
|
|
1571
1670
|
for (const [key, value] of searchParams.entries()) {
|
|
1572
1671
|
const simpleMatch = /^filter\[([^\]]+)\]$/.exec(key);
|
|
1573
1672
|
const opMatch = /^filter\[([^\]]+)\]\[([^\]]+)\]$/.exec(key);
|
|
1574
1673
|
if (simpleMatch) {
|
|
1575
1674
|
const field = simpleMatch[1];
|
|
1576
1675
|
if (!validFields.has(field)) return { ok: false, invalidField: field };
|
|
1577
|
-
const
|
|
1676
|
+
const storageKey = toKey(field);
|
|
1677
|
+
const existing = filters[storageKey];
|
|
1578
1678
|
if (existing !== void 0) {
|
|
1579
|
-
filters[
|
|
1679
|
+
filters[storageKey] = Array.isArray(existing) ? [...existing, value] : [existing, value];
|
|
1580
1680
|
} else {
|
|
1581
|
-
filters[
|
|
1681
|
+
filters[storageKey] = value;
|
|
1582
1682
|
}
|
|
1583
1683
|
} else if (opMatch) {
|
|
1584
1684
|
const field = opMatch[1];
|
|
1585
1685
|
const operator = opMatch[2];
|
|
1586
1686
|
if (!validFields.has(field)) return { ok: false, invalidField: field };
|
|
1587
1687
|
if (!["gt", "gte", "lt", "lte"].includes(operator)) continue;
|
|
1588
|
-
const
|
|
1589
|
-
|
|
1688
|
+
const storageKey = toKey(field);
|
|
1689
|
+
const existing = filters[storageKey] ?? {};
|
|
1690
|
+
filters[storageKey] = { ...existing, [operator]: value };
|
|
1590
1691
|
}
|
|
1591
1692
|
}
|
|
1592
1693
|
return { ok: true, filters };
|
|
@@ -1596,7 +1697,7 @@ function parseFilters(url, validFields) {
|
|
|
1596
1697
|
function isPublished(item) {
|
|
1597
1698
|
return item["published"] === true;
|
|
1598
1699
|
}
|
|
1599
|
-
function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolver) {
|
|
1700
|
+
function registerPublicContentRoutes(app, registry, repos, fieldKeyMaps, paths, listRateLimit, resolver) {
|
|
1600
1701
|
function registerListRoute(path, handler) {
|
|
1601
1702
|
if (listRateLimit) {
|
|
1602
1703
|
app.get(path, listRateLimit, handler);
|
|
@@ -1604,7 +1705,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
1604
1705
|
app.get(path, handler);
|
|
1605
1706
|
}
|
|
1606
1707
|
}
|
|
1607
|
-
registerListRoute("
|
|
1708
|
+
registerListRoute(paths.collection("content"), (c) => {
|
|
1608
1709
|
const data = Object.values(registry.content_types).map((ct) => ({
|
|
1609
1710
|
name: ct.name,
|
|
1610
1711
|
label: ct.label,
|
|
@@ -1612,7 +1713,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
1612
1713
|
}));
|
|
1613
1714
|
return c.json({ ok: true, data });
|
|
1614
1715
|
});
|
|
1615
|
-
registerListRoute("
|
|
1716
|
+
registerListRoute(paths.collection("taxonomy"), (c) => {
|
|
1616
1717
|
const data = Object.values(registry.taxonomy_types).map((tt) => ({
|
|
1617
1718
|
name: tt.name,
|
|
1618
1719
|
label: tt.label
|
|
@@ -1631,7 +1732,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
1631
1732
|
...contentType.system_fields.map((f) => f.name)
|
|
1632
1733
|
]);
|
|
1633
1734
|
if (contentType.only_one) {
|
|
1634
|
-
app.get(
|
|
1735
|
+
app.get(paths.collection(basePath), async (c) => {
|
|
1635
1736
|
const result = await repo.findMany({ published_only: true, page: 1, per_page: 1 });
|
|
1636
1737
|
if (result.data.length === 0) {
|
|
1637
1738
|
return c.json(
|
|
@@ -1639,12 +1740,13 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
1639
1740
|
404
|
|
1640
1741
|
);
|
|
1641
1742
|
}
|
|
1642
|
-
let data = result.data[0];
|
|
1743
|
+
let data = fieldKeyMaps[typeName].toLabels(result.data[0]);
|
|
1643
1744
|
if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
|
|
1644
1745
|
return c.json({ ok: true, data });
|
|
1645
1746
|
});
|
|
1646
1747
|
} else {
|
|
1647
|
-
registerListRoute(
|
|
1748
|
+
registerListRoute(paths.collection(basePath), async (c) => {
|
|
1749
|
+
const fieldKeys = fieldKeyMaps[typeName];
|
|
1648
1750
|
const pagination = parsePagination(c.req.query("page"), c.req.query("per_page"));
|
|
1649
1751
|
if (!pagination.ok) {
|
|
1650
1752
|
return c.json(
|
|
@@ -1684,7 +1786,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
1684
1786
|
400
|
|
1685
1787
|
);
|
|
1686
1788
|
}
|
|
1687
|
-
const filtersResult = parseFilters(c.req.url, filterableFieldNames);
|
|
1789
|
+
const filtersResult = parseFilters(c.req.url, filterableFieldNames, fieldKeys.columnFor);
|
|
1688
1790
|
if (!filtersResult.ok) {
|
|
1689
1791
|
return c.json(
|
|
1690
1792
|
{
|
|
@@ -1721,13 +1823,13 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
1721
1823
|
filters: filtersResult.filters,
|
|
1722
1824
|
include
|
|
1723
1825
|
});
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
return c.json(result);
|
|
1826
|
+
const labeled = result.data.map(
|
|
1827
|
+
(row) => fieldKeys.toLabels(row)
|
|
1828
|
+
);
|
|
1829
|
+
const data = resolver?.hasSchema(typeName) ? await resolver.resolveList(typeName, labeled) : labeled;
|
|
1830
|
+
return c.json({ ...result, data });
|
|
1729
1831
|
});
|
|
1730
|
-
app.get(
|
|
1832
|
+
app.get(paths.item(basePath), async (c) => {
|
|
1731
1833
|
const slug = c.req.param("slug");
|
|
1732
1834
|
const include = parseInclude(c.req.query("include"));
|
|
1733
1835
|
for (const field of include) {
|
|
@@ -1754,7 +1856,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
1754
1856
|
404
|
|
1755
1857
|
);
|
|
1756
1858
|
}
|
|
1757
|
-
let data = item;
|
|
1859
|
+
let data = fieldKeyMaps[typeName].toLabels(item);
|
|
1758
1860
|
if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
|
|
1759
1861
|
return c.json({ ok: true, data });
|
|
1760
1862
|
});
|
|
@@ -1763,7 +1865,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
1763
1865
|
for (const [typeName] of Object.entries(registry.taxonomy_types)) {
|
|
1764
1866
|
const repo = repos[typeName];
|
|
1765
1867
|
if (!repo) continue;
|
|
1766
|
-
registerListRoute(
|
|
1868
|
+
registerListRoute(paths.taxonomyCollection(typeName), async (c) => {
|
|
1767
1869
|
const pagination = parsePagination(c.req.query("page"), c.req.query("per_page"));
|
|
1768
1870
|
if (!pagination.ok) {
|
|
1769
1871
|
return c.json(
|
|
@@ -1782,13 +1884,14 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
1782
1884
|
page: pagination.page,
|
|
1783
1885
|
per_page: pagination.per_page
|
|
1784
1886
|
});
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1887
|
+
const fieldKeys = fieldKeyMaps[typeName];
|
|
1888
|
+
const labeled = result.data.map(
|
|
1889
|
+
(row) => fieldKeys.toLabels(row)
|
|
1890
|
+
);
|
|
1891
|
+
const data = resolver?.hasSchema(typeName) ? await resolver.resolveList(typeName, labeled) : labeled;
|
|
1892
|
+
return c.json({ ...result, data });
|
|
1790
1893
|
});
|
|
1791
|
-
app.get(
|
|
1894
|
+
app.get(paths.taxonomyItem(typeName), async (c) => {
|
|
1792
1895
|
const id = c.req.param("id");
|
|
1793
1896
|
const item = await repo.findOne(id);
|
|
1794
1897
|
if (!item || !isPublished(item)) {
|
|
@@ -1800,7 +1903,7 @@ function registerPublicContentRoutes(app, registry, repos, listRateLimit, resolv
|
|
|
1800
1903
|
404
|
|
1801
1904
|
);
|
|
1802
1905
|
}
|
|
1803
|
-
let data = item;
|
|
1906
|
+
let data = fieldKeyMaps[typeName].toLabels(item);
|
|
1804
1907
|
if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
|
|
1805
1908
|
return c.json({ ok: true, data });
|
|
1806
1909
|
});
|
|
@@ -1816,7 +1919,7 @@ function parsePagination2(pageStr, perPageStr) {
|
|
|
1816
1919
|
if (!Number.isInteger(per_page) || per_page < 1 || per_page > 100) return { ok: false };
|
|
1817
1920
|
return { ok: true, page, per_page };
|
|
1818
1921
|
}
|
|
1819
|
-
function registerPublicMediaRoutes(app, mediaRepo, listRateLimit) {
|
|
1922
|
+
function registerPublicMediaRoutes(app, mediaRepo, paths, listRateLimit) {
|
|
1820
1923
|
const mediaListHandler = async (c) => {
|
|
1821
1924
|
const pagination = parsePagination2(c.req.query("page"), c.req.query("per_page"));
|
|
1822
1925
|
if (!pagination.ok) {
|
|
@@ -1855,11 +1958,11 @@ function registerPublicMediaRoutes(app, mediaRepo, listRateLimit) {
|
|
|
1855
1958
|
return c.json(result);
|
|
1856
1959
|
};
|
|
1857
1960
|
if (listRateLimit) {
|
|
1858
|
-
app.get(
|
|
1961
|
+
app.get(paths.mediaCollection(), listRateLimit, mediaListHandler);
|
|
1859
1962
|
} else {
|
|
1860
|
-
app.get(
|
|
1963
|
+
app.get(paths.mediaCollection(), mediaListHandler);
|
|
1861
1964
|
}
|
|
1862
|
-
app.get(
|
|
1965
|
+
app.get(paths.mediaItem(), async (c) => {
|
|
1863
1966
|
const id = c.req.param("id");
|
|
1864
1967
|
const item = await mediaRepo.findOne(id);
|
|
1865
1968
|
if (!item) {
|
|
@@ -1883,9 +1986,11 @@ function topLevelMediaDelta(mediaFields, before, after) {
|
|
|
1883
1986
|
const added = [];
|
|
1884
1987
|
const removed = [];
|
|
1885
1988
|
for (const f of mediaFields) {
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1989
|
+
const key = f.db_column?.column_name;
|
|
1990
|
+
if (key === void 0 || key === "") continue;
|
|
1991
|
+
if (after !== null && !(key in after)) continue;
|
|
1992
|
+
const oldId = before ? extractMediaId(before[key]) : null;
|
|
1993
|
+
const newId = after ? extractMediaId(after[key]) : null;
|
|
1889
1994
|
if (newId === oldId) continue;
|
|
1890
1995
|
if (oldId) removed.push(oldId);
|
|
1891
1996
|
if (newId) added.push(newId);
|
|
@@ -1909,6 +2014,7 @@ async function applyMediaReferenceDelta(delta, mediaRepo) {
|
|
|
1909
2014
|
|
|
1910
2015
|
// src/routes/admin/content.ts
|
|
1911
2016
|
init_relations();
|
|
2017
|
+
init_field_keys();
|
|
1912
2018
|
function quoteIdent2(name) {
|
|
1913
2019
|
if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
|
|
1914
2020
|
return `"${name}"`;
|
|
@@ -1946,7 +2052,7 @@ function isEmpty(value) {
|
|
|
1946
2052
|
function checkRequiredFields(fields, data) {
|
|
1947
2053
|
return fields.filter((f) => f.required && isEmpty(data[f.name])).map((f) => ({ field: f.name, message: `${f.label} is required` }));
|
|
1948
2054
|
}
|
|
1949
|
-
function parseListQuery(url, schemaFieldNames, relationFieldNames) {
|
|
2055
|
+
function parseListQuery(url, schemaFieldNames, relationFieldNames, columnFor) {
|
|
1950
2056
|
const searchParams = new URL(url).searchParams;
|
|
1951
2057
|
const pagination = parsePagination(
|
|
1952
2058
|
searchParams.get("page") ?? void 0,
|
|
@@ -1981,7 +2087,7 @@ function parseListQuery(url, schemaFieldNames, relationFieldNames) {
|
|
|
1981
2087
|
status: 400
|
|
1982
2088
|
};
|
|
1983
2089
|
}
|
|
1984
|
-
const filtersResult = parseFilters(url, schemaFieldNames);
|
|
2090
|
+
const filtersResult = parseFilters(url, schemaFieldNames, columnFor);
|
|
1985
2091
|
if (!filtersResult.ok) {
|
|
1986
2092
|
return {
|
|
1987
2093
|
ok: false,
|
|
@@ -2013,7 +2119,7 @@ function parseListQuery(url, schemaFieldNames, relationFieldNames) {
|
|
|
2013
2119
|
search
|
|
2014
2120
|
};
|
|
2015
2121
|
}
|
|
2016
|
-
function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePermission, db) {
|
|
2122
|
+
function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRepo, requirePermission, db) {
|
|
2017
2123
|
for (const [typeName, contentType] of Object.entries(registry.content_types)) {
|
|
2018
2124
|
const basePath = `content/${typeName}`;
|
|
2019
2125
|
const repo = repos[typeName];
|
|
@@ -2025,6 +2131,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2025
2131
|
const relationFieldNames = new Set(
|
|
2026
2132
|
contentType.fields.filter((f) => RELATION_FIELD_TYPES.has(f.field_type)).map((f) => f.name)
|
|
2027
2133
|
);
|
|
2134
|
+
const fieldKeys = fieldKeyMaps[typeName];
|
|
2028
2135
|
const requiredFields = contentType.fields.filter((f) => f.required);
|
|
2029
2136
|
const mediaFields = contentType.fields.filter(
|
|
2030
2137
|
(f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file"
|
|
@@ -2038,7 +2145,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2038
2145
|
`/admin/api/${basePath}`,
|
|
2039
2146
|
requirePermission("content:read"),
|
|
2040
2147
|
async (c) => {
|
|
2041
|
-
const parsed = parseListQuery(c.req.url, schemaFieldNames, relationFieldNames);
|
|
2148
|
+
const parsed = parseListQuery(c.req.url, schemaFieldNames, relationFieldNames, fieldKeys.columnFor);
|
|
2042
2149
|
if (!parsed.ok) {
|
|
2043
2150
|
return c.json({ ok: false, error: parsed.response }, parsed.status);
|
|
2044
2151
|
}
|
|
@@ -2058,7 +2165,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2058
2165
|
findOpts.search = { term: parsed.search, columns: searchableColumns };
|
|
2059
2166
|
}
|
|
2060
2167
|
const result = await repo.findMany(findOpts);
|
|
2061
|
-
|
|
2168
|
+
const data = result.data.map((row) => fieldKeys.toLabels(row));
|
|
2169
|
+
return c.json({ ...result, data });
|
|
2062
2170
|
}
|
|
2063
2171
|
);
|
|
2064
2172
|
app.get(
|
|
@@ -2093,7 +2201,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2093
2201
|
}
|
|
2094
2202
|
}
|
|
2095
2203
|
}
|
|
2096
|
-
|
|
2204
|
+
const data = fieldKeys.toLabels(item);
|
|
2205
|
+
return c.json({ ok: true, data });
|
|
2097
2206
|
}
|
|
2098
2207
|
);
|
|
2099
2208
|
app.post(
|
|
@@ -2160,6 +2269,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2160
2269
|
}
|
|
2161
2270
|
);
|
|
2162
2271
|
const writeNewItem = async (c, body) => {
|
|
2272
|
+
const storageBody = fieldKeys.toStorage(body);
|
|
2163
2273
|
const fieldErrors = checkRequiredFields(requiredFields, body);
|
|
2164
2274
|
if (fieldErrors.length > 0) {
|
|
2165
2275
|
return c.json(
|
|
@@ -2183,9 +2293,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2183
2293
|
const junctionFields = contentType.fields.filter(
|
|
2184
2294
|
(f) => f.db_column !== null && f.db_column.junction !== void 0
|
|
2185
2295
|
);
|
|
2186
|
-
const
|
|
2187
|
-
(f) => f.db_column !== null && f.db_column.junction === void 0
|
|
2188
|
-
);
|
|
2296
|
+
const columnBackedFields = contentType.fields.filter(isColumnBacked);
|
|
2189
2297
|
const columnData = {
|
|
2190
2298
|
published: body["published"] ?? false
|
|
2191
2299
|
};
|
|
@@ -2194,9 +2302,10 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2194
2302
|
} else {
|
|
2195
2303
|
columnData["slug"] = body["slug"];
|
|
2196
2304
|
}
|
|
2197
|
-
for (const f of
|
|
2198
|
-
const
|
|
2199
|
-
|
|
2305
|
+
for (const f of columnBackedFields) {
|
|
2306
|
+
const key = f.db_column.column_name;
|
|
2307
|
+
if (!(key in storageBody)) continue;
|
|
2308
|
+
columnData[key] = storageBody[key];
|
|
2200
2309
|
}
|
|
2201
2310
|
if (db) {
|
|
2202
2311
|
const basePathId = await lookupBasePathId(db, contentType.default_base_path);
|
|
@@ -2216,7 +2325,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2216
2325
|
}
|
|
2217
2326
|
const item = await repo.create(columnData);
|
|
2218
2327
|
const itemId = item["id"];
|
|
2219
|
-
const mediaDeltas = [topLevelMediaDelta(mediaFields, null,
|
|
2328
|
+
const mediaDeltas = [topLevelMediaDelta(mediaFields, null, storageBody)];
|
|
2220
2329
|
if (db) {
|
|
2221
2330
|
for (const f of paragraphFields) {
|
|
2222
2331
|
const comp = f.ui_component;
|
|
@@ -2233,7 +2342,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2233
2342
|
}
|
|
2234
2343
|
}
|
|
2235
2344
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
2236
|
-
|
|
2345
|
+
const data = fieldKeys.toLabels(item);
|
|
2346
|
+
return c.json({ ok: true, data }, 201);
|
|
2237
2347
|
};
|
|
2238
2348
|
app.patch(
|
|
2239
2349
|
`/admin/api/${basePath}/:id`,
|
|
@@ -2293,11 +2403,12 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2293
2403
|
}
|
|
2294
2404
|
);
|
|
2295
2405
|
const writeExistingItem = async (c, id, existing, body) => {
|
|
2406
|
+
const storageBody = fieldKeys.toStorage(body);
|
|
2296
2407
|
if (body["published"] === true) {
|
|
2297
2408
|
const publishDeny = await requirePermission("content:edit")(c, async () => {
|
|
2298
2409
|
});
|
|
2299
2410
|
if (publishDeny) return publishDeny;
|
|
2300
|
-
const merged = { ...existing, ...body };
|
|
2411
|
+
const merged = { ...fieldKeys.toLabels(existing), ...body };
|
|
2301
2412
|
const fieldErrors = checkRequiredFields(requiredFields, merged);
|
|
2302
2413
|
if (fieldErrors.length > 0) {
|
|
2303
2414
|
return c.json(
|
|
@@ -2317,17 +2428,14 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2317
2428
|
const patchJunctionFields = contentType.fields.filter(
|
|
2318
2429
|
(f) => f.db_column !== null && f.db_column.junction !== void 0
|
|
2319
2430
|
);
|
|
2320
|
-
const
|
|
2321
|
-
(f) => f.db_column !== null && f.db_column.junction === void 0
|
|
2322
|
-
);
|
|
2431
|
+
const patchColumnBackedFields = contentType.fields.filter(isColumnBacked);
|
|
2323
2432
|
const patchData = {};
|
|
2324
2433
|
if (!contentType.only_one && "slug" in body) patchData["slug"] = body["slug"];
|
|
2325
2434
|
if ("published" in body) patchData["published"] = body["published"];
|
|
2326
|
-
for (const f of
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
}
|
|
2435
|
+
for (const f of patchColumnBackedFields) {
|
|
2436
|
+
const key = f.db_column.column_name;
|
|
2437
|
+
if (!(key in storageBody)) continue;
|
|
2438
|
+
patchData[key] = storageBody[key];
|
|
2331
2439
|
}
|
|
2332
2440
|
const updated = await repo.update(id, patchData);
|
|
2333
2441
|
if (!updated) {
|
|
@@ -2337,7 +2445,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2337
2445
|
);
|
|
2338
2446
|
}
|
|
2339
2447
|
const mediaDeltas = [
|
|
2340
|
-
topLevelMediaDelta(mediaFields, existing,
|
|
2448
|
+
topLevelMediaDelta(mediaFields, existing, storageBody)
|
|
2341
2449
|
];
|
|
2342
2450
|
if (db) {
|
|
2343
2451
|
for (const f of patchParagraphFields) {
|
|
@@ -2355,7 +2463,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2355
2463
|
}
|
|
2356
2464
|
}
|
|
2357
2465
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
2358
|
-
|
|
2466
|
+
const data = fieldKeys.toLabels(updated);
|
|
2467
|
+
return c.json({ ok: true, data });
|
|
2359
2468
|
};
|
|
2360
2469
|
if (contentType.only_one) {
|
|
2361
2470
|
app.put(
|
|
@@ -2415,6 +2524,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2415
2524
|
const relationFieldNames = new Set(
|
|
2416
2525
|
taxonomyType.fields.filter((f) => RELATION_FIELD_TYPES.has(f.field_type)).map((f) => f.name)
|
|
2417
2526
|
);
|
|
2527
|
+
const fieldKeys = fieldKeyMaps[typeName];
|
|
2418
2528
|
const requiredFields = taxonomyType.fields.filter((f) => f.required);
|
|
2419
2529
|
const mediaFields = taxonomyType.fields.filter(
|
|
2420
2530
|
(f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file"
|
|
@@ -2428,7 +2538,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2428
2538
|
`/admin/api/taxonomy/${typeName}`,
|
|
2429
2539
|
requirePermission("content:read"),
|
|
2430
2540
|
async (c) => {
|
|
2431
|
-
const parsed = parseListQuery(c.req.url, schemaFieldNames, relationFieldNames);
|
|
2541
|
+
const parsed = parseListQuery(c.req.url, schemaFieldNames, relationFieldNames, fieldKeys.columnFor);
|
|
2432
2542
|
if (!parsed.ok) {
|
|
2433
2543
|
return c.json({ ok: false, error: parsed.response }, parsed.status);
|
|
2434
2544
|
}
|
|
@@ -2448,7 +2558,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2448
2558
|
findOpts.search = { term: parsed.search, columns: searchableColumns };
|
|
2449
2559
|
}
|
|
2450
2560
|
const result = await repo.findMany(findOpts);
|
|
2451
|
-
|
|
2561
|
+
const data = result.data.map((row) => fieldKeys.toLabels(row));
|
|
2562
|
+
return c.json({ ...result, data });
|
|
2452
2563
|
}
|
|
2453
2564
|
);
|
|
2454
2565
|
app.get(
|
|
@@ -2463,7 +2574,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2463
2574
|
404
|
|
2464
2575
|
);
|
|
2465
2576
|
}
|
|
2466
|
-
|
|
2577
|
+
const data = fieldKeys.toLabels(item);
|
|
2578
|
+
return c.json({ ok: true, data });
|
|
2467
2579
|
}
|
|
2468
2580
|
);
|
|
2469
2581
|
app.post(
|
|
@@ -2471,6 +2583,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2471
2583
|
requirePermission("content:create"),
|
|
2472
2584
|
async (c) => {
|
|
2473
2585
|
const body = await c.req.json();
|
|
2586
|
+
const storageBody = fieldKeys.toStorage(body);
|
|
2474
2587
|
const fieldErrors = checkRequiredFields(requiredFields, body);
|
|
2475
2588
|
if (fieldErrors.length > 0) {
|
|
2476
2589
|
return c.json(
|
|
@@ -2490,20 +2603,19 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2490
2603
|
});
|
|
2491
2604
|
if (publishDeny) return publishDeny;
|
|
2492
2605
|
}
|
|
2493
|
-
const taxColumnFields = taxonomyType.fields.filter(
|
|
2494
|
-
(f) => f.db_column !== null && f.db_column.junction === void 0
|
|
2495
|
-
);
|
|
2496
2606
|
const taxParagraphFields = taxonomyType.fields.filter((f) => f.db_column === null);
|
|
2607
|
+
const taxColumnBackedFields = taxonomyType.fields.filter(isColumnBacked);
|
|
2497
2608
|
const taxColumnData = {
|
|
2498
2609
|
published: body["published"] ?? false
|
|
2499
2610
|
};
|
|
2500
|
-
for (const f of
|
|
2501
|
-
const
|
|
2502
|
-
|
|
2611
|
+
for (const f of taxColumnBackedFields) {
|
|
2612
|
+
const key = f.db_column.column_name;
|
|
2613
|
+
if (!(key in storageBody)) continue;
|
|
2614
|
+
taxColumnData[key] = storageBody[key];
|
|
2503
2615
|
}
|
|
2504
2616
|
const item = await repo.create(taxColumnData);
|
|
2505
2617
|
const taxItemId = item["id"];
|
|
2506
|
-
const mediaDeltas = [topLevelMediaDelta(mediaFields, null,
|
|
2618
|
+
const mediaDeltas = [topLevelMediaDelta(mediaFields, null, storageBody)];
|
|
2507
2619
|
if (db) {
|
|
2508
2620
|
for (const f of taxParagraphFields) {
|
|
2509
2621
|
const comp = f.ui_component;
|
|
@@ -2515,7 +2627,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2515
2627
|
}
|
|
2516
2628
|
}
|
|
2517
2629
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
2518
|
-
|
|
2630
|
+
const data = fieldKeys.toLabels(item);
|
|
2631
|
+
return c.json({ ok: true, data }, 201);
|
|
2519
2632
|
}
|
|
2520
2633
|
);
|
|
2521
2634
|
app.patch(
|
|
@@ -2524,6 +2637,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2524
2637
|
async (c) => {
|
|
2525
2638
|
const id = c.req.param("id");
|
|
2526
2639
|
const body = await c.req.json();
|
|
2640
|
+
const storageBody = fieldKeys.toStorage(body);
|
|
2527
2641
|
const existing = await repo.findOne(id);
|
|
2528
2642
|
if (!existing) {
|
|
2529
2643
|
return c.json(
|
|
@@ -2535,7 +2649,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2535
2649
|
const publishDeny = await requirePermission("content:edit")(c, async () => {
|
|
2536
2650
|
});
|
|
2537
2651
|
if (publishDeny) return publishDeny;
|
|
2538
|
-
const merged = { ...existing, ...body };
|
|
2652
|
+
const merged = { ...fieldKeys.toLabels(existing), ...body };
|
|
2539
2653
|
const fieldErrors = checkRequiredFields(requiredFields, merged);
|
|
2540
2654
|
if (fieldErrors.length > 0) {
|
|
2541
2655
|
return c.json(
|
|
@@ -2551,17 +2665,14 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2551
2665
|
);
|
|
2552
2666
|
}
|
|
2553
2667
|
}
|
|
2554
|
-
const taxPatchColumnFields = taxonomyType.fields.filter(
|
|
2555
|
-
(f) => f.db_column !== null && f.db_column.junction === void 0
|
|
2556
|
-
);
|
|
2557
2668
|
const taxPatchParagraphFields = taxonomyType.fields.filter((f) => f.db_column === null);
|
|
2669
|
+
const taxPatchColumnBackedFields = taxonomyType.fields.filter(isColumnBacked);
|
|
2558
2670
|
const taxPatchData = {};
|
|
2559
2671
|
if ("published" in body) taxPatchData["published"] = body["published"];
|
|
2560
|
-
for (const f of
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
}
|
|
2672
|
+
for (const f of taxPatchColumnBackedFields) {
|
|
2673
|
+
const key = f.db_column.column_name;
|
|
2674
|
+
if (!(key in storageBody)) continue;
|
|
2675
|
+
taxPatchData[key] = storageBody[key];
|
|
2565
2676
|
}
|
|
2566
2677
|
const updated = await repo.update(id, taxPatchData);
|
|
2567
2678
|
if (!updated) {
|
|
@@ -2571,7 +2682,7 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2571
2682
|
);
|
|
2572
2683
|
}
|
|
2573
2684
|
const mediaDeltas = [
|
|
2574
|
-
topLevelMediaDelta(mediaFields, existing,
|
|
2685
|
+
topLevelMediaDelta(mediaFields, existing, storageBody)
|
|
2575
2686
|
];
|
|
2576
2687
|
if (db) {
|
|
2577
2688
|
for (const f of taxPatchParagraphFields) {
|
|
@@ -2584,7 +2695,8 @@ function registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePerm
|
|
|
2584
2695
|
}
|
|
2585
2696
|
}
|
|
2586
2697
|
await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
|
|
2587
|
-
|
|
2698
|
+
const data = fieldKeys.toLabels(updated);
|
|
2699
|
+
return c.json({ ok: true, data });
|
|
2588
2700
|
}
|
|
2589
2701
|
);
|
|
2590
2702
|
app.delete(
|
|
@@ -3576,7 +3688,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
|
|
|
3576
3688
|
var import_drizzle_orm6 = require("drizzle-orm");
|
|
3577
3689
|
var API_VERSION = (() => {
|
|
3578
3690
|
try {
|
|
3579
|
-
return "0.
|
|
3691
|
+
return "0.4.0";
|
|
3580
3692
|
} catch {
|
|
3581
3693
|
return "0.0.0";
|
|
3582
3694
|
}
|
|
@@ -3978,6 +4090,7 @@ function createMediaRepository(db) {
|
|
|
3978
4090
|
}
|
|
3979
4091
|
|
|
3980
4092
|
// src/app.ts
|
|
4093
|
+
init_field_keys();
|
|
3981
4094
|
var MISSING_STORAGE_ERROR = `\u2717 api.storage is required but not configured.
|
|
3982
4095
|
Add a storage adapter to your manguito.config.ts:
|
|
3983
4096
|
|
|
@@ -3991,7 +4104,8 @@ function createCmsApp(options) {
|
|
|
3991
4104
|
if (!options.storage) {
|
|
3992
4105
|
throw new Error(MISSING_STORAGE_ERROR);
|
|
3993
4106
|
}
|
|
3994
|
-
const prefix = options.prefix
|
|
4107
|
+
const prefix = normalizePrefix(options.prefix);
|
|
4108
|
+
const publicPaths = createPublicPaths(prefix);
|
|
3995
4109
|
const { storage, registry, db, rateLimit, media, cors } = options;
|
|
3996
4110
|
const cmsName = options.name ?? "Manguito CMS";
|
|
3997
4111
|
const maxFileSize = media?.max_file_size;
|
|
@@ -4071,23 +4185,27 @@ function createCmsApp(options) {
|
|
|
4071
4185
|
registerAuthRoutes(app, db);
|
|
4072
4186
|
app.use("/admin/api/*", authMiddleware);
|
|
4073
4187
|
app.use("/admin/api/*", mustChangePasswordCheck);
|
|
4074
|
-
app.get(
|
|
4188
|
+
app.get(publicPaths.openapi(), (c) => {
|
|
4075
4189
|
const paths = {};
|
|
4076
4190
|
for (const ct of Object.values(registry.content_types)) {
|
|
4077
4191
|
const base = ct.default_base_path;
|
|
4078
4192
|
if (ct.only_one) {
|
|
4079
|
-
paths[
|
|
4193
|
+
paths[publicPaths.collection(base)] = { get: { summary: `Get ${ct.label}`, tags: [ct.label] } };
|
|
4080
4194
|
} else {
|
|
4081
|
-
paths[
|
|
4082
|
-
paths[
|
|
4195
|
+
paths[publicPaths.collection(base)] = { get: { summary: `List published ${ct.label}`, tags: [ct.label] } };
|
|
4196
|
+
paths[publicPaths.item(base).replace(":slug", "{slug}")] = {
|
|
4197
|
+
get: { summary: `Get ${ct.label} by slug`, tags: [ct.label] }
|
|
4198
|
+
};
|
|
4083
4199
|
}
|
|
4084
4200
|
}
|
|
4085
4201
|
for (const tt of Object.values(registry.taxonomy_types)) {
|
|
4086
|
-
paths[
|
|
4087
|
-
paths[
|
|
4202
|
+
paths[publicPaths.taxonomyCollection(tt.name)] = { get: { summary: `List published ${tt.label}`, tags: [tt.label] } };
|
|
4203
|
+
paths[publicPaths.taxonomyItem(tt.name).replace(":id", "{id}")] = {
|
|
4204
|
+
get: { summary: `Get ${tt.label} by id`, tags: [tt.label] }
|
|
4205
|
+
};
|
|
4088
4206
|
}
|
|
4089
|
-
paths[
|
|
4090
|
-
paths["
|
|
4207
|
+
paths[publicPaths.mediaCollection()] = { get: { summary: "List media items", tags: ["Media"] } };
|
|
4208
|
+
paths[publicPaths.mediaItem().replace(":id", "{id}")] = { get: { summary: "Get media item by id", tags: ["Media"] } };
|
|
4091
4209
|
return c.json({
|
|
4092
4210
|
openapi: "3.0.3",
|
|
4093
4211
|
info: { title: "Manguito CMS Public API", version: "1.0.0" },
|
|
@@ -4102,14 +4220,24 @@ function createCmsApp(options) {
|
|
|
4102
4220
|
paths: {}
|
|
4103
4221
|
})
|
|
4104
4222
|
);
|
|
4105
|
-
|
|
4106
|
-
|
|
4223
|
+
const fieldKeyMaps = Object.fromEntries([
|
|
4224
|
+
...Object.entries(registry.content_types).map(([typeName, ct]) => [
|
|
4225
|
+
typeName,
|
|
4226
|
+
createFieldKeyMap(ct.fields)
|
|
4227
|
+
]),
|
|
4228
|
+
...Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
|
|
4229
|
+
typeName,
|
|
4230
|
+
createFieldKeyMap(tt.fields)
|
|
4231
|
+
])
|
|
4232
|
+
]);
|
|
4233
|
+
registerPublicContentRoutes(app, registry, publicRepos, fieldKeyMaps, publicPaths, listRateLimit, programmaticResolver);
|
|
4234
|
+
registerPublicMediaRoutes(app, mediaRepo, publicPaths, listRateLimit);
|
|
4107
4235
|
if (options.graphql?.enabled) {
|
|
4108
4236
|
const gqlOptions = options.graphql;
|
|
4109
4237
|
let gqlHandler = null;
|
|
4110
4238
|
let initError = null;
|
|
4111
4239
|
const ready = Promise.resolve().then(() => (init_handler(), handler_exports)).then(({ createGraphQLHandler: createGraphQLHandler2 }) => {
|
|
4112
|
-
gqlHandler = createGraphQLHandler2(registry, graphqlRepos, programmaticResolver, db, gqlOptions);
|
|
4240
|
+
gqlHandler = createGraphQLHandler2(registry, graphqlRepos, fieldKeyMaps, programmaticResolver, db, gqlOptions);
|
|
4113
4241
|
}).catch((err) => {
|
|
4114
4242
|
initError = err;
|
|
4115
4243
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -4149,7 +4277,7 @@ function createCmsApp(options) {
|
|
|
4149
4277
|
);
|
|
4150
4278
|
registerSchemaRoute(app, registry, db);
|
|
4151
4279
|
registerUserRoutes(app, db, requirePermission, requireHierarchy);
|
|
4152
|
-
registerAdminContentRoutes(app, registry, repos, mediaRepo, requirePermission, db);
|
|
4280
|
+
registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRepo, requirePermission, db);
|
|
4153
4281
|
registerAdminMediaRoutes(app, mediaRepo, storage, requirePermission, maxFileSize);
|
|
4154
4282
|
return { prefix, app };
|
|
4155
4283
|
}
|
|
@@ -4190,7 +4318,7 @@ function resolveGraphQL(cfg) {
|
|
|
4190
4318
|
};
|
|
4191
4319
|
}
|
|
4192
4320
|
function createAPIAdapter(options = {}) {
|
|
4193
|
-
const prefix = options.prefix
|
|
4321
|
+
const prefix = normalizePrefix(options.prefix);
|
|
4194
4322
|
const media = options.media?.max_file_size !== void 0 ? { max_file_size: options.media.max_file_size } : void 0;
|
|
4195
4323
|
return {
|
|
4196
4324
|
prefix,
|