@saasicat/cli 1.0.0-rc.5 → 1.0.0-rc.7
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 +1 -1
- package/bin/saasicat.js +104 -19
- package/dist/.build-stamp +1 -1
- package/dist/index.cjs +136 -21
- package/dist/index.d.cts +26 -7
- package/dist/index.d.ts +26 -7
- package/dist/index.js +133 -19
- package/package.json +4 -4
- package/templates/init/config/saas.yaml.tpl +0 -1
- package/templates/init/src/saas/admin-manifest.contribution.ts.tpl +1 -1
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ import { PrismaUserPortAdapter } from './adapters/prisma-user-port';
|
|
|
43
43
|
imports: [
|
|
44
44
|
PrismaModule,
|
|
45
45
|
PlanCatalogModule.forRoot({
|
|
46
|
-
|
|
46
|
+
app: { name: 'MyApp' },
|
|
47
47
|
currency: 'EUR',
|
|
48
48
|
vatRate: 19,
|
|
49
49
|
// The catalogue is read from the database, not from a file — the
|
package/bin/saasicat.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// `saasicat` — bootstrap CLI for the SaaSiCat framework.
|
|
3
3
|
// naming-history: the codemod help below names the pre-1.0 spellings it rewrites.
|
|
4
|
+
// project-key-history: `codemod v1-project-key` is named after what it removes.
|
|
4
5
|
//
|
|
5
6
|
// Sub-commands:
|
|
6
7
|
// schema apply [--prisma-schema=PATH] [--fragments=01,02,03]
|
|
@@ -16,10 +17,11 @@
|
|
|
16
17
|
// apply --all, then `prisma migrate dev`, then append the constraints
|
|
17
18
|
// Prisma's DSL cannot express to the migration it just wrote.
|
|
18
19
|
//
|
|
19
|
-
// init --
|
|
20
|
+
// init --app-key=X --quota=key:Model [--quota=…]... [--app-name=X] [--api-base=X]
|
|
20
21
|
// codemod v1-imports [--dir=X] [--dry-run]
|
|
21
22
|
// codemod v1-rename [--dir=X] [--dry-run]
|
|
22
|
-
// codemod v1
|
|
23
|
+
// codemod v1-project-key [--dir=X] [--dry-run]
|
|
24
|
+
// codemod v1 [--dir=X] [--dry-run] — all three, in that order
|
|
23
25
|
// [--skip-hasher] [--dry-run] [--dir=.]
|
|
24
26
|
// Writes the platform wiring — config, persistence, manifest
|
|
25
27
|
// contribution, admin module, one provider per quota — and adds
|
|
@@ -27,7 +29,7 @@
|
|
|
27
29
|
|
|
28
30
|
import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises';
|
|
29
31
|
import { existsSync } from 'node:fs';
|
|
30
|
-
import { basename, dirname, join, resolve } from 'node:path';
|
|
32
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
31
33
|
import { createRequire } from 'node:module';
|
|
32
34
|
import { fileURLToPath } from 'node:url';
|
|
33
35
|
import { spawn } from 'node:child_process';
|
|
@@ -46,7 +48,7 @@ import {
|
|
|
46
48
|
extractFragmentBlocks,
|
|
47
49
|
findFkPointers,
|
|
48
50
|
hasConstraints,
|
|
49
|
-
|
|
51
|
+
assertValidAppKey,
|
|
50
52
|
judgeModuleResolution,
|
|
51
53
|
readEffectiveModuleResolution,
|
|
52
54
|
buildImportMap,
|
|
@@ -54,6 +56,7 @@ import {
|
|
|
54
56
|
rewriteImports,
|
|
55
57
|
rewriteManifest,
|
|
56
58
|
rewriteNames,
|
|
59
|
+
removeProjectKey,
|
|
57
60
|
reportConstraints,
|
|
58
61
|
patchAppModule,
|
|
59
62
|
patchOptionsFor,
|
|
@@ -71,7 +74,7 @@ const require_ = createRequire(import.meta.url);
|
|
|
71
74
|
// `pascalCase` and came back as `value.replace is not a function` with exit 99
|
|
72
75
|
// — an internal error for what is an ordinary typo.
|
|
73
76
|
const VALUE_FLAGS = new Set([
|
|
74
|
-
'
|
|
77
|
+
'app-key',
|
|
75
78
|
'app-name',
|
|
76
79
|
'api-base',
|
|
77
80
|
'quota',
|
|
@@ -678,11 +681,32 @@ function codemodTable(name) {
|
|
|
678
681
|
const CODEMOD_SKIP = new Set(['node_modules', '.git', '.output', 'coverage']);
|
|
679
682
|
const isBuildOutput = (name) => name === 'dist' || name.startsWith('dist-');
|
|
680
683
|
const CODEMOD_EXTENSIONS = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|vue|md)$/;
|
|
684
|
+
/**
|
|
685
|
+
* Widens the walk for `v1-project-key` alone — see `walkSources`.
|
|
686
|
+
*
|
|
687
|
+
* `.prisma` belongs here for the reason `.yaml` does: a consumer copies the
|
|
688
|
+
* platform's models into their own `schema.prisma`, which is the documented
|
|
689
|
+
* integration path, so that file carries `projectKey` fields and composite
|
|
690
|
+
* indexes of its own. Left out of the walk they were neither rewritten nor
|
|
691
|
+
* reported — and after the SQL migration drops the columns, a schema still
|
|
692
|
+
* declaring them generates a client that queries them, while the next
|
|
693
|
+
* `db push` tries to put them back.
|
|
694
|
+
*
|
|
695
|
+
* Reported rather than rewritten, like every other declaration: a `.prisma`
|
|
696
|
+
* model is a schema, and which of its fields a consumer still needs is theirs.
|
|
697
|
+
*/
|
|
698
|
+
const CODEMOD_CONFIG_EXTENSIONS = /\.(yaml|yml|prisma)$/;
|
|
681
699
|
/** Walked for the package renames alone; see `rewriteManifest`. */
|
|
682
700
|
const CODEMOD_MANIFEST = 'package.json';
|
|
683
701
|
|
|
684
|
-
/**
|
|
685
|
-
|
|
702
|
+
/**
|
|
703
|
+
* Every source file under `root` a codemod may touch, with its text.
|
|
704
|
+
*
|
|
705
|
+
* `extra` widens the set for one codemod. Only `v1-project-key` passes it, and
|
|
706
|
+
* only for `.yaml`: the other two rewrite identifiers and import specifiers,
|
|
707
|
+
* and letting them loose on a configuration file would corrupt it.
|
|
708
|
+
*/
|
|
709
|
+
async function walkSources(root, visit, extra = null) {
|
|
686
710
|
const walk = async (dir) => {
|
|
687
711
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
688
712
|
if (CODEMOD_SKIP.has(entry.name) || isBuildOutput(entry.name)) continue;
|
|
@@ -691,13 +715,70 @@ async function walkSources(root, visit) {
|
|
|
691
715
|
await walk(full);
|
|
692
716
|
continue;
|
|
693
717
|
}
|
|
694
|
-
|
|
718
|
+
const included =
|
|
719
|
+
CODEMOD_EXTENSIONS.test(entry.name) ||
|
|
720
|
+
entry.name === CODEMOD_MANIFEST ||
|
|
721
|
+
(extra !== null && extra.test(entry.name));
|
|
722
|
+
if (!included) continue;
|
|
695
723
|
await visit(full, await readFile(full, 'utf8'));
|
|
696
724
|
}
|
|
697
725
|
};
|
|
698
726
|
await walk(root);
|
|
699
727
|
}
|
|
700
728
|
|
|
729
|
+
/**
|
|
730
|
+
* Takes `projectKey` out of a consumer's code, and names what it will not.
|
|
731
|
+
*
|
|
732
|
+
* A removal is not a rename: the word is an ordinary property name. Two forms
|
|
733
|
+
* need no grammar to decide — a `?projectKey=` on a `/catalog/` URL, and the
|
|
734
|
+
* top-level key of a `saas.yaml` — and those are rewritten. An object member is
|
|
735
|
+
* reported: an object literal and a type literal are lexically identical in
|
|
736
|
+
* TypeScript, so removing one would sometimes delete a member of the consumer's
|
|
737
|
+
* own type. The migration guide's table says what each shape becomes.
|
|
738
|
+
*/
|
|
739
|
+
async function cmdCodemodV1ProjectKey(args) {
|
|
740
|
+
const root = resolve(args.dir ?? '.');
|
|
741
|
+
const dryRun = args['dry-run'] === true;
|
|
742
|
+
|
|
743
|
+
const undecided = [];
|
|
744
|
+
let rewritten = 0;
|
|
745
|
+
let touched = 0;
|
|
746
|
+
await walkSources(
|
|
747
|
+
root,
|
|
748
|
+
async (full, source) => {
|
|
749
|
+
if (basename(full) === CODEMOD_MANIFEST) return;
|
|
750
|
+
const result = removeProjectKey(source, isCatalogConfig(full) ? 'yaml' : 'source');
|
|
751
|
+
for (const line of result.undecided) undecided.push(`${relative(root, full)}:${line}`);
|
|
752
|
+
if (result.rewritten === 0) return;
|
|
753
|
+
if (!dryRun) await writeFile(full, result.text);
|
|
754
|
+
rewritten += result.rewritten;
|
|
755
|
+
touched += 1;
|
|
756
|
+
},
|
|
757
|
+
CODEMOD_CONFIG_EXTENSIONS,
|
|
758
|
+
);
|
|
759
|
+
|
|
760
|
+
console.log(
|
|
761
|
+
`${dryRun ? 'Would remove' : 'Removed'} ${rewritten} occurrence(s) in ${touched} file(s).`,
|
|
762
|
+
);
|
|
763
|
+
if (undecided.length === 0) return;
|
|
764
|
+
|
|
765
|
+
console.log('');
|
|
766
|
+
console.log(`${undecided.length} occurrence(s) are yours to look at:`);
|
|
767
|
+
for (const where of undecided) console.log(` ${where}`);
|
|
768
|
+
console.log('');
|
|
769
|
+
console.log(' Two kinds of file end up here. In TypeScript an object literal and a type');
|
|
770
|
+
console.log(' literal are the same tokens, so this cannot tell a payload member from one');
|
|
771
|
+
console.log(' of your own declarations without parsing. And a `.prisma` model is a schema:');
|
|
772
|
+
console.log(" which of its fields you still need is yours to say, not this tool's.");
|
|
773
|
+
console.log(' It reports rather than guesses — docs/guides/upgrade-to-1.0.md has a table');
|
|
774
|
+
console.log(' of what each shape becomes.');
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/** A `saas.yaml`, whose top-level `projectKey:` needs no anchor. */
|
|
778
|
+
function isCatalogConfig(full) {
|
|
779
|
+
return basename(full) === 'saas.yaml' || basename(full) === 'saas.yml';
|
|
780
|
+
}
|
|
781
|
+
|
|
701
782
|
/** Reads a repeated flag (`--quota=a --quota=b`) off argv. */
|
|
702
783
|
function repeatedFlag(argv, name) {
|
|
703
784
|
return argv
|
|
@@ -706,17 +787,17 @@ function repeatedFlag(argv, name) {
|
|
|
706
787
|
}
|
|
707
788
|
|
|
708
789
|
async function cmdInit(args, argv) {
|
|
709
|
-
if (!args['
|
|
710
|
-
console.error('✗ --
|
|
711
|
-
console.error(' It
|
|
790
|
+
if (!args['app-key']) {
|
|
791
|
+
console.error('✗ --app-key=<key> is required.');
|
|
792
|
+
console.error(' It is the slug of this application: npm package name, storage prefix.');
|
|
712
793
|
process.exit(1);
|
|
713
794
|
}
|
|
714
795
|
|
|
715
796
|
// A usage error, so it exits 1 like every other one here rather than
|
|
716
|
-
// through the top-level handler's 99. The rule itself
|
|
717
|
-
//
|
|
797
|
+
// through the top-level handler's 99. The rule itself is in
|
|
798
|
+
// src/init/catalog-keys.ts.
|
|
718
799
|
try {
|
|
719
|
-
|
|
800
|
+
assertValidAppKey(args['app-key']);
|
|
720
801
|
} catch (err) {
|
|
721
802
|
console.error(`✗ ${err.message}`);
|
|
722
803
|
process.exit(1);
|
|
@@ -741,7 +822,7 @@ async function cmdInit(args, argv) {
|
|
|
741
822
|
}
|
|
742
823
|
|
|
743
824
|
const plan = planInit({
|
|
744
|
-
|
|
825
|
+
appKey: args['app-key'],
|
|
745
826
|
appName: args['app-name'],
|
|
746
827
|
apiBase: args['api-base'],
|
|
747
828
|
quotas: repeatedFlag(argv, 'quota'),
|
|
@@ -881,11 +962,15 @@ async function main() {
|
|
|
881
962
|
if (cmd === 'codemod' && sub === 'v1-rename') {
|
|
882
963
|
return cmdCodemodV1Rename(parseArgs(rest));
|
|
883
964
|
}
|
|
965
|
+
if (cmd === 'codemod' && sub === 'v1-project-key') {
|
|
966
|
+
return cmdCodemodV1ProjectKey(parseArgs(rest));
|
|
967
|
+
}
|
|
884
968
|
if (cmd === 'codemod' && sub === 'v1') {
|
|
885
969
|
// Imports first: the rename table keys its per-entry tokens by the
|
|
886
970
|
// specifier they are imported from, which the import rewrite settles.
|
|
887
971
|
await cmdCodemodV1Imports(parseArgs(rest));
|
|
888
|
-
|
|
972
|
+
await cmdCodemodV1Rename(parseArgs(rest));
|
|
973
|
+
return cmdCodemodV1ProjectKey(parseArgs(rest));
|
|
889
974
|
}
|
|
890
975
|
if (cmd === 'init') {
|
|
891
976
|
return cmdInit(parseArgs([sub, ...rest].filter(Boolean)), process.argv.slice(3));
|
|
@@ -914,13 +999,13 @@ async function main() {
|
|
|
914
999
|
' schema migrate --name=<name> apply --all + migrate dev + constraints',
|
|
915
1000
|
);
|
|
916
1001
|
console.log('');
|
|
917
|
-
console.log(' init --
|
|
1002
|
+
console.log(' init --app-key=<key> --quota=<key>:<Model>');
|
|
918
1003
|
console.log(' scaffold the platform wiring. At least one --quota:');
|
|
919
1004
|
console.log(' every plan must declare one, or the catalogue does not load.');
|
|
920
|
-
console.log(' init --
|
|
1005
|
+
console.log(' init --app-key=myapp --quota=notes:Note --quota=seats:Seat');
|
|
921
1006
|
console.log('');
|
|
922
1007
|
console.log(' codemod v1 [--dir=.] [--dry-run]');
|
|
923
|
-
console.log(' the whole 1.0 migration:
|
|
1008
|
+
console.log(' the whole 1.0 migration: imports, names, then projectKey');
|
|
924
1009
|
console.log(' codemod v1-imports [--dir=.] [--dry-run]');
|
|
925
1010
|
console.log(' rewrite @saasicat/ui-vue imports to the 1.0 export map');
|
|
926
1011
|
console.log(' codemod v1-rename [--dir=.] [--dry-run]');
|
package/dist/.build-stamp
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
e08122ad3833db9d96fed2edad527604182ab8047a8d8afefec4239ec29fe8f8
|
package/dist/index.cjs
CHANGED
|
@@ -70,11 +70,12 @@ __export(index_exports, {
|
|
|
70
70
|
UserCommands: () => UserCommands,
|
|
71
71
|
UserPortDoctorCheck: () => UserPortDoctorCheck,
|
|
72
72
|
WhoAmIFlow: () => WhoAmIFlow,
|
|
73
|
+
appKeyPattern: () => appKeyPattern,
|
|
73
74
|
appendConstraints: () => appendConstraints,
|
|
74
75
|
applyFragmentBlocks: () => applyFragmentBlocks,
|
|
75
76
|
applyTokens: () => applyTokens,
|
|
76
77
|
assertModelsExist: () => assertModelsExist,
|
|
77
|
-
|
|
78
|
+
assertValidAppKey: () => assertValidAppKey,
|
|
78
79
|
assertValidQuotaKey: () => assertValidQuotaKey,
|
|
79
80
|
blankStringLiterals: () => blankStringLiterals,
|
|
80
81
|
blockBodyLines: () => blockBodyLines,
|
|
@@ -110,10 +111,10 @@ __export(index_exports, {
|
|
|
110
111
|
patchAppModule: () => patchAppModule,
|
|
111
112
|
patchOptionsFor: () => patchOptionsFor,
|
|
112
113
|
planInit: () => planInit,
|
|
113
|
-
projectKeyPattern: () => projectKeyPattern,
|
|
114
114
|
quotaKeyPattern: () => quotaKeyPattern,
|
|
115
115
|
readEffectiveModuleResolution: () => readEffectiveModuleResolution,
|
|
116
116
|
relationNameOf: () => relationNameOf,
|
|
117
|
+
removeProjectKey: () => removeProjectKey,
|
|
117
118
|
reportConstraints: () => reportConstraints,
|
|
118
119
|
rewriteImports: () => rewriteImports,
|
|
119
120
|
rewriteManifest: () => rewriteManifest,
|
|
@@ -1033,7 +1034,7 @@ var PlanCatalogDoctorCheck = class {
|
|
|
1033
1034
|
severity: "ok",
|
|
1034
1035
|
message: `${plans.length} plan(s), ${this.catalog.features?.length ?? 0} feature(s) loaded.`,
|
|
1035
1036
|
details: {
|
|
1036
|
-
|
|
1037
|
+
app: this.catalog.app.name,
|
|
1037
1038
|
planIds: plans.map((p) => p.id)
|
|
1038
1039
|
}
|
|
1039
1040
|
};
|
|
@@ -1705,10 +1706,11 @@ function required(value, what) {
|
|
|
1705
1706
|
return value;
|
|
1706
1707
|
}
|
|
1707
1708
|
__name(required, "required");
|
|
1708
|
-
|
|
1709
|
-
|
|
1709
|
+
var APP_KEY_PATTERN = /^[a-z][a-z0-9-]{1,30}$/;
|
|
1710
|
+
function appKeyPattern() {
|
|
1711
|
+
return APP_KEY_PATTERN;
|
|
1710
1712
|
}
|
|
1711
|
-
__name(
|
|
1713
|
+
__name(appKeyPattern, "appKeyPattern");
|
|
1712
1714
|
function quotaKeyPattern() {
|
|
1713
1715
|
const patterns = Object.keys(required(schema.$defs?.PlanDef?.properties?.quotas?.patternProperties, "quota key pattern"));
|
|
1714
1716
|
if (patterns.length !== 1) {
|
|
@@ -1721,12 +1723,12 @@ function minimumQuotasPerPlan() {
|
|
|
1721
1723
|
return required(schema.$defs?.PlanDef?.properties?.quotas?.minProperties, "minProperties");
|
|
1722
1724
|
}
|
|
1723
1725
|
__name(minimumQuotasPerPlan, "minimumQuotasPerPlan");
|
|
1724
|
-
function
|
|
1725
|
-
const pattern =
|
|
1726
|
-
if (pattern.test(
|
|
1727
|
-
throw new Error(`--
|
|
1726
|
+
function assertValidAppKey(appKey) {
|
|
1727
|
+
const pattern = appKeyPattern();
|
|
1728
|
+
if (pattern.test(appKey)) return;
|
|
1729
|
+
throw new Error(`--app-key=${appKey} is not a valid app key. It has to match ${pattern.source} \u2014 lower case, starting with a letter, at least two characters. It becomes an npm package name and a browser storage prefix, so this would fail after every file was written.`);
|
|
1728
1730
|
}
|
|
1729
|
-
__name(
|
|
1731
|
+
__name(assertValidAppKey, "assertValidAppKey");
|
|
1730
1732
|
function assertValidQuotaKey(quotaKey) {
|
|
1731
1733
|
const pattern = quotaKeyPattern();
|
|
1732
1734
|
if (pattern.test(quotaKey)) return;
|
|
@@ -1758,24 +1760,24 @@ function pascalCase(value) {
|
|
|
1758
1760
|
__name(pascalCase, "pascalCase");
|
|
1759
1761
|
var quotaFileName = /* @__PURE__ */ __name((key) => `${key.replace(/[^A-Za-z0-9]+/g, "-").toLowerCase()}-quota.provider.ts`, "quotaFileName");
|
|
1760
1762
|
function planInit(options) {
|
|
1761
|
-
const
|
|
1762
|
-
if (!
|
|
1763
|
-
|
|
1764
|
-
const appLabel = options.appName ?? pascalCase(
|
|
1763
|
+
const appKey = options.appKey;
|
|
1764
|
+
if (!appKey) throw new Error("init needs an --app-key.");
|
|
1765
|
+
assertValidAppKey(appKey);
|
|
1766
|
+
const appLabel = options.appName ?? pascalCase(appKey);
|
|
1765
1767
|
const appName = pascalCase(appLabel);
|
|
1766
1768
|
const apiBase = options.apiBase ?? "/api/v1/admin";
|
|
1767
1769
|
const quotas = (options.quotas ?? []).map(parseQuota);
|
|
1768
1770
|
assertEnoughQuotas(quotas);
|
|
1769
1771
|
const hasherClass = options.skipHasher ? null : `${appName}PasswordHasher`;
|
|
1770
|
-
const featureKey = `${
|
|
1772
|
+
const featureKey = `${appKey.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase()}_CORE`;
|
|
1771
1773
|
const shared = {
|
|
1772
|
-
|
|
1774
|
+
APP_KEY: appKey,
|
|
1773
1775
|
APP_NAME: appName,
|
|
1774
1776
|
APP_LABEL: appLabel,
|
|
1775
1777
|
API_BASE: apiBase,
|
|
1776
1778
|
FEATURE_KEY: featureKey,
|
|
1777
|
-
REGISTRY_CONST: `${constantCase(
|
|
1778
|
-
MANIFEST_CONST: `${constantCase(
|
|
1779
|
+
REGISTRY_CONST: `${constantCase(appKey)}_FEATURE_UI_REGISTRY`,
|
|
1780
|
+
MANIFEST_CONST: `${constantCase(appKey)}_MANIFEST_CONTRIBUTION`,
|
|
1779
1781
|
ADMIN_MODULE_CLASS: `${appName}AdminModule`,
|
|
1780
1782
|
HASHER_CLASS: hasherClass ?? "",
|
|
1781
1783
|
HASHER_FILE: hasherClass ? `${kebabCase(appName)}-password.hasher` : "",
|
|
@@ -2169,6 +2171,118 @@ function rewriteManifest(text, table, options) {
|
|
|
2169
2171
|
}
|
|
2170
2172
|
__name(rewriteManifest, "rewriteManifest");
|
|
2171
2173
|
|
|
2174
|
+
// src/codemods/v1-project-key.ts
|
|
2175
|
+
function stripQueryParameter(text) {
|
|
2176
|
+
const NEEDLE = "projectKey=";
|
|
2177
|
+
let out = "";
|
|
2178
|
+
let index = 0;
|
|
2179
|
+
const taken = /* @__PURE__ */ new Set();
|
|
2180
|
+
for (; ; ) {
|
|
2181
|
+
const at = text.indexOf(NEEDLE, index);
|
|
2182
|
+
if (at < 0) break;
|
|
2183
|
+
const separator = at === 0 ? "" : text[at - 1];
|
|
2184
|
+
const value = separator === "?" || separator === "&" ? simpleValueEnd(text, at + NEEDLE.length) : null;
|
|
2185
|
+
if (value === null || !servesTheCatalogue(text, at)) {
|
|
2186
|
+
out += text.slice(index, at + NEEDLE.length);
|
|
2187
|
+
index = at + NEEDLE.length;
|
|
2188
|
+
continue;
|
|
2189
|
+
}
|
|
2190
|
+
out += text.slice(index, at - 1);
|
|
2191
|
+
if (separator === "?" && text[value] === "&") {
|
|
2192
|
+
out += "?";
|
|
2193
|
+
index = value + 1;
|
|
2194
|
+
} else {
|
|
2195
|
+
index = value;
|
|
2196
|
+
}
|
|
2197
|
+
taken.add(at);
|
|
2198
|
+
}
|
|
2199
|
+
return {
|
|
2200
|
+
text: out + text.slice(index),
|
|
2201
|
+
taken
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
__name(stripQueryParameter, "stripQueryParameter");
|
|
2205
|
+
var HIDES_A_BRACE = /[`'"{}/\\]/;
|
|
2206
|
+
function simpleValueEnd(text, from) {
|
|
2207
|
+
let i = from;
|
|
2208
|
+
while (i < text.length) {
|
|
2209
|
+
const ch = text[i];
|
|
2210
|
+
if (ch === "&" || ch === "#" || ch === "'" || ch === '"' || ch === "`" || ch === "\n" || ch === " ") {
|
|
2211
|
+
return i;
|
|
2212
|
+
}
|
|
2213
|
+
if (ch !== "$" || text[i + 1] !== "{") {
|
|
2214
|
+
if (ch === "{" || ch === "}") return null;
|
|
2215
|
+
i += 1;
|
|
2216
|
+
continue;
|
|
2217
|
+
}
|
|
2218
|
+
const close = text.indexOf("}", i + 2);
|
|
2219
|
+
if (close < 0) return null;
|
|
2220
|
+
if (HIDES_A_BRACE.test(text.slice(i + 2, close))) return null;
|
|
2221
|
+
i = close + 1;
|
|
2222
|
+
}
|
|
2223
|
+
return null;
|
|
2224
|
+
}
|
|
2225
|
+
__name(simpleValueEnd, "simpleValueEnd");
|
|
2226
|
+
function servesTheCatalogue(text, at) {
|
|
2227
|
+
let start = at;
|
|
2228
|
+
while (start > 0) {
|
|
2229
|
+
const ch = text[start - 1];
|
|
2230
|
+
if (ch === "`" || ch === "'" || ch === '"' || ch === "\n") break;
|
|
2231
|
+
start -= 1;
|
|
2232
|
+
}
|
|
2233
|
+
const url = text.slice(start, at);
|
|
2234
|
+
const query = url.indexOf("?");
|
|
2235
|
+
return (query < 0 ? url : url.slice(0, query)).includes("/catalog/");
|
|
2236
|
+
}
|
|
2237
|
+
__name(servesTheCatalogue, "servesTheCatalogue");
|
|
2238
|
+
function isIdentifierChar(ch) {
|
|
2239
|
+
return ch !== void 0 && /[A-Za-z0-9_$]/.test(ch);
|
|
2240
|
+
}
|
|
2241
|
+
__name(isIdentifierChar, "isIdentifierChar");
|
|
2242
|
+
function lineAt(text, at) {
|
|
2243
|
+
let line = 1;
|
|
2244
|
+
for (let i = 0; i < at; i += 1) if (text[i] === "\n") line += 1;
|
|
2245
|
+
return line;
|
|
2246
|
+
}
|
|
2247
|
+
__name(lineAt, "lineAt");
|
|
2248
|
+
function removeProjectKey(text, kind = "source") {
|
|
2249
|
+
if (kind === "yaml") return removeFromYaml(text);
|
|
2250
|
+
const query = stripQueryParameter(text);
|
|
2251
|
+
const reported = /* @__PURE__ */ new Set();
|
|
2252
|
+
for (let at = text.indexOf("projectKey"); at >= 0; at = text.indexOf("projectKey", at + 1)) {
|
|
2253
|
+
if (query.taken.has(at)) continue;
|
|
2254
|
+
if (isIdentifierChar(text[at - 1])) continue;
|
|
2255
|
+
if (isIdentifierChar(text[at + "projectKey".length])) continue;
|
|
2256
|
+
reported.add(lineAt(text, at));
|
|
2257
|
+
}
|
|
2258
|
+
return {
|
|
2259
|
+
text: query.text,
|
|
2260
|
+
rewritten: query.taken.size,
|
|
2261
|
+
undecided: [
|
|
2262
|
+
...reported
|
|
2263
|
+
].sort((a, b) => a - b)
|
|
2264
|
+
};
|
|
2265
|
+
}
|
|
2266
|
+
__name(removeProjectKey, "removeProjectKey");
|
|
2267
|
+
function removeFromYaml(text) {
|
|
2268
|
+
const lines = text.split("\n");
|
|
2269
|
+
const kept = [];
|
|
2270
|
+
let rewritten = 0;
|
|
2271
|
+
for (const line of lines) {
|
|
2272
|
+
if (line.startsWith("projectKey:")) {
|
|
2273
|
+
rewritten += 1;
|
|
2274
|
+
continue;
|
|
2275
|
+
}
|
|
2276
|
+
kept.push(line);
|
|
2277
|
+
}
|
|
2278
|
+
return {
|
|
2279
|
+
text: kept.join("\n"),
|
|
2280
|
+
rewritten,
|
|
2281
|
+
undecided: []
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
__name(removeFromYaml, "removeFromYaml");
|
|
2285
|
+
|
|
2172
2286
|
// src/init/patch-app-module.ts
|
|
2173
2287
|
var MARKER = "SaaSiCatModule.forRoot";
|
|
2174
2288
|
function patchAppModule(source, options) {
|
|
@@ -3392,11 +3506,12 @@ UserCommands = _ts_decorate14([
|
|
|
3392
3506
|
UserCommands,
|
|
3393
3507
|
UserPortDoctorCheck,
|
|
3394
3508
|
WhoAmIFlow,
|
|
3509
|
+
appKeyPattern,
|
|
3395
3510
|
appendConstraints,
|
|
3396
3511
|
applyFragmentBlocks,
|
|
3397
3512
|
applyTokens,
|
|
3398
3513
|
assertModelsExist,
|
|
3399
|
-
|
|
3514
|
+
assertValidAppKey,
|
|
3400
3515
|
assertValidQuotaKey,
|
|
3401
3516
|
blankStringLiterals,
|
|
3402
3517
|
blockBodyLines,
|
|
@@ -3432,10 +3547,10 @@ UserCommands = _ts_decorate14([
|
|
|
3432
3547
|
patchAppModule,
|
|
3433
3548
|
patchOptionsFor,
|
|
3434
3549
|
planInit,
|
|
3435
|
-
projectKeyPattern,
|
|
3436
3550
|
quotaKeyPattern,
|
|
3437
3551
|
readEffectiveModuleResolution,
|
|
3438
3552
|
relationNameOf,
|
|
3553
|
+
removeProjectKey,
|
|
3439
3554
|
reportConstraints,
|
|
3440
3555
|
rewriteImports,
|
|
3441
3556
|
rewriteManifest,
|
package/dist/index.d.cts
CHANGED
|
@@ -719,9 +719,9 @@ declare function assertModelsExist(declaredModels: readonly string[], models: Fk
|
|
|
719
719
|
|
|
720
720
|
/** What the caller asked for. */
|
|
721
721
|
interface InitOptions {
|
|
722
|
-
/**
|
|
723
|
-
|
|
724
|
-
/** Human name in the manifest and the YAML. Defaults to `
|
|
722
|
+
/** Slug of the application: npm package name, storage-key prefix, id prefix. */
|
|
723
|
+
appKey: string;
|
|
724
|
+
/** Human name in the manifest and the YAML. Defaults to `appKey`. */
|
|
725
725
|
appName?: string;
|
|
726
726
|
/** Admin API prefix, e.g. `/api/v1/admin`. */
|
|
727
727
|
apiBase?: string;
|
|
@@ -854,8 +854,7 @@ declare function judgeModuleResolution(value: string | null): ModuleResolutionVe
|
|
|
854
854
|
*/
|
|
855
855
|
declare function readEffectiveModuleResolution(root: string, ts: typeof TypeScript): string | null;
|
|
856
856
|
|
|
857
|
-
|
|
858
|
-
declare function projectKeyPattern(): RegExp;
|
|
857
|
+
declare function appKeyPattern(): RegExp;
|
|
859
858
|
/** The pattern it puts on the keys inside a plan's `quotas` object. */
|
|
860
859
|
declare function quotaKeyPattern(): RegExp;
|
|
861
860
|
/** How many quotas a plan must declare — 1 today, and read rather than assumed. */
|
|
@@ -866,7 +865,7 @@ declare function minimumQuotasPerPlan(): number;
|
|
|
866
865
|
* The message carries the pattern rather than a prose paraphrase, because the
|
|
867
866
|
* paraphrase is what goes stale.
|
|
868
867
|
*/
|
|
869
|
-
declare function
|
|
868
|
+
declare function assertValidAppKey(appKey: string): void;
|
|
870
869
|
/** The same, for a quota key. `additionalProperties: false` makes it a hard rule. */
|
|
871
870
|
declare function assertValidQuotaKey(quotaKey: string): void;
|
|
872
871
|
|
|
@@ -981,6 +980,26 @@ interface ManifestRewriteOptions {
|
|
|
981
980
|
}
|
|
982
981
|
declare function rewriteManifest(text: string, table: RenameTable, options: ManifestRewriteOptions): RenameResult;
|
|
983
982
|
|
|
983
|
+
interface ProjectKeyResult {
|
|
984
|
+
readonly text: string;
|
|
985
|
+
/** How many occurrences were taken out. */
|
|
986
|
+
readonly rewritten: number;
|
|
987
|
+
/**
|
|
988
|
+
* 1-based line numbers of the occurrences left in place.
|
|
989
|
+
*
|
|
990
|
+
* Reported rather than removed: the codemod could not tell them from a
|
|
991
|
+
* consumer's own field, and a wrong deletion is worse than a named one.
|
|
992
|
+
*/
|
|
993
|
+
readonly undecided: readonly number[];
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Rewrites one source file.
|
|
997
|
+
*
|
|
998
|
+
* `yaml` switches to the config form: there the field is a top-level key in a
|
|
999
|
+
* file the platform owns the schema of, so it is decidable without an anchor.
|
|
1000
|
+
*/
|
|
1001
|
+
declare function removeProjectKey(text: string, kind?: 'source' | 'yaml'): ProjectKeyResult;
|
|
1002
|
+
|
|
984
1003
|
interface PatchAppModuleOptions {
|
|
985
1004
|
/** Import specifier for the persistence bundle, or null when not generated. */
|
|
986
1005
|
persistenceImport: string | null;
|
|
@@ -1224,4 +1243,4 @@ declare class UserCommands extends CommandRunner {
|
|
|
1224
1243
|
parsePassword(val: string): string;
|
|
1225
1244
|
}
|
|
1226
1245
|
|
|
1227
|
-
export { AUDIT_QUERY_PORT_TOKEN, AdminCommands, AdminManifestDoctorCheck, AdminMfaSetupCommand, AdminWhoamiCommand, type ApplyResult, AuditCommands, AuditTailCommand, AuditTailFlow, type AuditTailOptions, type BlockAttributeKind, type BlockAttributes, CLI_CONTEXT_CONFIG_TOKEN, CONSTRAINTS_MARKER, type CheckSeverity, type CliContextConfig, CliContextModule, type CliContextModuleOptions, CliContextService, CliError, type CliIdentity, type ConstraintsOutcome, type ConstraintsReport, DEFAULT_MANIFEST_CHECKS, DOCTOR_CHECKS_TOKEN, DiscoveryCommands, DiscoveryScanCommand, DiscoverySnapshotDoctorCheck, type DoctorCheck, type DoctorCheckResult, DoctorCommands, DoctorFlow, type DoctorReport, type EnableFkResult, type FieldMismatch, type FieldMismatchReason, type FieldSignature, type FkModelNames, type FkPointer, type FragmentBlocks, type InitOptions, type InitPlan, LIMIT_FILTER_IMPORTS, LIMIT_FILTER_PROVIDER, MANIFEST_ACCESS_PORT_TOKEN, MANIFEST_CHECKS_TOKEN, type ManifestCheck, ManifestCheckCommand, type ManifestCheckReport, type ManifestCheckResult, ManifestCliFlow, ManifestCommands, type ManifestDiff, ManifestDumpCommand, ManifestHashCommand, type ManifestRewriteOptions, ManifestValidateCommand, MfaSetupFlow, type MfaSetupOptions, type MfaSetupResult, type MissingBlockAttribute, type MissingEnumValue, type MissingField, type ModuleResolutionVerdict, type MoveTable, PLATFORM_DOCTOR_CHECK_PROVIDERS, type ParsedSchema, type PatchAppModuleOptions, type PatchOptionsFromPlan, type PatchResult, PlanCatalogDoctorCheck, type PlannedFile, type QuotaProviderFile, type QuotaSpec, type RenameResult, type RenameTable, type RewriteResult, type SchemaCheckReport, UI_VUE_SPECIFIER, USER_MANAGEMENT_PORT_TOKEN, USER_PORT_TOKEN, UserCommands, UserPortDoctorCheck, WhoAmIFlow, type WhoAmIResult, appendConstraints, applyFragmentBlocks, applyTokens, assertModelsExist,
|
|
1246
|
+
export { AUDIT_QUERY_PORT_TOKEN, AdminCommands, AdminManifestDoctorCheck, AdminMfaSetupCommand, AdminWhoamiCommand, type ApplyResult, AuditCommands, AuditTailCommand, AuditTailFlow, type AuditTailOptions, type BlockAttributeKind, type BlockAttributes, CLI_CONTEXT_CONFIG_TOKEN, CONSTRAINTS_MARKER, type CheckSeverity, type CliContextConfig, CliContextModule, type CliContextModuleOptions, CliContextService, CliError, type CliIdentity, type ConstraintsOutcome, type ConstraintsReport, DEFAULT_MANIFEST_CHECKS, DOCTOR_CHECKS_TOKEN, DiscoveryCommands, DiscoveryScanCommand, DiscoverySnapshotDoctorCheck, type DoctorCheck, type DoctorCheckResult, DoctorCommands, DoctorFlow, type DoctorReport, type EnableFkResult, type FieldMismatch, type FieldMismatchReason, type FieldSignature, type FkModelNames, type FkPointer, type FragmentBlocks, type InitOptions, type InitPlan, LIMIT_FILTER_IMPORTS, LIMIT_FILTER_PROVIDER, MANIFEST_ACCESS_PORT_TOKEN, MANIFEST_CHECKS_TOKEN, type ManifestCheck, ManifestCheckCommand, type ManifestCheckReport, type ManifestCheckResult, ManifestCliFlow, ManifestCommands, type ManifestDiff, ManifestDumpCommand, ManifestHashCommand, type ManifestRewriteOptions, ManifestValidateCommand, MfaSetupFlow, type MfaSetupOptions, type MfaSetupResult, type MissingBlockAttribute, type MissingEnumValue, type MissingField, type ModuleResolutionVerdict, type MoveTable, PLATFORM_DOCTOR_CHECK_PROVIDERS, type ParsedSchema, type PatchAppModuleOptions, type PatchOptionsFromPlan, type PatchResult, PlanCatalogDoctorCheck, type PlannedFile, type ProjectKeyResult, type QuotaProviderFile, type QuotaSpec, type RenameResult, type RenameTable, type RewriteResult, type SchemaCheckReport, UI_VUE_SPECIFIER, USER_MANAGEMENT_PORT_TOKEN, USER_PORT_TOKEN, UserCommands, UserPortDoctorCheck, WhoAmIFlow, type WhoAmIResult, appKeyPattern, appendConstraints, applyFragmentBlocks, applyTokens, assertModelsExist, assertValidAppKey, assertValidQuotaKey, blankStringLiterals, blockBodyLines, breaksContract, buildImportMap, checkSchema, constraintsFor, enableFkPointers, extractBlockNames, extractBlocks, extractEnumBlocks, extractEnumNames, extractFragmentBlocks, extractModelBlocks, extractModelNames, findFkPointers, foreignKeyOf, hasBackRelation, hasConstraints, isNoLongerPublic, isOneToOne, judgeModuleResolution, kebabCase, migrationCreatedBy, minimumQuotasPerPlan, namedImports, parseBlockAttributes, parseEnumValues, parseFields, parseQuota, parseSchema, pascalCase, patchAppModule, patchOptionsFor, planInit, quotaKeyPattern, readEffectiveModuleResolution, relationNameOf, removeProjectKey, reportConstraints, rewriteImports, rewriteManifest, rewriteNames, rewriteSubpath, stripLineComment, structuralOnly, tablesAddressedBy };
|
package/dist/index.d.ts
CHANGED
|
@@ -719,9 +719,9 @@ declare function assertModelsExist(declaredModels: readonly string[], models: Fk
|
|
|
719
719
|
|
|
720
720
|
/** What the caller asked for. */
|
|
721
721
|
interface InitOptions {
|
|
722
|
-
/**
|
|
723
|
-
|
|
724
|
-
/** Human name in the manifest and the YAML. Defaults to `
|
|
722
|
+
/** Slug of the application: npm package name, storage-key prefix, id prefix. */
|
|
723
|
+
appKey: string;
|
|
724
|
+
/** Human name in the manifest and the YAML. Defaults to `appKey`. */
|
|
725
725
|
appName?: string;
|
|
726
726
|
/** Admin API prefix, e.g. `/api/v1/admin`. */
|
|
727
727
|
apiBase?: string;
|
|
@@ -854,8 +854,7 @@ declare function judgeModuleResolution(value: string | null): ModuleResolutionVe
|
|
|
854
854
|
*/
|
|
855
855
|
declare function readEffectiveModuleResolution(root: string, ts: typeof TypeScript): string | null;
|
|
856
856
|
|
|
857
|
-
|
|
858
|
-
declare function projectKeyPattern(): RegExp;
|
|
857
|
+
declare function appKeyPattern(): RegExp;
|
|
859
858
|
/** The pattern it puts on the keys inside a plan's `quotas` object. */
|
|
860
859
|
declare function quotaKeyPattern(): RegExp;
|
|
861
860
|
/** How many quotas a plan must declare — 1 today, and read rather than assumed. */
|
|
@@ -866,7 +865,7 @@ declare function minimumQuotasPerPlan(): number;
|
|
|
866
865
|
* The message carries the pattern rather than a prose paraphrase, because the
|
|
867
866
|
* paraphrase is what goes stale.
|
|
868
867
|
*/
|
|
869
|
-
declare function
|
|
868
|
+
declare function assertValidAppKey(appKey: string): void;
|
|
870
869
|
/** The same, for a quota key. `additionalProperties: false` makes it a hard rule. */
|
|
871
870
|
declare function assertValidQuotaKey(quotaKey: string): void;
|
|
872
871
|
|
|
@@ -981,6 +980,26 @@ interface ManifestRewriteOptions {
|
|
|
981
980
|
}
|
|
982
981
|
declare function rewriteManifest(text: string, table: RenameTable, options: ManifestRewriteOptions): RenameResult;
|
|
983
982
|
|
|
983
|
+
interface ProjectKeyResult {
|
|
984
|
+
readonly text: string;
|
|
985
|
+
/** How many occurrences were taken out. */
|
|
986
|
+
readonly rewritten: number;
|
|
987
|
+
/**
|
|
988
|
+
* 1-based line numbers of the occurrences left in place.
|
|
989
|
+
*
|
|
990
|
+
* Reported rather than removed: the codemod could not tell them from a
|
|
991
|
+
* consumer's own field, and a wrong deletion is worse than a named one.
|
|
992
|
+
*/
|
|
993
|
+
readonly undecided: readonly number[];
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Rewrites one source file.
|
|
997
|
+
*
|
|
998
|
+
* `yaml` switches to the config form: there the field is a top-level key in a
|
|
999
|
+
* file the platform owns the schema of, so it is decidable without an anchor.
|
|
1000
|
+
*/
|
|
1001
|
+
declare function removeProjectKey(text: string, kind?: 'source' | 'yaml'): ProjectKeyResult;
|
|
1002
|
+
|
|
984
1003
|
interface PatchAppModuleOptions {
|
|
985
1004
|
/** Import specifier for the persistence bundle, or null when not generated. */
|
|
986
1005
|
persistenceImport: string | null;
|
|
@@ -1224,4 +1243,4 @@ declare class UserCommands extends CommandRunner {
|
|
|
1224
1243
|
parsePassword(val: string): string;
|
|
1225
1244
|
}
|
|
1226
1245
|
|
|
1227
|
-
export { AUDIT_QUERY_PORT_TOKEN, AdminCommands, AdminManifestDoctorCheck, AdminMfaSetupCommand, AdminWhoamiCommand, type ApplyResult, AuditCommands, AuditTailCommand, AuditTailFlow, type AuditTailOptions, type BlockAttributeKind, type BlockAttributes, CLI_CONTEXT_CONFIG_TOKEN, CONSTRAINTS_MARKER, type CheckSeverity, type CliContextConfig, CliContextModule, type CliContextModuleOptions, CliContextService, CliError, type CliIdentity, type ConstraintsOutcome, type ConstraintsReport, DEFAULT_MANIFEST_CHECKS, DOCTOR_CHECKS_TOKEN, DiscoveryCommands, DiscoveryScanCommand, DiscoverySnapshotDoctorCheck, type DoctorCheck, type DoctorCheckResult, DoctorCommands, DoctorFlow, type DoctorReport, type EnableFkResult, type FieldMismatch, type FieldMismatchReason, type FieldSignature, type FkModelNames, type FkPointer, type FragmentBlocks, type InitOptions, type InitPlan, LIMIT_FILTER_IMPORTS, LIMIT_FILTER_PROVIDER, MANIFEST_ACCESS_PORT_TOKEN, MANIFEST_CHECKS_TOKEN, type ManifestCheck, ManifestCheckCommand, type ManifestCheckReport, type ManifestCheckResult, ManifestCliFlow, ManifestCommands, type ManifestDiff, ManifestDumpCommand, ManifestHashCommand, type ManifestRewriteOptions, ManifestValidateCommand, MfaSetupFlow, type MfaSetupOptions, type MfaSetupResult, type MissingBlockAttribute, type MissingEnumValue, type MissingField, type ModuleResolutionVerdict, type MoveTable, PLATFORM_DOCTOR_CHECK_PROVIDERS, type ParsedSchema, type PatchAppModuleOptions, type PatchOptionsFromPlan, type PatchResult, PlanCatalogDoctorCheck, type PlannedFile, type QuotaProviderFile, type QuotaSpec, type RenameResult, type RenameTable, type RewriteResult, type SchemaCheckReport, UI_VUE_SPECIFIER, USER_MANAGEMENT_PORT_TOKEN, USER_PORT_TOKEN, UserCommands, UserPortDoctorCheck, WhoAmIFlow, type WhoAmIResult, appendConstraints, applyFragmentBlocks, applyTokens, assertModelsExist,
|
|
1246
|
+
export { AUDIT_QUERY_PORT_TOKEN, AdminCommands, AdminManifestDoctorCheck, AdminMfaSetupCommand, AdminWhoamiCommand, type ApplyResult, AuditCommands, AuditTailCommand, AuditTailFlow, type AuditTailOptions, type BlockAttributeKind, type BlockAttributes, CLI_CONTEXT_CONFIG_TOKEN, CONSTRAINTS_MARKER, type CheckSeverity, type CliContextConfig, CliContextModule, type CliContextModuleOptions, CliContextService, CliError, type CliIdentity, type ConstraintsOutcome, type ConstraintsReport, DEFAULT_MANIFEST_CHECKS, DOCTOR_CHECKS_TOKEN, DiscoveryCommands, DiscoveryScanCommand, DiscoverySnapshotDoctorCheck, type DoctorCheck, type DoctorCheckResult, DoctorCommands, DoctorFlow, type DoctorReport, type EnableFkResult, type FieldMismatch, type FieldMismatchReason, type FieldSignature, type FkModelNames, type FkPointer, type FragmentBlocks, type InitOptions, type InitPlan, LIMIT_FILTER_IMPORTS, LIMIT_FILTER_PROVIDER, MANIFEST_ACCESS_PORT_TOKEN, MANIFEST_CHECKS_TOKEN, type ManifestCheck, ManifestCheckCommand, type ManifestCheckReport, type ManifestCheckResult, ManifestCliFlow, ManifestCommands, type ManifestDiff, ManifestDumpCommand, ManifestHashCommand, type ManifestRewriteOptions, ManifestValidateCommand, MfaSetupFlow, type MfaSetupOptions, type MfaSetupResult, type MissingBlockAttribute, type MissingEnumValue, type MissingField, type ModuleResolutionVerdict, type MoveTable, PLATFORM_DOCTOR_CHECK_PROVIDERS, type ParsedSchema, type PatchAppModuleOptions, type PatchOptionsFromPlan, type PatchResult, PlanCatalogDoctorCheck, type PlannedFile, type ProjectKeyResult, type QuotaProviderFile, type QuotaSpec, type RenameResult, type RenameTable, type RewriteResult, type SchemaCheckReport, UI_VUE_SPECIFIER, USER_MANAGEMENT_PORT_TOKEN, USER_PORT_TOKEN, UserCommands, UserPortDoctorCheck, WhoAmIFlow, type WhoAmIResult, appKeyPattern, appendConstraints, applyFragmentBlocks, applyTokens, assertModelsExist, assertValidAppKey, assertValidQuotaKey, blankStringLiterals, blockBodyLines, breaksContract, buildImportMap, checkSchema, constraintsFor, enableFkPointers, extractBlockNames, extractBlocks, extractEnumBlocks, extractEnumNames, extractFragmentBlocks, extractModelBlocks, extractModelNames, findFkPointers, foreignKeyOf, hasBackRelation, hasConstraints, isNoLongerPublic, isOneToOne, judgeModuleResolution, kebabCase, migrationCreatedBy, minimumQuotasPerPlan, namedImports, parseBlockAttributes, parseEnumValues, parseFields, parseQuota, parseSchema, pascalCase, patchAppModule, patchOptionsFor, planInit, quotaKeyPattern, readEffectiveModuleResolution, relationNameOf, removeProjectKey, reportConstraints, rewriteImports, rewriteManifest, rewriteNames, rewriteSubpath, stripLineComment, structuralOnly, tablesAddressedBy };
|
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
|
};
|
|
@@ -1581,10 +1581,11 @@ function required(value, what) {
|
|
|
1581
1581
|
return value;
|
|
1582
1582
|
}
|
|
1583
1583
|
__name(required, "required");
|
|
1584
|
-
|
|
1585
|
-
|
|
1584
|
+
var APP_KEY_PATTERN = /^[a-z][a-z0-9-]{1,30}$/;
|
|
1585
|
+
function appKeyPattern() {
|
|
1586
|
+
return APP_KEY_PATTERN;
|
|
1586
1587
|
}
|
|
1587
|
-
__name(
|
|
1588
|
+
__name(appKeyPattern, "appKeyPattern");
|
|
1588
1589
|
function quotaKeyPattern() {
|
|
1589
1590
|
const patterns = Object.keys(required(schema.$defs?.PlanDef?.properties?.quotas?.patternProperties, "quota key pattern"));
|
|
1590
1591
|
if (patterns.length !== 1) {
|
|
@@ -1597,12 +1598,12 @@ function minimumQuotasPerPlan() {
|
|
|
1597
1598
|
return required(schema.$defs?.PlanDef?.properties?.quotas?.minProperties, "minProperties");
|
|
1598
1599
|
}
|
|
1599
1600
|
__name(minimumQuotasPerPlan, "minimumQuotasPerPlan");
|
|
1600
|
-
function
|
|
1601
|
-
const pattern =
|
|
1602
|
-
if (pattern.test(
|
|
1603
|
-
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.`);
|
|
1604
1605
|
}
|
|
1605
|
-
__name(
|
|
1606
|
+
__name(assertValidAppKey, "assertValidAppKey");
|
|
1606
1607
|
function assertValidQuotaKey(quotaKey) {
|
|
1607
1608
|
const pattern = quotaKeyPattern();
|
|
1608
1609
|
if (pattern.test(quotaKey)) return;
|
|
@@ -1634,24 +1635,24 @@ function pascalCase(value) {
|
|
|
1634
1635
|
__name(pascalCase, "pascalCase");
|
|
1635
1636
|
var quotaFileName = /* @__PURE__ */ __name((key) => `${key.replace(/[^A-Za-z0-9]+/g, "-").toLowerCase()}-quota.provider.ts`, "quotaFileName");
|
|
1636
1637
|
function planInit(options) {
|
|
1637
|
-
const
|
|
1638
|
-
if (!
|
|
1639
|
-
|
|
1640
|
-
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);
|
|
1641
1642
|
const appName = pascalCase(appLabel);
|
|
1642
1643
|
const apiBase = options.apiBase ?? "/api/v1/admin";
|
|
1643
1644
|
const quotas = (options.quotas ?? []).map(parseQuota);
|
|
1644
1645
|
assertEnoughQuotas(quotas);
|
|
1645
1646
|
const hasherClass = options.skipHasher ? null : `${appName}PasswordHasher`;
|
|
1646
|
-
const featureKey = `${
|
|
1647
|
+
const featureKey = `${appKey.replace(/[^A-Za-z0-9]+/g, "_").toUpperCase()}_CORE`;
|
|
1647
1648
|
const shared = {
|
|
1648
|
-
|
|
1649
|
+
APP_KEY: appKey,
|
|
1649
1650
|
APP_NAME: appName,
|
|
1650
1651
|
APP_LABEL: appLabel,
|
|
1651
1652
|
API_BASE: apiBase,
|
|
1652
1653
|
FEATURE_KEY: featureKey,
|
|
1653
|
-
REGISTRY_CONST: `${constantCase(
|
|
1654
|
-
MANIFEST_CONST: `${constantCase(
|
|
1654
|
+
REGISTRY_CONST: `${constantCase(appKey)}_FEATURE_UI_REGISTRY`,
|
|
1655
|
+
MANIFEST_CONST: `${constantCase(appKey)}_MANIFEST_CONTRIBUTION`,
|
|
1655
1656
|
ADMIN_MODULE_CLASS: `${appName}AdminModule`,
|
|
1656
1657
|
HASHER_CLASS: hasherClass ?? "",
|
|
1657
1658
|
HASHER_FILE: hasherClass ? `${kebabCase(appName)}-password.hasher` : "",
|
|
@@ -2045,6 +2046,118 @@ function rewriteManifest(text, table, options) {
|
|
|
2045
2046
|
}
|
|
2046
2047
|
__name(rewriteManifest, "rewriteManifest");
|
|
2047
2048
|
|
|
2049
|
+
// src/codemods/v1-project-key.ts
|
|
2050
|
+
function stripQueryParameter(text) {
|
|
2051
|
+
const NEEDLE = "projectKey=";
|
|
2052
|
+
let out = "";
|
|
2053
|
+
let index = 0;
|
|
2054
|
+
const taken = /* @__PURE__ */ new Set();
|
|
2055
|
+
for (; ; ) {
|
|
2056
|
+
const at = text.indexOf(NEEDLE, index);
|
|
2057
|
+
if (at < 0) break;
|
|
2058
|
+
const separator = at === 0 ? "" : text[at - 1];
|
|
2059
|
+
const value = separator === "?" || separator === "&" ? simpleValueEnd(text, at + NEEDLE.length) : null;
|
|
2060
|
+
if (value === null || !servesTheCatalogue(text, at)) {
|
|
2061
|
+
out += text.slice(index, at + NEEDLE.length);
|
|
2062
|
+
index = at + NEEDLE.length;
|
|
2063
|
+
continue;
|
|
2064
|
+
}
|
|
2065
|
+
out += text.slice(index, at - 1);
|
|
2066
|
+
if (separator === "?" && text[value] === "&") {
|
|
2067
|
+
out += "?";
|
|
2068
|
+
index = value + 1;
|
|
2069
|
+
} else {
|
|
2070
|
+
index = value;
|
|
2071
|
+
}
|
|
2072
|
+
taken.add(at);
|
|
2073
|
+
}
|
|
2074
|
+
return {
|
|
2075
|
+
text: out + text.slice(index),
|
|
2076
|
+
taken
|
|
2077
|
+
};
|
|
2078
|
+
}
|
|
2079
|
+
__name(stripQueryParameter, "stripQueryParameter");
|
|
2080
|
+
var HIDES_A_BRACE = /[`'"{}/\\]/;
|
|
2081
|
+
function simpleValueEnd(text, from) {
|
|
2082
|
+
let i = from;
|
|
2083
|
+
while (i < text.length) {
|
|
2084
|
+
const ch = text[i];
|
|
2085
|
+
if (ch === "&" || ch === "#" || ch === "'" || ch === '"' || ch === "`" || ch === "\n" || ch === " ") {
|
|
2086
|
+
return i;
|
|
2087
|
+
}
|
|
2088
|
+
if (ch !== "$" || text[i + 1] !== "{") {
|
|
2089
|
+
if (ch === "{" || ch === "}") return null;
|
|
2090
|
+
i += 1;
|
|
2091
|
+
continue;
|
|
2092
|
+
}
|
|
2093
|
+
const close = text.indexOf("}", i + 2);
|
|
2094
|
+
if (close < 0) return null;
|
|
2095
|
+
if (HIDES_A_BRACE.test(text.slice(i + 2, close))) return null;
|
|
2096
|
+
i = close + 1;
|
|
2097
|
+
}
|
|
2098
|
+
return null;
|
|
2099
|
+
}
|
|
2100
|
+
__name(simpleValueEnd, "simpleValueEnd");
|
|
2101
|
+
function servesTheCatalogue(text, at) {
|
|
2102
|
+
let start = at;
|
|
2103
|
+
while (start > 0) {
|
|
2104
|
+
const ch = text[start - 1];
|
|
2105
|
+
if (ch === "`" || ch === "'" || ch === '"' || ch === "\n") break;
|
|
2106
|
+
start -= 1;
|
|
2107
|
+
}
|
|
2108
|
+
const url = text.slice(start, at);
|
|
2109
|
+
const query = url.indexOf("?");
|
|
2110
|
+
return (query < 0 ? url : url.slice(0, query)).includes("/catalog/");
|
|
2111
|
+
}
|
|
2112
|
+
__name(servesTheCatalogue, "servesTheCatalogue");
|
|
2113
|
+
function isIdentifierChar(ch) {
|
|
2114
|
+
return ch !== void 0 && /[A-Za-z0-9_$]/.test(ch);
|
|
2115
|
+
}
|
|
2116
|
+
__name(isIdentifierChar, "isIdentifierChar");
|
|
2117
|
+
function lineAt(text, at) {
|
|
2118
|
+
let line = 1;
|
|
2119
|
+
for (let i = 0; i < at; i += 1) if (text[i] === "\n") line += 1;
|
|
2120
|
+
return line;
|
|
2121
|
+
}
|
|
2122
|
+
__name(lineAt, "lineAt");
|
|
2123
|
+
function removeProjectKey(text, kind = "source") {
|
|
2124
|
+
if (kind === "yaml") return removeFromYaml(text);
|
|
2125
|
+
const query = stripQueryParameter(text);
|
|
2126
|
+
const reported = /* @__PURE__ */ new Set();
|
|
2127
|
+
for (let at = text.indexOf("projectKey"); at >= 0; at = text.indexOf("projectKey", at + 1)) {
|
|
2128
|
+
if (query.taken.has(at)) continue;
|
|
2129
|
+
if (isIdentifierChar(text[at - 1])) continue;
|
|
2130
|
+
if (isIdentifierChar(text[at + "projectKey".length])) continue;
|
|
2131
|
+
reported.add(lineAt(text, at));
|
|
2132
|
+
}
|
|
2133
|
+
return {
|
|
2134
|
+
text: query.text,
|
|
2135
|
+
rewritten: query.taken.size,
|
|
2136
|
+
undecided: [
|
|
2137
|
+
...reported
|
|
2138
|
+
].sort((a, b) => a - b)
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
__name(removeProjectKey, "removeProjectKey");
|
|
2142
|
+
function removeFromYaml(text) {
|
|
2143
|
+
const lines = text.split("\n");
|
|
2144
|
+
const kept = [];
|
|
2145
|
+
let rewritten = 0;
|
|
2146
|
+
for (const line of lines) {
|
|
2147
|
+
if (line.startsWith("projectKey:")) {
|
|
2148
|
+
rewritten += 1;
|
|
2149
|
+
continue;
|
|
2150
|
+
}
|
|
2151
|
+
kept.push(line);
|
|
2152
|
+
}
|
|
2153
|
+
return {
|
|
2154
|
+
text: kept.join("\n"),
|
|
2155
|
+
rewritten,
|
|
2156
|
+
undecided: []
|
|
2157
|
+
};
|
|
2158
|
+
}
|
|
2159
|
+
__name(removeFromYaml, "removeFromYaml");
|
|
2160
|
+
|
|
2048
2161
|
// src/init/patch-app-module.ts
|
|
2049
2162
|
var MARKER = "SaaSiCatModule.forRoot";
|
|
2050
2163
|
function patchAppModule(source, options) {
|
|
@@ -3267,11 +3380,12 @@ export {
|
|
|
3267
3380
|
UserCommands,
|
|
3268
3381
|
UserPortDoctorCheck,
|
|
3269
3382
|
WhoAmIFlow,
|
|
3383
|
+
appKeyPattern,
|
|
3270
3384
|
appendConstraints,
|
|
3271
3385
|
applyFragmentBlocks,
|
|
3272
3386
|
applyTokens,
|
|
3273
3387
|
assertModelsExist,
|
|
3274
|
-
|
|
3388
|
+
assertValidAppKey,
|
|
3275
3389
|
assertValidQuotaKey,
|
|
3276
3390
|
blankStringLiterals,
|
|
3277
3391
|
blockBodyLines,
|
|
@@ -3307,10 +3421,10 @@ export {
|
|
|
3307
3421
|
patchAppModule,
|
|
3308
3422
|
patchOptionsFor,
|
|
3309
3423
|
planInit,
|
|
3310
|
-
projectKeyPattern,
|
|
3311
3424
|
quotaKeyPattern,
|
|
3312
3425
|
readEffectiveModuleResolution,
|
|
3313
3426
|
relationNameOf,
|
|
3427
|
+
removeProjectKey,
|
|
3314
3428
|
reportConstraints,
|
|
3315
3429
|
rewriteImports,
|
|
3316
3430
|
rewriteManifest,
|
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.7",
|
|
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",
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"qrcode-terminal": "^0.12.0",
|
|
33
|
-
"@saasicat/nest": "^1.0.0-rc.
|
|
34
|
-
"@saasicat/spec": "^1.0.0-rc.
|
|
35
|
-
"@saasicat/core": "^1.0.0-rc.
|
|
33
|
+
"@saasicat/nest": "^1.0.0-rc.7",
|
|
34
|
+
"@saasicat/spec": "^1.0.0-rc.7",
|
|
35
|
+
"@saasicat/core": "^1.0.0-rc.7"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@nestjs/common": "^11.0.0",
|
|
@@ -19,7 +19,7 @@ export const __MANIFEST_CONST__: ManifestContribution = {
|
|
|
19
19
|
dashboard: {
|
|
20
20
|
kpiCards: [
|
|
21
21
|
{
|
|
22
|
-
id: '
|
|
22
|
+
id: '__APP_KEY__.tenants',
|
|
23
23
|
label: 'Tenants',
|
|
24
24
|
// Served by your own controller — the platform does not know
|
|
25
25
|
// what your KPIs count.
|