@bobbykim/manguito-cms-cli 0.5.0 → 0.6.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.
@@ -49,7 +49,7 @@ var init_esm_shims = __esm({
49
49
  }
50
50
  });
51
51
 
52
- // ../api/dist/chunk-4Y4G564P.mjs
52
+ // ../api/dist/chunk-D5AO4BTL.mjs
53
53
  init_esm_shims();
54
54
  import crypto from "crypto";
55
55
 
@@ -1212,7 +1212,87 @@ Subquery.prototype.getSQL = function() {
1212
1212
  return new SQL([this]);
1213
1213
  };
1214
1214
 
1215
- // ../api/dist/chunk-4Y4G564P.mjs
1215
+ // ../api/dist/chunk-D5AO4BTL.mjs
1216
+ function isColumnBacked(field) {
1217
+ const col = field.db_column;
1218
+ if (col === null) return false;
1219
+ if (col.column_name === "") return false;
1220
+ if (col.junction) return false;
1221
+ return true;
1222
+ }
1223
+ function buildFieldKeyMap(pairs, allLabels, droppedKeys) {
1224
+ const labelToColumn = /* @__PURE__ */ new Map();
1225
+ const columnToLabel = /* @__PURE__ */ new Map();
1226
+ for (const { label, column } of pairs) {
1227
+ labelToColumn.set(label, column);
1228
+ columnToLabel.set(column, label);
1229
+ }
1230
+ for (const label of allLabels) {
1231
+ const columnOwner = columnToLabel.get(label);
1232
+ if (columnOwner !== void 0 && columnOwner !== label) {
1233
+ throw new Error(
1234
+ `Fatal: field key map failed to build \u2014 field label "${label}" collides with the storage column of field "${columnOwner}". A field label may not reuse another field's column name. Rename the field, then run \`manguito validate\` to check your schema.`
1235
+ );
1236
+ }
1237
+ }
1238
+ for (const label of labelToColumn.keys()) {
1239
+ if (droppedKeys.has(label)) labelToColumn.delete(label);
1240
+ }
1241
+ for (const column of columnToLabel.keys()) {
1242
+ if (droppedKeys.has(column)) columnToLabel.delete(column);
1243
+ }
1244
+ const diverges = [...labelToColumn].some(([label, column]) => label !== column);
1245
+ function remap(input, lookup) {
1246
+ const out = {};
1247
+ for (const key of Object.keys(input)) {
1248
+ if (droppedKeys.has(key)) continue;
1249
+ out[lookup.get(key) ?? key] = input[key];
1250
+ }
1251
+ return out;
1252
+ }
1253
+ return {
1254
+ toStorage: (input) => remap(input, labelToColumn),
1255
+ toLabels: (row) => remap(row, columnToLabel),
1256
+ columnFor: (label) => labelToColumn.get(label),
1257
+ labelFor: (column) => columnToLabel.get(column),
1258
+ labels: [...labelToColumn.keys()],
1259
+ diverges
1260
+ };
1261
+ }
1262
+ function createFieldKeyMap(fields) {
1263
+ const droppedKeys = /* @__PURE__ */ new Set();
1264
+ const pairs = [];
1265
+ for (const f of fields) {
1266
+ if (!isColumnBacked(f)) continue;
1267
+ pairs.push({ label: f.name, column: f.db_column.column_name });
1268
+ if (f.removed === true) {
1269
+ droppedKeys.add(f.name);
1270
+ droppedKeys.add(f.db_column.column_name);
1271
+ }
1272
+ }
1273
+ return buildFieldKeyMap(
1274
+ pairs,
1275
+ fields.map((f) => f.name),
1276
+ droppedKeys
1277
+ );
1278
+ }
1279
+ function createFieldKeyMapFromProjection(projectionType, allFields) {
1280
+ const pairs = projectionType.fields.map((f) => ({ label: f.exposed_as, column: f.column_name }));
1281
+ const projected = new Set(pairs.map((p) => p.label));
1282
+ const dropped = /* @__PURE__ */ new Set();
1283
+ for (const f of allFields) {
1284
+ if (!isColumnBacked(f)) continue;
1285
+ dropped.add(f.name);
1286
+ dropped.add(f.db_column.column_name);
1287
+ }
1288
+ for (const p of pairs) dropped.delete(p.column);
1289
+ for (const label of projected) dropped.delete(label);
1290
+ return buildFieldKeyMap(
1291
+ pairs,
1292
+ allFields.map((f) => f.name),
1293
+ dropped
1294
+ );
1295
+ }
1216
1296
  function quoteIdent(name) {
1217
1297
  if (!/^[a-z][a-z0-9_-]*$/.test(name)) throw new Error(`Unsafe identifier: ${name}`);
1218
1298
  return `"${name}"`;
@@ -1506,58 +1586,6 @@ async function resolveRelationBareIds(db, rows, fieldName, rel) {
1506
1586
  }
1507
1587
  }
1508
1588
  }
