@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.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
2
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
3
|
|
|
4
|
-
// src/tokens.ts
|
|
5
|
-
var CLI_CONTEXT_CONFIG_TOKEN = /* @__PURE__ */ Symbol.for("
|
|
6
|
-
var USER_PORT_TOKEN = /* @__PURE__ */ Symbol.for("
|
|
7
|
-
var USER_MANAGEMENT_PORT_TOKEN = /* @__PURE__ */ Symbol.for("
|
|
8
|
-
var AUDIT_QUERY_PORT_TOKEN = /* @__PURE__ */ Symbol.for("
|
|
9
|
-
var DOCTOR_CHECKS_TOKEN = /* @__PURE__ */ Symbol.for("
|
|
10
|
-
var MANIFEST_ACCESS_PORT_TOKEN = /* @__PURE__ */ Symbol.for("
|
|
11
|
-
var MANIFEST_CHECKS_TOKEN = /* @__PURE__ */ Symbol.for("
|
|
4
|
+
// src/cli.tokens.ts
|
|
5
|
+
var CLI_CONTEXT_CONFIG_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/cli/Config");
|
|
6
|
+
var USER_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/cli/UserPort");
|
|
7
|
+
var USER_MANAGEMENT_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/cli/UserManagementPort");
|
|
8
|
+
var AUDIT_QUERY_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/cli/AuditQueryPort");
|
|
9
|
+
var DOCTOR_CHECKS_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/cli/DoctorChecks");
|
|
10
|
+
var MANIFEST_ACCESS_PORT_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/cli/ManifestAccessPort");
|
|
11
|
+
var MANIFEST_CHECKS_TOKEN = /* @__PURE__ */ Symbol.for("saasicat/cli/ManifestChecks");
|
|
12
12
|
|
|
13
13
|
// src/cli-context.service.ts
|
|
14
14
|
import * as os from "os";
|
|
@@ -1072,21 +1072,21 @@ function structuralOnly(line) {
|
|
|
1072
1072
|
return stripLineComment(blankStringLiterals(line));
|
|
1073
1073
|
}
|
|
1074
1074
|
__name(structuralOnly, "structuralOnly");
|
|
1075
|
-
function extractBlockNames(
|
|
1075
|
+
function extractBlockNames(schema2, keyword) {
|
|
1076
1076
|
const pattern = declarationPattern(keyword);
|
|
1077
1077
|
const names = [];
|
|
1078
|
-
for (const line of
|
|
1078
|
+
for (const line of schema2.split("\n")) {
|
|
1079
1079
|
const match = structuralOnly(line).match(pattern);
|
|
1080
1080
|
if (match) names.push(match[1]);
|
|
1081
1081
|
}
|
|
1082
1082
|
return names;
|
|
1083
1083
|
}
|
|
1084
1084
|
__name(extractBlockNames, "extractBlockNames");
|
|
1085
|
-
function extractBlocks(
|
|
1085
|
+
function extractBlocks(schema2, keyword) {
|
|
1086
1086
|
const pattern = declarationPattern(keyword);
|
|
1087
1087
|
const blocks = /* @__PURE__ */ new Map();
|
|
1088
1088
|
let current = null;
|
|
1089
|
-
for (const rawLine of
|
|
1089
|
+
for (const rawLine of schema2.split("\n")) {
|
|
1090
1090
|
const stripped = structuralOnly(rawLine);
|
|
1091
1091
|
const openCount = (stripped.match(/\{/g) ?? []).length;
|
|
1092
1092
|
const closeCount = (stripped.match(/\}/g) ?? []).length;
|
|
@@ -1121,16 +1121,16 @@ function blockBodyLines(block) {
|
|
|
1121
1121
|
__name(blockBodyLines, "blockBodyLines");
|
|
1122
1122
|
|
|
1123
1123
|
// src/schema-apply.ts
|
|
1124
|
-
function extractModelNames(
|
|
1125
|
-
return extractBlockNames(
|
|
1124
|
+
function extractModelNames(schema2) {
|
|
1125
|
+
return extractBlockNames(schema2, "model");
|
|
1126
1126
|
}
|
|
1127
1127
|
__name(extractModelNames, "extractModelNames");
|
|
1128
1128
|
function extractModelBlocks(fragment) {
|
|
1129
1129
|
return extractBlocks(fragment, "model");
|
|
1130
1130
|
}
|
|
1131
1131
|
__name(extractModelBlocks, "extractModelBlocks");
|
|
1132
|
-
function applyFragmentBlocks(
|
|
1133
|
-
const existing = new Set(extractModelNames(
|
|
1132
|
+
function applyFragmentBlocks(schema2, fragmentBlocks, options = {}) {
|
|
1133
|
+
const existing = new Set(extractModelNames(schema2));
|
|
1134
1134
|
const added = [];
|
|
1135
1135
|
const skipped = [];
|
|
1136
1136
|
const additions = [];
|
|
@@ -1146,7 +1146,7 @@ function applyFragmentBlocks(schema, fragmentBlocks, options = {}) {
|
|
|
1146
1146
|
return {
|
|
1147
1147
|
added,
|
|
1148
1148
|
skipped,
|
|
1149
|
-
schema
|
|
1149
|
+
schema: schema2
|
|
1150
1150
|
};
|
|
1151
1151
|
}
|
|
1152
1152
|
const header = options.fragmentLabel ? `
|
|
@@ -1158,7 +1158,7 @@ function applyFragmentBlocks(schema, fragmentBlocks, options = {}) {
|
|
|
1158
1158
|
|
|
1159
1159
|
// Inserted by \`saasicat schema apply\`
|
|
1160
1160
|
`;
|
|
1161
|
-
const trimmedSchema =
|
|
1161
|
+
const trimmedSchema = schema2.endsWith("\n") ? schema2 : schema2 + "\n";
|
|
1162
1162
|
return {
|
|
1163
1163
|
added,
|
|
1164
1164
|
skipped,
|
|
@@ -1216,15 +1216,15 @@ function parseBlockAttributes(name, block) {
|
|
|
1216
1216
|
};
|
|
1217
1217
|
}
|
|
1218
1218
|
__name(parseBlockAttributes, "parseBlockAttributes");
|
|
1219
|
-
function parseSchema(
|
|
1219
|
+
function parseSchema(schema2) {
|
|
1220
1220
|
const models = /* @__PURE__ */ new Map();
|
|
1221
1221
|
const modelAttributes = /* @__PURE__ */ new Map();
|
|
1222
|
-
for (const [name, block] of extractBlocks(
|
|
1222
|
+
for (const [name, block] of extractBlocks(schema2, "model")) {
|
|
1223
1223
|
models.set(name, parseFields(block));
|
|
1224
1224
|
modelAttributes.set(name, parseBlockAttributes(name, block));
|
|
1225
1225
|
}
|
|
1226
1226
|
const enums = /* @__PURE__ */ new Map();
|
|
1227
|
-
for (const [name, block] of extractBlocks(
|
|
1227
|
+
for (const [name, block] of extractBlocks(schema2, "enum")) {
|
|
1228
1228
|
enums.set(name, parseEnumValues(block));
|
|
1229
1229
|
}
|
|
1230
1230
|
return {
|
|
@@ -1350,7 +1350,678 @@ function checkSchema(specSchema, appSchema) {
|
|
|
1350
1350
|
}
|
|
1351
1351
|
__name(checkSchema, "checkSchema");
|
|
1352
1352
|
|
|
1353
|
-
// src/
|
|
1353
|
+
// src/migration-constraints.ts
|
|
1354
|
+
var CONSTRAINTS_MARKER = "-- saasicat:constraints";
|
|
1355
|
+
function migrationCreatedBy(before, after) {
|
|
1356
|
+
const existing = new Set(before);
|
|
1357
|
+
const created = after.filter((name) => /^\d{14}_/.test(name) && !existing.has(name)).sort();
|
|
1358
|
+
return created.length > 0 ? created[created.length - 1] : null;
|
|
1359
|
+
}
|
|
1360
|
+
__name(migrationCreatedBy, "migrationCreatedBy");
|
|
1361
|
+
function hasConstraints(migrationSql) {
|
|
1362
|
+
return migrationSql.includes(CONSTRAINTS_MARKER);
|
|
1363
|
+
}
|
|
1364
|
+
__name(hasConstraints, "hasConstraints");
|
|
1365
|
+
function tablesAddressedBy(statement) {
|
|
1366
|
+
return [
|
|
1367
|
+
...statement.matchAll(/\bON\s+"?(\w+)"?|\bALTER\s+TABLE\s+"?(\w+)"?/gi)
|
|
1368
|
+
].map((m) => m[1] ?? m[2]);
|
|
1369
|
+
}
|
|
1370
|
+
__name(tablesAddressedBy, "tablesAddressedBy");
|
|
1371
|
+
function constraintsFor(constraintsSql, tables) {
|
|
1372
|
+
const known = new Set(tables);
|
|
1373
|
+
return constraintsSql.split(/\n\s*\n/).filter((block) => {
|
|
1374
|
+
const addressed = tablesAddressedBy(block);
|
|
1375
|
+
return addressed.length === 0 || addressed.every((table) => known.has(table));
|
|
1376
|
+
}).join("\n\n").trimEnd();
|
|
1377
|
+
}
|
|
1378
|
+
__name(constraintsFor, "constraintsFor");
|
|
1379
|
+
function appendConstraints(migrationSql, constraintsSql) {
|
|
1380
|
+
if (hasConstraints(migrationSql)) return migrationSql;
|
|
1381
|
+
if (constraintsSql.trim() === "") return migrationSql;
|
|
1382
|
+
const body = migrationSql.endsWith("\n") ? migrationSql : `${migrationSql}
|
|
1383
|
+
`;
|
|
1384
|
+
return `${body}
|
|
1385
|
+
${CONSTRAINTS_MARKER} \u2014 appended by \`saasicat schema migrate\`.
|
|
1386
|
+
-- Source: @saasicat/spec/sql/constraints.postgres.sql. These are part of the
|
|
1387
|
+
-- canonical schema: the adapter contract tests run against a database that has
|
|
1388
|
+
-- them. Edit the spec, not this copy.
|
|
1389
|
+
${constraintsSql.trimEnd()}
|
|
1390
|
+
`;
|
|
1391
|
+
}
|
|
1392
|
+
__name(appendConstraints, "appendConstraints");
|
|
1393
|
+
function reportConstraints(outcome, context) {
|
|
1394
|
+
switch (outcome) {
|
|
1395
|
+
case "appended":
|
|
1396
|
+
return {
|
|
1397
|
+
outcome,
|
|
1398
|
+
mayApply: true,
|
|
1399
|
+
message: ` + appended to ${context.migration}/migration.sql`
|
|
1400
|
+
};
|
|
1401
|
+
case "already-present":
|
|
1402
|
+
return {
|
|
1403
|
+
outcome,
|
|
1404
|
+
mayApply: true,
|
|
1405
|
+
message: ` = ${context.migration} already carries them.`
|
|
1406
|
+
};
|
|
1407
|
+
case "not-applicable":
|
|
1408
|
+
return {
|
|
1409
|
+
outcome,
|
|
1410
|
+
mayApply: true,
|
|
1411
|
+
message: " = none of them apply to the tables in this schema."
|
|
1412
|
+
};
|
|
1413
|
+
case "no-migration":
|
|
1414
|
+
return {
|
|
1415
|
+
outcome,
|
|
1416
|
+
mayApply: true,
|
|
1417
|
+
message: " = Prisma created no migration \u2014 nothing to append to, and nothing new to apply."
|
|
1418
|
+
};
|
|
1419
|
+
case "failed":
|
|
1420
|
+
return {
|
|
1421
|
+
outcome,
|
|
1422
|
+
mayApply: false,
|
|
1423
|
+
message: ` ! Could not append them, so the migration is incomplete. Add ${context.sqlPath} to it by hand, before applying it \u2014 nothing was applied.`
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
__name(reportConstraints, "reportConstraints");
|
|
1428
|
+
|
|
1429
|
+
// src/fk-pointers.ts
|
|
1430
|
+
var POINTER = /^(\s*)\/\/\s*(\w+)(\s+)(Tenant|User)(\??)(\s+)(@relation\(.*\))\s*$/;
|
|
1431
|
+
function findFkPointers(schema2) {
|
|
1432
|
+
const found = [];
|
|
1433
|
+
let model = "";
|
|
1434
|
+
schema2.split("\n").forEach((text, line) => {
|
|
1435
|
+
const opening = /^model\s+(\w+)\s*\{/.exec(text);
|
|
1436
|
+
if (opening) model = opening[1];
|
|
1437
|
+
const match = POINTER.exec(text);
|
|
1438
|
+
if (match) found.push({
|
|
1439
|
+
line,
|
|
1440
|
+
target: match[4],
|
|
1441
|
+
model,
|
|
1442
|
+
text
|
|
1443
|
+
});
|
|
1444
|
+
});
|
|
1445
|
+
return found;
|
|
1446
|
+
}
|
|
1447
|
+
__name(findFkPointers, "findFkPointers");
|
|
1448
|
+
function relationNameOf(relationAttribute) {
|
|
1449
|
+
const match = /@relation\(\s*"([^"]+)"/.exec(relationAttribute);
|
|
1450
|
+
return match ? match[1] : null;
|
|
1451
|
+
}
|
|
1452
|
+
__name(relationNameOf, "relationNameOf");
|
|
1453
|
+
function escapeForRegExp(value) {
|
|
1454
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1455
|
+
}
|
|
1456
|
+
__name(escapeForRegExp, "escapeForRegExp");
|
|
1457
|
+
function modelBody(schema2, model) {
|
|
1458
|
+
const name = escapeForRegExp(model);
|
|
1459
|
+
const block = new RegExp(`(^|\\n)model\\s+${name}\\s*\\{([\\s\\S]*?)\\n\\}`, "m").exec(schema2);
|
|
1460
|
+
return block ? block[2] : null;
|
|
1461
|
+
}
|
|
1462
|
+
__name(modelBody, "modelBody");
|
|
1463
|
+
function isOneToOne(schema2, model, foreignKey) {
|
|
1464
|
+
const body = modelBody(schema2, model);
|
|
1465
|
+
if (!body) return false;
|
|
1466
|
+
return new RegExp(`^\\s*${escapeForRegExp(foreignKey)}\\s+\\S+.*@unique`, "m").test(body);
|
|
1467
|
+
}
|
|
1468
|
+
__name(isOneToOne, "isOneToOne");
|
|
1469
|
+
function hasBackRelation(schema2, model, owner, relationName, singular = false) {
|
|
1470
|
+
const body = modelBody(schema2, owner);
|
|
1471
|
+
if (body === null) return false;
|
|
1472
|
+
const name = escapeForRegExp(model);
|
|
1473
|
+
const shape = singular ? `${name}\\??` : `${name}\\[\\]`;
|
|
1474
|
+
const candidates = [
|
|
1475
|
+
...body.matchAll(new RegExp(`^\\s*\\w+\\s+${shape}(\\s.*)?$`, "gm"))
|
|
1476
|
+
];
|
|
1477
|
+
return candidates.some((line) => relationNameOf(line[0]) === relationName);
|
|
1478
|
+
}
|
|
1479
|
+
__name(hasBackRelation, "hasBackRelation");
|
|
1480
|
+
function enableFkPointers(schema2, models) {
|
|
1481
|
+
const lines = schema2.split("\n");
|
|
1482
|
+
const enabled = [];
|
|
1483
|
+
const skipped = [];
|
|
1484
|
+
const needsBackRelation = [];
|
|
1485
|
+
for (const pointer of findFkPointers(schema2)) {
|
|
1486
|
+
const model = pointer.target === "Tenant" ? models.tenant : models.user;
|
|
1487
|
+
if (!model) {
|
|
1488
|
+
skipped.push({
|
|
1489
|
+
line: pointer.line,
|
|
1490
|
+
target: pointer.target
|
|
1491
|
+
});
|
|
1492
|
+
continue;
|
|
1493
|
+
}
|
|
1494
|
+
const match = POINTER.exec(pointer.text);
|
|
1495
|
+
const relationName = relationNameOf(match[7]);
|
|
1496
|
+
const foreignKey = foreignKeyOf(match[7]);
|
|
1497
|
+
const singular = foreignKey !== null && isOneToOne(schema2, pointer.model, foreignKey);
|
|
1498
|
+
if (!hasBackRelation(schema2, pointer.model, model, relationName, singular)) {
|
|
1499
|
+
needsBackRelation.push({
|
|
1500
|
+
line: pointer.line,
|
|
1501
|
+
owner: model,
|
|
1502
|
+
suggestion: backRelationSuggestion(pointer.model, relationName, singular)
|
|
1503
|
+
});
|
|
1504
|
+
continue;
|
|
1505
|
+
}
|
|
1506
|
+
const [, indent, field, gap1, , optional, gap2, relation] = match;
|
|
1507
|
+
lines[pointer.line] = `${indent}${field}${gap1}${model}${optional}${gap2}${relation}`;
|
|
1508
|
+
enabled.push({
|
|
1509
|
+
line: pointer.line,
|
|
1510
|
+
model
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
return {
|
|
1514
|
+
schema: lines.join("\n"),
|
|
1515
|
+
enabled,
|
|
1516
|
+
skipped,
|
|
1517
|
+
needsBackRelation
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
__name(enableFkPointers, "enableFkPointers");
|
|
1521
|
+
var lowerFirst = /* @__PURE__ */ __name((value) => value.charAt(0).toLowerCase() + value.slice(1), "lowerFirst");
|
|
1522
|
+
function foreignKeyOf(relationAttribute) {
|
|
1523
|
+
const match = /fields:\s*\[\s*(\w+)/.exec(relationAttribute);
|
|
1524
|
+
return match ? match[1] : null;
|
|
1525
|
+
}
|
|
1526
|
+
__name(foreignKeyOf, "foreignKeyOf");
|
|
1527
|
+
function backRelationSuggestion(model, relationName, singular) {
|
|
1528
|
+
const field = singular ? lowerFirst(model) : `${lowerFirst(model)}s`;
|
|
1529
|
+
const type = singular ? `${model}?` : `${model}[]`;
|
|
1530
|
+
return `${field} ${type}` + (relationName ? ` @relation("${relationName}")` : "");
|
|
1531
|
+
}
|
|
1532
|
+
__name(backRelationSuggestion, "backRelationSuggestion");
|
|
1533
|
+
function assertModelsExist(declaredModels, models) {
|
|
1534
|
+
const missing = Object.entries(models).filter(([, name]) => name && !declaredModels.includes(name)).map(([role, name]) => `--${role}-model=${name}`);
|
|
1535
|
+
if (missing.length === 0) return;
|
|
1536
|
+
throw new Error(`${missing.join(", ")} \u2014 no such model in this schema. It declares: ${declaredModels.slice().sort().join(", ")}.`);
|
|
1537
|
+
}
|
|
1538
|
+
__name(assertModelsExist, "assertModelsExist");
|
|
1539
|
+
|
|
1540
|
+
// src/init/catalog-keys.ts
|
|
1541
|
+
import { planCatalogSchema } from "@saasicat/spec";
|
|
1542
|
+
var schema = planCatalogSchema;
|
|
1543
|
+
function required(value, what) {
|
|
1544
|
+
if (value === void 0) {
|
|
1545
|
+
throw new Error(`plan-catalog.schema.json declares no ${what} \u2014 @saasicat/spec and @saasicat/cli are out of step.`);
|
|
1546
|
+
}
|
|
1547
|
+
return value;
|
|
1548
|
+
}
|
|
1549
|
+
__name(required, "required");
|
|
1550
|
+
function projectKeyPattern() {
|
|
1551
|
+
return new RegExp(required(schema.properties?.projectKey?.pattern, "pattern for projectKey"));
|
|
1552
|
+
}
|
|
1553
|
+
__name(projectKeyPattern, "projectKeyPattern");
|
|
1554
|
+
function quotaKeyPattern() {
|
|
1555
|
+
const patterns = Object.keys(required(schema.$defs?.PlanDef?.properties?.quotas?.patternProperties, "quota key pattern"));
|
|
1556
|
+
if (patterns.length !== 1) {
|
|
1557
|
+
throw new Error(`plan-catalog.schema.json declares ${patterns.length} quota key patterns; this derivation can express one.`);
|
|
1558
|
+
}
|
|
1559
|
+
return new RegExp(patterns[0]);
|
|
1560
|
+
}
|
|
1561
|
+
__name(quotaKeyPattern, "quotaKeyPattern");
|
|
1562
|
+
function minimumQuotasPerPlan() {
|
|
1563
|
+
return required(schema.$defs?.PlanDef?.properties?.quotas?.minProperties, "minProperties");
|
|
1564
|
+
}
|
|
1565
|
+
__name(minimumQuotasPerPlan, "minimumQuotasPerPlan");
|
|
1566
|
+
function assertValidProjectKey(projectKey) {
|
|
1567
|
+
const pattern = projectKeyPattern();
|
|
1568
|
+
if (pattern.test(projectKey)) return;
|
|
1569
|
+
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.`);
|
|
1570
|
+
}
|
|
1571
|
+
__name(assertValidProjectKey, "assertValidProjectKey");
|
|
1572
|
+
function assertValidQuotaKey(quotaKey) {
|
|
1573
|
+
const pattern = quotaKeyPattern();
|
|
1574
|
+
if (pattern.test(quotaKey)) return;
|
|
1575
|
+
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.`);
|
|
1576
|
+
}
|
|
1577
|
+
__name(assertValidQuotaKey, "assertValidQuotaKey");
|
|
1578
|
+
|
|
1579
|
+
// src/init/plan.ts
|
|
1580
|
+
function parseQuota(spec) {
|
|
1581
|
+
const [key, model] = spec.split(":");
|
|
1582
|
+
if (!key) throw new Error(`--quota needs a key: got '${spec}'`);
|
|
1583
|
+
assertValidQuotaKey(key);
|
|
1584
|
+
return {
|
|
1585
|
+
key,
|
|
1586
|
+
model: delegateName(model ?? key)
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
__name(parseQuota, "parseQuota");
|
|
1590
|
+
var delegateName = /* @__PURE__ */ __name((model) => model.charAt(0).toLowerCase() + model.slice(1), "delegateName");
|
|
1591
|
+
function assertEnoughQuotas(quotas) {
|
|
1592
|
+
const minimum = minimumQuotasPerPlan();
|
|
1593
|
+
if (quotas.length >= minimum) return;
|
|
1594
|
+
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.`);
|
|
1595
|
+
}
|
|
1596
|
+
__name(assertEnoughQuotas, "assertEnoughQuotas");
|
|
1597
|
+
function pascalCase(value) {
|
|
1598
|
+
return value.split(/[^A-Za-z0-9]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
1599
|
+
}
|
|
1600
|
+
__name(pascalCase, "pascalCase");
|
|
1601
|
+
var quotaFileName = /* @__PURE__ */ __name((key) => `${key.replace(/[^A-Za-z0-9]+/g, "-").toLowerCase()}-quota.provider.ts`, "quotaFileName");
|
|
1602
|
+
function planInit(options) {
|
|
1603
|
+
const projectKey = options.projectKey;
|
|
1604
|
+
if (!projectKey) throw new Error("init needs a --project-key.");
|
|
1605
|
+
assertValidProjectKey(projectKey);
|
|
1606
|
+
const appLabel = options.appName ?? pascalCase(projectKey);
|
|
1607
|
+
const appName = pascalCase(appLabel);
|
|
1608
|
+
const apiBase = options.apiBase ?? "/api/v1/admin";
|
|
1609
|
+
const quotas = (options.quotas ?? []).map(parseQuota);
|
|
1610
|
+
assertEnoughQuotas(quotas);
|
|
1611
|
+
const hasherClass = options.skipHasher ? null : `${appName}PasswordHasher`;
|
|
1612
|
+
const featureKey = `${projectKey.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase()}_CORE`;
|
|
1613
|
+
const shared = {
|
|
1614
|
+
PROJECT_KEY: projectKey,
|
|
1615
|
+
APP_NAME: appName,
|
|
1616
|
+
APP_LABEL: appLabel,
|
|
1617
|
+
API_BASE: apiBase,
|
|
1618
|
+
FEATURE_KEY: featureKey,
|
|
1619
|
+
REGISTRY_CONST: `${constantCase(projectKey)}_FEATURE_UI_REGISTRY`,
|
|
1620
|
+
MANIFEST_CONST: `${constantCase(projectKey)}_MANIFEST_CONTRIBUTION`,
|
|
1621
|
+
ADMIN_MODULE_CLASS: `${appName}AdminModule`,
|
|
1622
|
+
HASHER_CLASS: hasherClass ?? "",
|
|
1623
|
+
HASHER_FILE: hasherClass ? `${kebabCase(appName)}-password.hasher` : "",
|
|
1624
|
+
STARTER_QUOTAS: renderQuotaBlock(quotas, 25),
|
|
1625
|
+
PRO_QUOTAS: renderQuotaBlock(quotas, 1e3)
|
|
1626
|
+
};
|
|
1627
|
+
const files = [
|
|
1628
|
+
{
|
|
1629
|
+
path: "config/saas.yaml",
|
|
1630
|
+
template: "config/saas.yaml",
|
|
1631
|
+
tokens: {}
|
|
1632
|
+
},
|
|
1633
|
+
{
|
|
1634
|
+
path: "src/saas/feature-ui-registry.ts",
|
|
1635
|
+
template: "src/saas/feature-ui-registry.ts",
|
|
1636
|
+
tokens: {}
|
|
1637
|
+
},
|
|
1638
|
+
{
|
|
1639
|
+
path: "src/saas/admin-manifest.contribution.ts",
|
|
1640
|
+
template: "src/saas/admin-manifest.contribution.ts",
|
|
1641
|
+
tokens: {}
|
|
1642
|
+
},
|
|
1643
|
+
{
|
|
1644
|
+
path: `src/saas/${kebabCase(appName)}-admin.module.ts`,
|
|
1645
|
+
template: "src/saas/admin.module.ts",
|
|
1646
|
+
tokens: {}
|
|
1647
|
+
}
|
|
1648
|
+
];
|
|
1649
|
+
files.push({
|
|
1650
|
+
path: "src/saas/persistence.ts",
|
|
1651
|
+
template: hasherClass ? "src/saas/persistence.ts" : "src/saas/persistence-without-hasher.ts",
|
|
1652
|
+
tokens: {}
|
|
1653
|
+
});
|
|
1654
|
+
if (hasherClass) {
|
|
1655
|
+
files.push({
|
|
1656
|
+
path: `src/auth/${kebabCase(appName)}-password.hasher.ts`,
|
|
1657
|
+
template: "src/auth/password.hasher.ts",
|
|
1658
|
+
tokens: {}
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
const quotaProviders = quotas.map((quota) => ({
|
|
1662
|
+
className: `${pascalCase(quota.key)}QuotaProvider`,
|
|
1663
|
+
path: `src/saas/${quotaFileName(quota.key)}`
|
|
1664
|
+
}));
|
|
1665
|
+
for (const quota of quotas) {
|
|
1666
|
+
files.push({
|
|
1667
|
+
path: `src/saas/${quotaFileName(quota.key)}`,
|
|
1668
|
+
template: "src/saas/quota.provider.ts",
|
|
1669
|
+
tokens: {
|
|
1670
|
+
QUOTA_KEY: quota.key,
|
|
1671
|
+
QUOTA_LABEL: `${pascalCase(quota.key)} count`,
|
|
1672
|
+
QUOTA_CLASS: `${pascalCase(quota.key)}QuotaProvider`,
|
|
1673
|
+
QUOTA_MODEL: quota.model
|
|
1674
|
+
}
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
return {
|
|
1678
|
+
files,
|
|
1679
|
+
tokens: shared,
|
|
1680
|
+
quotaProviders,
|
|
1681
|
+
hasherClass
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
__name(planInit, "planInit");
|
|
1685
|
+
var constantCase = /* @__PURE__ */ __name((value) => value.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase(), "constantCase");
|
|
1686
|
+
var kebabCase = /* @__PURE__ */ __name((value) => value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^A-Za-z0-9]+/g, "-").toLowerCase(), "kebabCase");
|
|
1687
|
+
function renderQuotaBlock(quotas, limit) {
|
|
1688
|
+
if (quotas.length === 0) return " {}";
|
|
1689
|
+
return quotas.map((quota) => `
|
|
1690
|
+
${quota.key}: ${limit}`).join("");
|
|
1691
|
+
}
|
|
1692
|
+
__name(renderQuotaBlock, "renderQuotaBlock");
|
|
1693
|
+
function applyTokens(content, tokens) {
|
|
1694
|
+
return content.replace(/__([A-Z_]+)__/g, (full, key) => tokens[key] === void 0 ? full : tokens[key]);
|
|
1695
|
+
}
|
|
1696
|
+
__name(applyTokens, "applyTokens");
|
|
1697
|
+
function patchOptionsFor(plan) {
|
|
1698
|
+
const generates = /* @__PURE__ */ __name((path) => plan.files.some((file) => file.path === path), "generates");
|
|
1699
|
+
return {
|
|
1700
|
+
persistenceImport: generates("src/saas/persistence.ts") ? "./saas/persistence" : null,
|
|
1701
|
+
adminModule: {
|
|
1702
|
+
className: plan.tokens.ADMIN_MODULE_CLASS,
|
|
1703
|
+
importPath: `./saas/${kebabCase(plan.tokens.APP_NAME)}-admin.module`
|
|
1704
|
+
},
|
|
1705
|
+
// Read off the plan, not re-derived: the file it wrote is the file the
|
|
1706
|
+
// import has to name.
|
|
1707
|
+
quotaProviders: plan.quotaProviders.map(({ className, path }) => ({
|
|
1708
|
+
className,
|
|
1709
|
+
importPath: `./${path.replace(/^src\//, "").replace(/\.ts$/, "")}`
|
|
1710
|
+
})),
|
|
1711
|
+
registry: {
|
|
1712
|
+
constName: plan.tokens.REGISTRY_CONST,
|
|
1713
|
+
importPath: "./saas/feature-ui-registry"
|
|
1714
|
+
}
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
__name(patchOptionsFor, "patchOptionsFor");
|
|
1718
|
+
|
|
1719
|
+
// src/codemods/v1-imports.ts
|
|
1720
|
+
var PUBLIC_PREFIXES = [
|
|
1721
|
+
"ui/",
|
|
1722
|
+
"layouts/",
|
|
1723
|
+
"auth/",
|
|
1724
|
+
"pages/"
|
|
1725
|
+
];
|
|
1726
|
+
function buildImportMap(table) {
|
|
1727
|
+
const map = /* @__PURE__ */ new Map();
|
|
1728
|
+
for (const [from, to] of Object.entries(table.moves)) {
|
|
1729
|
+
const onSurface = PUBLIC_PREFIXES.some((prefix) => to.startsWith(prefix));
|
|
1730
|
+
if (!onSurface && !to.startsWith("@")) continue;
|
|
1731
|
+
if (from.startsWith("components/")) {
|
|
1732
|
+
map.set(from, to);
|
|
1733
|
+
continue;
|
|
1734
|
+
}
|
|
1735
|
+
if (!from.startsWith("pages-standard/")) continue;
|
|
1736
|
+
const file = from.slice("pages-standard/".length);
|
|
1737
|
+
if (file.includes("/")) continue;
|
|
1738
|
+
map.set(from, to);
|
|
1739
|
+
map.set(`pages/${file}`, to);
|
|
1740
|
+
}
|
|
1741
|
+
for (const [from, to] of Object.entries(table.packages ?? {})) {
|
|
1742
|
+
if (from === "_") continue;
|
|
1743
|
+
map.set(from, to);
|
|
1744
|
+
}
|
|
1745
|
+
for (const [from, to] of Object.entries(table.moveDirectories ?? {})) {
|
|
1746
|
+
map.set(`${from}/`, `${to}/`);
|
|
1747
|
+
}
|
|
1748
|
+
return map;
|
|
1749
|
+
}
|
|
1750
|
+
__name(buildImportMap, "buildImportMap");
|
|
1751
|
+
function wentPrivate(map, subpath) {
|
|
1752
|
+
for (const [prefix, target] of map) {
|
|
1753
|
+
if (prefix.endsWith("/") && target.startsWith("internal/") && subpath.startsWith(prefix)) {
|
|
1754
|
+
return true;
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
return false;
|
|
1758
|
+
}
|
|
1759
|
+
__name(wentPrivate, "wentPrivate");
|
|
1760
|
+
function rewriteSubpath(map, subpath) {
|
|
1761
|
+
const direct = map.get(subpath);
|
|
1762
|
+
if (direct !== void 0 && direct !== subpath) return direct;
|
|
1763
|
+
for (const [prefix, target] of map) {
|
|
1764
|
+
if (prefix.endsWith("/") && target.startsWith("@") && subpath.startsWith(prefix)) {
|
|
1765
|
+
return `${target}${subpath.slice(prefix.length)}`;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
if (subpath.startsWith("pages-standard/") && !wentPrivate(map, subpath)) {
|
|
1769
|
+
const file = subpath.slice("pages-standard/".length);
|
|
1770
|
+
if (!file.includes("/")) return `pages/${file}`;
|
|
1771
|
+
}
|
|
1772
|
+
return null;
|
|
1773
|
+
}
|
|
1774
|
+
__name(rewriteSubpath, "rewriteSubpath");
|
|
1775
|
+
function isNoLongerPublic(map, subpath) {
|
|
1776
|
+
if (subpath.startsWith("components/")) return !map.has(subpath);
|
|
1777
|
+
return subpath.startsWith("pages-standard/") && wentPrivate(map, subpath);
|
|
1778
|
+
}
|
|
1779
|
+
__name(isNoLongerPublic, "isNoLongerPublic");
|
|
1780
|
+
var UI_VUE_SPECIFIER = /@saasicat\/ui-vue\/([A-Za-z0-9/_.-]+)/g;
|
|
1781
|
+
function rewriteImports(text, map) {
|
|
1782
|
+
const unmapped = /* @__PURE__ */ new Map();
|
|
1783
|
+
let rewritten = 0;
|
|
1784
|
+
const next = text.replace(UI_VUE_SPECIFIER, (whole, subpath) => {
|
|
1785
|
+
const to = rewriteSubpath(map, subpath);
|
|
1786
|
+
if (to === null) {
|
|
1787
|
+
if (isNoLongerPublic(map, subpath)) {
|
|
1788
|
+
unmapped.set(subpath, (unmapped.get(subpath) ?? 0) + 1);
|
|
1789
|
+
}
|
|
1790
|
+
return whole;
|
|
1791
|
+
}
|
|
1792
|
+
rewritten += 1;
|
|
1793
|
+
return to.startsWith("@") ? to : `@saasicat/ui-vue/${to}`;
|
|
1794
|
+
});
|
|
1795
|
+
return {
|
|
1796
|
+
text: next,
|
|
1797
|
+
rewritten,
|
|
1798
|
+
unmapped
|
|
1799
|
+
};
|
|
1800
|
+
}
|
|
1801
|
+
__name(rewriteImports, "rewriteImports");
|
|
1802
|
+
|
|
1803
|
+
// src/codemods/v1-rename.ts
|
|
1804
|
+
var escape = /* @__PURE__ */ __name((s) => s.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&"), "escape");
|
|
1805
|
+
var FROM_SPECIFIER = /from\s*(['"])([^'"]+)\1/g;
|
|
1806
|
+
var isSpace = /* @__PURE__ */ __name((ch) => ch === " " || ch === " " || ch === "\n" || ch === "\r", "isSpace");
|
|
1807
|
+
function namedImports(text) {
|
|
1808
|
+
const found = [];
|
|
1809
|
+
for (const match of text.matchAll(FROM_SPECIFIER)) {
|
|
1810
|
+
let i = (match.index ?? 0) - 1;
|
|
1811
|
+
while (i >= 0 && isSpace(text[i])) i -= 1;
|
|
1812
|
+
if (text[i] !== "}") continue;
|
|
1813
|
+
const close = i;
|
|
1814
|
+
const open = text.lastIndexOf("{", close);
|
|
1815
|
+
if (open < 0) continue;
|
|
1816
|
+
i = open - 1;
|
|
1817
|
+
while (i >= 0 && isSpace(text[i])) i -= 1;
|
|
1818
|
+
let head = text.slice(Math.max(0, i - 3), i + 1);
|
|
1819
|
+
if (head === "type") {
|
|
1820
|
+
i -= 4;
|
|
1821
|
+
while (i >= 0 && isSpace(text[i])) i -= 1;
|
|
1822
|
+
head = text.slice(Math.max(0, i - 5), i + 1);
|
|
1823
|
+
} else {
|
|
1824
|
+
head = text.slice(Math.max(0, i - 5), i + 1);
|
|
1825
|
+
}
|
|
1826
|
+
if (head !== "import") continue;
|
|
1827
|
+
const names = text.slice(open + 1, close).split(",").map((raw) => {
|
|
1828
|
+
const words = raw.trim().split(/\s+/);
|
|
1829
|
+
if (words[0] === "type") words.shift();
|
|
1830
|
+
return words[0] ?? "";
|
|
1831
|
+
}).filter((name) => name.length > 0);
|
|
1832
|
+
found.push({
|
|
1833
|
+
names,
|
|
1834
|
+
specifier: match[2]
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
return found;
|
|
1838
|
+
}
|
|
1839
|
+
__name(namedImports, "namedImports");
|
|
1840
|
+
function rewriteNames(text, table) {
|
|
1841
|
+
let next = text;
|
|
1842
|
+
let rewritten = 0;
|
|
1843
|
+
const ambiguous = /* @__PURE__ */ new Set();
|
|
1844
|
+
const perEntry = /* @__PURE__ */ new Map();
|
|
1845
|
+
for (const { names, specifier } of namedImports(text)) {
|
|
1846
|
+
const mapping = table.entryTokens[specifier];
|
|
1847
|
+
for (const name of names) {
|
|
1848
|
+
const knownSomewhere = Object.values(table.entryTokens).some((m) => name in m);
|
|
1849
|
+
if (!knownSomewhere) continue;
|
|
1850
|
+
const already = perEntry.get(name);
|
|
1851
|
+
if (mapping && name in mapping && (already === void 0 || already === mapping[name])) {
|
|
1852
|
+
perEntry.set(name, mapping[name]);
|
|
1853
|
+
} else {
|
|
1854
|
+
if (already !== void 0) perEntry.delete(name);
|
|
1855
|
+
ambiguous.add(`${name} from '${specifier}'`);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
for (const [from, to] of perEntry) {
|
|
1860
|
+
next = next.replace(new RegExp(`\\b${escape(from)}\\b`, "g"), () => {
|
|
1861
|
+
rewritten += 1;
|
|
1862
|
+
return to;
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
for (const [from, to] of Object.entries(table.identifierStems)) {
|
|
1866
|
+
next = next.replace(new RegExp(escape(from), "g"), () => {
|
|
1867
|
+
rewritten += 1;
|
|
1868
|
+
return to;
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
for (const [from, to] of Object.entries(table.registryKeys)) {
|
|
1872
|
+
const symbolFor = new RegExp(`(Symbol\\.for\\(\\s*['"\`])${escape(from)}`, "g");
|
|
1873
|
+
next = next.replace(symbolFor, (_, head) => {
|
|
1874
|
+
rewritten += 1;
|
|
1875
|
+
return `${head}${to}`;
|
|
1876
|
+
});
|
|
1877
|
+
if (from.startsWith("@")) continue;
|
|
1878
|
+
const literal = new RegExp(`(['"\`])${escape(from)}`, "g");
|
|
1879
|
+
next = next.replace(literal, (_, quote) => {
|
|
1880
|
+
rewritten += 1;
|
|
1881
|
+
return `${quote}${to}`;
|
|
1882
|
+
});
|
|
1883
|
+
}
|
|
1884
|
+
for (const [from, to] of Object.entries(table.subpaths)) {
|
|
1885
|
+
next = next.replace(new RegExp(escape(from), "g"), () => {
|
|
1886
|
+
rewritten += 1;
|
|
1887
|
+
return to;
|
|
1888
|
+
});
|
|
1889
|
+
}
|
|
1890
|
+
return {
|
|
1891
|
+
text: next,
|
|
1892
|
+
rewritten,
|
|
1893
|
+
ambiguous: [
|
|
1894
|
+
...ambiguous
|
|
1895
|
+
].sort()
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
__name(rewriteNames, "rewriteNames");
|
|
1899
|
+
|
|
1900
|
+
// src/init/patch-app-module.ts
|
|
1901
|
+
var MARKER = "SaaSiCatModule.forRoot";
|
|
1902
|
+
function patchAppModule(source, options) {
|
|
1903
|
+
const block = renderForRootBlock(options);
|
|
1904
|
+
const manualBlock = `${renderImports(options)}
|
|
1905
|
+
|
|
1906
|
+
${block}`;
|
|
1907
|
+
if (source.includes(MARKER)) {
|
|
1908
|
+
return {
|
|
1909
|
+
source,
|
|
1910
|
+
status: "already-wired",
|
|
1911
|
+
reason: "",
|
|
1912
|
+
manualBlock: ""
|
|
1913
|
+
};
|
|
1914
|
+
}
|
|
1915
|
+
const importsArray = findImportsArray(source);
|
|
1916
|
+
if (!importsArray) {
|
|
1917
|
+
return {
|
|
1918
|
+
source,
|
|
1919
|
+
status: "declined",
|
|
1920
|
+
reason: "no `@Module({ imports: [ ... ] })` was found in this file, so there is nowhere to add the platform without guessing at the structure",
|
|
1921
|
+
manualBlock
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
const withImports = addImportStatements(source, options);
|
|
1925
|
+
const target = findImportsArray(withImports);
|
|
1926
|
+
const indent = " ".repeat(target.indent + 4);
|
|
1927
|
+
const entries = `${block},
|
|
1928
|
+
${options.adminModule.className},`;
|
|
1929
|
+
const wasEmpty = /^\s*\]/.test(withImports.slice(target.openBracket + 1));
|
|
1930
|
+
const trailing = wasEmpty ? " ".repeat(target.indent) : indent;
|
|
1931
|
+
const inserted = `
|
|
1932
|
+
${indent}${entries.split("\n").join(`
|
|
1933
|
+
${indent}`)}
|
|
1934
|
+
${trailing}`;
|
|
1935
|
+
return {
|
|
1936
|
+
source: withImports.slice(0, target.openBracket + 1) + inserted + withImports.slice(target.openBracket + 1),
|
|
1937
|
+
status: "patched",
|
|
1938
|
+
reason: "",
|
|
1939
|
+
manualBlock: ""
|
|
1940
|
+
};
|
|
1941
|
+
}
|
|
1942
|
+
__name(patchAppModule, "patchAppModule");
|
|
1943
|
+
function findImportsArray(source) {
|
|
1944
|
+
const decorator = source.indexOf("@Module(");
|
|
1945
|
+
if (decorator === -1) return null;
|
|
1946
|
+
const match = /(^|\n)([ \t]*)imports\s*:\s*\[/.exec(source.slice(decorator));
|
|
1947
|
+
if (!match) return null;
|
|
1948
|
+
const openBracket = decorator + match.index + match[0].length - 1;
|
|
1949
|
+
return {
|
|
1950
|
+
openBracket,
|
|
1951
|
+
indent: match[2].length
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
__name(findImportsArray, "findImportsArray");
|
|
1955
|
+
function renderImports(options) {
|
|
1956
|
+
const lines = [
|
|
1957
|
+
"import { loadPlanCatalogFromFile } from '@saasicat/nest/billing';",
|
|
1958
|
+
"import { SaaSiCatModule, defineSaaSiCat } from '@saasicat/nest/platform';",
|
|
1959
|
+
`import { ${options.registry.constName} } from '${options.registry.importPath}';`,
|
|
1960
|
+
`import { ${options.adminModule.className} } from '${options.adminModule.importPath}';`
|
|
1961
|
+
];
|
|
1962
|
+
if (options.persistenceImport) {
|
|
1963
|
+
lines.push(`import { persistence } from '${options.persistenceImport}';`);
|
|
1964
|
+
}
|
|
1965
|
+
for (const quota of options.quotaProviders) {
|
|
1966
|
+
lines.push(`import { ${quota.className} } from '${quota.importPath}';`);
|
|
1967
|
+
}
|
|
1968
|
+
return lines.join("\n");
|
|
1969
|
+
}
|
|
1970
|
+
__name(renderImports, "renderImports");
|
|
1971
|
+
function addImportStatements(source, options) {
|
|
1972
|
+
const lines = source.split("\n");
|
|
1973
|
+
const block = renderImports(options).split("\n");
|
|
1974
|
+
lines.splice(endOfLastImport(lines) + 1, 0, ...block);
|
|
1975
|
+
return lines.join("\n");
|
|
1976
|
+
}
|
|
1977
|
+
__name(addImportStatements, "addImportStatements");
|
|
1978
|
+
function endOfLastImport(lines) {
|
|
1979
|
+
let end = -1;
|
|
1980
|
+
let open = false;
|
|
1981
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
1982
|
+
const line = lines[index];
|
|
1983
|
+
if (!open && !/^\s*import\s/.test(line)) continue;
|
|
1984
|
+
open = true;
|
|
1985
|
+
if (/\bfrom\s+['"][^'"]+['"]/.test(line) || /^\s*import\s+['"][^'"]+['"]/.test(line)) {
|
|
1986
|
+
end = index;
|
|
1987
|
+
open = false;
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
return end;
|
|
1991
|
+
}
|
|
1992
|
+
__name(endOfLastImport, "endOfLastImport");
|
|
1993
|
+
function renderForRootBlock(options) {
|
|
1994
|
+
const quotaList = options.quotaProviders.map((q) => q.className).join(", ");
|
|
1995
|
+
return [
|
|
1996
|
+
"SaaSiCatModule.forRoot(",
|
|
1997
|
+
" defineSaaSiCat({",
|
|
1998
|
+
" // Plans straight from the YAML. Apps that manage plans in the",
|
|
1999
|
+
" // SuperAdmin UI pass `dbCatalog` instead.",
|
|
2000
|
+
" planCatalog: loadPlanCatalogFromFile({ path: 'config/saas.yaml' }),",
|
|
2001
|
+
" // Your authentication guard. This does NOT compile until you",
|
|
2002
|
+
" // name one, and that is deliberate: an empty array is how the",
|
|
2003
|
+
" // platform is told an endpoint should be auth-free, so a",
|
|
2004
|
+
" // placeholder `[]` here would publish GET /admin/discovery \u2014",
|
|
2005
|
+
" // your whole capability inventory \u2014 and the manifest routes to",
|
|
2006
|
+
" // anyone who asks. Import your guard and put it in.",
|
|
2007
|
+
" controller: { guards: [YourAuthGuard] },",
|
|
2008
|
+
options.persistenceImport ? " persistence," : " // persistence: prismaPersistence({ client: PrismaService }),",
|
|
2009
|
+
" catalog: { featureUiRegistry: " + options.registry.constName + " },",
|
|
2010
|
+
" adminResources: true,",
|
|
2011
|
+
" promoCodes: true,",
|
|
2012
|
+
quotaList ? ` quotaProviders: [${quotaList}],` : " quotaProviders: [],",
|
|
2013
|
+
" }),",
|
|
2014
|
+
")"
|
|
2015
|
+
].join("\n");
|
|
2016
|
+
}
|
|
2017
|
+
__name(renderForRootBlock, "renderForRootBlock");
|
|
2018
|
+
var LIMIT_FILTER_PROVIDER = "{ provide: APP_FILTER, useClass: LimitExceededFilter }";
|
|
2019
|
+
var LIMIT_FILTER_IMPORTS = [
|
|
2020
|
+
"import { APP_FILTER } from '@nestjs/core';",
|
|
2021
|
+
"import { LimitExceededFilter } from '@saasicat/nest/billing';"
|
|
2022
|
+
].join("\n");
|
|
2023
|
+
|
|
2024
|
+
// src/cli-context.module.ts
|
|
1354
2025
|
import { Module } from "@nestjs/common";
|
|
1355
2026
|
import { asProvider } from "@saasicat/nest";
|
|
1356
2027
|
function _ts_decorate8(decorators, target, key, desc) {
|
|
@@ -2411,6 +3082,7 @@ export {
|
|
|
2411
3082
|
AuditTailCommand,
|
|
2412
3083
|
AuditTailFlow,
|
|
2413
3084
|
CLI_CONTEXT_CONFIG_TOKEN,
|
|
3085
|
+
CONSTRAINTS_MARKER,
|
|
2414
3086
|
CliContextModule,
|
|
2415
3087
|
CliContextService,
|
|
2416
3088
|
CliError,
|
|
@@ -2421,6 +3093,8 @@ export {
|
|
|
2421
3093
|
DiscoverySnapshotDoctorCheck,
|
|
2422
3094
|
DoctorCommands,
|
|
2423
3095
|
DoctorFlow,
|
|
3096
|
+
LIMIT_FILTER_IMPORTS,
|
|
3097
|
+
LIMIT_FILTER_PROVIDER,
|
|
2424
3098
|
MANIFEST_ACCESS_PORT_TOKEN,
|
|
2425
3099
|
MANIFEST_CHECKS_TOKEN,
|
|
2426
3100
|
ManifestCheckCommand,
|
|
@@ -2432,24 +3106,56 @@ export {
|
|
|
2432
3106
|
MfaSetupFlow,
|
|
2433
3107
|
PLATFORM_DOCTOR_CHECK_PROVIDERS,
|
|
2434
3108
|
PlanCatalogDoctorCheck,
|
|
3109
|
+
UI_VUE_SPECIFIER,
|
|
2435
3110
|
USER_MANAGEMENT_PORT_TOKEN,
|
|
2436
3111
|
USER_PORT_TOKEN,
|
|
2437
3112
|
UserCommands,
|
|
2438
3113
|
UserPortDoctorCheck,
|
|
2439
3114
|
WhoAmIFlow,
|
|
3115
|
+
appendConstraints,
|
|
2440
3116
|
applyFragmentBlocks,
|
|
3117
|
+
applyTokens,
|
|
3118
|
+
assertModelsExist,
|
|
3119
|
+
assertValidProjectKey,
|
|
3120
|
+
assertValidQuotaKey,
|
|
2441
3121
|
blankStringLiterals,
|
|
2442
3122
|
blockBodyLines,
|
|
2443
3123
|
breaksContract,
|
|
3124
|
+
buildImportMap,
|
|
2444
3125
|
checkSchema,
|
|
3126
|
+
constraintsFor,
|
|
3127
|
+
enableFkPointers,
|
|
2445
3128
|
extractBlockNames,
|
|
2446
3129
|
extractBlocks,
|
|
2447
3130
|
extractModelBlocks,
|
|
2448
3131
|
extractModelNames,
|
|
3132
|
+
findFkPointers,
|
|
3133
|
+
foreignKeyOf,
|
|
3134
|
+
hasBackRelation,
|
|
3135
|
+
hasConstraints,
|
|
3136
|
+
isNoLongerPublic,
|
|
3137
|
+
isOneToOne,
|
|
3138
|
+
kebabCase,
|
|
3139
|
+
migrationCreatedBy,
|
|
3140
|
+
minimumQuotasPerPlan,
|
|
3141
|
+
namedImports,
|
|
2449
3142
|
parseBlockAttributes,
|
|
2450
3143
|
parseEnumValues,
|
|
2451
3144
|
parseFields,
|
|
3145
|
+
parseQuota,
|
|
2452
3146
|
parseSchema,
|
|
3147
|
+
pascalCase,
|
|
3148
|
+
patchAppModule,
|
|
3149
|
+
patchOptionsFor,
|
|
3150
|
+
planInit,
|
|
3151
|
+
projectKeyPattern,
|
|
3152
|
+
quotaKeyPattern,
|
|
3153
|
+
relationNameOf,
|
|
3154
|
+
reportConstraints,
|
|
3155
|
+
rewriteImports,
|
|
3156
|
+
rewriteNames,
|
|
3157
|
+
rewriteSubpath,
|
|
2453
3158
|
stripLineComment,
|
|
2454
|
-
structuralOnly
|
|
3159
|
+
structuralOnly,
|
|
3160
|
+
tablesAddressedBy
|
|
2455
3161
|
};
|