@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/LICENSE +90 -201
- package/README.md +4 -2
- package/bin/saasicat.js +562 -17
- package/codemods/v1-imports.map.json +135 -0
- package/codemods/v1-rename.map.json +45 -0
- package/dist/.build-stamp +1 -0
- package/dist/index.cjs +765 -24
- package/dist/index.d.cts +461 -1
- package/dist/index.d.ts +461 -1
- package/dist/index.js +729 -23
- package/package.json +10 -8
- package/templates/init/config/saas.yaml.tpl +30 -0
- package/templates/init/src/auth/password.hasher.ts.tpl +33 -0
- package/templates/init/src/saas/admin-manifest.contribution.ts.tpl +33 -0
- package/templates/init/src/saas/admin.module.ts.tpl +24 -0
- package/templates/init/src/saas/feature-ui-registry.ts.tpl +16 -0
- package/templates/init/src/saas/persistence-without-hasher.ts.tpl +24 -0
- package/templates/init/src/saas/persistence.ts.tpl +22 -0
- package/templates/init/src/saas/quota.provider.ts.tpl +31 -0
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("
|
|
89
|
-
var USER_PORT_TOKEN = /* @__PURE__ */ Symbol.for("
|
|
90
|
-
var USER_MANAGEMENT_PORT_TOKEN = /* @__PURE__ */ Symbol.for("
|
|
91
|
-
var AUDIT_QUERY_PORT_TOKEN = /* @__PURE__ */ Symbol.for("
|
|
92
|
-
var DOCTOR_CHECKS_TOKEN = /* @__PURE__ */ Symbol.for("
|
|
93
|
-
var MANIFEST_ACCESS_PORT_TOKEN = /* @__PURE__ */ Symbol.for("
|
|
94
|
-
var MANIFEST_CHECKS_TOKEN = /* @__PURE__ */ Symbol.for("
|
|
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(
|
|
1193
|
+
function extractBlockNames(schema2, keyword) {
|
|
1159
1194
|
const pattern = declarationPattern(keyword);
|
|
1160
1195
|
const names = [];
|
|
1161
|
-
for (const line of
|
|
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(
|
|
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
|
|
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(
|
|
1208
|
-
return extractBlockNames(
|
|
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(
|
|
1216
|
-
const existing = new Set(extractModelNames(
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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/
|
|
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
|
});
|