1509
- function isColumnBacked(field) {
1510
- const col = field.db_column;
1511
- if (col === null) return false;
1512
- if (col.column_name === "") return false;
1513
- if (col.junction) return false;
1514
- return true;
1515
- }
1516
- function createFieldKeyMap(fields) {
1517
- const labelToColumn = /* @__PURE__ */ new Map();
1518
- const columnToLabel = /* @__PURE__ */ new Map();
1519
- const droppedKeys = /* @__PURE__ */ new Set();
1520
- for (const f of fields) {
1521
- if (!isColumnBacked(f)) continue;
1522
- columnToLabel.set(f.db_column.column_name, f.name);
1523
- if (f.removed === true) {
1524
- droppedKeys.add(f.name);
1525
- droppedKeys.add(f.db_column.column_name);
1526
- continue;
1527
- }
1528
- labelToColumn.set(f.name, f.db_column.column_name);
1529
- }
1530
- for (const f of fields) {
1531
- const columnOwner = columnToLabel.get(f.name);
1532
- if (columnOwner !== void 0 && columnOwner !== f.name) {
1533
- throw new Error(
1534
- `Fatal: field key map failed to build \u2014 field label "${f.name}" collides with the storage column of field "${columnOwner}". A field label may not reuse another field's column name. Rename the field, then run \`manguito validate\` to check your schema.`
1535
- );
1536
- }
1537
- }
1538
- for (const f of fields) {
1539
- if (isColumnBacked(f) && f.removed === true) {
1540
- columnToLabel.delete(f.db_column.column_name);
1541
- }
1542
- }
1543
- const diverges = [...labelToColumn].some(([label, column]) => label !== column);
1544
- function remap(input, lookup) {
1545
- const out = {};
1546
- for (const key of Object.keys(input)) {
1547
- if (droppedKeys.has(key)) continue;
1548
- out[lookup.get(key) ?? key] = input[key];
1549
- }
1550
- return out;
1551
- }
1552
- return {
1553
- toStorage: (input) => remap(input, labelToColumn),
1554
- toLabels: (row) => remap(row, columnToLabel),
1555
- columnFor: (label) => labelToColumn.get(label),
1556
- labelFor: (column) => columnToLabel.get(column),
1557
- labels: [...labelToColumn.keys()],
1558
- diverges
1559
- };
1560
- }
1561
1589
 
1562
1590
  export {
1563
1591
  __require,
@@ -1590,12 +1618,13 @@ export {
1590
1618
  Placeholder,
1591
1619
  fillPlaceholders,
1592
1620
  View,
1621
+ isColumnBacked,
1622
+ createFieldKeyMap,
1623
+ createFieldKeyMapFromProjection,
1593
1624
  persistParagraphField,
1594
1625
  deleteParagraphField,
1595
1626
  persistJunctionField,
1596
1627
  buildRelationsMap,
1597
1628
  resolveRelationField,
1598
- resolveRelationBareIds,
1599
- isColumnBacked,
1600
- createFieldKeyMap
1629
+ resolveRelationBareIds
1601
1630
  };
@@ -11,7 +11,7 @@ import {
11
11
  init_esm_shims,
12
12
  isColumnBacked,
13
13
  resolveRelationField
14
- } from "./chunk-LFMOJRYP.js";
14
+ } from "./chunk-STFSR5LP.js";
15
15
 
16
16
  // ../../node_modules/.pnpm/@whatwg-node+fetch@0.10.13/node_modules/@whatwg-node/fetch/dist/shouldSkipPonyfill.js
17
17
  var require_shouldSkipPonyfill = __commonJS({
@@ -10786,10 +10786,10 @@ var require_parser = __commonJS({
10786
10786
  }
10787
10787
  });
10788
10788
 
10789
- // ../api/dist/handler-45O5OX2C.mjs
10789
+ // ../api/dist/handler-X7JDHJAL.mjs
10790
10790
  init_esm_shims();
10791
10791
 
10792
- // ../api/dist/chunk-HEYT5CH5.mjs
10792
+ // ../api/dist/chunk-ARZXLNZS.mjs
10793
10793
  init_esm_shims();
10794
10794
 
10795
10795
  // ../../node_modules/.pnpm/graphql-yoga@5.21.2_graphql@16.14.2/node_modules/graphql-yoga/esm/index.js
@@ -30122,7 +30122,7 @@ function createYoga(options) {
30122
30122
  });
30123
30123
  }
30124
30124
 
30125
- // ../api/dist/chunk-HEYT5CH5.mjs
30125
+ // ../api/dist/chunk-ARZXLNZS.mjs
30126
30126
  var import_dataloader = __toESM(require_dataloader(), 1);
30127
30127
 
30128
30128
  // ../../node_modules/.pnpm/@escape.tech+graphql-armor@3.2.0_@envelop+core@5.5.1_@escape.tech+graphql-armor-types@0.7.0/node_modules/@escape.tech/graphql-armor/dist/graphql-armor.esm.js
@@ -30880,7 +30880,7 @@ var EnvelopArmor = class {
30880
30880
  }
30881
30881
  };
30882
30882
 
