@saasicat/cli 1.0.0-rc.1 → 1.0.0-rc.10
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.js
CHANGED
|
@@ -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
|
};
|
|
@@ -1129,13 +1129,43 @@ function extractModelBlocks(fragment) {
|
|
|
1129
1129
|
return extractBlocks(fragment, "model");
|
|
1130
1130
|
}
|
|
1131
1131
|
__name(extractModelBlocks, "extractModelBlocks");
|
|
1132
|
+
function extractEnumBlocks(fragment) {
|
|
1133
|
+
return extractBlocks(fragment, "enum");
|
|
1134
|
+
}
|
|
1135
|
+
__name(extractEnumBlocks, "extractEnumBlocks");
|
|
1136
|
+
function extractEnumNames(schema2) {
|
|
1137
|
+
return extractBlockNames(schema2, "enum");
|
|
1138
|
+
}
|
|
1139
|
+
__name(extractEnumNames, "extractEnumNames");
|
|
1140
|
+
function extractFragmentBlocks(fragment) {
|
|
1141
|
+
return {
|
|
1142
|
+
enums: extractEnumBlocks(fragment),
|
|
1143
|
+
models: extractModelBlocks(fragment)
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
__name(extractFragmentBlocks, "extractFragmentBlocks");
|
|
1132
1147
|
function applyFragmentBlocks(schema2, fragmentBlocks, options = {}) {
|
|
1133
|
-
const
|
|
1148
|
+
const blocks = fragmentBlocks instanceof Map ? {
|
|
1149
|
+
enums: /* @__PURE__ */ new Map(),
|
|
1150
|
+
models: fragmentBlocks
|
|
1151
|
+
} : fragmentBlocks;
|
|
1152
|
+
const existingModels = new Set(extractModelNames(schema2));
|
|
1153
|
+
const existingEnums = new Set(extractEnumNames(schema2));
|
|
1134
1154
|
const added = [];
|
|
1135
1155
|
const skipped = [];
|
|
1156
|
+
const addedEnums = [];
|
|
1157
|
+
const skippedEnums = [];
|
|
1136
1158
|
const additions = [];
|
|
1137
|
-
for (const [name, block] of
|
|
1138
|
-
if (
|
|
1159
|
+
for (const [name, block] of blocks.enums) {
|
|
1160
|
+
if (existingEnums.has(name)) {
|
|
1161
|
+
skippedEnums.push(name);
|
|
1162
|
+
} else {
|
|
1163
|
+
addedEnums.push(name);
|
|
1164
|
+
additions.push(block);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
for (const [name, block] of blocks.models) {
|
|
1168
|
+
if (existingModels.has(name)) {
|
|
1139
1169
|
skipped.push(name);
|
|
1140
1170
|
} else {
|
|
1141
1171
|
added.push(name);
|
|
@@ -1146,6 +1176,8 @@ function applyFragmentBlocks(schema2, fragmentBlocks, options = {}) {
|
|
|
1146
1176
|
return {
|
|
1147
1177
|
added,
|
|
1148
1178
|
skipped,
|
|
1179
|
+
addedEnums,
|
|
1180
|
+
skippedEnums,
|
|
1149
1181
|
schema: schema2
|
|
1150
1182
|
};
|
|
1151
1183
|
}
|
|
@@ -1162,6 +1194,8 @@ function applyFragmentBlocks(schema2, fragmentBlocks, options = {}) {
|
|
|
1162
1194
|
return {
|
|
1163
1195
|
added,
|
|
1164
1196
|
skipped,
|
|
1197
|
+
addedEnums,
|
|
1198
|
+
skippedEnums,
|
|
1165
1199
|
schema: trimmedSchema + header + additions.join("\n\n") + "\n"
|
|
1166
1200
|
};
|
|
1167
1201
|
}
|
|
@@ -1547,10 +1581,11 @@ function required(value, what) {
|
|
|
1547
1581
|
return value;
|
|
1548
1582
|
}
|
|
1549
1583
|
__name(required, "required");
|
|
1550
|
-
|
|
1551
|
-
|
|
1584
|
+
var APP_KEY_PATTERN = /^[a-z][a-z0-9-]{1,30}$/;
|
|
1585
|
+
function appKeyPattern() {
|
|
1586
|
+
return APP_KEY_PATTERN;
|
|
1552
1587
|
}
|
|
1553
|
-
__name(
|
|
1588
|
+
__name(appKeyPattern, "appKeyPattern");
|
|
1554
1589
|
function quotaKeyPattern() {
|
|
1555
1590
|
const patterns = Object.keys(required(schema.$defs?.PlanDef?.properties?.quotas?.patternProperties, "quota key pattern"));
|
|
1556
1591
|
if (patterns.length !== 1) {
|
|
@@ -1563,12 +1598,12 @@ function minimumQuotasPerPlan() {
|
|
|
1563
1598
|
return required(schema.$defs?.PlanDef?.properties?.quotas?.minProperties, "minProperties");
|
|
1564
1599
|
}
|
|
1565
1600
|
__name(minimumQuotasPerPlan, "minimumQuotasPerPlan");
|
|
1566
|
-
function
|
|
1567
|
-
const pattern =
|
|
1568
|
-
if (pattern.test(
|
|
1569
|
-
throw new Error(`--
|
|
1601
|
+
function assertValidAppKey(appKey) {
|
|
1602
|
+
const pattern = appKeyPattern();
|
|
1603
|
+
if (pattern.test(appKey)) return;
|
|
1604
|
+
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.`);
|
|
1570
1605
|
}
|
|
1571
|
-
__name(
|
|
1606
|
+
__name(assertValidAppKey, "assertValidAppKey");
|
|
1572
1607
|
function assertValidQuotaKey(quotaKey) {
|
|
1573
1608
|
const pattern = quotaKeyPattern();
|
|
1574
1609
|
if (pattern.test(quotaKey)) return;
|
|
@@ -1600,24 +1635,24 @@ function pascalCase(value) {
|
|
|
1600
1635
|
__name(pascalCase, "pascalCase");
|
|
1601
1636
|
var quotaFileName = /* @__PURE__ */ __name((key) => `${key.replace(/[^A-Za-z0-9]+/g, "-").toLowerCase()}-quota.provider.ts`, "quotaFileName");
|
|
1602
1637
|
function planInit(options) {
|
|
1603
|
-
const
|
|
1604
|
-
if (!
|
|
1605
|
-
|
|
1606
|
-
const appLabel = options.appName ?? pascalCase(
|
|
1638
|
+
const appKey = options.appKey;
|
|
1639
|
+
if (!appKey) throw new Error("init needs an --app-key.");
|
|
1640
|
+
assertValidAppKey(appKey);
|
|
1641
|
+
const appLabel = options.appName ?? pascalCase(appKey);
|
|
1607
1642
|
const appName = pascalCase(appLabel);
|
|
1608
1643
|
const apiBase = options.apiBase ?? "/api/v1/admin";
|
|
1609
1644
|
const quotas = (options.quotas ?? []).map(parseQuota);
|
|
1610
1645
|
assertEnoughQuotas(quotas);
|
|
1611
1646
|
const hasherClass = options.skipHasher ? null : `${appName}PasswordHasher`;
|
|
1612
|
-
const featureKey = `${
|
|
1647
|
+
const featureKey = `${appKey.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase()}_CORE`;
|
|
1613
1648
|
const shared = {
|
|
1614
|
-
|
|
1649
|
+
APP_KEY: appKey,
|
|
1615
1650
|
APP_NAME: appName,
|
|
1616
1651
|
APP_LABEL: appLabel,
|
|
1617
1652
|
API_BASE: apiBase,
|
|
1618
1653
|
FEATURE_KEY: featureKey,
|
|
1619
|
-
REGISTRY_CONST: `${constantCase(
|
|
1620
|
-
MANIFEST_CONST: `${constantCase(
|
|
1654
|
+
REGISTRY_CONST: `${constantCase(appKey)}_FEATURE_UI_REGISTRY`,
|
|
1655
|
+
MANIFEST_CONST: `${constantCase(appKey)}_MANIFEST_CONTRIBUTION`,
|
|
1621
1656
|
ADMIN_MODULE_CLASS: `${appName}AdminModule`,
|
|
1622
1657
|
HASHER_CLASS: hasherClass ?? "",
|
|
1623
1658
|
HASHER_FILE: hasherClass ? `${kebabCase(appName)}-password.hasher` : "",
|
|
@@ -1716,6 +1751,63 @@ function patchOptionsFor(plan) {
|
|
|
1716
1751
|
}
|
|
1717
1752
|
__name(patchOptionsFor, "patchOptionsFor");
|
|
1718
1753
|
|
|
1754
|
+
// src/init/module-resolution.ts
|
|
1755
|
+
import { join } from "path";
|
|
1756
|
+
var RESOLVES_SUBPATHS = /* @__PURE__ */ new Set([
|
|
1757
|
+
"node16",
|
|
1758
|
+
"nodenext",
|
|
1759
|
+
"bundler"
|
|
1760
|
+
]);
|
|
1761
|
+
function judgeModuleResolution(value) {
|
|
1762
|
+
if (value === null || RESOLVES_SUBPATHS.has(value)) {
|
|
1763
|
+
return {
|
|
1764
|
+
ok: true,
|
|
1765
|
+
value
|
|
1766
|
+
};
|
|
1767
|
+
}
|
|
1768
|
+
return {
|
|
1769
|
+
ok: false,
|
|
1770
|
+
value,
|
|
1771
|
+
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.'
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
__name(judgeModuleResolution, "judgeModuleResolution");
|
|
1775
|
+
function readEffectiveModuleResolution(root, ts) {
|
|
1776
|
+
const configPath = join(root, "tsconfig.json");
|
|
1777
|
+
const read = ts.readConfigFile(configPath, ts.sys.readFile);
|
|
1778
|
+
if (read.error || read.config === void 0) return null;
|
|
1779
|
+
const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, root);
|
|
1780
|
+
const kind = parsed.options.moduleResolution;
|
|
1781
|
+
if (kind === void 0) return null;
|
|
1782
|
+
return ts.ModuleResolutionKind[kind].toLowerCase();
|
|
1783
|
+
}
|
|
1784
|
+
__name(readEffectiveModuleResolution, "readEffectiveModuleResolution");
|
|
1785
|
+
|
|
1786
|
+
// src/init/settings-written.ts
|
|
1787
|
+
import { loadPlanCatalogFromString } from "@saasicat/nest/billing";
|
|
1788
|
+
function flatten(prefix, value, into) {
|
|
1789
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
1790
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1791
|
+
flatten(`${prefix}.${key}`, child, into);
|
|
1792
|
+
}
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
into.push({
|
|
1796
|
+
key: prefix,
|
|
1797
|
+
value: JSON.stringify(value)
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
__name(flatten, "flatten");
|
|
1801
|
+
function settingsWrittenTo(catalogYaml, source = "config/saas.yaml") {
|
|
1802
|
+
const catalog = loadPlanCatalogFromString(catalogYaml, {
|
|
1803
|
+
source
|
|
1804
|
+
});
|
|
1805
|
+
const settings = [];
|
|
1806
|
+
flatten("tenantBilling", catalog.tenantBilling, settings);
|
|
1807
|
+
return settings;
|
|
1808
|
+
}
|
|
1809
|
+
__name(settingsWrittenTo, "settingsWrittenTo");
|
|
1810
|
+
|
|
1719
1811
|
// src/codemods/v1-imports.ts
|
|
1720
1812
|
var PUBLIC_PREFIXES = [
|
|
1721
1813
|
"ui/",
|
|
@@ -1979,6 +2071,327 @@ function rewriteManifest(text, table, options) {
|
|
|
1979
2071
|
}
|
|
1980
2072
|
__name(rewriteManifest, "rewriteManifest");
|
|
1981
2073
|
|
|
2074
|
+
// src/codemods/v1-project-key.ts
|
|
2075
|
+
function stripQueryParameter(text) {
|
|
2076
|
+
const NEEDLE = "projectKey=";
|
|
2077
|
+
let out = "";
|
|
2078
|
+
let index = 0;
|
|
2079
|
+
const taken = /* @__PURE__ */ new Set();
|
|
2080
|
+
for (; ; ) {
|
|
2081
|
+
const at = text.indexOf(NEEDLE, index);
|
|
2082
|
+
if (at < 0) break;
|
|
2083
|
+
const separator = at === 0 ? "" : text[at - 1];
|
|
2084
|
+
const value = separator === "?" || separator === "&" ? simpleValueEnd(text, at + NEEDLE.length) : null;
|
|
2085
|
+
if (value === null || !servesTheCatalogue(text, at)) {
|
|
2086
|
+
out += text.slice(index, at + NEEDLE.length);
|
|
2087
|
+
index = at + NEEDLE.length;
|
|
2088
|
+
continue;
|
|
2089
|
+
}
|
|
2090
|
+
out += text.slice(index, at - 1);
|
|
2091
|
+
if (separator === "?" && text[value] === "&") {
|
|
2092
|
+
out += "?";
|
|
2093
|
+
index = value + 1;
|
|
2094
|
+
} else {
|
|
2095
|
+
index = value;
|
|
2096
|
+
}
|
|
2097
|
+
taken.add(at);
|
|
2098
|
+
}
|
|
2099
|
+
return {
|
|
2100
|
+
text: out + text.slice(index),
|
|
2101
|
+
taken
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
__name(stripQueryParameter, "stripQueryParameter");
|
|
2105
|
+
var HIDES_A_BRACE = /[`'"{}/\\]/;
|
|
2106
|
+
function simpleValueEnd(text, from) {
|
|
2107
|
+
let i = from;
|
|
2108
|
+
while (i < text.length) {
|
|
2109
|
+
const ch = text[i];
|
|
2110
|
+
if (ch === "&" || ch === "#" || ch === "'" || ch === '"' || ch === "`" || ch === "\n" || ch === " ") {
|
|
2111
|
+
return i;
|
|
2112
|
+
}
|
|
2113
|
+
if (ch !== "$" || text[i + 1] !== "{") {
|
|
2114
|
+
if (ch === "{" || ch === "}") return null;
|
|
2115
|
+
i += 1;
|
|
2116
|
+
continue;
|
|
2117
|
+
}
|
|
2118
|
+
const close = text.indexOf("}", i + 2);
|
|
2119
|
+
if (close < 0) return null;
|
|
2120
|
+
if (HIDES_A_BRACE.test(text.slice(i + 2, close))) return null;
|
|
2121
|
+
i = close + 1;
|
|
2122
|
+
}
|
|
2123
|
+
return null;
|
|
2124
|
+
}
|
|
2125
|
+
__name(simpleValueEnd, "simpleValueEnd");
|
|
2126
|
+
function servesTheCatalogue(text, at) {
|
|
2127
|
+
let start = at;
|
|
2128
|
+
while (start > 0) {
|
|
2129
|
+
const ch = text[start - 1];
|
|
2130
|
+
if (ch === "`" || ch === "'" || ch === '"' || ch === "\n") break;
|
|
2131
|
+
start -= 1;
|
|
2132
|
+
}
|
|
2133
|
+
const url = text.slice(start, at);
|
|
2134
|
+
const query = url.indexOf("?");
|
|
2135
|
+
return (query < 0 ? url : url.slice(0, query)).includes("/catalog/");
|
|
2136
|
+
}
|
|
2137
|
+
__name(servesTheCatalogue, "servesTheCatalogue");
|
|
2138
|
+
function isIdentifierChar(ch) {
|
|
2139
|
+
return ch !== void 0 && /[A-Za-z0-9_$]/.test(ch);
|
|
2140
|
+
}
|
|
2141
|
+
__name(isIdentifierChar, "isIdentifierChar");
|
|
2142
|
+
function lineAt(text, at) {
|
|
2143
|
+
let line = 1;
|
|
2144
|
+
for (let i = 0; i < at; i += 1) if (text[i] === "\n") line += 1;
|
|
2145
|
+
return line;
|
|
2146
|
+
}
|
|
2147
|
+
__name(lineAt, "lineAt");
|
|
2148
|
+
function removeProjectKey(text, kind = "source") {
|
|
2149
|
+
if (kind === "yaml") return removeFromYaml(text);
|
|
2150
|
+
const query = stripQueryParameter(text);
|
|
2151
|
+
const reported = /* @__PURE__ */ new Set();
|
|
2152
|
+
for (let at = text.indexOf("projectKey"); at >= 0; at = text.indexOf("projectKey", at + 1)) {
|
|
2153
|
+
if (query.taken.has(at)) continue;
|
|
2154
|
+
if (isIdentifierChar(text[at - 1])) continue;
|
|
2155
|
+
if (isIdentifierChar(text[at + "projectKey".length])) continue;
|
|
2156
|
+
reported.add(lineAt(text, at));
|
|
2157
|
+
}
|
|
2158
|
+
return {
|
|
2159
|
+
text: query.text,
|
|
2160
|
+
rewritten: query.taken.size,
|
|
2161
|
+
undecided: [
|
|
2162
|
+
...reported
|
|
2163
|
+
].sort((a, b) => a - b)
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
__name(removeProjectKey, "removeProjectKey");
|
|
2167
|
+
function removeFromYaml(text) {
|
|
2168
|
+
const lines = text.split("\n");
|
|
2169
|
+
const kept = [];
|
|
2170
|
+
let rewritten = 0;
|
|
2171
|
+
for (const line of lines) {
|
|
2172
|
+
if (line.startsWith("projectKey:")) {
|
|
2173
|
+
rewritten += 1;
|
|
2174
|
+
continue;
|
|
2175
|
+
}
|
|
2176
|
+
kept.push(line);
|
|
2177
|
+
}
|
|
2178
|
+
return {
|
|
2179
|
+
text: kept.join("\n"),
|
|
2180
|
+
rewritten,
|
|
2181
|
+
undecided: []
|
|
2182
|
+
};
|
|
2183
|
+
}
|
|
2184
|
+
__name(removeFromYaml, "removeFromYaml");
|
|
2185
|
+
|
|
2186
|
+
// src/codemods/v1-moved-settings.ts
|
|
2187
|
+
import { planCatalogSchema as planCatalogSchema2 } from "@saasicat/spec";
|
|
2188
|
+
var SETTINGS_THAT_MOVED = Object.keys(planCatalogSchema2.properties?.tenantBilling?.properties ?? (() => {
|
|
2189
|
+
throw new Error("plan-catalog.schema.json declares no tenantBilling properties \u2014 @saasicat/spec and @saasicat/cli are out of step.");
|
|
2190
|
+
})());
|
|
2191
|
+
var isIdentifierChar2 = /* @__PURE__ */ __name((ch) => ch !== void 0 && /[A-Za-z0-9_$]/.test(ch), "isIdentifierChar");
|
|
2192
|
+
function lineAt2(text, index) {
|
|
2193
|
+
let line = 1;
|
|
2194
|
+
for (let at = 0; at < index; at += 1) {
|
|
2195
|
+
if (text[at] === "\n") line += 1;
|
|
2196
|
+
}
|
|
2197
|
+
return line;
|
|
2198
|
+
}
|
|
2199
|
+
__name(lineAt2, "lineAt");
|
|
2200
|
+
var SCANNED_FOR_MOVED_SETTINGS = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|vue)$/;
|
|
2201
|
+
function findMovedSettings(text) {
|
|
2202
|
+
const occurrences = [];
|
|
2203
|
+
for (const setting of SETTINGS_THAT_MOVED) {
|
|
2204
|
+
for (let at = text.indexOf(setting); at >= 0; at = text.indexOf(setting, at + 1)) {
|
|
2205
|
+
if (isIdentifierChar2(text[at - 1])) continue;
|
|
2206
|
+
if (isIdentifierChar2(text[at + setting.length])) continue;
|
|
2207
|
+
occurrences.push({
|
|
2208
|
+
setting,
|
|
2209
|
+
line: lineAt2(text, at)
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
return {
|
|
2214
|
+
occurrences: occurrences.sort((a, b) => a.line - b.line || a.setting.localeCompare(b.setting))
|
|
2215
|
+
};
|
|
2216
|
+
}
|
|
2217
|
+
__name(findMovedSettings, "findMovedSettings");
|
|
2218
|
+
var WHERE_IT_GOES = {
|
|
2219
|
+
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.",
|
|
2220
|
+
selfServiceBlockedPlans: 'config/saas.yaml \u2192 tenantBilling.selfServiceBlockedPlans \u2014 both `asTarget` and `asSource` are required, and `[]` is the way to say "nothing is blocked".'
|
|
2221
|
+
};
|
|
2222
|
+
|
|
2223
|
+
// src/codemods/v1-db-catalog.ts
|
|
2224
|
+
import { DB_CATALOG_MEMBERS } from "@saasicat/nest/platform";
|
|
2225
|
+
var SCANNED_FOR_DB_CATALOG = SCANNED_FOR_MOVED_SETTINGS;
|
|
2226
|
+
var PROPERTY = "dbCatalog";
|
|
2227
|
+
var TAKEN = new Set(DB_CATALOG_MEMBERS);
|
|
2228
|
+
var isIdentifierStart = /* @__PURE__ */ __name((ch) => ch !== void 0 && /[A-Za-z_$]/.test(ch), "isIdentifierStart");
|
|
2229
|
+
var isIdentifierChar3 = /* @__PURE__ */ __name((ch) => ch !== void 0 && /[A-Za-z0-9_$]/.test(ch), "isIdentifierChar");
|
|
2230
|
+
var isBlank = /* @__PURE__ */ __name((ch) => ch === " " || ch === " " || ch === "\n" || ch === "\r", "isBlank");
|
|
2231
|
+
function lineAt3(text, index) {
|
|
2232
|
+
let line = 1;
|
|
2233
|
+
for (let at = 0; at < index; at += 1) {
|
|
2234
|
+
if (text[at] === "\n") line += 1;
|
|
2235
|
+
}
|
|
2236
|
+
return line;
|
|
2237
|
+
}
|
|
2238
|
+
__name(lineAt3, "lineAt");
|
|
2239
|
+
function skipBlanks(text, from) {
|
|
2240
|
+
let at = from;
|
|
2241
|
+
while (isBlank(text[at])) at += 1;
|
|
2242
|
+
return at;
|
|
2243
|
+
}
|
|
2244
|
+
__name(skipBlanks, "skipBlanks");
|
|
2245
|
+
function closingQuote(text, open) {
|
|
2246
|
+
const quote = text[open];
|
|
2247
|
+
for (let at = open + 1; at < text.length; at += 1) {
|
|
2248
|
+
if (text[at] === "\\") {
|
|
2249
|
+
at += 1;
|
|
2250
|
+
continue;
|
|
2251
|
+
}
|
|
2252
|
+
if (text[at] === quote) return at;
|
|
2253
|
+
}
|
|
2254
|
+
return text.length;
|
|
2255
|
+
}
|
|
2256
|
+
__name(closingQuote, "closingQuote");
|
|
2257
|
+
function skipCommentOrString(text, at) {
|
|
2258
|
+
const ch = text[at];
|
|
2259
|
+
if (ch === "/" && text[at + 1] === "/") {
|
|
2260
|
+
const end = text.indexOf("\n", at);
|
|
2261
|
+
return end === -1 ? text.length : end;
|
|
2262
|
+
}
|
|
2263
|
+
if (ch === "/" && text[at + 1] === "*") {
|
|
2264
|
+
const end = text.indexOf("*/", at + 2);
|
|
2265
|
+
return end === -1 ? text.length : end + 2;
|
|
2266
|
+
}
|
|
2267
|
+
if (ch === "'" || ch === '"' || ch === "`") return closingQuote(text, at) + 1;
|
|
2268
|
+
return at;
|
|
2269
|
+
}
|
|
2270
|
+
__name(skipCommentOrString, "skipCommentOrString");
|
|
2271
|
+
function objectLiteralAt(text, open) {
|
|
2272
|
+
const members = [];
|
|
2273
|
+
let depth = 0;
|
|
2274
|
+
let expectingKey = true;
|
|
2275
|
+
let at = open + 1;
|
|
2276
|
+
while (at < text.length) {
|
|
2277
|
+
const ch = text[at];
|
|
2278
|
+
if (ch === "'" || ch === '"' || ch === "`") {
|
|
2279
|
+
const end = closingQuote(text, at);
|
|
2280
|
+
if (depth === 0 && expectingKey && text[skipBlanks(text, end + 1)] === ":") {
|
|
2281
|
+
members.push(text.slice(at + 1, end));
|
|
2282
|
+
}
|
|
2283
|
+
at = end + 1;
|
|
2284
|
+
continue;
|
|
2285
|
+
}
|
|
2286
|
+
const skipped = skipCommentOrString(text, at);
|
|
2287
|
+
if (skipped > at) {
|
|
2288
|
+
at = skipped;
|
|
2289
|
+
continue;
|
|
2290
|
+
}
|
|
2291
|
+
if (ch === "{" || ch === "[" || ch === "(") {
|
|
2292
|
+
depth += 1;
|
|
2293
|
+
at += 1;
|
|
2294
|
+
continue;
|
|
2295
|
+
}
|
|
2296
|
+
if (ch === "}" || ch === "]" || ch === ")") {
|
|
2297
|
+
if (depth === 0) return {
|
|
2298
|
+
members,
|
|
2299
|
+
end: at + 1
|
|
2300
|
+
};
|
|
2301
|
+
depth -= 1;
|
|
2302
|
+
at += 1;
|
|
2303
|
+
continue;
|
|
2304
|
+
}
|
|
2305
|
+
if (depth === 0 && ch === ":") expectingKey = false;
|
|
2306
|
+
if (depth === 0 && ch === ",") expectingKey = true;
|
|
2307
|
+
if (depth === 0 && expectingKey && ch === "." && text.startsWith("...", at)) {
|
|
2308
|
+
let end = at + 3;
|
|
2309
|
+
while (isIdentifierChar3(text[end])) end += 1;
|
|
2310
|
+
members.push(text.slice(at, end));
|
|
2311
|
+
at = end;
|
|
2312
|
+
continue;
|
|
2313
|
+
}
|
|
2314
|
+
if (depth === 0 && expectingKey && isIdentifierStart(ch)) {
|
|
2315
|
+
let end = at;
|
|
2316
|
+
while (isIdentifierChar3(text[end])) end += 1;
|
|
2317
|
+
const next = text[skipBlanks(text, end)];
|
|
2318
|
+
if (next === ":" || next === "," || next === "}") members.push(text.slice(at, end));
|
|
2319
|
+
at = end;
|
|
2320
|
+
continue;
|
|
2321
|
+
}
|
|
2322
|
+
at += 1;
|
|
2323
|
+
}
|
|
2324
|
+
return {
|
|
2325
|
+
members,
|
|
2326
|
+
end: text.length
|
|
2327
|
+
};
|
|
2328
|
+
}
|
|
2329
|
+
__name(objectLiteralAt, "objectLiteralAt");
|
|
2330
|
+
function findDbCatalogBlocks(text) {
|
|
2331
|
+
const occurrences = [];
|
|
2332
|
+
let at = 0;
|
|
2333
|
+
while (at < text.length) {
|
|
2334
|
+
const skipped = skipCommentOrString(text, at);
|
|
2335
|
+
if (skipped > at) {
|
|
2336
|
+
at = skipped;
|
|
2337
|
+
continue;
|
|
2338
|
+
}
|
|
2339
|
+
if (!text.startsWith(PROPERTY, at) || isIdentifierChar3(text[at - 1])) {
|
|
2340
|
+
at += 1;
|
|
2341
|
+
continue;
|
|
2342
|
+
}
|
|
2343
|
+
const afterName = at + PROPERTY.length;
|
|
2344
|
+
if (isIdentifierChar3(text[afterName])) {
|
|
2345
|
+
at = afterName;
|
|
2346
|
+
continue;
|
|
2347
|
+
}
|
|
2348
|
+
const colon = skipBlanks(text, afterName);
|
|
2349
|
+
if (text[colon] !== ":") {
|
|
2350
|
+
at = afterName;
|
|
2351
|
+
continue;
|
|
2352
|
+
}
|
|
2353
|
+
const value = skipBlanks(text, colon + 1);
|
|
2354
|
+
const line = lineAt3(text, at);
|
|
2355
|
+
if (text[value] !== "{") {
|
|
2356
|
+
occurrences.push({
|
|
2357
|
+
line,
|
|
2358
|
+
shape: "reference",
|
|
2359
|
+
leftovers: []
|
|
2360
|
+
});
|
|
2361
|
+
at = value;
|
|
2362
|
+
continue;
|
|
2363
|
+
}
|
|
2364
|
+
const { members, end } = objectLiteralAt(text, value);
|
|
2365
|
+
const leftovers = members.filter((member) => !TAKEN.has(member));
|
|
2366
|
+
if (!members.includes("path")) {
|
|
2367
|
+
occurrences.push({
|
|
2368
|
+
line,
|
|
2369
|
+
shape: "values",
|
|
2370
|
+
leftovers
|
|
2371
|
+
});
|
|
2372
|
+
} else if (leftovers.length > 0) {
|
|
2373
|
+
occurrences.push({
|
|
2374
|
+
line,
|
|
2375
|
+
shape: "mixed",
|
|
2376
|
+
leftovers
|
|
2377
|
+
});
|
|
2378
|
+
}
|
|
2379
|
+
at = end;
|
|
2380
|
+
}
|
|
2381
|
+
return {
|
|
2382
|
+
occurrences
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
__name(findDbCatalogBlocks, "findDbCatalogBlocks");
|
|
2386
|
+
function describeDbCatalogOccurrence(occurrence) {
|
|
2387
|
+
const { shape, leftovers } = occurrence;
|
|
2388
|
+
if (shape === "reference") return "carries something this cannot see into";
|
|
2389
|
+
if (shape === "mixed") return `names the path but still carries ${leftovers.join(", ")}`;
|
|
2390
|
+
return leftovers.length > 0 ? `carries the values: ${leftovers.join(", ")}` : "names no path";
|
|
2391
|
+
}
|
|
2392
|
+
__name(describeDbCatalogOccurrence, "describeDbCatalogOccurrence");
|
|
2393
|
+
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.";
|
|
2394
|
+
|
|
1982
2395
|
// src/init/patch-app-module.ts
|
|
1983
2396
|
var MARKER = "SaaSiCatModule.forRoot";
|
|
1984
2397
|
function patchAppModule(source, options) {
|
|
@@ -1999,7 +2412,7 @@ ${block}`;
|
|
|
1999
2412
|
return {
|
|
2000
2413
|
source,
|
|
2001
2414
|
status: "declined",
|
|
2002
|
-
reason: "no `@Module({
|
|
2415
|
+
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",
|
|
2003
2416
|
manualBlock
|
|
2004
2417
|
};
|
|
2005
2418
|
}
|
|
@@ -2078,7 +2491,7 @@ function renderForRootBlock(options) {
|
|
|
2078
2491
|
"SaaSiCatModule.forRoot(",
|
|
2079
2492
|
" defineSaaSiCat({",
|
|
2080
2493
|
" // Plans straight from the YAML. Apps that manage plans in the",
|
|
2081
|
-
" // SuperAdmin UI pass `dbCatalog` instead.",
|
|
2494
|
+
" // SuperAdmin UI pass `dbCatalog: { path: 'config/saas.yaml' }` instead.",
|
|
2082
2495
|
" planCatalog: loadPlanCatalogFromFile({ path: 'config/saas.yaml' }),",
|
|
2083
2496
|
" // Your authentication guard. This does NOT compile until you",
|
|
2084
2497
|
" // name one, and that is deliberate: an empty array is how the",
|
|
@@ -2087,6 +2500,13 @@ function renderForRootBlock(options) {
|
|
|
2087
2500
|
" // your whole capability inventory \u2014 and the manifest routes to",
|
|
2088
2501
|
" // anyone who asks. Import your guard and put it in.",
|
|
2089
2502
|
" controller: { guards: [YourAuthGuard] },",
|
|
2503
|
+
" // The modules whose providers the platform injects: the one",
|
|
2504
|
+
" // exporting PrismaService, and the one your guard depends on.",
|
|
2505
|
+
" // `prismaPersistence({ client: PrismaService })` is resolved",
|
|
2506
|
+
" // inside the platform module, which sees only what is listed",
|
|
2507
|
+
" // here or declared @Global. Same rule as the guard: this does",
|
|
2508
|
+
" // not compile until you name them.",
|
|
2509
|
+
" imports: [YourPrismaModule, YourAuthModule],",
|
|
2090
2510
|
options.persistenceImport ? " persistence," : " // persistence: prismaPersistence({ client: PrismaService }),",
|
|
2091
2511
|
" catalog: { featureUiRegistry: " + options.registry.constName + " },",
|
|
2092
2512
|
" adminResources: true,",
|
|
@@ -3188,17 +3608,23 @@ export {
|
|
|
3188
3608
|
MfaSetupFlow,
|
|
3189
3609
|
PLATFORM_DOCTOR_CHECK_PROVIDERS,
|
|
3190
3610
|
PlanCatalogDoctorCheck,
|
|
3611
|
+
SCANNED_FOR_DB_CATALOG,
|
|
3612
|
+
SCANNED_FOR_MOVED_SETTINGS,
|
|
3613
|
+
SETTINGS_THAT_MOVED,
|
|
3191
3614
|
UI_VUE_SPECIFIER,
|
|
3192
3615
|
USER_MANAGEMENT_PORT_TOKEN,
|
|
3193
3616
|
USER_PORT_TOKEN,
|
|
3194
3617
|
UserCommands,
|
|
3195
3618
|
UserPortDoctorCheck,
|
|
3619
|
+
WHERE_DB_CATALOG_GOES,
|
|
3620
|
+
WHERE_IT_GOES,
|
|
3196
3621
|
WhoAmIFlow,
|
|
3622
|
+
appKeyPattern,
|
|
3197
3623
|
appendConstraints,
|
|
3198
3624
|
applyFragmentBlocks,
|
|
3199
3625
|
applyTokens,
|
|
3200
3626
|
assertModelsExist,
|
|
3201
|
-
|
|
3627
|
+
assertValidAppKey,
|
|
3202
3628
|
assertValidQuotaKey,
|
|
3203
3629
|
blankStringLiterals,
|
|
3204
3630
|
blockBodyLines,
|
|
@@ -3206,17 +3632,24 @@ export {
|
|
|
3206
3632
|
buildImportMap,
|
|
3207
3633
|
checkSchema,
|
|
3208
3634
|
constraintsFor,
|
|
3635
|
+
describeDbCatalogOccurrence,
|
|
3209
3636
|
enableFkPointers,
|
|
3210
3637
|
extractBlockNames,
|
|
3211
3638
|
extractBlocks,
|
|
3639
|
+
extractEnumBlocks,
|
|
3640
|
+
extractEnumNames,
|
|
3641
|
+
extractFragmentBlocks,
|
|
3212
3642
|
extractModelBlocks,
|
|
3213
3643
|
extractModelNames,
|
|
3644
|
+
findDbCatalogBlocks,
|
|
3214
3645
|
findFkPointers,
|
|
3646
|
+
findMovedSettings,
|
|
3215
3647
|
foreignKeyOf,
|
|
3216
3648
|
hasBackRelation,
|
|
3217
3649
|
hasConstraints,
|
|
3218
3650
|
isNoLongerPublic,
|
|
3219
3651
|
isOneToOne,
|
|
3652
|
+
judgeModuleResolution,
|
|
3220
3653
|
kebabCase,
|
|
3221
3654
|
migrationCreatedBy,
|
|
3222
3655
|
minimumQuotasPerPlan,
|
|
@@ -3230,14 +3663,16 @@ export {
|
|
|
3230
3663
|
patchAppModule,
|
|
3231
3664
|
patchOptionsFor,
|
|
3232
3665
|
planInit,
|
|
3233
|
-
projectKeyPattern,
|
|
3234
3666
|
quotaKeyPattern,
|
|
3667
|
+
readEffectiveModuleResolution,
|
|
3235
3668
|
relationNameOf,
|
|
3669
|
+
removeProjectKey,
|
|
3236
3670
|
reportConstraints,
|
|
3237
3671
|
rewriteImports,
|
|
3238
3672
|
rewriteManifest,
|
|
3239
3673
|
rewriteNames,
|
|
3240
3674
|
rewriteSubpath,
|
|
3675
|
+
settingsWrittenTo,
|
|
3241
3676
|
stripLineComment,
|
|
3242
3677
|
structuralOnly,
|
|
3243
3678
|
tablesAddressedBy
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saasicat/cli",
|
|
3
|
-
"version": "1.0.0-rc.
|
|
3
|
+
"version": "1.0.0-rc.10",
|
|
4
4
|
"description": "CLI helpers for SaaS platform consumers. Provides CliContextService (identity, MFA, production confirm, audit tag).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"types": "./dist/index.d.cts",
|
|
17
17
|
"default": "./dist/index.cjs"
|
|
18
18
|
}
|
|
19
|
-
}
|
|
19
|
+
},
|
|
20
|
+
"./package.json": "./package.json"
|
|
20
21
|
},
|
|
21
22
|
"files": [
|
|
22
23
|
"dist",
|
|
@@ -29,9 +30,9 @@
|
|
|
29
30
|
},
|
|
30
31
|
"dependencies": {
|
|
31
32
|
"qrcode-terminal": "^0.12.0",
|
|
32
|
-
"@saasicat/nest": "^1.0.0-rc.
|
|
33
|
-
"@saasicat/spec": "^1.0.0-rc.
|
|
34
|
-
"@saasicat/core": "^1.0.0-rc.
|
|
33
|
+
"@saasicat/nest": "^1.0.0-rc.10",
|
|
34
|
+
"@saasicat/spec": "^1.0.0-rc.10",
|
|
35
|
+
"@saasicat/core": "^1.0.0-rc.10"
|
|
35
36
|
},
|
|
36
37
|
"peerDependencies": {
|
|
37
38
|
"@nestjs/common": "^11.0.0",
|