@saasicat/cli 1.0.0-rc.0 → 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 +335 -34
- package/codemods/v1-rename.map.json +6 -0
- package/dist/.build-stamp +1 -1
- package/dist/index.cjs +564 -30
- package/dist/index.d.cts +246 -14
- package/dist/index.d.ts +246 -14
- package/dist/index.js +542 -24
- package/package.json +7 -6
- package/templates/init/config/saas.yaml.tpl +40 -2
- package/templates/init/src/auth/password.hasher.ts.tpl +1 -1
- package/templates/init/src/saas/admin-manifest.contribution.ts.tpl +2 -2
- package/templates/init/src/saas/feature-ui-registry.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/templates/init/src/saas/quota.provider.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/",
|
|
@@ -1881,6 +1973,13 @@ function rewriteNames(text, table) {
|
|
|
1881
1973
|
return `${quote}${to}`;
|
|
1882
1974
|
});
|
|
1883
1975
|
}
|
|
1976
|
+
for (const [from, to] of Object.entries(table.packages ?? {})) {
|
|
1977
|
+
if (from === "_") continue;
|
|
1978
|
+
next = next.replace(new RegExp(`${escape(from)}(?=['"\`/])`, "g"), () => {
|
|
1979
|
+
rewritten += 1;
|
|
1980
|
+
return to;
|
|
1981
|
+
});
|
|
1982
|
+
}
|
|
1884
1983
|
for (const [from, to] of Object.entries(table.subpaths)) {
|
|
1885
1984
|
next = next.replace(new RegExp(escape(from), "g"), () => {
|
|
1886
1985
|
rewritten += 1;
|
|
@@ -1896,6 +1995,402 @@ function rewriteNames(text, table) {
|
|
|
1896
1995
|
};
|
|
1897
1996
|
}
|
|
1898
1997
|
__name(rewriteNames, "rewriteNames");
|
|
1998
|
+
var DEPENDENCY_FIELDS = [
|
|
1999
|
+
"dependencies",
|
|
2000
|
+
"devDependencies",
|
|
2001
|
+
"peerDependencies",
|
|
2002
|
+
"optionalDependencies"
|
|
2003
|
+
];
|
|
2004
|
+
var UNTRANSLATABLE_RANGE = /^(workspace:|file:|link:|npm:|git\+|https?:)/;
|
|
2005
|
+
function rewriteManifest(text, table, options) {
|
|
2006
|
+
const renames = Object.entries(table.packages ?? {}).filter(([from]) => from !== "_");
|
|
2007
|
+
const ambiguous = [];
|
|
2008
|
+
let manifest;
|
|
2009
|
+
try {
|
|
2010
|
+
manifest = JSON.parse(text);
|
|
2011
|
+
} catch {
|
|
2012
|
+
return {
|
|
2013
|
+
text,
|
|
2014
|
+
rewritten: 0,
|
|
2015
|
+
ambiguous
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
let rewritten = 0;
|
|
2019
|
+
for (const field of DEPENDENCY_FIELDS) {
|
|
2020
|
+
const deps = manifest[field];
|
|
2021
|
+
if (!deps || typeof deps !== "object") continue;
|
|
2022
|
+
const entries = Object.entries(deps);
|
|
2023
|
+
const renamed = entries.map(([name, range]) => {
|
|
2024
|
+
const to = renames.find(([from]) => from === name)?.[1];
|
|
2025
|
+
if (!to) return [
|
|
2026
|
+
name,
|
|
2027
|
+
range
|
|
2028
|
+
];
|
|
2029
|
+
if (UNTRANSLATABLE_RANGE.test(range)) {
|
|
2030
|
+
ambiguous.push(`${name} in ${field} (${range})`);
|
|
2031
|
+
return [
|
|
2032
|
+
name,
|
|
2033
|
+
range
|
|
2034
|
+
];
|
|
2035
|
+
}
|
|
2036
|
+
rewritten += 1;
|
|
2037
|
+
return [
|
|
2038
|
+
to,
|
|
2039
|
+
options.targetRange
|
|
2040
|
+
];
|
|
2041
|
+
});
|
|
2042
|
+
manifest[field] = Object.fromEntries(renamed);
|
|
2043
|
+
}
|
|
2044
|
+
const meta = manifest.peerDependenciesMeta;
|
|
2045
|
+
if (meta && typeof meta === "object") {
|
|
2046
|
+
manifest.peerDependenciesMeta = Object.fromEntries(Object.entries(meta).map(([name, flags]) => {
|
|
2047
|
+
const to = renames.find(([from]) => from === name)?.[1];
|
|
2048
|
+
if (!to) return [
|
|
2049
|
+
name,
|
|
2050
|
+
flags
|
|
2051
|
+
];
|
|
2052
|
+
rewritten += 1;
|
|
2053
|
+
return [
|
|
2054
|
+
to,
|
|
2055
|
+
flags
|
|
2056
|
+
];
|
|
2057
|
+
}));
|
|
2058
|
+
}
|
|
2059
|
+
if (rewritten === 0) return {
|
|
2060
|
+
text,
|
|
2061
|
+
rewritten: 0,
|
|
2062
|
+
ambiguous
|
|
2063
|
+
};
|
|
2064
|
+
const indent = /^[ \t]+/m.exec(text)?.[0] ?? " ";
|
|
2065
|
+
const trailing = text.endsWith("\n") ? "\n" : "";
|
|
2066
|
+
return {
|
|
2067
|
+
text: JSON.stringify(manifest, null, indent) + trailing,
|
|
2068
|
+
rewritten,
|
|
2069
|
+
ambiguous
|
|
2070
|
+
};
|
|
2071
|
+
}
|
|
2072
|
+
__name(rewriteManifest, "rewriteManifest");
|
|
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.";
|
|
1899
2394
|
|
|
1900
2395
|
// src/init/patch-app-module.ts
|
|
1901
2396
|
var MARKER = "SaaSiCatModule.forRoot";
|
|
@@ -1917,7 +2412,7 @@ ${block}`;
|
|
|
1917
2412
|
return {
|
|
1918
2413
|
source,
|
|
1919
2414
|
status: "declined",
|
|
1920
|
-
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",
|
|
1921
2416
|
manualBlock
|
|
1922
2417
|
};
|
|
1923
2418
|
}
|
|
@@ -1996,7 +2491,7 @@ function renderForRootBlock(options) {
|
|
|
1996
2491
|
"SaaSiCatModule.forRoot(",
|
|
1997
2492
|
" defineSaaSiCat({",
|
|
1998
2493
|
" // Plans straight from the YAML. Apps that manage plans in the",
|
|
1999
|
-
" // SuperAdmin UI pass `dbCatalog` instead.",
|
|
2494
|
+
" // SuperAdmin UI pass `dbCatalog: { path: 'config/saas.yaml' }` instead.",
|
|
2000
2495
|
" planCatalog: loadPlanCatalogFromFile({ path: 'config/saas.yaml' }),",
|
|
2001
2496
|
" // Your authentication guard. This does NOT compile until you",
|
|
2002
2497
|
" // name one, and that is deliberate: an empty array is how the",
|
|
@@ -2005,6 +2500,13 @@ function renderForRootBlock(options) {
|
|
|
2005
2500
|
" // your whole capability inventory \u2014 and the manifest routes to",
|
|
2006
2501
|
" // anyone who asks. Import your guard and put it in.",
|
|
2007
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],",
|
|
2008
2510
|
options.persistenceImport ? " persistence," : " // persistence: prismaPersistence({ client: PrismaService }),",
|
|
2009
2511
|
" catalog: { featureUiRegistry: " + options.registry.constName + " },",
|
|
2010
2512
|
" adminResources: true,",
|
|
@@ -3106,17 +3608,23 @@ export {
|
|
|
3106
3608
|
MfaSetupFlow,
|
|
3107
3609
|
PLATFORM_DOCTOR_CHECK_PROVIDERS,
|
|
3108
3610
|
PlanCatalogDoctorCheck,
|
|
3611
|
+
SCANNED_FOR_DB_CATALOG,
|
|
3612
|
+
SCANNED_FOR_MOVED_SETTINGS,
|
|
3613
|
+
SETTINGS_THAT_MOVED,
|
|
3109
3614
|
UI_VUE_SPECIFIER,
|
|
3110
3615
|
USER_MANAGEMENT_PORT_TOKEN,
|
|
3111
3616
|
USER_PORT_TOKEN,
|
|
3112
3617
|
UserCommands,
|
|
3113
3618
|
UserPortDoctorCheck,
|
|
3619
|
+
WHERE_DB_CATALOG_GOES,
|
|
3620
|
+
WHERE_IT_GOES,
|
|
3114
3621
|
WhoAmIFlow,
|
|
3622
|
+
appKeyPattern,
|
|
3115
3623
|
appendConstraints,
|
|
3116
3624
|
applyFragmentBlocks,
|
|
3117
3625
|
applyTokens,
|
|
3118
3626
|
assertModelsExist,
|
|
3119
|
-
|
|
3627
|
+
assertValidAppKey,
|
|
3120
3628
|
assertValidQuotaKey,
|
|
3121
3629
|
blankStringLiterals,
|
|
3122
3630
|
blockBodyLines,
|
|
@@ -3124,17 +3632,24 @@ export {
|
|
|
3124
3632
|
buildImportMap,
|
|
3125
3633
|
checkSchema,
|
|
3126
3634
|
constraintsFor,
|
|
3635
|
+
describeDbCatalogOccurrence,
|
|
3127
3636
|
enableFkPointers,
|
|
3128
3637
|
extractBlockNames,
|
|
3129
3638
|
extractBlocks,
|
|
3639
|
+
extractEnumBlocks,
|
|
3640
|
+
extractEnumNames,
|
|
3641
|
+
extractFragmentBlocks,
|
|
3130
3642
|
extractModelBlocks,
|
|
3131
3643
|
extractModelNames,
|
|
3644
|
+
findDbCatalogBlocks,
|
|
3132
3645
|
findFkPointers,
|
|
3646
|
+
findMovedSettings,
|
|
3133
3647
|
foreignKeyOf,
|
|
3134
3648
|
hasBackRelation,
|
|
3135
3649
|
hasConstraints,
|
|
3136
3650
|
isNoLongerPublic,
|
|
3137
3651
|
isOneToOne,
|
|
3652
|
+
judgeModuleResolution,
|
|
3138
3653
|
kebabCase,
|
|
3139
3654
|
migrationCreatedBy,
|
|
3140
3655
|
minimumQuotasPerPlan,
|
|
@@ -3148,13 +3663,16 @@ export {
|
|
|
3148
3663
|
patchAppModule,
|
|
3149
3664
|
patchOptionsFor,
|
|
3150
3665
|
planInit,
|
|
3151
|
-
projectKeyPattern,
|
|
3152
3666
|
quotaKeyPattern,
|
|
3667
|
+
readEffectiveModuleResolution,
|
|
3153
3668
|
relationNameOf,
|
|
3669
|
+
removeProjectKey,
|
|
3154
3670
|
reportConstraints,
|
|
3155
3671
|
rewriteImports,
|
|
3672
|
+
rewriteManifest,
|
|
3156
3673
|
rewriteNames,
|
|
3157
3674
|
rewriteSubpath,
|
|
3675
|
+
settingsWrittenTo,
|
|
3158
3676
|
stripLineComment,
|
|
3159
3677
|
structuralOnly,
|
|
3160
3678
|
tablesAddressedBy
|