@bobbykim/manguito-cms-api 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -500,7 +500,7 @@ var init_naming = __esm({
500
500
  function buildSortFieldEnum(typeName) {
501
501
  return new import_graphql3.GraphQLEnumType({
502
502
  name: `${typeName}SortField`,
503
- values: Object.fromEntries(SORTABLE.map((s) => [s.gql, { value: s.column }]))
503
+ values: Object.fromEntries(SORTABLE.map((s) => [s.gql, { value: s.value }]))
504
504
  });
505
505
  }
506
506
  function comparableFilterInput(name, scalar) {
@@ -586,9 +586,9 @@ var init_filters = __esm({
586
586
  values: { ASC: { value: "asc" }, DESC: { value: "desc" } }
587
587
  });
588
588
  SORTABLE = [
589
- { gql: "title", column: "title" },
590
- { gql: "createdAt", column: "created_at" },
591
- { gql: "updatedAt", column: "updated_at" }
589
+ { gql: "title", value: "title" },
590
+ { gql: "createdAt", value: "created_at" },
591
+ { gql: "updatedAt", value: "updated_at" }
592
592
  ];
593
593
  StringFilter = equalityFilterInput("StringFilter", import_graphql3.GraphQLString);
594
594
  BooleanFilter = new import_graphql3.GraphQLInputObjectType({
@@ -647,11 +647,13 @@ function collectionResolver(typeName, nameMap, fieldKeys) {
647
647
  throw codeError2("INVALID_PAGINATION", "perPage must be between 1 and 100");
648
648
  }
649
649
  const repo = ctx.repos[typeName];
650
+ const sortBy = args.sortBy ?? "created_at";
651
+ const sortColumn = fieldKeys?.columnFor(sortBy) ?? sortBy;
650
652
  const result = await repo.findMany({
651
653
  published_only: true,
652
654
  page,
653
655
  per_page: perPage,
654
- sort_by: args.sortBy ?? "created_at",
656
+ sort_by: sortColumn,
655
657
  sort_order: args.sortOrder ?? "asc",
656
658
  filters: translateFilters(args.filter, nameMap, fieldKeys?.columnFor)
657
659
  });
@@ -1643,6 +1645,54 @@ function createProgrammaticResolver(resolvers, options) {
1643
1645
  return { resolveItem, resolveList, hasSchema };
1644
1646
  }
1645
1647
 
1648
+ // src/projector.ts
1649
+ function isPlainRow(v) {
1650
+ return typeof v === "object" && v !== null && !Array.isArray(v) && !(v instanceof Date);
1651
+ }
1652
+ var MEDIA_FIELD_TYPES = /* @__PURE__ */ new Set(["image", "video", "file"]);
1653
+ function nestedTargets(fields) {
1654
+ const out = [];
1655
+ for (const f of fields) {
1656
+ if (MEDIA_FIELD_TYPES.has(f.field_type)) continue;
1657
+ if (f.field_type !== "paragraph" && f.field_type !== "reference") continue;
1658
+ const ref = f.ui_component.ref;
1659
+ if (!ref) continue;
1660
+ out.push({ label: f.name, target: ref });
1661
+ }
1662
+ return out;
1663
+ }
1664
+ function buildProjectors(registry, fieldKeyMaps) {
1665
+ const projectors = {};
1666
+ const sources = [
1667
+ registry.content_types,
1668
+ registry.taxonomy_types,
1669
+ registry.paragraph_types
1670
+ ];
1671
+ for (const source of sources) {
1672
+ for (const [typeName, type] of Object.entries(source)) {
1673
+ const map = fieldKeyMaps[typeName];
1674
+ if (!map) continue;
1675
+ projectors[typeName] = { map, nested: nestedTargets(type.fields) };
1676
+ }
1677
+ }
1678
+ return projectors;
1679
+ }
1680
+ function projectRow(row, typeName, projectors) {
1681
+ const p = projectors[typeName];
1682
+ if (!p) return row;
1683
+ const out = p.map.toLabels(row);
1684
+ for (const { label, target } of p.nested) {
1685
+ const v = out[label];
1686
+ if (v === null || v === void 0) continue;
1687
+ if (Array.isArray(v)) {
1688
+ out[label] = v.map((item) => isPlainRow(item) ? projectRow(item, target, projectors) : item);
1689
+ } else if (isPlainRow(v)) {
1690
+ out[label] = projectRow(v, target, projectors);
1691
+ }
1692
+ }
1693
+ return out;
1694
+ }
1695
+
1646
1696
  // src/routes/query-params.ts
1647
1697
  var SORTABLE_FIELDS = /* @__PURE__ */ new Set(["title", "created_at", "updated_at"]);
1648
1698
  var RELATION_FIELD_TYPES = /* @__PURE__ */ new Set([
@@ -1697,7 +1747,7 @@ function parseFilters(url, validFields, columnFor) {
1697
1747
  function isPublished(item) {
1698
1748
  return item["published"] === true;
1699
1749
  }
1700
- function registerPublicContentRoutes(app, registry, repos, fieldKeyMaps, paths, listRateLimit, resolver) {
1750
+ function registerPublicContentRoutes(app, registry, repos, projectors, paths, listRateLimit, resolver) {
1701
1751
  function registerListRoute(path, handler) {
1702
1752
  if (listRateLimit) {
1703
1753
  app.get(path, listRateLimit, handler);
@@ -1740,13 +1790,14 @@ function registerPublicContentRoutes(app, registry, repos, fieldKeyMaps, paths,
1740
1790
  404
1741
1791
  );
1742
1792
  }
1743
- let data = fieldKeyMaps[typeName].toLabels(result.data[0]);
1793
+ let data = projectRow(result.data[0], typeName, projectors);
1744
1794
  if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
1745
1795
  return c.json({ ok: true, data });
1746
1796
  });
1747
1797
  } else {
1748
1798
  registerListRoute(paths.collection(basePath), async (c) => {
1749
- const fieldKeys = fieldKeyMaps[typeName];
1799
+ const projector = projectors[typeName];
1800
+ const fieldKeys = projector.map;
1750
1801
  const pagination = parsePagination(c.req.query("page"), c.req.query("per_page"));
1751
1802
  if (!pagination.ok) {
1752
1803
  return c.json(
@@ -1814,17 +1865,18 @@ function registerPublicContentRoutes(app, registry, repos, fieldKeyMaps, paths,
1814
1865
  );
1815
1866
  }
1816
1867
  }
1868
+ const sortColumn = fieldKeys.columnFor(sortBy) ?? sortBy;
1817
1869
  const result = await repo.findMany({
1818
1870
  published_only: true,
1819
1871
  page: pagination.page,
1820
1872
  per_page: pagination.per_page,
1821
- sort_by: sortBy,
1873
+ sort_by: sortColumn,
1822
1874
  sort_order: sortOrder,
1823
1875
  filters: filtersResult.filters,
1824
1876
  include
1825
1877
  });
1826
1878
  const labeled = result.data.map(
1827
- (row) => fieldKeys.toLabels(row)
1879
+ (row) => projectRow(row, typeName, projectors)
1828
1880
  );
1829
1881
  const data = resolver?.hasSchema(typeName) ? await resolver.resolveList(typeName, labeled) : labeled;
1830
1882
  return c.json({ ...result, data });
@@ -1856,7 +1908,7 @@ function registerPublicContentRoutes(app, registry, repos, fieldKeyMaps, paths,
1856
1908
  404
1857
1909
  );
1858
1910
  }
1859
- let data = fieldKeyMaps[typeName].toLabels(item);
1911
+ let data = projectRow(item, typeName, projectors);
1860
1912
  if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
1861
1913
  return c.json({ ok: true, data });
1862
1914
  });
@@ -1884,9 +1936,8 @@ function registerPublicContentRoutes(app, registry, repos, fieldKeyMaps, paths,
1884
1936
  page: pagination.page,
1885
1937
  per_page: pagination.per_page
1886
1938
  });
1887
- const fieldKeys = fieldKeyMaps[typeName];
1888
1939
  const labeled = result.data.map(
1889
- (row) => fieldKeys.toLabels(row)
1940
+ (row) => projectRow(row, typeName, projectors)
1890
1941
  );
1891
1942
  const data = resolver?.hasSchema(typeName) ? await resolver.resolveList(typeName, labeled) : labeled;
1892
1943
  return c.json({ ...result, data });
@@ -1903,7 +1954,7 @@ function registerPublicContentRoutes(app, registry, repos, fieldKeyMaps, paths,
1903
1954
  404
1904
1955
  );
1905
1956
  }
1906
- let data = fieldKeyMaps[typeName].toLabels(item);
1957
+ let data = projectRow(item, typeName, projectors);
1907
1958
  if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
1908
1959
  return c.json({ ok: true, data });
1909
1960
  });
@@ -2119,7 +2170,7 @@ function parseListQuery(url, schemaFieldNames, relationFieldNames, columnFor) {
2119
2170
  search
2120
2171
  };
2121
2172
  }
2122
- function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRepo, requirePermission, db) {
2173
+ function registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo, requirePermission, db) {
2123
2174
  for (const [typeName, contentType] of Object.entries(registry.content_types)) {
2124
2175
  const basePath = `content/${typeName}`;
2125
2176
  const repo = repos[typeName];
@@ -2131,7 +2182,8 @@ function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRep
2131
2182
  const relationFieldNames = new Set(
2132
2183
  contentType.fields.filter((f) => RELATION_FIELD_TYPES.has(f.field_type)).map((f) => f.name)
2133
2184
  );
2134
- const fieldKeys = fieldKeyMaps[typeName];
2185
+ const projector = projectors[typeName];
2186
+ const fieldKeys = projector.map;
2135
2187
  const requiredFields = contentType.fields.filter((f) => f.required);
2136
2188
  const mediaFields = contentType.fields.filter(
2137
2189
  (f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file"
@@ -2152,10 +2204,11 @@ function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRep
2152
2204
  const publishedParam = c.req.query("published");
2153
2205
  const extraFilters = {};
2154
2206
  if (publishedParam === "false") extraFilters["published"] = false;
2207
+ const sortColumn = fieldKeys.columnFor(parsed.sortBy) ?? parsed.sortBy;
2155
2208
  const findOpts = {
2156
2209
  page: parsed.pagination.page,
2157
2210
  per_page: parsed.pagination.per_page,
2158
- sort_by: parsed.sortBy,
2211
+ sort_by: sortColumn,
2159
2212
  sort_order: parsed.sortOrder,
2160
2213
  filters: { ...parsed.filters, ...extraFilters },
2161
2214
  include: parsed.include
@@ -2165,7 +2218,9 @@ function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRep
2165
2218
  findOpts.search = { term: parsed.search, columns: searchableColumns };
2166
2219
  }
2167
2220
  const result = await repo.findMany(findOpts);
2168
- const data = result.data.map((row) => fieldKeys.toLabels(row));
2221
+ const data = result.data.map(
2222
+ (row) => projectRow(row, typeName, projectors)
2223
+ );
2169
2224
  return c.json({ ...result, data });
2170
2225
  }
2171
2226
  );
@@ -2201,7 +2256,7 @@ function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRep
2201
2256
  }
2202
2257
  }
2203
2258
  }
2204
- const data = fieldKeys.toLabels(item);
2259
+ const data = projectRow(item, typeName, projectors);
2205
2260
  return c.json({ ok: true, data });
2206
2261
  }
