@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.cjs
CHANGED
|
@@ -51,6 +51,7 @@ __export(index_exports, {
|
|
|
51
51
|
DiscoverySnapshotDoctorCheck: () => DiscoverySnapshotDoctorCheck,
|
|
52
52
|
DoctorCommands: () => DoctorCommands,
|
|
53
53
|
DoctorFlow: () => DoctorFlow,
|
|
54
|
+
IssuerIdentityDoctorCheck: () => IssuerIdentityDoctorCheck,
|
|
54
55
|
LIMIT_FILTER_IMPORTS: () => LIMIT_FILTER_IMPORTS,
|
|
55
56
|
LIMIT_FILTER_PROVIDER: () => LIMIT_FILTER_PROVIDER,
|
|
56
57
|
MANIFEST_ACCESS_PORT_TOKEN: () => MANIFEST_ACCESS_PORT_TOKEN,
|
|
@@ -64,17 +65,23 @@ __export(index_exports, {
|
|
|
64
65
|
MfaSetupFlow: () => MfaSetupFlow,
|
|
65
66
|
PLATFORM_DOCTOR_CHECK_PROVIDERS: () => PLATFORM_DOCTOR_CHECK_PROVIDERS,
|
|
66
67
|
PlanCatalogDoctorCheck: () => PlanCatalogDoctorCheck,
|
|
68
|
+
SCANNED_FOR_DB_CATALOG: () => SCANNED_FOR_DB_CATALOG,
|
|
69
|
+
SCANNED_FOR_MOVED_SETTINGS: () => SCANNED_FOR_MOVED_SETTINGS,
|
|
70
|
+
SETTINGS_THAT_MOVED: () => SETTINGS_THAT_MOVED,
|
|
67
71
|
UI_VUE_SPECIFIER: () => UI_VUE_SPECIFIER,
|
|
68
72
|
USER_MANAGEMENT_PORT_TOKEN: () => USER_MANAGEMENT_PORT_TOKEN,
|
|
69
73
|
USER_PORT_TOKEN: () => USER_PORT_TOKEN,
|
|
70
74
|
UserCommands: () => UserCommands,
|
|
71
75
|
UserPortDoctorCheck: () => UserPortDoctorCheck,
|
|
76
|
+
WHERE_DB_CATALOG_GOES: () => WHERE_DB_CATALOG_GOES,
|
|
77
|
+
WHERE_IT_GOES: () => WHERE_IT_GOES,
|
|
72
78
|
WhoAmIFlow: () => WhoAmIFlow,
|
|
79
|
+
appKeyPattern: () => appKeyPattern,
|
|
73
80
|
appendConstraints: () => appendConstraints,
|
|
74
81
|
applyFragmentBlocks: () => applyFragmentBlocks,
|
|
75
82
|
applyTokens: () => applyTokens,
|
|
76
83
|
assertModelsExist: () => assertModelsExist,
|
|
77
|
-
|
|
84
|
+
assertValidAppKey: () => assertValidAppKey,
|
|
78
85
|
assertValidQuotaKey: () => assertValidQuotaKey,
|
|
79
86
|
blankStringLiterals: () => blankStringLiterals,
|
|
80
87
|
blockBodyLines: () => blockBodyLines,
|
|
@@ -82,6 +89,7 @@ __export(index_exports, {
|
|
|
82
89
|
buildImportMap: () => buildImportMap,
|
|
83
90
|
checkSchema: () => checkSchema,
|
|
84
91
|
constraintsFor: () => constraintsFor,
|
|
92
|
+
describeDbCatalogOccurrence: () => describeDbCatalogOccurrence,
|
|
85
93
|
enableFkPointers: () => enableFkPointers,
|
|
86
94
|
extractBlockNames: () => extractBlockNames,
|
|
87
95
|
extractBlocks: () => extractBlocks,
|
|
@@ -90,7 +98,9 @@ __export(index_exports, {
|
|
|
90
98
|
extractFragmentBlocks: () => extractFragmentBlocks,
|
|
91
99
|
extractModelBlocks: () => extractModelBlocks,
|
|
92
100
|
extractModelNames: () => extractModelNames,
|
|
101
|
+
findDbCatalogBlocks: () => findDbCatalogBlocks,
|
|
93
102
|
findFkPointers: () => findFkPointers,
|
|
103
|
+
findMovedSettings: () => findMovedSettings,
|
|
94
104
|
foreignKeyOf: () => foreignKeyOf,
|
|
95
105
|
hasBackRelation: () => hasBackRelation,
|
|
96
106
|
hasConstraints: () => hasConstraints,
|
|
@@ -100,6 +110,7 @@ __export(index_exports, {
|
|
|
100
110
|
kebabCase: () => kebabCase,
|
|
101
111
|
migrationCreatedBy: () => migrationCreatedBy,
|
|
102
112
|
minimumQuotasPerPlan: () => minimumQuotasPerPlan,
|
|
113
|
+
modelsKeptPastTheTenant: () => modelsKeptPastTheTenant,
|
|
103
114
|
namedImports: () => namedImports,
|
|
104
115
|
parseBlockAttributes: () => parseBlockAttributes,
|
|
105
116
|
parseEnumValues: () => parseEnumValues,
|
|
@@ -110,15 +121,16 @@ __export(index_exports, {
|
|
|
110
121
|
patchAppModule: () => patchAppModule,
|
|
111
122
|
patchOptionsFor: () => patchOptionsFor,
|
|
112
123
|
planInit: () => planInit,
|
|
113
|
-
projectKeyPattern: () => projectKeyPattern,
|
|
114
124
|
quotaKeyPattern: () => quotaKeyPattern,
|
|
115
125
|
readEffectiveModuleResolution: () => readEffectiveModuleResolution,
|
|
116
126
|
relationNameOf: () => relationNameOf,
|
|
127
|
+
removeProjectKey: () => removeProjectKey,
|
|
117
128
|
reportConstraints: () => reportConstraints,
|
|
118
129
|
rewriteImports: () => rewriteImports,
|
|
119
130
|
rewriteManifest: () => rewriteManifest,
|
|
120
131
|
rewriteNames: () => rewriteNames,
|
|
121
132
|
rewriteSubpath: () => rewriteSubpath,
|
|
133
|
+
settingsWrittenTo: () => settingsWrittenTo,
|
|
122
134
|
stripLineComment: () => stripLineComment,
|
|
123
135
|
structuralOnly: () => structuralOnly,
|
|
124
136
|
tablesAddressedBy: () => tablesAddressedBy
|
|
@@ -1033,7 +1045,7 @@ var PlanCatalogDoctorCheck = class {
|
|
|
1033
1045
|
severity: "ok",
|
|
1034
1046
|
message: `${plans.length} plan(s), ${this.catalog.features?.length ?? 0} feature(s) loaded.`,
|
|
1035
1047
|
details: {
|
|
1036
|
-
|
|
1048
|
+
app: this.catalog.app.name,
|
|
1037
1049
|
planIds: plans.map((p) => p.id)
|
|
1038
1050
|
}
|
|
1039
1051
|
};
|
|
@@ -1145,11 +1157,85 @@ AdminManifestDoctorCheck = _ts_decorate7([
|
|
|
1145
1157
|
typeof import_nest4.AdminManifestService === "undefined" ? Object : import_nest4.AdminManifestService
|
|
1146
1158
|
])
|
|
1147
1159
|
], AdminManifestDoctorCheck);
|
|
1160
|
+
var IssuerIdentityDoctorCheck = class {
|
|
1161
|
+
static {
|
|
1162
|
+
__name(this, "IssuerIdentityDoctorCheck");
|
|
1163
|
+
}
|
|
1164
|
+
issuer;
|
|
1165
|
+
id = "platform.issuer-identity";
|
|
1166
|
+
label = "Issuer identity against the recorded one";
|
|
1167
|
+
constructor(issuer) {
|
|
1168
|
+
this.issuer = issuer;
|
|
1169
|
+
}
|
|
1170
|
+
async run() {
|
|
1171
|
+
const verdict = await this.issuer.inspect();
|
|
1172
|
+
if (verdict.kind === "refused") {
|
|
1173
|
+
return {
|
|
1174
|
+
severity: "error",
|
|
1175
|
+
message: verdict.refusal,
|
|
1176
|
+
// The number and the identifiers, not the rows: `DoctorReport`
|
|
1177
|
+
// is serialised as JSON, and the dates on a row would come out
|
|
1178
|
+
// as timestamps beside the same dates already written as days in
|
|
1179
|
+
// the message above.
|
|
1180
|
+
// One unknown answered once: `null` for both where the
|
|
1181
|
+
// contracts could not be read, rather than a null count beside
|
|
1182
|
+
// an empty list that reads as "none".
|
|
1183
|
+
details: verdict.running ? {
|
|
1184
|
+
runningContracts: verdict.running.total,
|
|
1185
|
+
named: verdict.running.contracts.map((row) => row.id)
|
|
1186
|
+
} : {
|
|
1187
|
+
runningContracts: null,
|
|
1188
|
+
named: null
|
|
1189
|
+
}
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
if (verdict.kind === "not-compared") {
|
|
1193
|
+
return {
|
|
1194
|
+
severity: "warning",
|
|
1195
|
+
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.`
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
const change = verdict.change;
|
|
1199
|
+
switch (change.kind) {
|
|
1200
|
+
case "none-named":
|
|
1201
|
+
return {
|
|
1202
|
+
severity: "ok",
|
|
1203
|
+
message: "No issuer is named, and none was recorded."
|
|
1204
|
+
};
|
|
1205
|
+
case "first-naming":
|
|
1206
|
+
return {
|
|
1207
|
+
severity: "ok",
|
|
1208
|
+
message: `'${change.identity.legalName}' is named for the first time; no contract can name another.`
|
|
1209
|
+
};
|
|
1210
|
+
case "corrected":
|
|
1211
|
+
return {
|
|
1212
|
+
severity: "ok",
|
|
1213
|
+
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.`
|
|
1214
|
+
};
|
|
1215
|
+
case "unchanged": {
|
|
1216
|
+
const running = await this.issuer.runningContractCount();
|
|
1217
|
+
const weighedAgainst = running === null ? "" : ` ${running} contract(s) are running, and their issuer copies do not follow a change.`;
|
|
1218
|
+
return {
|
|
1219
|
+
severity: "ok",
|
|
1220
|
+
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."
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
};
|
|
1226
|
+
IssuerIdentityDoctorCheck = _ts_decorate7([
|
|
1227
|
+
(0, import_common7.Injectable)(),
|
|
1228
|
+
_ts_metadata7("design:type", Function),
|
|
1229
|
+
_ts_metadata7("design:paramtypes", [
|
|
1230
|
+
typeof import_nest4.IssuerIdentityInspector === "undefined" ? Object : import_nest4.IssuerIdentityInspector
|
|
1231
|
+
])
|
|
1232
|
+
], IssuerIdentityDoctorCheck);
|
|
1148
1233
|
var PLATFORM_DOCTOR_CHECK_PROVIDERS = [
|
|
1149
1234
|
PlanCatalogDoctorCheck,
|
|
1150
1235
|
DiscoverySnapshotDoctorCheck,
|
|
1151
1236
|
UserPortDoctorCheck,
|
|
1152
|
-
AdminManifestDoctorCheck
|
|
1237
|
+
AdminManifestDoctorCheck,
|
|
1238
|
+
IssuerIdentityDoctorCheck
|
|
1153
1239
|
];
|
|
1154
1240
|
|
|
1155
1241
|
// src/prisma-blocks.ts
|
|
@@ -1325,6 +1411,117 @@ function applyFragmentBlocks(schema2, fragmentBlocks, options = {}) {
|
|
|
1325
1411
|
}
|
|
1326
1412
|
__name(applyFragmentBlocks, "applyFragmentBlocks");
|
|
1327
1413
|
|
|
1414
|
+
// src/fk-pointers.ts
|
|
1415
|
+
var POINTER = /^(\s*)\/\/\s*(\w+)(\s+)(Tenant|User)(\??)(\s+)(@relation\(.*\))\s*$/;
|
|
1416
|
+
function findFkPointers(schema2) {
|
|
1417
|
+
const found = [];
|
|
1418
|
+
let model = "";
|
|
1419
|
+
schema2.split("\n").forEach((text, line) => {
|
|
1420
|
+
const opening = /^model\s+(\w+)\s*\{/.exec(text);
|
|
1421
|
+
if (opening) model = opening[1];
|
|
1422
|
+
const match = POINTER.exec(text);
|
|
1423
|
+
if (match) found.push({
|
|
1424
|
+
line,
|
|
1425
|
+
target: match[4],
|
|
1426
|
+
model,
|
|
1427
|
+
text
|
|
1428
|
+
});
|
|
1429
|
+
});
|
|
1430
|
+
return found;
|
|
1431
|
+
}
|
|
1432
|
+
__name(findFkPointers, "findFkPointers");
|
|
1433
|
+
function relationNameOf(relationAttribute) {
|
|
1434
|
+
const match = /@relation\(\s*"([^"]+)"/.exec(relationAttribute);
|
|
1435
|
+
return match ? match[1] : null;
|
|
1436
|
+
}
|
|
1437
|
+
__name(relationNameOf, "relationNameOf");
|
|
1438
|
+
function escapeForRegExp(value) {
|
|
1439
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1440
|
+
}
|
|
1441
|
+
__name(escapeForRegExp, "escapeForRegExp");
|
|
1442
|
+
function modelBody(schema2, model) {
|
|
1443
|
+
const name = escapeForRegExp(model);
|
|
1444
|
+
const block = new RegExp(`(^|\\n)model\\s+${name}\\s*\\{([\\s\\S]*?)\\n\\}`, "m").exec(schema2);
|
|
1445
|
+
return block ? block[2] : null;
|
|
1446
|
+
}
|
|
1447
|
+
__name(modelBody, "modelBody");
|
|
1448
|
+
function isOneToOne(schema2, model, foreignKey) {
|
|
1449
|
+
const body = modelBody(schema2, model);
|
|
1450
|
+
if (!body) return false;
|
|
1451
|
+
return new RegExp(`^\\s*${escapeForRegExp(foreignKey)}\\s+\\S+.*@unique`, "m").test(body);
|
|
1452
|
+
}
|
|
1453
|
+
__name(isOneToOne, "isOneToOne");
|
|
1454
|
+
function hasBackRelation(schema2, model, owner, relationName, singular = false) {
|
|
1455
|
+
const body = modelBody(schema2, owner);
|
|
1456
|
+
if (body === null) return false;
|
|
1457
|
+
const name = escapeForRegExp(model);
|
|
1458
|
+
const shape = singular ? `${name}\\??` : `${name}\\[\\]`;
|
|
1459
|
+
const candidates = [
|
|
1460
|
+
...body.matchAll(new RegExp(`^\\s*\\w+\\s+${shape}(\\s.*)?$`, "gm"))
|
|
1461
|
+
];
|
|
1462
|
+
return candidates.some((line) => relationNameOf(line[0]) === relationName);
|
|
1463
|
+
}
|
|
1464
|
+
__name(hasBackRelation, "hasBackRelation");
|
|
1465
|
+
function enableFkPointers(schema2, models) {
|
|
1466
|
+
const lines = schema2.split("\n");
|
|
1467
|
+
const enabled = [];
|
|
1468
|
+
const skipped = [];
|
|
1469
|
+
const needsBackRelation = [];
|
|
1470
|
+
for (const pointer of findFkPointers(schema2)) {
|
|
1471
|
+
const model = pointer.target === "Tenant" ? models.tenant : models.user;
|
|
1472
|
+
if (!model) {
|
|
1473
|
+
skipped.push({
|
|
1474
|
+
line: pointer.line,
|
|
1475
|
+
target: pointer.target
|
|
1476
|
+
});
|
|
1477
|
+
continue;
|
|
1478
|
+
}
|
|
1479
|
+
const match = POINTER.exec(pointer.text);
|
|
1480
|
+
const relationName = relationNameOf(match[7]);
|
|
1481
|
+
const foreignKey = foreignKeyOf(match[7]);
|
|
1482
|
+
const singular = foreignKey !== null && isOneToOne(schema2, pointer.model, foreignKey);
|
|
1483
|
+
if (!hasBackRelation(schema2, pointer.model, model, relationName, singular)) {
|
|
1484
|
+
needsBackRelation.push({
|
|
1485
|
+
line: pointer.line,
|
|
1486
|
+
owner: model,
|
|
1487
|
+
suggestion: backRelationSuggestion(pointer.model, relationName, singular)
|
|
1488
|
+
});
|
|
1489
|
+
continue;
|
|
1490
|
+
}
|
|
1491
|
+
const [, indent, field, gap1, , optional, gap2, relation] = match;
|
|
1492
|
+
lines[pointer.line] = `${indent}${field}${gap1}${model}${optional}${gap2}${relation}`;
|
|
1493
|
+
enabled.push({
|
|
1494
|
+
line: pointer.line,
|
|
1495
|
+
model
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
return {
|
|
1499
|
+
schema: lines.join("\n"),
|
|
1500
|
+
enabled,
|
|
1501
|
+
skipped,
|
|
1502
|
+
needsBackRelation
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
__name(enableFkPointers, "enableFkPointers");
|
|
1506
|
+
var lowerFirst = /* @__PURE__ */ __name((value) => value.charAt(0).toLowerCase() + value.slice(1), "lowerFirst");
|
|
1507
|
+
function foreignKeyOf(relationAttribute) {
|
|
1508
|
+
const match = /fields:\s*\[\s*(\w+)/.exec(relationAttribute);
|
|
1509
|
+
return match ? match[1] : null;
|
|
1510
|
+
}
|
|
1511
|
+
__name(foreignKeyOf, "foreignKeyOf");
|
|
1512
|
+
function backRelationSuggestion(model, relationName, singular) {
|
|
1513
|
+
const field = singular ? lowerFirst(model) : `${lowerFirst(model)}s`;
|
|
1514
|
+
const type = singular ? `${model}?` : `${model}[]`;
|
|
1515
|
+
return `${field} ${type}` + (relationName ? ` @relation("${relationName}")` : "");
|
|
1516
|
+
}
|
|
1517
|
+
__name(backRelationSuggestion, "backRelationSuggestion");
|
|
1518
|
+
function assertModelsExist(declaredModels, models) {
|
|
1519
|
+
const missing = Object.entries(models).filter(([, name]) => name && !declaredModels.includes(name)).map(([role, name]) => `--${role}-model=${name}`);
|
|
1520
|
+
if (missing.length === 0) return;
|
|
1521
|
+
throw new Error(`${missing.join(", ")} \u2014 no such model in this schema. It declares: ${declaredModels.slice().sort().join(", ")}.`);
|
|
1522
|
+
}
|
|
1523
|
+
__name(assertModelsExist, "assertModelsExist");
|
|
1524
|
+
|
|
1328
1525
|
// src/schema-check.ts
|
|
1329
1526
|
function renderType(signature) {
|
|
1330
1527
|
return `${signature.type}${signature.list ? "[]" : ""}${signature.optional ? "?" : ""}`;
|
|
@@ -1396,10 +1593,11 @@ function isDocumentedTypeSubstitution(spec, app, appEnums) {
|
|
|
1396
1593
|
return spec.type === "String" && appEnums.has(app.type);
|
|
1397
1594
|
}
|
|
1398
1595
|
__name(isDocumentedTypeSubstitution, "isDocumentedTypeSubstitution");
|
|
1399
|
-
function compareFields(model, specFields, appFields, appEnums, missingFields, fieldMismatches) {
|
|
1596
|
+
function compareFields(model, specFields, appFields, appEnums, notAdopted, missingFields, fieldMismatches) {
|
|
1400
1597
|
for (const [name, spec] of specFields) {
|
|
1401
1598
|
const app = appFields.get(name);
|
|
1402
1599
|
if (!app) {
|
|
1600
|
+
if (notAdopted.has(spec.type)) continue;
|
|
1403
1601
|
missingFields.push({
|
|
1404
1602
|
model,
|
|
1405
1603
|
field: name,
|
|
@@ -1457,34 +1655,62 @@ function breaksContract(attribute) {
|
|
|
1457
1655
|
return attribute.kind !== "index";
|
|
1458
1656
|
}
|
|
1459
1657
|
__name(breaksContract, "breaksContract");
|
|
1460
|
-
function
|
|
1658
|
+
function modelsKeptPastTheTenant(specSchema) {
|
|
1659
|
+
const pointed = new Set(findFkPointers(specSchema).filter((pointer) => pointer.target === "Tenant").map((pointer) => pointer.model));
|
|
1660
|
+
return [
|
|
1661
|
+
...parseSchema(specSchema).models
|
|
1662
|
+
].filter(([model, fields]) => fields.has("tenantId") && !pointed.has(model)).map(([model]) => model);
|
|
1663
|
+
}
|
|
1664
|
+
__name(modelsKeptPastTheTenant, "modelsKeptPastTheTenant");
|
|
1665
|
+
function cascadesFromTenant(model, block) {
|
|
1666
|
+
const found = [];
|
|
1667
|
+
for (const line of block.split("\n")) {
|
|
1668
|
+
const compact = structuralOnly(line).replace(/\s+/g, "");
|
|
1669
|
+
if (compact.includes("@relation(") && compact.includes("fields:[tenantId]") && compact.includes("onDelete:Cascade")) {
|
|
1670
|
+
found.push({
|
|
1671
|
+
model,
|
|
1672
|
+
field: line.trim().split(/\s/)[0]
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
return found;
|
|
1677
|
+
}
|
|
1678
|
+
__name(cascadesFromTenant, "cascadesFromTenant");
|
|
1679
|
+
function checkSchema(specSchema, appSchema, knownModels) {
|
|
1461
1680
|
const spec = parseSchema(specSchema);
|
|
1462
1681
|
const app = parseSchema(appSchema);
|
|
1463
|
-
const absentModels = [];
|
|
1464
1682
|
const missingFields = [];
|
|
1465
1683
|
const fieldMismatches = [];
|
|
1466
1684
|
const missingBlockAttributes = [];
|
|
1685
|
+
const absentModels = [
|
|
1686
|
+
...spec.models.keys()
|
|
1687
|
+
].filter((name) => !app.models.has(name));
|
|
1688
|
+
const absentEnums = [
|
|
1689
|
+
...spec.enums.keys()
|
|
1690
|
+
].filter((name) => !app.enums.has(name));
|
|
1691
|
+
const shipped = knownModels ?? new Set(spec.models.keys());
|
|
1692
|
+
const notAdopted = new Set([
|
|
1693
|
+
...shipped
|
|
1694
|
+
].filter((name) => !app.models.has(name)));
|
|
1467
1695
|
for (const [model, specFields] of spec.models) {
|
|
1468
1696
|
const appFields = app.models.get(model);
|
|
1469
|
-
if (!appFields)
|
|
1470
|
-
|
|
1471
|
-
continue;
|
|
1472
|
-
}
|
|
1473
|
-
compareFields(model, specFields, appFields, app.enums, missingFields, fieldMismatches);
|
|
1697
|
+
if (!appFields) continue;
|
|
1698
|
+
compareFields(model, specFields, appFields, app.enums, notAdopted, missingFields, fieldMismatches);
|
|
1474
1699
|
const specAttrs = spec.modelAttributes.get(model);
|
|
1475
1700
|
const appAttrs = app.modelAttributes.get(model);
|
|
1476
1701
|
if (specAttrs && appAttrs) {
|
|
1477
1702
|
compareBlockAttributes(model, specAttrs, appAttrs, missingBlockAttributes);
|
|
1478
1703
|
}
|
|
1479
1704
|
}
|
|
1480
|
-
const
|
|
1705
|
+
const appBlocks = extractBlocks(appSchema, "model");
|
|
1706
|
+
const tenantCascades = modelsKeptPastTheTenant(specSchema).flatMap((model) => {
|
|
1707
|
+
const block = appBlocks.get(model);
|
|
1708
|
+
return block ? cascadesFromTenant(model, block) : [];
|
|
1709
|
+
});
|
|
1481
1710
|
const missingEnumValues = [];
|
|
1482
1711
|
for (const [name, specValues] of spec.enums) {
|
|
1483
1712
|
const appValues = app.enums.get(name);
|
|
1484
|
-
if (!appValues)
|
|
1485
|
-
absentEnums.push(name);
|
|
1486
|
-
continue;
|
|
1487
|
-
}
|
|
1713
|
+
if (!appValues) continue;
|
|
1488
1714
|
for (const value of specValues) {
|
|
1489
1715
|
if (!appValues.includes(value)) {
|
|
1490
1716
|
missingEnumValues.push({
|
|
@@ -1501,9 +1727,10 @@ function checkSchema(specSchema, appSchema) {
|
|
|
1501
1727
|
missingEnumValues,
|
|
1502
1728
|
fieldMismatches,
|
|
1503
1729
|
missingBlockAttributes,
|
|
1730
|
+
tenantCascades,
|
|
1504
1731
|
checkedModelCount: spec.models.size - absentModels.length,
|
|
1505
1732
|
checkedEnumCount: spec.enums.size - absentEnums.length,
|
|
1506
|
-
ok: missingFields.length === 0 && missingEnumValues.length === 0 && fieldMismatches.length === 0 && !missingBlockAttributes.some(breaksContract)
|
|
1733
|
+
ok: missingFields.length === 0 && missingEnumValues.length === 0 && fieldMismatches.length === 0 && tenantCascades.length === 0 && !missingBlockAttributes.some(breaksContract)
|
|
1507
1734
|
};
|
|
1508
1735
|
}
|
|
1509
1736
|
__name(checkSchema, "checkSchema");
|
|
@@ -1584,117 +1811,6 @@ function reportConstraints(outcome, context) {
|
|
|
1584
1811
|
}
|
|
1585
1812
|
__name(reportConstraints, "reportConstraints");
|
|
1586
1813
|
|
|
1587
|
-
// src/fk-pointers.ts
|
|
1588
|
-
var POINTER = /^(\s*)\/\/\s*(\w+)(\s+)(Tenant|User)(\??)(\s+)(@relation\(.*\))\s*$/;
|
|
1589
|
-
function findFkPointers(schema2) {
|
|
1590
|
-
const found = [];
|
|
1591
|
-
let model = "";
|
|
1592
|
-
schema2.split("\n").forEach((text, line) => {
|
|
1593
|
-
const opening = /^model\s+(\w+)\s*\{/.exec(text);
|
|
1594
|
-
if (opening) model = opening[1];
|
|
1595
|
-
const match = POINTER.exec(text);
|
|
1596
|
-
if (match) found.push({
|
|
1597
|
-
line,
|
|
1598
|
-
target: match[4],
|
|
1599
|
-
model,
|
|
1600
|
-
text
|
|
1601
|
-
});
|
|
1602
|
-
});
|
|
1603
|
-
return found;
|
|
1604
|
-
}
|
|
1605
|
-
__name(findFkPointers, "findFkPointers");
|
|
1606
|
-
function relationNameOf(relationAttribute) {
|
|
1607
|
-
const match = /@relation\(\s*"([^"]+)"/.exec(relationAttribute);
|
|
1608
|
-
return match ? match[1] : null;
|
|
1609
|
-
}
|
|
1610
|
-
__name(relationNameOf, "relationNameOf");
|
|
1611
|
-
function escapeForRegExp(value) {
|
|
1612
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1613
|
-
}
|
|
1614
|
-
__name(escapeForRegExp, "escapeForRegExp");
|
|
1615
|
-
function modelBody(schema2, model) {
|
|
1616
|
-
const name = escapeForRegExp(model);
|
|
1617
|
-
const block = new RegExp(`(^|\\n)model\\s+${name}\\s*\\{([\\s\\S]*?)\\n\\}`, "m").exec(schema2);
|
|
1618
|
-
return block ? block[2] : null;
|
|
1619
|
-
}
|
|
1620
|
-
__name(modelBody, "modelBody");
|
|
1621
|
-
function isOneToOne(schema2, model, foreignKey) {
|
|
1622
|
-
const body = modelBody(schema2, model);
|
|
1623
|
-
if (!body) return false;
|
|
1624
|
-
return new RegExp(`^\\s*${escapeForRegExp(foreignKey)}\\s+\\S+.*@unique`, "m").test(body);
|
|
1625
|
-
}
|
|
1626
|
-
__name(isOneToOne, "isOneToOne");
|
|
1627
|
-
function hasBackRelation(schema2, model, owner, relationName, singular = false) {
|
|
1628
|
-
const body = modelBody(schema2, owner);
|
|
1629
|
-
if (body === null) return false;
|
|
1630
|
-
const name = escapeForRegExp(model);
|
|
1631
|
-
const shape = singular ? `${name}\\??` : `${name}\\[\\]`;
|
|
1632
|
-
const candidates = [
|
|
1633
|
-
...body.matchAll(new RegExp(`^\\s*\\w+\\s+${shape}(\\s.*)?$`, "gm"))
|
|
1634
|
-
];
|
|
1635
|
-
return candidates.some((line) => relationNameOf(line[0]) === relationName);
|
|
1636
|
-
}
|
|
1637
|
-
__name(hasBackRelation, "hasBackRelation");
|
|
1638
|
-
function enableFkPointers(schema2, models) {
|
|
1639
|
-
const lines = schema2.split("\n");
|
|
1640
|
-
const enabled = [];
|
|
1641
|
-
const skipped = [];
|
|
1642
|
-
const needsBackRelation = [];
|
|
1643
|
-
for (const pointer of findFkPointers(schema2)) {
|
|
1644
|
-
const model = pointer.target === "Tenant" ? models.tenant : models.user;
|
|
1645
|
-
if (!model) {
|
|
1646
|
-
skipped.push({
|
|
1647
|
-
line: pointer.line,
|
|
1648
|
-
target: pointer.target
|
|
1649
|
-
});
|
|
1650
|
-
continue;
|
|
1651
|
-
}
|
|
1652
|
-
const match = POINTER.exec(pointer.text);
|
|
1653
|
-
const relationName = relationNameOf(match[7]);
|
|
1654
|
-
const foreignKey = foreignKeyOf(match[7]);
|
|
1655
|
-
const singular = foreignKey !== null && isOneToOne(schema2, pointer.model, foreignKey);
|
|
1656
|
-
if (!hasBackRelation(schema2, pointer.model, model, relationName, singular)) {
|
|
1657
|
-
needsBackRelation.push({
|
|
1658
|
-
line: pointer.line,
|
|
1659
|
-
owner: model,
|
|
1660
|
-
suggestion: backRelationSuggestion(pointer.model, relationName, singular)
|
|
1661
|
-
});
|
|
1662
|
-
continue;
|
|
1663
|
-
}
|
|
1664
|
-
const [, indent, field, gap1, , optional, gap2, relation] = match;
|
|
1665
|
-
lines[pointer.line] = `${indent}${field}${gap1}${model}${optional}${gap2}${relation}`;
|
|
1666
|
-
enabled.push({
|
|
1667
|
-
line: pointer.line,
|
|
1668
|
-
model
|
|
1669
|
-
});
|
|
1670
|
-
}
|
|
1671
|
-
return {
|
|
1672
|
-
schema: lines.join("\n"),
|
|
1673
|
-
enabled,
|
|
1674
|
-
skipped,
|
|
1675
|
-
needsBackRelation
|
|
1676
|
-
};
|
|
1677
|
-
}
|
|
1678
|
-
__name(enableFkPointers, "enableFkPointers");
|
|
1679
|
-
var lowerFirst = /* @__PURE__ */ __name((value) => value.charAt(0).toLowerCase() + value.slice(1), "lowerFirst");
|
|
1680
|
-
function foreignKeyOf(relationAttribute) {
|
|
1681
|
-
const match = /fields:\s*\[\s*(\w+)/.exec(relationAttribute);
|
|
1682
|
-
return match ? match[1] : null;
|
|
1683
|
-
}
|
|
1684
|
-
__name(foreignKeyOf, "foreignKeyOf");
|
|
1685
|
-
function backRelationSuggestion(model, relationName, singular) {
|
|
1686
|
-
const field = singular ? lowerFirst(model) : `${lowerFirst(model)}s`;
|
|
1687
|
-
const type = singular ? `${model}?` : `${model}[]`;
|
|
1688
|
-
return `${field} ${type}` + (relationName ? ` @relation("${relationName}")` : "");
|
|
1689
|
-
}
|
|
1690
|
-
__name(backRelationSuggestion, "backRelationSuggestion");
|
|
1691
|
-
function assertModelsExist(declaredModels, models) {
|
|
1692
|
-
const missing = Object.entries(models).filter(([, name]) => name && !declaredModels.includes(name)).map(([role, name]) => `--${role}-model=${name}`);
|
|
1693
|
-
if (missing.length === 0) return;
|
|
1694
|
-
throw new Error(`${missing.join(", ")} \u2014 no such model in this schema. It declares: ${declaredModels.slice().sort().join(", ")}.`);
|
|
1695
|
-
}
|
|
1696
|
-
__name(assertModelsExist, "assertModelsExist");
|
|
1697
|
-
|
|
1698
1814
|
// src/init/catalog-keys.ts
|
|
1699
1815
|
var import_spec = require("@saasicat/spec");
|
|
1700
1816
|
var schema = import_spec.planCatalogSchema;
|
|
@@ -1705,10 +1821,11 @@ function required(value, what) {
|
|
|
1705
1821
|
return value;
|
|
1706
1822
|
}
|
|
1707
1823
|
__name(required, "required");
|
|
1708
|
-
|
|
1709
|
-
|
|
1824
|
+
var APP_KEY_PATTERN = /^[a-z][a-z0-9-]{1,30}$/;
|
|
1825
|
+
function appKeyPattern() {
|
|
1826
|
+
return APP_KEY_PATTERN;
|
|
1710
1827
|
}
|
|
1711
|
-
__name(
|
|
1828
|
+
__name(appKeyPattern, "appKeyPattern");
|
|
1712
1829
|
function quotaKeyPattern() {
|
|
1713
1830
|
const patterns = Object.keys(required(schema.$defs?.PlanDef?.properties?.quotas?.patternProperties, "quota key pattern"));
|
|
1714
1831
|
if (patterns.length !== 1) {
|
|
@@ -1721,12 +1838,12 @@ function minimumQuotasPerPlan() {
|
|
|
1721
1838
|
return required(schema.$defs?.PlanDef?.properties?.quotas?.minProperties, "minProperties");
|
|
1722
1839
|
}
|
|
1723
1840
|
__name(minimumQuotasPerPlan, "minimumQuotasPerPlan");
|
|
1724
|
-
function
|
|
1725
|
-
const pattern =
|
|
1726
|
-
if (pattern.test(
|
|
1727
|
-
throw new Error(`--
|
|
1841
|
+
function assertValidAppKey(appKey) {
|
|
1842
|
+
const pattern = appKeyPattern();
|
|
1843
|
+
if (pattern.test(appKey)) return;
|
|
1844
|
+
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.`);
|
|
1728
1845
|
}
|
|
1729
|
-
__name(
|
|
1846
|
+
__name(assertValidAppKey, "assertValidAppKey");
|
|
1730
1847
|
function assertValidQuotaKey(quotaKey) {
|
|
1731
1848
|
const pattern = quotaKeyPattern();
|
|
1732
1849
|
if (pattern.test(quotaKey)) return;
|
|
@@ -1758,24 +1875,24 @@ function pascalCase(value) {
|
|
|
1758
1875
|
__name(pascalCase, "pascalCase");
|
|
1759
1876
|
var quotaFileName = /* @__PURE__ */ __name((key) => `${key.replace(/[^A-Za-z0-9]+/g, "-").toLowerCase()}-quota.provider.ts`, "quotaFileName");
|
|
1760
1877
|
function planInit(options) {
|
|
1761
|
-
const
|
|
1762
|
-
if (!
|
|
1763
|
-
|
|
1764
|
-
const appLabel = options.appName ?? pascalCase(
|
|
1878
|
+
const appKey = options.appKey;
|
|
1879
|
+
if (!appKey) throw new Error("init needs an --app-key.");
|
|
1880
|
+
assertValidAppKey(appKey);
|
|
1881
|
+
const appLabel = options.appName ?? pascalCase(appKey);
|
|
1765
1882
|
const appName = pascalCase(appLabel);
|
|
1766
1883
|
const apiBase = options.apiBase ?? "/api/v1/admin";
|
|
1767
1884
|
const quotas = (options.quotas ?? []).map(parseQuota);
|
|
1768
1885
|
assertEnoughQuotas(quotas);
|
|
1769
1886
|
const hasherClass = options.skipHasher ? null : `${appName}PasswordHasher`;
|
|
1770
|
-
const featureKey = `${
|
|
1887
|
+
const featureKey = `${appKey.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase()}_CORE`;
|
|
1771
1888
|
const shared = {
|
|
1772
|
-
|
|
1889
|
+
APP_KEY: appKey,
|
|
1773
1890
|
APP_NAME: appName,
|
|
1774
1891
|
APP_LABEL: appLabel,
|
|
1775
1892
|
API_BASE: apiBase,
|
|
1776
1893
|
FEATURE_KEY: featureKey,
|
|
1777
|
-
REGISTRY_CONST: `${constantCase(
|
|
1778
|
-
MANIFEST_CONST: `${constantCase(
|
|
1894
|
+
REGISTRY_CONST: `${constantCase(appKey)}_FEATURE_UI_REGISTRY`,
|
|
1895
|
+
MANIFEST_CONST: `${constantCase(appKey)}_MANIFEST_CONTRIBUTION`,
|
|
1779
1896
|
ADMIN_MODULE_CLASS: `${appName}AdminModule`,
|
|
1780
1897
|
HASHER_CLASS: hasherClass ?? "",
|
|
1781
1898
|
HASHER_FILE: hasherClass ? `${kebabCase(appName)}-password.hasher` : "",
|
|
@@ -1906,6 +2023,31 @@ function readEffectiveModuleResolution(root, ts) {
|
|
|
1906
2023
|
}
|
|
1907
2024
|
__name(readEffectiveModuleResolution, "readEffectiveModuleResolution");
|
|
1908
2025
|
|
|
2026
|
+
// src/init/settings-written.ts
|
|
2027
|
+
var import_billing = require("@saasicat/nest/billing");
|
|
2028
|
+
function flatten(prefix, value, into) {
|
|
2029
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
2030
|
+
for (const [key, child] of Object.entries(value)) {
|
|
2031
|
+
flatten(`${prefix}.${key}`, child, into);
|
|
2032
|
+
}
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
into.push({
|
|
2036
|
+
key: prefix,
|
|
2037
|
+
value: JSON.stringify(value)
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
2040
|
+
__name(flatten, "flatten");
|
|
2041
|
+
function settingsWrittenTo(catalogYaml, source = "config/saas.yaml") {
|
|
2042
|
+
const catalog = (0, import_billing.loadPlanCatalogFromString)(catalogYaml, {
|
|
2043
|
+
source
|
|
2044
|
+
});
|
|
2045
|
+
const settings = [];
|
|
2046
|
+
flatten("tenantBilling", catalog.tenantBilling, settings);
|
|
2047
|
+
return settings;
|
|
2048
|
+
}
|
|
2049
|
+
__name(settingsWrittenTo, "settingsWrittenTo");
|
|
2050
|
+
|
|
1909
2051
|
// src/codemods/v1-imports.ts
|
|
1910
2052
|
var PUBLIC_PREFIXES = [
|
|
1911
2053
|
"ui/",
|
|
@@ -2169,6 +2311,327 @@ function rewriteManifest(text, table, options) {
|
|
|
2169
2311
|
}
|
|
2170
2312
|
__name(rewriteManifest, "rewriteManifest");
|
|
2171
2313
|
|
|
2314
|
+
// src/codemods/v1-project-key.ts
|
|
2315
|
+
function stripQueryParameter(text) {
|
|
2316
|
+
const NEEDLE = "projectKey=";
|
|
2317
|
+
let out = "";
|
|
2318
|
+
let index = 0;
|
|
2319
|
+
const taken = /* @__PURE__ */ new Set();
|
|
2320
|
+
for (; ; ) {
|
|
2321
|
+
const at = text.indexOf(NEEDLE, index);
|
|
2322
|
+
if (at < 0) break;
|
|
2323
|
+
const separator = at === 0 ? "" : text[at - 1];
|
|
2324
|
+
const value = separator === "?" || separator === "&" ? simpleValueEnd(text, at + NEEDLE.length) : null;
|
|
2325
|
+
if (value === null || !servesTheCatalogue(text, at)) {
|
|
2326
|
+
out += text.slice(index, at + NEEDLE.length);
|
|
2327
|
+
index = at + NEEDLE.length;
|
|
2328
|
+
continue;
|
|
2329
|
+
}
|
|
2330
|
+
out += text.slice(index, at - 1);
|
|
2331
|
+
if (separator === "?" && text[value] === "&") {
|
|
2332
|
+
out += "?";
|
|
2333
|
+
index = value + 1;
|
|
2334
|
+
} else {
|
|
2335
|
+
index = value;
|
|
2336
|
+
}
|
|
2337
|
+
taken.add(at);
|
|
2338
|
+
}
|
|
2339
|
+
return {
|
|
2340
|
+
text: out + text.slice(index),
|
|
2341
|
+
taken
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
__name(stripQueryParameter, "stripQueryParameter");
|
|
2345
|
+
var HIDES_A_BRACE = /[`'"{}/\\]/;
|
|
2346
|
+
function simpleValueEnd(text, from) {
|
|
2347
|
+
let i = from;
|
|
2348
|
+
while (i < text.length) {
|
|
2349
|
+
const ch = text[i];
|
|
2350
|
+
if (ch === "&" || ch === "#" || ch === "'" || ch === '"' || ch === "`" || ch === "\n" || ch === " ") {
|
|
2351
|
+
return i;
|
|
2352
|
+
}
|
|
2353
|
+
if (ch !== "$" || text[i + 1] !== "{") {
|
|
2354
|
+
if (ch === "{" || ch === "}") return null;
|
|
2355
|
+
i += 1;
|
|
2356
|
+
continue;
|
|
2357
|
+
}
|
|
2358
|
+
const close = text.indexOf("}", i + 2);
|
|
2359
|
+
if (close < 0) return null;
|
|
2360
|
+
if (HIDES_A_BRACE.test(text.slice(i + 2, close))) return null;
|
|
2361
|
+
i = close + 1;
|
|
2362
|
+
}
|
|
2363
|
+
return null;
|
|
2364
|
+
}
|
|
2365
|
+
__name(simpleValueEnd, "simpleValueEnd");
|
|
2366
|
+
function servesTheCatalogue(text, at) {
|
|
2367
|
+
let start = at;
|
|
2368
|
+
while (start > 0) {
|
|
2369
|
+
const ch = text[start - 1];
|
|
2370
|
+
if (ch === "`" || ch === "'" || ch === '"' || ch === "\n") break;
|
|
2371
|
+
start -= 1;
|
|
2372
|
+
}
|
|
2373
|
+
const url = text.slice(start, at);
|
|
2374
|
+
const query = url.indexOf("?");
|
|
2375
|
+
return (query < 0 ? url : url.slice(0, query)).includes("/catalog/");
|
|
2376
|
+
}
|
|
2377
|
+
__name(servesTheCatalogue, "servesTheCatalogue");
|
|
2378
|
+
function isIdentifierChar(ch) {
|
|
2379
|
+
return ch !== void 0 && /[A-Za-z0-9_$]/.test(ch);
|
|
2380
|
+
}
|
|
2381
|
+
__name(isIdentifierChar, "isIdentifierChar");
|
|
2382
|
+
function lineAt(text, at) {
|
|
2383
|
+
let line = 1;
|
|
2384
|
+
for (let i = 0; i < at; i += 1) if (text[i] === "\n") line += 1;
|
|
2385
|
+
return line;
|
|
2386
|
+
}
|
|
2387
|
+
__name(lineAt, "lineAt");
|
|
2388
|
+
function removeProjectKey(text, kind = "source") {
|
|
2389
|
+
if (kind === "yaml") return removeFromYaml(text);
|
|
2390
|
+
const query = stripQueryParameter(text);
|
|
2391
|
+
const reported = /* @__PURE__ */ new Set();
|
|
2392
|
+
for (let at = text.indexOf("projectKey"); at >= 0; at = text.indexOf("projectKey", at + 1)) {
|
|
2393
|
+
if (query.taken.has(at)) continue;
|
|
2394
|
+
if (isIdentifierChar(text[at - 1])) continue;
|
|
2395
|
+
if (isIdentifierChar(text[at + "projectKey".length])) continue;
|
|
2396
|
+
reported.add(lineAt(text, at));
|
|
2397
|
+
}
|
|
2398
|
+
return {
|
|
2399
|
+
text: query.text,
|
|
2400
|
+
rewritten: query.taken.size,
|
|
2401
|
+
undecided: [
|
|
2402
|
+
...reported
|
|
2403
|
+
].sort((a, b) => a - b)
|
|
2404
|
+
};
|
|
2405
|
+
}
|
|
2406
|
+
__name(removeProjectKey, "removeProjectKey");
|
|
2407
|
+
function removeFromYaml(text) {
|
|
2408
|
+
const lines = text.split("\n");
|
|
2409
|
+
const kept = [];
|
|
2410
|
+
let rewritten = 0;
|
|
2411
|
+
for (const line of lines) {
|
|
2412
|
+
if (line.startsWith("projectKey:")) {
|
|
2413
|
+
rewritten += 1;
|
|
2414
|
+
continue;
|
|
2415
|
+
}
|
|
2416
|
+
kept.push(line);
|
|
2417
|
+
}
|
|
2418
|
+
return {
|
|
2419
|
+
text: kept.join("\n"),
|
|
2420
|
+
rewritten,
|
|
2421
|
+
undecided: []
|
|
2422
|
+
};
|
|
2423
|
+
}
|
|
2424
|
+
__name(removeFromYaml, "removeFromYaml");
|
|
2425
|
+
|
|
2426
|
+
// src/codemods/v1-moved-settings.ts
|
|
2427
|
+
var import_spec2 = require("@saasicat/spec");
|
|
2428
|
+
var SETTINGS_THAT_MOVED = Object.keys(import_spec2.planCatalogSchema.properties?.tenantBilling?.properties ?? (() => {
|
|
2429
|
+
throw new Error("plan-catalog.schema.json declares no tenantBilling properties \u2014 @saasicat/spec and @saasicat/cli are out of step.");
|
|
2430
|
+
})());
|
|
2431
|
+
var isIdentifierChar2 = /* @__PURE__ */ __name((ch) => ch !== void 0 && /[A-Za-z0-9_$]/.test(ch), "isIdentifierChar");
|
|
2432
|
+
function lineAt2(text, index) {
|
|
2433
|
+
let line = 1;
|
|
2434
|
+
for (let at = 0; at < index; at += 1) {
|
|
2435
|
+
if (text[at] === "\n") line += 1;
|
|
2436
|
+
}
|
|
2437
|
+
return line;
|
|
2438
|
+
}
|
|
2439
|
+
__name(lineAt2, "lineAt");
|
|
2440
|
+
var SCANNED_FOR_MOVED_SETTINGS = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|vue)$/;
|
|
2441
|
+
function findMovedSettings(text) {
|
|
2442
|
+
const occurrences = [];
|
|
2443
|
+
for (const setting of SETTINGS_THAT_MOVED) {
|
|
2444
|
+
for (let at = text.indexOf(setting); at >= 0; at = text.indexOf(setting, at + 1)) {
|
|
2445
|
+
if (isIdentifierChar2(text[at - 1])) continue;
|
|
2446
|
+
if (isIdentifierChar2(text[at + setting.length])) continue;
|
|
2447
|
+
occurrences.push({
|
|
2448
|
+
setting,
|
|
2449
|
+
line: lineAt2(text, at)
|
|
2450
|
+
});
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
return {
|
|
2454
|
+
occurrences: occurrences.sort((a, b) => a.line - b.line || a.setting.localeCompare(b.setting))
|
|
2455
|
+
};
|
|
2456
|
+
}
|
|
2457
|
+
__name(findMovedSettings, "findMovedSettings");
|
|
2458
|
+
var WHERE_IT_GOES = {
|
|
2459
|
+
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.",
|
|
2460
|
+
selfServiceBlockedPlans: 'config/saas.yaml \u2192 tenantBilling.selfServiceBlockedPlans \u2014 both `asTarget` and `asSource` are required, and `[]` is the way to say "nothing is blocked".'
|
|
2461
|
+
};
|
|
2462
|
+
|
|
2463
|
+
// src/codemods/v1-db-catalog.ts
|
|
2464
|
+
var import_platform = require("@saasicat/nest/platform");
|
|
2465
|
+
var SCANNED_FOR_DB_CATALOG = SCANNED_FOR_MOVED_SETTINGS;
|
|
2466
|
+
var PROPERTY = "dbCatalog";
|
|
2467
|
+
var TAKEN = new Set(import_platform.DB_CATALOG_MEMBERS);
|
|
2468
|
+
var isIdentifierStart = /* @__PURE__ */ __name((ch) => ch !== void 0 && /[A-Za-z_$]/.test(ch), "isIdentifierStart");
|
|
2469
|
+
var isIdentifierChar3 = /* @__PURE__ */ __name((ch) => ch !== void 0 && /[A-Za-z0-9_$]/.test(ch), "isIdentifierChar");
|
|
2470
|
+
var isBlank = /* @__PURE__ */ __name((ch) => ch === " " || ch === " " || ch === "\n" || ch === "\r", "isBlank");
|
|
2471
|
+
function lineAt3(text, index) {
|
|
2472
|
+
let line = 1;
|
|
2473
|
+
for (let at = 0; at < index; at += 1) {
|
|
2474
|
+
if (text[at] === "\n") line += 1;
|
|
2475
|
+
}
|
|
2476
|
+
return line;
|
|
2477
|
+
}
|
|
2478
|
+
__name(lineAt3, "lineAt");
|
|
2479
|
+
function skipBlanks(text, from) {
|
|
2480
|
+
let at = from;
|
|
2481
|
+
while (isBlank(text[at])) at += 1;
|
|
2482
|
+
return at;
|
|
2483
|
+
}
|
|
2484
|
+
__name(skipBlanks, "skipBlanks");
|
|
2485
|
+
function closingQuote(text, open) {
|
|
2486
|
+
const quote = text[open];
|
|
2487
|
+
for (let at = open + 1; at < text.length; at += 1) {
|
|
2488
|
+
if (text[at] === "\\") {
|
|
2489
|
+
at += 1;
|
|
2490
|
+
continue;
|
|
2491
|
+
}
|
|
2492
|
+
if (text[at] === quote) return at;
|
|
2493
|
+
}
|
|
2494
|
+
return text.length;
|
|
2495
|
+
}
|
|
2496
|
+
__name(closingQuote, "closingQuote");
|
|
2497
|
+
function skipCommentOrString(text, at) {
|
|
2498
|
+
const ch = text[at];
|
|
2499
|
+
if (ch === "/" && text[at + 1] === "/") {
|
|
2500
|
+
const end = text.indexOf("\n", at);
|
|
2501
|
+
return end === -1 ? text.length : end;
|
|
2502
|
+
}
|
|
2503
|
+
if (ch === "/" && text[at + 1] === "*") {
|
|
2504
|
+
const end = text.indexOf("*/", at + 2);
|
|
2505
|
+
return end === -1 ? text.length : end + 2;
|
|
2506
|
+
}
|
|
2507
|
+
if (ch === "'" || ch === '"' || ch === "`") return closingQuote(text, at) + 1;
|
|
2508
|
+
return at;
|
|
2509
|
+
}
|
|
2510
|
+
__name(skipCommentOrString, "skipCommentOrString");
|
|
2511
|
+
function objectLiteralAt(text, open) {
|
|
2512
|
+
const members = [];
|
|
2513
|
+
let depth = 0;
|
|
2514
|
+
let expectingKey = true;
|
|
2515
|
+
let at = open + 1;
|
|
2516
|
+
while (at < text.length) {
|
|
2517
|
+
const ch = text[at];
|
|
2518
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
2519
|
+
const end = closingQuote(text, at);
|
|
2520
|
+
if (depth === 0 && expectingKey && text[skipBlanks(text, end + 1)] === ":") {
|
|
2521
|
+
members.push(text.slice(at + 1, end));
|
|
2522
|
+
}
|
|
2523
|
+
at = end + 1;
|
|
2524
|
+
continue;
|
|
2525
|
+
}
|
|
2526
|
+
const skipped = skipCommentOrString(text, at);
|
|
2527
|
+
if (skipped > at) {
|
|
2528
|
+
at = skipped;
|
|
2529
|
+
continue;
|
|
2530
|
+
}
|
|
2531
|
+
if (ch === "{" || ch === "[" || ch === "(") {
|
|
2532
|
+
depth += 1;
|
|
2533
|
+
at += 1;
|
|
2534
|
+
continue;
|
|
2535
|
+
}
|
|
2536
|
+
if (ch === "}" || ch === "]" || ch === ")") {
|
|
2537
|
+
if (depth === 0) return {
|
|
2538
|
+
members,
|
|
2539
|
+
end: at + 1
|
|
2540
|
+
};
|
|
2541
|
+
depth -= 1;
|
|
2542
|
+
at += 1;
|
|
2543
|
+
continue;
|
|
2544
|
+
}
|
|
2545
|
+
if (depth === 0 && ch === ":") expectingKey = false;
|
|
2546
|
+
if (depth === 0 && ch === ",") expectingKey = true;
|
|
2547
|
+
if (depth === 0 && expectingKey && ch === "." && text.startsWith("...", at)) {
|
|
2548
|
+
let end = at + 3;
|
|
2549
|
+
while (isIdentifierChar3(text[end])) end += 1;
|
|
2550
|
+
members.push(text.slice(at, end));
|
|
2551
|
+
at = end;
|
|
2552
|
+
continue;
|
|
2553
|
+
}
|
|
2554
|
+
if (depth === 0 && expectingKey && isIdentifierStart(ch)) {
|
|
2555
|
+
let end = at;
|
|
2556
|
+
while (isIdentifierChar3(text[end])) end += 1;
|
|
2557
|
+
const next = text[skipBlanks(text, end)];
|
|
2558
|
+
if (next === ":" || next === "," || next === "}") members.push(text.slice(at, end));
|
|
2559
|
+
at = end;
|
|
2560
|
+
continue;
|
|
2561
|
+
}
|
|
2562
|
+
at += 1;
|
|
2563
|
+
}
|
|
2564
|
+
return {
|
|
2565
|
+
members,
|
|
2566
|
+
end: text.length
|
|
2567
|
+
};
|
|
2568
|
+
}
|
|
2569
|
+
__name(objectLiteralAt, "objectLiteralAt");
|
|
2570
|
+
function findDbCatalogBlocks(text) {
|
|
2571
|
+
const occurrences = [];
|
|
2572
|
+
let at = 0;
|
|
2573
|
+
while (at < text.length) {
|
|
2574
|
+
const skipped = skipCommentOrString(text, at);
|
|
2575
|
+
if (skipped > at) {
|
|
2576
|
+
at = skipped;
|
|
2577
|
+
continue;
|
|
2578
|
+
}
|
|
2579
|
+
if (!text.startsWith(PROPERTY, at) || isIdentifierChar3(text[at - 1])) {
|
|
2580
|
+
at += 1;
|
|
2581
|
+
continue;
|
|
2582
|
+
}
|
|
2583
|
+
const afterName = at + PROPERTY.length;
|
|
2584
|
+
if (isIdentifierChar3(text[afterName])) {
|
|
2585
|
+
at = afterName;
|
|
2586
|
+
continue;
|
|
2587
|
+
}
|
|
2588
|
+
const colon = skipBlanks(text, afterName);
|
|
2589
|
+
if (text[colon] !== ":") {
|
|
2590
|
+
at = afterName;
|
|
2591
|
+
continue;
|
|
2592
|
+
}
|
|
2593
|
+
const value = skipBlanks(text, colon + 1);
|
|
2594
|
+
const line = lineAt3(text, at);
|
|
2595
|
+
if (text[value] !== "{") {
|
|
2596
|
+
occurrences.push({
|
|
2597
|
+
line,
|
|
2598
|
+
shape: "reference",
|
|
2599
|
+
leftovers: []
|
|
2600
|
+
});
|
|
2601
|
+
at = value;
|
|
2602
|
+
continue;
|
|
2603
|
+
}
|
|
2604
|
+
const { members, end } = objectLiteralAt(text, value);
|
|
2605
|
+
const leftovers = members.filter((member) => !TAKEN.has(member));
|
|
2606
|
+
if (!members.includes("path")) {
|
|
2607
|
+
occurrences.push({
|
|
2608
|
+
line,
|
|
2609
|
+
shape: "values",
|
|
2610
|
+
leftovers
|
|
2611
|
+
});
|
|
2612
|
+
} else if (leftovers.length > 0) {
|
|
2613
|
+
occurrences.push({
|
|
2614
|
+
line,
|
|
2615
|
+
shape: "mixed",
|
|
2616
|
+
leftovers
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
at = end;
|
|
2620
|
+
}
|
|
2621
|
+
return {
|
|
2622
|
+
occurrences
|
|
2623
|
+
};
|
|
2624
|
+
}
|
|
2625
|
+
__name(findDbCatalogBlocks, "findDbCatalogBlocks");
|
|
2626
|
+
function describeDbCatalogOccurrence(occurrence) {
|
|
2627
|
+
const { shape, leftovers } = occurrence;
|
|
2628
|
+
if (shape === "reference") return "carries something this cannot see into";
|
|
2629
|
+
if (shape === "mixed") return `names the path but still carries ${leftovers.join(", ")}`;
|
|
2630
|
+
return leftovers.length > 0 ? `carries the values: ${leftovers.join(", ")}` : "names no path";
|
|
2631
|
+
}
|
|
2632
|
+
__name(describeDbCatalogOccurrence, "describeDbCatalogOccurrence");
|
|
2633
|
+
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.";
|
|
2634
|
+
|
|
2172
2635
|
// src/init/patch-app-module.ts
|
|
2173
2636
|
var MARKER = "SaaSiCatModule.forRoot";
|
|
2174
2637
|
function patchAppModule(source, options) {
|
|
@@ -2268,7 +2731,7 @@ function renderForRootBlock(options) {
|
|
|
2268
2731
|
"SaaSiCatModule.forRoot(",
|
|
2269
2732
|
" defineSaaSiCat({",
|
|
2270
2733
|
" // Plans straight from the YAML. Apps that manage plans in the",
|
|
2271
|
-
" // SuperAdmin UI pass `dbCatalog` instead.",
|
|
2734
|
+
" // SuperAdmin UI pass `dbCatalog: { path: 'config/saas.yaml' }` instead.",
|
|
2272
2735
|
" planCatalog: loadPlanCatalogFromFile({ path: 'config/saas.yaml' }),",
|
|
2273
2736
|
" // Your authentication guard. This does NOT compile until you",
|
|
2274
2737
|
" // name one, and that is deliberate: an empty array is how the",
|
|
@@ -3373,6 +3836,7 @@ UserCommands = _ts_decorate14([
|
|
|
3373
3836
|
DiscoverySnapshotDoctorCheck,
|
|
3374
3837
|
DoctorCommands,
|
|
3375
3838
|
DoctorFlow,
|
|
3839
|
+
IssuerIdentityDoctorCheck,
|
|
3376
3840
|
LIMIT_FILTER_IMPORTS,
|
|
3377
3841
|
LIMIT_FILTER_PROVIDER,
|
|
3378
3842
|
MANIFEST_ACCESS_PORT_TOKEN,
|
|
@@ -3386,17 +3850,23 @@ UserCommands = _ts_decorate14([
|
|
|
3386
3850
|
MfaSetupFlow,
|
|
3387
3851
|
PLATFORM_DOCTOR_CHECK_PROVIDERS,
|
|
3388
3852
|
PlanCatalogDoctorCheck,
|
|
3853
|
+
SCANNED_FOR_DB_CATALOG,
|
|
3854
|
+
SCANNED_FOR_MOVED_SETTINGS,
|
|
3855
|
+
SETTINGS_THAT_MOVED,
|
|
3389
3856
|
UI_VUE_SPECIFIER,
|
|
3390
3857
|
USER_MANAGEMENT_PORT_TOKEN,
|
|
3391
3858
|
USER_PORT_TOKEN,
|
|
3392
3859
|
UserCommands,
|
|
3393
3860
|
UserPortDoctorCheck,
|
|
3861
|
+
WHERE_DB_CATALOG_GOES,
|
|
3862
|
+
WHERE_IT_GOES,
|
|
3394
3863
|
WhoAmIFlow,
|
|
3864
|
+
appKeyPattern,
|
|
3395
3865
|
appendConstraints,
|
|
3396
3866
|
applyFragmentBlocks,
|
|
3397
3867
|
applyTokens,
|
|
3398
3868
|
assertModelsExist,
|
|
3399
|
-
|
|
3869
|
+
assertValidAppKey,
|
|
3400
3870
|
assertValidQuotaKey,
|
|
3401
3871
|
blankStringLiterals,
|
|
3402
3872
|
blockBodyLines,
|
|
@@ -3404,6 +3874,7 @@ UserCommands = _ts_decorate14([
|
|
|
3404
3874
|
buildImportMap,
|
|
3405
3875
|
checkSchema,
|
|
3406
3876
|
constraintsFor,
|
|
3877
|
+
describeDbCatalogOccurrence,
|
|
3407
3878
|
enableFkPointers,
|
|
3408
3879
|
extractBlockNames,
|
|
3409
3880
|
extractBlocks,
|
|
@@ -3412,7 +3883,9 @@ UserCommands = _ts_decorate14([
|
|
|
3412
3883
|
extractFragmentBlocks,
|
|
3413
3884
|
extractModelBlocks,
|
|
3414
3885
|
extractModelNames,
|
|
3886
|
+
findDbCatalogBlocks,
|
|
3415
3887
|
findFkPointers,
|
|
3888
|
+
findMovedSettings,
|
|
3416
3889
|
foreignKeyOf,
|
|
3417
3890
|
hasBackRelation,
|
|
3418
3891
|
hasConstraints,
|
|
@@ -3422,6 +3895,7 @@ UserCommands = _ts_decorate14([
|
|
|
3422
3895
|
kebabCase,
|
|
3423
3896
|
migrationCreatedBy,
|
|
3424
3897
|
minimumQuotasPerPlan,
|
|
3898
|
+
modelsKeptPastTheTenant,
|
|
3425
3899
|
namedImports,
|
|
3426
3900
|
parseBlockAttributes,
|
|
3427
3901
|
parseEnumValues,
|
|
@@ -3432,15 +3906,16 @@ UserCommands = _ts_decorate14([
|
|
|
3432
3906
|
patchAppModule,
|
|
3433
3907
|
patchOptionsFor,
|
|
3434
3908
|
planInit,
|
|
3435
|
-
projectKeyPattern,
|
|
3436
3909
|
quotaKeyPattern,
|
|
3437
3910
|
readEffectiveModuleResolution,
|
|
3438
3911
|
relationNameOf,
|
|
3912
|
+
removeProjectKey,
|
|
3439
3913
|
reportConstraints,
|
|
3440
3914
|
rewriteImports,
|
|
3441
3915
|
rewriteManifest,
|
|
3442
3916
|
rewriteNames,
|
|
3443
3917
|
rewriteSubpath,
|
|
3918
|
+
settingsWrittenTo,
|
|
3444
3919
|
stripLineComment,
|
|
3445
3920
|
structuralOnly,
|
|
3446
3921
|
tablesAddressedBy
|