@bobbykim/manguito-cms-api 0.4.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  persistParagraphField,
8
8
  resolveRelationBareIds,
9
9
  resolveRelationField
10
- } from "./chunk-4F2CRXPG.mjs";
10
+ } from "./chunk-4Y4G564P.mjs";
11
11
 
12
12
  // src/paths.ts
13
13
  function normalizePrefix(prefix) {
@@ -606,6 +606,54 @@ function createProgrammaticResolver(resolvers, options) {
606
606
  return { resolveItem, resolveList, hasSchema };
607
607
  }
608
608
 
609
+ // src/projector.ts
610
+ function isPlainRow(v) {
611
+ return typeof v === "object" && v !== null && !Array.isArray(v) && !(v instanceof Date);
612
+ }
613
+ var MEDIA_FIELD_TYPES = /* @__PURE__ */ new Set(["image", "video", "file"]);
614
+ function nestedTargets(fields) {
615
+ const out = [];
616
+ for (const f of fields) {
617
+ if (MEDIA_FIELD_TYPES.has(f.field_type)) continue;
618
+ if (f.field_type !== "paragraph" && f.field_type !== "reference") continue;
619
+ const ref = f.ui_component.ref;
620
+ if (!ref) continue;
621
+ out.push({ label: f.name, target: ref });
622
+ }
623
+ return out;
624
+ }
625
+ function buildProjectors(registry, fieldKeyMaps) {
626
+ const projectors = {};
627
+ const sources = [
628
+ registry.content_types,
629
+ registry.taxonomy_types,
630
+ registry.paragraph_types
631
+ ];
632
+ for (const source of sources) {
633
+ for (const [typeName, type] of Object.entries(source)) {
634
+ const map = fieldKeyMaps[typeName];
635
+ if (!map) continue;
636
+ projectors[typeName] = { map, nested: nestedTargets(type.fields) };
637
+ }
638
+ }
639
+ return projectors;
640
+ }
641
+ function projectRow(row, typeName, projectors) {
642
+ const p = projectors[typeName];
643
+ if (!p) return row;
644
+ const out = p.map.toLabels(row);
645
+ for (const { label, target } of p.nested) {
646
+ const v = out[label];
647
+ if (v === null || v === void 0) continue;
648
+ if (Array.isArray(v)) {
649
+ out[label] = v.map((item) => isPlainRow(item) ? projectRow(item, target, projectors) : item);
650
+ } else if (isPlainRow(v)) {
651
+ out[label] = projectRow(v, target, projectors);
652
+ }
653
+ }
654
+ return out;
655
+ }
656
+
609
657
  // src/routes/query-params.ts
610
658
  var SORTABLE_FIELDS = /* @__PURE__ */ new Set(["title", "created_at", "updated_at"]);
611
659
  var RELATION_FIELD_TYPES = /* @__PURE__ */ new Set([
@@ -660,7 +708,7 @@ function parseFilters(url, validFields, columnFor) {
660
708
  function isPublished(item) {
661
709
  return item["published"] === true;
662
710
  }
663
- function registerPublicContentRoutes(app, registry, repos, fieldKeyMaps, paths, listRateLimit, resolver) {
711
+ function registerPublicContentRoutes(app, registry, repos, projectors, paths, listRateLimit, resolver) {
664
712
  function registerListRoute(path, handler) {
665
713
  if (listRateLimit) {
666
714
  app.get(path, listRateLimit, handler);
@@ -703,13 +751,14 @@ function registerPublicContentRoutes(app, registry, repos, fieldKeyMaps, paths,
703
751
  404
704
752
  );
705
753
  }
706
- let data = fieldKeyMaps[typeName].toLabels(result.data[0]);
754
+ let data = projectRow(result.data[0], typeName, projectors);
707
755
  if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
708
756
  return c.json({ ok: true, data });
709
757
  });
710
758
  } else {
711
759
  registerListRoute(paths.collection(basePath), async (c) => {
712
- const fieldKeys = fieldKeyMaps[typeName];
760
+ const projector = projectors[typeName];
761
+ const fieldKeys = projector.map;
713
762
  const pagination = parsePagination(c.req.query("page"), c.req.query("per_page"));
714
763
  if (!pagination.ok) {
715
764
  return c.json(
@@ -777,17 +826,18 @@ function registerPublicContentRoutes(app, registry, repos, fieldKeyMaps, paths,
777
826
  );
778
827
  }
779
828
  }
829
+ const sortColumn = fieldKeys.columnFor(sortBy) ?? sortBy;
780
830
  const result = await repo.findMany({
781
831
  published_only: true,
782
832
  page: pagination.page,
783
833
  per_page: pagination.per_page,
784
- sort_by: sortBy,
834
+ sort_by: sortColumn,
785
835
  sort_order: sortOrder,
786
836
  filters: filtersResult.filters,
787
837
  include
788
838
  });
789
839
  const labeled = result.data.map(
790
- (row) => fieldKeys.toLabels(row)
840
+ (row) => projectRow(row, typeName, projectors)
791
841
  );
792
842
  const data = resolver?.hasSchema(typeName) ? await resolver.resolveList(typeName, labeled) : labeled;
793
843
  return c.json({ ...result, data });
@@ -819,7 +869,7 @@ function registerPublicContentRoutes(app, registry, repos, fieldKeyMaps, paths,
819
869
  404
820
870
  );
821
871
  }
822
- let data = fieldKeyMaps[typeName].toLabels(item);
872
+ let data = projectRow(item, typeName, projectors);
823
873
  if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
824
874
  return c.json({ ok: true, data });
825
875
  });
@@ -847,9 +897,8 @@ function registerPublicContentRoutes(app, registry, repos, fieldKeyMaps, paths,
847
897
  page: pagination.page,
848
898
  per_page: pagination.per_page
849
899
  });
850
- const fieldKeys = fieldKeyMaps[typeName];
851
900
  const labeled = result.data.map(
852
- (row) => fieldKeys.toLabels(row)
901
+ (row) => projectRow(row, typeName, projectors)
853
902
  );
854
903
  const data = resolver?.hasSchema(typeName) ? await resolver.resolveList(typeName, labeled) : labeled;
855
904
  return c.json({ ...result, data });
@@ -866,7 +915,7 @@ function registerPublicContentRoutes(app, registry, repos, fieldKeyMaps, paths,
866
915
  404
867
916
  );
868
917
  }
869
- let data = fieldKeyMaps[typeName].toLabels(item);
918
+ let data = projectRow(item, typeName, projectors);
870
919
  if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
871
920
  return c.json({ ok: true, data });
872
921
  });
