@bobbykim/manguito-cms-cli 0.4.3 → 0.4.5

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.
@@ -49,7 +49,7 @@ var init_esm_shims = __esm({
49
49
  }
50
50
  });
51
51
 
52
- // ../api/dist/chunk-TK4DNRFF.mjs
52
+ // ../api/dist/chunk-4F2CRXPG.mjs
53
53
  init_esm_shims();
54
54
  import crypto from "crypto";
55
55
 
@@ -1212,7 +1212,7 @@ Subquery.prototype.getSQL = function() {
1212
1212
  return new SQL([this]);
1213
1213
  };
1214
1214
 
1215
- // ../api/dist/chunk-TK4DNRFF.mjs
1215
+ // ../api/dist/chunk-4F2CRXPG.mjs
1216
1216
  function quoteIdent(name) {
1217
1217
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
1218
1218
  return `"${name}"`;
@@ -1382,22 +1382,34 @@ function groupBy(items, key) {
1382
1382
  }
1383
1383
  return result;
1384
1384
  }
1385
+ function needsFkResolution(row, fkColumn, fieldName) {
1386
+ if (fkColumn !== fieldName && !(fkColumn in row)) return false;
1387
+ const current = row[fieldName];
1388
+ return !(typeof current === "object" && current !== null);
1389
+ }
1385
1390
  async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOnly = false) {
1386
1391
  if (rows.length === 0) return;
1392
+ const batch = [...new Set(rows)];
1387
1393
  if (rel.type === "paragraph") {
1388
- const parentIds = rows.map((r) => r["id"]);
1394
+ const parentIds = batch.map((r) => r["id"]);
1389
1395
  const inList = sql.join(parentIds.map((id) => sql`${id}`), sql`, `);
1390
1396
  const result = await db.execute(
1391
1397
  sql`SELECT * FROM ${sql.raw(quoteIdent(rel.table))} WHERE parent_id IN (${inList}) AND parent_field = ${fieldName} ORDER BY "order" ASC`
1392
1398
  );
1393
1399
  const byParent = groupBy(result.rows, "parent_id");
1394
- for (const row of rows) {
1400
+ for (const row of batch) {
1395
1401
  row[fieldName] = byParent[row["id"]] ?? [];
1396
1402
  }
1397
1403
  } else if (rel.type === "reference") {
1398
- const fkValues = rows.map((r) => r[rel.fk_column]).filter(Boolean);
1404
+ const dropFk = rel.fk_column !== fieldName;
1405
+ const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column, fieldName));
1406
+ if (pending.length === 0) return;
1407
+ const fkValues = pending.map((r) => r[rel.fk_column]).filter(Boolean);
1399
1408
  if (fkValues.length === 0) {
1400
- for (const row of rows) row[fieldName] = null;
1409
+ for (const row of pending) {
1410
+ row[fieldName] = null;
1411
+ if (dropFk) delete row[rel.fk_column];
1412
+ }
1401
1413
  return;
1402
1414
  }
1403
1415
  const unique = [...new Set(fkValues)];
@@ -1412,12 +1424,13 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
1412
1424
  cache.set(`${rel.table}:${item["id"]}`, item);
1413
1425
  }
1414
1426
  }
1415
- for (const row of rows) {
1427
+ for (const row of pending) {
1416
1428
  const fkVal = row[rel.fk_column];
1417
1429
  row[fieldName] = fkVal ? cache.get(`${rel.table}:${fkVal}`) ?? null : null;
1430
+ if (dropFk) delete row[rel.fk_column];
1418
1431
  }
1419
1432
  } else if (rel.type === "junction") {
1420
- const parentIds = rows.map((r) => r["id"]);
1433
+ const parentIds = batch.map((r) => r["id"]);
1421
1434
  const inList = sql.join(parentIds.map((id) => sql`${id}`), sql`, `);
1422
1435
  const orderBy = rel.order_column ? sql` ORDER BY "order" ASC` : sql``;
1423
1436
  const junctionResult = await db.execute(
@@ -1437,14 +1450,20 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
1437
1450
  }
1438
1451
  }
1439
1452
  const byLeft = groupBy(jRows, rel.left_column);
1440
- for (const row of rows) {
1453
+ for (const row of batch) {
1441
1454
  const jEntries = byLeft[row["id"]] ?? [];
1442
1455
  row[fieldName] = jEntries.map((jr) => cache.get(`${rel.table}:${jr[rel.right_column]}`)).filter(Boolean);
1443
1456
  }
1444
1457
  } else if (rel.type === "media") {
1445
- const fkValues = rows.map((r) => r[rel.fk_column]).filter(Boolean);
1458
+ const dropFk = rel.fk_column !== fieldName;
1459
+ const pending = batch.filter((r) => needsFkResolution(r, rel.fk_column, fieldName));
1460
+ if (pending.length === 0) return;
1461
+ const fkValues = pending.map((r) => r[rel.fk_column]).filter(Boolean);
1446
1462
  if (fkValues.length === 0) {
1447
- for (const row of rows) row[fieldName] = null;
1463
+ for (const row of pending) {
1464
+ row[fieldName] = null;
1465
+ if (dropFk) delete row[rel.fk_column];
1466
+ }
1448
1467
  return;
1449
1468
  }
1450
1469
  const unique = [...new Set(fkValues)];
@@ -1456,9 +1475,10 @@ async function resolveRelationField(db, rows, fieldName, rel, cache, publishedOn
1456
1475
  cache.set(`media:${item["id"]}`, item);
1457
1476
  }
1458
1477
  }
1459
- for (const row of rows) {
1478
+ for (const row of pending) {
1460
1479
  const fkVal = row[rel.fk_column];
1461
1480
  row[fieldName] = fkVal ? cache.get(`media:${fkVal}`) ?? null : null;
1481
+ if (dropFk) delete row[rel.fk_column];
1462
1482
  }
1463
1483
  }
1464
1484
  }
@@ -1486,6 +1506,46 @@ async function resolveRelationBareIds(db, rows, fieldName, rel) {
1486
1506
  }
1487
1507
  }
1488
1508
  }
1509
+ function isColumnBacked(field) {
1510
+ const col = field.db_column;
1511
+ if (col === null) return false;
1512
+ if (col.column_name === "") return false;
1513
+ if (col.junction) return false;
1514
+ return true;
1515
+ }
1516
+ function createFieldKeyMap(fields) {
1517
+ const labelToColumn = /* @__PURE__ */ new Map();
1518
+ const columnToLabel = /* @__PURE__ */ new Map();
1519
+ for (const f of fields) {
1520
+ if (!isColumnBacked(f)) continue;
1521
+ labelToColumn.set(f.name, f.db_column.column_name);
1522
+ columnToLabel.set(f.db_column.column_name, f.name);
1523
+ }
1524
+ for (const f of fields) {
1525
+ const columnOwner = columnToLabel.get(f.name);
1526
+ if (columnOwner !== void 0 && columnOwner !== f.name) {
1527
+ throw new Error(
1528
+ `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.`
1529
+ );
1530
+ }
1531
+ }
1532
+ const diverges = [...labelToColumn].some(([label, column]) => label !== column);
1533
+ function remap(input, lookup) {
1534
+ const out = {};
1535
+ for (const key of Object.keys(input)) {
1536
+ out[lookup.get(key) ?? key] = input[key];
1537
+ }
1538
+ return out;
1539
+ }
1540
+ return {
1541
+ toStorage: (input) => remap(input, labelToColumn),
1542
+ toLabels: (row) => remap(row, columnToLabel),
1543
+ columnFor: (label) => labelToColumn.get(label),
1544
+ labelFor: (column) => columnToLabel.get(column),
1545
+ labels: [...labelToColumn.keys()],
1546
+ diverges
1547
+ };
1548
+ }
1489
1549
 
1490
1550
  export {
1491
1551
  __require,
@@ -1523,5 +1583,7 @@ export {
1523
1583
  persistJunctionField,
1524
1584
  buildRelationsMap,
1525
1585
  resolveRelationField,
1526
- resolveRelationBareIds
1586
+ resolveRelationBareIds,
1587
+ isColumnBacked,
1588
+ createFieldKeyMap
1527
1589
  };
@@ -9,8 +9,9 @@ import {
9
9
  __toESM,
10
10
  buildRelationsMap,
11
11
  init_esm_shims,
12
+ isColumnBacked,
12
13
  resolveRelationField
13
- } from "./chunk-OAUBUE7N.js";
14
+ } from "./chunk-UNBMPHIT.js";
14
15
 
