@bobbykim/manguito-cms-api 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  persistParagraphField,
9
9
  resolveRelationBareIds,
10
10
  resolveRelationField
11
- } from "./chunk-D5AO4BTL.mjs";
11
+ } from "./chunk-FXPKGPYF.mjs";
12
12
 
13
13
  // src/paths.ts
14
14
  function normalizePrefix(prefix) {
@@ -102,7 +102,7 @@ function createSecurityHeadersMiddleware(options = {}) {
102
102
  c.res.headers.set("X-Content-Type-Options", "nosniff");
103
103
  c.res.headers.set("X-Frame-Options", "DENY");
104
104
  c.res.headers.set("Referrer-Policy", "no-referrer");
105
- const isGraphiql = graphiqlPath !== void 0 && c.req.path === graphiqlPath;
105
+ const isGraphiql = graphiqlPath !== void 0 && (c.req.path === graphiqlPath || c.req.path.startsWith(`${graphiqlPath}/`));
106
106
  c.res.headers.set("Content-Security-Policy", isGraphiql ? graphiqlCsp : csp);
107
107
  };
108
108
  }
@@ -617,14 +617,16 @@ function isPlainRow(v) {
617
617
  return typeof v === "object" && v !== null && !Array.isArray(v) && !(v instanceof Date);
618
618
  }
619
619
  var MEDIA_FIELD_TYPES = /* @__PURE__ */ new Set(["image", "video", "file"]);