2207
2262
  );
@@ -2342,7 +2397,7 @@ function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRep
2342
2397
  }
2343
2398
  }
2344
2399
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
2345
- const data = fieldKeys.toLabels(item);
2400
+ const data = projectRow(item, typeName, projectors);
2346
2401
  return c.json({ ok: true, data }, 201);
2347
2402
  };
2348
2403
  app.patch(
@@ -2463,7 +2518,7 @@ function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRep
2463
2518
  }
2464
2519
  }
2465
2520
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
2466
- const data = fieldKeys.toLabels(updated);
2521
+ const data = projectRow(updated, typeName, projectors);
2467
2522
  return c.json({ ok: true, data });
2468
2523
  };
2469
2524
  if (contentType.only_one) {
@@ -2524,7 +2579,8 @@ function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRep
2524
2579
  const relationFieldNames = new Set(
2525
2580
  taxonomyType.fields.filter((f) => RELATION_FIELD_TYPES.has(f.field_type)).map((f) => f.name)
2526
2581
  );
2527
- const fieldKeys = fieldKeyMaps[typeName];
2582
+ const projector = projectors[typeName];
2583
+ const fieldKeys = projector.map;
2528
2584
  const requiredFields = taxonomyType.fields.filter((f) => f.required);
2529
2585
  const mediaFields = taxonomyType.fields.filter(
2530
2586
  (f) => f.field_type === "image" || f.field_type === "video" || f.field_type === "file"
@@ -2545,10 +2601,11 @@ function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRep
2545
2601
  const publishedParam = c.req.query("published");
2546
2602
  const extraFilters = {};
2547
2603
  if (publishedParam === "false") extraFilters["published"] = false;
2604
+ const sortColumn = fieldKeys.columnFor(parsed.sortBy) ?? parsed.sortBy;
2548
2605
  const findOpts = {
2549
2606
  page: parsed.pagination.page,
2550
2607
  per_page: parsed.pagination.per_page,
2551
- sort_by: parsed.sortBy,
2608
+ sort_by: sortColumn,
2552
2609
  sort_order: parsed.sortOrder,
2553
2610
  filters: { ...parsed.filters, ...extraFilters },
2554
2611
  include: parsed.include
@@ -2558,7 +2615,9 @@ function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRep
2558
2615
  findOpts.search = { term: parsed.search, columns: searchableColumns };
2559
2616
  }
2560
2617
  const result = await repo.findMany(findOpts);
2561
- const data = result.data.map((row) => fieldKeys.toLabels(row));
2618
+ const data = result.data.map(
2619
+ (row) => projectRow(row, typeName, projectors)
2620
+ );
2562
2621
  return c.json({ ...result, data });
2563
2622
  }
2564
2623
  );