15
16
  // ../../node_modules/.pnpm/@whatwg-node+fetch@0.10.13/node_modules/@whatwg-node/fetch/dist/shouldSkipPonyfill.js
16
17
  var require_shouldSkipPonyfill = __commonJS({
@@ -10785,10 +10786,10 @@ var require_parser = __commonJS({
10785
10786
  }
10786
10787
  });
10787
10788
 
10788
- // ../api/dist/handler-CGHZG3J5.mjs
10789
+ // ../api/dist/handler-7LVYEU5C.mjs
10789
10790
  init_esm_shims();
10790
10791
 
10791
- // ../api/dist/chunk-GM55HGWA.mjs
10792
+ // ../api/dist/chunk-S4NT6FUM.mjs
10792
10793
  init_esm_shims();
10793
10794
 
10794
10795
  // ../../node_modules/.pnpm/graphql-yoga@5.21.2_graphql@16.14.2/node_modules/graphql-yoga/esm/index.js
@@ -30121,7 +30122,7 @@ function createYoga(options) {
30121
30122
  });
30122
30123
  }
30123
30124
 
30124
- // ../api/dist/chunk-GM55HGWA.mjs
30125
+ // ../api/dist/chunk-S4NT6FUM.mjs
30125
30126
  var import_dataloader = __toESM(require_dataloader(), 1);
30126
30127
 
30127
30128
  // ../../node_modules/.pnpm/@escape.tech+graphql-armor@3.2.0_@envelop+core@5.5.1_@escape.tech+graphql-armor-types@0.7.0/node_modules/@escape.tech/graphql-armor/dist/graphql-armor.esm.js
@@ -30879,7 +30880,7 @@ var EnvelopArmor = class {
30879
30880
  }
30880
30881
  };
30881
30882
 