620
- function nestedTargets(fields) {
620
+ function nestedTargets(fields, map) {
621
621
  const out = [];
622
622
  for (const f of fields) {
623
623
  if (MEDIA_FIELD_TYPES.has(f.field_type)) continue;
624
624
  if (f.field_type !== "paragraph" && f.field_type !== "reference") continue;
625
625
  const ref = f.ui_component.ref;
626
626
  if (!ref) continue;
627
- out.push({ label: f.name, target: ref });
627
+ const label = isColumnBacked(f) ? map.labelFor(f.db_column.column_name) : f.name;
628
+ if (label === void 0) continue;
629
+ out.push({ label, target: ref });
628
630
  }
629
631
  return out;
630
632
  }
@@ -642,7 +644,7 @@ function buildProjectors(registry, fieldKeyMaps, fallbacks) {
642
644
  const typeFallbacks = fallbacks?.[typeName];
643
645
  projectors[typeName] = {
644
646
  map,
645
- nested: nestedTargets(type.fields),
647
+ nested: nestedTargets(type.fields, map),
646
648
  // Spread rather than assign: with exactOptionalPropertyTypes, an optional
647
649
  // property may be ABSENT but not present-and-undefined.
648
650
  ...typeFallbacks !== void 0 && { fallbacks: typeFallbacks }
@@ -726,7 +728,28 @@ function parseFilters(url, validFields, columnFor) {
726
728
  function isPublished(item) {
727
729
  return item["published"] === true;
728
730
  }
729
- function registerPublicContentRoutes(app, registry, repos, projectors, paths, listRateLimit, resolver) {
731
+ function registerPublicContentRoutes(app, registry, repos, projectors, paths, listRateLimit, resolver, resolverProjectors) {
732
+ const forResolver = resolverProjectors ?? projectors;
733
+ function pickProgrammatic(resolved, names) {
734
+ const out = {};
735
+ for (const name of names) if (name in resolved) out[name] = resolved[name];
736
+ return out;
737
+ }
738
+ async function respondItem(row, typeName, programmatic) {
739
+ const data = projectRow(row, typeName, projectors);
740
+ if (!resolver?.hasSchema(typeName)) return data;
741
+ const resolved = await resolver.resolveItem(typeName, projectRow(row, typeName, forResolver));
742
+ return { ...data, ...pickProgrammatic(resolved, programmatic) };
743
+ }
744
+ async function respondList(rows, typeName, programmatic) {
745
+ const data = rows.map((row) => projectRow(row, typeName, projectors));
746
+ if (!resolver?.hasSchema(typeName)) return data;
747
+ const resolved = await resolver.resolveList(
748
+ typeName,
749
+ rows.map((row) => projectRow(row, typeName, forResolver))
750
+ );
751
+ return data.map((d, i) => ({ ...d, ...pickProgrammatic(resolved[i], programmatic) }));
752
+ }
730
753
  function registerListRoute(path, handler) {
731
754
  if (listRateLimit) {
732
755
  app.get(path, listRateLimit, handler);
@@ -750,18 +773,29 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
750
773
  return c.json({ ok: true, data });
751
774
  });
752
775
  for (const [typeName, contentType] of Object.entries(registry.content_types)) {
776
+ let translateInclude2 = function(requested) {
777
+ const include = [];
778
+ for (const field of requested) {
779
+ const registryName = includeNames.get(field);
780
+ if (registryName === void 0) return { ok: false, field };
781
+ include.push(registryName);
782
+ }
783
+ return { ok: true, include };
784
+ };
785
+ var translateInclude = translateInclude2;
753
786
  const basePath = contentType.default_base_path;
754
787
  const repo = repos[typeName];
755
788
  if (!repo) continue;
756
789
  const fieldKeys = projectors[typeName].map;
757
- const relationFieldNames = /* @__PURE__ */ new Set();
790
+ const programmatic = contentType.fields.filter((f) => f.field_type === "programmatic").map((f) => f.name);
791
+ const includeNames = /* @__PURE__ */ new Map();
758
792
  for (const f of contentType.fields) {
759
793
  if (!RELATION_FIELD_TYPES.has(f.field_type)) continue;
760
794
  if (isColumnBacked(f)) {
761
795
  const label = fieldKeys.labelFor(f.db_column.column_name);
762
- if (label !== void 0) relationFieldNames.add(label);
796
+ if (label !== void 0) includeNames.set(label, f.name);
763
797
  } else {
764
- relationFieldNames.add(f.name);
798
+ includeNames.set(f.name, f.name);
765
799
  }
766
800
  }
767
801
  const filterableFieldNames = /* @__PURE__ */ new Set([
@@ -780,8 +814,11 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
780
814
  404
781
815
  );
782
816
  }
783
- let data = projectRow(result.data[0], typeName, projectors);
784
- if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
817
+ const data = await respondItem(
818
+ result.data[0],
819
+ typeName,
820
+ programmatic
821
+ );
785
822
  return c.json({ ok: true, data });
786
823
  });
787
824
  } else {
@@ -838,21 +875,20 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
838
875
  400
839
876
  );
840
877
  }
841
- const include = parseInclude(c.req.query("include"));
842
- for (const field of include) {
843
- if (!relationFieldNames.has(field)) {
844
- return c.json(
845
- {
846
- ok: false,
847
- error: {
848
- code: "INVALID_INCLUDE_FIELD",
849
- message: `'${field}' is not a valid relation field`
850
- }
851
- },
852
- 400
853
- );
854
- }
878
+ const translated = translateInclude2(parseInclude(c.req.query("include")));
879
+ if (!translated.ok) {
880
+ return c.json(
881
+ {
882
+ ok: false,
883
+ error: {
884
+ code: "INVALID_INCLUDE_FIELD",
885
+ message: `'${translated.field}' is not a valid relation field`
886
+ }
887
+ },
888
+ 400
889
+ );
855
890
  }
891
+ const include = translated.include;
856
892
  const sortColumn = fieldKeys.labels.includes(sortBy) ? fieldKeys.columnFor(sortBy) : sortBy;
857
893
  const result = await repo.findMany({
858
894
  published_only: true,
@@ -863,29 +899,25 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
863
899
  filters: filtersResult.filters,
864
900
  include
865
901
  });
866
- const labeled = result.data.map(
867
- (row) => projectRow(row, typeName, projectors)
868
- );
869
- const data = resolver?.hasSchema(typeName) ? await resolver.resolveList(typeName, labeled) : labeled;
902
+ const data = await respondList(result.data, typeName, programmatic);
870
903
  return c.json({ ...result, data });
871
904
  });
872
905
  app.get(paths.item(basePath), async (c) => {
873
906
  const slug = c.req.param("slug");
874
- const include = parseInclude(c.req.query("include"));
875
- for (const field of include) {
876
- if (!relationFieldNames.has(field)) {
877
- return c.json(
878
- {
879
- ok: false,
880
- error: {
881
- code: "INVALID_INCLUDE_FIELD",
882
- message: `'${field}' is not a valid relation field`
883
- }
884
- },
885
- 400
886
- );
887
- }
907
+ const translated = translateInclude2(parseInclude(c.req.query("include")));
908
+ if (!translated.ok) {
909
+ return c.json(
910
+ {
911
+ ok: false,
912
+ error: {
913
+ code: "INVALID_INCLUDE_FIELD",
914
+ message: `'${translated.field}' is not a valid relation field`
915
+ }
916
+ },
917
+ 400
918
+ );
888
919
  }
920
+ const include = translated.include;
889
921
  const item = await repo.findBySlug(slug, include);
890
922
  if (!item || !isPublished(item)) {
891
923
  return c.json(
@@ -896,15 +928,15 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
896
928
  404
897
929
  );
898
930
  }
899
- let data = projectRow(item, typeName, projectors);
900
- if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
931
+ const data = await respondItem(item, typeName, programmatic);
901
932
  return c.json({ ok: true, data });
902
933
  });
903
934
  }
904
935
  }
905
- for (const [typeName] of Object.entries(registry.taxonomy_types)) {
936
+ for (const [typeName, taxonomyType] of Object.entries(registry.taxonomy_types)) {
906
937
  const repo = repos[typeName];
907
938
  if (!repo) continue;
939
+ const programmatic = taxonomyType.fields.filter((f) => f.field_type === "programmatic").map((f) => f.name);
908
940
  registerListRoute(paths.taxonomyCollection(typeName), async (c) => {
909
941
  const pagination = parsePagination(c.req.query("page"), c.req.query("per_page"));
910
942
  if (!pagination.ok) {
@@ -924,10 +956,7 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
924
956
  page: pagination.page,
925
957
  per_page: pagination.per_page
926
958
  });
927
- const labeled = result.data.map(
928
- (row) => projectRow(row, typeName, projectors)
929
- );
930
- const data = resolver?.hasSchema(typeName) ? await resolver.resolveList(typeName, labeled) : labeled;
959
+ const data = await respondList(result.data, typeName, programmatic);
931
960
  return c.json({ ...result, data });
932
961
  });
933
962
  app.get(paths.taxonomyItem(typeName), async (c) => {
@@ -942,8 +971,7 @@ function registerPublicContentRoutes(app, registry, repos, projectors, paths, li
942
971
  404
943
972
  );
944
973
  }
945
- let data = projectRow(item, typeName, projectors);
946
- if (resolver?.hasSchema(typeName)) data = await resolver.resolveItem(typeName, data);
974
+ const data = await respondItem(item, typeName, programmatic);
947
975
  return c.json({ ok: true, data });
948
976
  });
949
977
  }
