@saasicat/cli 1.0.0-rc.1 → 1.0.0-rc.11
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 +296 -33
- package/dist/.build-stamp +1 -1
- package/dist/index.cjs +480 -30
- package/dist/index.d.cts +218 -13
- package/dist/index.d.ts +218 -13
- package/dist/index.js +459 -24
- package/package.json +6 -5
- 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
|
@@ -64,17 +64,23 @@ __export(index_exports, {
|
|
|
64
64
|
MfaSetupFlow: () => MfaSetupFlow,
|
|
65
65
|
PLATFORM_DOCTOR_CHECK_PROVIDERS: () => PLATFORM_DOCTOR_CHECK_PROVIDERS,
|
|
66
66
|
PlanCatalogDoctorCheck: () => PlanCatalogDoctorCheck,
|
|
67
|
+
SCANNED_FOR_DB_CATALOG: () => SCANNED_FOR_DB_CATALOG,
|
|
68
|
+
SCANNED_FOR_MOVED_SETTINGS: () => SCANNED_FOR_MOVED_SETTINGS,
|
|
69
|
+
SETTINGS_THAT_MOVED: () => SETTINGS_THAT_MOVED,
|
|
67
70
|
UI_VUE_SPECIFIER: () => UI_VUE_SPECIFIER,
|
|
68
71
|
USER_MANAGEMENT_PORT_TOKEN: () => USER_MANAGEMENT_PORT_TOKEN,
|
|
69
72
|
USER_PORT_TOKEN: () => USER_PORT_TOKEN,
|
|
70
73
|
UserCommands: () => UserCommands,
|
|
71
74
|
UserPortDoctorCheck: () => UserPortDoctorCheck,
|
|
75
|
+
WHERE_DB_CATALOG_GOES: () => WHERE_DB_CATALOG_GOES,
|
|
76
|
+
WHERE_IT_GOES: () => WHERE_IT_GOES,
|
|
72
77
|
WhoAmIFlow: () => WhoAmIFlow,
|
|
78
|
+
appKeyPattern: () => appKeyPattern,
|
|
73
79
|
appendConstraints: () => appendConstraints,
|
|
74
80
|
applyFragmentBlocks: () => applyFragmentBlocks,
|
|
75
81
|
applyTokens: () => applyTokens,
|
|
76
82
|
assertModelsExist: () => assertModelsExist,
|
|
77
|
-
|
|
83
|
+
assertValidAppKey: () => assertValidAppKey,
|
|
78
84
|
assertValidQuotaKey: () => assertValidQuotaKey,
|
|
79
85
|
blankStringLiterals: () => blankStringLiterals,
|
|
80
86
|
blockBodyLines: () => blockBodyLines,
|
|
@@ -82,17 +88,24 @@ __export(index_exports, {
|
|
|
82
88
|
buildImportMap: () => buildImportMap,
|
|
83
89
|
checkSchema: () => checkSchema,
|
|
84
90
|
constraintsFor: () => constraintsFor,
|
|
91
|
+
describeDbCatalogOccurrence: () => describeDbCatalogOccurrence,
|
|
85
92
|
enableFkPointers: () => enableFkPointers,
|
|
86
93
|
extractBlockNames: () => extractBlockNames,
|
|
87
94
|
extractBlocks: () => extractBlocks,
|
|
95
|
+
extractEnumBlocks: () => extractEnumBlocks,
|
|
96
|
+
extractEnumNames: () => extractEnumNames,
|
|
97
|
+
extractFragmentBlocks: () => extractFragmentBlocks,
|
|
88
98
|
extractModelBlocks: () => extractModelBlocks,
|
|
89
99
|
extractModelNames: () => extractModelNames,
|
|
100
|
+
findDbCatalogBlocks: () => findDbCatalogBlocks,
|
|
90
101
|
findFkPointers: () => findFkPointers,
|
|
102
|
+
findMovedSettings: () => findMovedSettings,
|
|
91
103
|
foreignKeyOf: () => foreignKeyOf,
|
|
92
104
|
hasBackRelation: () => hasBackRelation,
|
|
93
105
|
hasConstraints: () => hasConstraints,
|
|
94
106
|
isNoLongerPublic: () => isNoLongerPublic,
|
|
95
107
|
isOneToOne: () => isOneToOne,
|
|
108
|
+
judgeModuleResolution: () => judgeModuleResolution,
|
|
96
109
|
kebabCase: () => kebabCase,
|
|
97
110
|
migrationCreatedBy: () => migrationCreatedBy,
|
|
98
111
|
minimumQuotasPerPlan: () => minimumQuotasPerPlan,
|
|
@@ -106,14 +119,16 @@ __export(index_exports, {
|
|
|
106
119
|
patchAppModule: () => patchAppModule,
|
|
107
120
|
patchOptionsFor: () => patchOptionsFor,
|
|
108
121
|
planInit: () => planInit,
|
|
109
|
-
projectKeyPattern: () => projectKeyPattern,
|
|
110
122
|
quotaKeyPattern: () => quotaKeyPattern,
|
|
123
|
+
readEffectiveModuleResolution: () => readEffectiveModuleResolution,
|
|
111
124
|
relationNameOf: () => relationNameOf,
|
|
125
|
+
removeProjectKey: () => removeProjectKey,
|
|
112
126
|
reportConstraints: () => reportConstraints,
|
|
113
127
|
rewriteImports: () => rewriteImports,
|
|
114
128
|
rewriteManifest: () => rewriteManifest,
|
|
115
129
|
rewriteNames: () => rewriteNames,
|
|
116
130
|
rewriteSubpath: () => rewriteSubpath,
|
|
131
|
+
settingsWrittenTo: () => settingsWrittenTo,
|
|
117
132
|
stripLineComment: () => stripLineComment,
|
|
118
133
|
structuralOnly: () => structuralOnly,
|
|
119
134
|
tablesAddressedBy: () => tablesAddressedBy
|
|
@@ -1028,7 +1043,7 @@ var PlanCatalogDoctorCheck = class {
|
|
|
1028
1043
|
severity: "ok",
|
|
1029
1044
|
message: `${plans.length} plan(s), ${this.catalog.features?.length ?? 0} feature(s) loaded.`,
|
|
1030
1045
|
details: {
|
|
1031
|
-
|
|
1046
|
+
app: this.catalog.app.name,
|
|
1032
1047
|
planIds: plans.map((p) => p.id)
|
|
1033
1048
|
}
|
|
1034
1049
|
};
|
|
@@ -1248,13 +1263,43 @@ function extractModelBlocks(fragment) {
|
|
|
1248
1263
|
return extractBlocks(fragment, "model");
|
|
1249
1264
|
}
|
|
1250
1265
|
__name(extractModelBlocks, "extractModelBlocks");
|
|
1266
|
+
function extractEnumBlocks(fragment) {
|
|
1267
|
+
return extractBlocks(fragment, "enum");
|
|
1268
|
+
}
|
|
1269
|
+
__name(extractEnumBlocks, "extractEnumBlocks");
|
|
1270
|
+
function extractEnumNames(schema2) {
|
|
1271
|
+
return extractBlockNames(schema2, "enum");
|
|
1272
|
+
}
|
|
1273
|
+
__name(extractEnumNames, "extractEnumNames");
|
|
1274
|
+
function extractFragmentBlocks(fragment) {
|
|
1275
|
+
return {
|
|
1276
|
+
enums: extractEnumBlocks(fragment),
|
|
1277
|
+
models: extractModelBlocks(fragment)
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
__name(extractFragmentBlocks, "extractFragmentBlocks");
|
|
1251
1281
|
function applyFragmentBlocks(schema2, fragmentBlocks, options = {}) {
|
|
1252
|
-
const
|
|
1282
|
+
const blocks = fragmentBlocks instanceof Map ? {
|
|
1283
|
+
enums: /* @__PURE__ */ new Map(),
|
|
1284
|
+
models: fragmentBlocks
|
|
1285
|
+
} : fragmentBlocks;
|
|
1286
|
+
const existingModels = new Set(extractModelNames(schema2));
|
|
1287
|
+
const existingEnums = new Set(extractEnumNames(schema2));
|
|
1253
1288
|
const added = [];
|
|
1254
1289
|
const skipped = [];
|
|
1290
|
+
const addedEnums = [];
|
|
1291
|
+
const skippedEnums = [];
|
|
1255
1292
|
const additions = [];
|
|
1256
|
-
for (const [name, block] of
|
|
1257
|
-
if (
|
|
1293
|
+
for (const [name, block] of blocks.enums) {
|
|
1294
|
+
if (existingEnums.has(name)) {
|
|
1295
|
+
skippedEnums.push(name);
|
|
1296
|
+
} else {
|
|
1297
|
+
addedEnums.push(name);
|
|
1298
|
+
additions.push(block);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
for (const [name, block] of blocks.models) {
|
|
1302
|
+
if (existingModels.has(name)) {
|
|
1258
1303
|
skipped.push(name);
|
|
1259
1304
|
} else {
|
|
1260
1305
|
added.push(name);
|
|
@@ -1265,6 +1310,8 @@ function applyFragmentBlocks(schema2, fragmentBlocks, options = {}) {
|
|
|
1265
1310
|
return {
|
|
1266
1311
|
added,
|
|
1267
1312
|
skipped,
|
|
1313
|
+
addedEnums,
|
|
1314
|
+
skippedEnums,
|
|
1268
1315
|
schema: schema2
|
|
1269
1316
|
};
|
|
1270
1317
|
}
|
|
@@ -1281,6 +1328,8 @@ function applyFragmentBlocks(schema2, fragmentBlocks, options = {}) {
|
|
|
1281
1328
|
return {
|
|
1282
1329
|
added,
|
|
1283
1330
|
skipped,
|
|
1331
|
+
addedEnums,
|
|
1332
|
+
skippedEnums,
|
|
1284
1333
|
schema: trimmedSchema + header + additions.join("\n\n") + "\n"
|
|
1285
1334
|
};
|
|
1286
1335
|
}
|
|
@@ -1666,10 +1715,11 @@ function required(value, what) {
|
|
|
1666
1715
|
return value;
|
|
1667
1716
|
}
|
|
1668
1717
|
__name(required, "required");
|
|
1669
|
-
|
|
1670
|
-
|
|
1718
|
+
var APP_KEY_PATTERN = /^[a-z][a-z0-9-]{1,30}$/;
|
|
1719
|
+
function appKeyPattern() {
|
|
1720
|
+
return APP_KEY_PATTERN;
|
|
1671
1721
|
}
|
|
1672
|
-
__name(
|
|
1722
|
+
__name(appKeyPattern, "appKeyPattern");
|
|
1673
1723
|
function quotaKeyPattern() {
|
|
1674
1724
|
const patterns = Object.keys(required(schema.$defs?.PlanDef?.properties?.quotas?.patternProperties, "quota key pattern"));
|
|
1675
1725
|
if (patterns.length !== 1) {
|
|
@@ -1682,12 +1732,12 @@ function minimumQuotasPerPlan() {
|
|
|
1682
1732
|
return required(schema.$defs?.PlanDef?.properties?.quotas?.minProperties, "minProperties");
|
|
1683
1733
|
}
|
|
1684
1734
|
__name(minimumQuotasPerPlan, "minimumQuotasPerPlan");
|
|
1685
|
-
function
|
|
1686
|
-
const pattern =
|
|
1687
|
-
if (pattern.test(
|
|
1688
|
-
throw new Error(`--
|
|
1735
|
+
function assertValidAppKey(appKey) {
|
|
1736
|
+
const pattern = appKeyPattern();
|
|
1737
|
+
if (pattern.test(appKey)) return;
|
|
1738
|
+
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.`);
|
|
1689
1739
|
}
|
|
1690
|
-
__name(
|
|
1740
|
+
__name(assertValidAppKey, "assertValidAppKey");
|
|
1691
1741
|
function assertValidQuotaKey(quotaKey) {
|
|
1692
1742
|
const pattern = quotaKeyPattern();
|
|
1693
1743
|
if (pattern.test(quotaKey)) return;
|
|
@@ -1719,24 +1769,24 @@ function pascalCase(value) {
|
|
|
1719
1769
|
__name(pascalCase, "pascalCase");
|
|
1720
1770
|
var quotaFileName = /* @__PURE__ */ __name((key) => `${key.replace(/[^A-Za-z0-9]+/g, "-").toLowerCase()}-quota.provider.ts`, "quotaFileName");
|
|
1721
1771
|
function planInit(options) {
|
|
1722
|
-
const
|
|
1723
|
-
if (!
|
|
1724
|
-
|
|
1725
|
-
const appLabel = options.appName ?? pascalCase(
|
|
1772
|
+
const appKey = options.appKey;
|
|
1773
|
+
if (!appKey) throw new Error("init needs an --app-key.");
|
|
1774
|
+
assertValidAppKey(appKey);
|
|
1775
|
+
const appLabel = options.appName ?? pascalCase(appKey);
|
|
1726
1776
|
const appName = pascalCase(appLabel);
|
|
1727
1777
|
const apiBase = options.apiBase ?? "/api/v1/admin";
|
|
1728
1778
|
const quotas = (options.quotas ?? []).map(parseQuota);
|
|
1729
1779
|
assertEnoughQuotas(quotas);
|
|
1730
1780
|
const hasherClass = options.skipHasher ? null : `${appName}PasswordHasher`;
|
|
1731
|
-
const featureKey = `${
|
|
1781
|
+
const featureKey = `${appKey.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase()}_CORE`;
|
|
1732
1782
|
const shared = {
|
|
1733
|
-
|
|
1783
|
+
APP_KEY: appKey,
|
|
1734
1784
|
APP_NAME: appName,
|
|
1735
1785
|
APP_LABEL: appLabel,
|
|
1736
1786
|
API_BASE: apiBase,
|
|
1737
1787
|
FEATURE_KEY: featureKey,
|
|
1738
|
-
REGISTRY_CONST: `${constantCase(
|
|
1739
|
-
MANIFEST_CONST: `${constantCase(
|
|
1788
|
+
REGISTRY_CONST: `${constantCase(appKey)}_FEATURE_UI_REGISTRY`,
|
|
1789
|
+
MANIFEST_CONST: `${constantCase(appKey)}_MANIFEST_CONTRIBUTION`,
|
|
1740
1790
|
ADMIN_MODULE_CLASS: `${appName}AdminModule`,
|
|
1741
1791
|
HASHER_CLASS: hasherClass ?? "",
|
|
1742
1792
|
HASHER_FILE: hasherClass ? `${kebabCase(appName)}-password.hasher` : "",
|
|
@@ -1835,6 +1885,63 @@ function patchOptionsFor(plan) {
|
|
|
1835
1885
|
}
|
|
1836
1886
|
__name(patchOptionsFor, "patchOptionsFor");
|
|
1837
1887
|
|
|
1888
|
+
// src/init/module-resolution.ts
|
|
1889
|
+
var import_node_path = require("path");
|
|
1890
|
+
var RESOLVES_SUBPATHS = /* @__PURE__ */ new Set([
|
|
1891
|
+
"node16",
|
|
1892
|
+
"nodenext",
|
|
1893
|
+
"bundler"
|
|
1894
|
+
]);
|
|
1895
|
+
function judgeModuleResolution(value) {
|
|
1896
|
+
if (value === null || RESOLVES_SUBPATHS.has(value)) {
|
|
1897
|
+
return {
|
|
1898
|
+
ok: true,
|
|
1899
|
+
value
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
return {
|
|
1903
|
+
ok: false,
|
|
1904
|
+
value,
|
|
1905
|
+
reason: `tsconfig.json resolves modules with "moduleResolution": "${value}"` + (value === "node10" ? ' (what TypeScript calls the "node" setting)' : "") + '. The files init writes import subpath exports (`@saasicat/nest/platform`, `/billing`, `/discovery`), which TypeScript resolves only under "node16", "nodenext" or "bundler". Set one of those first \u2014 `nest new` has used "nodenext" since NestJS 10.'
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
__name(judgeModuleResolution, "judgeModuleResolution");
|
|
1909
|
+
function readEffectiveModuleResolution(root, ts) {
|
|
1910
|
+
const configPath = (0, import_node_path.join)(root, "tsconfig.json");
|
|
1911
|
+
const read = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
1912
|
+
if (read.error || read.config === void 0) return null;
|
|
1913
|
+
const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, root);
|
|
1914
|
+
const kind = parsed.options.moduleResolution;
|
|
1915
|
+
if (kind === void 0) return null;
|
|
1916
|
+
return ts.ModuleResolutionKind[kind].toLowerCase();
|
|
1917
|
+
}
|
|
1918
|
+
__name(readEffectiveModuleResolution, "readEffectiveModuleResolution");
|
|
1919
|
+
|
|
1920
|
+
// src/init/settings-written.ts
|
|
1921
|
+
var import_billing = require("@saasicat/nest/billing");
|
|
1922
|
+
function flatten(prefix, value, into) {
|
|
1923
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
1924
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1925
|
+
flatten(`${prefix}.${key}`, child, into);
|
|
1926
|
+
}
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
into.push({
|
|
1930
|
+
key: prefix,
|
|
1931
|
+
value: JSON.stringify(value)
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
__name(flatten, "flatten");
|
|
1935
|
+
function settingsWrittenTo(catalogYaml, source = "config/saas.yaml") {
|
|
1936
|
+
const catalog = (0, import_billing.loadPlanCatalogFromString)(catalogYaml, {
|
|
1937
|
+
source
|
|
1938
|
+
});
|
|
1939
|
+
const settings = [];
|
|
1940
|
+
flatten("tenantBilling", catalog.tenantBilling, settings);
|
|
1941
|
+
return settings;
|
|
1942
|
+
}
|
|
1943
|
+
__name(settingsWrittenTo, "settingsWrittenTo");
|
|
1944
|
+
|
|
1838
1945
|
// src/codemods/v1-imports.ts
|
|
1839
1946
|
var PUBLIC_PREFIXES = [
|
|
1840
1947
|
"ui/",
|
|
@@ -2098,6 +2205,327 @@ function rewriteManifest(text, table, options) {
|
|
|
2098
2205
|
}
|
|
2099
2206
|
__name(rewriteManifest, "rewriteManifest");
|
|
2100
2207
|
|
|
2208
|
+
// src/codemods/v1-project-key.ts
|
|
2209
|
+
function stripQueryParameter(text) {
|
|
2210
|
+
const NEEDLE = "projectKey=";
|
|
2211
|
+
let out = "";
|
|
2212
|
+
let index = 0;
|
|
2213
|
+
const taken = /* @__PURE__ */ new Set();
|
|
2214
|
+
for (; ; ) {
|
|
2215
|
+
const at = text.indexOf(NEEDLE, index);
|
|
2216
|
+
if (at < 0) break;
|
|
2217
|
+
const separator = at === 0 ? "" : text[at - 1];
|
|
2218
|
+
const value = separator === "?" || separator === "&" ? simpleValueEnd(text, at + NEEDLE.length) : null;
|
|
2219
|
+
if (value === null || !servesTheCatalogue(text, at)) {
|
|
2220
|
+
out += text.slice(index, at + NEEDLE.length);
|
|
2221
|
+
index = at + NEEDLE.length;
|
|
2222
|
+
continue;
|
|
2223
|
+
}
|
|
2224
|
+
out += text.slice(index, at - 1);
|
|
2225
|
+
if (separator === "?" && text[value] === "&") {
|
|
2226
|
+
out += "?";
|
|
2227
|
+
index = value + 1;
|
|
2228
|
+
} else {
|
|
2229
|
+
index = value;
|
|
2230
|
+
}
|
|
2231
|
+
taken.add(at);
|
|
2232
|
+
}
|
|
2233
|
+
return {
|
|
2234
|
+
text: out + text.slice(index),
|
|
2235
|
+
taken
|
|
2236
|
+
};
|
|
2237
|
+
}
|
|
2238
|
+
__name(stripQueryParameter, "stripQueryParameter");
|
|
2239
|
+
var HIDES_A_BRACE = /[`'"{}/\\]/;
|
|
2240
|
+
function simpleValueEnd(text, from) {
|
|
2241
|
+
let i = from;
|
|
2242
|
+
while (i < text.length) {
|
|
2243
|
+
const ch = text[i];
|
|
2244
|
+
if (ch === "&" || ch === "#" || ch === "'" || ch === '"' || ch === "`" || ch === "\n" || ch === " ") {
|
|
2245
|
+
return i;
|
|
2246
|
+
}
|
|
2247
|
+
if (ch !== "$" || text[i + 1] !== "{") {
|
|
2248
|
+
if (ch === "{" || ch === "}") return null;
|
|
2249
|
+
i += 1;
|
|
2250
|
+
continue;
|
|
2251
|
+
}
|
|
2252
|
+
const close = text.indexOf("}", i + 2);
|
|
2253
|
+
if (close < 0) return null;
|
|
2254
|
+
if (HIDES_A_BRACE.test(text.slice(i + 2, close))) return null;
|
|
2255
|
+
i = close + 1;
|
|
2256
|
+
}
|
|
2257
|
+
return null;
|
|
2258
|
+
}
|
|
2259
|
+
__name(simpleValueEnd, "simpleValueEnd");
|
|
2260
|
+
function servesTheCatalogue(text, at) {
|
|
2261
|
+
let start = at;
|
|
2262
|
+
while (start > 0) {
|
|
2263
|
+
const ch = text[start - 1];
|
|
2264
|
+
if (ch === "`" || ch === "'" || ch === '"' || ch === "\n") break;
|
|
2265
|
+
start -= 1;
|
|
2266
|
+
}
|
|
2267
|
+
const url = text.slice(start, at);
|
|
2268
|
+
const query = url.indexOf("?");
|
|
2269
|
+
return (query < 0 ? url : url.slice(0, query)).includes("/catalog/");
|
|
2270
|
+
}
|
|
2271
|
+
__name(servesTheCatalogue, "servesTheCatalogue");
|
|
2272
|
+
function isIdentifierChar(ch) {
|
|
2273
|
+
return ch !== void 0 && /[A-Za-z0-9_$]/.test(ch);
|
|
2274
|
+
}
|
|
2275
|
+
__name(isIdentifierChar, "isIdentifierChar");
|
|
2276
|
+
function lineAt(text, at) {
|
|
2277
|
+
let line = 1;
|
|
2278
|
+
for (let i = 0; i < at; i += 1) if (text[i] === "\n") line += 1;
|
|
2279
|
+
return line;
|
|
2280
|
+
}
|
|
2281
|
+
__name(lineAt, "lineAt");
|
|
2282
|
+
function removeProjectKey(text, kind = "source") {
|
|
2283
|
+
if (kind === "yaml") return removeFromYaml(text);
|
|
2284
|
+
const query = stripQueryParameter(text);
|
|
2285
|
+
const reported = /* @__PURE__ */ new Set();
|
|
2286
|
+
for (let at = text.indexOf("projectKey"); at >= 0; at = text.indexOf("projectKey", at + 1)) {
|
|
2287
|
+
if (query.taken.has(at)) continue;
|
|
2288
|
+
if (isIdentifierChar(text[at - 1])) continue;
|
|
2289
|
+
if (isIdentifierChar(text[at + "projectKey".length])) continue;
|
|
2290
|
+
reported.add(lineAt(text, at));
|
|
2291
|
+
}
|
|
2292
|
+
return {
|
|
2293
|
+
text: query.text,
|
|
2294
|
+
rewritten: query.taken.size,
|
|
2295
|
+
undecided: [
|
|
2296
|
+
...reported
|
|
2297
|
+
].sort((a, b) => a - b)
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
__name(removeProjectKey, "removeProjectKey");
|
|
2301
|
+
function removeFromYaml(text) {
|
|
2302
|
+
const lines = text.split("\n");
|
|
2303
|
+
const kept = [];
|
|
2304
|
+
let rewritten = 0;
|
|
2305
|
+
for (const line of lines) {
|
|
2306
|
+
if (line.startsWith("projectKey:")) {
|
|
2307
|
+
rewritten += 1;
|
|
2308
|
+
continue;
|
|
2309
|
+
}
|
|
2310
|
+
kept.push(line);
|
|
2311
|
+
}
|
|
2312
|
+
return {
|
|
2313
|
+
text: kept.join("\n"),
|
|
2314
|
+
rewritten,
|
|
2315
|
+
undecided: []
|
|
2316
|
+
};
|
|
2317
|
+
}
|
|
2318
|
+
__name(removeFromYaml, "removeFromYaml");
|
|
2319
|
+
|
|
2320
|
+
// src/codemods/v1-moved-settings.ts
|
|
2321
|
+
var import_spec2 = require("@saasicat/spec");
|
|
2322
|
+
var SETTINGS_THAT_MOVED = Object.keys(import_spec2.planCatalogSchema.properties?.tenantBilling?.properties ?? (() => {
|
|
2323
|
+
throw new Error("plan-catalog.schema.json declares no tenantBilling properties \u2014 @saasicat/spec and @saasicat/cli are out of step.");
|
|
2324
|
+
})());
|
|
2325
|
+
var isIdentifierChar2 = /* @__PURE__ */ __name((ch) => ch !== void 0 && /[A-Za-z0-9_$]/.test(ch), "isIdentifierChar");
|
|
2326
|
+
function lineAt2(text, index) {
|
|
2327
|
+
let line = 1;
|
|
2328
|
+
for (let at = 0; at < index; at += 1) {
|
|
2329
|
+
if (text[at] === "\n") line += 1;
|
|
2330
|
+
}
|
|
2331
|
+
return line;
|
|
2332
|
+
}
|
|
2333
|
+
__name(lineAt2, "lineAt");
|
|
2334
|
+
var SCANNED_FOR_MOVED_SETTINGS = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|vue)$/;
|
|
2335
|
+
function findMovedSettings(text) {
|
|
2336
|
+
const occurrences = [];
|
|
2337
|
+
for (const setting of SETTINGS_THAT_MOVED) {
|
|
2338
|
+
for (let at = text.indexOf(setting); at >= 0; at = text.indexOf(setting, at + 1)) {
|
|
2339
|
+
if (isIdentifierChar2(text[at - 1])) continue;
|
|
2340
|
+
if (isIdentifierChar2(text[at + setting.length])) continue;
|
|
2341
|
+
occurrences.push({
|
|
2342
|
+
setting,
|
|
2343
|
+
line: lineAt2(text, at)
|
|
2344
|
+
});
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
return {
|
|
2348
|
+
occurrences: occurrences.sort((a, b) => a.line - b.line || a.setting.localeCompare(b.setting))
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
__name(findMovedSettings, "findMovedSettings");
|
|
2352
|
+
var WHERE_IT_GOES = {
|
|
2353
|
+
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.",
|
|
2354
|
+
selfServiceBlockedPlans: 'config/saas.yaml \u2192 tenantBilling.selfServiceBlockedPlans \u2014 both `asTarget` and `asSource` are required, and `[]` is the way to say "nothing is blocked".'
|
|
2355
|
+
};
|
|
2356
|
+
|
|
2357
|
+
// src/codemods/v1-db-catalog.ts
|
|
2358
|
+
var import_platform = require("@saasicat/nest/platform");
|
|
2359
|
+
var SCANNED_FOR_DB_CATALOG = SCANNED_FOR_MOVED_SETTINGS;
|
|
2360
|
+
var PROPERTY = "dbCatalog";
|
|
2361
|
+
var TAKEN = new Set(import_platform.DB_CATALOG_MEMBERS);
|
|
2362
|
+
var isIdentifierStart = /* @__PURE__ */ __name((ch) => ch !== void 0 && /[A-Za-z_$]/.test(ch), "isIdentifierStart");
|
|
2363
|
+
var isIdentifierChar3 = /* @__PURE__ */ __name((ch) => ch !== void 0 && /[A-Za-z0-9_$]/.test(ch), "isIdentifierChar");
|
|
2364
|
+
var isBlank = /* @__PURE__ */ __name((ch) => ch === " " || ch === " " || ch === "\n" || ch === "\r", "isBlank");
|
|
2365
|
+
function lineAt3(text, index) {
|
|
2366
|
+
let line = 1;
|
|
2367
|
+
for (let at = 0; at < index; at += 1) {
|
|
2368
|
+
if (text[at] === "\n") line += 1;
|
|
2369
|
+
}
|
|
2370
|
+
return line;
|
|
2371
|
+
}
|
|
2372
|
+
__name(lineAt3, "lineAt");
|
|
2373
|
+
function skipBlanks(text, from) {
|
|
2374
|
+
let at = from;
|
|
2375
|
+
while (isBlank(text[at])) at += 1;
|
|
2376
|
+
return at;
|
|
2377
|
+
}
|
|
2378
|
+
__name(skipBlanks, "skipBlanks");
|
|
2379
|
+
function closingQuote(text, open) {
|
|
2380
|
+
const quote = text[open];
|
|
2381
|
+
for (let at = open + 1; at < text.length; at += 1) {
|
|
2382
|
+
if (text[at] === "\\") {
|
|
2383
|
+
at += 1;
|
|
2384
|
+
continue;
|
|
2385
|
+
}
|
|
2386
|
+
if (text[at] === quote) return at;
|
|
2387
|
+
}
|
|
2388
|
+
return text.length;
|
|
2389
|
+
}
|
|
2390
|
+
__name(closingQuote, "closingQuote");
|
|
2391
|
+
function skipCommentOrString(text, at) {
|
|
2392
|
+
const ch = text[at];
|
|
2393
|
+
if (ch === "/" && text[at + 1] === "/") {
|
|
2394
|
+
const end = text.indexOf("\n", at);
|
|
2395
|
+
return end === -1 ? text.length : end;
|
|
2396
|
+
}
|
|
2397
|
+
if (ch === "/" && text[at + 1] === "*") {
|
|
2398
|
+
const end = text.indexOf("*/", at + 2);
|
|
2399
|
+
return end === -1 ? text.length : end + 2;
|
|
2400
|
+
}
|
|
2401
|
+
if (ch === "'" || ch === '"' || ch === "`") return closingQuote(text, at) + 1;
|
|
2402
|
+
return at;
|
|
2403
|
+
}
|
|
2404
|
+
__name(skipCommentOrString, "skipCommentOrString");
|
|
2405
|
+
function objectLiteralAt(text, open) {
|
|
2406
|
+
const members = [];
|
|
2407
|
+
let depth = 0;
|
|
2408
|
+
let expectingKey = true;
|
|
2409
|
+
let at = open + 1;
|
|
2410
|
+
while (at < text.length) {
|
|
2411
|
+
const ch = text[at];
|
|
2412
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
2413
|
+
const end = closingQuote(text, at);
|
|
2414
|
+
if (depth === 0 && expectingKey && text[skipBlanks(text, end + 1)] === ":") {
|
|
2415
|
+
members.push(text.slice(at + 1, end));
|
|
2416
|
+
}
|
|
2417
|
+
at = end + 1;
|
|
2418
|
+
continue;
|
|
2419
|
+
}
|
|
2420
|
+
const skipped = skipCommentOrString(text, at);
|
|
2421
|
+
if (skipped > at) {
|
|
2422
|
+
at = skipped;
|
|
2423
|
+
continue;
|
|
2424
|
+
}
|
|
2425
|
+
if (ch === "{" || ch === "[" || ch === "(") {
|
|
2426
|
+
depth += 1;
|
|
2427
|
+
at += 1;
|
|
2428
|
+
continue;
|
|
2429
|
+
}
|
|
2430
|
+
if (ch === "}" || ch === "]" || ch === ")") {
|
|
2431
|
+
if (depth === 0) return {
|
|
2432
|
+
members,
|
|
2433
|
+
end: at + 1
|
|
2434
|
+
};
|
|
2435
|
+
depth -= 1;
|
|
2436
|
+
at += 1;
|
|
2437
|
+
continue;
|
|
2438
|
+
}
|
|
2439
|
+
if (depth === 0 && ch === ":") expectingKey = false;
|
|
2440
|
+
if (depth === 0 && ch === ",") expectingKey = true;
|
|
2441
|
+
if (depth === 0 && expectingKey && ch === "." && text.startsWith("...", at)) {
|
|
2442
|
+
let end = at + 3;
|
|
2443
|
+
while (isIdentifierChar3(text[end])) end += 1;
|
|
2444
|
+
members.push(text.slice(at, end));
|
|
2445
|
+
at = end;
|
|
2446
|
+
continue;
|
|
2447
|
+
}
|
|
2448
|
+
if (depth === 0 && expectingKey && isIdentifierStart(ch)) {
|
|
2449
|
+
let end = at;
|
|
2450
|
+
while (isIdentifierChar3(text[end])) end += 1;
|
|
2451
|
+
const next = text[skipBlanks(text, end)];
|
|
2452
|
+
if (next === ":" || next === "," || next === "}") members.push(text.slice(at, end));
|
|
2453
|
+
at = end;
|
|
2454
|
+
continue;
|
|
2455
|
+
}
|
|
2456
|
+
at += 1;
|
|
2457
|
+
}
|
|
2458
|
+
return {
|
|
2459
|
+
members,
|
|
2460
|
+
end: text.length
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
__name(objectLiteralAt, "objectLiteralAt");
|
|
2464
|
+
function findDbCatalogBlocks(text) {
|
|
2465
|
+
const occurrences = [];
|
|
2466
|
+
let at = 0;
|
|
2467
|
+
while (at < text.length) {
|
|
2468
|
+
const skipped = skipCommentOrString(text, at);
|
|
2469
|
+
if (skipped > at) {
|
|
2470
|
+
at = skipped;
|
|
2471
|
+
continue;
|
|
2472
|
+
}
|
|
2473
|
+
if (!text.startsWith(PROPERTY, at) || isIdentifierChar3(text[at - 1])) {
|
|
2474
|
+
at += 1;
|
|
2475
|
+
continue;
|
|
2476
|
+
}
|
|
2477
|
+
const afterName = at + PROPERTY.length;
|
|
2478
|
+
if (isIdentifierChar3(text[afterName])) {
|
|
2479
|
+
at = afterName;
|
|
2480
|
+
continue;
|
|
2481
|
+
}
|
|
2482
|
+
const colon = skipBlanks(text, afterName);
|
|
2483
|
+
if (text[colon] !== ":") {
|
|
2484
|
+
at = afterName;
|
|
2485
|
+
continue;
|
|
2486
|
+
}
|
|
2487
|
+
const value = skipBlanks(text, colon + 1);
|
|
2488
|
+
const line = lineAt3(text, at);
|
|
2489
|
+
if (text[value] !== "{") {
|
|
2490
|
+
occurrences.push({
|
|
2491
|
+
line,
|
|
2492
|
+
shape: "reference",
|
|
2493
|
+
leftovers: []
|
|
2494
|
+
});
|
|
2495
|
+
at = value;
|
|
2496
|
+
continue;
|
|
2497
|
+
}
|
|
2498
|
+
const { members, end } = objectLiteralAt(text, value);
|
|
2499
|
+
const leftovers = members.filter((member) => !TAKEN.has(member));
|
|
2500
|
+
if (!members.includes("path")) {
|
|
2501
|
+
occurrences.push({
|
|
2502
|
+
line,
|
|
2503
|
+
shape: "values",
|
|
2504
|
+
leftovers
|
|
2505
|
+
});
|
|
2506
|
+
} else if (leftovers.length > 0) {
|
|
2507
|
+
occurrences.push({
|
|
2508
|
+
line,
|
|
2509
|
+
shape: "mixed",
|
|
2510
|
+
leftovers
|
|
2511
|
+
});
|
|
2512
|
+
}
|
|
2513
|
+
at = end;
|
|
2514
|
+
}
|
|
2515
|
+
return {
|
|
2516
|
+
occurrences
|
|
2517
|
+
};
|
|
2518
|
+
}
|
|
2519
|
+
__name(findDbCatalogBlocks, "findDbCatalogBlocks");
|
|
2520
|
+
function describeDbCatalogOccurrence(occurrence) {
|
|
2521
|
+
const { shape, leftovers } = occurrence;
|
|
2522
|
+
if (shape === "reference") return "carries something this cannot see into";
|
|
2523
|
+
if (shape === "mixed") return `names the path but still carries ${leftovers.join(", ")}`;
|
|
2524
|
+
return leftovers.length > 0 ? `carries the values: ${leftovers.join(", ")}` : "names no path";
|
|
2525
|
+
}
|
|
2526
|
+
__name(describeDbCatalogOccurrence, "describeDbCatalogOccurrence");
|
|
2527
|
+
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.";
|
|
2528
|
+
|
|
2101
2529
|
// src/init/patch-app-module.ts
|
|
2102
2530
|
var MARKER = "SaaSiCatModule.forRoot";
|
|
2103
2531
|
function patchAppModule(source, options) {
|
|
@@ -2118,7 +2546,7 @@ ${block}`;
|
|
|
2118
2546
|
return {
|
|
2119
2547
|
source,
|
|
2120
2548
|
status: "declined",
|
|
2121
|
-
reason: "no `@Module({
|
|
2549
|
+
reason: "this file has no `@Module({ ... })` with an `imports:` key on a line of its own \u2014 the shape `nest new` writes \u2014 so there is nowhere to add the platform without guessing at the structure",
|
|
2122
2550
|
manualBlock
|
|
2123
2551
|
};
|
|
2124
2552
|
}
|
|
@@ -2197,7 +2625,7 @@ function renderForRootBlock(options) {
|
|
|
2197
2625
|
"SaaSiCatModule.forRoot(",
|
|
2198
2626
|
" defineSaaSiCat({",
|
|
2199
2627
|
" // Plans straight from the YAML. Apps that manage plans in the",
|
|
2200
|
-
" // SuperAdmin UI pass `dbCatalog` instead.",
|
|
2628
|
+
" // SuperAdmin UI pass `dbCatalog: { path: 'config/saas.yaml' }` instead.",
|
|
2201
2629
|
" planCatalog: loadPlanCatalogFromFile({ path: 'config/saas.yaml' }),",
|
|
2202
2630
|
" // Your authentication guard. This does NOT compile until you",
|
|
2203
2631
|
" // name one, and that is deliberate: an empty array is how the",
|
|
@@ -2206,6 +2634,13 @@ function renderForRootBlock(options) {
|
|
|
2206
2634
|
" // your whole capability inventory \u2014 and the manifest routes to",
|
|
2207
2635
|
" // anyone who asks. Import your guard and put it in.",
|
|
2208
2636
|
" controller: { guards: [YourAuthGuard] },",
|
|
2637
|
+
" // The modules whose providers the platform injects: the one",
|
|
2638
|
+
" // exporting PrismaService, and the one your guard depends on.",
|
|
2639
|
+
" // `prismaPersistence({ client: PrismaService })` is resolved",
|
|
2640
|
+
" // inside the platform module, which sees only what is listed",
|
|
2641
|
+
" // here or declared @Global. Same rule as the guard: this does",
|
|
2642
|
+
" // not compile until you name them.",
|
|
2643
|
+
" imports: [YourPrismaModule, YourAuthModule],",
|
|
2209
2644
|
options.persistenceImport ? " persistence," : " // persistence: prismaPersistence({ client: PrismaService }),",
|
|
2210
2645
|
" catalog: { featureUiRegistry: " + options.registry.constName + " },",
|
|
2211
2646
|
" adminResources: true,",
|
|
@@ -2852,7 +3287,7 @@ DoctorCommands = _ts_decorate12([
|
|
|
2852
3287
|
|
|
2853
3288
|
// src/discovery.command.ts
|
|
2854
3289
|
var import_node_fs = require("fs");
|
|
2855
|
-
var
|
|
3290
|
+
var import_node_path2 = require("path");
|
|
2856
3291
|
var import_common13 = require("@nestjs/common");
|
|
2857
3292
|
var import_nest_commander5 = require("nest-commander");
|
|
2858
3293
|
var import_nest6 = require("@saasicat/nest");
|
|
@@ -2890,8 +3325,8 @@ var DiscoveryScanCommand = class extends import_nest_commander5.CommandRunner {
|
|
|
2890
3325
|
try {
|
|
2891
3326
|
const snapshot = this.scanner.rebuildSnapshot();
|
|
2892
3327
|
if (flags.out) {
|
|
2893
|
-
const outPath = (0,
|
|
2894
|
-
(0, import_node_fs.mkdirSync)((0,
|
|
3328
|
+
const outPath = (0, import_node_path2.resolve)(flags.out);
|
|
3329
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path2.dirname)(outPath), {
|
|
2895
3330
|
recursive: true
|
|
2896
3331
|
});
|
|
2897
3332
|
(0, import_node_fs.writeFileSync)(outPath, JSON.stringify(snapshot, null, 2) + "\n", "utf-8");
|
|
@@ -2900,7 +3335,7 @@ var DiscoveryScanCommand = class extends import_nest_commander5.CommandRunner {
|
|
|
2900
3335
|
process.stdout.write(`Discovery-Scan (${snapshot.app.key} v${snapshot.app.version}): ${snapshot.capabilities.length} Capabilities \xB7 ${snapshot.features.length} Features \xB7 ${snapshot.quotas.length} Quotas \xB7 hash ${snapshot.hash.slice(0, 19)}\u2026
|
|
2901
3336
|
`);
|
|
2902
3337
|
if (target) {
|
|
2903
|
-
process.stdout.write(`Snapshot persisted: ${(0,
|
|
3338
|
+
process.stdout.write(`Snapshot persisted: ${(0, import_node_path2.resolve)(target)}
|
|
2904
3339
|
`);
|
|
2905
3340
|
} else {
|
|
2906
3341
|
process.stderr.write("WARNING: the snapshot was not persisted \u2014 neither snapshotPath (DiscoveryModule.forRoot) configured nor --out given. The seed gate will find no snapshot this way.\n");
|
|
@@ -3308,17 +3743,23 @@ UserCommands = _ts_decorate14([
|
|
|
3308
3743
|
MfaSetupFlow,
|
|
3309
3744
|
PLATFORM_DOCTOR_CHECK_PROVIDERS,
|
|
3310
3745
|
PlanCatalogDoctorCheck,
|
|
3746
|
+
SCANNED_FOR_DB_CATALOG,
|
|
3747
|
+
SCANNED_FOR_MOVED_SETTINGS,
|
|
3748
|
+
SETTINGS_THAT_MOVED,
|
|
3311
3749
|
UI_VUE_SPECIFIER,
|
|
3312
3750
|
USER_MANAGEMENT_PORT_TOKEN,
|
|
3313
3751
|
USER_PORT_TOKEN,
|
|
3314
3752
|
UserCommands,
|
|
3315
3753
|
UserPortDoctorCheck,
|
|
3754
|
+
WHERE_DB_CATALOG_GOES,
|
|
3755
|
+
WHERE_IT_GOES,
|
|
3316
3756
|
WhoAmIFlow,
|
|
3757
|
+
appKeyPattern,
|
|
3317
3758
|
appendConstraints,
|
|
3318
3759
|
applyFragmentBlocks,
|
|
3319
3760
|
applyTokens,
|
|
3320
3761
|
assertModelsExist,
|
|
3321
|
-
|
|
3762
|
+
assertValidAppKey,
|
|
3322
3763
|
assertValidQuotaKey,
|
|
3323
3764
|
blankStringLiterals,
|
|
3324
3765
|
blockBodyLines,
|
|
@@ -3326,17 +3767,24 @@ UserCommands = _ts_decorate14([
|
|
|
3326
3767
|
buildImportMap,
|
|
3327
3768
|
checkSchema,
|
|
3328
3769
|
constraintsFor,
|
|
3770
|
+
describeDbCatalogOccurrence,
|
|
3329
3771
|
enableFkPointers,
|
|
3330
3772
|
extractBlockNames,
|
|
3331
3773
|
extractBlocks,
|
|
3774
|
+
extractEnumBlocks,
|
|
3775
|
+
extractEnumNames,
|
|
3776
|
+
extractFragmentBlocks,
|
|
3332
3777
|
extractModelBlocks,
|
|
3333
3778
|
extractModelNames,
|
|
3779
|
+
findDbCatalogBlocks,
|
|
3334
3780
|
findFkPointers,
|
|
3781
|
+
findMovedSettings,
|
|
3335
3782
|
foreignKeyOf,
|
|
3336
3783
|
hasBackRelation,
|
|
3337
3784
|
hasConstraints,
|
|
3338
3785
|
isNoLongerPublic,
|
|
3339
3786
|
isOneToOne,
|
|
3787
|
+
judgeModuleResolution,
|
|
3340
3788
|
kebabCase,
|
|
3341
3789
|
migrationCreatedBy,
|
|
3342
3790
|
minimumQuotasPerPlan,
|
|
@@ -3350,14 +3798,16 @@ UserCommands = _ts_decorate14([
|
|
|
3350
3798
|
patchAppModule,
|
|
3351
3799
|
patchOptionsFor,
|
|
3352
3800
|
planInit,
|
|
3353
|
-
projectKeyPattern,
|
|
3354
3801
|
quotaKeyPattern,
|
|
3802
|
+
readEffectiveModuleResolution,
|
|
3355
3803
|
relationNameOf,
|
|
3804
|
+
removeProjectKey,
|
|
3356
3805
|
reportConstraints,
|
|
3357
3806
|
rewriteImports,
|
|
3358
3807
|
rewriteManifest,
|
|
3359
3808
|
rewriteNames,
|
|
3360
3809
|
rewriteSubpath,
|
|
3810
|
+
settingsWrittenTo,
|
|
3361
3811
|
stripLineComment,
|
|
3362
3812
|
structuralOnly,
|
|
3363
3813
|
tablesAddressedBy
|