@@ -1080,7 +1129,7 @@ function parseListQuery(url, schemaFieldNames, relationFieldNames, columnFor) {
1080
1129
  search
1081
1130
  };
1082
1131
  }
1083
- function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRepo, requirePermission, db) {
1132
+ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo, requirePermission, db) {
1084
1133
  for (const [typeName, contentType] of Object.entries(registry.content_types)) {
1085
1134
  const basePath = `content/${typeName}`;
1086
1135
  const repo = repos[typeName];
@@ -1092,7 +1141,8 @@ function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRep
1092
1141
  const relationFieldNames = new Set(
1093
1142
  contentType.fields.filter((f) => RELATION_FIELD_TYPES.has(f.field_type)).map((f) => f.name)
1094
1143
  );
1095
- const fieldKeys = fieldKeyMaps[typeName];
1144
+ const projector = projectors[typeName];
1145
+ const fieldKeys = projector.map;
1096
1146
  const requiredFields = contentType.fields.filter((f) => f.required);
1097
1147
  const mediaFields = contentType.fields.filter(
1098
1148
  (f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file"
@@ -1113,10 +1163,11 @@ function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRep
1113
1163
  const publishedParam = c.req.query("published");
1114
1164
  const extraFilters = {};
1115
1165
  if (publishedParam === "false") extraFilters["published"] = false;
1166
+ const sortColumn = fieldKeys.columnFor(parsed.sortBy) ?? parsed.sortBy;
1116
1167
  const findOpts = {
1117
1168
  page: parsed.pagination.page,
1118
1169
  per_page: parsed.pagination.per_page,
1119
- sort_by: parsed.sortBy,
1170
+ sort_by: sortColumn,
1120
1171
  sort_order: parsed.sortOrder,
1121
1172
  filters: { ...parsed.filters, ...extraFilters },
1122
1173
  include: parsed.include
@@ -1126,7 +1177,9 @@ function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRep
1126
1177
  findOpts.search = { term: parsed.search, columns: searchableColumns };
1127
1178
  }
1128
1179
  const result = await repo.findMany(findOpts);
1129
- const data = result.data.map((row) => fieldKeys.toLabels(row));
1180
+ const data = result.data.map(
1181
+ (row) => projectRow(row, typeName, projectors)
1182
+ );
1130
1183
  return c.json({ ...result, data });
1131
1184
  }
1132
1185
  );
@@ -1162,7 +1215,7 @@ function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRep
1162
1215
  }
1163
1216
  }