@@ -2733,7 +2761,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
2733
2761
  import { sql as sql5 } from "drizzle-orm";
2734
2762
  var API_VERSION = (() => {
2735
2763
  try {
2736
- return "0.5.0";
2764
+ return "0.7.0";
2737
2765
  } catch {
2738
2766
  return "0.0.0";
2739
2767
  }
@@ -3323,20 +3351,6 @@ function createCmsApp(options) {
3323
3351
  sortableColumns
3324
3352
  });
3325
3353
  };
3326
- const graphqlRepos = Object.fromEntries([
3327
- ...Object.entries(registry.content_types).map(([typeName, ct]) => [
3328
- typeName,
3329
- createDrizzleContentRepository(db, ct.db.table_name, {
3330
- sortableColumns: sortableColumnsFor(typeName)
3331
- })
3332
- ]),
3333
- ...Object.entries(registry.taxonomy_types).map(([typeName, tt]) => [
3334
- typeName,
3335
- createDrizzleContentRepository(db, tt.db.table_name, {
3336
- sortableColumns: sortableColumnsFor(typeName)
3337
- })
3338
- ])
3339
- ]);
3340
3354
  registerAuthRoutes(app, db);
3341
3355
  app.use("/admin/api/*", authMiddleware);
3342
3356
  app.use("/admin/api/*", mustChangePasswordCheck);
@@ -3403,7 +3417,7 @@ function createCmsApp(options) {
3403
3417
  app.use(path, attachDeprecationHeaders);
3404
3418
  }
3405
3419
  }
3406
- registerPublicContentRoutes(app, registry, surface.repos, surface.projectors, surface.paths, listRateLimit, programmaticResolver);
3420
+ registerPublicContentRoutes(app, registry, surface.repos, surface.projectors, surface.paths, listRateLimit, programmaticResolver, projectors);
3407
3421
  }