@@ -2574,7 +2633,7 @@ function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRep
2574
2633
  404
2575
2634
  );
2576
2635
  }
2577
- const data = fieldKeys.toLabels(item);
2636
+ const data = projectRow(item, typeName, projectors);
2578
2637
  return c.json({ ok: true, data });
2579
2638
  }
2580
2639
  );
@@ -2627,7 +2686,7 @@ function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRep
2627
2686
  }
2628
2687
  }
2629
2688
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
2630
- const data = fieldKeys.toLabels(item);
2689
+ const data = projectRow(item, typeName, projectors);
2631
2690
  return c.json({ ok: true, data }, 201);
2632
2691
  }
2633
2692
  );
@@ -2695,7 +2754,7 @@ function registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRep
2695
2754
  }
2696
2755
  }
2697
2756
  await applyMediaReferenceDelta(mergeMediaDeltas(...mediaDeltas), mediaRepo);
2698
- const data = fieldKeys.toLabels(updated);
2757
+ const data = projectRow(updated, typeName, projectors);
2699
2758
  return c.json({ ok: true, data });
2700
2759
  }
2701
2760
  );
@@ -3688,7 +3747,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
3688
3747
  var import_drizzle_orm6 = require("drizzle-orm");
3689
3748
  var API_VERSION = (() => {
3690
3749
  try {
3691
- return "0.4.0";
3750
+ return "0.4.1";
3692
3751
  } catch {
3693
3752
  return "0.0.0";
3694
3753
  }
@@ -3851,7 +3910,7 @@ function whereFragment(conds) {
3851
3910
  return conds.length > 0 ? import_drizzle_orm8.sql` WHERE ${import_drizzle_orm8.sql.join(conds, import_drizzle_orm8.sql` AND `)}` : import_drizzle_orm8.sql``;
3852
3911
  }
3853
3912
  function createDrizzleContentRepository(db, tableName, options = {}) {
3854
- const { relations = {}, publishedRelations = false } = options;
3913
+ const { relations = {}, publishedRelations = false, sortableColumns } = options;
3855
3914
  function tableRaw() {
3856
3915
  return import_drizzle_orm8.sql.raw(quoteIdent3(tableName));
3857
3916
  }
@@ -3889,8 +3948,9 @@ function createDrizzleContentRepository(db, tableName, options = {}) {
3889
3948
  sort_order = "asc",
3890
3949
  search
3891
3950
  } = opts;
3892
- if (!SORTABLE_FIELDS2.has(sort_by)) {
3893
- throw codeError("INVALID_SORT_FIELD", `'${sort_by}' is not sortable. Allowed: title, created_at, updated_at`);
3951
+ const allowed = sortableColumns ?? SORTABLE_FIELDS2;
3952
+ if (!allowed.has(sort_by)) {
3953
+ throw codeError("INVALID_SORT_FIELD", `'${sort_by}' is not sortable.`);
3894
3954
  }
3895
3955
  const offset = (page - 1) * per_page;
3896
3956
  const conds = buildConditions(published_only, filters, search);
@@ -4139,16 +4199,38 @@ function createCmsApp(options) {
4139
4199
  return result.rows[0]?.role ?? null;
4140
4200
  };
4141
4201
  const requireHierarchy = createHierarchyMiddleware(rolesRegistry, getUserRole);
4202
+ const fieldKeyMaps = Object.fromEntries([
4203
+ ...Object.entries(registry.content_types).map(([typeName, ct]) => [
4204
+ typeName,
4205
+ createFieldKeyMap(ct.fields)
4206
+ ]),
4207
+ ...Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
4208
+ typeName,
4209
+ createFieldKeyMap(tt.fields)
4210
+ ]),
4211
+ ...Object.entries(registry.paragraph_types).map(([typeName, pt]) => [
4212
+ typeName,
4213
+ createFieldKeyMap(pt.fields)
4214
+ ])
4215
+ ]);
4216
+ const projectors = buildProjectors(registry, fieldKeyMaps);
4217
+ const sortableColumnsFor = (typeName) => new Set(
4218
+ [...SORTABLE_FIELDS].map((label) => fieldKeyMaps[typeName].columnFor(label) ?? label)
4219
+ );
4142
4220
  const contentRepos = Object.fromEntries(
4143
4221
  Object.entries(registry.content_types).map(([typeName, ct]) => [
4144
4222
  typeName,
4145
- createDrizzleContentRepository(db, ct.db.table_name)
4223
+ createDrizzleContentRepository(db, ct.db.table_name, {
4224
+ sortableColumns: sortableColumnsFor(typeName)
4225
+ })
4146
4226
  ])
4147
4227
  );
4148
4228
  const taxonomyRepos = Object.fromEntries(
4149
4229
  Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
4150
4230
  typeName,
4151
- createDrizzleContentRepository(db, tt.db.table_name)
4231
+ createDrizzleContentRepository(db, tt.db.table_name, {
4232
+ sortableColumns: sortableColumnsFor(typeName)
4233
+ })
4152
4234
  ])
4153
4235
  );