1164
1217
  }
1165
- const data = fieldKeys.toLabels(item);
1218
+ const data = projectRow(item, typeName, projectors);
1166
1219
  return c.json({ ok: true, data });
1167
1220
  }
1168
1221
  );
@@ -1303,7 +1356,7 @@ function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRep
1303
1356
  }
1304
1357
  }
1305
1358
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
1306
- const data = fieldKeys.toLabels(item);
1359
+ const data = projectRow(item, typeName, projectors);
1307
1360
  return c.json({ ok: true, data }, 201);
1308
1361
  };
1309
1362
  app.patch(
@@ -1424,7 +1477,7 @@ function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRep
1424
1477
  }
1425
1478
  }
1426
1479
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
1427
- const data = fieldKeys.toLabels(updated);
1480
+ const data = projectRow(updated, typeName, projectors);
1428
1481
  return c.json({ ok: true, data });
1429
1482
  };
1430
1483
  if (contentType.only_one) {
@@ -1485,7 +1538,8 @@ function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRep
1485
1538
  const relationFieldNames = new Set(
1486
1539
  taxonomyType.fields.filter((f) => RELATION_FIELD_TYPES.has(f.field_type)).map((f) => f.name)
1487
1540
  );
1488
- const fieldKeys = fieldKeyMaps[typeName];
1541
+ const projector = projectors[typeName];
1542
+ const fieldKeys = projector.map;
1489
1543
  const requiredFields = taxonomyType.fields.filter((f) => f.required);
1490
1544
  const mediaFields = taxonomyType.fields.filter(
1491
1545
  (f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file"
@@ -1506,10 +1560,11 @@ function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRep
1506
1560
  const publishedParam = c.req.query("published");
1507
1561
  const extraFilters = {};
1508
1562
  if (publishedParam === "false") extraFilters["published"] = false;
1563
+ const sortColumn = fieldKeys.columnFor(parsed.sortBy) ?? parsed.sortBy;
1509
1564
  const findOpts = {
1510
1565
  page: parsed.pagination.page,
1511
1566
  per_page: parsed.pagination.per_page,
1512
- sort_by: parsed.sortBy,
1567
+ sort_by: sortColumn,
1513
1568
  sort_order: parsed.sortOrder,
1514
1569
  filters: { ...parsed.filters, ...extraFilters },
1515
1570
  include: parsed.include
@@ -1519,7 +1574,9 @@ function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRep
1519
1574
  findOpts.search = { term: parsed.search, columns: searchableColumns };
1520
1575
  }
1521
1576
  const result = await repo.findMany(findOpts);
1522
- const data = result.data.map((row) => fieldKeys.toLabels(row));
1577
+ const data = result.data.map(
1578
+ (row) => projectRow(row, typeName, projectors)
1579
+ );
1523
1580
  return c.json({ ...result, data });
1524
1581
  }
1525
1582
  );
@@ -1535,7 +1592,7 @@ function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRep
1535
1592
  404
1536
1593
  );
1537
1594
  }
1538
- const data = fieldKeys.toLabels(item);
1595
+ const data = projectRow(item, typeName, projectors);
1539
1596
  return c.json({ ok: true, data });
1540
1597
  }
1541
1598
  );
@@ -1588,7 +1645,7 @@ function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRep
1588
1645
  }
1589
1646
  }
1590
1647
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
1591
- const data = fieldKeys.toLabels(item);
1648
+ const data = projectRow(item, typeName, projectors);
1592
1649
  return c.json({ ok: true, data }, 201);
1593
1650
  }
1594
1651
  );
@@ -1656,7 +1713,7 @@ function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRep
1656
1713
  }
1657
1714
  }
1658
1715
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
1659
- const data = fieldKeys.toLabels(updated);
1716
+ const data = projectRow(updated, typeName, projectors);
1660
1717
  return c.json({ ok: true, data });
1661
1718
  }
1662
1719
  );
@@ -2649,7 +2706,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2649
2706
  import { sql as sql5 } from "drizzle-orm";
