@bobbykim/manguito-cms-cli 0.5.0 → 0.6.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
@@ -22,6 +22,7 @@ import {
22
22
  __toESM,
23
23
  buildRelationsMap,
24
24
  createFieldKeyMap,
25
+ createFieldKeyMapFromProjection,
25
26
  deleteParagraphField,
26
27
  entityKind,
27
28
  fillPlaceholders,
@@ -38,7 +39,7 @@ import {
38
39
  resolveRelationField,
39
40
  sql,
40
41
  tracer
41
- } from "./chunk-LFMOJRYP.js";
42
+ } from "./chunk-SW3T2M64.js";
42
43
 
43
44
  // ../../node_modules/.pnpm/yaml@2.9.0/node_modules/yaml/dist/nodes/identity.js
44
45
  var require_identity = __commonJS({
@@ -14469,7 +14470,7 @@ import { Command } from "commander";
14469
14470
  // src/commands/build.ts
14470
14471
  init_esm_shims();
14471
14472
  import { mkdirSync, statSync as statSync2, existsSync as existsSync3, readdirSync as readdirSync3, writeFileSync } from "fs";
14472
- import { resolve as resolve3, join as join8, dirname } from "path";
14473
+ import { resolve as resolve4, join as join9, dirname } from "path";
14473
14474
  import { createRequire } from "module";
14474
14475
  import { build as viteBuild } from "vite";
14475
14476
  import { build as tsupBuild } from "tsup";
@@ -30443,7 +30444,7 @@ function buildProjections(input2) {
30443
30444
  const fields = type.fields.filter((f) => isColumnBacked2(f) && f.removed !== true).map((f) => {
30444
30445
  const column_name = f.db_column.column_name;
30445
30446
  const fallback = fallbacks.get(`${typeName}.${column_name}`);
30446
- return fallback === void 0 ? { column_name, exposed_as: f.name } : { column_name, exposed_as: f.name, fallback };
30447
+ return fallback === void 0 ? { column_name, exposed_as: f.name, required: f.required } : { column_name, exposed_as: f.name, required: f.required, fallback };
30447
30448
  });
30448
30449
  types3[typeName] = { fields };
30449
30450
  }
@@ -34195,7 +34196,7 @@ var PgSelectQueryBuilderBase = class extends TypedQueryBuilder {
34195
34196
  const baseTableName = this.tableName;
34196
34197
  const tableName = getTableLikeName(table);
34197
34198
  for (const item of extractUsedTable(table)) this.usedTables.add(item);
34198
- if (typeof tableName === "string" && this.config.joins?.some((join14) => join14.alias === tableName)) {
34199
+ if (typeof tableName === "string" && this.config.joins?.some((join15) => join15.alias === tableName)) {
34199
34200
  throw new Error(`Alias "${tableName}" is already used in this query`);
34200
34201
  }
34201
34202
  if (!this.isPartialSelect) {
@@ -35419,7 +35420,7 @@ var PgUpdateBase = class extends QueryPromise {
35419
35420
  createJoin(joinType) {
35420
35421
  return (table, on) => {
35421
35422
  const tableName = getTableLikeName(table);
35422
- if (typeof tableName === "string" && this.config.joins.some((join14) => join14.alias === tableName)) {
35423
+ if (typeof tableName === "string" && this.config.joins.some((join15) => join15.alias === tableName)) {
35423
35424
  throw new Error(`Alias "${tableName}" is already used in this query`);
35424
35425
  }
35425
35426
  if (typeof on === "function") {
@@ -35515,10 +35516,10 @@ var PgUpdateBase = class extends QueryPromise {
35515
35516
  const fromFields = this.getTableLikeFields(this.config.from);
35516
35517
  fields[tableName] = fromFields;
35517
35518
  }
35518
- for (const join14 of this.config.joins) {
35519
- const tableName2 = getTableLikeName(join14.table);
35520
- if (typeof tableName2 === "string" && !is(join14.table, SQL)) {
35521
- const fromFields = this.getTableLikeFields(join14.table);
35519
+ for (const join15 of this.config.joins) {
35520
+ const tableName2 = getTableLikeName(join15.table);
35521
+ if (typeof tableName2 === "string" && !is(join15.table, SQL)) {
35522
+ const fromFields = this.getTableLikeFields(join15.table);
35522
35523
  fields[tableName2] = fromFields;
35523
35524
  }
35524
35525
  }
@@ -43617,6 +43618,7 @@ async function generateServerEntries(options, targetDir) {
43617
43618
  function appSetup() {
43618
43619
  return `import config from '../../manguito.config.js'
43619
43620
  import { schemaRegistry } from './schema-registry.js'
43621
+ import { versionModel } from './version-model.js'
43620
43622
  import { programmaticResolvers } from './programmatic-registry.js'
43621
43623
  import { createCmsApp } from '@bobbykim/manguito-cms-api'
43622
43624
  import { createPostgresAdapter } from '@bobbykim/manguito-cms-db'
@@ -43627,6 +43629,7 @@ await dbAdapter.connect()
43627
43629
  const { app } = createCmsApp({
43628
43630
  name: config.name,
43629
43631
  registry: schemaRegistry,
43632
+ versions: versionModel,
43630
43633
  db: dbAdapter.getDb(),
43631
43634
  storage: config.storage,
43632
43635
  prefix: config.api.prefix,
@@ -43820,6 +43823,25 @@ ${entries},
43820
43823
  await writeFile5(join7(targetDir, "programmatic-registry.ts"), body, "utf8");
43821
43824
  }
43822
43825
 
43826
+ // src/codegen/version-model.ts
43827
+ init_esm_shims();
43828
+ import { writeFile as writeFile6 } from "fs/promises";
43829
+ import { join as join8 } from "path";
43830
+ function reduceVersionModel(model) {
43831
+ return { current: model.current, live: model.live, projections: model.projections };
43832
+ }
43833
+ async function generateVersionModel(model, targetDir) {
43834
+ const reduced = reduceVersionModel(model);
43835
+ const content = [
43836
+ "// GENERATED by manguito build \u2014 do not edit.",
43837
+ "import type { BakedVersionModel } from '@bobbykim/manguito-cms-api'",
43838
+ "",
43839
+ `export const versionModel: BakedVersionModel = ${JSON.stringify(reduced, null, 2)}`,
43840
+ ""
43841
+ ].join("\n");
43842
+ await writeFile6(join8(targetDir, "version-model.ts"), content, "utf8");
43843
+ }
43844
+
43823
43845
  // src/utils/config.ts
43824
43846
  init_esm_shims();
43825
43847
  import { existsSync } from "fs";
@@ -43910,6 +43932,16 @@ function isValidConfig(value) {
43910
43932
  return value !== null && typeof value === "object" && "db" in value && "storage" in value && "server" in value && "api" in value && "admin" in value;
43911
43933
  }
43912
43934
 
43935
+ // src/utils/schema-config.ts
43936
+ init_esm_shims();
43937
+ import { resolve as resolve3 } from "path";
43938
+ function resolveSchemaConfig(cwd, config3) {
43939
+ return {
43940
+ base_path: resolve3(cwd, config3.schema.base_path),
43941
+ folders: config3.schema.folders
43942
+ };
43943
+ }
43944
+
43913
43945
  // src/utils/env.ts
43914
43946
  init_esm_shims();
43915
43947
  import { existsSync as existsSync2 } from "fs";
@@ -43951,7 +43983,7 @@ async function runBuild(options, deps) {
43951
43983
  }
43952
43984
  }
43953
43985
  }
43954
- const rolesPath = resolve3(cwd, config3.schema.base_path, "roles.json");
43986
+ const rolesPath = resolve4(cwd, config3.schema.base_path, "roles.json");
43955
43987
  const rolesLoadResult = loadSchemaFile(rolesPath);
43956
43988
  let parsedRoles = null;
43957
43989
  if (!rolesLoadResult.ok) {
@@ -43964,7 +43996,7 @@ async function runBuild(options, deps) {
43964
43996
  parsedRoles = rolesParseResult.value;
43965
43997
  }
43966
43998
  }
43967
- const routesPath = resolve3(cwd, config3.schema.base_path, "routes.json");
43999
+ const routesPath = resolve4(cwd, config3.schema.base_path, "routes.json");
43968
44000
  const routesLoadResult = loadSchemaFile(routesPath);
43969
44001
  let parsedRoutesDef = null;
43970
44002
  if (!routesLoadResult.ok) {
@@ -43995,13 +44027,26 @@ async function runBuild(options, deps) {
43995
44027
  printSuccess(
43996
44028
  `Schemas parsed (${contentCount} content types, ${paragraphCount} paragraph types, ${taxonomyCount} taxonomy types${enumPart})`
43997
44029
  );
43998
- const generatedDir = resolve3(cwd, "dist/generated");
44030
+ const generatedDir = resolve4(cwd, "dist/generated");
43999
44031
  mkdirSync(generatedDir, { recursive: true });
44000
- writeFileSync(join8(generatedDir, "schema.ts"), generateSchemaFile(registry2), "utf8");
44032
+ writeFileSync(join9(generatedDir, "schema.ts"), generateSchemaFile(registry2), "utf8");
44001
44033
  await generateSchemaRegistry(registry2, generatedDir);
44002
44034
  await generateRoutes2(registry2, generatedDir);
44003
- await generateForms(registry2, join8(generatedDir, "forms"));
44004
- const programmaticDir = resolve3(cwd, config3.programmatic.dir);
44035
+ await generateForms(registry2, join9(generatedDir, "forms"));
44036
+ const schema = resolveSchemaConfig(cwd, config3);
44037
+ const snapshots = loadVersionSnapshots(schema, registry2);
44038
+ if (!snapshots.ok) {
44039
+ printValidationErrors(snapshots.errors, "Snapshot errors", "manguito build");
44040
+ process.exit(1);
44041
+ }
44042
+ const versionModel = computeVersionModel({ current: registry2, snapshots: snapshots.value });
44043
+ if (!versionModel.ok) {
44044
+ printValidationErrors(versionModel.errors, "Version model errors", "manguito build");
44045
+ process.exit(1);
44046
+ }
44047
+ await generateVersionModel(versionModel.value, generatedDir);
44048
+ printSuccess(`Version model baked (live: ${versionModel.value.live.join(", ")})`);
44049
+ const programmaticDir = resolve4(cwd, config3.programmatic.dir);
44005
44050
  const resolverFiles = existsSync3(programmaticDir) ? walkFiles(programmaticDir).filter((f) => /\.(ts|mjs|js)$/.test(f)) : [];
44006
44051
  await generateProgrammaticRegistry(resolverFiles, generatedDir);
44007
44052
  await generateServerEntries(
@@ -44014,7 +44059,7 @@ async function runBuild(options, deps) {
44014
44059
  root: resolveAdminRoot(cwd),
44015
44060
  base: config3.admin.prefix + "/",
44016
44061
  build: {
44017
- outDir: resolve3(cwd, "dist/admin"),
44062
+ outDir: resolve4(cwd, "dist/admin"),
44018
44063
  emptyOutDir: true
44019
44064
  },
44020
44065
  define: {
@@ -44034,12 +44079,12 @@ async function runBuild(options, deps) {
44034
44079
  try {
44035
44080
  await tsupBuild({
44036
44081
  entry: {
44037
- server: resolve3(cwd, "dist/generated/server.ts"),
44038
- handler: resolve3(cwd, "dist/generated/handler.ts"),
44039
- vercel: resolve3(cwd, "dist/generated/vercel.ts")
44082
+ server: resolve4(cwd, "dist/generated/server.ts"),
44083
+ handler: resolve4(cwd, "dist/generated/handler.ts"),
44084
+ vercel: resolve4(cwd, "dist/generated/vercel.ts")
44040
44085
  },
44041
44086
  format: ["esm"],
44042
- outDir: resolve3(cwd, "dist"),
44087
+ outDir: resolve4(cwd, "dist"),
44043
44088
  dts: false,
44044
44089
  silent: true,
44045
44090
  // hono/aws-lambda and hono/vercel are optional peer deps — mark external
@@ -44057,24 +44102,24 @@ async function runBuild(options, deps) {
44057
44102
  process.stdout.write("\nBuild complete \u2192 dist/\n");
44058
44103
  }
44059
44104
  async function needsRebuild(cwd) {
44060
- const sentinel = resolve3(cwd, "dist/generated/schema.ts");
44105
+ const sentinel = resolve4(cwd, "dist/generated/schema.ts");
44061
44106
  if (!existsSync3(sentinel)) return true;
44062
44107
  const sentinelMtime = statSync2(sentinel).mtimeMs;
44063
- const schemasDir = resolve3(cwd, "schemas");
44108
+ const schemasDir = resolve4(cwd, "schemas");
44064
44109
  if (!existsSync3(schemasDir)) return false;
44065
44110
  return walkFiles(schemasDir).some(
44066
44111
  (file2) => statSync2(file2).mtimeMs > sentinelMtime
44067
44112
  );
44068
44113
  }
44069
44114
  function resolveAdminRoot(cwd) {
44070
- const cwdRequire = createRequire(join8(cwd, "package.json"));
44115
+ const cwdRequire = createRequire(join9(cwd, "package.json"));
44071
44116
  const adminPkgJson = cwdRequire.resolve("@bobbykim/manguito-cms-admin/package.json");
44072
44117
  return dirname(adminPkgJson);
44073
44118
  }
44074
44119
  function walkFiles(dir) {
44075
44120
  const files = [];
44076
44121
  for (const entry of readdirSync3(dir, { withFileTypes: true })) {
44077
- const full = join8(dir, entry.name);
44122
+ const full = join9(dir, entry.name);
44078
44123
  if (entry.isDirectory()) {
44079
44124
  files.push(...walkFiles(full));
44080
44125
  } else {
@@ -44088,8 +44133,8 @@ function walkFiles(dir) {
44088
44133
  init_esm_shims();
44089
44134
  import { createServer as createHttpServer } from "http";
44090
44135
  import { existsSync as existsSync5 } from "fs";
44091
- import { resolve as resolve6, join as join12, dirname as dirname2, extname as extname4 } from "path";
44092
- import { mkdir as mkdir2, writeFile as writeFile8, watch, readFile } from "fs/promises";
44136
+ import { resolve as resolve7, join as join13, dirname as dirname2, extname as extname4 } from "path";
44137
+ import { mkdir as mkdir2, writeFile as writeFile9, watch, readFile } from "fs/promises";
44093
44138
  import { createRequire as createRequire2 } from "module";
44094
44139
  import { createServer as createViteServer, searchForWorkspaceRoot } from "vite";
44095
44140
 
@@ -47009,15 +47054,20 @@ function normalizePrefix(prefix) {
47009
47054
  }
47010
47055
  function createPublicPaths(prefix) {
47011
47056
  return {
47012
- collection: (basePath) => `${prefix}/${basePath}`,
47013
- item: (basePath) => `${prefix}/${basePath}/:slug`,
47014
- taxonomyCollection: (typeName) => `${prefix}/taxonomy/${typeName}`,
47015
- taxonomyItem: (typeName) => `${prefix}/taxonomy/${typeName}/:id`,
47016
47057
  mediaCollection: () => `${prefix}/media`,
47017
47058
  mediaItem: () => `${prefix}/media/:id`,
47018
47059
  openapi: () => `${prefix}/openapi.json`
47019
47060
  };
47020
47061
  }
47062
+ function createVersionedPaths(prefix, version3) {
47063
+ const root = version3 === null ? prefix : `${prefix}/${version3}`;
47064
+ return {
47065
+ collection: (basePath) => `${root}/${basePath}`,
47066
+ item: (basePath) => `${root}/${basePath}/:slug`,
47067
+ taxonomyCollection: (typeName) => `${root}/taxonomy/${typeName}`,
47068
+ taxonomyItem: (typeName) => `${root}/taxonomy/${typeName}/:id`
47069
+ };
47070
+ }
47021
47071
  function createCorsMiddleware(corsConfig) {
47022
47072
  const allowList = Array.isArray(corsConfig.origin) ? corsConfig.origin : [corsConfig.origin];
47023
47073
  const wildcard = allowList.includes("*");
@@ -47079,7 +47129,7 @@ function createSecurityHeadersMiddleware(options = {}) {
47079
47129
  c.res.headers.set("X-Content-Type-Options", "nosniff");
47080
47130
  c.res.headers.set("X-Frame-Options", "DENY");
47081
47131
  c.res.headers.set("Referrer-Policy", "no-referrer");
47082
- const isGraphiql = graphiqlPath !== void 0 && c.req.path === graphiqlPath;
47132
+ const isGraphiql = graphiqlPath !== void 0 && (c.req.path === graphiqlPath || c.req.path.startsWith(`${graphiqlPath}/`));
47083
47133
  c.res.headers.set("Content-Security-Policy", isGraphiql ? graphiqlCsp : csp);
47084
47134
  };
47085
47135
  }
@@ -47579,7 +47629,7 @@ function nestedTargets(fields) {
47579
47629
  }
47580
47630
  return out;
47581
47631
  }
47582
- function buildProjectors(registry2, fieldKeyMaps) {
47632
+ function buildProjectors(registry2, fieldKeyMaps, fallbacks) {
47583
47633
  const projectors = {};
47584
47634
  const sources = [
47585
47635
  registry2.content_types,
@@ -47590,7 +47640,14 @@ function buildProjectors(registry2, fieldKeyMaps) {
47590
47640
  for (const [typeName, type] of Object.entries(source)) {
47591
47641
  const map2 = fieldKeyMaps[typeName];
47592
47642
  if (!map2) continue;
47593
- projectors[typeName] = { map: map2, nested: nestedTargets(type.fields) };
47643
+ const typeFallbacks = fallbacks?.[typeName];
47644
+ projectors[typeName] = {
47645
+ map: map2,
47646
+ nested: nestedTargets(type.fields),
47647
+ // Spread rather than assign: with exactOptionalPropertyTypes, an optional
47648
+ // property may be ABSENT but not present-and-undefined.
47649
+ ...typeFallbacks !== void 0 && { fallbacks: typeFallbacks }
47650
+ };
47594
47651
  }
47595
47652
  }
47596
47653
  return projectors;
@@ -47599,6 +47656,11 @@ function projectRow(row, typeName, projectors) {
47599
47656
  const p2 = projectors[typeName];
47600
47657
  if (!p2) return row;
47601
47658
  const out = p2.map.toLabels(row);
47659
+ if (p2.fallbacks !== void 0) {
47660
+ for (const [label, value] of Object.entries(p2.fallbacks)) {
47661
+ if (out[label] === null || out[label] === void 0) out[label] = value;
47662
+ }
47663
+ }
47602
47664
  for (const { label, target } of p2.nested) {
47603
47665
  const v2 = out[label];
47604
47666
  if (v2 === null || v2 === void 0) continue;
@@ -47688,13 +47750,24 @@ function registerPublicContentRoutes(app, registry2, repos, projectors, paths, l
47688
47750
  const basePath = contentType.default_base_path;
47689
47751
  const repo = repos[typeName];
47690
47752
  if (!repo) continue;
47691
- const relationFieldNames = new Set(
47692
- contentType.fields.filter((f) => RELATION_FIELD_TYPES.has(f.field_type)).map((f) => f.name)
47693
- );
47753
+ const fieldKeys = projectors[typeName].map;
47754
+ const relationFieldNames = /* @__PURE__ */ new Set();
47755
+ for (const f of contentType.fields) {
47756
+ if (!RELATION_FIELD_TYPES.has(f.field_type)) continue;
47757
+ if (isColumnBacked(f)) {
47758
+ const label = fieldKeys.labelFor(f.db_column.column_name);
47759
+ if (label !== void 0) relationFieldNames.add(label);
47760
+ } else {
47761
+ relationFieldNames.add(f.name);
47762
+ }
47763
+ }
47694
47764
  const filterableFieldNames = /* @__PURE__ */ new Set([
47695
- ...contentType.fields.filter((f) => f.field_type !== "programmatic").map((f) => f.name),
47765
+ ...fieldKeys.labels,
47696
47766
  ...contentType.system_fields.map((f) => f.name)
47697
47767
  ]);
47768
+ const sortableFieldNames = new Set(
47769
+ [...SORTABLE_FIELDS].filter((f) => filterableFieldNames.has(f))
47770
+ );
47698
47771
  if (contentType.only_one) {
47699
47772
  app.get(paths.collection(basePath), async (c) => {
47700
47773
  const result = await repo.findMany({ published_only: true, page: 1, per_page: 1 });
@@ -47710,8 +47783,6 @@ function registerPublicContentRoutes(app, registry2, repos, projectors, paths, l
47710
47783
  });
47711
47784
  } else {
47712
47785
  registerListRoute(paths.collection(basePath), async (c) => {
47713
- const projector = projectors[typeName];
47714
- const fieldKeys = projector.map;
47715
47786
  const pagination = parsePagination(c.req.query("page"), c.req.query("per_page"));
47716
47787
  if (!pagination.ok) {
47717
47788
  return c.json(
@@ -47726,13 +47797,13 @@ function registerPublicContentRoutes(app, registry2, repos, projectors, paths, l
47726
47797
  );
47727
47798
  }
47728
47799
  const sortBy = c.req.query("sort_by") ?? "created_at";
47729
- if (!SORTABLE_FIELDS.has(sortBy)) {
47800
+ if (!sortableFieldNames.has(sortBy)) {
47730
47801
  return c.json(
47731
47802
  {
47732
47803
  ok: false,
47733
47804
  error: {
47734
47805
  code: "INVALID_SORT_FIELD",
47735
- message: `'${sortBy}' is not sortable. Allowed: title, created_at, updated_at`
47806
+ message: `'${sortBy}' is not sortable. Allowed: ${[...sortableFieldNames].join(", ")}`
47736
47807
  }
47737
47808
  },
47738
47809
  400
@@ -47779,7 +47850,7 @@ function registerPublicContentRoutes(app, registry2, repos, projectors, paths, l
47779
47850
  );
47780
47851
  }
47781
47852
  }
47782
- const sortColumn = fieldKeys.columnFor(sortBy) ?? sortBy;
47853
+ const sortColumn = fieldKeys.labels.includes(sortBy) ? fieldKeys.columnFor(sortBy) : sortBy;
47783
47854
  const result = await repo.findMany({
47784
47855
  published_only: true,
47785
47856
  page: pagination.page,
@@ -49631,7 +49702,7 @@ function registerUserRoutes(app, db, requirePermission, requireHierarchy) {
49631
49702
  }
49632
49703
  var API_VERSION = (() => {
49633
49704
  try {
49634
- return "0.4.3";
49705
+ return "0.6.0";
49635
49706
  } catch {
49636
49707
  return "0.0.0";
49637
49708
  }
@@ -50020,6 +50091,90 @@ function createMediaRepository(db) {
50020
50091
  }
50021
50092
  };
50022
50093
  }
50094
+ var VERSION_SEGMENT = /^v\d+$/;
50095
+ function versionNumber2(version3) {
50096
+ return Number.parseInt(version3.slice(1), 10);
50097
+ }
50098
+ function classifyVersion(segment, model) {
50099
+ if (!VERSION_SEGMENT.test(segment)) return "not-a-version";
50100
+ if (model.live.includes(segment)) return "live";
50101
+ return versionNumber2(segment) < versionNumber2(model.current) ? "retired" : "unknown";
50102
+ }
50103
+ function fallbacksFor(projection) {
50104
+ const out = {};
50105
+ for (const [typeName, type] of Object.entries(projection.types)) {
50106
+ const perLabel = {};
50107
+ for (const f of type.fields) {
50108
+ if (f.fallback !== void 0) perLabel[f.exposed_as] = f.fallback;
50109
+ }
50110
+ if (Object.keys(perLabel).length > 0) out[typeName] = perLabel;
50111
+ }
50112
+ return out;
50113
+ }
50114
+ function buildVersionSurface(input2) {
50115
+ const { version: version3, projectionVersion, prefix, registry: registry2, model, makeRepo } = input2;
50116
+ const paths = createVersionedPaths(prefix, version3);
50117
+ const projection = model.projections[projectionVersion];
50118
+ const fieldKeyMaps = {};
50119
+ const sources = [
50120
+ registry2.content_types,
50121
+ registry2.taxonomy_types,
50122
+ registry2.paragraph_types
50123
+ ];
50124
+ for (const source of sources) {
50125
+ for (const [typeName, type] of Object.entries(source)) {
50126
+ const projectionType = projection?.types[typeName];
50127
+ fieldKeyMaps[typeName] = projectionType ? createFieldKeyMapFromProjection(projectionType, type.fields) : createFieldKeyMap(type.fields);
50128
+ }
50129
+ }
50130
+ const fallbacks = projection ? fallbacksFor(projection) : void 0;
50131
+ const projectors = buildProjectors(registry2, fieldKeyMaps, fallbacks);
50132
+ const sortableColumnsFor = (typeName) => {
50133
+ const map2 = fieldKeyMaps[typeName];
50134
+ const type = registry2.content_types[typeName] ?? registry2.taxonomy_types[typeName];
50135
+ const systemFieldNames = new Set((type?.system_fields ?? []).map((f) => f.name));
50136
+ const out = /* @__PURE__ */ new Set();
50137
+ for (const label of SORTABLE_FIELDS) {
50138
+ const column = map2.columnFor(label);
50139
+ if (column !== void 0) out.add(column);
50140
+ else if (systemFieldNames.has(label)) out.add(label);
50141
+ }
50142
+ return out;
50143
+ };
50144
+ const repos = {};
50145
+ for (const [typeName, ct2] of Object.entries(registry2.content_types)) {
50146
+ repos[typeName] = makeRepo(typeName, ct2.db.table_name, sortableColumnsFor(typeName));
50147
+ }
50148
+ for (const [typeName, tt2] of Object.entries(registry2.taxonomy_types)) {
50149
+ repos[typeName] = makeRepo(typeName, tt2.db.table_name, sortableColumnsFor(typeName));
50150
+ }
50151
+ return { paths, fieldKeyMaps, projectors, repos };
50152
+ }
50153
+ function deprecationHeaders(input2) {
50154
+ const { requested, model, successor } = input2;
50155
+ const link = `<${successor}>; rel="successor-version"`;
50156
+ if (requested === null) {
50157
+ if (model.live.length <= 1) return null;
50158
+ return {
50159
+ Deprecation: "true",
50160
+ Link: link,
50161
+ Warning: `299 - "Unversioned path resolves to the latest version (${model.current}) and will change when a new version is cut. Pin a version."`
50162
+ };
50163
+ }
50164
+ if (requested === model.current) return null;
50165
+ return { Deprecation: "true", Link: link };
50166
+ }
50167
+ function versionRoutePaths(registry2, paths) {
50168
+ const out = [paths.collection("content"), paths.collection("taxonomy")];
50169
+ for (const ct2 of Object.values(registry2.content_types)) {
50170
+ out.push(paths.collection(ct2.default_base_path));
50171
+ if (!ct2.only_one) out.push(paths.item(ct2.default_base_path));
50172
+ }
50173
+ for (const tt2 of Object.values(registry2.taxonomy_types)) {
50174
+ out.push(paths.taxonomyCollection(tt2.name), paths.taxonomyItem(tt2.name));
50175
+ }
50176
+ return out;
50177
+ }
50023
50178
  var MISSING_STORAGE_ERROR = `\u2717 api.storage is required but not configured.
50024
50179
  Add a storage adapter to your manguito.config.ts:
50025
50180
 
@@ -50035,6 +50190,7 @@ function createCmsApp(options) {
50035
50190
  }
50036
50191
  const prefix = normalizePrefix(options.prefix);
50037
50192
  const publicPaths = createPublicPaths(prefix);
50193
+ const versionedPaths = createVersionedPaths(prefix, null);
50038
50194
  const { storage, registry: registry2, db, rateLimit, media, cors } = options;
50039
50195
  const cmsName = options.name ?? "Manguito CMS";
50040
50196
  const maxFileSize = media?.max_file_size;
@@ -50083,9 +50239,18 @@ function createCmsApp(options) {
50083
50239
  ])
50084
50240
  ]);
50085
50241
  const projectors = buildProjectors(registry2, fieldKeyMaps);
50086
- const sortableColumnsFor = (typeName) => new Set(
50087
- [...SORTABLE_FIELDS].map((label) => fieldKeyMaps[typeName].columnFor(label) ?? label)
50088
- );
50242
+ const sortableColumnsFor = (typeName) => {
50243
+ const map2 = fieldKeyMaps[typeName];
50244
+ const type = registry2.content_types[typeName] ?? registry2.taxonomy_types[typeName];
50245
+ const systemFieldNames = new Set((type?.system_fields ?? []).map((f) => f.name));
50246
+ const out = /* @__PURE__ */ new Set();
50247
+ for (const label of SORTABLE_FIELDS) {
50248
+ const column = map2.columnFor(label);
50249
+ if (column !== void 0) out.add(column);
50250
+ else if (systemFieldNames.has(label)) out.add(label);
50251
+ }
50252
+ return out;
50253
+ };
50089
50254
  const contentRepos = Object.fromEntries(
50090
50255
  Object.entries(registry2.content_types).map(([typeName, ct2]) => [
50091
50256
  typeName,
@@ -50104,41 +50269,14 @@ function createCmsApp(options) {
50104
50269
  );
50105
50270
  const repos = { ...contentRepos, ...taxonomyRepos };
50106
50271
  const mediaRepo = createMediaRepository(db);
50107
- const publicContentRepos = Object.fromEntries(
50108
- Object.entries(registry2.content_types).map(([typeName, ct2]) => [
50109
- typeName,
50110
- createDrizzleContentRepository(db, ct2.db.table_name, {
50111
- relations: buildRelationsMap(ct2.fields, registry2),
50112
- publishedRelations: true,
50113
- sortableColumns: sortableColumnsFor(typeName)
50114
- })
50115
- ])
50116
- );
50117
- const publicTaxonomyRepos = Object.fromEntries(
50118
- Object.entries(registry2.taxonomy_types).map(([typeName, tt2]) => [
50119
- typeName,
50120
- createDrizzleContentRepository(db, tt2.db.table_name, {
50121
- relations: buildRelationsMap(tt2.fields, registry2),
50122
- publishedRelations: true,
50123
- sortableColumns: sortableColumnsFor(typeName)
50124
- })
50125
- ])
50126
- );
50127
- const publicRepos = { ...publicContentRepos, ...publicTaxonomyRepos };
50128
- const graphqlRepos = Object.fromEntries([
50129
- ...Object.entries(registry2.content_types).map(([typeName, ct2]) => [
50130
- typeName,
50131
- createDrizzleContentRepository(db, ct2.db.table_name, {
50132
- sortableColumns: sortableColumnsFor(typeName)
50133
- })
50134
- ]),
50135
- ...Object.entries(registry2.taxonomy_types).map(([typeName, tt2]) => [
50136
- typeName,
50137
- createDrizzleContentRepository(db, tt2.db.table_name, {
50138
- sortableColumns: sortableColumnsFor(typeName)
50139
- })
50140
- ])
50141
- ]);
50272
+ const makeRepo = (typeName, tableName, sortableColumns) => {
50273
+ const type = registry2.content_types[typeName] ?? registry2.taxonomy_types[typeName];
50274
+ return createDrizzleContentRepository(db, tableName, {
50275
+ relations: buildRelationsMap(type.fields, registry2),
50276
+ publishedRelations: true,
50277
+ sortableColumns
50278
+ });
50279
+ };
50142
50280
  registerAuthRoutes(app, db);
50143
50281
  app.use("/admin/api/*", authMiddleware);
50144
50282
  app.use("/admin/api/*", mustChangePasswordCheck);
@@ -50147,17 +50285,17 @@ function createCmsApp(options) {
50147
50285
  for (const ct2 of Object.values(registry2.content_types)) {
50148
50286
  const base = ct2.default_base_path;
50149
50287
  if (ct2.only_one) {
50150
- paths[publicPaths.collection(base)] = { get: { summary: `Get ${ct2.label}`, tags: [ct2.label] } };
50288
+ paths[versionedPaths.collection(base)] = { get: { summary: `Get ${ct2.label}`, tags: [ct2.label] } };
50151
50289
  } else {
50152
- paths[publicPaths.collection(base)] = { get: { summary: `List published ${ct2.label}`, tags: [ct2.label] } };
50153
- paths[publicPaths.item(base).replace(":slug", "{slug}")] = {
50290
+ paths[versionedPaths.collection(base)] = { get: { summary: `List published ${ct2.label}`, tags: [ct2.label] } };
50291
+ paths[versionedPaths.item(base).replace(":slug", "{slug}")] = {
50154
50292
  get: { summary: `Get ${ct2.label} by slug`, tags: [ct2.label] }
50155
50293
  };
50156
50294
  }
50157
50295
  }
50158
50296
  for (const tt2 of Object.values(registry2.taxonomy_types)) {
50159
- paths[publicPaths.taxonomyCollection(tt2.name)] = { get: { summary: `List published ${tt2.label}`, tags: [tt2.label] } };
50160
- paths[publicPaths.taxonomyItem(tt2.name).replace(":id", "{id}")] = {
50297
+ paths[versionedPaths.taxonomyCollection(tt2.name)] = { get: { summary: `List published ${tt2.label}`, tags: [tt2.label] } };
50298
+ paths[versionedPaths.taxonomyItem(tt2.name).replace(":id", "{id}")] = {
50161
50299
  get: { summary: `Get ${tt2.label} by id`, tags: [tt2.label] }
50162
50300
  };
50163
50301
  }
@@ -50177,35 +50315,166 @@ function createCmsApp(options) {
50177
50315
  paths: {}
50178
50316
  })
50179
50317
  );
50180
- registerPublicContentRoutes(app, registry2, publicRepos, projectors, publicPaths, listRateLimit, programmaticResolver);
50318
+ const model = options.versions ?? {
50319
+ current: "v1",
50320
+ live: ["v1"],
50321
+ projections: {}
50322
+ };
50323
+ const passes = [
50324
+ ...options.versions !== void 0 ? model.live.map((v2) => ({ version: v2, projectionVersion: v2 })) : [],
50325
+ { version: null, projectionVersion: model.current }
50326
+ ];
50327
+ for (const pass of passes) {
50328
+ const surface = buildVersionSurface({ ...pass, prefix, registry: registry2, model, makeRepo });
50329
+ const headers = deprecationHeaders({
50330
+ requested: pass.version,
50331
+ model,
50332
+ // The version root, with no trailing slash — never
50333
+ // createVersionedPaths(prefix, model.current).collection(''), which
50334
+ // would yield `${prefix}/${model.current}/` in every Link header.
50335
+ successor: `${prefix}/${model.current}`
50336
+ });
50337
+ if (headers !== null) {
50338
+ const attachDeprecationHeaders = async (c, next) => {
50339
+ await next();
50340
+ for (const [key, value] of Object.entries(headers)) c.res.headers.set(key, value);
50341
+ };
50342
+ for (const path7 of versionRoutePaths(registry2, surface.paths)) {
50343
+ app.use(path7, attachDeprecationHeaders);
50344
+ }
50345
+ }
50346
+ registerPublicContentRoutes(app, registry2, surface.repos, surface.projectors, surface.paths, listRateLimit, programmaticResolver);
50347
+ }
50348
+ if (options.versions !== void 0) {
50349
+ app.all(`${prefix}/:version/*`, async (c, next) => {
50350
+ const kind = classifyVersion(c.req.param("version"), model);
50351
+ if (kind === "not-a-version" || kind === "live") return next();
50352
+ const live = model.live.join(", ");
50353
+ return kind === "retired" ? c.json(
50354
+ {
50355
+ ok: false,
50356
+ error: {
50357
+ code: "VERSION_RETIRED",
50358
+ message: `Version ${c.req.param("version")} is no longer served. Live versions: ${live}.`
50359
+ }
50360
+ },
50361
+ 410
50362
+ ) : c.json(
50363
+ {
50364
+ ok: false,
50365
+ error: {
50366
+ code: "VERSION_NOT_FOUND",
50367
+ message: `No version ${c.req.param("version")} is served. Live versions: ${live}.`
50368
+ }
50369
+ },
50370
+ 404
50371
+ );
50372
+ });
50373
+ }
50181
50374
  registerPublicMediaRoutes(app, mediaRepo, publicPaths, listRateLimit);
50182
50375
  if (options.graphql?.enabled) {
50183
50376
  const gqlOptions = options.graphql;
50184
- let gqlHandler = null;
50185
- let initError = null;
50186
- const ready = import("./handler-45O5OX2C-OIEJ52PN.js").then(({ createGraphQLHandler }) => {
50187
- gqlHandler = createGraphQLHandler(registry2, graphqlRepos, fieldKeyMaps, programmaticResolver, db, gqlOptions);
50377
+ const makeGraphqlRepo = (_typeName, tableName, sortableColumns) => createDrizzleContentRepository(db, tableName, { sortableColumns });
50378
+ const gqlPasses = [
50379
+ ...options.versions !== void 0 ? model.live.map((v2) => ({ version: v2, projectionVersion: v2 })) : [],
50380
+ { version: null, projectionVersion: model.current }
50381
+ ];
50382
+ const mounts = gqlPasses.map((pass) => ({
50383
+ path: pass.version === null ? "/graphql" : `/graphql/${pass.version}`,
50384
+ version: pass.version,
50385
+ handler: null,
50386
+ error: null
50387
+ }));
50388
+ const ready = import("./handler-XZVDLKZ2-2TIH4GG7.js").then(({ createGraphQLHandler }) => {
50389
+ gqlPasses.forEach((pass, i) => {
50390
+ const mount = mounts[i];
50391
+ try {
50392
+ const surface = buildVersionSurface({
50393
+ ...pass,
50394
+ prefix,
50395
+ registry: registry2,
50396
+ model,
50397
+ makeRepo: makeGraphqlRepo
50398
+ });
50399
+ const projection = model.projections[pass.projectionVersion];
50400
+ const currentProjection = model.projections[model.current];
50401
+ mount.handler = createGraphQLHandler(
50402
+ registry2,
50403
+ surface.repos,
50404
+ surface.fieldKeyMaps,
50405
+ programmaticResolver,
50406
+ db,
50407
+ gqlOptions,
50408
+ {
50409
+ endpoint: mount.path,
50410
+ currentVersion: model.current,
50411
+ ...projection !== void 0 && { projection },
50412
+ ...currentProjection !== void 0 && { currentProjection }
50413
+ }
50414
+ );
50415
+ } catch (err) {
50416
+ mount.error = err;
50417
+ const message = err instanceof Error ? err.message : String(err);
50418
+ process.stderr.write(
50419
+ `\u2717 GraphQL schema failed to initialize for ${mount.path}; it will return 500. ${message}
50420
+ `
50421
+ );
50422
+ }
50423
+ });
50188
50424
  }).catch((err) => {
50189
- initError = err;
50425
+ for (const m2 of mounts) m2.error = err;
50190
50426
  const message = err instanceof Error ? err.message : String(err);
50191
- process.stderr.write(`\u2717 GraphQL schema failed to initialize; /graphql will return 500. ${message}
50427
+ const paths = mounts.map((m2) => m2.path).join(", ");
50428
+ process.stderr.write(`\u2717 GraphQL module failed to load; ${paths} will return 500. ${message}
50192
50429
  `);
50193
50430
  });
50194
- const invokeGraphQL = async (c) => {
50195
- if (!gqlHandler && !initError) await ready;
50196
- if (!gqlHandler) {
50431
+ const invokeFor = (mount) => async (c) => {
50432
+ if (!mount.handler && !mount.error) await ready;
50433
+ if (!mount.handler) {
50197
50434
  return c.json(
50198
50435
  { ok: false, error: { code: "GRAPHQL_INIT_FAILED", message: "GraphQL schema failed to initialize" } },
50199
50436
  500
50200
50437
  );
50201
50438
  }
50202
- return gqlHandler(c, async () => {
50439
+ return mount.handler(c, async () => {
50203
50440
  });
50204
50441
  };
50205
- if (listRateLimit) {
50206
- app.all("/graphql", listRateLimit, invokeGraphQL);
50207
- } else {
50208
- app.all("/graphql", invokeGraphQL);
50442
+ for (const mount of mounts) {
50443
+ const headers = deprecationHeaders({
50444
+ requested: mount.version,
50445
+ model,
50446
+ successor: `/graphql/${model.current}`
50447
+ });
50448
+ if (headers === null) continue;
50449
+ const attach = async (c, next) => {
50450
+ await next();
50451
+ for (const [key, value] of Object.entries(headers)) c.res.headers.set(key, value);
50452
+ };
50453
+ app.use(mount.path, attach);
50454
+ }
50455
+ for (const mount of mounts) {
50456
+ if (listRateLimit) app.all(mount.path, listRateLimit, invokeFor(mount));
50457
+ else app.all(mount.path, invokeFor(mount));
50458
+ }
50459
+ if (options.versions !== void 0) {
50460
+ app.all("/graphql/:version", async (c, next) => {
50461
+ const segment = c.req.param("version");
50462
+ const kind = classifyVersion(segment, model);
50463
+ if (kind === "not-a-version" || kind === "live") return next();
50464
+ const live = model.live.join(", ");
50465
+ return c.json({
50466
+ errors: [
50467
+ {
50468
+ message: kind === "retired" ? `Version ${segment} is no longer served. Live versions: ${live}.` : `Version ${segment} does not exist. Live versions: ${live}.`,
50469
+ extensions: {
50470
+ code: kind === "retired" ? "VERSION_RETIRED" : "VERSION_UNKNOWN",
50471
+ current: model.current,
50472
+ live: model.live
50473
+ }
50474
+ }
50475
+ ]
50476
+ });
50477
+ });
50209
50478
  }
50210
50479
  }
50211
50480
  registerConfigRoute(
@@ -50231,12 +50500,12 @@ function createCmsApp(options) {
50231
50500
 
50232
50501
  // src/codegen/drizzle-config.ts
50233
50502
  init_esm_shims();
50234
- import { writeFile as writeFile6 } from "fs/promises";
50235
- import { join as join9, relative as relative2, resolve as resolve4 } from "path";
50503
+ import { writeFile as writeFile7 } from "fs/promises";
50504
+ import { join as join10, relative as relative2, resolve as resolve5 } from "path";
50236
50505
  async function generateDrizzleConfig(config3, targetDir) {
50237
50506
  const migrationsFolder = config3.migrations?.folder ?? "./migrations";
50238
50507
  const migrationsTable = config3.migrations?.table ?? "__manguito_migrations";
50239
- const outPath = relative2(resolve4(targetDir), resolve4(migrationsFolder)).replace(/\\/g, "/");
50508
+ const outPath = relative2(resolve5(targetDir), resolve5(migrationsFolder)).replace(/\\/g, "/");
50240
50509
  const content = [
50241
50510
  `import { defineConfig } from 'drizzle-kit'`,
50242
50511
  ``,
@@ -50254,13 +50523,13 @@ async function generateDrizzleConfig(config3, targetDir) {
50254
50523
  ` tablesFilter: ['!${migrationsTable}'],`,
50255
50524
  `})`
50256
50525
  ].join("\n");
50257
- await writeFile6(join9(targetDir, "drizzle.config.ts"), content, "utf8");
50526
+ await writeFile7(join10(targetDir, "drizzle.config.ts"), content, "utf8");
50258
50527
  }
50259
50528
 
50260
50529
  // src/codegen/nav.ts
50261
50530
  init_esm_shims();
50262
- import { writeFile as writeFile7 } from "fs/promises";
50263
- import { join as join10 } from "path";
50531
+ import { writeFile as writeFile8 } from "fs/promises";
50532
+ import { join as join11 } from "path";
50264
50533
  function schemaBasePath(name) {
50265
50534
  const segment = name.includes("--") ? name.slice(name.indexOf("--") + 2) : name;
50266
50535
  return segment.replace(/_/g, "-");
@@ -50278,7 +50547,7 @@ async function generateNav(registry2, targetDir) {
50278
50547
  })).sort((a2, b2) => a2.label.localeCompare(b2.label));
50279
50548
  const content = `export const nav = ${JSON.stringify({ contentTypes, taxonomyTypes }, null, 2)}
50280
50549
  `;
50281
- await writeFile7(join10(targetDir, "nav.ts"), content, "utf8");
50550
+ await writeFile8(join11(targetDir, "nav.ts"), content, "utf8");
50282
50551
  }
50283
50552
 
50284
50553
  // src/commands/dev-routing.ts
@@ -50323,7 +50592,7 @@ function createPromptAdapter() {
50323
50592
  init_esm_shims();
50324
50593
  import { existsSync as existsSync4 } from "fs";
50325
50594
  import { readdir } from "fs/promises";
50326
- import { resolve as resolve5, join as join11, extname as extname3 } from "path";
50595
+ import { resolve as resolve6, join as join12, extname as extname3 } from "path";
50327
50596
  import { pathToFileURL as pathToFileURL2 } from "url";
50328
50597
  var RESOLVER_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".mjs", ".js"]);
50329
50598
  function isProgrammaticFieldDefinition(v2) {
@@ -50332,7 +50601,7 @@ function isProgrammaticFieldDefinition(v2) {
50332
50601
  async function walkTsFiles(dir) {
50333
50602
  const out = [];
50334
50603
  for (const entry of await readdir(dir, { withFileTypes: true })) {
50335
- const full = join11(dir, entry.name);
50604
+ const full = join12(dir, entry.name);
50336
50605
  if (entry.isDirectory()) {
50337
50606
  out.push(...await walkTsFiles(full));
50338
50607
  } else if (RESOLVER_EXTENSIONS.has(extname3(entry.name))) {
@@ -50342,7 +50611,7 @@ async function walkTsFiles(dir) {
50342
50611
  return out;
50343
50612
  }
50344
50613
  async function loadProgrammaticResolvers(cwd, dir) {
50345
- const abs = resolve5(cwd, dir);
50614
+ const abs = resolve6(cwd, dir);
50346
50615
  const map2 = /* @__PURE__ */ new Map();
50347
50616
  if (!existsSync4(abs)) return map2;
50348
50617
  for (const file2 of (await walkTsFiles(abs)).sort()) {
@@ -50389,13 +50658,13 @@ async function runDev(options, deps) {
50389
50658
  printSuccess("Config loaded");
50390
50659
  const db = await connectDb(config3);
50391
50660
  printSuccess("Database connected");
50392
- const manguitoDir = resolve6(cwd, ".manguito");
50661
+ const manguitoDir = resolve7(cwd, ".manguito");
50393
50662
  await mkdir2(manguitoDir, { recursive: true });
50394
- const drizzleConfigPath = resolve6(manguitoDir, "drizzle.config.ts");
50663
+ const drizzleConfigPath = resolve7(manguitoDir, "drizzle.config.ts");
50395
50664
  const needsSeed = !await db.tableExists("users");
50396
50665
  const bootstrapRegistry = await parseAllSchemas(cwd, config3);
50397
50666
  await generateDrizzleConfig(config3, manguitoDir);
50398
- await writeFile8(join12(manguitoDir, "schema.ts"), generateSchemaFile(bootstrapRegistry), "utf8");
50667
+ await writeFile9(join13(manguitoDir, "schema.ts"), generateSchemaFile(bootstrapRegistry), "utf8");
50399
50668
  await runDevMigration(drizzleConfigPath);
50400
50669
  printSuccess(needsSeed ? "Schema tables created" : "Schema tables up to date");
50401
50670
  if (needsSeed) {
@@ -50441,8 +50710,20 @@ async function runDev(options, deps) {
50441
50710
  );
50442
50711
  await generateSchemaRegistry(registry2, manguitoDir);
50443
50712
  await generateRoutes2(registry2, manguitoDir);
50444
- await generateForms(registry2, join12(manguitoDir, "forms"));
50713
+ await generateForms(registry2, join13(manguitoDir, "forms"));
50445
50714
  await generateNav(registry2, manguitoDir);
50715
+ const schemaConfig = resolveSchemaConfig(cwd, config3);
50716
+ const snapshotsResult = loadVersionSnapshots(schemaConfig, registry2);
50717
+ if (!snapshotsResult.ok) {
50718
+ printValidationErrors(snapshotsResult.errors, "Version snapshot errors", "manguito version:diff");
50719
+ process.exit(1);
50720
+ }
50721
+ const versionModelResult = computeVersionModel({ current: registry2, snapshots: snapshotsResult.value });
50722
+ if (!versionModelResult.ok) {
50723
+ printValidationErrors(versionModelResult.errors, "Version model errors", "manguito version:diff");
50724
+ process.exit(1);
50725
+ }
50726
+ const versionModel = reduceVersionModel(versionModelResult.value);
50446
50727
  const resolverMap = await loadProgrammaticResolvers(cwd, config3.programmatic.dir);
50447
50728
  const adapter = createCmsApp({
50448
50729
  name: config3.name,
@@ -50450,6 +50731,7 @@ async function runDev(options, deps) {
50450
50731
  db: db.getDb(),
50451
50732
  storage: config3.storage,
50452
50733
  resolvers: resolverMap,
50734
+ versions: versionModel,
50453
50735
  ...config3.api.prefix ? { prefix: config3.api.prefix } : {},
50454
50736
  ...config3.api.media?.max_file_size ? { media: { max_file_size: config3.api.media.max_file_size } } : {},
50455
50737
  ...config3.api.rateLimit ? { rateLimit: config3.api.rateLimit } : {},
@@ -50482,8 +50764,8 @@ async function runDev(options, deps) {
50482
50764
  }
50483
50765
  if (url2.startsWith("/uploads/")) {
50484
50766
  const relativePath = (url2.slice("/uploads/".length).split("?")[0] ?? "").replace(/\.\./g, "");
50485
- const uploadsDir = resolve6(cwd, "uploads");
50486
- const filePath = resolve6(uploadsDir, relativePath);
50767
+ const uploadsDir = resolve7(cwd, "uploads");
50768
+ const filePath = resolve7(uploadsDir, relativePath);
50487
50769
  if (!filePath.startsWith(uploadsDir + "/") && filePath !== uploadsDir) {
50488
50770
  res.statusCode = 403;
50489
50771
  res.end("Forbidden");
@@ -50524,7 +50806,7 @@ async function runDev(options, deps) {
50524
50806
  printSuccess(`Dev server running at http://localhost:${port}`);
50525
50807
  printSuccess(`Admin panel at http://localhost:${port}${config3.admin.prefix}`);
50526
50808
  });
50527
- const schemasDir = resolve6(cwd, config3.schema.base_path);
50809
+ const schemasDir = resolve7(cwd, config3.schema.base_path);
50528
50810
  const watcher = watch(schemasDir, { recursive: true });
50529
50811
  void (async () => {
50530
50812
  for await (const event of watcher) {
@@ -50543,7 +50825,7 @@ async function runDev(options, deps) {
50543
50825
  }
50544
50826
  }
50545
50827
  })();
50546
- const programmaticDir = resolve6(cwd, config3.programmatic.dir);
50828
+ const programmaticDir = resolve7(cwd, config3.programmatic.dir);
50547
50829
  if (existsSync5(programmaticDir)) {
50548
50830
  const progWatcher = watch(programmaticDir, { recursive: true });
50549
50831
  void (async () => {
@@ -50575,11 +50857,25 @@ async function onSchemaFileChange(args) {
50575
50857
  return;
50576
50858
  }
50577
50859
  await generateDrizzleConfig(config3, manguitoDir);
50578
- await writeFile8(join12(manguitoDir, "schema.ts"), generateSchemaFile(registry2), "utf8");
50860
+ await writeFile9(join13(manguitoDir, "schema.ts"), generateSchemaFile(registry2), "utf8");
50579
50861
  await generateSchemaRegistry(registry2, manguitoDir);
50580
50862
  await generateRoutes2(registry2, manguitoDir);
50581
- await generateForms(registry2, join12(manguitoDir, "forms"));
50863
+ await generateForms(registry2, join13(manguitoDir, "forms"));
50582
50864
  await generateNav(registry2, manguitoDir);
50865
+ const schemaConfig = resolveSchemaConfig(cwd, config3);
50866
+ const snapshotsResult = loadVersionSnapshots(schemaConfig, registry2);
50867
+ if (!snapshotsResult.ok) {
50868
+ printValidationErrors(snapshotsResult.errors, "Version snapshot errors", "manguito version:diff");
50869
+ process.stderr.write("\u26A0 Changes not applied.\n");
50870
+ return;
50871
+ }
50872
+ const versionModelResult = computeVersionModel({ current: registry2, snapshots: snapshotsResult.value });
50873
+ if (!versionModelResult.ok) {
50874
+ printValidationErrors(versionModelResult.errors, "Version model errors", "manguito version:diff");
50875
+ process.stderr.write("\u26A0 Changes not applied.\n");
50876
+ return;
50877
+ }
50878
+ const versionModel = reduceVersionModel(versionModelResult.value);
50583
50879
  try {
50584
50880
  await runDevMigration(drizzleConfigPath);
50585
50881
  } catch (err) {
@@ -50594,6 +50890,7 @@ async function onSchemaFileChange(args) {
50594
50890
  db: db.getDb(),
50595
50891
  storage: config3.storage,
50596
50892
  resolvers: resolverMap,
50893
+ versions: versionModel,
50597
50894
  ...config3.api.prefix ? { prefix: config3.api.prefix } : {},
50598
50895
  ...config3.api.media?.max_file_size ? { media: { max_file_size: config3.api.media.max_file_size } } : {},
50599
50896
  ...config3.api.rateLimit ? { rateLimit: config3.api.rateLimit } : {},
@@ -50618,7 +50915,7 @@ async function parseAllSchemas(cwd, config3) {
50618
50915
  }
50619
50916
  parsedSchemas.push(parseResult.schema);
50620
50917
  }
50621
- const rolesPath = resolve6(cwd, config3.schema.base_path, "roles.json");
50918
+ const rolesPath = resolve7(cwd, config3.schema.base_path, "roles.json");
50622
50919
  const rolesLoad = loadSchemaFile(rolesPath);
50623
50920
  if (!rolesLoad.ok) {
50624
50921
  printGuidedError("Could not read roles.json.");
@@ -50629,7 +50926,7 @@ async function parseAllSchemas(cwd, config3) {
50629
50926
  printGuidedError("roles.json parse errors \u2014 run `manguito validate` for details.");
50630
50927
  process.exit(1);
50631
50928
  }
50632
- const routesPath = resolve6(cwd, config3.schema.base_path, "routes.json");
50929
+ const routesPath = resolve7(cwd, config3.schema.base_path, "routes.json");
50633
50930
  const routesLoad = loadSchemaFile(routesPath);
50634
50931
  if (!routesLoad.ok) {
50635
50932
  printGuidedError("Could not read routes.json.");
@@ -50643,7 +50940,7 @@ async function parseAllSchemas(cwd, config3) {
50643
50940
  return buildSchemaRegistry(parsedSchemas, routesResult.value, rolesResult.value);
50644
50941
  }
50645
50942
  function resolveAdminRoot2(cwd) {
50646
- const cwdRequire = createRequire2(join12(cwd, "package.json"));
50943
+ const cwdRequire = createRequire2(join13(cwd, "package.json"));
50647
50944
  const adminPkgJson = cwdRequire.resolve("@bobbykim/manguito-cms-admin/package.json");
50648
50945
  return dirname2(adminPkgJson);
50649
50946
  }
@@ -50689,7 +50986,7 @@ async function bridgeToHono(req, res, honoFetch) {
50689
50986
 
50690
50987
  // src/commands/start.ts
50691
50988
  init_esm_shims();
50692
- import { resolve as resolve7 } from "path";
50989
+ import { resolve as resolve8 } from "path";
50693
50990
  import { pathToFileURL as pathToFileURL3 } from "url";
50694
50991
  function registerStart(program2) {
50695
50992
  program2.command("start").description("Start the production server from dist/").option("--env <path>", "path to .env file to load").action(async (options) => {
@@ -50702,7 +50999,7 @@ async function runStart(options, deps) {
50702
50999
  const config3 = await resolveConfig(cwd);
50703
51000
  const db = await connectDb(config3);
50704
51001
  const migrationsTable = config3.migrations?.table ?? "__manguito_migrations";
50705
- const migrationsFolder = config3.migrations?.folder ? resolve7(cwd, config3.migrations.folder) : resolve7(cwd, "./migrations");
51002
+ const migrationsFolder = config3.migrations?.folder ? resolve8(cwd, config3.migrations.folder) : resolve8(cwd, "./migrations");
50706
51003
  const trackingTableExists = await db.tableExists(migrationsTable);
50707
51004
  if (!trackingTableExists) {
50708
51005
  printGuidedError(
@@ -50737,7 +51034,7 @@ async function runStart(options, deps) {
50737
51034
  } else {
50738
51035
  allErrors.push(...walkResult.errors);
50739
51036
  }
50740
- const rolesPath = resolve7(cwd, config3.schema.base_path, "roles.json");
51037
+ const rolesPath = resolve8(cwd, config3.schema.base_path, "roles.json");
50741
51038
  const rolesLoad = loadSchemaFile(rolesPath);
50742
51039
  let parsedRoles = null;
50743
51040
  if (rolesLoad.ok) {
@@ -50750,7 +51047,7 @@ async function runStart(options, deps) {
50750
51047
  } else {
50751
51048
  allErrors.push(...rolesLoad.errors);
50752
51049
  }
50753
- const routesPath = resolve7(cwd, config3.schema.base_path, "routes.json");
51050
+ const routesPath = resolve8(cwd, config3.schema.base_path, "routes.json");
50754
51051
  const routesLoad = loadSchemaFile(routesPath);
50755
51052
  let parsedRoutes = null;
50756
51053
  if (routesLoad.ok) {
@@ -50772,7 +51069,7 @@ async function runStart(options, deps) {
50772
51069
  }
50773
51070
  const registry2 = buildSchemaRegistry(parsedSchemas, parsedRoutes, parsedRoles);
50774
51071
  await seedSystemTables(db.getDb(), registry2);
50775
- const serverUrl = pathToFileURL3(resolve7(cwd, "dist/server.js")).href;
51072
+ const serverUrl = pathToFileURL3(resolve8(cwd, "dist/server.js")).href;
50776
51073
  await (deps.loadServer ? deps.loadServer(serverUrl) : import(serverUrl));
50777
51074
  const port = Number(process.env["PORT"] ?? 3e3);
50778
51075
  printSuccess(`Server running at http://localhost:${port}`);
@@ -50781,7 +51078,7 @@ async function runStart(options, deps) {
50781
51078
 
50782
51079
  // src/commands/migrate.ts
50783
51080
  init_esm_shims();
50784
- import { resolve as resolve8, join as join13 } from "path";
51081
+ import { resolve as resolve9, join as join14 } from "path";
50785
51082
  import { readFile as readFile2 } from "fs/promises";
50786
51083
  function registerMigrate(program2) {
50787
51084
  program2.command("migrate").description("Apply pending database migrations").option("--env <path>", "path to .env file to load").option("--status", "show migration state without applying").option("--dry-run", "preview migrations without writing").option("--force", "skip destructive-change confirmation prompt").action(async (options) => {
@@ -50799,8 +51096,8 @@ function buildDeps(options, cwd) {
50799
51096
  buildRunner: async () => runBuild(options.env ? { env: options.env } : {}, { cwd }),
50800
51097
  needsRebuild: () => needsRebuild(cwd),
50801
51098
  db: createPostgresAdapter(),
50802
- migrationsFolder: resolve8(cwd, "./migrations"),
50803
- configPath: resolve8(cwd, "dist/generated/drizzle.config.ts"),
51099
+ migrationsFolder: resolve9(cwd, "./migrations"),
51100
+ configPath: resolve9(cwd, "dist/generated/drizzle.config.ts"),
50804
51101
  prompt: createPromptAdapter()
50805
51102
  };
50806
51103
  }
@@ -50815,7 +51112,7 @@ async function runMigrate(options, deps) {
50815
51112
  }
50816
51113
  const config4 = await resolveConfig(process.cwd());
50817
51114
  const migrationsTable2 = config4.migrations?.table ?? "__manguito_migrations";
50818
- const migrationsFolder2 = config4.migrations?.folder ? resolve8(process.cwd(), config4.migrations.folder) : deps.migrationsFolder;
51115
+ const migrationsFolder2 = config4.migrations?.folder ? resolve9(process.cwd(), config4.migrations.folder) : deps.migrationsFolder;
50819
51116
  const status = await getMigrationStatus(deps.db.getDb(), { migrationsTable: migrationsTable2, migrationsFolder: migrationsFolder2 });
50820
51117
  if (status.applied.length === 0 && status.pending.length === 0) {
50821
51118
  process.stdout.write(
@@ -50847,7 +51144,7 @@ async function runMigrate(options, deps) {
50847
51144
  loadEnvFile(options.env);
50848
51145
  const cwd = process.cwd();
50849
51146
  const config3 = await resolveConfig(cwd);
50850
- const migrationsFolder = config3.migrations?.folder ? resolve8(cwd, config3.migrations.folder) : deps.migrationsFolder;
51147
+ const migrationsFolder = config3.migrations?.folder ? resolve9(cwd, config3.migrations.folder) : deps.migrationsFolder;
50851
51148
  const migrationsTable = config3.migrations?.table ?? "__manguito_migrations";
50852
51149
  try {
50853
51150
  await deps.db.connect();
@@ -50867,10 +51164,10 @@ async function runMigrate(options, deps) {
50867
51164
  }
50868
51165
  }
50869
51166
  printSuccess("Build complete");
50870
- const generatedDir = resolve8(cwd, "dist/generated");
51167
+ const generatedDir = resolve9(cwd, "dist/generated");
50871
51168
  await generateDrizzleConfig(config3, generatedDir);
50872
51169
  const newFileNames = await generateMigration(deps.configPath, migrationsFolder);
50873
- const newFilePaths = newFileNames.map((f) => join13(migrationsFolder, f));
51170
+ const newFilePaths = newFileNames.map((f) => join14(migrationsFolder, f));
50874
51171
  printSuccess(
50875
51172
  newFileNames.length === 0 ? "No new migration files generated" : `Generated ${newFileNames.length} migration file${newFileNames.length !== 1 ? "s" : ""}: ${newFileNames.join(", ")}`
50876
51173
  );
@@ -50940,7 +51237,7 @@ async function runMigrate(options, deps) {
50940
51237
  } else {
50941
51238
  allErrors.push(...walkResult.errors);
50942
51239
  }
50943
- const rolesPath = resolve8(cwd, config3.schema.base_path, "roles.json");
51240
+ const rolesPath = resolve9(cwd, config3.schema.base_path, "roles.json");
50944
51241
  const rolesLoad = loadSchemaFile(rolesPath);
50945
51242
  let parsedRoles = null;
50946
51243
  if (rolesLoad.ok) {
@@ -50953,7 +51250,7 @@ async function runMigrate(options, deps) {
50953
51250
  } else {
50954
51251
  allErrors.push(...rolesLoad.errors);
50955
51252
  }
50956
- const routesPath = resolve8(cwd, config3.schema.base_path, "routes.json");
51253
+ const routesPath = resolve9(cwd, config3.schema.base_path, "routes.json");
50957
51254
  const routesLoad = loadSchemaFile(routesPath);
50958
51255
  let parsedRoutes = null;
50959
51256
  if (routesLoad.ok) {
@@ -50985,7 +51282,7 @@ async function runMigrate(options, deps) {
50985
51282
 
50986
51283
  // src/commands/validate.ts
50987
51284
  init_esm_shims();
50988
- import { resolve as resolve9 } from "path";
51285
+ import { resolve as resolve10 } from "path";
50989
51286
  function registerValidate(program2) {
50990
51287
  program2.command("validate").description("Parse and validate all schemas, config, roles, and routes").option("--env <path>", "path to .env file to load").action(async (options) => {
50991
51288
  await runValidate(options, { cwd: process.cwd() });
@@ -51010,7 +51307,7 @@ async function runValidate(options, deps) {
51010
51307
  }
51011
51308
  }
51012
51309
  }
51013
- const rolesPath = resolve9(cwd, config3.schema.base_path, "roles.json");
51310
+ const rolesPath = resolve10(cwd, config3.schema.base_path, "roles.json");
51014
51311
  const rolesLoadResult = loadSchemaFile(rolesPath);
51015
51312
  let parsedRoles = null;
51016
51313
  if (!rolesLoadResult.ok) {
@@ -51023,7 +51320,7 @@ async function runValidate(options, deps) {
51023
51320
  parsedRoles = rolesParseResult.value;
51024
51321
  }
51025
51322
  }
51026
- const routesPath = resolve9(cwd, config3.schema.base_path, "routes.json");
51323
+ const routesPath = resolve10(cwd, config3.schema.base_path, "routes.json");
51027
51324
  const routesLoadResult = loadSchemaFile(routesPath);
51028
51325
  let parsedRoutesDef = null;
51029
51326
  if (!routesLoadResult.ok) {
@@ -51306,18 +51603,6 @@ import path6 from "path";
51306
51603
  // src/utils/registry.ts
51307
51604
  init_esm_shims();
51308
51605
  import { resolve as resolve11 } from "path";
51309
-
51310
- // src/utils/schema-config.ts
51311
- init_esm_shims();
51312
- import { resolve as resolve10 } from "path";
51313
- function resolveSchemaConfig(cwd, config3) {
51314
- return {
51315
- base_path: resolve10(cwd, config3.schema.base_path),
51316
- folders: config3.schema.folders
51317
- };
51318
- }
51319
-
51320
- // src/utils/registry.ts
51321
51606
  function loadWorkingRegistry(cwd, config3, command) {
51322
51607
  const schema = resolveSchemaConfig(cwd, config3);
51323
51608
  const allErrors = [];