@saasicat/cli 1.0.0-rc.2 → 1.0.0-rc.20
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/README.md +36 -1
- package/bin/saasicat.js +267 -21
- package/dist/.build-stamp +1 -1
- package/dist/index.cjs +623 -148
- package/dist/index.d.cts +222 -15
- package/dist/index.d.ts +222 -15
- package/dist/index.js +610 -147
- package/package.json +4 -4
- package/templates/init/config/saas.yaml.tpl +40 -2
- package/templates/init/src/saas/admin-manifest.contribution.ts.tpl +1 -1
- package/templates/init/src/saas/persistence-without-hasher.ts.tpl +1 -1
- package/templates/init/src/saas/persistence.ts.tpl +1 -1
package/dist/index.js
CHANGED
|
@@ -869,7 +869,7 @@ __name(collectComponentKeys, "collectComponentKeys");
|
|
|
869
869
|
|
|
870
870
|
// src/default-doctor-checks.ts
|
|
871
871
|
import { Inject as Inject6, Injectable as Injectable7 } from "@nestjs/common";
|
|
872
|
-
import { AdminManifestService, DISCOVERY_SNAPSHOT_TOKEN, PLAN_CATALOG_TOKEN } from "@saasicat/nest";
|
|
872
|
+
import { AdminManifestService, DISCOVERY_SNAPSHOT_TOKEN, IssuerIdentityInspector, PLAN_CATALOG_TOKEN } from "@saasicat/nest";
|
|
873
873
|
function _ts_decorate7(decorators, target, key, desc) {
|
|
874
874
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
875
875
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -909,7 +909,7 @@ var PlanCatalogDoctorCheck = class {
|
|
|
909
909
|
severity: "ok",
|
|
910
910
|
message: `${plans.length} plan(s), ${this.catalog.features?.length ?? 0} feature(s) loaded.`,
|
|
911
911
|
details: {
|
|
912
|
-
|
|
912
|
+
app: this.catalog.app.name,
|
|
913
913
|
planIds: plans.map((p) => p.id)
|
|
914
914
|
}
|
|
915
915
|
};
|
|
@@ -1021,11 +1021,85 @@ AdminManifestDoctorCheck = _ts_decorate7([
|
|
|
1021
1021
|
typeof AdminManifestService === "undefined" ? Object : AdminManifestService
|
|
1022
1022
|
])
|
|
1023
1023
|
], AdminManifestDoctorCheck);
|
|
1024
|
+
var IssuerIdentityDoctorCheck = class {
|
|
1025
|
+
static {
|
|
1026
|
+
__name(this, "IssuerIdentityDoctorCheck");
|
|
1027
|
+
}
|
|
1028
|
+
issuer;
|
|
1029
|
+
id = "platform.issuer-identity";
|
|
1030
|
+
label = "Issuer identity against the recorded one";
|
|
1031
|
+
constructor(issuer) {
|
|
1032
|
+
this.issuer = issuer;
|
|
1033
|
+
}
|
|
1034
|
+
async run() {
|
|
1035
|
+
const verdict = await this.issuer.inspect();
|
|
1036
|
+
if (verdict.kind === "refused") {
|
|
1037
|
+
return {
|
|
1038
|
+
severity: "error",
|
|
1039
|
+
message: verdict.refusal,
|
|
1040
|
+
// The number and the identifiers, not the rows: `DoctorReport`
|
|
1041
|
+
// is serialised as JSON, and the dates on a row would come out
|
|
1042
|
+
// as timestamps beside the same dates already written as days in
|
|
1043
|
+
// the message above.
|
|
1044
|
+
// One unknown answered once: `null` for both where the
|
|
1045
|
+
// contracts could not be read, rather than a null count beside
|
|
1046
|
+
// an empty list that reads as "none".
|
|
1047
|
+
details: verdict.running ? {
|
|
1048
|
+
runningContracts: verdict.running.total,
|
|
1049
|
+
named: verdict.running.contracts.map((row) => row.id)
|
|
1050
|
+
} : {
|
|
1051
|
+
runningContracts: null,
|
|
1052
|
+
named: null
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
if (verdict.kind === "not-compared") {
|
|
1057
|
+
return {
|
|
1058
|
+
severity: "warning",
|
|
1059
|
+
message: `The issuer in the file is compared with nothing \u2014 ${verdict.why} \u2014 so a start that names another legal entity than the last one is not refused.`
|
|
1060
|
+
};
|
|
1061
|
+
}
|
|
1062
|
+
const change = verdict.change;
|
|
1063
|
+
switch (change.kind) {
|
|
1064
|
+
case "none-named":
|
|
1065
|
+
return {
|
|
1066
|
+
severity: "ok",
|
|
1067
|
+
message: "No issuer is named, and none was recorded."
|
|
1068
|
+
};
|
|
1069
|
+
case "first-naming":
|
|
1070
|
+
return {
|
|
1071
|
+
severity: "ok",
|
|
1072
|
+
message: `'${change.identity.legalName}' is named for the first time; no contract can name another.`
|
|
1073
|
+
};
|
|
1074
|
+
case "corrected":
|
|
1075
|
+
return {
|
|
1076
|
+
severity: "ok",
|
|
1077
|
+
message: `A correction of '${change.recorded.legalName}' is declared: ${change.reason}. This is what the start found; whether it has been recorded is what \`GET /admin/settings\` shows, and \`issuer.correctionOf\` may be dropped once it does.`
|
|
1078
|
+
};
|
|
1079
|
+
case "unchanged": {
|
|
1080
|
+
const running = await this.issuer.runningContractCount();
|
|
1081
|
+
const weighedAgainst = running === null ? "" : ` ${running} contract(s) are running, and their issuer copies do not follow a change.`;
|
|
1082
|
+
return {
|
|
1083
|
+
severity: "ok",
|
|
1084
|
+
message: `'${change.identity.legalName}' is what the installation recorded.` + weighedAgainst + " Changing the legal name or a tax identifier needs `issuer.correctionOf` beside the values it replaces; the address and the contact details do not."
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
};
|
|
1090
|
+
IssuerIdentityDoctorCheck = _ts_decorate7([
|
|
1091
|
+
Injectable7(),
|
|
1092
|
+
_ts_metadata7("design:type", Function),
|
|
1093
|
+
_ts_metadata7("design:paramtypes", [
|
|
1094
|
+
typeof IssuerIdentityInspector === "undefined" ? Object : IssuerIdentityInspector
|
|
1095
|
+
])
|
|
1096
|
+
], IssuerIdentityDoctorCheck);
|
|
1024
1097
|
var PLATFORM_DOCTOR_CHECK_PROVIDERS = [
|
|
1025
1098
|
PlanCatalogDoctorCheck,
|
|
1026
1099
|
DiscoverySnapshotDoctorCheck,
|
|
1027
1100
|
UserPortDoctorCheck,
|
|
1028
|
-
AdminManifestDoctorCheck
|
|
1101
|
+
AdminManifestDoctorCheck,
|
|
1102
|
+
IssuerIdentityDoctorCheck
|
|
1029
1103
|
];
|
|
1030
1104
|
|
|
1031
1105
|
// src/prisma-blocks.ts
|
|
@@ -1201,6 +1275,117 @@ function applyFragmentBlocks(schema2, fragmentBlocks, options = {}) {
|
|
|
1201
1275
|
}
|
|
1202
1276
|
__name(applyFragmentBlocks, "applyFragmentBlocks");
|
|
1203
1277
|
|
|
1278
|
+
// src/fk-pointers.ts
|
|
1279
|
+
var POINTER = /^(\s*)\/\/\s*(\w+)(\s+)(Tenant|User)(\??)(\s+)(@relation\(.*\))\s*$/;
|
|
1280
|
+
function findFkPointers(schema2) {
|
|
1281
|
+
const found = [];
|
|
1282
|
+
let model = "";
|
|
1283
|
+
schema2.split("\n").forEach((text, line) => {
|
|
1284
|
+
const opening = /^model\s+(\w+)\s*\{/.exec(text);
|
|
1285
|
+
if (opening) model = opening[1];
|
|
1286
|
+
const match = POINTER.exec(text);
|
|
1287
|
+
if (match) found.push({
|
|
1288
|
+
line,
|
|
1289
|
+
target: match[4],
|
|
1290
|
+
model,
|
|
1291
|
+
text
|
|
1292
|
+
});
|
|
1293
|
+
});
|
|
1294
|
+
return found;
|
|
1295
|
+
}
|
|
1296
|
+
__name(findFkPointers, "findFkPointers");
|
|
1297
|
+
function relationNameOf(relationAttribute) {
|
|
1298
|
+
const match = /@relation\(\s*"([^"]+)"/.exec(relationAttribute);
|
|
1299
|
+
return match ? match[1] : null;
|
|
1300
|
+
}
|
|
1301
|
+
__name(relationNameOf, "relationNameOf");
|
|
1302
|
+
function escapeForRegExp(value) {
|
|
1303
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1304
|
+
}
|
|
1305
|
+
__name(escapeForRegExp, "escapeForRegExp");
|
|
1306
|
+
function modelBody(schema2, model) {
|
|
1307
|
+
const name = escapeForRegExp(model);
|
|
1308
|
+
const block = new RegExp(`(^|\\n)model\\s+${name}\\s*\\{([\\s\\S]*?)\\n\\}`, "m").exec(schema2);
|
|
1309
|
+
return block ? block[2] : null;
|
|
1310
|
+
}
|
|
1311
|
+
__name(modelBody, "modelBody");
|
|
1312
|
+
function isOneToOne(schema2, model, foreignKey) {
|
|
1313
|
+
const body = modelBody(schema2, model);
|
|
1314
|
+
if (!body) return false;
|
|
1315
|
+
return new RegExp(`^\\s*${escapeForRegExp(foreignKey)}\\s+\\S+.*@unique`, "m").test(body);
|
|
1316
|
+
}
|
|
1317
|
+
__name(isOneToOne, "isOneToOne");
|
|
1318
|
+
function hasBackRelation(schema2, model, owner, relationName, singular = false) {
|
|
1319
|
+
const body = modelBody(schema2, owner);
|
|
1320
|
+
if (body === null) return false;
|
|
1321
|
+
const name = escapeForRegExp(model);
|
|
1322
|
+
const shape = singular ? `${name}\\??` : `${name}\\[\\]`;
|
|
1323
|
+
const candidates = [
|
|
1324
|
+
...body.matchAll(new RegExp(`^\\s*\\w+\\s+${shape}(\\s.*)?$`, "gm"))
|
|
1325
|
+
];
|
|
1326
|
+
return candidates.some((line) => relationNameOf(line[0]) === relationName);
|
|
1327
|
+
}
|
|
1328
|
+
__name(hasBackRelation, "hasBackRelation");
|
|
1329
|
+
function enableFkPointers(schema2, models) {
|
|
1330
|
+
const lines = schema2.split("\n");
|
|
1331
|
+
const enabled = [];
|
|
1332
|
+
const skipped = [];
|
|
1333
|
+
const needsBackRelation = [];
|
|
1334
|
+
for (const pointer of findFkPointers(schema2)) {
|
|
1335
|
+
const model = pointer.target === "Tenant" ? models.tenant : models.user;
|
|
1336
|
+
if (!model) {
|
|
1337
|
+
skipped.push({
|
|
1338
|
+
line: pointer.line,
|
|
1339
|
+
target: pointer.target
|
|
1340
|
+
});
|
|
1341
|
+
continue;
|
|
1342
|
+
}
|
|
1343
|
+
const match = POINTER.exec(pointer.text);
|
|
1344
|
+
const relationName = relationNameOf(match[7]);
|
|
1345
|
+
const foreignKey = foreignKeyOf(match[7]);
|
|
1346
|
+
const singular = foreignKey !== null && isOneToOne(schema2, pointer.model, foreignKey);
|
|
1347
|
+
if (!hasBackRelation(schema2, pointer.model, model, relationName, singular)) {
|
|
1348
|
+
needsBackRelation.push({
|
|
1349
|
+
line: pointer.line,
|
|
1350
|
+
owner: model,
|
|
1351
|
+
suggestion: backRelationSuggestion(pointer.model, relationName, singular)
|
|
1352
|
+
});
|
|
1353
|
+
continue;
|
|
1354
|
+
}
|
|
1355
|
+
const [, indent, field, gap1, , optional, gap2, relation] = match;
|
|
1356
|
+
lines[pointer.line] = `${indent}${field}${gap1}${model}${optional}${gap2}${relation}`;
|
|
1357
|
+
enabled.push({
|
|
1358
|
+
line: pointer.line,
|
|
1359
|
+
model
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
return {
|
|
1363
|
+
schema: lines.join("\n"),
|
|
1364
|
+
enabled,
|
|
1365
|
+
skipped,
|
|
1366
|
+
needsBackRelation
|
|
1367
|
+
};
|
|
1368
|
+
}
|
|
1369
|
+
__name(enableFkPointers, "enableFkPointers");
|
|
1370
|
+
var lowerFirst = /* @__PURE__ */ __name((value) => value.charAt(0).toLowerCase() + value.slice(1), "lowerFirst");
|
|
1371
|
+
function foreignKeyOf(relationAttribute) {
|
|
1372
|
+
const match = /fields:\s*\[\s*(\w+)/.exec(relationAttribute);
|
|
1373
|
+
return match ? match[1] : null;
|
|
1374
|
+
}
|
|
1375
|
+
__name(foreignKeyOf, "foreignKeyOf");
|
|
1376
|
+
function backRelationSuggestion(model, relationName, singular) {
|
|
1377
|
+
const field = singular ? lowerFirst(model) : `${lowerFirst(model)}s`;
|
|
1378
|
+
const type = singular ? `${model}?` : `${model}[]`;
|
|
1379
|
+
return `${field} ${type}` + (relationName ? ` @relation("${relationName}")` : "");
|
|
1380
|
+
}
|
|
1381
|
+
__name(backRelationSuggestion, "backRelationSuggestion");
|
|
1382
|
+
function assertModelsExist(declaredModels, models) {
|
|
1383
|
+
const missing = Object.entries(models).filter(([, name]) => name && !declaredModels.includes(name)).map(([role, name]) => `--${role}-model=${name}`);
|
|
1384
|
+
if (missing.length === 0) return;
|
|
1385
|
+
throw new Error(`${missing.join(", ")} \u2014 no such model in this schema. It declares: ${declaredModels.slice().sort().join(", ")}.`);
|
|
1386
|
+
}
|
|
1387
|
+
__name(assertModelsExist, "assertModelsExist");
|
|
1388
|
+
|
|
1204
1389
|
// src/schema-check.ts
|
|
1205
1390
|
function renderType(signature) {
|
|
1206
1391
|
return `${signature.type}${signature.list ? "[]" : ""}${signature.optional ? "?" : ""}`;
|
|
@@ -1272,10 +1457,11 @@ function isDocumentedTypeSubstitution(spec, app, appEnums) {
|
|
|
1272
1457
|
return spec.type === "String" && appEnums.has(app.type);
|
|
1273
1458
|
}
|
|
1274
1459
|
__name(isDocumentedTypeSubstitution, "isDocumentedTypeSubstitution");
|
|
1275
|
-
function compareFields(model, specFields, appFields, appEnums, missingFields, fieldMismatches) {
|
|
1460
|
+
function compareFields(model, specFields, appFields, appEnums, notAdopted, missingFields, fieldMismatches) {
|
|
1276
1461
|
for (const [name, spec] of specFields) {
|
|
1277
1462
|
const app = appFields.get(name);
|
|
1278
1463
|
if (!app) {
|
|
1464
|
+
if (notAdopted.has(spec.type)) continue;
|
|
1279
1465
|
missingFields.push({
|
|
1280
1466
|
model,
|
|
1281
1467
|
field: name,
|
|
@@ -1333,34 +1519,62 @@ function breaksContract(attribute) {
|
|
|
1333
1519
|
return attribute.kind !== "index";
|
|
1334
1520
|
}
|
|
1335
1521
|
__name(breaksContract, "breaksContract");
|
|
1336
|
-
function
|
|
1522
|
+
function modelsKeptPastTheTenant(specSchema) {
|
|
1523
|
+
const pointed = new Set(findFkPointers(specSchema).filter((pointer) => pointer.target === "Tenant").map((pointer) => pointer.model));
|
|
1524
|
+
return [
|
|
1525
|
+
...parseSchema(specSchema).models
|
|
1526
|
+
].filter(([model, fields]) => fields.has("tenantId") && !pointed.has(model)).map(([model]) => model);
|
|
1527
|
+
}
|
|
1528
|
+
__name(modelsKeptPastTheTenant, "modelsKeptPastTheTenant");
|
|
1529
|
+
function cascadesFromTenant(model, block) {
|
|
1530
|
+
const found = [];
|
|
1531
|
+
for (const line of block.split("\n")) {
|
|
1532
|
+
const compact = structuralOnly(line).replace(/\s+/g, "");
|
|
1533
|
+
if (compact.includes("@relation(") && compact.includes("fields:[tenantId]") && compact.includes("onDelete:Cascade")) {
|
|
1534
|
+
found.push({
|
|
1535
|
+
model,
|
|
1536
|
+
field: line.trim().split(/\s/)[0]
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
return found;
|
|
1541
|
+
}
|
|
1542
|
+
__name(cascadesFromTenant, "cascadesFromTenant");
|
|
1543
|
+
function checkSchema(specSchema, appSchema, knownModels) {
|
|
1337
1544
|
const spec = parseSchema(specSchema);
|
|
1338
1545
|
const app = parseSchema(appSchema);
|
|
1339
|
-
const absentModels = [];
|
|
1340
1546
|
const missingFields = [];
|
|
1341
1547
|
const fieldMismatches = [];
|
|
1342
1548
|
const missingBlockAttributes = [];
|
|
1549
|
+
const absentModels = [
|
|
1550
|
+
...spec.models.keys()
|
|
1551
|
+
].filter((name) => !app.models.has(name));
|
|
1552
|
+
const absentEnums = [
|
|
1553
|
+
...spec.enums.keys()
|
|
1554
|
+
].filter((name) => !app.enums.has(name));
|
|
1555
|
+
const shipped = knownModels ?? new Set(spec.models.keys());
|
|
1556
|
+
const notAdopted = new Set([
|
|
1557
|
+
...shipped
|
|
1558
|
+
].filter((name) => !app.models.has(name)));
|
|
1343
1559
|
for (const [model, specFields] of spec.models) {
|
|
1344
1560
|
const appFields = app.models.get(model);
|
|
1345
|
-
if (!appFields)
|
|
1346
|
-
|
|
1347
|
-
continue;
|
|
1348
|
-
}
|
|
1349
|
-
compareFields(model, specFields, appFields, app.enums, missingFields, fieldMismatches);
|
|
1561
|
+
if (!appFields) continue;
|
|
1562
|
+
compareFields(model, specFields, appFields, app.enums, notAdopted, missingFields, fieldMismatches);
|
|
1350
1563
|
const specAttrs = spec.modelAttributes.get(model);
|
|
1351
1564
|
const appAttrs = app.modelAttributes.get(model);
|
|
1352
1565
|
if (specAttrs && appAttrs) {
|
|
1353
1566
|
compareBlockAttributes(model, specAttrs, appAttrs, missingBlockAttributes);
|
|
1354
1567
|
}
|
|
1355
1568
|
}
|
|
1356
|
-
const
|
|
1569
|
+
const appBlocks = extractBlocks(appSchema, "model");
|
|
1570
|
+
const tenantCascades = modelsKeptPastTheTenant(specSchema).flatMap((model) => {
|
|
1571
|
+
const block = appBlocks.get(model);
|
|
1572
|
+
return block ? cascadesFromTenant(model, block) : [];
|
|
1573
|
+
});
|
|
1357
1574
|
const missingEnumValues = [];
|
|
1358
1575
|
for (const [name, specValues] of spec.enums) {
|
|
1359
1576
|
const appValues = app.enums.get(name);
|
|
1360
|
-
if (!appValues)
|
|
1361
|
-
absentEnums.push(name);
|
|
1362
|
-
continue;
|
|
1363
|
-
}
|
|
1577
|
+
if (!appValues) continue;
|
|
1364
1578
|
for (const value of specValues) {
|
|
1365
1579
|
if (!appValues.includes(value)) {
|
|
1366
1580
|
missingEnumValues.push({
|
|
@@ -1377,9 +1591,10 @@ function checkSchema(specSchema, appSchema) {
|
|
|
1377
1591
|
missingEnumValues,
|
|
1378
1592
|
fieldMismatches,
|
|
1379
1593
|
missingBlockAttributes,
|
|
1594
|
+
tenantCascades,
|
|
1380
1595
|
checkedModelCount: spec.models.size - absentModels.length,
|
|
1381
1596
|
checkedEnumCount: spec.enums.size - absentEnums.length,
|
|
1382
|
-
ok: missingFields.length === 0 && missingEnumValues.length === 0 && fieldMismatches.length === 0 && !missingBlockAttributes.some(breaksContract)
|
|
1597
|
+
ok: missingFields.length === 0 && missingEnumValues.length === 0 && fieldMismatches.length === 0 && tenantCascades.length === 0 && !missingBlockAttributes.some(breaksContract)
|
|
1383
1598
|
};
|
|
1384
1599
|
}
|
|
1385
1600
|
__name(checkSchema, "checkSchema");
|
|
@@ -1460,117 +1675,6 @@ function reportConstraints(outcome, context) {
|
|
|
1460
1675
|
}
|
|
1461
1676
|
__name(reportConstraints, "reportConstraints");
|
|
1462
1677
|
|
|
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
1678
|
// src/init/catalog-keys.ts
|
|
1575
1679
|
import { planCatalogSchema } from "@saasicat/spec";
|
|
1576
1680
|
var schema = planCatalogSchema;
|
|
@@ -1581,10 +1685,11 @@ function required(value, what) {
|
|
|
1581
1685
|
return value;
|
|
1582
1686
|
}
|
|
1583
1687
|
__name(required, "required");
|
|
1584
|
-
|
|
1585
|
-
|
|
1688
|
+
var APP_KEY_PATTERN = /^[a-z][a-z0-9-]{1,30}$/;
|
|
1689
|
+
function appKeyPattern() {
|
|
1690
|
+
return APP_KEY_PATTERN;
|
|
1586
1691
|
}
|
|
1587
|
-
__name(
|
|
1692
|
+
__name(appKeyPattern, "appKeyPattern");
|
|
1588
1693
|
function quotaKeyPattern() {
|
|
1589
1694
|
const patterns = Object.keys(required(schema.$defs?.PlanDef?.properties?.quotas?.patternProperties, "quota key pattern"));
|
|
1590
1695
|
if (patterns.length !== 1) {
|
|
@@ -1597,12 +1702,12 @@ function minimumQuotasPerPlan() {
|
|
|
1597
1702
|
return required(schema.$defs?.PlanDef?.properties?.quotas?.minProperties, "minProperties");
|
|
1598
1703
|
}
|
|
1599
1704
|
__name(minimumQuotasPerPlan, "minimumQuotasPerPlan");
|
|
1600
|
-
function
|
|
1601
|
-
const pattern =
|
|
1602
|
-
if (pattern.test(
|
|
1603
|
-
throw new Error(`--
|
|
1705
|
+
function assertValidAppKey(appKey) {
|
|
1706
|
+
const pattern = appKeyPattern();
|
|
1707
|
+
if (pattern.test(appKey)) return;
|
|
1708
|
+
throw new Error(`--app-key=${appKey} is not a valid app key. It has to match ${pattern.source} \u2014 lower case, starting with a letter, at least two characters. It becomes an npm package name and a browser storage prefix, so this would fail after every file was written.`);
|
|
1604
1709
|
}
|
|
1605
|
-
__name(
|
|
1710
|
+
__name(assertValidAppKey, "assertValidAppKey");
|
|
1606
1711
|
function assertValidQuotaKey(quotaKey) {
|
|
1607
1712
|
const pattern = quotaKeyPattern();
|
|
1608
1713
|
if (pattern.test(quotaKey)) return;
|
|
@@ -1634,24 +1739,24 @@ function pascalCase(value) {
|
|
|
1634
1739
|
__name(pascalCase, "pascalCase");
|
|
1635
1740
|
var quotaFileName = /* @__PURE__ */ __name((key) => `${key.replace(/[^A-Za-z0-9]+/g, "-").toLowerCase()}-quota.provider.ts`, "quotaFileName");
|
|
1636
1741
|
function planInit(options) {
|
|
1637
|
-
const
|
|
1638
|
-
if (!
|
|
1639
|
-
|
|
1640
|
-
const appLabel = options.appName ?? pascalCase(
|
|
1742
|
+
const appKey = options.appKey;
|
|
1743
|
+
if (!appKey) throw new Error("init needs an --app-key.");
|
|
1744
|
+
assertValidAppKey(appKey);
|
|
1745
|
+
const appLabel = options.appName ?? pascalCase(appKey);
|
|
1641
1746
|
const appName = pascalCase(appLabel);
|
|
1642
1747
|
const apiBase = options.apiBase ?? "/api/v1/admin";
|
|
1643
1748
|
const quotas = (options.quotas ?? []).map(parseQuota);
|
|
1644
1749
|
assertEnoughQuotas(quotas);
|
|
1645
1750
|
const hasherClass = options.skipHasher ? null : `${appName}PasswordHasher`;
|
|
1646
|
-
const featureKey = `${
|
|
1751
|
+
const featureKey = `${appKey.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase()}_CORE`;
|
|
1647
1752
|
const shared = {
|
|
1648
|
-
|
|
1753
|
+
APP_KEY: appKey,
|
|
1649
1754
|
APP_NAME: appName,
|
|
1650
1755
|
APP_LABEL: appLabel,
|
|
1651
1756
|
API_BASE: apiBase,
|
|
1652
1757
|
FEATURE_KEY: featureKey,
|
|
1653
|
-
REGISTRY_CONST: `${constantCase(
|
|
1654
|
-
MANIFEST_CONST: `${constantCase(
|
|
1758
|
+
REGISTRY_CONST: `${constantCase(appKey)}_FEATURE_UI_REGISTRY`,
|
|
1759
|
+
MANIFEST_CONST: `${constantCase(appKey)}_MANIFEST_CONTRIBUTION`,
|
|
1655
1760
|
ADMIN_MODULE_CLASS: `${appName}AdminModule`,
|
|
1656
1761
|
HASHER_CLASS: hasherClass ?? "",
|
|
1657
1762
|
HASHER_FILE: hasherClass ? `${kebabCase(appName)}-password.hasher` : "",
|
|
@@ -1782,6 +1887,31 @@ function readEffectiveModuleResolution(root, ts) {
|
|
|
1782
1887
|
}
|
|
1783
1888
|
__name(readEffectiveModuleResolution, "readEffectiveModuleResolution");
|
|
1784
1889
|
|
|
1890
|
+
// src/init/settings-written.ts
|
|
1891
|
+
import { loadPlanCatalogFromString } from "@saasicat/nest/billing";
|
|
1892
|
+
function flatten(prefix, value, into) {
|
|
1893
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
1894
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1895
|
+
flatten(`${prefix}.${key}`, child, into);
|
|
1896
|
+
}
|
|
1897
|
+
return;
|
|
1898
|
+
}
|
|
1899
|
+
into.push({
|
|
1900
|
+
key: prefix,
|
|
1901
|
+
value: JSON.stringify(value)
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
__name(flatten, "flatten");
|
|
1905
|
+
function settingsWrittenTo(catalogYaml, source = "config/saas.yaml") {
|
|
1906
|
+
const catalog = loadPlanCatalogFromString(catalogYaml, {
|
|
1907
|
+
source
|
|
1908
|
+
});
|
|
1909
|
+
const settings = [];
|
|
1910
|
+
flatten("tenantBilling", catalog.tenantBilling, settings);
|
|
1911
|
+
return settings;
|
|
1912
|
+
}
|
|
1913
|
+
__name(settingsWrittenTo, "settingsWrittenTo");
|
|
1914
|
+
|
|
1785
1915
|
// src/codemods/v1-imports.ts
|
|
1786
1916
|
var PUBLIC_PREFIXES = [
|
|
1787
1917
|
"ui/",
|
|
@@ -2045,6 +2175,327 @@ function rewriteManifest(text, table, options) {
|
|
|
2045
2175
|
}
|
|
2046
2176
|
__name(rewriteManifest, "rewriteManifest");
|
|
2047
2177
|
|
|
2178
|
+
// src/codemods/v1-project-key.ts
|
|
2179
|
+
function stripQueryParameter(text) {
|
|
2180
|
+
const NEEDLE = "projectKey=";
|
|
2181
|
+
let out = "";
|
|
2182
|
+
let index = 0;
|
|
2183
|
+
const taken = /* @__PURE__ */ new Set();
|
|
2184
|
+
for (; ; ) {
|
|
2185
|
+
const at = text.indexOf(NEEDLE, index);
|
|
2186
|
+
if (at < 0) break;
|
|
2187
|
+
const separator = at === 0 ? "" : text[at - 1];
|
|
2188
|
+
const value = separator === "?" || separator === "&" ? simpleValueEnd(text, at + NEEDLE.length) : null;
|
|
2189
|
+
if (value === null || !servesTheCatalogue(text, at)) {
|
|
2190
|
+
out += text.slice(index, at + NEEDLE.length);
|
|
2191
|
+
index = at + NEEDLE.length;
|
|
2192
|
+
continue;
|
|
2193
|
+
}
|
|
2194
|
+
out += text.slice(index, at - 1);
|
|
2195
|
+
if (separator === "?" && text[value] === "&") {
|
|
2196
|
+
out += "?";
|
|
2197
|
+
index = value + 1;
|
|
2198
|
+
} else {
|
|
2199
|
+
index = value;
|
|
2200
|
+
}
|
|
2201
|
+
taken.add(at);
|
|
2202
|
+
}
|
|
2203
|
+
return {
|
|
2204
|
+
text: out + text.slice(index),
|
|
2205
|
+
taken
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
__name(stripQueryParameter, "stripQueryParameter");
|
|
2209
|
+
var HIDES_A_BRACE = /[`'"{}/\\]/;
|
|
2210
|
+
function simpleValueEnd(text, from) {
|
|
2211
|
+
let i = from;
|
|
2212
|
+
while (i < text.length) {
|
|
2213
|
+
const ch = text[i];
|
|
2214
|
+
if (ch === "&" || ch === "#" || ch === "'" || ch === '"' || ch === "`" || ch === "\n" || ch === " ") {
|
|
2215
|
+
return i;
|
|
2216
|
+
}
|
|
2217
|
+
if (ch !== "$" || text[i + 1] !== "{") {
|
|
2218
|
+
if (ch === "{" || ch === "}") return null;
|
|
2219
|
+
i += 1;
|
|
2220
|
+
continue;
|
|
2221
|
+
}
|
|
2222
|
+
const close = text.indexOf("}", i + 2);
|
|
2223
|
+
if (close < 0) return null;
|
|
2224
|
+
if (HIDES_A_BRACE.test(text.slice(i + 2, close))) return null;
|
|
2225
|
+
i = close + 1;
|
|
2226
|
+
}
|
|
2227
|
+
return null;
|
|
2228
|
+
}
|
|
2229
|
+
__name(simpleValueEnd, "simpleValueEnd");
|
|
2230
|
+
function servesTheCatalogue(text, at) {
|
|
2231
|
+
let start = at;
|
|
2232
|
+
while (start > 0) {
|
|
2233
|
+
const ch = text[start - 1];
|
|
2234
|
+
if (ch === "`" || ch === "'" || ch === '"' || ch === "\n") break;
|
|
2235
|
+
start -= 1;
|
|
2236
|
+
}
|
|
2237
|
+
const url = text.slice(start, at);
|
|
2238
|
+
const query = url.indexOf("?");
|
|
2239
|
+
return (query < 0 ? url : url.slice(0, query)).includes("/catalog/");
|
|
2240
|
+
}
|
|
2241
|
+
__name(servesTheCatalogue, "servesTheCatalogue");
|
|
2242
|
+
function isIdentifierChar(ch) {
|
|
2243
|
+
return ch !== void 0 && /[A-Za-z0-9_$]/.test(ch);
|
|
2244
|
+
}
|
|
2245
|
+
__name(isIdentifierChar, "isIdentifierChar");
|
|
2246
|
+
function lineAt(text, at) {
|
|
2247
|
+
let line = 1;
|
|
2248
|
+
for (let i = 0; i < at; i += 1) if (text[i] === "\n") line += 1;
|
|
2249
|
+
return line;
|
|
2250
|
+
}
|
|
2251
|
+
__name(lineAt, "lineAt");
|
|
2252
|
+
function removeProjectKey(text, kind = "source") {
|
|
2253
|
+
if (kind === "yaml") return removeFromYaml(text);
|
|
2254
|
+
const query = stripQueryParameter(text);
|
|
2255
|
+
const reported = /* @__PURE__ */ new Set();
|
|
2256
|
+
for (let at = text.indexOf("projectKey"); at >= 0; at = text.indexOf("projectKey", at + 1)) {
|
|
2257
|
+
if (query.taken.has(at)) continue;
|
|
2258
|
+
if (isIdentifierChar(text[at - 1])) continue;
|
|
2259
|
+
if (isIdentifierChar(text[at + "projectKey".length])) continue;
|
|
2260
|
+
reported.add(lineAt(text, at));
|
|
2261
|
+
}
|
|
2262
|
+
return {
|
|
2263
|
+
text: query.text,
|
|
2264
|
+
rewritten: query.taken.size,
|
|
2265
|
+
undecided: [
|
|
2266
|
+
...reported
|
|
2267
|
+
].sort((a, b) => a - b)
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
__name(removeProjectKey, "removeProjectKey");
|
|
2271
|
+
function removeFromYaml(text) {
|
|
2272
|
+
const lines = text.split("\n");
|
|
2273
|
+
const kept = [];
|
|
2274
|
+
let rewritten = 0;
|
|
2275
|
+
for (const line of lines) {
|
|
2276
|
+
if (line.startsWith("projectKey:")) {
|
|
2277
|
+
rewritten += 1;
|
|
2278
|
+
continue;
|
|
2279
|
+
}
|
|
2280
|
+
kept.push(line);
|
|
2281
|
+
}
|
|
2282
|
+
return {
|
|
2283
|
+
text: kept.join("\n"),
|
|
2284
|
+
rewritten,
|
|
2285
|
+
undecided: []
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
__name(removeFromYaml, "removeFromYaml");
|
|
2289
|
+
|
|
2290
|
+
// src/codemods/v1-moved-settings.ts
|
|
2291
|
+
import { planCatalogSchema as planCatalogSchema2 } from "@saasicat/spec";
|
|
2292
|
+
var SETTINGS_THAT_MOVED = Object.keys(planCatalogSchema2.properties?.tenantBilling?.properties ?? (() => {
|
|
2293
|
+
throw new Error("plan-catalog.schema.json declares no tenantBilling properties \u2014 @saasicat/spec and @saasicat/cli are out of step.");
|
|
2294
|
+
})());
|
|
2295
|
+
var isIdentifierChar2 = /* @__PURE__ */ __name((ch) => ch !== void 0 && /[A-Za-z0-9_$]/.test(ch), "isIdentifierChar");
|
|
2296
|
+
function lineAt2(text, index) {
|
|
2297
|
+
let line = 1;
|
|
2298
|
+
for (let at = 0; at < index; at += 1) {
|
|
2299
|
+
if (text[at] === "\n") line += 1;
|
|
2300
|
+
}
|
|
2301
|
+
return line;
|
|
2302
|
+
}
|
|
2303
|
+
__name(lineAt2, "lineAt");
|
|
2304
|
+
var SCANNED_FOR_MOVED_SETTINGS = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|vue)$/;
|
|
2305
|
+
function findMovedSettings(text) {
|
|
2306
|
+
const occurrences = [];
|
|
2307
|
+
for (const setting of SETTINGS_THAT_MOVED) {
|
|
2308
|
+
for (let at = text.indexOf(setting); at >= 0; at = text.indexOf(setting, at + 1)) {
|
|
2309
|
+
if (isIdentifierChar2(text[at - 1])) continue;
|
|
2310
|
+
if (isIdentifierChar2(text[at + setting.length])) continue;
|
|
2311
|
+
occurrences.push({
|
|
2312
|
+
setting,
|
|
2313
|
+
line: lineAt2(text, at)
|
|
2314
|
+
});
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
return {
|
|
2318
|
+
occurrences: occurrences.sort((a, b) => a.line - b.line || a.setting.localeCompare(b.setting))
|
|
2319
|
+
};
|
|
2320
|
+
}
|
|
2321
|
+
__name(findMovedSettings, "findMovedSettings");
|
|
2322
|
+
var WHERE_IT_GOES = {
|
|
2323
|
+
cancellationNoticeDays: "config/saas.yaml \u2192 tenantBilling.cancellationNoticeDays \u2014 both `monthly` and `yearly` are required, so write the number you are running today rather than leaving one out.",
|
|
2324
|
+
selfServiceBlockedPlans: 'config/saas.yaml \u2192 tenantBilling.selfServiceBlockedPlans \u2014 both `asTarget` and `asSource` are required, and `[]` is the way to say "nothing is blocked".'
|
|
2325
|
+
};
|
|
2326
|
+
|
|
2327
|
+
// src/codemods/v1-db-catalog.ts
|
|
2328
|
+
import { DB_CATALOG_MEMBERS } from "@saasicat/nest/platform";
|
|
2329
|
+
var SCANNED_FOR_DB_CATALOG = SCANNED_FOR_MOVED_SETTINGS;
|
|
2330
|
+
var PROPERTY = "dbCatalog";
|
|
2331
|
+
var TAKEN = new Set(DB_CATALOG_MEMBERS);
|
|
2332
|
+
var isIdentifierStart = /* @__PURE__ */ __name((ch) => ch !== void 0 && /[A-Za-z_$]/.test(ch), "isIdentifierStart");
|
|
2333
|
+
var isIdentifierChar3 = /* @__PURE__ */ __name((ch) => ch !== void 0 && /[A-Za-z0-9_$]/.test(ch), "isIdentifierChar");
|
|
2334
|
+
var isBlank = /* @__PURE__ */ __name((ch) => ch === " " || ch === " " || ch === "\n" || ch === "\r", "isBlank");
|
|
2335
|
+
function lineAt3(text, index) {
|
|
2336
|
+
let line = 1;
|
|
2337
|
+
for (let at = 0; at < index; at += 1) {
|
|
2338
|
+
if (text[at] === "\n") line += 1;
|
|
2339
|
+
}
|
|
2340
|
+
return line;
|
|
2341
|
+
}
|
|
2342
|
+
__name(lineAt3, "lineAt");
|
|
2343
|
+
function skipBlanks(text, from) {
|
|
2344
|
+
let at = from;
|
|
2345
|
+
while (isBlank(text[at])) at += 1;
|
|
2346
|
+
return at;
|
|
2347
|
+
}
|
|
2348
|
+
__name(skipBlanks, "skipBlanks");
|
|
2349
|
+
function closingQuote(text, open) {
|
|
2350
|
+
const quote = text[open];
|
|
2351
|
+
for (let at = open + 1; at < text.length; at += 1) {
|
|
2352
|
+
if (text[at] === "\\") {
|
|
2353
|
+
at += 1;
|
|
2354
|
+
continue;
|
|
2355
|
+
}
|
|
2356
|
+
if (text[at] === quote) return at;
|
|
2357
|
+
}
|
|
2358
|
+
return text.length;
|
|
2359
|
+
}
|
|
2360
|
+
__name(closingQuote, "closingQuote");
|
|
2361
|
+
function skipCommentOrString(text, at) {
|
|
2362
|
+
const ch = text[at];
|
|
2363
|
+
if (ch === "/" && text[at + 1] === "/") {
|
|
2364
|
+
const end = text.indexOf("\n", at);
|
|
2365
|
+
return end === -1 ? text.length : end;
|
|
2366
|
+
}
|
|
2367
|
+
if (ch === "/" && text[at + 1] === "*") {
|
|
2368
|
+
const end = text.indexOf("*/", at + 2);
|
|
2369
|
+
return end === -1 ? text.length : end + 2;
|
|
2370
|
+
}
|
|
2371
|
+
if (ch === "'" || ch === '"' || ch === "`") return closingQuote(text, at) + 1;
|
|
2372
|
+
return at;
|
|
2373
|
+
}
|
|
2374
|
+
__name(skipCommentOrString, "skipCommentOrString");
|
|
2375
|
+
function objectLiteralAt(text, open) {
|
|
2376
|
+
const members = [];
|
|
2377
|
+
let depth = 0;
|
|
2378
|
+
let expectingKey = true;
|
|
2379
|
+
let at = open + 1;
|
|
2380
|
+
while (at < text.length) {
|
|
2381
|
+
const ch = text[at];
|
|
2382
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
2383
|
+
const end = closingQuote(text, at);
|
|
2384
|
+
if (depth === 0 && expectingKey && text[skipBlanks(text, end + 1)] === ":") {
|
|
2385
|
+
members.push(text.slice(at + 1, end));
|
|
2386
|
+
}
|
|
2387
|
+
at = end + 1;
|
|
2388
|
+
continue;
|
|
2389
|
+
}
|
|
2390
|
+
const skipped = skipCommentOrString(text, at);
|
|
2391
|
+
if (skipped > at) {
|
|
2392
|
+
at = skipped;
|
|
2393
|
+
continue;
|
|
2394
|
+
}
|
|
2395
|
+
if (ch === "{" || ch === "[" || ch === "(") {
|
|
2396
|
+
depth += 1;
|
|
2397
|
+
at += 1;
|
|
2398
|
+
continue;
|
|
2399
|
+
}
|
|
2400
|
+
if (ch === "}" || ch === "]" || ch === ")") {
|
|
2401
|
+
if (depth === 0) return {
|
|
2402
|
+
members,
|
|
2403
|
+
end: at + 1
|
|
2404
|
+
};
|
|
2405
|
+
depth -= 1;
|
|
2406
|
+
at += 1;
|
|
2407
|
+
continue;
|
|
2408
|
+
}
|
|
2409
|
+
if (depth === 0 && ch === ":") expectingKey = false;
|
|
2410
|
+
if (depth === 0 && ch === ",") expectingKey = true;
|
|
2411
|
+
if (depth === 0 && expectingKey && ch === "." && text.startsWith("...", at)) {
|
|
2412
|
+
let end = at + 3;
|
|
2413
|
+
while (isIdentifierChar3(text[end])) end += 1;
|
|
2414
|
+
members.push(text.slice(at, end));
|
|
2415
|
+
at = end;
|
|
2416
|
+
continue;
|
|
2417
|
+
}
|
|
2418
|
+
if (depth === 0 && expectingKey && isIdentifierStart(ch)) {
|
|
2419
|
+
let end = at;
|
|
2420
|
+
while (isIdentifierChar3(text[end])) end += 1;
|
|
2421
|
+
const next = text[skipBlanks(text, end)];
|
|
2422
|
+
if (next === ":" || next === "," || next === "}") members.push(text.slice(at, end));
|
|
2423
|
+
at = end;
|
|
2424
|
+
continue;
|
|
2425
|
+
}
|
|
2426
|
+
at += 1;
|
|
2427
|
+
}
|
|
2428
|
+
return {
|
|
2429
|
+
members,
|
|
2430
|
+
end: text.length
|
|
2431
|
+
};
|
|
2432
|
+
}
|
|
2433
|
+
__name(objectLiteralAt, "objectLiteralAt");
|
|
2434
|
+
function findDbCatalogBlocks(text) {
|
|
2435
|
+
const occurrences = [];
|
|
2436
|
+
let at = 0;
|
|
2437
|
+
while (at < text.length) {
|
|
2438
|
+
const skipped = skipCommentOrString(text, at);
|
|
2439
|
+
if (skipped > at) {
|
|
2440
|
+
at = skipped;
|
|
2441
|
+
continue;
|
|
2442
|
+
}
|
|
2443
|
+
if (!text.startsWith(PROPERTY, at) || isIdentifierChar3(text[at - 1])) {
|
|
2444
|
+
at += 1;
|
|
2445
|
+
continue;
|
|
2446
|
+
}
|
|
2447
|
+
const afterName = at + PROPERTY.length;
|
|
2448
|
+
if (isIdentifierChar3(text[afterName])) {
|
|
2449
|
+
at = afterName;
|
|
2450
|
+
continue;
|
|
2451
|
+
}
|
|
2452
|
+
const colon = skipBlanks(text, afterName);
|
|
2453
|
+
if (text[colon] !== ":") {
|
|
2454
|
+
at = afterName;
|
|
2455
|
+
continue;
|
|
2456
|
+
}
|
|
2457
|
+
const value = skipBlanks(text, colon + 1);
|
|
2458
|
+
const line = lineAt3(text, at);
|
|
2459
|
+
if (text[value] !== "{") {
|
|
2460
|
+
occurrences.push({
|
|
2461
|
+
line,
|
|
2462
|
+
shape: "reference",
|
|
2463
|
+
leftovers: []
|
|
2464
|
+
});
|
|
2465
|
+
at = value;
|
|
2466
|
+
continue;
|
|
2467
|
+
}
|
|
2468
|
+
const { members, end } = objectLiteralAt(text, value);
|
|
2469
|
+
const leftovers = members.filter((member) => !TAKEN.has(member));
|
|
2470
|
+
if (!members.includes("path")) {
|
|
2471
|
+
occurrences.push({
|
|
2472
|
+
line,
|
|
2473
|
+
shape: "values",
|
|
2474
|
+
leftovers
|
|
2475
|
+
});
|
|
2476
|
+
} else if (leftovers.length > 0) {
|
|
2477
|
+
occurrences.push({
|
|
2478
|
+
line,
|
|
2479
|
+
shape: "mixed",
|
|
2480
|
+
leftovers
|
|
2481
|
+
});
|
|
2482
|
+
}
|
|
2483
|
+
at = end;
|
|
2484
|
+
}
|
|
2485
|
+
return {
|
|
2486
|
+
occurrences
|
|
2487
|
+
};
|
|
2488
|
+
}
|
|
2489
|
+
__name(findDbCatalogBlocks, "findDbCatalogBlocks");
|
|
2490
|
+
function describeDbCatalogOccurrence(occurrence) {
|
|
2491
|
+
const { shape, leftovers } = occurrence;
|
|
2492
|
+
if (shape === "reference") return "carries something this cannot see into";
|
|
2493
|
+
if (shape === "mixed") return `names the path but still carries ${leftovers.join(", ")}`;
|
|
2494
|
+
return leftovers.length > 0 ? `carries the values: ${leftovers.join(", ")}` : "names no path";
|
|
2495
|
+
}
|
|
2496
|
+
__name(describeDbCatalogOccurrence, "describeDbCatalogOccurrence");
|
|
2497
|
+
var WHERE_DB_CATALOG_GOES = "dbCatalog: { path: 'config/saas.yaml' } \u2014 the file the values were forwarded from. Delete the values; the platform reads app, currency, vatRate, tenantBilling, marketing and notifications from the file it names, and the plans from the read sink as before.";
|
|
2498
|
+
|
|
2048
2499
|
// src/init/patch-app-module.ts
|
|
2049
2500
|
var MARKER = "SaaSiCatModule.forRoot";
|
|
2050
2501
|
function patchAppModule(source, options) {
|
|
@@ -2144,7 +2595,7 @@ function renderForRootBlock(options) {
|
|
|
2144
2595
|
"SaaSiCatModule.forRoot(",
|
|
2145
2596
|
" defineSaaSiCat({",
|
|
2146
2597
|
" // Plans straight from the YAML. Apps that manage plans in the",
|
|
2147
|
-
" // SuperAdmin UI pass `dbCatalog` instead.",
|
|
2598
|
+
" // SuperAdmin UI pass `dbCatalog: { path: 'config/saas.yaml' }` instead.",
|
|
2148
2599
|
" planCatalog: loadPlanCatalogFromFile({ path: 'config/saas.yaml' }),",
|
|
2149
2600
|
" // Your authentication guard. This does NOT compile until you",
|
|
2150
2601
|
" // name one, and that is deliberate: an empty array is how the",
|
|
@@ -3248,6 +3699,7 @@ export {
|
|
|
3248
3699
|
DiscoverySnapshotDoctorCheck,
|
|
3249
3700
|
DoctorCommands,
|
|
3250
3701
|
DoctorFlow,
|
|
3702
|
+
IssuerIdentityDoctorCheck,
|
|
3251
3703
|
LIMIT_FILTER_IMPORTS,
|
|
3252
3704
|
LIMIT_FILTER_PROVIDER,
|
|
3253
3705
|
MANIFEST_ACCESS_PORT_TOKEN,
|
|
@@ -3261,17 +3713,23 @@ export {
|
|
|
3261
3713
|
MfaSetupFlow,
|
|
3262
3714
|
PLATFORM_DOCTOR_CHECK_PROVIDERS,
|
|
3263
3715
|
PlanCatalogDoctorCheck,
|
|
3716
|
+
SCANNED_FOR_DB_CATALOG,
|
|
3717
|
+
SCANNED_FOR_MOVED_SETTINGS,
|
|
3718
|
+
SETTINGS_THAT_MOVED,
|
|
3264
3719
|
UI_VUE_SPECIFIER,
|
|
3265
3720
|
USER_MANAGEMENT_PORT_TOKEN,
|
|
3266
3721
|
USER_PORT_TOKEN,
|
|
3267
3722
|
UserCommands,
|
|
3268
3723
|
UserPortDoctorCheck,
|
|
3724
|
+
WHERE_DB_CATALOG_GOES,
|
|
3725
|
+
WHERE_IT_GOES,
|
|
3269
3726
|
WhoAmIFlow,
|
|
3727
|
+
appKeyPattern,
|
|
3270
3728
|
appendConstraints,
|
|
3271
3729
|
applyFragmentBlocks,
|
|
3272
3730
|
applyTokens,
|
|
3273
3731
|
assertModelsExist,
|
|
3274
|
-
|
|
3732
|
+
assertValidAppKey,
|
|
3275
3733
|
assertValidQuotaKey,
|
|
3276
3734
|
blankStringLiterals,
|
|
3277
3735
|
blockBodyLines,
|
|
@@ -3279,6 +3737,7 @@ export {
|
|
|
3279
3737
|
buildImportMap,
|
|
3280
3738
|
checkSchema,
|
|
3281
3739
|
constraintsFor,
|
|
3740
|
+
describeDbCatalogOccurrence,
|
|
3282
3741
|
enableFkPointers,
|
|
3283
3742
|
extractBlockNames,
|
|
3284
3743
|
extractBlocks,
|
|
@@ -3287,7 +3746,9 @@ export {
|
|
|
3287
3746
|
extractFragmentBlocks,
|
|
3288
3747
|
extractModelBlocks,
|
|
3289
3748
|
extractModelNames,
|
|
3749
|
+
findDbCatalogBlocks,
|
|
3290
3750
|
findFkPointers,
|
|
3751
|
+
findMovedSettings,
|
|
3291
3752
|
foreignKeyOf,
|
|
3292
3753
|
hasBackRelation,
|
|
3293
3754
|
hasConstraints,
|
|
@@ -3297,6 +3758,7 @@ export {
|
|
|
3297
3758
|
kebabCase,
|
|
3298
3759
|
migrationCreatedBy,
|
|
3299
3760
|
minimumQuotasPerPlan,
|
|
3761
|
+
modelsKeptPastTheTenant,
|
|
3300
3762
|
namedImports,
|
|
3301
3763
|
parseBlockAttributes,
|
|
3302
3764
|
parseEnumValues,
|
|
@@ -3307,15 +3769,16 @@ export {
|
|
|
3307
3769
|
patchAppModule,
|
|
3308
3770
|
patchOptionsFor,
|
|
3309
3771
|
planInit,
|
|
3310
|
-
projectKeyPattern,
|
|
3311
3772
|
quotaKeyPattern,
|
|
3312
3773
|
readEffectiveModuleResolution,
|
|
3313
3774
|
relationNameOf,
|
|
3775
|
+
removeProjectKey,
|
|
3314
3776
|
reportConstraints,
|
|
3315
3777
|
rewriteImports,
|
|
3316
3778
|
rewriteManifest,
|
|
3317
3779
|
rewriteNames,
|
|
3318
3780
|
rewriteSubpath,
|
|
3781
|
+
settingsWrittenTo,
|
|
3319
3782
|
stripLineComment,
|
|
3320
3783
|
structuralOnly,
|
|
3321
3784
|
tablesAddressedBy
|