2650
2707
  var API_VERSION = (() => {
2651
2708
  try {
2652
- return "0.4.0";
2709
+ return "0.4.2";
2653
2710
  } catch {
2654
2711
  return "0.0.0";
2655
2712
  }
@@ -2695,6 +2752,9 @@ function registerConfigRoute(app, config, registry, db) {
2695
2752
 
2696
2753
  // src/routes/admin/schema.ts
2697
2754
  import { sql as sql6 } from "drizzle-orm";
2755
+ function liveFields(fields) {
2756
+ return fields.filter((f) => f.removed !== true);
2757
+ }
2698
2758
  function registerSchemaRoute(app, registry, db) {
2699
2759
  app.get("/admin/api/schema", (c) => {
2700
2760
  const content_types = Object.values(registry.content_types).map((ct) => ({
@@ -2703,19 +2763,19 @@ function registerSchemaRoute(app, registry, db) {
2703
2763
  only_one: ct.only_one,
2704
2764
  ui: ct.ui,
2705
2765
  system_fields: ct.system_fields,
2706
- fields: ct.fields
2766
+ fields: liveFields(ct.fields)
2707
2767
  }));
2708
2768
  const taxonomy_types = Object.values(registry.taxonomy_types).map((tt) => ({
2709
2769
  name: tt.name,
2710
2770
  label: tt.label,
2711
2771
  system_fields: tt.system_fields,
2712
- fields: tt.fields
2772
+ fields: liveFields(tt.fields)
2713
2773
  }));
2714
2774
  const paragraph_types = Object.values(registry.paragraph_types).map((pt) => ({
2715
2775
  name: pt.name,
2716
2776
  label: pt.label,
2717
2777
  system_fields: pt.system_fields,
2718
- fields: pt.fields
2778
+ fields: liveFields(pt.fields)
2719
2779
  }));
2720
2780
  const enum_types = Object.values(registry.enum_types).map((et) => ({
2721
2781
  name: et.name,
@@ -2811,7 +2871,7 @@ function whereFragment(conds) {
2811
2871
  return conds.length > 0 ? sql7` WHERE ${sql7.join(conds, sql7` AND `)}` : sql7``;
2812
2872
  }
2813
2873
  function createDrizzleContentRepository(db, tableName, options = {}) {
2814
- const { relations = {}, publishedRelations = false } = options;
2874
+ const { relations = {}, publishedRelations = false, sortableColumns } = options;
2815
2875
  function tableRaw() {
2816
2876
  return sql7.raw(quoteIdent2(tableName));
2817
2877
  }
@@ -2849,8 +2909,9 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
2849
2909
  sort_order = "asc",
2850
2910
  search
2851
2911
  } = opts;
2852
- if (!SORTABLE_FIELDS2.has(sort_by)) {
2853
- throw codeError("INVALID_SORT_FIELD", `'${sort_by}' is not sortable. Allowed: title, created_at, updated_at`);
2912
+ const allowed = sortableColumns ?? SORTABLE_FIELDS2;
2913
+ if (!allowed.has(sort_by)) {
2914
+ throw codeError("INVALID_SORT_FIELD", `'${sort_by}' is not sortable.`);
2854
2915
  }
2855
2916
  const offset = (page - 1) * per_page;
2856
2917
  const conds = buildConditions(published_only, filters, search);
@@ -3095,16 +3156,38 @@ function createCmsApp(options) {
3095
3156
  return result.rows[0]?.role ?? null;
3096
3157
  };
3097
3158
  const requireHierarchy = createHierarchyMiddleware(rolesRegistry, getUserRole);
3159
+ const fieldKeyMaps = Object.fromEntries([
3160
+ ...Object.entries(registry.content_types).map(([typeName, ct]) => [
3161
+ typeName,
3162
+ createFieldKeyMap(ct.fields)
3163
+ ]),
3164
+ ...Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
3165
+ typeName,
3166
+ createFieldKeyMap(tt.fields)
3167
+ ]),
3168
+ ...Object.entries(registry.paragraph_types).map(([typeName, pt]) => [
3169
+ typeName,
3170
+ createFieldKeyMap(pt.fields)
3171
+ ])
3172
+ ]);
3173
+ const projectors = buildProjectors(registry, fieldKeyMaps);
3174
+ const sortableColumnsFor = (typeName) => new Set(
3175
+ [...SORTABLE_FIELDS].map((label) => fieldKeyMaps[typeName].columnFor(label) ?? label)
3176
+ );
3098
3177
  const contentRepos = Object.fromEntries(
3099
3178
  Object.entries(registry.content_types).map(([typeName, ct]) => [
3100
3179
  typeName,
3101
- createDrizzleContentRepository(db, ct.db.table_name)
3180
+ createDrizzleContentRepository(db, ct.db.table_name, {
3181
+ sortableColumns: sortableColumnsFor(typeName)
3182
+ })
3102
3183
  ])
3103
3184
  );
3104
3185
  const taxonomyRepos = Object.fromEntries(
3105
3186
  Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
3106
3187
  typeName,
3107
- createDrizzleContentRepository(db, tt.db.table_name)
3188
+ createDrizzleContentRepository(db, tt.db.table_name, {
3189
+ sortableColumns: sortableColumnsFor(typeName)
3190
+ })
3108
3191
  ])
3109
3192
  );
3110
3193
  const repos = { ...contentRepos, ...taxonomyRepos };
@@ -3114,7 +3197,8 @@ function createCmsApp(options) {
3114
3197
  typeName,
3115
3198
  createDrizzleContentRepository(db, ct.db.table_name, {
3116
3199
  relations: buildRelationsMap(ct.fields, registry),
3117
- publishedRelations: true
3200
+ publishedRelations: true,
3201
+ sortableColumns: sortableColumnsFor(typeName)
3118
3202
  })
3119
3203
  ])
3120
3204
  );
@@ -3123,7 +3207,8 @@ function createCmsApp(options) {
3123
3207
  typeName,
3124
3208
  createDrizzleContentRepository(db, tt.db.table_name, {
3125
3209
  relations: buildRelationsMap(tt.fields, registry),
3126
- publishedRelations: true
3210
+ publishedRelations: true,
3211
+ sortableColumns: sortableColumnsFor(typeName)
3127
3212
  })
3128
3213
  ])
3129
3214
  );
@@ -3131,11 +3216,15 @@ function createCmsApp(options) {
3131
3216
  const graphqlRepos = Object.fromEntries([
3132
3217
  ...Object.entries(registry.content_types).map(([typeName, ct]) => [
3133
3218
  typeName,
3134
- createDrizzleContentRepository(db, ct.db.table_name)
3219
+ createDrizzleContentRepository(db, ct.db.table_name, {
3220
+ sortableColumns: sortableColumnsFor(typeName)
3221
+ })
3135
3222
  ]),
3136
3223
  ...Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
3137
3224
  typeName,
3138
- createDrizzleContentRepository(db, tt.db.table_name)
3225
+ createDrizzleContentRepository(db, tt.db.table_name, {
3226
+ sortableColumns: sortableColumnsFor(typeName)
3227
+ })
3139
3228
  ])
3140
3229
  ]);
3141
3230
  registerAuthRoutes(app, db);
@@ -3176,23 +3265,13 @@ function createCmsApp(options) {
3176
3265
  paths: {}
3177
3266
  })
3178
3267
  );
