@saasicat/cli 1.0.0-rc.2 → 1.0.0-rc.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -1
- package/bin/saasicat.js +267 -21
- package/dist/.build-stamp +1 -1
- package/dist/index.cjs +623 -148
- package/dist/index.d.cts +222 -15
- package/dist/index.d.ts +222 -15
- package/dist/index.js +610 -147
- package/package.json +4 -4
- package/templates/init/config/saas.yaml.tpl +40 -2
- package/templates/init/src/saas/admin-manifest.contribution.ts.tpl +1 -1
- package/templates/init/src/saas/persistence-without-hasher.ts.tpl +1 -1
- package/templates/init/src/saas/persistence.ts.tpl +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @saasicat/cli
|
|
2
2
|
|
|
3
|
+
## What this is
|
|
4
|
+
|
|
3
5
|
Cross-cutting helpers for consumer CLIs. Provides:
|
|
4
6
|
|
|
5
7
|
- `CliContextService` — identity / MFA / production-confirm / audit-tag
|
|
@@ -12,6 +14,16 @@ Cross-cutting helpers for consumer CLIs. Provides:
|
|
|
12
14
|
|
|
13
15
|
Spec: [`cli-conventions.md`][conventions] in `@saasicat/spec`.
|
|
14
16
|
|
|
17
|
+
## What this is not
|
|
18
|
+
|
|
19
|
+
Not a CLI. There is no binary here: these are `nest-commander` flows and
|
|
20
|
+
services you register in **your** application's CLI, so they run with your
|
|
21
|
+
DI container, your database connection and your configuration.
|
|
22
|
+
|
|
23
|
+
Not the schema tooling either — `saasicat schema apply|check|migrate` and
|
|
24
|
+
`saasicat init` ship in the `saasicat` binary of this package's `bin`, and
|
|
25
|
+
are documented in the quickstart rather than here.
|
|
26
|
+
|
|
15
27
|
## Plugin Architecture
|
|
16
28
|
|
|
17
29
|
Consumer CLIs are NestJS-Standalone applications based on
|
|
@@ -30,7 +42,25 @@ import { PrismaUserPortAdapter } from './adapters/prisma-user-port';
|
|
|
30
42
|
@Module({
|
|
31
43
|
imports: [
|
|
32
44
|
PrismaModule,
|
|
33
|
-
PlanCatalogModule.forRoot({
|
|
45
|
+
PlanCatalogModule.forRoot({
|
|
46
|
+
app: { name: 'MyApp' },
|
|
47
|
+
currency: 'EUR',
|
|
48
|
+
vatRate: 19,
|
|
49
|
+
// Spelled out here like the three above it. In an application
|
|
50
|
+
// these come from `loadPlanCatalogFromFile('config/saas.yaml')` —
|
|
51
|
+
// the settings are never in the database, so the file is the only
|
|
52
|
+
// place they can come from.
|
|
53
|
+
tenantBilling: {
|
|
54
|
+
cancellationNoticeDays: { monthly: 0, yearly: 0 },
|
|
55
|
+
selfServiceBlockedPlans: { asTarget: [], asSource: [] },
|
|
56
|
+
},
|
|
57
|
+
// The catalogue is read from the database, not from a file — the
|
|
58
|
+
// CLI's `plan-catalog import` puts it there.
|
|
59
|
+
sink: {
|
|
60
|
+
useFactory: (p) => new PrismaPlanCatalogReadSink(p),
|
|
61
|
+
inject: [PrismaService],
|
|
62
|
+
},
|
|
63
|
+
}),
|
|
34
64
|
AdminModule.forRoot({
|
|
35
65
|
mfaPort: { useFactory: (p) => new PrismaMfaAdapter(p), inject: [PrismaService] },
|
|
36
66
|
auditPort: { useFactory: (p) => new PrismaAuditAdapter(p), inject: [PrismaService] },
|
|
@@ -141,3 +171,8 @@ Per [`cli-conventions.md`][conventions] §6:
|
|
|
141
171
|
| 99 | internal |
|
|
142
172
|
|
|
143
173
|
[conventions]: https://github.com/uelker70/saasicat/blob/main/packages/spec/cli-conventions.md
|
|
174
|
+
|
|
175
|
+
## Next
|
|
176
|
+
|
|
177
|
+
- [Extend your CLI](../../docs/guides/extend-your-cli.md) — registering these flows in your app
|
|
178
|
+
- [Quickstart](../../docs/quickstart.md) — `saasicat init`, `schema apply`, `schema check`
|
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,12 @@
|
|
|
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-moved-settings [--dir=X] — and a `dbCatalog` still carrying values
|
|
25
|
+
// codemod v1 [--dir=X] [--dry-run] — all four, in that order
|
|
23
26
|
// [--skip-hasher] [--dry-run] [--dir=.]
|
|
24
27
|
// Writes the platform wiring — config, persistence, manifest
|
|
25
28
|
// contribution, admin module, one provider per quota — and adds
|
|
@@ -27,7 +30,7 @@
|
|
|
27
30
|
|
|
28
31
|
import { readFile, writeFile, readdir, mkdir } from 'node:fs/promises';
|
|
29
32
|
import { existsSync } from 'node:fs';
|
|
30
|
-
import { basename, dirname, join, resolve } from 'node:path';
|
|
33
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
31
34
|
import { createRequire } from 'node:module';
|
|
32
35
|
import { fileURLToPath } from 'node:url';
|
|
33
36
|
import { spawn } from 'node:child_process';
|
|
@@ -46,7 +49,7 @@ import {
|
|
|
46
49
|
extractFragmentBlocks,
|
|
47
50
|
findFkPointers,
|
|
48
51
|
hasConstraints,
|
|
49
|
-
|
|
52
|
+
assertValidAppKey,
|
|
50
53
|
judgeModuleResolution,
|
|
51
54
|
readEffectiveModuleResolution,
|
|
52
55
|
buildImportMap,
|
|
@@ -54,10 +57,19 @@ import {
|
|
|
54
57
|
rewriteImports,
|
|
55
58
|
rewriteManifest,
|
|
56
59
|
rewriteNames,
|
|
60
|
+
removeProjectKey,
|
|
57
61
|
reportConstraints,
|
|
58
62
|
patchAppModule,
|
|
59
63
|
patchOptionsFor,
|
|
60
64
|
planInit,
|
|
65
|
+
settingsWrittenTo,
|
|
66
|
+
findMovedSettings,
|
|
67
|
+
SCANNED_FOR_MOVED_SETTINGS,
|
|
68
|
+
WHERE_IT_GOES,
|
|
69
|
+
describeDbCatalogOccurrence,
|
|
70
|
+
findDbCatalogBlocks,
|
|
71
|
+
SCANNED_FOR_DB_CATALOG,
|
|
72
|
+
WHERE_DB_CATALOG_GOES,
|
|
61
73
|
} from '../dist/index.js';
|
|
62
74
|
|
|
63
75
|
const require_ = createRequire(import.meta.url);
|
|
@@ -71,7 +83,7 @@ const require_ = createRequire(import.meta.url);
|
|
|
71
83
|
// `pascalCase` and came back as `value.replace is not a function` with exit 99
|
|
72
84
|
// — an internal error for what is an ordinary typo.
|
|
73
85
|
const VALUE_FLAGS = new Set([
|
|
74
|
-
'
|
|
86
|
+
'app-key',
|
|
75
87
|
'app-name',
|
|
76
88
|
'api-base',
|
|
77
89
|
'quota',
|
|
@@ -278,6 +290,16 @@ function printCheckReport(report) {
|
|
|
278
290
|
console.log('');
|
|
279
291
|
}
|
|
280
292
|
|
|
293
|
+
if (report.tenantCascades.length > 0) {
|
|
294
|
+
console.log(`✗ Deleted together with the tenant (${report.tenantCascades.length}):`);
|
|
295
|
+
for (const { model, field } of report.tenantCascades) {
|
|
296
|
+
console.log(` ${`${model}.${field}`.padEnd(44)} onDelete: Cascade`);
|
|
297
|
+
}
|
|
298
|
+
console.log(' These records outlive their tenant — a contract is kept for tax purposes.');
|
|
299
|
+
console.log(' Remove the relation; `tenantId` stays as a trace.');
|
|
300
|
+
console.log('');
|
|
301
|
+
}
|
|
302
|
+
|
|
281
303
|
const breaking = report.missingBlockAttributes.filter((a) => a.kind !== 'index');
|
|
282
304
|
if (breaking.length > 0) {
|
|
283
305
|
console.log(`✗ Missing constraints (${breaking.length}):`);
|
|
@@ -298,7 +320,16 @@ function printCheckReport(report) {
|
|
|
298
320
|
console.log('');
|
|
299
321
|
}
|
|
300
322
|
|
|
301
|
-
|
|
323
|
+
// An enum a reported field names is not a fragment the app went without: the
|
|
324
|
+
// field above it is the drift, and copying the enum is what fixes both.
|
|
325
|
+
// Listing it as "not an error" in the same run would print the contradiction
|
|
326
|
+
// this check exists to avoid — which is exactly what it used to do for a
|
|
327
|
+
// relation pointing at an unadopted model.
|
|
328
|
+
const named = new Set(report.missingFields.map((field) => field.type));
|
|
329
|
+
const absent = [
|
|
330
|
+
...report.absentModels,
|
|
331
|
+
...report.absentEnums.filter((name) => !named.has(name)),
|
|
332
|
+
];
|
|
302
333
|
if (absent.length > 0) {
|
|
303
334
|
console.log(`→ Not adopted (${absent.length}): ${absent.join(', ')}`);
|
|
304
335
|
console.log(' Not an error — the app does not use these fragments.');
|
|
@@ -325,7 +356,21 @@ async function cmdSchemaCheck(args) {
|
|
|
325
356
|
console.log(`→ Checking ${schemaPath} against ${files.length} fragment(s) from @saasicat/spec`);
|
|
326
357
|
console.log('');
|
|
327
358
|
|
|
328
|
-
|
|
359
|
+
// Every model the shipped fragments declare, not only the selected ones: a
|
|
360
|
+
// relation from a selected fragment can point at a model in one that was
|
|
361
|
+
// left out, and without this the narrowed run would report that field as
|
|
362
|
+
// missing — the very contradiction the full run stopped producing. Without
|
|
363
|
+
// a filter the files in hand are already all of them.
|
|
364
|
+
const allFragments = filter
|
|
365
|
+
? await Promise.all(
|
|
366
|
+
(await selectFragmentFiles(fragmentsDir, null)).map((file) =>
|
|
367
|
+
readFile(join(fragmentsDir, file), 'utf8'),
|
|
368
|
+
),
|
|
369
|
+
)
|
|
370
|
+
: fragments;
|
|
371
|
+
const knownModels = new Set(extractModelNames(allFragments.join('\n')));
|
|
372
|
+
|
|
373
|
+
const report = checkSchema(fragments.join('\n'), schema, knownModels);
|
|
329
374
|
printCheckReport(report);
|
|
330
375
|
|
|
331
376
|
const checked = `${report.checkedModelCount} Model(s), ${report.checkedEnumCount} Enum(s)`;
|
|
@@ -678,11 +723,32 @@ function codemodTable(name) {
|
|
|
678
723
|
const CODEMOD_SKIP = new Set(['node_modules', '.git', '.output', 'coverage']);
|
|
679
724
|
const isBuildOutput = (name) => name === 'dist' || name.startsWith('dist-');
|
|
680
725
|
const CODEMOD_EXTENSIONS = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|vue|md)$/;
|
|
726
|
+
/**
|
|
727
|
+
* Widens the walk for `v1-project-key` alone — see `walkSources`.
|
|
728
|
+
*
|
|
729
|
+
* `.prisma` belongs here for the reason `.yaml` does: a consumer copies the
|
|
730
|
+
* platform's models into their own `schema.prisma`, which is the documented
|
|
731
|
+
* integration path, so that file carries `projectKey` fields and composite
|
|
732
|
+
* indexes of its own. Left out of the walk they were neither rewritten nor
|
|
733
|
+
* reported — and after the SQL migration drops the columns, a schema still
|
|
734
|
+
* declaring them generates a client that queries them, while the next
|
|
735
|
+
* `db push` tries to put them back.
|
|
736
|
+
*
|
|
737
|
+
* Reported rather than rewritten, like every other declaration: a `.prisma`
|
|
738
|
+
* model is a schema, and which of its fields a consumer still needs is theirs.
|
|
739
|
+
*/
|
|
740
|
+
const CODEMOD_CONFIG_EXTENSIONS = /\.(yaml|yml|prisma)$/;
|
|
681
741
|
/** Walked for the package renames alone; see `rewriteManifest`. */
|
|
682
742
|
const CODEMOD_MANIFEST = 'package.json';
|
|
683
743
|
|
|
684
|
-
/**
|
|
685
|
-
|
|
744
|
+
/**
|
|
745
|
+
* Every source file under `root` a codemod may touch, with its text.
|
|
746
|
+
*
|
|
747
|
+
* `extra` widens the set for one codemod. Only `v1-project-key` passes it, and
|
|
748
|
+
* only for `.yaml`: the other two rewrite identifiers and import specifiers,
|
|
749
|
+
* and letting them loose on a configuration file would corrupt it.
|
|
750
|
+
*/
|
|
751
|
+
async function walkSources(root, visit, extra = null) {
|
|
686
752
|
const walk = async (dir) => {
|
|
687
753
|
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
688
754
|
if (CODEMOD_SKIP.has(entry.name) || isBuildOutput(entry.name)) continue;
|
|
@@ -691,13 +757,151 @@ async function walkSources(root, visit) {
|
|
|
691
757
|
await walk(full);
|
|
692
758
|
continue;
|
|
693
759
|
}
|
|
694
|
-
|
|
760
|
+
const included =
|
|
761
|
+
CODEMOD_EXTENSIONS.test(entry.name) ||
|
|
762
|
+
entry.name === CODEMOD_MANIFEST ||
|
|
763
|
+
(extra !== null && extra.test(entry.name));
|
|
764
|
+
if (!included) continue;
|
|
695
765
|
await visit(full, await readFile(full, 'utf8'));
|
|
696
766
|
}
|
|
697
767
|
};
|
|
698
768
|
await walk(root);
|
|
699
769
|
}
|
|
700
770
|
|
|
771
|
+
/**
|
|
772
|
+
* Takes `projectKey` out of a consumer's code, and names what it will not.
|
|
773
|
+
*
|
|
774
|
+
* A removal is not a rename: the word is an ordinary property name. Two forms
|
|
775
|
+
* need no grammar to decide — a `?projectKey=` on a `/catalog/` URL, and the
|
|
776
|
+
* top-level key of a `saas.yaml` — and those are rewritten. An object member is
|
|
777
|
+
* reported: an object literal and a type literal are lexically identical in
|
|
778
|
+
* TypeScript, so removing one would sometimes delete a member of the consumer's
|
|
779
|
+
* own type. The migration guide's table says what each shape becomes.
|
|
780
|
+
*/
|
|
781
|
+
/**
|
|
782
|
+
* Names the module options that moved into `config/saas.yaml`.
|
|
783
|
+
*
|
|
784
|
+
* Read-only on purpose — see `codemods/v1-moved-settings.ts` for why removing
|
|
785
|
+
* them would lose the decision instead of moving it. `--dry-run` is accepted
|
|
786
|
+
* and does nothing, so `codemod v1 --dry-run` behaves the same throughout.
|
|
787
|
+
*/
|
|
788
|
+
async function cmdCodemodV1MovedSettings(args) {
|
|
789
|
+
const root = resolve(args.dir ?? '.');
|
|
790
|
+
const found = [];
|
|
791
|
+
const blocks = [];
|
|
792
|
+
|
|
793
|
+
await walkSources(root, async (full, source) => {
|
|
794
|
+
// Code only. The walk includes Markdown, and a documentation file that
|
|
795
|
+
// mentions a setting is not a file that passes one — see
|
|
796
|
+
// `SCANNED_FOR_MOVED_SETTINGS`.
|
|
797
|
+
if (!SCANNED_FOR_MOVED_SETTINGS.test(full)) return;
|
|
798
|
+
for (const { setting, line } of findMovedSettings(source).occurrences) {
|
|
799
|
+
found.push({ where: `${relative(root, full)}:${line}`, setting });
|
|
800
|
+
}
|
|
801
|
+
if (!SCANNED_FOR_DB_CATALOG.test(full)) return;
|
|
802
|
+
for (const { shape, line, leftovers } of findDbCatalogBlocks(source).occurrences) {
|
|
803
|
+
blocks.push({ where: `${relative(root, full)}:${line}`, shape, leftovers });
|
|
804
|
+
}
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
if (found.length === 0) {
|
|
808
|
+
console.log('No module option that moved into config/saas.yaml is still passed.');
|
|
809
|
+
reportDbCatalogBlocks(blocks);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
console.log(`${found.length} occurrence(s) of a setting that moved:`);
|
|
814
|
+
const width = Math.max(...found.map((f) => f.where.length));
|
|
815
|
+
for (const { where, setting } of found) {
|
|
816
|
+
console.log(` ${where.padEnd(width)} ${setting}`);
|
|
817
|
+
}
|
|
818
|
+
console.log('');
|
|
819
|
+
console.log(' Not removed, and that is the point: the value is a term somebody agreed,');
|
|
820
|
+
console.log(' and deleting it here without writing it into the file would leave the');
|
|
821
|
+
console.log(' application running on whatever the file happens to say. Move each one:');
|
|
822
|
+
console.log('');
|
|
823
|
+
for (const setting of new Set(found.map((f) => f.setting))) {
|
|
824
|
+
console.log(` ${setting}`);
|
|
825
|
+
console.log(` ${WHERE_IT_GOES[setting]}`);
|
|
826
|
+
}
|
|
827
|
+
console.log('');
|
|
828
|
+
console.log(' TenantBillingModule.forRoot() refuses to boot while either is still');
|
|
829
|
+
console.log(' passed, so this cannot be half-done quietly.');
|
|
830
|
+
console.log('');
|
|
831
|
+
reportDbCatalogBlocks(blocks);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Names a `dbCatalog` that still carries the settings as values.
|
|
836
|
+
*
|
|
837
|
+
* The same decision as the settings above, one option up: the values were
|
|
838
|
+
* forwarded from `config/saas.yaml`, and the option names that file now. Not
|
|
839
|
+
* rewritten, because which file is a variable in another module more often
|
|
840
|
+
* than a literal here — see `codemods/v1-db-catalog.ts`.
|
|
841
|
+
*/
|
|
842
|
+
function reportDbCatalogBlocks(blocks) {
|
|
843
|
+
if (blocks.length === 0) {
|
|
844
|
+
console.log('No dbCatalog still carries the settings as values.');
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
console.log(`${blocks.length} dbCatalog block(s) to point at the file:`);
|
|
848
|
+
const width = Math.max(...blocks.map((b) => b.where.length));
|
|
849
|
+
for (const { where, shape, leftovers } of blocks) {
|
|
850
|
+
console.log(
|
|
851
|
+
` ${where.padEnd(width)} ${describeDbCatalogOccurrence({ shape, leftovers })}`,
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
console.log('');
|
|
855
|
+
console.log(` ${WHERE_DB_CATALOG_GOES}`);
|
|
856
|
+
console.log('');
|
|
857
|
+
console.log(' SaaSiCatModule.forRoot() refuses to boot while the values are still passed,');
|
|
858
|
+
console.log(' so this cannot be half-done quietly.');
|
|
859
|
+
console.log('');
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
async function cmdCodemodV1ProjectKey(args) {
|
|
863
|
+
const root = resolve(args.dir ?? '.');
|
|
864
|
+
const dryRun = args['dry-run'] === true;
|
|
865
|
+
|
|
866
|
+
const undecided = [];
|
|
867
|
+
let rewritten = 0;
|
|
868
|
+
let touched = 0;
|
|
869
|
+
await walkSources(
|
|
870
|
+
root,
|
|
871
|
+
async (full, source) => {
|
|
872
|
+
if (basename(full) === CODEMOD_MANIFEST) return;
|
|
873
|
+
const result = removeProjectKey(source, isCatalogConfig(full) ? 'yaml' : 'source');
|
|
874
|
+
for (const line of result.undecided) undecided.push(`${relative(root, full)}:${line}`);
|
|
875
|
+
if (result.rewritten === 0) return;
|
|
876
|
+
if (!dryRun) await writeFile(full, result.text);
|
|
877
|
+
rewritten += result.rewritten;
|
|
878
|
+
touched += 1;
|
|
879
|
+
},
|
|
880
|
+
CODEMOD_CONFIG_EXTENSIONS,
|
|
881
|
+
);
|
|
882
|
+
|
|
883
|
+
console.log(
|
|
884
|
+
`${dryRun ? 'Would remove' : 'Removed'} ${rewritten} occurrence(s) in ${touched} file(s).`,
|
|
885
|
+
);
|
|
886
|
+
if (undecided.length === 0) return;
|
|
887
|
+
|
|
888
|
+
console.log('');
|
|
889
|
+
console.log(`${undecided.length} occurrence(s) are yours to look at:`);
|
|
890
|
+
for (const where of undecided) console.log(` ${where}`);
|
|
891
|
+
console.log('');
|
|
892
|
+
console.log(' Two kinds of file end up here. In TypeScript an object literal and a type');
|
|
893
|
+
console.log(' literal are the same tokens, so this cannot tell a payload member from one');
|
|
894
|
+
console.log(' of your own declarations without parsing. And a `.prisma` model is a schema:');
|
|
895
|
+
console.log(" which of its fields you still need is yours to say, not this tool's.");
|
|
896
|
+
console.log(' It reports rather than guesses — docs/guides/upgrade-to-1.0.md has a table');
|
|
897
|
+
console.log(' of what each shape becomes.');
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/** A `saas.yaml`, whose top-level `projectKey:` needs no anchor. */
|
|
901
|
+
function isCatalogConfig(full) {
|
|
902
|
+
return basename(full) === 'saas.yaml' || basename(full) === 'saas.yml';
|
|
903
|
+
}
|
|
904
|
+
|
|
701
905
|
/** Reads a repeated flag (`--quota=a --quota=b`) off argv. */
|
|
702
906
|
function repeatedFlag(argv, name) {
|
|
703
907
|
return argv
|
|
@@ -706,17 +910,17 @@ function repeatedFlag(argv, name) {
|
|
|
706
910
|
}
|
|
707
911
|
|
|
708
912
|
async function cmdInit(args, argv) {
|
|
709
|
-
if (!args['
|
|
710
|
-
console.error('✗ --
|
|
711
|
-
console.error(' It
|
|
913
|
+
if (!args['app-key']) {
|
|
914
|
+
console.error('✗ --app-key=<key> is required.');
|
|
915
|
+
console.error(' It is the slug of this application: npm package name, storage prefix.');
|
|
712
916
|
process.exit(1);
|
|
713
917
|
}
|
|
714
918
|
|
|
715
919
|
// 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
|
-
//
|
|
920
|
+
// through the top-level handler's 99. The rule itself is in
|
|
921
|
+
// src/init/catalog-keys.ts.
|
|
718
922
|
try {
|
|
719
|
-
|
|
923
|
+
assertValidAppKey(args['app-key']);
|
|
720
924
|
} catch (err) {
|
|
721
925
|
console.error(`✗ ${err.message}`);
|
|
722
926
|
process.exit(1);
|
|
@@ -741,7 +945,7 @@ async function cmdInit(args, argv) {
|
|
|
741
945
|
}
|
|
742
946
|
|
|
743
947
|
const plan = planInit({
|
|
744
|
-
|
|
948
|
+
appKey: args['app-key'],
|
|
745
949
|
appName: args['app-name'],
|
|
746
950
|
apiBase: args['api-base'],
|
|
747
951
|
quotas: repeatedFlag(argv, 'quota'),
|
|
@@ -793,6 +997,8 @@ async function cmdInit(args, argv) {
|
|
|
793
997
|
for (const w of writes) console.log(` ${w.path}`);
|
|
794
998
|
}
|
|
795
999
|
|
|
1000
|
+
reportSettings(writes, root);
|
|
1001
|
+
|
|
796
1002
|
await patchAppModuleFile(root, plan, args);
|
|
797
1003
|
|
|
798
1004
|
console.log('');
|
|
@@ -814,6 +1020,33 @@ async function cmdInit(args, argv) {
|
|
|
814
1020
|
}
|
|
815
1021
|
}
|
|
816
1022
|
|
|
1023
|
+
/**
|
|
1024
|
+
* Says which settings the catalogue now carries, with their values and its path.
|
|
1025
|
+
*
|
|
1026
|
+
* These are required fields without defaults, and there is no second place they
|
|
1027
|
+
* can come from. Somebody who has just run `init` should not have to find that
|
|
1028
|
+
* out from a boot failure six weeks later — so the command names the file it
|
|
1029
|
+
* put them in while they are still looking at the output.
|
|
1030
|
+
*
|
|
1031
|
+
* Derived in `init/settings-written.ts`, where it can be tested: this file is
|
|
1032
|
+
* not.
|
|
1033
|
+
*/
|
|
1034
|
+
function reportSettings(writes, root) {
|
|
1035
|
+
const catalog = writes.find((w) => w.path === 'config/saas.yaml');
|
|
1036
|
+
if (!catalog) return;
|
|
1037
|
+
const settings = settingsWrittenTo(catalog.content, catalog.path);
|
|
1038
|
+
if (settings.length === 0) return;
|
|
1039
|
+
|
|
1040
|
+
const width = Math.max(...settings.map((s) => s.key.length));
|
|
1041
|
+
console.log('');
|
|
1042
|
+
console.log(`Settings written to ${join(root, catalog.path)}:`);
|
|
1043
|
+
for (const { key, value } of settings) {
|
|
1044
|
+
console.log(` ${key.padEnd(width)} ${value}`);
|
|
1045
|
+
}
|
|
1046
|
+
console.log(' This file is where they live. Editing it is how they change, and the');
|
|
1047
|
+
console.log(' change lands on the next restart — the platform reads it at boot.');
|
|
1048
|
+
}
|
|
1049
|
+
|
|
817
1050
|
/**
|
|
818
1051
|
* The TypeScript that will compile the project: the consumer's own, resolved
|
|
819
1052
|
* from the project root, so the tsconfig is read the way the build reads it.
|
|
@@ -881,11 +1114,19 @@ async function main() {
|
|
|
881
1114
|
if (cmd === 'codemod' && sub === 'v1-rename') {
|
|
882
1115
|
return cmdCodemodV1Rename(parseArgs(rest));
|
|
883
1116
|
}
|
|
1117
|
+
if (cmd === 'codemod' && sub === 'v1-project-key') {
|
|
1118
|
+
return cmdCodemodV1ProjectKey(parseArgs(rest));
|
|
1119
|
+
}
|
|
1120
|
+
if (cmd === 'codemod' && sub === 'v1-moved-settings') {
|
|
1121
|
+
return cmdCodemodV1MovedSettings(parseArgs(rest));
|
|
1122
|
+
}
|
|
884
1123
|
if (cmd === 'codemod' && sub === 'v1') {
|
|
885
1124
|
// Imports first: the rename table keys its per-entry tokens by the
|
|
886
1125
|
// specifier they are imported from, which the import rewrite settles.
|
|
887
1126
|
await cmdCodemodV1Imports(parseArgs(rest));
|
|
888
|
-
|
|
1127
|
+
await cmdCodemodV1Rename(parseArgs(rest));
|
|
1128
|
+
await cmdCodemodV1ProjectKey(parseArgs(rest));
|
|
1129
|
+
return cmdCodemodV1MovedSettings(parseArgs(rest));
|
|
889
1130
|
}
|
|
890
1131
|
if (cmd === 'init') {
|
|
891
1132
|
return cmdInit(parseArgs([sub, ...rest].filter(Boolean)), process.argv.slice(3));
|
|
@@ -914,18 +1155,23 @@ async function main() {
|
|
|
914
1155
|
' schema migrate --name=<name> apply --all + migrate dev + constraints',
|
|
915
1156
|
);
|
|
916
1157
|
console.log('');
|
|
917
|
-
console.log(' init --
|
|
1158
|
+
console.log(' init --app-key=<key> --quota=<key>:<Model>');
|
|
918
1159
|
console.log(' scaffold the platform wiring. At least one --quota:');
|
|
919
1160
|
console.log(' every plan must declare one, or the catalogue does not load.');
|
|
920
|
-
console.log(' init --
|
|
1161
|
+
console.log(' init --app-key=myapp --quota=notes:Note --quota=seats:Seat');
|
|
921
1162
|
console.log('');
|
|
922
1163
|
console.log(' codemod v1 [--dir=.] [--dry-run]');
|
|
923
|
-
console.log(' the whole 1.0 migration:
|
|
1164
|
+
console.log(' the whole 1.0 migration: imports, names, projectKey, then the');
|
|
1165
|
+
console.log(' settings that moved into config/saas.yaml');
|
|
924
1166
|
console.log(' codemod v1-imports [--dir=.] [--dry-run]');
|
|
925
1167
|
console.log(' rewrite @saasicat/ui-vue imports to the 1.0 export map');
|
|
926
1168
|
console.log(' codemod v1-rename [--dir=.] [--dry-run]');
|
|
927
1169
|
console.log(' rewrite the names 1.0 changed: SaasPlatform*, Symbol.for keys,');
|
|
928
1170
|
console.log(' FEATURE_UI_REGISTRY_TOKEN, @saasicat/ui-vue/testing-e2e/*');
|
|
1171
|
+
console.log(' codemod v1-moved-settings [--dir=.]');
|
|
1172
|
+
console.log(' name the module options that moved into config/saas.yaml.');
|
|
1173
|
+
console.log(' Reports only — the value is a commercial decision, and this');
|
|
1174
|
+
console.log(' would delete it without writing it anywhere.');
|
|
929
1175
|
console.log('');
|
|
930
1176
|
console.log('Optional --prisma-schema=PATH (default prisma/schema.prisma).');
|
|
931
1177
|
console.log('Optional --tenant-model=X --user-model=Y for apply/migrate — enables');
|
package/dist/.build-stamp
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
89cdc61db4ad8f0d8bc18287c0d1fe76e1a7e9410f0797a525db4c9b1bbdc3ab
|