3408
3422
  if (options.versions !== void 0) {
3409
3423
  app.all(`${prefix}/:version/*`, async (c, next) => {
@@ -3434,31 +3448,108 @@ function createCmsApp(options) {
3434
3448
  registerPublicMediaRoutes(app, mediaRepo, publicPaths, listRateLimit);
3435
3449
  if (options.graphql?.enabled) {
3436
3450
  const gqlOptions = options.graphql;
3437
- let gqlHandler = null;
3438
- let initError = null;
3439
- const ready = import("./handler-X7JDHJAL.mjs").then(({ createGraphQLHandler }) => {
3440
- gqlHandler = createGraphQLHandler(registry, graphqlRepos, fieldKeyMaps, programmaticResolver, db, gqlOptions);
3451
+ const makeGraphqlRepo = (_typeName, tableName, sortableColumns) => createDrizzleContentRepository(db, tableName, { sortableColumns });
3452
+ const gqlPasses = [
3453
+ ...options.versions !== void 0 ? model.live.map((v) => ({ version: v, projectionVersion: v })) : [],
3454
+ { version: null, projectionVersion: model.current }
3455
+ ];
3456
+ const mounts = gqlPasses.map((pass) => ({
3457
+ path: pass.version === null ? "/graphql" : `/graphql/${pass.version}`,
3458
+ version: pass.version,
3459
+ handler: null,
3460
+ error: null
3461
+ }));
3462
+ const ready = import("./handler-GED3I7Z2.mjs").then(({ createGraphQLHandler }) => {
3463
+ gqlPasses.forEach((pass, i) => {
3464
+ const mount = mounts[i];
3465
+ try {
3466
+ const surface = buildVersionSurface({
3467
+ ...pass,
3468
+ prefix,
3469
+ registry,
3470
+ model,
3471
+ makeRepo: makeGraphqlRepo
3472
+ });
3473
+ const projection = model.projections[pass.projectionVersion];
3474
+ const currentProjection = model.projections[model.current];
3475
+ mount.handler = createGraphQLHandler(
3476
+ registry,
3477
+ surface.repos,
3478
+ surface.fieldKeyMaps,
3479
+ programmaticResolver,
3480
+ db,
3481
+ gqlOptions,
3482
+ {
3483
+ endpoint: mount.path,
3484
+ currentVersion: model.current,
3485
+ ...projection !== void 0 && { projection },
3486
+ ...currentProjection !== void 0 && { currentProjection },
3487
+ currentFieldKeyMaps: fieldKeyMaps
3488
+ }
3489
+ );
3490
+ } catch (err) {
3491
+ mount.error = err;
3492
+ const message = err instanceof Error ? err.message : String(err);
3493
+ process.stderr.write(
3494
+ `\u2717 GraphQL schema failed to initialize for ${mount.path}; it will return 500. ${message}
3495
+ `
3496
+ );
3497
+ }
3498
+ });
3441
3499
  }).catch((err) => {
3442
- initError = err;
3500
+ for (const m of mounts) m.error = err;
3443
3501
  const message = err instanceof Error ? err.message : String(err);
3444
- process.stderr.write(`\u2717 GraphQL schema failed to initialize; /graphql will return 500. ${message}
3502
+ const paths = mounts.map((m) => m.path).join(", ");
3503
+ process.stderr.write(`\u2717 GraphQL module failed to load; ${paths} will return 500. ${message}
3445
3504
  `);
3446
3505
  });
3447
- const invokeGraphQL = async (c) => {
3448
- if (!gqlHandler && !initError) await ready;
3449
- if (!gqlHandler) {
3506
+ const invokeFor = (mount) => async (c) => {
3507
+ if (!mount.handler && !mount.error) await ready;
3508
+ if (!mount.handler) {
3450
3509
  return c.json(
3451
3510
  { ok: false, error: { code: "GRAPHQL_INIT_FAILED", message: "GraphQL schema failed to initialize" } },
3452
3511
  500
3453
3512
  );
3454
3513
  }
3455
- return gqlHandler(c, async () => {
3514
+ return mount.handler(c, async () => {
3456
3515
  });
3457
3516
  };
3458
- if (listRateLimit) {
3459
- app.all("/graphql", listRateLimit, invokeGraphQL);
3460
- } else {
3461
- app.all("/graphql", invokeGraphQL);
3517
+ for (const mount of mounts) {
3518
+ const headers = deprecationHeaders({
3519
+ requested: mount.version,
3520
+ model,
3521
+ successor: `/graphql/${model.current}`
3522
+ });
3523
+ if (headers === null) continue;
3524
+ const attach = async (c, next) => {
3525
+ await next();
3526
+ for (const [key, value] of Object.entries(headers)) c.res.headers.set(key, value);
3527
+ };
3528
+ app.use(mount.path, attach);
3529
+ }
3530
+ for (const mount of mounts) {
3531
+ if (listRateLimit) app.all(mount.path, listRateLimit, invokeFor(mount));
3532
+ else app.all(mount.path, invokeFor(mount));
3533
+ }
3534
+ if (options.versions !== void 0) {
3535
+ app.all("/graphql/:version", async (c, next) => {
3536
+ const segment = c.req.param("version");
3537
+ const kind = classifyVersion(segment, model);
3538
+ if (kind === "not-a-version" || kind === "live") return next();
3539
+ const live = model.live.join(", ");
3540
+ return c.json({
3541
+ errors: [
3542
+ {
3543
+ message: kind === "retired" ? `Version ${segment} is no longer served. Live versions: ${live}.` : `Version ${segment} does not exist. Live versions: ${live}.`,
3544
+ extensions: {
3545
+ code: kind === "retired" ? "VERSION_RETIRED" : "VERSION_UNKNOWN",
3546
+ current: model.current,
3547
+ live: model.live
3548
+ }
3549
+ }
3550
+ ]
3551
+ });
3552
+ });
3462
3553
  }
3463
3554
  }
3464
3555
  registerConfigRoute(