@saasicat/cli 1.0.0-rc.14 → 1.0.0-rc.16

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/bin/saasicat.js CHANGED
@@ -290,6 +290,16 @@ function printCheckReport(report) {
290
290
  console.log('');
291
291
  }
292
292
 
293
+ if (report.tenantCascades.length > 0) {
294
+ console.log(`✗ Deleted together with the tenant (${report.tenantCascades.length}):`);
295
+ for (const { model, field } of report.tenantCascades) {
296
+ console.log(` ${`${model}.${field}`.padEnd(44)} onDelete: Cascade`);
297
+ }
298
+ console.log(' These records outlive their tenant — a contract is kept for tax purposes.');
299
+ console.log(' Remove the relation; `tenantId` stays as a trace.');
300
+ console.log('');
301
+ }
302
+
293
303
  const breaking = report.missingBlockAttributes.filter((a) => a.kind !== 'index');
294
304
  if (breaking.length > 0) {
295
305
  console.log(`✗ Missing constraints (${breaking.length}):`);
package/dist/.build-stamp CHANGED
@@ -1 +1 @@
1
- e5be15ecef9e7070e3a31f68ab42c3a6f1225f21c914a6c55a4b6ed7345809c3
1
+ 2c53f8cea2ea7de0b41c9b894cdc45c45c9173c816c239c3afa50d5b9326914e
package/dist/index.cjs CHANGED
@@ -109,6 +109,7 @@ __export(index_exports, {
109
109
  kebabCase: () => kebabCase,
110
110
  migrationCreatedBy: () => migrationCreatedBy,
111
111
  minimumQuotasPerPlan: () => minimumQuotasPerPlan,
112
+ modelsKeptPastTheTenant: () => modelsKeptPastTheTenant,
112
113
  namedImports: () => namedImports,
113
114
  parseBlockAttributes: () => parseBlockAttributes,
114
115
  parseEnumValues: () => parseEnumValues,
@@ -1335,6 +1336,117 @@ function applyFragmentBlocks(schema2, fragmentBlocks, options = {}) {
1335
1336
  }
1336
1337
  __name(applyFragmentBlocks, "applyFragmentBlocks");
1337
1338
 
1339
+ // src/fk-pointers.ts
1340
+ var POINTER = /^(\s*)\/\/\s*(\w+)(\s+)(Tenant|User)(\??)(\s+)(@relation\(.*\))\s*$/;
1341
+ function findFkPointers(schema2) {
1342
+ const found = [];
1343
+ let model = "";
1344
+ schema2.split("\n").forEach((text, line) => {
1345
+ const opening = /^model\s+(\w+)\s*\{/.exec(text);
1346
+ if (opening) model = opening[1];
1347
+ const match = POINTER.exec(text);
1348
+ if (match) found.push({
1349
+ line,
1350
+ target: match[4],
1351
+ model,
1352
+ text
1353
+ });
1354
+ });
1355
+ return found;
1356
+ }
1357
+ __name(findFkPointers, "findFkPointers");
1358
+ function relationNameOf(relationAttribute) {
1359
+ const match = /@relation\(\s*"([^"]+)"/.exec(relationAttribute);
1360
+ return match ? match[1] : null;
1361
+ }
1362
+ __name(relationNameOf, "relationNameOf");
1363
+ function escapeForRegExp(value) {
1364
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1365
+ }
1366
+ __name(escapeForRegExp, "escapeForRegExp");
1367
+ function modelBody(schema2, model) {
1368
+ const name = escapeForRegExp(model);
1369
+ const block = new RegExp(`(^|\\n)model\\s+${name}\\s*\\{([\\s\\S]*?)\\n\\}`, "m").exec(schema2);
1370
+ return block ? block[2] : null;
1371
+ }
1372
+ __name(modelBody, "modelBody");
1373
+ function isOneToOne(schema2, model, foreignKey) {
1374
+ const body = modelBody(schema2, model);
1375
+ if (!body) return false;
1376
+ return new RegExp(`^\\s*${escapeForRegExp(foreignKey)}\\s+\\S+.*@unique`, "m").test(body);
1377
+ }
1378
+ __name(isOneToOne, "isOneToOne");
1379
+ function hasBackRelation(schema2, model, owner, relationName, singular = false) {
1380
+ const body = modelBody(schema2, owner);
1381
+ if (body === null) return false;
1382
+ const name = escapeForRegExp(model);
1383
+ const shape = singular ? `${name}\\??` : `${name}\\[\\]`;
1384
+ const candidates = [
1385
+ ...body.matchAll(new RegExp(`^\\s*\\w+\\s+${shape}(\\s.*)?$`, "gm"))
1386
+ ];
1387
+ return candidates.some((line) => relationNameOf(line[0]) === relationName);
1388
+ }
1389
+ __name(hasBackRelation, "hasBackRelation");
1390
+ function enableFkPointers(schema2, models) {
1391
+ const lines = schema2.split("\n");
1392
+ const enabled = [];
1393
+ const skipped = [];
1394
+ const needsBackRelation = [];
1395
+ for (const pointer of findFkPointers(schema2)) {
1396
+ const model = pointer.target === "Tenant" ? models.tenant : models.user;
1397
+ if (!model) {
1398
+ skipped.push({
1399
+ line: pointer.line,
1400
+ target: pointer.target
1401
+ });
1402
+ continue;
1403
+ }
1404
+ const match = POINTER.exec(pointer.text);
1405
+ const relationName = relationNameOf(match[7]);
1406
+ const foreignKey = foreignKeyOf(match[7]);
1407
+ const singular = foreignKey !== null && isOneToOne(schema2, pointer.model, foreignKey);
1408
+ if (!hasBackRelation(schema2, pointer.model, model, relationName, singular)) {
1409
+ needsBackRelation.push({
1410
+ line: pointer.line,
1411
+ owner: model,
1412
+ suggestion: backRelationSuggestion(pointer.model, relationName, singular)
1413
+ });
1414
+ continue;
1415
+ }
1416
+ const [, indent, field, gap1, , optional, gap2, relation] = match;
1417
+ lines[pointer.line] = `${indent}${field}${gap1}${model}${optional}${gap2}${relation}`;
1418
+ enabled.push({
1419
+ line: pointer.line,
1420
+ model
1421
+ });
1422
+ }
1423
+ return {
1424
+ schema: lines.join("\n"),
1425
+ enabled,
1426
+ skipped,
1427
+ needsBackRelation
1428
+ };
1429
+ }
1430
+ __name(enableFkPointers, "enableFkPointers");
1431
+ var lowerFirst = /* @__PURE__ */ __name((value) => value.charAt(0).toLowerCase() + value.slice(1), "lowerFirst");
1432
+ function foreignKeyOf(relationAttribute) {
1433
+ const match = /fields:\s*\[\s*(\w+)/.exec(relationAttribute);
1434
+ return match ? match[1] : null;
1435
+ }
1436
+ __name(foreignKeyOf, "foreignKeyOf");
1437
+ function backRelationSuggestion(model, relationName, singular) {
1438
+ const field = singular ? lowerFirst(model) : `${lowerFirst(model)}s`;
1439
+ const type = singular ? `${model}?` : `${model}[]`;
1440
+ return `${field} ${type}` + (relationName ? ` @relation("${relationName}")` : "");
1441
+ }
1442
+ __name(backRelationSuggestion, "backRelationSuggestion");
1443
+ function assertModelsExist(declaredModels, models) {
1444
+ const missing = Object.entries(models).filter(([, name]) => name && !declaredModels.includes(name)).map(([role, name]) => `--${role}-model=${name}`);
1445
+ if (missing.length === 0) return;
1446
+ throw new Error(`${missing.join(", ")} \u2014 no such model in this schema. It declares: ${declaredModels.slice().sort().join(", ")}.`);
1447
+ }
1448
+ __name(assertModelsExist, "assertModelsExist");
1449
+
1338
1450
  // src/schema-check.ts
1339
1451
  function renderType(signature) {
1340
1452
  return `${signature.type}${signature.list ? "[]" : ""}${signature.optional ? "?" : ""}`;
@@ -1467,6 +1579,27 @@ function breaksContract(attribute) {
1467
1579
  return attribute.kind !== "index";
1468
1580
  }
1469
1581
  __name(breaksContract, "breaksContract");
1582
+ function modelsKeptPastTheTenant(specSchema) {
1583
+ const pointed = new Set(findFkPointers(specSchema).filter((pointer) => pointer.target === "Tenant").map((pointer) => pointer.model));
1584
+ return [
1585
+ ...parseSchema(specSchema).models
1586
+ ].filter(([model, fields]) => fields.has("tenantId") && !pointed.has(model)).map(([model]) => model);
1587
+ }
1588
+ __name(modelsKeptPastTheTenant, "modelsKeptPastTheTenant");
1589
+ function cascadesFromTenant(model, block) {
1590
+ const found = [];
1591
+ for (const line of block.split("\n")) {
1592
+ const compact = structuralOnly(line).replace(/\s+/g, "");
1593
+ if (compact.includes("@relation(") && compact.includes("fields:[tenantId]") && compact.includes("onDelete:Cascade")) {
1594
+ found.push({
1595
+ model,
1596
+ field: line.trim().split(/\s/)[0]
1597
+ });
1598
+ }
1599
+ }
1600
+ return found;
1601
+ }
1602
+ __name(cascadesFromTenant, "cascadesFromTenant");
1470
1603
  function checkSchema(specSchema, appSchema) {
1471
1604
  const spec = parseSchema(specSchema);
1472
1605
  const app = parseSchema(appSchema);
@@ -1487,6 +1620,11 @@ function checkSchema(specSchema, appSchema) {
1487
1620
  compareBlockAttributes(model, specAttrs, appAttrs, missingBlockAttributes);
1488
1621
  }
1489
1622
  }
1623
+ const appBlocks = extractBlocks(appSchema, "model");
1624
+ const tenantCascades = modelsKeptPastTheTenant(specSchema).flatMap((model) => {
1625
+ const block = appBlocks.get(model);
1626
+ return block ? cascadesFromTenant(model, block) : [];
1627
+ });
1490
1628
  const absentEnums = [];
1491
1629
  const missingEnumValues = [];
1492
1630
  for (const [name, specValues] of spec.enums) {
@@ -1511,9 +1649,10 @@ function checkSchema(specSchema, appSchema) {
1511
1649
  missingEnumValues,
1512
1650
  fieldMismatches,
1513
1651
  missingBlockAttributes,
1652
+ tenantCascades,
1514
1653
  checkedModelCount: spec.models.size - absentModels.length,
1515
1654
  checkedEnumCount: spec.enums.size - absentEnums.length,
1516
- ok: missingFields.length === 0 && missingEnumValues.length === 0 && fieldMismatches.length === 0 && !missingBlockAttributes.some(breaksContract)
1655
+ ok: missingFields.length === 0 && missingEnumValues.length === 0 && fieldMismatches.length === 0 && tenantCascades.length === 0 && !missingBlockAttributes.some(breaksContract)
1517
1656
  };
1518
1657
  }
1519
1658
  __name(checkSchema, "checkSchema");
@@ -1594,117 +1733,6 @@ function reportConstraints(outcome, context) {
1594
1733
  }
1595
1734
  __name(reportConstraints, "reportConstraints");
1596
1735
 
1597
- // src/fk-pointers.ts
1598
- var POINTER = /^(\s*)\/\/\s*(\w+)(\s+)(Tenant|User)(\??)(\s+)(@relation\(.*\))\s*$/;
1599
- function findFkPointers(schema2) {
1600
- const found = [];
1601
- let model = "";
1602
- schema2.split("\n").forEach((text, line) => {
1603
- const opening = /^model\s+(\w+)\s*\{/.exec(text);
1604
- if (opening) model = opening[1];
1605
- const match = POINTER.exec(text);
1606
- if (match) found.push({
1607
- line,
1608
- target: match[4],
1609
- model,
1610
- text
1611
- });
1612
- });
1613
- return found;
1614
- }
1615
- __name(findFkPointers, "findFkPointers");
1616
- function relationNameOf(relationAttribute) {
1617
- const match = /@relation\(\s*"([^"]+)"/.exec(relationAttribute);
1618
- return match ? match[1] : null;
1619
- }
1620
- __name(relationNameOf, "relationNameOf");
1621
- function escapeForRegExp(value) {
1622
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1623
- }
1624
- __name(escapeForRegExp, "escapeForRegExp");
1625
- function modelBody(schema2, model) {
1626
- const name = escapeForRegExp(model);
1627
- const block = new RegExp(`(^|\\n)model\\s+${name}\\s*\\{([\\s\\S]*?)\\n\\}`, "m").exec(schema2);
1628
- return block ? block[2] : null;
1629
- }
1630
- __name(modelBody, "modelBody");
1631
- function isOneToOne(schema2, model, foreignKey) {
1632
- const body = modelBody(schema2, model);
1633
- if (!body) return false;
1634
- return new RegExp(`^\\s*${escapeForRegExp(foreignKey)}\\s+\\S+.*@unique`, "m").test(body);
1635
- }
1636
- __name(isOneToOne, "isOneToOne");
1637
- function hasBackRelation(schema2, model, owner, relationName, singular = false) {
1638
- const body = modelBody(schema2, owner);
1639
- if (body === null) return false;
1640
- const name = escapeForRegExp(model);
1641
- const shape = singular ? `${name}\\??` : `${name}\\[\\]`;
1642
- const candidates = [
1643
- ...body.matchAll(new RegExp(`^\\s*\\w+\\s+${shape}(\\s.*)?$`, "gm"))
1644
- ];
1645
- return candidates.some((line) => relationNameOf(line[0]) === relationName);
1646
- }
1647
- __name(hasBackRelation, "hasBackRelation");
1648
- function enableFkPointers(schema2, models) {
1649
- const lines = schema2.split("\n");
1650
- const enabled = [];
1651
- const skipped = [];
1652
- const needsBackRelation = [];
1653
- for (const pointer of findFkPointers(schema2)) {
1654
- const model = pointer.target === "Tenant" ? models.tenant : models.user;
1655
- if (!model) {
1656
- skipped.push({
1657
- line: pointer.line,
1658
- target: pointer.target
1659
- });
1660
- continue;
1661
- }
1662
- const match = POINTER.exec(pointer.text);
1663
- const relationName = relationNameOf(match[7]);
1664
- const foreignKey = foreignKeyOf(match[7]);
1665
- const singular = foreignKey !== null && isOneToOne(schema2, pointer.model, foreignKey);
1666
- if (!hasBackRelation(schema2, pointer.model, model, relationName, singular)) {
1667
- needsBackRelation.push({
1668
- line: pointer.line,
1669
- owner: model,
1670
- suggestion: backRelationSuggestion(pointer.model, relationName, singular)
1671
- });
1672
- continue;
1673
- }
1674
- const [, indent, field, gap1, , optional, gap2, relation] = match;
1675
- lines[pointer.line] = `${indent}${field}${gap1}${model}${optional}${gap2}${relation}`;
1676
- enabled.push({
1677
- line: pointer.line,
1678
- model
1679
- });
1680
- }
1681
- return {
1682
- schema: lines.join("\n"),
1683
- enabled,
1684
- skipped,
1685
- needsBackRelation
1686
- };
1687
- }
1688
- __name(enableFkPointers, "enableFkPointers");
1689
- var lowerFirst = /* @__PURE__ */ __name((value) => value.charAt(0).toLowerCase() + value.slice(1), "lowerFirst");
1690
- function foreignKeyOf(relationAttribute) {
1691
- const match = /fields:\s*\[\s*(\w+)/.exec(relationAttribute);
1692
- return match ? match[1] : null;
1693
- }
1694
- __name(foreignKeyOf, "foreignKeyOf");
1695
- function backRelationSuggestion(model, relationName, singular) {
1696
- const field = singular ? lowerFirst(model) : `${lowerFirst(model)}s`;
1697
- const type = singular ? `${model}?` : `${model}[]`;
1698
- return `${field} ${type}` + (relationName ? ` @relation("${relationName}")` : "");
1699
- }
1700
- __name(backRelationSuggestion, "backRelationSuggestion");
1701
- function assertModelsExist(declaredModels, models) {
1702
- const missing = Object.entries(models).filter(([, name]) => name && !declaredModels.includes(name)).map(([role, name]) => `--${role}-model=${name}`);
1703
- if (missing.length === 0) return;
1704
- throw new Error(`${missing.join(", ")} \u2014 no such model in this schema. It declares: ${declaredModels.slice().sort().join(", ")}.`);
1705
- }
1706
- __name(assertModelsExist, "assertModelsExist");
1707
-
1708
1736
  // src/init/catalog-keys.ts
1709
1737
  var import_spec = require("@saasicat/spec");
1710
1738
  var schema = import_spec.planCatalogSchema;
@@ -3788,6 +3816,7 @@ UserCommands = _ts_decorate14([
3788
3816
  kebabCase,
3789
3817
  migrationCreatedBy,
3790
3818
  minimumQuotasPerPlan,
3819
+ modelsKeptPastTheTenant,
3791
3820
  namedImports,
3792
3821
  parseBlockAttributes,
3793
3822
  parseEnumValues,
package/dist/index.d.cts CHANGED
@@ -467,6 +467,15 @@ interface MissingBlockAttribute {
467
467
  /** For `map`: what the consumer maps to instead. */
468
468
  actual?: string;
469
469
  }
470
+ /**
471
+ * A relation that deletes a record the platform keeps past its tenant, together
472
+ * with the tenant.
473
+ */
474
+ interface TenantCascade {
475
+ model: string;
476
+ /** The relation field that cascades, e.g. `tenant`. */
477
+ field: string;
478
+ }
470
479
  interface SchemaCheckReport {
471
480
  /** Platform models the consumer does not carry — informational. */
472
481
  absentModels: string[];
@@ -480,6 +489,11 @@ interface SchemaCheckReport {
480
489
  * `@@index`, `@@unique`, and a diverging `@@map`.
481
490
  */
482
491
  missingBlockAttributes: MissingBlockAttribute[];
492
+ /**
493
+ * Relations that cascade from the tenant onto a model the platform keeps
494
+ * after the tenant is gone, such as a contract.
495
+ */
496
+ tenantCascades: TenantCascade[];
483
497
  /** Models present in both schemas, i.e. actually compared. */
484
498
  checkedModelCount: number;
485
499
  /** Enums present in both schemas, i.e. actually compared. */
@@ -514,6 +528,15 @@ declare function parseSchema(schema: string): ParsedSchema;
514
528
  * the check.
515
529
  */
516
530
  declare function breaksContract(attribute: MissingBlockAttribute): boolean;
531
+ /**
532
+ * The platform models that outlive their tenant: the ones whose fragment
533
+ * declares `tenantId` and deliberately comments no relation to `Tenant`.
534
+ *
535
+ * Read off the fragments rather than listed, so the next model kept past its
536
+ * tenant is covered by being written that way. Every other model with a
537
+ * `tenantId` ships the pointer a consumer enables, cascade and all.
538
+ */
539
+ declare function modelsKeptPastTheTenant(specSchema: string): string[];
517
540
  /**
518
541
  * Compares a consumer schema against the canonical fragments. `specSchema` is
519
542
  * the concatenation of the fragments the check should cover.
@@ -1379,4 +1402,4 @@ declare class UserCommands extends CommandRunner {
1379
1402
  parsePassword(val: string): string;
1380
1403
  }
1381
1404
 
1382
- export { AUDIT_QUERY_PORT_TOKEN, AdminCommands, AdminManifestDoctorCheck, AdminMfaSetupCommand, AdminWhoamiCommand, type ApplyResult, AuditCommands, AuditTailCommand, AuditTailFlow, type AuditTailOptions, type BlockAttributeKind, type BlockAttributes, CLI_CONTEXT_CONFIG_TOKEN, CONSTRAINTS_MARKER, type CheckSeverity, type CliContextConfig, CliContextModule, type CliContextModuleOptions, CliContextService, CliError, type CliIdentity, type ConstraintsOutcome, type ConstraintsReport, DEFAULT_MANIFEST_CHECKS, DOCTOR_CHECKS_TOKEN, type DbCatalogOccurrence, type DbCatalogResult, type DbCatalogShape, DiscoveryCommands, DiscoveryScanCommand, DiscoverySnapshotDoctorCheck, type DoctorCheck, type DoctorCheckResult, DoctorCommands, DoctorFlow, type DoctorReport, type EnableFkResult, type FieldMismatch, type FieldMismatchReason, type FieldSignature, type FkModelNames, type FkPointer, type FragmentBlocks, type InitOptions, type InitPlan, LIMIT_FILTER_IMPORTS, LIMIT_FILTER_PROVIDER, MANIFEST_ACCESS_PORT_TOKEN, MANIFEST_CHECKS_TOKEN, type ManifestCheck, ManifestCheckCommand, type ManifestCheckReport, type ManifestCheckResult, ManifestCliFlow, ManifestCommands, type ManifestDiff, ManifestDumpCommand, ManifestHashCommand, type ManifestRewriteOptions, ManifestValidateCommand, MfaSetupFlow, type MfaSetupOptions, type MfaSetupResult, type MissingBlockAttribute, type MissingEnumValue, type MissingField, type ModuleResolutionVerdict, type MoveTable, type MovedSetting, type MovedSettingOccurrence, type MovedSettingsResult, PLATFORM_DOCTOR_CHECK_PROVIDERS, type ParsedSchema, type PatchAppModuleOptions, type PatchOptionsFromPlan, type PatchResult, PlanCatalogDoctorCheck, type PlannedFile, type ProjectKeyResult, type QuotaProviderFile, type QuotaSpec, type RenameResult, type RenameTable, type RewriteResult, SCANNED_FOR_DB_CATALOG, SCANNED_FOR_MOVED_SETTINGS, SETTINGS_THAT_MOVED, type SchemaCheckReport, UI_VUE_SPECIFIER, USER_MANAGEMENT_PORT_TOKEN, USER_PORT_TOKEN, UserCommands, UserPortDoctorCheck, WHERE_DB_CATALOG_GOES, WHERE_IT_GOES, WhoAmIFlow, type WhoAmIResult, type WrittenSetting, appKeyPattern, appendConstraints, applyFragmentBlocks, applyTokens, assertModelsExist, assertValidAppKey, assertValidQuotaKey, blankStringLiterals, blockBodyLines, breaksContract, buildImportMap, checkSchema, constraintsFor, describeDbCatalogOccurrence, enableFkPointers, extractBlockNames, extractBlocks, extractEnumBlocks, extractEnumNames, extractFragmentBlocks, extractModelBlocks, extractModelNames, findDbCatalogBlocks, findFkPointers, findMovedSettings, foreignKeyOf, hasBackRelation, hasConstraints, isNoLongerPublic, isOneToOne, judgeModuleResolution, kebabCase, migrationCreatedBy, minimumQuotasPerPlan, namedImports, parseBlockAttributes, parseEnumValues, parseFields, parseQuota, parseSchema, pascalCase, patchAppModule, patchOptionsFor, planInit, quotaKeyPattern, readEffectiveModuleResolution, relationNameOf, removeProjectKey, reportConstraints, rewriteImports, rewriteManifest, rewriteNames, rewriteSubpath, settingsWrittenTo, stripLineComment, structuralOnly, tablesAddressedBy };
1405
+ export { AUDIT_QUERY_PORT_TOKEN, AdminCommands, AdminManifestDoctorCheck, AdminMfaSetupCommand, AdminWhoamiCommand, type ApplyResult, AuditCommands, AuditTailCommand, AuditTailFlow, type AuditTailOptions, type BlockAttributeKind, type BlockAttributes, CLI_CONTEXT_CONFIG_TOKEN, CONSTRAINTS_MARKER, type CheckSeverity, type CliContextConfig, CliContextModule, type CliContextModuleOptions, CliContextService, CliError, type CliIdentity, type ConstraintsOutcome, type ConstraintsReport, DEFAULT_MANIFEST_CHECKS, DOCTOR_CHECKS_TOKEN, type DbCatalogOccurrence, type DbCatalogResult, type DbCatalogShape, DiscoveryCommands, DiscoveryScanCommand, DiscoverySnapshotDoctorCheck, type DoctorCheck, type DoctorCheckResult, DoctorCommands, DoctorFlow, type DoctorReport, type EnableFkResult, type FieldMismatch, type FieldMismatchReason, type FieldSignature, type FkModelNames, type FkPointer, type FragmentBlocks, type InitOptions, type InitPlan, LIMIT_FILTER_IMPORTS, LIMIT_FILTER_PROVIDER, MANIFEST_ACCESS_PORT_TOKEN, MANIFEST_CHECKS_TOKEN, type ManifestCheck, ManifestCheckCommand, type ManifestCheckReport, type ManifestCheckResult, ManifestCliFlow, ManifestCommands, type ManifestDiff, ManifestDumpCommand, ManifestHashCommand, type ManifestRewriteOptions, ManifestValidateCommand, MfaSetupFlow, type MfaSetupOptions, type MfaSetupResult, type MissingBlockAttribute, type MissingEnumValue, type MissingField, type ModuleResolutionVerdict, type MoveTable, type MovedSetting, type MovedSettingOccurrence, type MovedSettingsResult, PLATFORM_DOCTOR_CHECK_PROVIDERS, type ParsedSchema, type PatchAppModuleOptions, type PatchOptionsFromPlan, type PatchResult, PlanCatalogDoctorCheck, type PlannedFile, type ProjectKeyResult, type QuotaProviderFile, type QuotaSpec, type RenameResult, type RenameTable, type RewriteResult, SCANNED_FOR_DB_CATALOG, SCANNED_FOR_MOVED_SETTINGS, SETTINGS_THAT_MOVED, type SchemaCheckReport, type TenantCascade, UI_VUE_SPECIFIER, USER_MANAGEMENT_PORT_TOKEN, USER_PORT_TOKEN, UserCommands, UserPortDoctorCheck, WHERE_DB_CATALOG_GOES, WHERE_IT_GOES, WhoAmIFlow, type WhoAmIResult, type WrittenSetting, appKeyPattern, appendConstraints, applyFragmentBlocks, applyTokens, assertModelsExist, assertValidAppKey, assertValidQuotaKey, blankStringLiterals, blockBodyLines, breaksContract, buildImportMap, checkSchema, constraintsFor, describeDbCatalogOccurrence, enableFkPointers, extractBlockNames, extractBlocks, extractEnumBlocks, extractEnumNames, extractFragmentBlocks, extractModelBlocks, extractModelNames, findDbCatalogBlocks, findFkPointers, findMovedSettings, foreignKeyOf, hasBackRelation, hasConstraints, isNoLongerPublic, isOneToOne, judgeModuleResolution, kebabCase, migrationCreatedBy, minimumQuotasPerPlan, modelsKeptPastTheTenant, namedImports, parseBlockAttributes, parseEnumValues, parseFields, parseQuota, parseSchema, pascalCase, patchAppModule, patchOptionsFor, planInit, quotaKeyPattern, readEffectiveModuleResolution, relationNameOf, removeProjectKey, reportConstraints, rewriteImports, rewriteManifest, rewriteNames, rewriteSubpath, settingsWrittenTo, stripLineComment, structuralOnly, tablesAddressedBy };
package/dist/index.d.ts CHANGED
@@ -467,6 +467,15 @@ interface MissingBlockAttribute {
467
467
  /** For `map`: what the consumer maps to instead. */
468
468
  actual?: string;
469
469
  }
470
+ /**
471
+ * A relation that deletes a record the platform keeps past its tenant, together
472
+ * with the tenant.
473
+ */
474
+ interface TenantCascade {
475
+ model: string;
476
+ /** The relation field that cascades, e.g. `tenant`. */
477
+ field: string;
478
+ }
470
479
  interface SchemaCheckReport {
471
480
  /** Platform models the consumer does not carry — informational. */
472
481
  absentModels: string[];
@@ -480,6 +489,11 @@ interface SchemaCheckReport {
480
489
  * `@@index`, `@@unique`, and a diverging `@@map`.
481
490
  */
482
491
  missingBlockAttributes: MissingBlockAttribute[];
492
+ /**
493
+ * Relations that cascade from the tenant onto a model the platform keeps
494
+ * after the tenant is gone, such as a contract.
495
+ */
496
+ tenantCascades: TenantCascade[];
483
497
  /** Models present in both schemas, i.e. actually compared. */
484
498
  checkedModelCount: number;
485
499
  /** Enums present in both schemas, i.e. actually compared. */
@@ -514,6 +528,15 @@ declare function parseSchema(schema: string): ParsedSchema;
514
528
  * the check.
515
529
  */
516
530
  declare function breaksContract(attribute: MissingBlockAttribute): boolean;
531
+ /**
532
+ * The platform models that outlive their tenant: the ones whose fragment
533
+ * declares `tenantId` and deliberately comments no relation to `Tenant`.
534
+ *
535
+ * Read off the fragments rather than listed, so the next model kept past its
536
+ * tenant is covered by being written that way. Every other model with a
537
+ * `tenantId` ships the pointer a consumer enables, cascade and all.
538
+ */
539
+ declare function modelsKeptPastTheTenant(specSchema: string): string[];
517
540
  /**
518
541
  * Compares a consumer schema against the canonical fragments. `specSchema` is
519
542
  * the concatenation of the fragments the check should cover.
@@ -1379,4 +1402,4 @@ declare class UserCommands extends CommandRunner {
1379
1402
  parsePassword(val: string): string;
1380
1403
  }
1381
1404
 
1382
- export { AUDIT_QUERY_PORT_TOKEN, AdminCommands, AdminManifestDoctorCheck, AdminMfaSetupCommand, AdminWhoamiCommand, type ApplyResult, AuditCommands, AuditTailCommand, AuditTailFlow, type AuditTailOptions, type BlockAttributeKind, type BlockAttributes, CLI_CONTEXT_CONFIG_TOKEN, CONSTRAINTS_MARKER, type CheckSeverity, type CliContextConfig, CliContextModule, type CliContextModuleOptions, CliContextService, CliError, type CliIdentity, type ConstraintsOutcome, type ConstraintsReport, DEFAULT_MANIFEST_CHECKS, DOCTOR_CHECKS_TOKEN, type DbCatalogOccurrence, type DbCatalogResult, type DbCatalogShape, DiscoveryCommands, DiscoveryScanCommand, DiscoverySnapshotDoctorCheck, type DoctorCheck, type DoctorCheckResult, DoctorCommands, DoctorFlow, type DoctorReport, type EnableFkResult, type FieldMismatch, type FieldMismatchReason, type FieldSignature, type FkModelNames, type FkPointer, type FragmentBlocks, type InitOptions, type InitPlan, LIMIT_FILTER_IMPORTS, LIMIT_FILTER_PROVIDER, MANIFEST_ACCESS_PORT_TOKEN, MANIFEST_CHECKS_TOKEN, type ManifestCheck, ManifestCheckCommand, type ManifestCheckReport, type ManifestCheckResult, ManifestCliFlow, ManifestCommands, type ManifestDiff, ManifestDumpCommand, ManifestHashCommand, type ManifestRewriteOptions, ManifestValidateCommand, MfaSetupFlow, type MfaSetupOptions, type MfaSetupResult, type MissingBlockAttribute, type MissingEnumValue, type MissingField, type ModuleResolutionVerdict, type MoveTable, type MovedSetting, type MovedSettingOccurrence, type MovedSettingsResult, PLATFORM_DOCTOR_CHECK_PROVIDERS, type ParsedSchema, type PatchAppModuleOptions, type PatchOptionsFromPlan, type PatchResult, PlanCatalogDoctorCheck, type PlannedFile, type ProjectKeyResult, type QuotaProviderFile, type QuotaSpec, type RenameResult, type RenameTable, type RewriteResult, SCANNED_FOR_DB_CATALOG, SCANNED_FOR_MOVED_SETTINGS, SETTINGS_THAT_MOVED, type SchemaCheckReport, UI_VUE_SPECIFIER, USER_MANAGEMENT_PORT_TOKEN, USER_PORT_TOKEN, UserCommands, UserPortDoctorCheck, WHERE_DB_CATALOG_GOES, WHERE_IT_GOES, WhoAmIFlow, type WhoAmIResult, type WrittenSetting, appKeyPattern, appendConstraints, applyFragmentBlocks, applyTokens, assertModelsExist, assertValidAppKey, assertValidQuotaKey, blankStringLiterals, blockBodyLines, breaksContract, buildImportMap, checkSchema, constraintsFor, describeDbCatalogOccurrence, enableFkPointers, extractBlockNames, extractBlocks, extractEnumBlocks, extractEnumNames, extractFragmentBlocks, extractModelBlocks, extractModelNames, findDbCatalogBlocks, findFkPointers, findMovedSettings, foreignKeyOf, hasBackRelation, hasConstraints, isNoLongerPublic, isOneToOne, judgeModuleResolution, kebabCase, migrationCreatedBy, minimumQuotasPerPlan, namedImports, parseBlockAttributes, parseEnumValues, parseFields, parseQuota, parseSchema, pascalCase, patchAppModule, patchOptionsFor, planInit, quotaKeyPattern, readEffectiveModuleResolution, relationNameOf, removeProjectKey, reportConstraints, rewriteImports, rewriteManifest, rewriteNames, rewriteSubpath, settingsWrittenTo, stripLineComment, structuralOnly, tablesAddressedBy };
1405
+ export { AUDIT_QUERY_PORT_TOKEN, AdminCommands, AdminManifestDoctorCheck, AdminMfaSetupCommand, AdminWhoamiCommand, type ApplyResult, AuditCommands, AuditTailCommand, AuditTailFlow, type AuditTailOptions, type BlockAttributeKind, type BlockAttributes, CLI_CONTEXT_CONFIG_TOKEN, CONSTRAINTS_MARKER, type CheckSeverity, type CliContextConfig, CliContextModule, type CliContextModuleOptions, CliContextService, CliError, type CliIdentity, type ConstraintsOutcome, type ConstraintsReport, DEFAULT_MANIFEST_CHECKS, DOCTOR_CHECKS_TOKEN, type DbCatalogOccurrence, type DbCatalogResult, type DbCatalogShape, DiscoveryCommands, DiscoveryScanCommand, DiscoverySnapshotDoctorCheck, type DoctorCheck, type DoctorCheckResult, DoctorCommands, DoctorFlow, type DoctorReport, type EnableFkResult, type FieldMismatch, type FieldMismatchReason, type FieldSignature, type FkModelNames, type FkPointer, type FragmentBlocks, type InitOptions, type InitPlan, LIMIT_FILTER_IMPORTS, LIMIT_FILTER_PROVIDER, MANIFEST_ACCESS_PORT_TOKEN, MANIFEST_CHECKS_TOKEN, type ManifestCheck, ManifestCheckCommand, type ManifestCheckReport, type ManifestCheckResult, ManifestCliFlow, ManifestCommands, type ManifestDiff, ManifestDumpCommand, ManifestHashCommand, type ManifestRewriteOptions, ManifestValidateCommand, MfaSetupFlow, type MfaSetupOptions, type MfaSetupResult, type MissingBlockAttribute, type MissingEnumValue, type MissingField, type ModuleResolutionVerdict, type MoveTable, type MovedSetting, type MovedSettingOccurrence, type MovedSettingsResult, PLATFORM_DOCTOR_CHECK_PROVIDERS, type ParsedSchema, type PatchAppModuleOptions, type PatchOptionsFromPlan, type PatchResult, PlanCatalogDoctorCheck, type PlannedFile, type ProjectKeyResult, type QuotaProviderFile, type QuotaSpec, type RenameResult, type RenameTable, type RewriteResult, SCANNED_FOR_DB_CATALOG, SCANNED_FOR_MOVED_SETTINGS, SETTINGS_THAT_MOVED, type SchemaCheckReport, type TenantCascade, UI_VUE_SPECIFIER, USER_MANAGEMENT_PORT_TOKEN, USER_PORT_TOKEN, UserCommands, UserPortDoctorCheck, WHERE_DB_CATALOG_GOES, WHERE_IT_GOES, WhoAmIFlow, type WhoAmIResult, type WrittenSetting, appKeyPattern, appendConstraints, applyFragmentBlocks, applyTokens, assertModelsExist, assertValidAppKey, assertValidQuotaKey, blankStringLiterals, blockBodyLines, breaksContract, buildImportMap, checkSchema, constraintsFor, describeDbCatalogOccurrence, enableFkPointers, extractBlockNames, extractBlocks, extractEnumBlocks, extractEnumNames, extractFragmentBlocks, extractModelBlocks, extractModelNames, findDbCatalogBlocks, findFkPointers, findMovedSettings, foreignKeyOf, hasBackRelation, hasConstraints, isNoLongerPublic, isOneToOne, judgeModuleResolution, kebabCase, migrationCreatedBy, minimumQuotasPerPlan, modelsKeptPastTheTenant, namedImports, parseBlockAttributes, parseEnumValues, parseFields, parseQuota, parseSchema, pascalCase, patchAppModule, patchOptionsFor, planInit, quotaKeyPattern, readEffectiveModuleResolution, relationNameOf, removeProjectKey, reportConstraints, rewriteImports, rewriteManifest, rewriteNames, rewriteSubpath, settingsWrittenTo, stripLineComment, structuralOnly, tablesAddressedBy };
package/dist/index.js CHANGED
@@ -1201,6 +1201,117 @@ function applyFragmentBlocks(schema2, fragmentBlocks, options = {}) {
1201
1201
  }
1202
1202
  __name(applyFragmentBlocks, "applyFragmentBlocks");
1203
1203
 
1204
+ // src/fk-pointers.ts
1205
+ var POINTER = /^(\s*)\/\/\s*(\w+)(\s+)(Tenant|User)(\??)(\s+)(@relation\(.*\))\s*$/;
1206
+ function findFkPointers(schema2) {
1207
+ const found = [];
1208
+ let model = "";
1209
+ schema2.split("\n").forEach((text, line) => {
1210
+ const opening = /^model\s+(\w+)\s*\{/.exec(text);
1211
+ if (opening) model = opening[1];
1212
+ const match = POINTER.exec(text);
1213
+ if (match) found.push({
1214
+ line,
1215
+ target: match[4],
1216
+ model,
1217
+ text
1218
+ });
1219
+ });
1220
+ return found;
1221
+ }
1222
+ __name(findFkPointers, "findFkPointers");
1223
+ function relationNameOf(relationAttribute) {
1224
+ const match = /@relation\(\s*"([^"]+)"/.exec(relationAttribute);
1225
+ return match ? match[1] : null;
1226
+ }
1227
+ __name(relationNameOf, "relationNameOf");
1228
+ function escapeForRegExp(value) {
1229
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1230
+ }
1231
+ __name(escapeForRegExp, "escapeForRegExp");
1232
+ function modelBody(schema2, model) {
1233
+ const name = escapeForRegExp(model);
1234
+ const block = new RegExp(`(^|\\n)model\\s+${name}\\s*\\{([\\s\\S]*?)\\n\\}`, "m").exec(schema2);
1235
+ return block ? block[2] : null;
1236
+ }
1237
+ __name(modelBody, "modelBody");
1238
+ function isOneToOne(schema2, model, foreignKey) {
1239
+ const body = modelBody(schema2, model);
1240
+ if (!body) return false;
1241
+ return new RegExp(`^\\s*${escapeForRegExp(foreignKey)}\\s+\\S+.*@unique`, "m").test(body);
1242
+ }
1243
+ __name(isOneToOne, "isOneToOne");
1244
+ function hasBackRelation(schema2, model, owner, relationName, singular = false) {
1245
+ const body = modelBody(schema2, owner);
1246
+ if (body === null) return false;
1247
+ const name = escapeForRegExp(model);
1248
+ const shape = singular ? `${name}\\??` : `${name}\\[\\]`;
1249
+ const candidates = [
1250
+ ...body.matchAll(new RegExp(`^\\s*\\w+\\s+${shape}(\\s.*)?$`, "gm"))
1251
+ ];
1252
+ return candidates.some((line) => relationNameOf(line[0]) === relationName);
1253
+ }
1254
+ __name(hasBackRelation, "hasBackRelation");
1255
+ function enableFkPointers(schema2, models) {
1256
+ const lines = schema2.split("\n");
1257
+ const enabled = [];
1258
+ const skipped = [];
1259
+ const needsBackRelation = [];
1260
+ for (const pointer of findFkPointers(schema2)) {
1261
+ const model = pointer.target === "Tenant" ? models.tenant : models.user;
1262
+ if (!model) {
1263
+ skipped.push({
1264
+ line: pointer.line,
1265
+ target: pointer.target
1266
+ });
1267
+ continue;
1268
+ }
1269
+ const match = POINTER.exec(pointer.text);
1270
+ const relationName = relationNameOf(match[7]);
1271
+ const foreignKey = foreignKeyOf(match[7]);
1272
+ const singular = foreignKey !== null && isOneToOne(schema2, pointer.model, foreignKey);
1273
+ if (!hasBackRelation(schema2, pointer.model, model, relationName, singular)) {
1274
+ needsBackRelation.push({
1275
+ line: pointer.line,
1276
+ owner: model,
1277
+ suggestion: backRelationSuggestion(pointer.model, relationName, singular)
1278
+ });
1279
+ continue;
1280
+ }
1281
+ const [, indent, field, gap1, , optional, gap2, relation] = match;
1282
+ lines[pointer.line] = `${indent}${field}${gap1}${model}${optional}${gap2}${relation}`;
1283
+ enabled.push({
1284
+ line: pointer.line,
1285
+ model
1286
+ });
1287
+ }
1288
+ return {
1289
+ schema: lines.join("\n"),
1290
+ enabled,
1291
+ skipped,
1292
+ needsBackRelation
1293
+ };
1294
+ }
1295
+ __name(enableFkPointers, "enableFkPointers");
1296
+ var lowerFirst = /* @__PURE__ */ __name((value) => value.charAt(0).toLowerCase() + value.slice(1), "lowerFirst");
1297
+ function foreignKeyOf(relationAttribute) {
1298
+ const match = /fields:\s*\[\s*(\w+)/.exec(relationAttribute);
1299
+ return match ? match[1] : null;
1300
+ }
1301
+ __name(foreignKeyOf, "foreignKeyOf");
1302
+ function backRelationSuggestion(model, relationName, singular) {
1303
+ const field = singular ? lowerFirst(model) : `${lowerFirst(model)}s`;
1304
+ const type = singular ? `${model}?` : `${model}[]`;
1305
+ return `${field} ${type}` + (relationName ? ` @relation("${relationName}")` : "");
1306
+ }
1307
+ __name(backRelationSuggestion, "backRelationSuggestion");
1308
+ function assertModelsExist(declaredModels, models) {
1309
+ const missing = Object.entries(models).filter(([, name]) => name && !declaredModels.includes(name)).map(([role, name]) => `--${role}-model=${name}`);
1310
+ if (missing.length === 0) return;
1311
+ throw new Error(`${missing.join(", ")} \u2014 no such model in this schema. It declares: ${declaredModels.slice().sort().join(", ")}.`);
1312
+ }
1313
+ __name(assertModelsExist, "assertModelsExist");
1314
+
1204
1315
  // src/schema-check.ts
1205
1316
  function renderType(signature) {
1206
1317
  return `${signature.type}${signature.list ? "[]" : ""}${signature.optional ? "?" : ""}`;
@@ -1333,6 +1444,27 @@ function breaksContract(attribute) {
1333
1444
  return attribute.kind !== "index";
1334
1445
  }
1335
1446
  __name(breaksContract, "breaksContract");
1447
+ function modelsKeptPastTheTenant(specSchema) {
1448
+ const pointed = new Set(findFkPointers(specSchema).filter((pointer) => pointer.target === "Tenant").map((pointer) => pointer.model));
1449
+ return [
1450
+ ...parseSchema(specSchema).models
1451
+ ].filter(([model, fields]) => fields.has("tenantId") && !pointed.has(model)).map(([model]) => model);
1452
+ }
1453
+ __name(modelsKeptPastTheTenant, "modelsKeptPastTheTenant");
1454
+ function cascadesFromTenant(model, block) {
1455
+ const found = [];
1456
+ for (const line of block.split("\n")) {
1457
+ const compact = structuralOnly(line).replace(/\s+/g, "");
1458
+ if (compact.includes("@relation(") && compact.includes("fields:[tenantId]") && compact.includes("onDelete:Cascade")) {
1459
+ found.push({
1460
+ model,
1461
+ field: line.trim().split(/\s/)[0]
1462
+ });
1463
+ }
1464
+ }
1465
+ return found;
1466
+ }
1467
+ __name(cascadesFromTenant, "cascadesFromTenant");
1336
1468
  function checkSchema(specSchema, appSchema) {
1337
1469
  const spec = parseSchema(specSchema);
1338
1470
  const app = parseSchema(appSchema);
@@ -1353,6 +1485,11 @@ function checkSchema(specSchema, appSchema) {
1353
1485
  compareBlockAttributes(model, specAttrs, appAttrs, missingBlockAttributes);
1354
1486
  }
1355
1487
  }
1488
+ const appBlocks = extractBlocks(appSchema, "model");
1489
+ const tenantCascades = modelsKeptPastTheTenant(specSchema).flatMap((model) => {
1490
+ const block = appBlocks.get(model);
1491
+ return block ? cascadesFromTenant(model, block) : [];
1492
+ });
1356
1493
  const absentEnums = [];
1357
1494
  const missingEnumValues = [];
1358
1495
  for (const [name, specValues] of spec.enums) {
@@ -1377,9 +1514,10 @@ function checkSchema(specSchema, appSchema) {
1377
1514
  missingEnumValues,
1378
1515
  fieldMismatches,
1379
1516
  missingBlockAttributes,
1517
+ tenantCascades,
1380
1518
  checkedModelCount: spec.models.size - absentModels.length,
1381
1519
  checkedEnumCount: spec.enums.size - absentEnums.length,
1382
- ok: missingFields.length === 0 && missingEnumValues.length === 0 && fieldMismatches.length === 0 && !missingBlockAttributes.some(breaksContract)
1520
+ ok: missingFields.length === 0 && missingEnumValues.length === 0 && fieldMismatches.length === 0 && tenantCascades.length === 0 && !missingBlockAttributes.some(breaksContract)
1383
1521
  };
1384
1522
  }
1385
1523
  __name(checkSchema, "checkSchema");
@@ -1460,117 +1598,6 @@ function reportConstraints(outcome, context) {
1460
1598
  }
1461
1599
  __name(reportConstraints, "reportConstraints");
1462
1600
 
1463
- // src/fk-pointers.ts
1464
- var POINTER = /^(\s*)\/\/\s*(\w+)(\s+)(Tenant|User)(\??)(\s+)(@relation\(.*\))\s*$/;
1465
- function findFkPointers(schema2) {
1466
- const found = [];
1467
- let model = "";
1468
- schema2.split("\n").forEach((text, line) => {
1469
- const opening = /^model\s+(\w+)\s*\{/.exec(text);
1470
- if (opening) model = opening[1];
1471
- const match = POINTER.exec(text);
1472
- if (match) found.push({
1473
- line,
1474
- target: match[4],
1475
- model,
1476
- text
1477
- });
1478
- });
1479
- return found;
1480
- }
1481
- __name(findFkPointers, "findFkPointers");
1482
- function relationNameOf(relationAttribute) {
1483
- const match = /@relation\(\s*"([^"]+)"/.exec(relationAttribute);
1484
- return match ? match[1] : null;
1485
- }
1486
- __name(relationNameOf, "relationNameOf");
1487
- function escapeForRegExp(value) {
1488
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1489
- }
1490
- __name(escapeForRegExp, "escapeForRegExp");
1491
- function modelBody(schema2, model) {
1492
- const name = escapeForRegExp(model);
1493
- const block = new RegExp(`(^|\\n)model\\s+${name}\\s*\\{([\\s\\S]*?)\\n\\}`, "m").exec(schema2);
1494
- return block ? block[2] : null;
1495
- }
1496
- __name(modelBody, "modelBody");
1497
- function isOneToOne(schema2, model, foreignKey) {
1498
- const body = modelBody(schema2, model);
1499
- if (!body) return false;
1500
- return new RegExp(`^\\s*${escapeForRegExp(foreignKey)}\\s+\\S+.*@unique`, "m").test(body);
1501
- }
1502
- __name(isOneToOne, "isOneToOne");
1503
- function hasBackRelation(schema2, model, owner, relationName, singular = false) {
1504
- const body = modelBody(schema2, owner);
1505
- if (body === null) return false;
1506
- const name = escapeForRegExp(model);
1507
- const shape = singular ? `${name}\\??` : `${name}\\[\\]`;
1508
- const candidates = [
1509
- ...body.matchAll(new RegExp(`^\\s*\\w+\\s+${shape}(\\s.*)?$`, "gm"))
1510
- ];
1511
- return candidates.some((line) => relationNameOf(line[0]) === relationName);
1512
- }
1513
- __name(hasBackRelation, "hasBackRelation");
1514
- function enableFkPointers(schema2, models) {
1515
- const lines = schema2.split("\n");
1516
- const enabled = [];
1517
- const skipped = [];
1518
- const needsBackRelation = [];
1519
- for (const pointer of findFkPointers(schema2)) {
1520
- const model = pointer.target === "Tenant" ? models.tenant : models.user;
1521
- if (!model) {
1522
- skipped.push({
1523
- line: pointer.line,
1524
- target: pointer.target
1525
- });
1526
- continue;
1527
- }
1528
- const match = POINTER.exec(pointer.text);
1529
- const relationName = relationNameOf(match[7]);
1530
- const foreignKey = foreignKeyOf(match[7]);
1531
- const singular = foreignKey !== null && isOneToOne(schema2, pointer.model, foreignKey);
1532
- if (!hasBackRelation(schema2, pointer.model, model, relationName, singular)) {
1533
- needsBackRelation.push({
1534
- line: pointer.line,
1535
- owner: model,
1536
- suggestion: backRelationSuggestion(pointer.model, relationName, singular)
1537
- });
1538
- continue;
1539
- }
1540
- const [, indent, field, gap1, , optional, gap2, relation] = match;
1541
- lines[pointer.line] = `${indent}${field}${gap1}${model}${optional}${gap2}${relation}`;
1542
- enabled.push({
1543
- line: pointer.line,
1544
- model
1545
- });
1546
- }
1547
- return {
1548
- schema: lines.join("\n"),
1549
- enabled,
1550
- skipped,
1551
- needsBackRelation
1552
- };
1553
- }
1554
- __name(enableFkPointers, "enableFkPointers");
1555
- var lowerFirst = /* @__PURE__ */ __name((value) => value.charAt(0).toLowerCase() + value.slice(1), "lowerFirst");
1556
- function foreignKeyOf(relationAttribute) {
1557
- const match = /fields:\s*\[\s*(\w+)/.exec(relationAttribute);
1558
- return match ? match[1] : null;
1559
- }
1560
- __name(foreignKeyOf, "foreignKeyOf");
1561
- function backRelationSuggestion(model, relationName, singular) {
1562
- const field = singular ? lowerFirst(model) : `${lowerFirst(model)}s`;
1563
- const type = singular ? `${model}?` : `${model}[]`;
1564
- return `${field} ${type}` + (relationName ? ` @relation("${relationName}")` : "");
1565
- }
1566
- __name(backRelationSuggestion, "backRelationSuggestion");
1567
- function assertModelsExist(declaredModels, models) {
1568
- const missing = Object.entries(models).filter(([, name]) => name && !declaredModels.includes(name)).map(([role, name]) => `--${role}-model=${name}`);
1569
- if (missing.length === 0) return;
1570
- throw new Error(`${missing.join(", ")} \u2014 no such model in this schema. It declares: ${declaredModels.slice().sort().join(", ")}.`);
1571
- }
1572
- __name(assertModelsExist, "assertModelsExist");
1573
-
1574
1601
  // src/init/catalog-keys.ts
1575
1602
  import { planCatalogSchema } from "@saasicat/spec";
1576
1603
  var schema = planCatalogSchema;
@@ -3653,6 +3680,7 @@ export {
3653
3680
  kebabCase,
3654
3681
  migrationCreatedBy,
3655
3682
  minimumQuotasPerPlan,
3683
+ modelsKeptPastTheTenant,
3656
3684
  namedImports,
3657
3685
  parseBlockAttributes,
3658
3686
  parseEnumValues,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saasicat/cli",
3
- "version": "1.0.0-rc.14",
3
+ "version": "1.0.0-rc.16",
4
4
  "description": "CLI helpers for SaaS platform consumers. Provides CliContextService (identity, MFA, production confirm, audit tag).",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -30,9 +30,9 @@
30
30
  },
31
31
  "dependencies": {
32
32
  "qrcode-terminal": "^0.12.0",
33
- "@saasicat/nest": "^1.0.0-rc.14",
34
- "@saasicat/spec": "^1.0.0-rc.14",
35
- "@saasicat/core": "^1.0.0-rc.14"
33
+ "@saasicat/nest": "^1.0.0-rc.16",
34
+ "@saasicat/spec": "^1.0.0-rc.16",
35
+ "@saasicat/core": "^1.0.0-rc.16"
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@nestjs/common": "^11.0.0",