@saasicat/cli 0.26.1 → 1.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -40,6 +40,7 @@ __export(index_exports, {
40
40
  AuditTailCommand: () => AuditTailCommand,
41
41
  AuditTailFlow: () => AuditTailFlow,
42
42
  CLI_CONTEXT_CONFIG_TOKEN: () => CLI_CONTEXT_CONFIG_TOKEN,
43
+ CONSTRAINTS_MARKER: () => CONSTRAINTS_MARKER,
43
44
  CliContextModule: () => CliContextModule,
44
45
  CliContextService: () => CliContextService,
45
46
  CliError: () => CliError,
@@ -50,6 +51,8 @@ __export(index_exports, {
50
51
  DiscoverySnapshotDoctorCheck: () => DiscoverySnapshotDoctorCheck,
51
52
  DoctorCommands: () => DoctorCommands,
52
53
  DoctorFlow: () => DoctorFlow,
54
+ LIMIT_FILTER_IMPORTS: () => LIMIT_FILTER_IMPORTS,
55
+ LIMIT_FILTER_PROVIDER: () => LIMIT_FILTER_PROVIDER,
53
56
  MANIFEST_ACCESS_PORT_TOKEN: () => MANIFEST_ACCESS_PORT_TOKEN,
54
57
  MANIFEST_CHECKS_TOKEN: () => MANIFEST_CHECKS_TOKEN,
55
58
  ManifestCheckCommand: () => ManifestCheckCommand,
@@ -61,37 +64,69 @@ __export(index_exports, {
61
64
  MfaSetupFlow: () => MfaSetupFlow,
62
65
  PLATFORM_DOCTOR_CHECK_PROVIDERS: () => PLATFORM_DOCTOR_CHECK_PROVIDERS,
63
66
  PlanCatalogDoctorCheck: () => PlanCatalogDoctorCheck,
67
+ UI_VUE_SPECIFIER: () => UI_VUE_SPECIFIER,
64
68
  USER_MANAGEMENT_PORT_TOKEN: () => USER_MANAGEMENT_PORT_TOKEN,
65
69
  USER_PORT_TOKEN: () => USER_PORT_TOKEN,
66
70
  UserCommands: () => UserCommands,
67
71
  UserPortDoctorCheck: () => UserPortDoctorCheck,
68
72
  WhoAmIFlow: () => WhoAmIFlow,
73
+ appendConstraints: () => appendConstraints,
69
74
  applyFragmentBlocks: () => applyFragmentBlocks,
75
+ applyTokens: () => applyTokens,
76
+ assertModelsExist: () => assertModelsExist,
77
+ assertValidProjectKey: () => assertValidProjectKey,
78
+ assertValidQuotaKey: () => assertValidQuotaKey,
70
79
  blankStringLiterals: () => blankStringLiterals,
71
80
  blockBodyLines: () => blockBodyLines,
72
81
  breaksContract: () => breaksContract,
82
+ buildImportMap: () => buildImportMap,
73
83
  checkSchema: () => checkSchema,
84
+ constraintsFor: () => constraintsFor,
85
+ enableFkPointers: () => enableFkPointers,
74
86
  extractBlockNames: () => extractBlockNames,
75
87
  extractBlocks: () => extractBlocks,
76
88
  extractModelBlocks: () => extractModelBlocks,
77
89
  extractModelNames: () => extractModelNames,
90
+ findFkPointers: () => findFkPointers,
91
+ foreignKeyOf: () => foreignKeyOf,
92
+ hasBackRelation: () => hasBackRelation,
93
+ hasConstraints: () => hasConstraints,
94
+ isNoLongerPublic: () => isNoLongerPublic,
95
+ isOneToOne: () => isOneToOne,
96
+ kebabCase: () => kebabCase,
97
+ migrationCreatedBy: () => migrationCreatedBy,
98
+ minimumQuotasPerPlan: () => minimumQuotasPerPlan,
99
+ namedImports: () => namedImports,
78
100
  parseBlockAttributes: () => parseBlockAttributes,
79
101
  parseEnumValues: () => parseEnumValues,
80
102
  parseFields: () => parseFields,
103
+ parseQuota: () => parseQuota,
81
104
  parseSchema: () => parseSchema,
105
+ pascalCase: () => pascalCase,
106
+ patchAppModule: () => patchAppModule,
107
+ patchOptionsFor: () => patchOptionsFor,
108
+ planInit: () => planInit,
109
+ projectKeyPattern: () => projectKeyPattern,
110
+ quotaKeyPattern: () => quotaKeyPattern,
111
+ relationNameOf: () => relationNameOf,
112
+ reportConstraints: () => reportConstraints,
113
+ rewriteImports: () => rewriteImports,
114
+ rewriteNames: () => rewriteNames,
115
+ rewriteSubpath: () => rewriteSubpath,
82
116
  stripLineComment: () => stripLineComment,
83
- structuralOnly: () => structuralOnly
117
+ structuralOnly: () => structuralOnly,
118
+ tablesAddressedBy: () => tablesAddressedBy
84
119
  });
85
120
  module.exports = __toCommonJS(index_exports);
86
121
 
87
- // src/tokens.ts
88
- var CLI_CONTEXT_CONFIG_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform-cli/Config");
89
- var USER_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform-cli/UserPort");
90
- var USER_MANAGEMENT_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform-cli/UserManagementPort");
91
- var AUDIT_QUERY_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform-cli/AuditQueryPort");
92
- var DOCTOR_CHECKS_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform-cli/DoctorChecks");
93
- var MANIFEST_ACCESS_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform-cli/ManifestAccessPort");
94
- var MANIFEST_CHECKS_TOKEN = /* @__PURE__ */ Symbol.for("saas-platform-cli/ManifestChecks");
122
+ // src/cli.tokens.ts
123
+ var CLI_CONTEXT_CONFIG_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/cli/Config");
124
+ var USER_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/cli/UserPort");
125
+ var USER_MANAGEMENT_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/cli/UserManagementPort");
126
+ var AUDIT_QUERY_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/cli/AuditQueryPort");
127
+ var DOCTOR_CHECKS_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/cli/DoctorChecks");
128
+ var MANIFEST_ACCESS_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/cli/ManifestAccessPort");
129
+ var MANIFEST_CHECKS_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/cli/ManifestChecks");
95
130
 
96
131
  // src/cli-context.service.ts
97
132
  var os = __toESM(require("os"), 1);
@@ -1155,21 +1190,21 @@ function structuralOnly(line) {
1155
1190
  return stripLineComment(blankStringLiterals(line));
1156
1191
  }
1157
1192
  __name(structuralOnly, "structuralOnly");
1158
- function extractBlockNames(schema, keyword) {
1193
+ function extractBlockNames(schema2, keyword) {
1159
1194
  const pattern = declarationPattern(keyword);
1160
1195
  const names = [];
1161
- for (const line of schema.split("\n")) {
1196
+ for (const line of schema2.split("\n")) {
1162
1197
  const match = structuralOnly(line).match(pattern);
1163
1198
  if (match) names.push(match[1]);
1164
1199
  }
1165
1200
  return names;
1166
1201
  }
1167
1202
  __name(extractBlockNames, "extractBlockNames");
1168
- function extractBlocks(schema, keyword) {
1203
+ function extractBlocks(schema2, keyword) {
1169
1204
  const pattern = declarationPattern(keyword);
1170
1205
  const blocks = /* @__PURE__ */ new Map();
1171
1206
  let current = null;
1172
- for (const rawLine of schema.split("\n")) {
1207
+ for (const rawLine of schema2.split("\n")) {
1173
1208
  const stripped = structuralOnly(rawLine);
1174
1209
  const openCount = (stripped.match(/\{/g) ?? []).length;
1175
1210
  const closeCount = (stripped.match(/\}/g) ?? []).length;
@@ -1204,16 +1239,16 @@ function blockBodyLines(block) {
1204
1239
  __name(blockBodyLines, "blockBodyLines");
1205
1240
 
1206
1241
  // src/schema-apply.ts
1207
- function extractModelNames(schema) {
1208
- return extractBlockNames(schema, "model");
1242
+ function extractModelNames(schema2) {
1243
+ return extractBlockNames(schema2, "model");
1209
1244
  }
1210
1245
  __name(extractModelNames, "extractModelNames");
1211
1246
  function extractModelBlocks(fragment) {
1212
1247
  return extractBlocks(fragment, "model");
1213
1248
  }
1214
1249
  __name(extractModelBlocks, "extractModelBlocks");
1215
- function applyFragmentBlocks(schema, fragmentBlocks, options = {}) {
1216
- const existing = new Set(extractModelNames(schema));
1250
+ function applyFragmentBlocks(schema2, fragmentBlocks, options = {}) {
1251
+ const existing = new Set(extractModelNames(schema2));
1217
1252
  const added = [];
1218
1253
  const skipped = [];
1219
1254
  const additions = [];
@@ -1229,7 +1264,7 @@ function applyFragmentBlocks(schema, fragmentBlocks, options = {}) {
1229
1264
  return {
1230
1265
  added,
1231
1266
  skipped,
1232
- schema
1267
+ schema: schema2
1233
1268
  };
1234
1269
  }
1235
1270
  const header = options.fragmentLabel ? `
@@ -1241,7 +1276,7 @@ function applyFragmentBlocks(schema, fragmentBlocks, options = {}) {
1241
1276
 
1242
1277
  // Inserted by \`saasicat schema apply\`
1243
1278
  `;
1244
- const trimmedSchema = schema.endsWith("\n") ? schema : schema + "\n";
1279
+ const trimmedSchema = schema2.endsWith("\n") ? schema2 : schema2 + "\n";
1245
1280
  return {
1246
1281
  added,
1247
1282
  skipped,
@@ -1299,15 +1334,15 @@ function parseBlockAttributes(name, block) {
1299
1334
  };
1300
1335
  }
1301
1336
  __name(parseBlockAttributes, "parseBlockAttributes");
1302
- function parseSchema(schema) {
1337
+ function parseSchema(schema2) {
1303
1338
  const models = /* @__PURE__ */ new Map();
1304
1339
  const modelAttributes = /* @__PURE__ */ new Map();
1305
- for (const [name, block] of extractBlocks(schema, "model")) {
1340
+ for (const [name, block] of extractBlocks(schema2, "model")) {
1306
1341
  models.set(name, parseFields(block));
1307
1342
  modelAttributes.set(name, parseBlockAttributes(name, block));
1308
1343
  }
1309
1344
  const enums = /* @__PURE__ */ new Map();
1310
- for (const [name, block] of extractBlocks(schema, "enum")) {
1345
+ for (const [name, block] of extractBlocks(schema2, "enum")) {
1311
1346
  enums.set(name, parseEnumValues(block));
1312
1347
  }
1313
1348
  return {
@@ -1433,7 +1468,678 @@ function checkSchema(specSchema, appSchema) {
1433
1468
  }
1434
1469
  __name(checkSchema, "checkSchema");
1435
1470
 
1436
- // src/module.ts
1471
+ // src/migration-constraints.ts
1472
+ var CONSTRAINTS_MARKER = "-- saasicat:constraints";
1473
+ function migrationCreatedBy(before, after) {
1474
+ const existing = new Set(before);
1475
+ const created = after.filter((name) => /^\d{14}_/.test(name) && !existing.has(name)).sort();
1476
+ return created.length > 0 ? created[created.length - 1] : null;
1477
+ }
1478
+ __name(migrationCreatedBy, "migrationCreatedBy");
1479
+ function hasConstraints(migrationSql) {
1480
+ return migrationSql.includes(CONSTRAINTS_MARKER);
1481
+ }
1482
+ __name(hasConstraints, "hasConstraints");
1483
+ function tablesAddressedBy(statement) {
1484
+ return [
1485
+ ...statement.matchAll(/\bON\s+"?(\w+)"?|\bALTER\s+TABLE\s+"?(\w+)"?/gi)
1486
+ ].map((m) => m[1] ?? m[2]);
1487
+ }
1488
+ __name(tablesAddressedBy, "tablesAddressedBy");
1489
+ function constraintsFor(constraintsSql, tables) {
1490
+ const known = new Set(tables);
1491
+ return constraintsSql.split(/\n\s*\n/).filter((block) => {
1492
+ const addressed = tablesAddressedBy(block);
1493
+ return addressed.length === 0 || addressed.every((table) => known.has(table));
1494
+ }).join("\n\n").trimEnd();
1495
+ }
1496
+ __name(constraintsFor, "constraintsFor");
1497
+ function appendConstraints(migrationSql, constraintsSql) {
1498
+ if (hasConstraints(migrationSql)) return migrationSql;
1499
+ if (constraintsSql.trim() === "") return migrationSql;
1500
+ const body = migrationSql.endsWith("\n") ? migrationSql : `${migrationSql}
1501
+ `;
1502
+ return `${body}
1503
+ ${CONSTRAINTS_MARKER} \u2014 appended by \`saasicat schema migrate\`.
1504
+ -- Source: @saasicat/spec/sql/constraints.postgres.sql. These are part of the
1505
+ -- canonical schema: the adapter contract tests run against a database that has
1506
+ -- them. Edit the spec, not this copy.
1507
+ ${constraintsSql.trimEnd()}
1508
+ `;
1509
+ }
1510
+ __name(appendConstraints, "appendConstraints");
1511
+ function reportConstraints(outcome, context) {
1512
+ switch (outcome) {
1513
+ case "appended":
1514
+ return {
1515
+ outcome,
1516
+ mayApply: true,
1517
+ message: ` + appended to ${context.migration}/migration.sql`
1518
+ };
1519
+ case "already-present":
1520
+ return {
1521
+ outcome,
1522
+ mayApply: true,
1523
+ message: ` = ${context.migration} already carries them.`
1524
+ };
1525
+ case "not-applicable":
1526
+ return {
1527
+ outcome,
1528
+ mayApply: true,
1529
+ message: " = none of them apply to the tables in this schema."
1530
+ };
1531
+ case "no-migration":
1532
+ return {
1533
+ outcome,
1534
+ mayApply: true,
1535
+ message: " = Prisma created no migration \u2014 nothing to append to, and nothing new to apply."
1536
+ };
1537
+ case "failed":
1538
+ return {
1539
+ outcome,
1540
+ mayApply: false,
1541
+ message: ` ! Could not append them, so the migration is incomplete. Add ${context.sqlPath} to it by hand, before applying it \u2014 nothing was applied.`
1542
+ };
1543
+ }
1544
+ }
1545
+ __name(reportConstraints, "reportConstraints");
1546
+
1547
+ // src/fk-pointers.ts
1548
+ var POINTER = /^(\s*)\/\/\s*(\w+)(\s+)(Tenant|User)(\??)(\s+)(@relation\(.*\))\s*$/;
1549
+ function findFkPointers(schema2) {
1550
+ const found = [];
1551
+ let model = "";
1552
+ schema2.split("\n").forEach((text, line) => {
1553
+ const opening = /^model\s+(\w+)\s*\{/.exec(text);
1554
+ if (opening) model = opening[1];
1555
+ const match = POINTER.exec(text);
1556
+ if (match) found.push({
1557
+ line,
1558
+ target: match[4],
1559
+ model,
1560
+ text
1561
+ });
1562
+ });
1563
+ return found;
1564
+ }
1565
+ __name(findFkPointers, "findFkPointers");
1566
+ function relationNameOf(relationAttribute) {
1567
+ const match = /@relation\(\s*"([^"]+)"/.exec(relationAttribute);
1568
+ return match ? match[1] : null;
1569
+ }
1570
+ __name(relationNameOf, "relationNameOf");
1571
+ function escapeForRegExp(value) {
1572
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1573
+ }
1574
+ __name(escapeForRegExp, "escapeForRegExp");
1575
+ function modelBody(schema2, model) {
1576
+ const name = escapeForRegExp(model);
1577
+ const block = new RegExp(`(^|\\n)model\\s+${name}\\s*\\{([\\s\\S]*?)\\n\\}`, "m").exec(schema2);
1578
+ return block ? block[2] : null;
1579
+ }
1580
+ __name(modelBody, "modelBody");
1581
+ function isOneToOne(schema2, model, foreignKey) {
1582
+ const body = modelBody(schema2, model);
1583
+ if (!body) return false;
1584
+ return new RegExp(`^\\s*${escapeForRegExp(foreignKey)}\\s+\\S+.*@unique`, "m").test(body);
1585
+ }
1586
+ __name(isOneToOne, "isOneToOne");
1587
+ function hasBackRelation(schema2, model, owner, relationName, singular = false) {
1588
+ const body = modelBody(schema2, owner);
1589
+ if (body === null) return false;
1590
+ const name = escapeForRegExp(model);
1591
+ const shape = singular ? `${name}\\??` : `${name}\\[\\]`;
1592
+ const candidates = [
1593
+ ...body.matchAll(new RegExp(`^\\s*\\w+\\s+${shape}(\\s.*)?$`, "gm"))
1594
+ ];
1595
+ return candidates.some((line) => relationNameOf(line[0]) === relationName);
1596
+ }
1597
+ __name(hasBackRelation, "hasBackRelation");
1598
+ function enableFkPointers(schema2, models) {
1599
+ const lines = schema2.split("\n");
1600
+ const enabled = [];
1601
+ const skipped = [];
1602
+ const needsBackRelation = [];
1603
+ for (const pointer of findFkPointers(schema2)) {
1604
+ const model = pointer.target === "Tenant" ? models.tenant : models.user;
1605
+ if (!model) {
1606
+ skipped.push({
1607
+ line: pointer.line,
1608
+ target: pointer.target
1609
+ });
1610
+ continue;
1611
+ }
1612
+ const match = POINTER.exec(pointer.text);
1613
+ const relationName = relationNameOf(match[7]);
1614
+ const foreignKey = foreignKeyOf(match[7]);
1615
+ const singular = foreignKey !== null && isOneToOne(schema2, pointer.model, foreignKey);
1616
+ if (!hasBackRelation(schema2, pointer.model, model, relationName, singular)) {
1617
+ needsBackRelation.push({
1618
+ line: pointer.line,
1619
+ owner: model,
1620
+ suggestion: backRelationSuggestion(pointer.model, relationName, singular)
1621
+ });
1622
+ continue;
1623
+ }
1624
+ const [, indent, field, gap1, , optional, gap2, relation] = match;
1625
+ lines[pointer.line] = `${indent}${field}${gap1}${model}${optional}${gap2}${relation}`;
1626
+ enabled.push({
1627
+ line: pointer.line,
1628
+ model
1629
+ });
1630
+ }
1631
+ return {
1632
+ schema: lines.join("\n"),
1633
+ enabled,
1634
+ skipped,
1635
+ needsBackRelation
1636
+ };
1637
+ }
1638
+ __name(enableFkPointers, "enableFkPointers");
1639
+ var lowerFirst = /* @__PURE__ */ __name((value) => value.charAt(0).toLowerCase() + value.slice(1), "lowerFirst");
1640
+ function foreignKeyOf(relationAttribute) {
1641
+ const match = /fields:\s*\[\s*(\w+)/.exec(relationAttribute);
1642
+ return match ? match[1] : null;
1643
+ }
1644
+ __name(foreignKeyOf, "foreignKeyOf");
1645
+ function backRelationSuggestion(model, relationName, singular) {
1646
+ const field = singular ? lowerFirst(model) : `${lowerFirst(model)}s`;
1647
+ const type = singular ? `${model}?` : `${model}[]`;
1648
+ return `${field} ${type}` + (relationName ? ` @relation("${relationName}")` : "");
1649
+ }
1650
+ __name(backRelationSuggestion, "backRelationSuggestion");
1651
+ function assertModelsExist(declaredModels, models) {
1652
+ const missing = Object.entries(models).filter(([, name]) => name && !declaredModels.includes(name)).map(([role, name]) => `--${role}-model=${name}`);
1653
+ if (missing.length === 0) return;
1654
+ throw new Error(`${missing.join(", ")} \u2014 no such model in this schema. It declares: ${declaredModels.slice().sort().join(", ")}.`);
1655
+ }
1656
+ __name(assertModelsExist, "assertModelsExist");
1657
+
1658
+ // src/init/catalog-keys.ts
1659
+ var import_spec = require("@saasicat/spec");
1660
+ var schema = import_spec.planCatalogSchema;
1661
+ function required(value, what) {
1662
+ if (value === void 0) {
1663
+ throw new Error(`plan-catalog.schema.json declares no ${what} \u2014 @saasicat/spec and @saasicat/cli are out of step.`);
1664
+ }
1665
+ return value;
1666
+ }
1667
+ __name(required, "required");
1668
+ function projectKeyPattern() {
1669
+ return new RegExp(required(schema.properties?.projectKey?.pattern, "pattern for projectKey"));
1670
+ }
1671
+ __name(projectKeyPattern, "projectKeyPattern");
1672
+ function quotaKeyPattern() {
1673
+ const patterns = Object.keys(required(schema.$defs?.PlanDef?.properties?.quotas?.patternProperties, "quota key pattern"));
1674
+ if (patterns.length !== 1) {
1675
+ throw new Error(`plan-catalog.schema.json declares ${patterns.length} quota key patterns; this derivation can express one.`);
1676
+ }
1677
+ return new RegExp(patterns[0]);
1678
+ }
1679
+ __name(quotaKeyPattern, "quotaKeyPattern");
1680
+ function minimumQuotasPerPlan() {
1681
+ return required(schema.$defs?.PlanDef?.properties?.quotas?.minProperties, "minProperties");
1682
+ }
1683
+ __name(minimumQuotasPerPlan, "minimumQuotasPerPlan");
1684
+ function assertValidProjectKey(projectKey) {
1685
+ const pattern = projectKeyPattern();
1686
+ if (pattern.test(projectKey)) return;
1687
+ throw new Error(`--project-key=${projectKey} is not a valid project key. It has to match ${pattern.source} \u2014 lower case, starting with a letter, at least two characters. The platform validates config/saas.yaml against the same pattern at boot, so this would fail after every file was written.`);
1688
+ }
1689
+ __name(assertValidProjectKey, "assertValidProjectKey");
1690
+ function assertValidQuotaKey(quotaKey) {
1691
+ const pattern = quotaKeyPattern();
1692
+ if (pattern.test(quotaKey)) return;
1693
+ throw new Error(`--quota=${quotaKey}:\u2026 is not a valid quota key. It has to match ${pattern.source} \u2014 lower camel case, no separators, so \`activeSeats\` rather than \`active-seats\` or \`active_seats\`. The plan's \`quotas\` object forbids additional properties, so the platform rejects config/saas.yaml at boot otherwise.`);
1694
+ }
1695
+ __name(assertValidQuotaKey, "assertValidQuotaKey");
1696
+
1697
+ // src/init/plan.ts
1698
+ function parseQuota(spec) {
1699
+ const [key, model] = spec.split(":");
1700
+ if (!key) throw new Error(`--quota needs a key: got '${spec}'`);
1701
+ assertValidQuotaKey(key);
1702
+ return {
1703
+ key,
1704
+ model: delegateName(model ?? key)
1705
+ };
1706
+ }
1707
+ __name(parseQuota, "parseQuota");
1708
+ var delegateName = /* @__PURE__ */ __name((model) => model.charAt(0).toLowerCase() + model.slice(1), "delegateName");
1709
+ function assertEnoughQuotas(quotas) {
1710
+ const minimum = minimumQuotasPerPlan();
1711
+ if (quotas.length >= minimum) return;
1712
+ throw new Error(`init needs at least ${minimum} --quota=<key>:<Model>. Every plan in config/saas.yaml must declare one, and the platform refuses a catalogue without it \u2014 so a project generated without one cannot boot. Name what your app counts, for example --quota=notes:Note; the generated provider is where you say how to count it.`);
1713
+ }
1714
+ __name(assertEnoughQuotas, "assertEnoughQuotas");
1715
+ function pascalCase(value) {
1716
+ return value.split(/[^A-Za-z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
1717
+ }
1718
+ __name(pascalCase, "pascalCase");
1719
+ var quotaFileName = /* @__PURE__ */ __name((key) => `${key.replace(/[^A-Za-z0-9]+/g, "-").toLowerCase()}-quota.provider.ts`, "quotaFileName");
1720
+ function planInit(options) {
1721
+ const projectKey = options.projectKey;
1722
+ if (!projectKey) throw new Error("init needs a --project-key.");
1723
+ assertValidProjectKey(projectKey);
1724
+ const appLabel = options.appName ?? pascalCase(projectKey);
1725
+ const appName = pascalCase(appLabel);
1726
+ const apiBase = options.apiBase ?? "/api/v1/admin";
1727
+ const quotas = (options.quotas ?? []).map(parseQuota);
1728
+ assertEnoughQuotas(quotas);
1729
+ const hasherClass = options.skipHasher ? null : `${appName}PasswordHasher`;
1730
+ const featureKey = `${projectKey.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase()}_CORE`;
1731
+ const shared = {
1732
+ PROJECT_KEY: projectKey,
1733
+ APP_NAME: appName,
1734
+ APP_LABEL: appLabel,
1735
+ API_BASE: apiBase,
1736
+ FEATURE_KEY: featureKey,
1737
+ REGISTRY_CONST: `${constantCase(projectKey)}_FEATURE_UI_REGISTRY`,
1738
+ MANIFEST_CONST: `${constantCase(projectKey)}_MANIFEST_CONTRIBUTION`,
1739
+ ADMIN_MODULE_CLASS: `${appName}AdminModule`,
1740
+ HASHER_CLASS: hasherClass ?? "",
1741
+ HASHER_FILE: hasherClass ? `${kebabCase(appName)}-password.hasher` : "",
1742
+ STARTER_QUOTAS: renderQuotaBlock(quotas, 25),
1743
+ PRO_QUOTAS: renderQuotaBlock(quotas, 1e3)
1744
+ };
1745
+ const files = [
1746
+ {
1747
+ path: "config/saas.yaml",
1748
+ template: "config/saas.yaml",
1749
+ tokens: {}
1750
+ },
1751
+ {
1752
+ path: "src/saas/feature-ui-registry.ts",
1753
+ template: "src/saas/feature-ui-registry.ts",
1754
+ tokens: {}
1755
+ },
1756
+ {
1757
+ path: "src/saas/admin-manifest.contribution.ts",
1758
+ template: "src/saas/admin-manifest.contribution.ts",
1759
+ tokens: {}
1760
+ },
1761
+ {
1762
+ path: `src/saas/${kebabCase(appName)}-admin.module.ts`,
1763
+ template: "src/saas/admin.module.ts",
1764
+ tokens: {}
1765
+ }
1766
+ ];
1767
+ files.push({
1768
+ path: "src/saas/persistence.ts",
1769
+ template: hasherClass ? "src/saas/persistence.ts" : "src/saas/persistence-without-hasher.ts",
1770
+ tokens: {}
1771
+ });
1772
+ if (hasherClass) {
1773
+ files.push({
1774
+ path: `src/auth/${kebabCase(appName)}-password.hasher.ts`,
1775
+ template: "src/auth/password.hasher.ts",
1776
+ tokens: {}
1777
+ });
1778
+ }
1779
+ const quotaProviders = quotas.map((quota) => ({
1780
+ className: `${pascalCase(quota.key)}QuotaProvider`,
1781
+ path: `src/saas/${quotaFileName(quota.key)}`
1782
+ }));
1783
+ for (const quota of quotas) {
1784
+ files.push({
1785
+ path: `src/saas/${quotaFileName(quota.key)}`,
1786
+ template: "src/saas/quota.provider.ts",
1787
+ tokens: {
1788
+ QUOTA_KEY: quota.key,
1789
+ QUOTA_LABEL: `${pascalCase(quota.key)} count`,
1790
+ QUOTA_CLASS: `${pascalCase(quota.key)}QuotaProvider`,
1791
+ QUOTA_MODEL: quota.model
1792
+ }
1793
+ });
1794
+ }
1795
+ return {
1796
+ files,
1797
+ tokens: shared,
1798
+ quotaProviders,
1799
+ hasherClass
1800
+ };
1801
+ }
1802
+ __name(planInit, "planInit");
1803
+ var constantCase = /* @__PURE__ */ __name((value) => value.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase(), "constantCase");
1804
+ var kebabCase = /* @__PURE__ */ __name((value) => value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^A-Za-z0-9]+/g, "-").toLowerCase(), "kebabCase");
1805
+ function renderQuotaBlock(quotas, limit) {
1806
+ if (quotas.length === 0) return " {}";
1807
+ return quotas.map((quota) => `
1808
+ ${quota.key}: ${limit}`).join("");
1809
+ }
1810
+ __name(renderQuotaBlock, "renderQuotaBlock");
1811
+ function applyTokens(content, tokens) {
1812
+ return content.replace(/__([A-Z_]+)__/g, (full, key) => tokens[key] === void 0 ? full : tokens[key]);
1813
+ }
1814
+ __name(applyTokens, "applyTokens");
1815
+ function patchOptionsFor(plan) {
1816
+ const generates = /* @__PURE__ */ __name((path) => plan.files.some((file) => file.path === path), "generates");
1817
+ return {
1818
+ persistenceImport: generates("src/saas/persistence.ts") ? "./saas/persistence" : null,
1819
+ adminModule: {
1820
+ className: plan.tokens.ADMIN_MODULE_CLASS,
1821
+ importPath: `./saas/${kebabCase(plan.tokens.APP_NAME)}-admin.module`
1822
+ },
1823
+ // Read off the plan, not re-derived: the file it wrote is the file the
1824
+ // import has to name.
1825
+ quotaProviders: plan.quotaProviders.map(({ className, path }) => ({
1826
+ className,
1827
+ importPath: `./${path.replace(/^src\//, "").replace(/\.ts$/, "")}`
1828
+ })),
1829
+ registry: {
1830
+ constName: plan.tokens.REGISTRY_CONST,
1831
+ importPath: "./saas/feature-ui-registry"
1832
+ }
1833
+ };
1834
+ }
1835
+ __name(patchOptionsFor, "patchOptionsFor");
1836
+
1837
+ // src/codemods/v1-imports.ts
1838
+ var PUBLIC_PREFIXES = [
1839
+ "ui/",
1840
+ "layouts/",
1841
+ "auth/",
1842
+ "pages/"
1843
+ ];
1844
+ function buildImportMap(table) {
1845
+ const map = /* @__PURE__ */ new Map();
1846
+ for (const [from, to] of Object.entries(table.moves)) {
1847
+ const onSurface = PUBLIC_PREFIXES.some((prefix) => to.startsWith(prefix));
1848
+ if (!onSurface && !to.startsWith("@")) continue;
1849
+ if (from.startsWith("components/")) {
1850
+ map.set(from, to);
1851
+ continue;
1852
+ }
1853
+ if (!from.startsWith("pages-standard/")) continue;
1854
+ const file = from.slice("pages-standard/".length);
1855
+ if (file.includes("/")) continue;
1856
+ map.set(from, to);
1857
+ map.set(`pages/${file}`, to);
1858
+ }
1859
+ for (const [from, to] of Object.entries(table.packages ?? {})) {
1860
+ if (from === "_") continue;
1861
+ map.set(from, to);
1862
+ }
1863
+ for (const [from, to] of Object.entries(table.moveDirectories ?? {})) {
1864
+ map.set(`${from}/`, `${to}/`);
1865
+ }
1866
+ return map;
1867
+ }
1868
+ __name(buildImportMap, "buildImportMap");
1869
+ function wentPrivate(map, subpath) {
1870
+ for (const [prefix, target] of map) {
1871
+ if (prefix.endsWith("/") && target.startsWith("internal/") && subpath.startsWith(prefix)) {
1872
+ return true;
1873
+ }
1874
+ }
1875
+ return false;
1876
+ }
1877
+ __name(wentPrivate, "wentPrivate");
1878
+ function rewriteSubpath(map, subpath) {
1879
+ const direct = map.get(subpath);
1880
+ if (direct !== void 0 && direct !== subpath) return direct;
1881
+ for (const [prefix, target] of map) {
1882
+ if (prefix.endsWith("/") && target.startsWith("@") && subpath.startsWith(prefix)) {
1883
+ return `${target}${subpath.slice(prefix.length)}`;
1884
+ }
1885
+ }
1886
+ if (subpath.startsWith("pages-standard/") && !wentPrivate(map, subpath)) {
1887
+ const file = subpath.slice("pages-standard/".length);
1888
+ if (!file.includes("/")) return `pages/${file}`;
1889
+ }
1890
+ return null;
1891
+ }
1892
+ __name(rewriteSubpath, "rewriteSubpath");
1893
+ function isNoLongerPublic(map, subpath) {
1894
+ if (subpath.startsWith("components/")) return !map.has(subpath);
1895
+ return subpath.startsWith("pages-standard/") && wentPrivate(map, subpath);
1896
+ }
1897
+ __name(isNoLongerPublic, "isNoLongerPublic");
1898
+ var UI_VUE_SPECIFIER = /@saasicat\/ui-vue\/([A-Za-z0-9/_.-]+)/g;
1899
+ function rewriteImports(text, map) {
1900
+ const unmapped = /* @__PURE__ */ new Map();
1901
+ let rewritten = 0;
1902
+ const next = text.replace(UI_VUE_SPECIFIER, (whole, subpath) => {
1903
+ const to = rewriteSubpath(map, subpath);
1904
+ if (to === null) {
1905
+ if (isNoLongerPublic(map, subpath)) {
1906
+ unmapped.set(subpath, (unmapped.get(subpath) ?? 0) + 1);
1907
+ }
1908
+ return whole;
1909
+ }
1910
+ rewritten += 1;
1911
+ return to.startsWith("@") ? to : `@saasicat/ui-vue/${to}`;
1912
+ });
1913
+ return {
1914
+ text: next,
1915
+ rewritten,
1916
+ unmapped
1917
+ };
1918
+ }
1919
+ __name(rewriteImports, "rewriteImports");
1920
+
1921
+ // src/codemods/v1-rename.ts
1922
+ var escape = /* @__PURE__ */ __name((s) => s.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&"), "escape");
1923
+ var FROM_SPECIFIER = /from\s*(['"])([^'"]+)\1/g;
1924
+ var isSpace = /* @__PURE__ */ __name((ch) => ch === " " || ch === " " || ch === "\n" || ch === "\r", "isSpace");
1925
+ function namedImports(text) {
1926
+ const found = [];
1927
+ for (const match of text.matchAll(FROM_SPECIFIER)) {
1928
+ let i = (match.index ?? 0) - 1;
1929
+ while (i >= 0 && isSpace(text[i])) i -= 1;
1930
+ if (text[i] !== "}") continue;
1931
+ const close = i;
1932
+ const open = text.lastIndexOf("{", close);
1933
+ if (open < 0) continue;
1934
+ i = open - 1;
1935
+ while (i >= 0 && isSpace(text[i])) i -= 1;
1936
+ let head = text.slice(Math.max(0, i - 3), i + 1);
1937
+ if (head === "type") {
1938
+ i -= 4;
1939
+ while (i >= 0 && isSpace(text[i])) i -= 1;
1940
+ head = text.slice(Math.max(0, i - 5), i + 1);
1941
+ } else {
1942
+ head = text.slice(Math.max(0, i - 5), i + 1);
1943
+ }
1944
+ if (head !== "import") continue;
1945
+ const names = text.slice(open + 1, close).split(",").map((raw) => {
1946
+ const words = raw.trim().split(/\s+/);
1947
+ if (words[0] === "type") words.shift();
1948
+ return words[0] ?? "";
1949
+ }).filter((name) => name.length > 0);
1950
+ found.push({
1951
+ names,
1952
+ specifier: match[2]
1953
+ });
1954
+ }
1955
+ return found;
1956
+ }
1957
+ __name(namedImports, "namedImports");
1958
+ function rewriteNames(text, table) {
1959
+ let next = text;
1960
+ let rewritten = 0;
1961
+ const ambiguous = /* @__PURE__ */ new Set();
1962
+ const perEntry = /* @__PURE__ */ new Map();
1963
+ for (const { names, specifier } of namedImports(text)) {
1964
+ const mapping = table.entryTokens[specifier];
1965
+ for (const name of names) {
1966
+ const knownSomewhere = Object.values(table.entryTokens).some((m) => name in m);
1967
+ if (!knownSomewhere) continue;
1968
+ const already = perEntry.get(name);
1969
+ if (mapping && name in mapping && (already === void 0 || already === mapping[name])) {
1970
+ perEntry.set(name, mapping[name]);
1971
+ } else {
1972
+ if (already !== void 0) perEntry.delete(name);
1973
+ ambiguous.add(`${name} from '${specifier}'`);
1974
+ }
1975
+ }
1976
+ }
1977
+ for (const [from, to] of perEntry) {
1978
+ next = next.replace(new RegExp(`\\b${escape(from)}\\b`, "g"), () => {
1979
+ rewritten += 1;
1980
+ return to;
1981
+ });
1982
+ }
1983
+ for (const [from, to] of Object.entries(table.identifierStems)) {
1984
+ next = next.replace(new RegExp(escape(from), "g"), () => {
1985
+ rewritten += 1;
1986
+ return to;
1987
+ });
1988
+ }
1989
+ for (const [from, to] of Object.entries(table.registryKeys)) {
1990
+ const symbolFor = new RegExp(`(Symbol\\.for\\(\\s*['"\`])${escape(from)}`, "g");
1991
+ next = next.replace(symbolFor, (_, head) => {
1992
+ rewritten += 1;
1993
+ return `${head}${to}`;
1994
+ });
1995
+ if (from.startsWith("@")) continue;
1996
+ const literal = new RegExp(`(['"\`])${escape(from)}`, "g");
1997
+ next = next.replace(literal, (_, quote) => {
1998
+ rewritten += 1;
1999
+ return `${quote}${to}`;
2000
+ });
2001
+ }
2002
+ for (const [from, to] of Object.entries(table.subpaths)) {
2003
+ next = next.replace(new RegExp(escape(from), "g"), () => {
2004
+ rewritten += 1;
2005
+ return to;
2006
+ });
2007
+ }
2008
+ return {
2009
+ text: next,
2010
+ rewritten,
2011
+ ambiguous: [
2012
+ ...ambiguous
2013
+ ].sort()
2014
+ };
2015
+ }
2016
+ __name(rewriteNames, "rewriteNames");
2017
+
2018
+ // src/init/patch-app-module.ts
2019
+ var MARKER = "SaaSiCatModule.forRoot";
2020
+ function patchAppModule(source, options) {
2021
+ const block = renderForRootBlock(options);
2022
+ const manualBlock = `${renderImports(options)}
2023
+
2024
+ ${block}`;
2025
+ if (source.includes(MARKER)) {
2026
+ return {
2027
+ source,
2028
+ status: "already-wired",
2029
+ reason: "",
2030
+ manualBlock: ""
2031
+ };
2032
+ }
2033
+ const importsArray = findImportsArray(source);
2034
+ if (!importsArray) {
2035
+ return {
2036
+ source,
2037
+ status: "declined",
2038
+ reason: "no `@Module({ imports: [ ... ] })` was found in this file, so there is nowhere to add the platform without guessing at the structure",
2039
+ manualBlock
2040
+ };
2041
+ }
2042
+ const withImports = addImportStatements(source, options);
2043
+ const target = findImportsArray(withImports);
2044
+ const indent = " ".repeat(target.indent + 4);
2045
+ const entries = `${block},
2046
+ ${options.adminModule.className},`;
2047
+ const wasEmpty = /^\s*\]/.test(withImports.slice(target.openBracket + 1));
2048
+ const trailing = wasEmpty ? " ".repeat(target.indent) : indent;
2049
+ const inserted = `
2050
+ ${indent}${entries.split("\n").join(`
2051
+ ${indent}`)}
2052
+ ${trailing}`;
2053
+ return {
2054
+ source: withImports.slice(0, target.openBracket + 1) + inserted + withImports.slice(target.openBracket + 1),
2055
+ status: "patched",
2056
+ reason: "",
2057
+ manualBlock: ""
2058
+ };
2059
+ }
2060
+ __name(patchAppModule, "patchAppModule");
2061
+ function findImportsArray(source) {
2062
+ const decorator = source.indexOf("@Module(");
2063
+ if (decorator === -1) return null;
2064
+ const match = /(^|\n)([ \t]*)imports\s*:\s*\[/.exec(source.slice(decorator));
2065
+ if (!match) return null;
2066
+ const openBracket = decorator + match.index + match[0].length - 1;
2067
+ return {
2068
+ openBracket,
2069
+ indent: match[2].length
2070
+ };
2071
+ }
2072
+ __name(findImportsArray, "findImportsArray");
2073
+ function renderImports(options) {
2074
+ const lines = [
2075
+ "import { loadPlanCatalogFromFile } from '@saasicat/nest/billing';",
2076
+ "import { SaaSiCatModule, defineSaaSiCat } from '@saasicat/nest/platform';",
2077
+ `import { ${options.registry.constName} } from '${options.registry.importPath}';`,
2078
+ `import { ${options.adminModule.className} } from '${options.adminModule.importPath}';`
2079
+ ];
2080
+ if (options.persistenceImport) {
2081
+ lines.push(`import { persistence } from '${options.persistenceImport}';`);
2082
+ }
2083
+ for (const quota of options.quotaProviders) {
2084
+ lines.push(`import { ${quota.className} } from '${quota.importPath}';`);
2085
+ }
2086
+ return lines.join("\n");
2087
+ }
2088
+ __name(renderImports, "renderImports");
2089
+ function addImportStatements(source, options) {
2090
+ const lines = source.split("\n");
2091
+ const block = renderImports(options).split("\n");
2092
+ lines.splice(endOfLastImport(lines) + 1, 0, ...block);
2093
+ return lines.join("\n");
2094
+ }
2095
+ __name(addImportStatements, "addImportStatements");
2096
+ function endOfLastImport(lines) {
2097
+ let end = -1;
2098
+ let open = false;
2099
+ for (let index = 0; index < lines.length; index += 1) {
2100
+ const line = lines[index];
2101
+ if (!open && !/^\s*import\s/.test(line)) continue;
2102
+ open = true;
2103
+ if (/\bfrom\s+['"][^'"]+['"]/.test(line) || /^\s*import\s+['"][^'"]+['"]/.test(line)) {
2104
+ end = index;
2105
+ open = false;
2106
+ }
2107
+ }
2108
+ return end;
2109
+ }
2110
+ __name(endOfLastImport, "endOfLastImport");
2111
+ function renderForRootBlock(options) {
2112
+ const quotaList = options.quotaProviders.map((q) => q.className).join(", ");
2113
+ return [
2114
+ "SaaSiCatModule.forRoot(",
2115
+ " defineSaaSiCat({",
2116
+ " // Plans straight from the YAML. Apps that manage plans in the",
2117
+ " // SuperAdmin UI pass `dbCatalog` instead.",
2118
+ " planCatalog: loadPlanCatalogFromFile({ path: 'config/saas.yaml' }),",
2119
+ " // Your authentication guard. This does NOT compile until you",
2120
+ " // name one, and that is deliberate: an empty array is how the",
2121
+ " // platform is told an endpoint should be auth-free, so a",
2122
+ " // placeholder `[]` here would publish GET /admin/discovery \u2014",
2123
+ " // your whole capability inventory \u2014 and the manifest routes to",
2124
+ " // anyone who asks. Import your guard and put it in.",
2125
+ " controller: { guards: [YourAuthGuard] },",
2126
+ options.persistenceImport ? " persistence," : " // persistence: prismaPersistence({ client: PrismaService }),",
2127
+ " catalog: { featureUiRegistry: " + options.registry.constName + " },",
2128
+ " adminResources: true,",
2129
+ " promoCodes: true,",
2130
+ quotaList ? ` quotaProviders: [${quotaList}],` : " quotaProviders: [],",
2131
+ " }),",
2132
+ ")"
2133
+ ].join("\n");
2134
+ }
2135
+ __name(renderForRootBlock, "renderForRootBlock");
2136
+ var LIMIT_FILTER_PROVIDER = "{ provide: APP_FILTER, useClass: LimitExceededFilter }";
2137
+ var LIMIT_FILTER_IMPORTS = [
2138
+ "import { APP_FILTER } from '@nestjs/core';",
2139
+ "import { LimitExceededFilter } from '@saasicat/nest/billing';"
2140
+ ].join("\n");
2141
+
2142
+ // src/cli-context.module.ts
1437
2143
  var import_common8 = require("@nestjs/common");
1438
2144
  var import_nest5 = require("@saasicat/nest");
1439
2145
  function _ts_decorate8(decorators, target, key, desc) {
@@ -2495,6 +3201,7 @@ UserCommands = _ts_decorate14([
2495
3201
  AuditTailCommand,
2496
3202
  AuditTailFlow,
2497
3203
  CLI_CONTEXT_CONFIG_TOKEN,
3204
+ CONSTRAINTS_MARKER,
2498
3205
  CliContextModule,
2499
3206
  CliContextService,
2500
3207
  CliError,
@@ -2505,6 +3212,8 @@ UserCommands = _ts_decorate14([
2505
3212
  DiscoverySnapshotDoctorCheck,
2506
3213
  DoctorCommands,
2507
3214
  DoctorFlow,
3215
+ LIMIT_FILTER_IMPORTS,
3216
+ LIMIT_FILTER_PROVIDER,
2508
3217
  MANIFEST_ACCESS_PORT_TOKEN,
2509
3218
  MANIFEST_CHECKS_TOKEN,
2510
3219
  ManifestCheckCommand,
@@ -2516,24 +3225,56 @@ UserCommands = _ts_decorate14([
2516
3225
  MfaSetupFlow,
2517
3226
  PLATFORM_DOCTOR_CHECK_PROVIDERS,
2518
3227
  PlanCatalogDoctorCheck,
3228
+ UI_VUE_SPECIFIER,
2519
3229
  USER_MANAGEMENT_PORT_TOKEN,
2520
3230
  USER_PORT_TOKEN,
2521
3231
  UserCommands,
2522
3232
  UserPortDoctorCheck,
2523
3233
  WhoAmIFlow,
3234
+ appendConstraints,
2524
3235
  applyFragmentBlocks,
3236
+ applyTokens,
3237
+ assertModelsExist,
3238
+ assertValidProjectKey,
3239
+ assertValidQuotaKey,
2525
3240
  blankStringLiterals,
2526
3241
  blockBodyLines,
2527
3242
  breaksContract,
3243
+ buildImportMap,
2528
3244
  checkSchema,
3245
+ constraintsFor,
3246
+ enableFkPointers,
2529
3247
  extractBlockNames,
2530
3248
  extractBlocks,
2531
3249
  extractModelBlocks,
2532
3250
  extractModelNames,
3251
+ findFkPointers,
3252
+ foreignKeyOf,
3253
+ hasBackRelation,
3254
+ hasConstraints,
3255
+ isNoLongerPublic,
3256
+ isOneToOne,
3257
+ kebabCase,
3258
+ migrationCreatedBy,
3259
+ minimumQuotasPerPlan,
3260
+ namedImports,
2533
3261
  parseBlockAttributes,
2534
3262
  parseEnumValues,
2535
3263
  parseFields,
3264
+ parseQuota,
2536
3265
  parseSchema,
3266
+ pascalCase,
3267
+ patchAppModule,
3268
+ patchOptionsFor,
3269
+ planInit,
3270
+ projectKeyPattern,
3271
+ quotaKeyPattern,
3272
+ relationNameOf,
3273
+ reportConstraints,
3274
+ rewriteImports,
3275
+ rewriteNames,
3276
+ rewriteSubpath,
2537
3277
  stripLineComment,
2538
- structuralOnly
3278
+ structuralOnly,
3279
+ tablesAddressedBy
2539
3280
  });