30882
- // ../api/dist/chunk-GM55HGWA.mjs
30883
+ // ../api/dist/chunk-S4NT6FUM.mjs
30883
30884
  var DateTimeScalar = new GraphQLScalarType({
30884
30885
  name: "DateTime",
30885
30886
  description: "ISO-8601 date-time string",
@@ -30974,14 +30975,14 @@ var SortOrderEnum = new GraphQLEnumType({
30974
30975
  values: { ASC: { value: "asc" }, DESC: { value: "desc" } }
30975
30976
  });
30976
30977
  var SORTABLE = [
30977
- { gql: "title", column: "title" },
30978
- { gql: "createdAt", column: "created_at" },
30979
- { gql: "updatedAt", column: "updated_at" }
30978
+ { gql: "title", value: "title" },
30979
+ { gql: "createdAt", value: "created_at" },
30980
+ { gql: "updatedAt", value: "updated_at" }
30980
30981
  ];
30981
30982
  function buildSortFieldEnum(typeName) {
30982
30983
  return new GraphQLEnumType({
30983
30984
  name: `${typeName}SortField`,
30984
- values: Object.fromEntries(SORTABLE.map((s) => [s.gql, { value: s.column }]))
30985
+ values: Object.fromEntries(SORTABLE.map((s) => [s.gql, { value: s.value }]))
30985
30986
  });
30986
30987
  }
30987
30988
  function comparableFilterInput(name, scalar) {
@@ -31041,12 +31042,13 @@ function buildFilterInputType(type) {
31041
31042
  if (Object.keys(fields).length === 0) return null;
31042
31043
  return new GraphQLInputObjectType({ name: `${graphqlTypeName(type.name)}Filter`, fields });
31043
31044
  }
31044
- function translateFilters(input, nameMap) {
31045
+ function translateFilters(input, nameMap, columnFor) {
31045
31046
  const out = {};
31046
31047
  if (!input) return out;
31047
31048
  for (const [gqlField, raw] of Object.entries(input)) {
31048
31049
  if (raw === null || typeof raw !== "object") continue;
31049
- const column = nameMap.toSchema(gqlField);
31050
+ const label = nameMap.toSchema(gqlField);
31051
+ const column = columnFor?.(label) ?? label;
31050
31052
  const spec = raw;
31051
31053
  if (spec["eq"] !== void 0) {
31052
31054
  out[column] = spec["eq"];
@@ -31068,29 +31070,34 @@ function codeError(code, message) {
31068
31070
  function isPublished(item) {
31069
31071
  return !!item && item["published"] === true;
31070
31072
  }
31071
- function scalarFieldResolver(schemaName) {
31072
- return (parent) => parent[schemaName];
31073
+ function resolveFieldValue(field, row) {
31074
+ const key = isColumnBacked(field) ? field.db_column.column_name : field.name;
31075
+ return row[key] ?? null;
31076
+ }
31077
+ function scalarFieldResolver(field) {
31078
+ return (parent) => resolveFieldValue(field, parent);
31073
31079
  }
31074
31080
  function relationFieldResolver(typeName, schemaFieldName) {
31075
31081
  return (parent, _args, ctx) => ctx.loaders.load(typeName, schemaFieldName, parent);
31076
31082
  }
31077
- function programmaticFieldResolver(typeName, schemaFieldName, mediaFieldNames = []) {
31083
+ function programmaticFieldResolver(typeName, schemaFieldName, mediaFieldNames = [], fieldKeys) {
31078
31084
  return async (parent, _args, ctx) => {
31079
31085
  let p = ctx.programmaticMemo.get(parent);
31080
31086
  if (!p) {
31081
- p = resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames);
31087
+ p = resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames, fieldKeys);
31082
31088
  ctx.programmaticMemo.set(parent, p);
31083
31089
  }
31084
31090
  return (await p)[schemaFieldName];
31085
31091
  };
31086
31092
  }
31087
- async function resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames) {
31088
- if (mediaFieldNames.length === 0) return ctx.resolver.resolveItem(typeName, parent);
31093
+ async function resolveProgrammaticRow(typeName, parent, ctx, mediaFieldNames, fieldKeys) {
31094
+ const toLabels = (row) => fieldKeys ? fieldKeys.toLabels(row) : row;
31095
+ if (mediaFieldNames.length === 0) return ctx.resolver.resolveItem(typeName, toLabels(parent));
31089
31096
  const enriched = { ...parent };
31090
31097
  await Promise.all(mediaFieldNames.map((name) => ctx.loaders.load(typeName, name, enriched)));
31091
- return ctx.resolver.resolveItem(typeName, enriched);
31098
+ return ctx.resolver.resolveItem(typeName, toLabels(enriched));
31092
31099
  }
31093
- function collectionResolver(typeName, nameMap) {
31100
+ function collectionResolver(typeName, nameMap, fieldKeys) {
31094
31101
  return async (_root, args, ctx) => {
31095
31102
  const page = args.page ?? 1;
31096
31103
  const perPage = args.perPage ?? 10;
@@ -31101,13 +31108,15 @@ function collectionResolver(typeName, nameMap) {
31101
31108
  throw codeError("INVALID_PAGINATION", "perPage must be between 1 and 100");
31102
31109
  }
31103
31110
  const repo = ctx.repos[typeName];
31111
+ const sortBy = args.sortBy ?? "created_at";
31112
+ const sortColumn = fieldKeys?.columnFor(sortBy) ?? sortBy;
31104
31113
  const result = await repo.findMany({
31105
31114
  published_only: true,
31106
31115
  page,
31107
31116
  per_page: perPage,
31108
- sort_by: args.sortBy ?? "created_at",
31117
+ sort_by: sortColumn,
31109
31118
  sort_order: args.sortOrder ?? "asc",
31110
- filters: translateFilters(args.filter, nameMap)
31119
+ filters: translateFilters(args.filter, nameMap, fieldKeys?.columnFor)
31111
31120
  });
31112
31121
  return { data: result.data, meta: result.meta };
31113
31122
  };
@@ -31158,7 +31167,7 @@ var MEDIA = new GraphQLObjectType({
31158
31167
  duration: { type: GraphQLInt, resolve: (m) => m.duration }
31159
31168
  }
31160
31169
  });
31161
- function buildGraphQLSchema(registry) {
31170
+ function buildGraphQLSchema(registry, fieldKeyMaps = {}) {
31162
31171
  const objectTypes = /* @__PURE__ */ new Map();
31163
31172
  const enumTypes = /* @__PURE__ */ new Map();
31164
31173
  for (const [name, enumType] of Object.entries(registry.enum_types)) {
@@ -31234,11 +31243,16 @@ function buildGraphQLSchema(registry) {
31234
31243
  const outType = outputTypeForField(field);
31235
31244
  let resolve;
31236
31245
  if (field.field_type === "programmatic") {
31237
- resolve = programmaticFieldResolver(machineName, field.name, mediaFieldNames);
31246
+ resolve = programmaticFieldResolver(
31247
+ machineName,
31248
+ field.name,
31249
+ mediaFieldNames,
31250
+ fieldKeyMaps[machineName]
31251
+ );
31238
31252
  } else if (field.field_type === "reference" || field.field_type === "paragraph" || field.field_type === "image" || field.field_type === "video" || field.field_type === "file") {
31239
31253
  resolve = relationFieldResolver(machineName, field.name);
31240
31254
  } else {
31241
- resolve = scalarFieldResolver(field.name);
31255
+ resolve = scalarFieldResolver(field);
31242
31256
  }
31243
31257
  fields[gqlName] = { type: outType, resolve };
31244
31258
  }
@@ -31294,7 +31308,7 @@ function buildGraphQLSchema(registry) {
31294
31308
  sortOrder: { type: SortOrderEnum },
31295
31309
  ...filterType ? { filter: { type: filterType } } : {}
31296
31310
  },
31297
- resolve: collectionResolver(name, nameMap)
31311
+ resolve: collectionResolver(name, nameMap, fieldKeyMaps[name])
31298
31312
  };
31299
31313
  queryFields[singleQueryName(name)] = {
31300
31314
  type: objType,
@@ -31314,7 +31328,7 @@ function buildGraphQLSchema(registry) {
31314
31328
  queryFields[collectionQueryName(name)] = {
31315
31329
  type: new GraphQLNonNull(listType),
31316
31330
  args: { page: { type: GraphQLInt }, perPage: { type: GraphQLInt } },
31317
- resolve: collectionResolver(name, buildFieldNameMap(tt.fields.map((f) => f.name)))
31331
+ resolve: collectionResolver(name, buildFieldNameMap(tt.fields.map((f) => f.name)), fieldKeyMaps[name])
31318
31332
  };
31319
31333
  queryFields[singleQueryName(name)] = {
31320
31334
  type: objType,
@@ -31393,8 +31407,8 @@ function isClientSafeGraphQLError(error) {
31393
31407
  }
31394
31408
  return false;
31395
31409
  }
31396
- function createGraphQLHandler(registry, repos, resolver, db, options) {
31397
- const schema = buildGraphQLSchema(registry);
31410
+ function createGraphQLHandler(registry, repos, fieldKeyMaps, resolver, db, options) {
31411
+ const schema = buildGraphQLSchema(registry, fieldKeyMaps);
31398
31412
  const { plugins } = buildArmorPlugin({
31399
31413
  maxDepth: options.maxDepth,
31400
31414
  maxComplexity: options.maxComplexity
package/dist/index.js CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  __require,
22
22
  __toESM,
23
23
  buildRelationsMap,
24
+ createFieldKeyMap,
24
25
  deleteParagraphField,
25
26
  entityKind,
26
27
  fillPlaceholders,
@@ -28,6 +29,7 @@ import {
28
29
  getTableUniqueName,
29
30
  init_esm_shims,
30
31
  is,
32
+ isColumnBacked,
31
33
  isDriverValueEncoder,
32
34
  isSQLWrapper,
33
35
  persistJunctionField,
@@ -36,7 +38,7 @@ import {
36
38
  resolveRelationField,
37
39
  sql,
38
40
  tracer
39
- } from "./chunk-OAUBUE7N.js";
41
+ } from "./chunk-UNBMPHIT.js";
40
42
 
41
43
  // ../../node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/nodes/identity.js
42
44
  var require_identity = __commonJS({
@@ -46557,6 +46559,23 @@ var sign2 = Jwt.sign;
46557
46559
  import { randomBytes } from "crypto";
46558
46560
  import crypto3 from "crypto";
46559
46561
  import crypto22 from "crypto";
46562
+ function normalizePrefix(prefix) {
46563
+ if (prefix === void 0) return "/api";
46564
+ const withSlash = prefix.startsWith("/") ? prefix : `/${prefix}`;
46565
+ const trimmed = withSlash.replace(/\/+$/, "");
46566
+ return trimmed === "" ? "/api" : trimmed;
46567
+ }
46568
+ function createPublicPaths(prefix) {
46569
+ return {
46570
+ collection: (basePath) => `${prefix}/${basePath}`,
46571
+ item: (basePath) => `${prefix}/${basePath}/:slug`,
46572
+ taxonomyCollection: (typeName) => `${prefix}/taxonomy/${typeName}`,
46573
+ taxonomyItem: (typeName) => `${prefix}/taxonomy/${typeName}/:id`,
46574
+ mediaCollection: () => `${prefix}/media`,
46575
+ mediaItem: () => `${prefix}/media/:id`,
46576
+ openapi: () => `${prefix}/openapi.json`
46577
+ };
46578
+ }
46560
46579
  function createCorsMiddleware(corsConfig) {
46561
46580
  const allowList = Array.isArray(corsConfig.origin) ? corsConfig.origin : [corsConfig.origin];
46562
46581
  const wildcard = allowList.includes("*");
@@ -47103,6 +47122,52 @@ function createProgrammaticResolver(resolvers, options) {
47103
47122
  }
47104
47123
  return { resolveItem, resolveList, hasSchema };
47105
47124
  }
47125
+ function isPlainRow(v2) {
47126
+ return typeof v2 === "object" && v2 !== null && !Array.isArray(v2) && !(v2 instanceof Date);
47127
+ }
47128
+ var MEDIA_FIELD_TYPES = /* @__PURE__ */ new Set(["image", "video", "file"]);
47129
+ function nestedTargets(fields) {
47130
+ const out = [];
47131
+ for (const f of fields) {
47132
+ if (MEDIA_FIELD_TYPES.has(f.field_type)) continue;
47133
+ if (f.field_type !== "paragraph" && f.field_type !== "reference") continue;
47134
+ const ref = f.ui_component.ref;
47135
+ if (!ref) continue;
47136
+ out.push({ label: f.name, target: ref });
47137
+ }
47138
+ return out;
47139
+ }
47140
+ function buildProjectors(registry2, fieldKeyMaps) {
47141
+ const projectors = {};
47142
+ const sources = [
47143
+ registry2.content_types,
47144
+ registry2.taxonomy_types,
47145
+ registry2.paragraph_types
47146
+ ];
47147
+ for (const source of sources) {
47148
+ for (const [typeName, type] of Object.entries(source)) {
47149
+ const map2 = fieldKeyMaps[typeName];
47150
+ if (!map2) continue;
47151
+ projectors[typeName] = { map: map2, nested: nestedTargets(type.fields) };
47152
+ }
47153
+ }
47154
+ return projectors;
47155
+ }
47156
+ function projectRow(row, typeName, projectors) {
47157
+ const p2 = projectors[typeName];
47158
+ if (!p2) return row;
47159
+ const out = p2.map.toLabels(row);
47160
+ for (const { label, target } of p2.nested) {
47161
+ const v2 = out[label];
47162
+ if (v2 === null || v2 === void 0) continue;
47163
+ if (Array.isArray(v2)) {
47164
+ out[label] = v2.map((item) => isPlainRow(item) ? projectRow(item, target, projectors) : item);
47165
+ } else if (isPlainRow(v2)) {
47166
+ out[label] = projectRow(v2, target, projectors);
47167
+ }
47168
+ }
47169
+ return out;
47170
+ }
47106
47171
  var SORTABLE_FIELDS = /* @__PURE__ */ new Set(["title", "created_at", "updated_at"]);
47107
47172
  var RELATION_FIELD_TYPES = /* @__PURE__ */ new Set([
47108
47173
  "paragraph",
@@ -47122,28 +47187,31 @@ function parseInclude(includeParam) {
47122
47187
  if (!includeParam) return [];
47123
47188
  return includeParam.split(",").map((s) => s.trim()).filter(Boolean);
47124
47189
  }
47125
- function parseFilters(url2, validFields) {
47190
+ function parseFilters(url2, validFields, columnFor) {
47126
47191
  const { searchParams } = new URL(url2);
47127
47192
  const filters = {};
47193
+ const toKey = (label) => columnFor?.(label) ?? label;
47128
47194
  for (const [key, value] of searchParams.entries()) {
47129
47195
  const simpleMatch = /^filter\[([^\]]+)\]$/.exec(key);
47130
47196
  const opMatch = /^filter\[([^\]]+)\]\[([^\]]+)\]$/.exec(key);
47131
47197
  if (simpleMatch) {
47132
47198
  const field = simpleMatch[1];
47133
47199
  if (!validFields.has(field)) return { ok: false, invalidField: field };
47134
- const existing = filters[field];
47200
+ const storageKey = toKey(field);
47201
+ const existing = filters[storageKey];
47135
47202
  if (existing !== void 0) {
47136
- filters[field] = Array.isArray(existing) ? [...existing, value] : [existing, value];
47203
+ filters[storageKey] = Array.isArray(existing) ? [...existing, value] : [existing, value];
47137
47204
  } else {
47138
- filters[field] = value;
47205
+ filters[storageKey] = value;
47139
47206
  }
47140
47207
  } else if (opMatch) {
47141
47208
  const field = opMatch[1];
47142
47209
  const operator = opMatch[2];
47143
47210
  if (!validFields.has(field)) return { ok: false, invalidField: field };
47144
47211
  if (!["gt", "gte", "lt", "lte"].includes(operator)) continue;
47145
- const existing = filters[field] ?? {};
47146
- filters[field] = { ...existing, [operator]: value };
47212
+ const storageKey = toKey(field);
47213
+ const existing = filters[storageKey] ?? {};
47214
+ filters[storageKey] = { ...existing, [operator]: value };
47147
47215
  }
47148
47216
  }
47149
47217
  return { ok: true, filters };
@@ -47151,7 +47219,7 @@ function parseFilters(url2, validFields) {
47151
47219
  function isPublished(item) {
47152
47220
  return item["published"] === true;
47153
47221
  }
47154
- function registerPublicContentRoutes(app, registry2, repos, listRateLimit, resolver) {
47222
+ function registerPublicContentRoutes(app, registry2, repos, projectors, paths, listRateLimit, resolver) {
47155
47223
  function registerListRoute(path3, handler) {
47156
47224
  if (listRateLimit) {
47157
47225
  app.get(path3, listRateLimit, handler);
@@ -47159,7 +47227,7 @@ function registerPublicContentRoutes(app, registry2, repos, listRateLimit, resol
47159
47227
  app.get(path3, handler);
47160
47228
  }
47161
47229
  }
47162
- registerListRoute("/api/content", (c) => {
47230
+ registerListRoute(paths.collection("content"), (c) => {
47163
47231
  const data = Object.values(registry2.content_types).map((ct2) => ({
47164
47232
  name: ct2.name,
47165
47233
  label: ct2.label,
@@ -47167,7 +47235,7 @@ function registerPublicContentRoutes(app, registry2, repos, listRateLimit, resol
47167
47235
  }));
47168
47236
  return c.json({ ok: true, data });
47169
47237
  });
47170
- registerListRoute("/api/taxonomy", (c) => {
47238
+ registerListRoute(paths.collection("taxonomy"), (c) => {
47171
47239
  const data = Object.values(registry2.taxonomy_types).map((tt2) => ({
47172
47240
  name: tt2.name,
47173
47241
  label: tt2.label
@@ -47186,7 +47254,7 @@ function registerPublicContentRoutes(app, registry2, repos, listRateLimit, resol
47186
47254
  ...contentType.system_fields.map((f) => f.name)
47187
47255
  ]);
47188
47256
  if (contentType.only_one) {
47189
- app.get(`/api/${basePath}`, async (c) => {
47257
+ app.get(paths.collection(basePath), async (c) => {
47190
47258
  const result = await repo.findMany({ published_only: true, page: 1, per_page: 1 });
47191
47259
  if (result.data.length === 0) {
47192
47260
  return c.json(
@@ -47194,12 +47262,14 @@ function registerPublicContentRoutes(app, registry2, repos, listRateLimit, resol
47194
47262
  404
47195
47263
  );
47196
47264
  }
47197
- let data = result.data[0];
47265
+ let data = projectRow(result.data[0], typeName, projectors);
47198
47266
  if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
47199
47267
  return c.json({ ok: true, data });
47200
47268
  });
47201
47269
  } else {
47202
- registerListRoute(`/api/${basePath}`, async (c) => {
47270
+ registerListRoute(paths.collection(basePath), async (c) => {
47271
+ const projector = projectors[typeName];
47272
+ const fieldKeys = projector.map;
47203
47273
  const pagination = parsePagination(c.req.query("page"), c.req.query("per_page"));
47204
47274
  if (!pagination.ok) {
47205
47275
  return c.json(
@@ -47239,7 +47309,7 @@ function registerPublicContentRoutes(app, registry2, repos, listRateLimit, resol
47239
47309
  400
47240
47310
  );
47241
47311
  }
47242
- const filtersResult = parseFilters(c.req.url, filterableFieldNames);
47312
+ const filtersResult = parseFilters(c.req.url, filterableFieldNames, fieldKeys.columnFor);
47243
47313
  if (!filtersResult.ok) {
47244
47314
  return c.json(
47245
47315
  {
@@ -47267,22 +47337,23 @@ function registerPublicContentRoutes(app, registry2, repos, listRateLimit, resol
47267
47337
  );
47268
47338
  }
47269
47339
  }
47340
+ const sortColumn = fieldKeys.columnFor(sortBy) ?? sortBy;
47270
47341
  const result = await repo.findMany({
47271
47342
  published_only: true,
47272
47343
  page: pagination.page,
47273
47344
  per_page: pagination.per_page,
47274
- sort_by: sortBy,
47345
+ sort_by: sortColumn,
47275
47346
  sort_order: sortOrder,
47276
47347
  filters: filtersResult.filters,
47277
47348
  include
47278
47349
  });
47279
- if (resolver?.hasSchema(typeName)) {
47280
- const resolved = await resolver.resolveList(typeName, result.data);
47281
- return c.json({ ...result, data: resolved });
47282
- }
47283
- return c.json(result);
47350
+ const labeled = result.data.map(
47351
+ (row) => projectRow(row, typeName, projectors)
47352
+ );
47353
+ const data = resolver?.hasSchema(typeName) ? await resolver.resolveList(typeName, labeled) : labeled;
47354
+ return c.json({ ...result, data });
47284
47355
  });
47285
- app.get(`/api/${basePath}/:slug`, async (c) => {
47356
+ app.get(paths.item(basePath), async (c) => {
47286
47357
  const slug = c.req.param("slug");
47287
47358
  const include = parseInclude(c.req.query("include"));
47288
47359
  for (const field of include) {
@@ -47309,7 +47380,7 @@ function registerPublicContentRoutes(app, registry2, repos, listRateLimit, resol
47309
47380
  404
47310
47381
  );
47311
47382
  }
47312
- let data = item;
47383
+ let data = projectRow(item, typeName, projectors);
47313
47384
  if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
47314
47385
  return c.json({ ok: true, data });
47315
47386
  });
@@ -47318,7 +47389,7 @@ function registerPublicContentRoutes(app, registry2, repos, listRateLimit, resol
47318
47389
  for (const [typeName] of Object.entries(registry2.taxonomy_types)) {
47319
47390
  const repo = repos[typeName];
47320
47391
  if (!repo) continue;
47321
- registerListRoute(`/api/taxonomy/${typeName}`, async (c) => {
47392
+ registerListRoute(paths.taxonomyCollection(typeName), async (c) => {
47322
47393
  const pagination = parsePagination(c.req.query("page"), c.req.query("per_page"));
47323
47394
  if (!pagination.ok) {
47324
47395
  return c.json(
@@ -47337,13 +47408,13 @@ function registerPublicContentRoutes(app, registry2, repos, listRateLimit, resol
47337
47408
  page: pagination.page,
47338
47409
  per_page: pagination.per_page
47339
47410
  });
47340
- if (resolver?.hasSchema(typeName)) {
47341
- const resolved = await resolver.resolveList(typeName, result.data);
47342
- return c.json({ ...result, data: resolved });
47343
- }
47344
- return c.json(result);
47411
+ const labeled = result.data.map(
47412
+ (row) => projectRow(row, typeName, projectors)
47413
+ );
47414
+ const data = resolver?.hasSchema(typeName) ? await resolver.resolveList(typeName, labeled) : labeled;
47415
+ return c.json({ ...result, data });
47345
47416
  });
47346
- app.get(`/api/taxonomy/${typeName}/:id`, async (c) => {
47417
+ app.get(paths.taxonomyItem(typeName), async (c) => {
47347
47418
  const id = c.req.param("id");
47348
47419
  const item = await repo.findOne(id);
47349
47420
  if (!item || !isPublished(item)) {
@@ -47355,7 +47426,7 @@ function registerPublicContentRoutes(app, registry2, repos, listRateLimit, resol
47355
47426
  404
47356
47427
  );
47357
47428
  }
47358
- let data = item;
47429
+ let data = projectRow(item, typeName, projectors);
47359
47430
  if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
47360
47431
  return c.json({ ok: true, data });
47361
47432
  });
@@ -47369,7 +47440,7 @@ function parsePagination2(pageStr, perPageStr) {
47369
47440
  if (!Number.isInteger(per_page) || per_page < 1 || per_page > 100) return { ok: false };
47370
47441
  return { ok: true, page, per_page };
47371
47442
  }
47372
- function registerPublicMediaRoutes(app, mediaRepo, listRateLimit) {
47443
+ function registerPublicMediaRoutes(app, mediaRepo, paths, listRateLimit) {
47373
47444
  const mediaListHandler = async (c) => {
47374
47445
  const pagination = parsePagination2(c.req.query("page"), c.req.query("per_page"));
47375
47446
  if (!pagination.ok) {
@@ -47408,11 +47479,11 @@ function registerPublicMediaRoutes(app, mediaRepo, listRateLimit) {
47408
47479
  return c.json(result);
47409
47480
  };
47410
47481
  if (listRateLimit) {
47411
- app.get("/api/media", listRateLimit, mediaListHandler);
47482
+ app.get(paths.mediaCollection(), listRateLimit, mediaListHandler);
47412
47483
  } else {
47413
- app.get("/api/media", mediaListHandler);
47484
+ app.get(paths.mediaCollection(), mediaListHandler);
47414
47485
  }
47415
- app.get("/api/media/:id", async (c) => {
47486
+ app.get(paths.mediaItem(), async (c) => {
47416
47487
  const id = c.req.param("id");
47417
47488
  const item = await mediaRepo.findOne(id);
47418
47489
  if (!item) {
@@ -47431,9 +47502,11 @@ function topLevelMediaDelta(mediaFields, before, after) {
47431
47502
  const added = [];
47432
47503
  const removed = [];
47433
47504
  for (const f of mediaFields) {
47434
- if (after !== null && !(f.name in after)) continue;
47435
- const oldId = before ? extractMediaId(before[f.name]) : null;
47436
- const newId = after ? extractMediaId(after[f.name]) : null;
47505
+ const key = f.db_column?.column_name;
47506
+ if (key === void 0 || key === "") continue;
47507
+ if (after !== null && !(key in after)) continue;
47508
+ const oldId = before ? extractMediaId(before[key]) : null;
47509
+ const newId = after ? extractMediaId(after[key]) : null;
47437
47510
  if (newId === oldId) continue;
47438
47511
  if (oldId) removed.push(oldId);
47439
47512
  if (newId) added.push(newId);
@@ -47491,7 +47564,7 @@ function isEmpty(value) {
47491
47564
  function checkRequiredFields(fields, data) {
47492
47565
  return fields.filter((f) => f.required && isEmpty(data[f.name])).map((f) => ({ field: f.name, message: `${f.label} is required` }));
47493
47566
  }
47494
- function parseListQuery(url2, schemaFieldNames, relationFieldNames) {
47567
+ function parseListQuery(url2, schemaFieldNames, relationFieldNames, columnFor) {
47495
47568
  const searchParams = new URL(url2).searchParams;
47496
47569
  const pagination = parsePagination(
47497
47570
  searchParams.get("page") ?? void 0,
@@ -47526,7 +47599,7 @@ function parseListQuery(url2, schemaFieldNames, relationFieldNames) {
47526
47599
  status: 400
47527
47600
  };
47528
47601
  }
47529
- const filtersResult = parseFilters(url2, schemaFieldNames);
47602
+ const filtersResult = parseFilters(url2, schemaFieldNames, columnFor);
47530
47603
  if (!filtersResult.ok) {
47531
47604
  return {
47532
47605
  ok: false,
@@ -47558,7 +47631,7 @@ function parseListQuery(url2, schemaFieldNames, relationFieldNames) {
47558
47631
  search
47559
47632
  };
47560
47633
  }
47561
- function registerAdminContentRoutes(app, registry2, repos, mediaRepo, requirePermission, db) {
47634
+ function registerAdminContentRoutes(app, registry2, repos, projectors, mediaRepo, requirePermission, db) {
47562
47635
  for (const [typeName, contentType] of Object.entries(registry2.content_types)) {
47563
47636
  const basePath = `content/${typeName}`;
47564
47637
  const repo = repos[typeName];
@@ -47570,6 +47643,8 @@ function registerAdminContentRoutes(app, registry2, repos, mediaRepo, requirePer
47570
47643
  const relationFieldNames = new Set(
47571
47644
  contentType.fields.filter((f) => RELATION_FIELD_TYPES.has(f.field_type)).map((f) => f.name)
47572
47645
  );
47646
+ const projector = projectors[typeName];
47647
+ const fieldKeys = projector.map;
47573
47648
  const requiredFields = contentType.fields.filter((f) => f.required);
47574
47649
  const mediaFields = contentType.fields.filter(
47575
47650
  (f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file"
@@ -47583,17 +47658,18 @@ function registerAdminContentRoutes(app, registry2, repos, mediaRepo, requirePer
47583
47658
  `/admin/api/${basePath}`,
47584
47659
  requirePermission("content:read"),
47585
47660
  async (c) => {
47586
- const parsed = parseListQuery(c.req.url, schemaFieldNames, relationFieldNames);
47661
+ const parsed = parseListQuery(c.req.url, schemaFieldNames, relationFieldNames, fieldKeys.columnFor);
47587
47662
  if (!parsed.ok) {
47588
47663
  return c.json({ ok: false, error: parsed.response }, parsed.status);
47589
47664
  }
47590
47665
  const publishedParam = c.req.query("published");
47591
47666
  const extraFilters = {};
47592
47667
  if (publishedParam === "false") extraFilters["published"] = false;
47668
+ const sortColumn = fieldKeys.columnFor(parsed.sortBy) ?? parsed.sortBy;
47593
47669
  const findOpts = {
47594
47670
  page: parsed.pagination.page,
47595
47671
  per_page: parsed.pagination.per_page,
47596
- sort_by: parsed.sortBy,
47672
+ sort_by: sortColumn,
47597
47673
  sort_order: parsed.sortOrder,
47598
47674
  filters: { ...parsed.filters, ...extraFilters },
47599
47675
  include: parsed.include
@@ -47603,7 +47679,10 @@ function registerAdminContentRoutes(app, registry2, repos, mediaRepo, requirePer
47603
47679
  findOpts.search = { term: parsed.search, columns: searchableColumns };
47604
47680
  }
47605
47681
  const result = await repo.findMany(findOpts);
47606
- return c.json(result);
47682
+ const data = result.data.map(
47683
+ (row) => projectRow(row, typeName, projectors)
47684
+ );
47685
+ return c.json({ ...result, data });
47607
47686
  }
47608
47687
  );
47609
47688
  app.get(
@@ -47638,7 +47717,8 @@ function registerAdminContentRoutes(app, registry2, repos, mediaRepo, requirePer
47638
47717
  }
47639
47718
  }
47640
47719
  }
47641
- return c.json({ ok: true, data: item });
47720
+ const data = projectRow(item, typeName, projectors);
47721
+ return c.json({ ok: true, data });
47642
47722
  }
47643
47723
  );
47644
47724
  app.post(
@@ -47705,6 +47785,7 @@ function registerAdminContentRoutes(app, registry2, repos, mediaRepo, requirePer
47705
47785
  }
47706
47786
  );
47707
47787
  const writeNewItem = async (c, body) => {
47788
+ const storageBody = fieldKeys.toStorage(body);
47708
47789
  const fieldErrors = checkRequiredFields(requiredFields, body);
47709
47790
  if (fieldErrors.length > 0) {
47710
47791
  return c.json(
@@ -47728,9 +47809,7 @@ function registerAdminContentRoutes(app, registry2, repos, mediaRepo, requirePer
47728
47809
  const junctionFields = contentType.fields.filter(
47729
47810
  (f) => f.db_column !== null && f.db_column.junction !== void 0
47730
47811
  );
47731
- const columnFields = contentType.fields.filter(
47732
- (f) => f.db_column !== null && f.db_column.junction === void 0
47733
- );
47812
+ const columnBackedFields = contentType.fields.filter(isColumnBacked);
47734
47813
  const columnData = {
47735
47814
  published: body["published"] ?? false
47736
47815
  };
@@ -47739,9 +47818,10 @@ function registerAdminContentRoutes(app, registry2, repos, mediaRepo, requirePer
47739
47818
  } else {
47740
47819
  columnData["slug"] = body["slug"];
47741
47820
  }
47742
- for (const f of columnFields) {
47743
- const val = body[f.name];
47744
- columnData[f.db_column.column_name] = val !== void 0 ? val : null;
47821
+ for (const f of columnBackedFields) {
47822
+ const key = f.db_column.column_name;
47823
+ if (!(key in storageBody)) continue;
47824
+ columnData[key] = storageBody[key];
47745
47825
  }
47746
47826
  if (db) {
47747
47827
  const basePathId = await lookupBasePathId(db, contentType.default_base_path);
@@ -47761,7 +47841,7 @@ function registerAdminContentRoutes(app, registry2, repos, mediaRepo, requirePer
47761
47841
  }
47762
47842
  const item = await repo.create(columnData);
47763
47843
  const itemId = item["id"];
47764
- const mediaDeltas = [topLevelMediaDelta(mediaFields, null, body)];
47844
+ const mediaDeltas = [topLevelMediaDelta(mediaFields, null, storageBody)];
47765
47845
  if (db) {
47766
47846
  for (const f of paragraphFields) {
47767
47847
  const comp = f.ui_component;
@@ -47778,7 +47858,8 @@ function registerAdminContentRoutes(app, registry2, repos, mediaRepo, requirePer
47778
47858
  }
47779
47859
  }
47780
47860
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
47781
- return c.json({ ok: true, data: item }, 201);
47861
+ const data = projectRow(item, typeName, projectors);
47862
+ return c.json({ ok: true, data }, 201);
47782
47863
  };
47783
47864
  app.patch(
47784
47865
  `/admin/api/${basePath}/:id`,
@@ -47838,11 +47919,12 @@ function registerAdminContentRoutes(app, registry2, repos, mediaRepo, requirePer
47838
47919
  }
47839
47920
  );
47840
47921
  const writeExistingItem = async (c, id, existing, body) => {
47922
+ const storageBody = fieldKeys.toStorage(body);
47841
47923
  if (body["published"] === true) {
47842
47924
  const publishDeny = await requirePermission("content:edit")(c, async () => {
47843
47925
  });
47844
47926
  if (publishDeny) return publishDeny;
47845
- const merged = { ...existing, ...body };
47927
+ const merged = { ...fieldKeys.toLabels(existing), ...body };
47846
47928
  const fieldErrors = checkRequiredFields(requiredFields, merged);
47847
47929
  if (fieldErrors.length > 0) {
47848
47930
  return c.json(
@@ -47862,17 +47944,14 @@ function registerAdminContentRoutes(app, registry2, repos, mediaRepo, requirePer
47862
47944
  const patchJunctionFields = contentType.fields.filter(
47863
47945
  (f) => f.db_column !== null && f.db_column.junction !== void 0
47864
47946
  );
47865
- const patchColumnFields = contentType.fields.filter(
47866
- (f) => f.db_column !== null && f.db_column.junction === void 0
47867
- );
47947
+ const patchColumnBackedFields = contentType.fields.filter(isColumnBacked);
47868
47948
  const patchData = {};
47869
47949
  if (!contentType.only_one && "slug" in body) patchData["slug"] = body["slug"];
47870
47950
  if ("published" in body) patchData["published"] = body["published"];
47871
- for (const f of patchColumnFields) {
47872
- if (f.name in body) {
47873
- const val = body[f.name];
47874
- patchData[f.db_column.column_name] = val !== void 0 ? val : null;
47875
- }
47951
+ for (const f of patchColumnBackedFields) {
47952
+ const key = f.db_column.column_name;
47953
+ if (!(key in storageBody)) continue;
47954
+ patchData[key] = storageBody[key];
47876
47955
  }
47877
47956
  const updated = await repo.update(id, patchData);
47878
47957
  if (!updated) {
@@ -47882,7 +47961,7 @@ function registerAdminContentRoutes(app, registry2, repos, mediaRepo, requirePer
47882
47961
  );
47883
47962
  }
47884
47963
  const mediaDeltas = [
47885
- topLevelMediaDelta(mediaFields, existing, body)
47964
+ topLevelMediaDelta(mediaFields, existing, storageBody)
47886
47965
  ];
47887
47966
  if (db) {
47888
47967
  for (const f of patchParagraphFields) {
@@ -47900,7 +47979,8 @@ function registerAdminContentRoutes(app, registry2, repos, mediaRepo, requirePer
47900
47979
  }
47901
47980
  }
47902
47981
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
47903
- return c.json({ ok: true, data: updated });
47982
+ const data = projectRow(updated, typeName, projectors);
47983
+ return c.json({ ok: true, data });
47904
47984
  };
47905
47985
  if (contentType.only_one) {
47906
47986
  app.put(
@@ -47960,6 +48040,8 @@ function registerAdminContentRoutes(app, registry2, repos, mediaRepo, requirePer
47960
48040
  const relationFieldNames = new Set(
47961
48041
  taxonomyType.fields.filter((f) => RELATION_FIELD_TYPES.has(f.field_type)).map((f) => f.name)
47962
48042
  );
48043
+ const projector = projectors[typeName];
48044
+ const fieldKeys = projector.map;
47963
48045
  const requiredFields = taxonomyType.fields.filter((f) => f.required);
47964
48046
  const mediaFields = taxonomyType.fields.filter(
47965
48047
  (f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file"
@@ -47973,17 +48055,18 @@ function registerAdminContentRoutes(app, registry2, repos, mediaRepo, requirePer
47973
48055
  `/admin/api/taxonomy/${typeName}`,
47974
48056
  requirePermission("content:read"),
47975
48057
  async (c) => {
47976
- const parsed = parseListQuery(c.req.url, schemaFieldNames, relationFieldNames);
48058
+ const parsed = parseListQuery(c.req.url, schemaFieldNames, relationFieldNames, fieldKeys.columnFor);
47977
48059
  if (!parsed.ok) {
47978
48060
  return c.json({ ok: false, error: parsed.response }, parsed.status);
47979
48061
  }
47980
48062
  const publishedParam = c.req.query("published");
47981
48063
  const extraFilters = {};
47982
48064
  if (publishedParam === "false") extraFilters["published"] = false;
48065
+ const sortColumn = fieldKeys.columnFor(parsed.sortBy) ?? parsed.sortBy;
47983
48066
  const findOpts = {
47984
48067
  page: parsed.pagination.page,
47985
48068
  per_page: parsed.pagination.per_page,
47986
- sort_by: parsed.sortBy,
48069
+ sort_by: sortColumn,
47987
48070
  sort_order: parsed.sortOrder,
47988
48071
  filters: { ...parsed.filters, ...extraFilters },
47989
48072
  include: parsed.include
@@ -47993,7 +48076,10 @@ function registerAdminContentRoutes(app, registry2, repos, mediaRepo, requirePer
47993
48076
  findOpts.search = { term: parsed.search, columns: searchableColumns };
47994
48077
  }
47995
48078
  const result = await repo.findMany(findOpts);
47996
- return c.json(result);
48079
+ const data = result.data.map(
48080
+ (row) => projectRow(row, typeName, projectors)
48081
+ );
48082
+ return c.json({ ...result, data });
47997
48083
  }
47998
48084
  );
47999
48085
  app.get(
@@ -48008,7 +48094,8 @@ function registerAdminContentRoutes(app, registry2, repos, mediaRepo, requirePer
48008
48094
  404
48009
48095
  );
48010
48096
  }
48011
- return c.json({ ok: true, data: item });
48097
+ const data = projectRow(item, typeName, projectors);
48098
+ return c.json({ ok: true, data });
48012
48099
  }
48013
48100
  );
48014
48101
  app.post(
@@ -48016,6 +48103,7 @@ function registerAdminContentRoutes(app, registry2, repos, mediaRepo, requirePer
48016
48103
  requirePermission("content:create"),
48017
48104
  async (c) => {
48018
48105
  const body = await c.req.json();
48106
+ const storageBody = fieldKeys.toStorage(body);
48019
48107
  const fieldErrors = checkRequiredFields(requiredFields, body);
48020
48108
  if (fieldErrors.length > 0) {
48021
48109
  return c.json(
@@ -48035,20 +48123,19 @@ function registerAdminContentRoutes(app, registry2, repos, mediaRepo, requirePer
48035
48123
  });
48036
48124
  if (publishDeny) return publishDeny;
48037
48125
  }
48038
- const taxColumnFields = taxonomyType.fields.filter(
48039
- (f) => f.db_column !== null && f.db_column.junction === void 0
48040
- );
48041
48126
  const taxParagraphFields = taxonomyType.fields.filter((f) => f.db_column === null);
48127
+ const taxColumnBackedFields = taxonomyType.fields.filter(isColumnBacked);
48042
48128
  const taxColumnData = {
48043
48129
  published: body["published"] ?? false
48044
48130
  };
48045
- for (const f of taxColumnFields) {
48046
- const val = body[f.name];
48047
- taxColumnData[f.db_column.column_name] = val !== void 0 ? val : null;
48131
+ for (const f of taxColumnBackedFields) {
48132
+ const key = f.db_column.column_name;
48133
+ if (!(key in storageBody)) continue;
48134
+ taxColumnData[key] = storageBody[key];
48048
48135
  }
48049
48136
  const item = await repo.create(taxColumnData);
48050
48137
  const taxItemId = item["id"];
48051
- const mediaDeltas = [topLevelMediaDelta(mediaFields, null, body)];
48138
+ const mediaDeltas = [topLevelMediaDelta(mediaFields, null, storageBody)];
48052
48139
  if (db) {
48053
48140
  for (const f of taxParagraphFields) {
48054
48141
  const comp = f.ui_component;
@@ -48060,7 +48147,8 @@ function registerAdminContentRoutes(app, registry2, repos, mediaRepo, requirePer
48060
48147
  }
48061
48148
  }
48062
48149
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
48063
- return c.json({ ok: true, data: item }, 201);
48150
+ const data = projectRow(item, typeName, projectors);
48151
+ return c.json({ ok: true, data }, 201);
48064
48152
  }
48065
48153
  );
48066
48154
  app.patch(
@@ -48069,6 +48157,7 @@ function registerAdminContentRoutes(app, registry2, repos, mediaRepo, requirePer
48069
48157
  async (c) => {
48070
48158
  const id = c.req.param("id");
48071
48159
  const body = await c.req.json();
48160
+ const storageBody = fieldKeys.toStorage(body);
48072
48161
  const existing = await repo.findOne(id);
48073
48162
  if (!existing) {
48074
48163
  return c.json(
@@ -48080,7 +48169,7 @@ function registerAdminContentRoutes(app, registry2, repos, mediaRepo, requirePer
48080
48169
  const publishDeny = await requirePermission("content:edit")(c, async () => {
48081
48170
  });
48082
48171
  if (publishDeny) return publishDeny;
48083
- const merged = { ...existing, ...body };
48172
+ const merged = { ...fieldKeys.toLabels(existing), ...body };
48084
48173
  const fieldErrors = checkRequiredFields(requiredFields, merged);
48085
48174
  if (fieldErrors.length > 0) {
48086
48175
  return c.json(
@@ -48096,17 +48185,14 @@ function registerAdminContentRoutes(app, registry2, repos, mediaRepo, requirePer
48096
48185
  );
48097
48186
  }
48098
48187
  }
48099
- const taxPatchColumnFields = taxonomyType.fields.filter(
48100
- (f) => f.db_column !== null && f.db_column.junction === void 0
48101
- );
48102
48188
  const taxPatchParagraphFields = taxonomyType.fields.filter((f) => f.db_column === null);
48189
+ const taxPatchColumnBackedFields = taxonomyType.fields.filter(isColumnBacked);
48103
48190
  const taxPatchData = {};
48104
48191
  if ("published" in body) taxPatchData["published"] = body["published"];
48105
- for (const f of taxPatchColumnFields) {
48106
- if (f.name in body) {
48107
- const val = body[f.name];
48108
- taxPatchData[f.db_column.column_name] = val !== void 0 ? val : null;
48109
- }
48192
+ for (const f of taxPatchColumnBackedFields) {
48193
+ const key = f.db_column.column_name;
48194
+ if (!(key in storageBody)) continue;
48195
+ taxPatchData[key] = storageBody[key];
48110
48196
  }
48111
48197
  const updated = await repo.update(id, taxPatchData);
48112
48198
  if (!updated) {
@@ -48116,7 +48202,7 @@ function registerAdminContentRoutes(app, registry2, repos, mediaRepo, requirePer
48116
48202
  );
48117
48203
  }
48118
48204
  const mediaDeltas = [
48119
- topLevelMediaDelta(mediaFields, existing, body)
48205
+ topLevelMediaDelta(mediaFields, existing, storageBody)
48120
48206
  ];
48121
48207
  if (db) {
48122
48208
  for (const f of taxPatchParagraphFields) {
@@ -48129,7 +48215,8 @@ function registerAdminContentRoutes(app, registry2, repos, mediaRepo, requirePer
48129
48215
  }
48130
48216
  }
48131
48217
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
48132
- return c.json({ ok: true, data: updated });
48218
+ const data = projectRow(updated, typeName, projectors);
48219
+ return c.json({ ok: true, data });
48133
48220
  }
48134
48221
  );
48135
48222
  app.delete(
@@ -49102,7 +49189,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
49102
49189
  }
49103
49190
  var API_VERSION = (() => {
49104
49191
  try {
49105
- return "0.3.3";
49192
+ return "0.4.1";
49106
49193
  } catch {
49107
49194
  return "0.0.0";
49108
49195
  }
@@ -49257,7 +49344,7 @@ function whereFragment(conds) {
49257
49344
  return conds.length > 0 ? sql` WHERE ${sql.join(conds, sql` AND `)}` : sql``;
49258
49345
  }
49259
49346
  function createDrizzleContentRepository(db, tableName, options = {}) {
49260
- const { relations = {}, publishedRelations = false } = options;
49347
+ const { relations = {}, publishedRelations = false, sortableColumns } = options;
49261
49348
  function tableRaw() {
49262
49349
  return sql.raw(quoteIdent2(tableName));
49263
49350
  }
@@ -49295,8 +49382,9 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
49295
49382
  sort_order = "asc",
49296
49383
  search
49297
49384
  } = opts;
49298
- if (!SORTABLE_FIELDS2.has(sort_by)) {
49299
- throw codeError("INVALID_SORT_FIELD", `'${sort_by}' is not sortable. Allowed: title, created_at, updated_at`);
49385
+ const allowed = sortableColumns ?? SORTABLE_FIELDS2;
49386
+ if (!allowed.has(sort_by)) {
49387
+ throw codeError("INVALID_SORT_FIELD", `'${sort_by}' is not sortable.`);
49300
49388
  }
49301
49389
  const offset = (page - 1) * per_page;
49302
49390
  const conds = buildConditions(published_only, filters, search);
@@ -49500,7 +49588,8 @@ function createCmsApp(options) {
49500
49588
  if (!options.storage) {
49501
49589
  throw new Error(MISSING_STORAGE_ERROR);
49502
49590
  }
49503
- const prefix = options.prefix ?? "/api";
49591
+ const prefix = normalizePrefix(options.prefix);
49592
+ const publicPaths = createPublicPaths(prefix);
49504
49593
  const { storage, registry: registry2, db, rateLimit, media, cors } = options;
49505
49594
  const cmsName = options.name ?? "Manguito CMS";
49506
49595
  const maxFileSize = media?.max_file_size;
@@ -49534,16 +49623,38 @@ function createCmsApp(options) {
49534
49623
  return result.rows[0]?.role ?? null;
49535
49624
  };
49536
49625
  const requireHierarchy = createHierarchyMiddleware(rolesRegistry, getUserRole);
49626
+ const fieldKeyMaps = Object.fromEntries([
49627
+ ...Object.entries(registry2.content_types).map(([typeName, ct2]) => [
49628
+ typeName,
49629
+ createFieldKeyMap(ct2.fields)
49630
+ ]),
49631
+ ...Object.entries(registry2.taxonomy_types).map(([typeName, tt2]) => [
49632
+ typeName,
49633
+ createFieldKeyMap(tt2.fields)
49634
+ ]),
49635
+ ...Object.entries(registry2.paragraph_types).map(([typeName, pt]) => [
49636
+ typeName,
49637
+ createFieldKeyMap(pt.fields)
49638
+ ])
49639
+ ]);
49640
+ const projectors = buildProjectors(registry2, fieldKeyMaps);
49641
+ const sortableColumnsFor = (typeName) => new Set(
49642
+ [...SORTABLE_FIELDS].map((label) => fieldKeyMaps[typeName].columnFor(label) ?? label)
49643
+ );
49537
49644
  const contentRepos = Object.fromEntries(
49538
49645
  Object.entries(registry2.content_types).map(([typeName, ct2]) => [
49539
49646
  typeName,
49540
- createDrizzleContentRepository(db, ct2.db.table_name)
49647
+ createDrizzleContentRepository(db, ct2.db.table_name, {
49648
+ sortableColumns: sortableColumnsFor(typeName)
49649
+ })
49541
49650
  ])
49542
49651
  );
49543
49652
  const taxonomyRepos = Object.fromEntries(
49544
49653
  Object.entries(registry2.taxonomy_types).map(([typeName, tt2]) => [
49545
49654
  typeName,
49546
- createDrizzleContentRepository(db, tt2.db.table_name)
49655
+ createDrizzleContentRepository(db, tt2.db.table_name, {
49656
+ sortableColumns: sortableColumnsFor(typeName)
49657
+ })
49547
49658
  ])
49548
49659
  );
49549
49660
  const repos = { ...contentRepos, ...taxonomyRepos };
@@ -49553,7 +49664,8 @@ function createCmsApp(options) {
49553
49664
  typeName,
49554
49665
  createDrizzleContentRepository(db, ct2.db.table_name, {
49555
49666
  relations: buildRelationsMap(ct2.fields, registry2),
49556
- publishedRelations: true
49667
+ publishedRelations: true,
49668
+ sortableColumns: sortableColumnsFor(typeName)
49557
49669
  })
49558
49670
  ])
49559
49671
  );
@@ -49562,7 +49674,8 @@ function createCmsApp(options) {
49562
49674
  typeName,
49563
49675
  createDrizzleContentRepository(db, tt2.db.table_name, {
49564
49676
  relations: buildRelationsMap(tt2.fields, registry2),
49565
- publishedRelations: true
49677
+ publishedRelations: true,
49678
+ sortableColumns: sortableColumnsFor(typeName)
49566
49679
  })
49567
49680
  ])
49568
49681
  );
@@ -49570,33 +49683,41 @@ function createCmsApp(options) {
49570
49683
  const graphqlRepos = Object.fromEntries([
49571
49684
  ...Object.entries(registry2.content_types).map(([typeName, ct2]) => [
49572
49685
  typeName,
49573
- createDrizzleContentRepository(db, ct2.db.table_name)
49686
+ createDrizzleContentRepository(db, ct2.db.table_name, {
49687
+ sortableColumns: sortableColumnsFor(typeName)
49688
+ })
49574
49689
  ]),
49575
49690
  ...Object.entries(registry2.taxonomy_types).map(([typeName, tt2]) => [
49576
49691
  typeName,
49577
- createDrizzleContentRepository(db, tt2.db.table_name)
49692
+ createDrizzleContentRepository(db, tt2.db.table_name, {
49693
+ sortableColumns: sortableColumnsFor(typeName)
49694
+ })
49578
49695
  ])
49579
49696
  ]);
49580
49697
  registerAuthRoutes(app, db);
49581
49698
  app.use("/admin/api/*", authMiddleware);
49582
49699
  app.use("/admin/api/*", mustChangePasswordCheck);
49583
- app.get("/api/openapi.json", (c) => {
49700
+ app.get(publicPaths.openapi(), (c) => {
49584
49701
  const paths = {};
49585
49702
  for (const ct2 of Object.values(registry2.content_types)) {
49586
49703
  const base = ct2.default_base_path;
49587
49704
  if (ct2.only_one) {
49588
- paths[`/api/${base}`] = { get: { summary: `Get ${ct2.label}`, tags: [ct2.label] } };
49705
+ paths[publicPaths.collection(base)] = { get: { summary: `Get ${ct2.label}`, tags: [ct2.label] } };
49589
49706
  } else {
49590
- paths[`/api/${base}`] = { get: { summary: `List published ${ct2.label}`, tags: [ct2.label] } };
49591
- paths[`/api/${base}/{slug}`] = { get: { summary: `Get ${ct2.label} by slug`, tags: [ct2.label] } };
49707
+ paths[publicPaths.collection(base)] = { get: { summary: `List published ${ct2.label}`, tags: [ct2.label] } };
49708
+ paths[publicPaths.item(base).replace(":slug", "{slug}")] = {
49709
+ get: { summary: `Get ${ct2.label} by slug`, tags: [ct2.label] }
49710
+ };
49592
49711
  }
49593
49712
  }
49594
49713
  for (const tt2 of Object.values(registry2.taxonomy_types)) {
49595
- paths[`/api/taxonomy/${tt2.name}`] = { get: { summary: `List published ${tt2.label}`, tags: [tt2.label] } };
49596
- paths[`/api/taxonomy/${tt2.name}/{id}`] = { get: { summary: `Get ${tt2.label} by id`, tags: [tt2.label] } };
49714
+ paths[publicPaths.taxonomyCollection(tt2.name)] = { get: { summary: `List published ${tt2.label}`, tags: [tt2.label] } };
49715
+ paths[publicPaths.taxonomyItem(tt2.name).replace(":id", "{id}")] = {
49716
+ get: { summary: `Get ${tt2.label} by id`, tags: [tt2.label] }
49717
+ };
49597
49718
  }
49598
- paths["/api/media"] = { get: { summary: "List media items", tags: ["Media"] } };
49599
- paths["/api/media/{id}"] = { get: { summary: "Get media item by id", tags: ["Media"] } };
49719
+ paths[publicPaths.mediaCollection()] = { get: { summary: "List media items", tags: ["Media"] } };
49720
+ paths[publicPaths.mediaItem().replace(":id", "{id}")] = { get: { summary: "Get media item by id", tags: ["Media"] } };
49600
49721
  return c.json({
49601
49722
  openapi: "3.0.3",
49602
49723
  info: { title: "Manguito CMS Public API", version: "1.0.0" },
@@ -49611,14 +49732,14 @@ function createCmsApp(options) {
49611
49732
  paths: {}
49612
49733
  })
49613
49734
  );
49614
- registerPublicContentRoutes(app, registry2, publicRepos, listRateLimit, programmaticResolver);
49615
- registerPublicMediaRoutes(app, mediaRepo, listRateLimit);
49735
+ registerPublicContentRoutes(app, registry2, publicRepos, projectors, publicPaths, listRateLimit, programmaticResolver);
49736
+ registerPublicMediaRoutes(app, mediaRepo, publicPaths, listRateLimit);
49616
49737
  if (options.graphql?.enabled) {
49617
49738
  const gqlOptions = options.graphql;
49618
49739
  let gqlHandler = null;
49619
49740
  let initError = null;
49620
- const ready = import("./handler-CGHZG3J5-TCG6K3FE.js").then(({ createGraphQLHandler }) => {
49621
- gqlHandler = createGraphQLHandler(registry2, graphqlRepos, programmaticResolver, db, gqlOptions);
49741
+ const ready = import("./handler-7LVYEU5C-ZAJVEGKT.js").then(({ createGraphQLHandler }) => {
49742
+ gqlHandler = createGraphQLHandler(registry2, graphqlRepos, fieldKeyMaps, programmaticResolver, db, gqlOptions);
49622
49743
  }).catch((err) => {
49623
49744
  initError = err;
49624
49745
  const message = err instanceof Error ? err.message : String(err);
@@ -49658,7 +49779,7 @@ function createCmsApp(options) {
49658
49779
  );
49659
49780
  registerSchemaRoute(app, registry2, db);
49660
49781
  registerUserRoutes(app, db, requirePermission, requireHierarchy);
49661
- registerAdminContentRoutes(app, registry2, repos, mediaRepo, requirePermission, db);
49782
+ registerAdminContentRoutes(app, registry2, repos, projectors, mediaRepo, requirePermission, db);
49662
49783
  registerAdminMediaRoutes(app, mediaRepo, storage, requirePermission, maxFileSize);
49663
49784
  return { prefix, app };
49664
49785
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobbykim/manguito-cms-cli",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "license": "MIT",
5
5
  "author": "Bobby Kim <bobby.sihun.kim@gmail.com>",
6
6
  "homepage": "https://github.com/bobbykim89/manguito-cms",
@@ -32,7 +32,7 @@
32
32
  "@bobbykim/manguito-cms-admin": "0.4.0",
33
33
  "@bobbykim/manguito-cms-core": "0.3.0",
34
34
  "@bobbykim/manguito-cms-db": "0.1.3",
35
- "@bobbykim/manguito-cms-api": "0.3.3"
35
+ "@bobbykim/manguito-cms-api": "0.4.1"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^25.9.5",