4154
4236
  const repos = { ...contentRepos, ...taxonomyRepos };
@@ -4158,7 +4240,8 @@ function createCmsApp(options) {
4158
4240
  typeName,
4159
4241
  createDrizzleContentRepository(db, ct.db.table_name, {
4160
4242
  relations: buildRelationsMap(ct.fields, registry),
4161
- publishedRelations: true
4243
+ publishedRelations: true,
4244
+ sortableColumns: sortableColumnsFor(typeName)
4162
4245
  })
4163
4246
  ])
4164
4247
  );
@@ -4167,7 +4250,8 @@ function createCmsApp(options) {
4167
4250
  typeName,
4168
4251
  createDrizzleContentRepository(db, tt.db.table_name, {
4169
4252
  relations: buildRelationsMap(tt.fields, registry),
4170
- publishedRelations: true
4253
+ publishedRelations: true,
4254
+ sortableColumns: sortableColumnsFor(typeName)
4171
4255
  })
4172
4256
  ])
4173
4257
  );
@@ -4175,11 +4259,15 @@ function createCmsApp(options) {
4175
4259
  const graphqlRepos = Object.fromEntries([
4176
4260
  ...Object.entries(registry.content_types).map(([typeName, ct]) => [
4177
4261
  typeName,
4178
- createDrizzleContentRepository(db, ct.db.table_name)
4262
+ createDrizzleContentRepository(db, ct.db.table_name, {
4263
+ sortableColumns: sortableColumnsFor(typeName)
4264
+ })
4179
4265
  ]),
4180
4266
  ...Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
4181
4267
  typeName,
4182
- createDrizzleContentRepository(db, tt.db.table_name)
4268
+ createDrizzleContentRepository(db, tt.db.table_name, {
4269
+ sortableColumns: sortableColumnsFor(typeName)
4270
+ })
4183
4271
  ])
4184
4272
  ]);
4185
4273
  registerAuthRoutes(app, db);
@@ -4220,17 +4308,7 @@ function createCmsApp(options) {
4220
4308
  paths: {}
4221
4309
  })
4222
4310
  );
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);
4311
+ registerPublicContentRoutes(app, registry, publicRepos, projectors, publicPaths, listRateLimit, programmaticResolver);
4234
4312
  registerPublicMediaRoutes(app, mediaRepo, publicPaths, listRateLimit);
4235
4313
  if (options.graphql?.enabled) {
4236
4314
  const gqlOptions = options.graphql;
@@ -4277,7 +4355,7 @@ function createCmsApp(options) {
4277
4355
  );
4278
4356
  registerSchemaRoute(app, registry, db);
4279
4357
  registerUserRoutes(app, db, requirePermission, requireHierarchy);
4280
- registerAdminContentRoutes(app, registry, repos, fieldKeyMaps, mediaRepo, requirePermission, db);
4358
+ registerAdminContentRoutes(app, registry, repos, projectors, mediaRepo, requirePermission, db);
4281
4359
  registerAdminMediaRoutes(app, mediaRepo, storage, requirePermission, maxFileSize);
4282
4360
  return { prefix, app };
4283
4361
  }