30883
- // ../api/dist/chunk-HEYT5CH5.mjs
30883
+ // ../api/dist/chunk-ARZXLNZS.mjs
30884
30884
  var DateTimeScalar = new GraphQLScalarType({
30885
30885
  name: "DateTime",
30886
30886
  description: "ISO-8601 date-time string",
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-STFSR5LP.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";
@@ -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("*");
@@ -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.5.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,27 +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 };
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
+ };
50128
50280
  const graphqlRepos = Object.fromEntries([
50129
50281
  ...Object.entries(registry2.content_types).map(([typeName, ct2]) => [
50130
50282
  typeName,
@@ -50147,17 +50299,17 @@ function createCmsApp(options) {
50147
50299
  for (const ct2 of Object.values(registry2.content_types)) {
50148
50300
  const base = ct2.default_base_path;
50149
50301
  if (ct2.only_one) {
50150
- paths[publicPaths.collection(base)] = { get: { summary: `Get ${ct2.label}`, tags: [ct2.label] } };
50302
+ paths[versionedPaths.collection(base)] = { get: { summary: `Get ${ct2.label}`, tags: [ct2.label] } };
50151
50303
  } else {
50152
- paths[publicPaths.collection(base)] = { get: { summary: `List published ${ct2.label}`, tags: [ct2.label] } };
50153
- paths[publicPaths.item(base).replace(":slug", "{slug}")] = {
50304
+ paths[versionedPaths.collection(base)] = { get: { summary: `List published ${ct2.label}`, tags: [ct2.label] } };
50305
+ paths[versionedPaths.item(base).replace(":slug", "{slug}")] = {
50154
50306
  get: { summary: `Get ${ct2.label} by slug`, tags: [ct2.label] }
50155
50307
  };
50156
50308
  }
50157
50309
  }
50158
50310
  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}")] = {
50311
+ paths[versionedPaths.taxonomyCollection(tt2.name)] = { get: { summary: `List published ${tt2.label}`, tags: [tt2.label] } };
50312
+ paths[versionedPaths.taxonomyItem(tt2.name).replace(":id", "{id}")] = {
50161
50313
  get: { summary: `Get ${tt2.label} by id`, tags: [tt2.label] }
50162
50314
  };
50163
50315
  }
@@ -50177,13 +50329,68 @@ function createCmsApp(options) {
50177
50329
  paths: {}
50178
50330
  })
50179
50331
  );
50180
- registerPublicContentRoutes(app, registry2, publicRepos, projectors, publicPaths, listRateLimit, programmaticResolver);
50332
+ const model = options.versions ?? {
50333
+ current: "v1",
50334
+ live: ["v1"],
50335
+ projections: {}
50336
+ };
50337
+ const passes = [
50338
+ ...options.versions !== void 0 ? model.live.map((v2) => ({ version: v2, projectionVersion: v2 })) : [],
50339
+ { version: null, projectionVersion: model.current }
50340
+ ];
50341
+ for (const pass of passes) {
50342
+ const surface = buildVersionSurface({ ...pass, prefix, registry: registry2, model, makeRepo });
50343
+ const headers = deprecationHeaders({
50344
+ requested: pass.version,
50345
+ model,
50346
+ // The version root, with no trailing slash — never
50347
+ // createVersionedPaths(prefix, model.current).collection(''), which
50348
+ // would yield `${prefix}/${model.current}/` in every Link header.
50349
+ successor: `${prefix}/${model.current}`
50350
+ });
50351
+ if (headers !== null) {
50352
+ const attachDeprecationHeaders = async (c, next) => {
50353
+ await next();
50354
+ for (const [key, value] of Object.entries(headers)) c.res.headers.set(key, value);
50355
+ };
50356
+ for (const path7 of versionRoutePaths(registry2, surface.paths)) {
50357
+ app.use(path7, attachDeprecationHeaders);
50358
+ }
50359
+ }
50360
+ registerPublicContentRoutes(app, registry2, surface.repos, surface.projectors, surface.paths, listRateLimit, programmaticResolver);
50361
+ }
50362
+ if (options.versions !== void 0) {
50363
+ app.all(`${prefix}/:version/*`, async (c, next) => {
50364
+ const kind = classifyVersion(c.req.param("version"), model);
50365
+ if (kind === "not-a-version" || kind === "live") return next();
50366
+ const live = model.live.join(", ");
50367
+ return kind === "retired" ? c.json(
50368
+ {
50369
+ ok: false,
50370
+ error: {
50371
+ code: "VERSION_RETIRED",
50372
+ message: `Version ${c.req.param("version")} is no longer served. Live versions: ${live}.`
50373
+ }
50374
+ },
50375
+ 410
50376
+ ) : c.json(
50377
+ {
50378
+ ok: false,
50379
+ error: {
50380
+ code: "VERSION_NOT_FOUND",
50381
+ message: `No version ${c.req.param("version")} is served. Live versions: ${live}.`
50382
+ }
50383
+ },
50384
+ 404
50385
+ );
50386
+ });
50387
+ }
50181
50388
  registerPublicMediaRoutes(app, mediaRepo, publicPaths, listRateLimit);
50182
50389
  if (options.graphql?.enabled) {
50183
50390
  const gqlOptions = options.graphql;
50184
50391
  let gqlHandler = null;
50185
50392
  let initError = null;
50186
- const ready = import("./handler-45O5OX2C-OIEJ52PN.js").then(({ createGraphQLHandler }) => {
50393
+ const ready = import("./handler-X7JDHJAL-JEAY3P6W.js").then(({ createGraphQLHandler }) => {
50187
50394
  gqlHandler = createGraphQLHandler(registry2, graphqlRepos, fieldKeyMaps, programmaticResolver, db, gqlOptions);
50188
50395
  }).catch((err) => {
50189
50396
  initError = err;
@@ -50231,12 +50438,12 @@ function createCmsApp(options) {
50231
50438
 
50232
50439
  // src/codegen/drizzle-config.ts
50233
50440
  init_esm_shims();
50234
- import { writeFile as writeFile6 } from "fs/promises";
50235
- import { join as join9, relative as relative2, resolve as resolve4 } from "path";
50441
+ import { writeFile as writeFile7 } from "fs/promises";
50442
+ import { join as join10, relative as relative2, resolve as resolve5 } from "path";
50236
50443
  async function generateDrizzleConfig(config3, targetDir) {
50237
50444
  const migrationsFolder = config3.migrations?.folder ?? "./migrations";
50238
50445
  const migrationsTable = config3.migrations?.table ?? "__manguito_migrations";
50239
- const outPath = relative2(resolve4(targetDir), resolve4(migrationsFolder)).replace(/\\/g, "/");
50446
+ const outPath = relative2(resolve5(targetDir), resolve5(migrationsFolder)).replace(/\\/g, "/");
50240
50447
  const content = [
50241
50448
  `import { defineConfig } from 'drizzle-kit'`,
50242
50449
  ``,
@@ -50254,13 +50461,13 @@ async function generateDrizzleConfig(config3, targetDir) {
50254
50461
  ` tablesFilter: ['!${migrationsTable}'],`,
50255
50462
  `})`
50256
50463
  ].join("\n");
50257
- await writeFile6(join9(targetDir, "drizzle.config.ts"), content, "utf8");
50464
+ await writeFile7(join10(targetDir, "drizzle.config.ts"), content, "utf8");
50258
50465
  }
50259
50466
 
50260
50467
  // src/codegen/nav.ts
50261
50468
  init_esm_shims();
50262
- import { writeFile as writeFile7 } from "fs/promises";
50263
- import { join as join10 } from "path";
50469
+ import { writeFile as writeFile8 } from "fs/promises";
50470
+ import { join as join11 } from "path";
50264
50471
  function schemaBasePath(name) {
50265
50472
  const segment = name.includes("--") ? name.slice(name.indexOf("--") + 2) : name;
50266
50473
  return segment.replace(/_/g, "-");
@@ -50278,7 +50485,7 @@ async function generateNav(registry2, targetDir) {
50278
50485
  })).sort((a2, b2) => a2.label.localeCompare(b2.label));
50279
50486
  const content = `export const nav = ${JSON.stringify({ contentTypes, taxonomyTypes }, null, 2)}
50280
50487
  `;
50281
- await writeFile7(join10(targetDir, "nav.ts"), content, "utf8");
50488
+ await writeFile8(join11(targetDir, "nav.ts"), content, "utf8");
50282
50489
  }
50283
50490
 
50284
50491
  // src/commands/dev-routing.ts
@@ -50323,7 +50530,7 @@ function createPromptAdapter() {
50323
50530
  init_esm_shims();
50324
50531
  import { existsSync as existsSync4 } from "fs";
50325
50532
  import { readdir } from "fs/promises";
50326
- import { resolve as resolve5, join as join11, extname as extname3 } from "path";
50533
+ import { resolve as resolve6, join as join12, extname as extname3 } from "path";
50327
50534
  import { pathToFileURL as pathToFileURL2 } from "url";
50328
50535
  var RESOLVER_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".mjs", ".js"]);
50329
50536
  function isProgrammaticFieldDefinition(v2) {
@@ -50332,7 +50539,7 @@ function isProgrammaticFieldDefinition(v2) {
50332
50539
  async function walkTsFiles(dir) {
50333
50540
  const out = [];
50334
50541
  for (const entry of await readdir(dir, { withFileTypes: true })) {
50335
- const full = join11(dir, entry.name);
50542
+ const full = join12(dir, entry.name);
50336
50543
  if (entry.isDirectory()) {
50337
50544
  out.push(...await walkTsFiles(full));
50338
50545
  } else if (RESOLVER_EXTENSIONS.has(extname3(entry.name))) {
@@ -50342,7 +50549,7 @@ async function walkTsFiles(dir) {
50342
50549
  return out;
50343
50550
  }
50344
50551
  async function loadProgrammaticResolvers(cwd, dir) {
50345
- const abs = resolve5(cwd, dir);
50552
+ const abs = resolve6(cwd, dir);
50346
50553
  const map2 = /* @__PURE__ */ new Map();
50347
50554
  if (!existsSync4(abs)) return map2;
50348
50555
  for (const file2 of (await walkTsFiles(abs)).sort()) {
@@ -50389,13 +50596,13 @@ async function runDev(options, deps) {
50389
50596
  printSuccess("Config loaded");
50390
50597
  const db = await connectDb(config3);
50391
50598
  printSuccess("Database connected");
50392
- const manguitoDir = resolve6(cwd, ".manguito");
50599
+ const manguitoDir = resolve7(cwd, ".manguito");
50393
50600
  await mkdir2(manguitoDir, { recursive: true });
50394
- const drizzleConfigPath = resolve6(manguitoDir, "drizzle.config.ts");
50601
+ const drizzleConfigPath = resolve7(manguitoDir, "drizzle.config.ts");
50395
50602
  const needsSeed = !await db.tableExists("users");
50396
50603
  const bootstrapRegistry = await parseAllSchemas(cwd, config3);
50397
50604
  await generateDrizzleConfig(config3, manguitoDir);
50398
- await writeFile8(join12(manguitoDir, "schema.ts"), generateSchemaFile(bootstrapRegistry), "utf8");
50605
+ await writeFile9(join13(manguitoDir, "schema.ts"), generateSchemaFile(bootstrapRegistry), "utf8");
50399
50606
  await runDevMigration(drizzleConfigPath);
50400
50607
  printSuccess(needsSeed ? "Schema tables created" : "Schema tables up to date");
50401
50608
  if (needsSeed) {
@@ -50441,8 +50648,20 @@ async function runDev(options, deps) {
50441
50648
  );
50442
50649
  await generateSchemaRegistry(registry2, manguitoDir);
50443
50650
  await generateRoutes2(registry2, manguitoDir);
50444
- await generateForms(registry2, join12(manguitoDir, "forms"));
50651
+ await generateForms(registry2, join13(manguitoDir, "forms"));
50445
50652
  await generateNav(registry2, manguitoDir);
50653
+ const schemaConfig = resolveSchemaConfig(cwd, config3);
50654
+ const snapshotsResult = loadVersionSnapshots(schemaConfig, registry2);
50655
+ if (!snapshotsResult.ok) {
50656
+ printValidationErrors(snapshotsResult.errors, "Version snapshot errors", "manguito version:diff");
50657
+ process.exit(1);
50658
+ }
50659
+ const versionModelResult = computeVersionModel({ current: registry2, snapshots: snapshotsResult.value });
50660
+ if (!versionModelResult.ok) {
50661
+ printValidationErrors(versionModelResult.errors, "Version model errors", "manguito version:diff");
50662
+ process.exit(1);
50663
+ }
50664
+ const versionModel = reduceVersionModel(versionModelResult.value);
50446
50665
  const resolverMap = await loadProgrammaticResolvers(cwd, config3.programmatic.dir);
50447
50666
  const adapter = createCmsApp({
50448
50667
  name: config3.name,
@@ -50450,6 +50669,7 @@ async function runDev(options, deps) {
50450
50669
  db: db.getDb(),
50451
50670
  storage: config3.storage,
50452
50671
  resolvers: resolverMap,
50672
+ versions: versionModel,
50453
50673
  ...config3.api.prefix ? { prefix: config3.api.prefix } : {},
50454
50674
  ...config3.api.media?.max_file_size ? { media: { max_file_size: config3.api.media.max_file_size } } : {},
50455
50675
  ...config3.api.rateLimit ? { rateLimit: config3.api.rateLimit } : {},
@@ -50482,8 +50702,8 @@ async function runDev(options, deps) {
50482
50702
  }
50483
50703
  if (url2.startsWith("/uploads/")) {
50484
50704
  const relativePath = (url2.slice("/uploads/".length).split("?")[0] ?? "").replace(/\.\./g, "");
50485
- const uploadsDir = resolve6(cwd, "uploads");
50486
- const filePath = resolve6(uploadsDir, relativePath);
50705
+ const uploadsDir = resolve7(cwd, "uploads");
50706
+ const filePath = resolve7(uploadsDir, relativePath);
50487
50707
  if (!filePath.startsWith(uploadsDir + "/") && filePath !== uploadsDir) {
50488
50708
  res.statusCode = 403;
50489
50709
  res.end("Forbidden");
@@ -50524,7 +50744,7 @@ async function runDev(options, deps) {
50524
50744
  printSuccess(`Dev server running at http://localhost:${port}`);
50525
50745
  printSuccess(`Admin panel at http://localhost:${port}${config3.admin.prefix}`);
50526
50746
  });
50527
- const schemasDir = resolve6(cwd, config3.schema.base_path);
50747
+ const schemasDir = resolve7(cwd, config3.schema.base_path);
50528
50748
  const watcher = watch(schemasDir, { recursive: true });
50529
50749
  void (async () => {
50530
50750
  for await (const event of watcher) {
@@ -50543,7 +50763,7 @@ async function runDev(options, deps) {
50543
50763
  }
50544
50764
  }
50545
50765
  })();
50546
- const programmaticDir = resolve6(cwd, config3.programmatic.dir);
50766
+ const programmaticDir = resolve7(cwd, config3.programmatic.dir);
50547
50767
  if (existsSync5(programmaticDir)) {
50548
50768
  const progWatcher = watch(programmaticDir, { recursive: true });
50549
50769
  void (async () => {
@@ -50575,11 +50795,25 @@ async function onSchemaFileChange(args) {
50575
50795
  return;
50576
50796
  }
50577
50797
  await generateDrizzleConfig(config3, manguitoDir);
50578
- await writeFile8(join12(manguitoDir, "schema.ts"), generateSchemaFile(registry2), "utf8");
50798
+ await writeFile9(join13(manguitoDir, "schema.ts"), generateSchemaFile(registry2), "utf8");
50579
50799
  await generateSchemaRegistry(registry2, manguitoDir);
50580
50800
  await generateRoutes2(registry2, manguitoDir);
50581
- await generateForms(registry2, join12(manguitoDir, "forms"));
50801
+ await generateForms(registry2, join13(manguitoDir, "forms"));
50582
50802
  await generateNav(registry2, manguitoDir);
50803
+ const schemaConfig = resolveSchemaConfig(cwd, config3);
50804
+ const snapshotsResult = loadVersionSnapshots(schemaConfig, registry2);
50805
+ if (!snapshotsResult.ok) {
50806
+ printValidationErrors(snapshotsResult.errors, "Version snapshot errors", "manguito version:diff");
50807
+ process.stderr.write("\u26A0 Changes not applied.\n");
50808
+ return;
50809
+ }
50810
+ const versionModelResult = computeVersionModel({ current: registry2, snapshots: snapshotsResult.value });
50811
+ if (!versionModelResult.ok) {
50812
+ printValidationErrors(versionModelResult.errors, "Version model errors", "manguito version:diff");
50813
+ process.stderr.write("\u26A0 Changes not applied.\n");
50814
+ return;
50815
+ }
50816
+ const versionModel = reduceVersionModel(versionModelResult.value);
50583
50817
  try {
50584
50818
  await runDevMigration(drizzleConfigPath);
50585
50819
  } catch (err) {
@@ -50594,6 +50828,7 @@ async function onSchemaFileChange(args) {
50594
50828
  db: db.getDb(),
50595
50829
  storage: config3.storage,
50596
50830
  resolvers: resolverMap,
50831
+ versions: versionModel,
50597
50832
  ...config3.api.prefix ? { prefix: config3.api.prefix } : {},
50598
50833
  ...config3.api.media?.max_file_size ? { media: { max_file_size: config3.api.media.max_file_size } } : {},
50599
50834
  ...config3.api.rateLimit ? { rateLimit: config3.api.rateLimit } : {},
@@ -50618,7 +50853,7 @@ async function parseAllSchemas(cwd, config3) {
50618
50853
  }
50619
50854
  parsedSchemas.push(parseResult.schema);
50620
50855
  }
50621
- const rolesPath = resolve6(cwd, config3.schema.base_path, "roles.json");
50856
+ const rolesPath = resolve7(cwd, config3.schema.base_path, "roles.json");
50622
50857
  const rolesLoad = loadSchemaFile(rolesPath);
50623
50858
  if (!rolesLoad.ok) {
50624
50859
  printGuidedError("Could not read roles.json.");
@@ -50629,7 +50864,7 @@ async function parseAllSchemas(cwd, config3) {
50629
50864
  printGuidedError("roles.json parse errors \u2014 run `manguito validate` for details.");
50630
50865
  process.exit(1);
50631
50866
  }
50632
- const routesPath = resolve6(cwd, config3.schema.base_path, "routes.json");
50867
+ const routesPath = resolve7(cwd, config3.schema.base_path, "routes.json");
50633
50868
  const routesLoad = loadSchemaFile(routesPath);
50634
50869
  if (!routesLoad.ok) {
50635
50870
  printGuidedError("Could not read routes.json.");
@@ -50643,7 +50878,7 @@ async function parseAllSchemas(cwd, config3) {
50643
50878
  return buildSchemaRegistry(parsedSchemas, routesResult.value, rolesResult.value);
50644
50879
  }
50645
50880
  function resolveAdminRoot2(cwd) {
50646
- const cwdRequire = createRequire2(join12(cwd, "package.json"));
50881
+ const cwdRequire = createRequire2(join13(cwd, "package.json"));
50647
50882
  const adminPkgJson = cwdRequire.resolve("@bobbykim/manguito-cms-admin/package.json");
50648
50883
  return dirname2(adminPkgJson);
50649
50884
  }
@@ -50689,7 +50924,7 @@ async function bridgeToHono(req, res, honoFetch) {
50689
50924
 
50690
50925
  // src/commands/start.ts
50691
50926
  init_esm_shims();
50692
- import { resolve as resolve7 } from "path";
50927
+ import { resolve as resolve8 } from "path";
50693
50928
  import { pathToFileURL as pathToFileURL3 } from "url";
50694
50929
  function registerStart(program2) {
50695
50930
  program2.command("start").description("Start the production server from dist/").option("--env <path>", "path to .env file to load").action(async (options) => {
@@ -50702,7 +50937,7 @@ async function runStart(options, deps) {
50702
50937
  const config3 = await resolveConfig(cwd);
50703
50938
  const db = await connectDb(config3);
50704
50939
  const migrationsTable = config3.migrations?.table ?? "__manguito_migrations";
50705
- const migrationsFolder = config3.migrations?.folder ? resolve7(cwd, config3.migrations.folder) : resolve7(cwd, "./migrations");
50940
+ const migrationsFolder = config3.migrations?.folder ? resolve8(cwd, config3.migrations.folder) : resolve8(cwd, "./migrations");
50706
50941
  const trackingTableExists = await db.tableExists(migrationsTable);
50707
50942
  if (!trackingTableExists) {
50708
50943
  printGuidedError(
@@ -50737,7 +50972,7 @@ async function runStart(options, deps) {
50737
50972
  } else {
50738
50973
  allErrors.push(...walkResult.errors);
50739
50974
  }
50740
- const rolesPath = resolve7(cwd, config3.schema.base_path, "roles.json");
50975
+ const rolesPath = resolve8(cwd, config3.schema.base_path, "roles.json");
50741
50976
  const rolesLoad = loadSchemaFile(rolesPath);
50742
50977
  let parsedRoles = null;
50743
50978
  if (rolesLoad.ok) {
@@ -50750,7 +50985,7 @@ async function runStart(options, deps) {
50750
50985
  } else {
50751
50986
  allErrors.push(...rolesLoad.errors);
50752
50987
  }
50753
- const routesPath = resolve7(cwd, config3.schema.base_path, "routes.json");
50988
+ const routesPath = resolve8(cwd, config3.schema.base_path, "routes.json");
50754
50989
  const routesLoad = loadSchemaFile(routesPath);
50755
50990
  let parsedRoutes = null;
50756
50991
  if (routesLoad.ok) {
@@ -50772,7 +51007,7 @@ async function runStart(options, deps) {
50772
51007
  }
50773
51008
  const registry2 = buildSchemaRegistry(parsedSchemas, parsedRoutes, parsedRoles);
50774
51009
  await seedSystemTables(db.getDb(), registry2);
50775
- const serverUrl = pathToFileURL3(resolve7(cwd, "dist/server.js")).href;
51010
+ const serverUrl = pathToFileURL3(resolve8(cwd, "dist/server.js")).href;
50776
51011
  await (deps.loadServer ? deps.loadServer(serverUrl) : import(serverUrl));
50777
51012
  const port = Number(process.env["PORT"] ?? 3e3);
50778
51013
  printSuccess(`Server running at http://localhost:${port}`);
@@ -50781,7 +51016,7 @@ async function runStart(options, deps) {
50781
51016
 
50782
51017
  // src/commands/migrate.ts
50783
51018
  init_esm_shims();
50784
- import { resolve as resolve8, join as join13 } from "path";
51019
+ import { resolve as resolve9, join as join14 } from "path";
50785
51020
  import { readFile as readFile2 } from "fs/promises";
50786
51021
  function registerMigrate(program2) {
50787
51022
  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 +51034,8 @@ function buildDeps(options, cwd) {
50799
51034
  buildRunner: async () => runBuild(options.env ? { env: options.env } : {}, { cwd }),
50800
51035
  needsRebuild: () => needsRebuild(cwd),
50801
51036
  db: createPostgresAdapter(),
50802
- migrationsFolder: resolve8(cwd, "./migrations"),
50803
- configPath: resolve8(cwd, "dist/generated/drizzle.config.ts"),
51037
+ migrationsFolder: resolve9(cwd, "./migrations"),
51038
+ configPath: resolve9(cwd, "dist/generated/drizzle.config.ts"),
50804
51039
  prompt: createPromptAdapter()
50805
51040
  };
50806
51041
  }
@@ -50815,7 +51050,7 @@ async function runMigrate(options, deps) {
50815
51050
  }
50816
51051
  const config4 = await resolveConfig(process.cwd());
50817
51052
  const migrationsTable2 = config4.migrations?.table ?? "__manguito_migrations";
50818
- const migrationsFolder2 = config4.migrations?.folder ? resolve8(process.cwd(), config4.migrations.folder) : deps.migrationsFolder;
51053
+ const migrationsFolder2 = config4.migrations?.folder ? resolve9(process.cwd(), config4.migrations.folder) : deps.migrationsFolder;
50819
51054
  const status = await getMigrationStatus(deps.db.getDb(), { migrationsTable: migrationsTable2, migrationsFolder: migrationsFolder2 });
50820
51055
  if (status.applied.length === 0 && status.pending.length === 0) {
50821
51056
  process.stdout.write(
@@ -50847,7 +51082,7 @@ async function runMigrate(options, deps) {
50847
51082
  loadEnvFile(options.env);
50848
51083
  const cwd = process.cwd();
50849
51084
  const config3 = await resolveConfig(cwd);
50850
- const migrationsFolder = config3.migrations?.folder ? resolve8(cwd, config3.migrations.folder) : deps.migrationsFolder;
51085
+ const migrationsFolder = config3.migrations?.folder ? resolve9(cwd, config3.migrations.folder) : deps.migrationsFolder;
50851
51086
  const migrationsTable = config3.migrations?.table ?? "__manguito_migrations";
50852
51087
  try {
50853
51088
  await deps.db.connect();
@@ -50867,10 +51102,10 @@ async function runMigrate(options, deps) {
50867
51102
  }
50868
51103
  }
50869
51104
  printSuccess("Build complete");
50870
- const generatedDir = resolve8(cwd, "dist/generated");
51105
+ const generatedDir = resolve9(cwd, "dist/generated");
50871
51106
  await generateDrizzleConfig(config3, generatedDir);
50872
51107
  const newFileNames = await generateMigration(deps.configPath, migrationsFolder);
50873
- const newFilePaths = newFileNames.map((f) => join13(migrationsFolder, f));
51108
+ const newFilePaths = newFileNames.map((f) => join14(migrationsFolder, f));
50874
51109
  printSuccess(
50875
51110
  newFileNames.length === 0 ? "No new migration files generated" : `Generated ${newFileNames.length} migration file${newFileNames.length !== 1 ? "s" : ""}: ${newFileNames.join(", ")}`
50876
51111
  );
@@ -50940,7 +51175,7 @@ async function runMigrate(options, deps) {
50940
51175
  } else {
50941
51176
  allErrors.push(...walkResult.errors);
50942
51177
  }
50943
- const rolesPath = resolve8(cwd, config3.schema.base_path, "roles.json");
51178
+ const rolesPath = resolve9(cwd, config3.schema.base_path, "roles.json");
50944
51179
  const rolesLoad = loadSchemaFile(rolesPath);
50945
51180
  let parsedRoles = null;
50946
51181
  if (rolesLoad.ok) {
@@ -50953,7 +51188,7 @@ async function runMigrate(options, deps) {
50953
51188
  } else {
50954
51189
  allErrors.push(...rolesLoad.errors);
50955
51190
  }
50956
- const routesPath = resolve8(cwd, config3.schema.base_path, "routes.json");
51191
+ const routesPath = resolve9(cwd, config3.schema.base_path, "routes.json");
50957
51192
  const routesLoad = loadSchemaFile(routesPath);
50958
51193
  let parsedRoutes = null;
50959
51194
  if (routesLoad.ok) {
@@ -50985,7 +51220,7 @@ async function runMigrate(options, deps) {
50985
51220
 
50986
51221
  // src/commands/validate.ts
50987
51222
  init_esm_shims();
50988
- import { resolve as resolve9 } from "path";
51223
+ import { resolve as resolve10 } from "path";
50989
51224
  function registerValidate(program2) {
50990
51225
  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
51226
  await runValidate(options, { cwd: process.cwd() });
@@ -51010,7 +51245,7 @@ async function runValidate(options, deps) {
51010
51245
  }
51011
51246
  }
51012
51247
  }
51013
- const rolesPath = resolve9(cwd, config3.schema.base_path, "roles.json");
51248
+ const rolesPath = resolve10(cwd, config3.schema.base_path, "roles.json");
51014
51249
  const rolesLoadResult = loadSchemaFile(rolesPath);
51015
51250
  let parsedRoles = null;
51016
51251
  if (!rolesLoadResult.ok) {
@@ -51023,7 +51258,7 @@ async function runValidate(options, deps) {
51023
51258
  parsedRoles = rolesParseResult.value;
51024
51259
  }
51025
51260
  }
51026
- const routesPath = resolve9(cwd, config3.schema.base_path, "routes.json");
51261
+ const routesPath = resolve10(cwd, config3.schema.base_path, "routes.json");
51027
51262
  const routesLoadResult = loadSchemaFile(routesPath);
51028
51263
  let parsedRoutesDef = null;
51029
51264
  if (!routesLoadResult.ok) {
@@ -51306,18 +51541,6 @@ import path6 from "path";
51306
51541
  // src/utils/registry.ts
51307
51542
  init_esm_shims();
51308
51543
  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
51544
  function loadWorkingRegistry(cwd, config3, command) {
51322
51545
  const schema = resolveSchemaConfig(cwd, config3);
51323
51546
  const allErrors = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bobbykim/manguito-cms-cli",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "license": "MIT",
5
5
  "author": "Bobby Kim <bobby.sihun.kim@gmail.com>",
6
6
  "homepage": "https://github.com/bobbykim89/manguito-cms",
@@ -29,10 +29,10 @@
29
29
  "tsup": "^8.5.1",
30
30
  "typescript": "^6.0.3",
31
31
  "vite": "^8.1.3",
32
- "@bobbykim/manguito-cms-api": "0.4.3",
33
- "@bobbykim/manguito-cms-core": "0.5.0",
34
32
  "@bobbykim/manguito-cms-admin": "0.4.2",
35
- "@bobbykim/manguito-cms-db": "0.1.5"
33
+ "@bobbykim/manguito-cms-core": "0.5.0",
34
+ "@bobbykim/manguito-cms-db": "0.1.5",
35
+ "@bobbykim/manguito-cms-api": "0.5.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@types/node": "^25.9.5",
@@ -43,6 +43,7 @@
43
43
  "build": "tsup",
44
44
  "dev": "tsup --watch",
45
45
  "test": "dotenv -e .env.test -- vitest run",
46
- "lint": "eslint src"
46
+ "lint": "eslint src",
47
+ "typecheck": "tsc --noEmit"
47
48
  }
48
49
  }