3179
- const fieldKeyMaps = Object.fromEntries([
3180
- ...Object.entries(registry.content_types).map(([typeName, ct]) => [
3181
- typeName,
3182
- createFieldKeyMap(ct.fields)
3183
- ]),
3184
- ...Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
3185
- typeName,
3186
- createFieldKeyMap(tt.fields)
3187
- ])
3188
- ]);
3189
- registerPublicContentRoutes(app, registry, publicRepos, fieldKeyMaps, publicPaths, listRateLimit, programmaticResolver);
3268
+ registerPublicContentRoutes(app, registry, publicRepos, projectors, publicPaths, listRateLimit, programmaticResolver);
3190
3269
  registerPublicMediaRoutes(app, mediaRepo, publicPaths, listRateLimit);
3191
3270
  if (options.graphql?.enabled) {
3192
3271
  const gqlOptions = options.graphql;
3193
3272
  let gqlHandler = null;
3194
3273
  let initError = null;
3195
- const ready = import("./handler-TPEULQ5W.mjs").then(({ createGraphQLHandler }) => {
3274
+ const ready = import("./handler-45O5OX2C.mjs").then(({ createGraphQLHandler }) => {
3196
3275
  gqlHandler = createGraphQLHandler(registry, graphqlRepos, fieldKeyMaps, programmaticResolver, db, gqlOptions);
3197
3276
  }).catch((err) => {
3198
3277
  initError = err;
@@ -3233,7 +3312,7 @@ function createCmsApp(options) {
3233
3312
  );
3234
3313
  registerSchemaRoute(app, registry, db);
3235
3314
  registerUserRoutes(app, db, requirePermission, requireHierarchy);
3236
- registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRepo, requirePermission, db);
3315
+ registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo, requirePermission, db);
3237
3316
  registerAdminMediaRoutes(app, mediaRepo, storage, requirePermission, maxFileSize);
3238
3317
  return { prefix, app };
3239
3318
  }