@saasicat/cli 0.10.0 → 0.11.0
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/bin/{saas-platform.js → saasicat.js} +107 -16
- package/dist/index.cjs +188 -34
- package/dist/index.d.cts +87 -7
- package/dist/index.d.ts +87 -7
- package/dist/index.js +179 -33
- package/package.json +5 -5
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// `
|
|
2
|
+
// `saasicat` — bootstrap CLI for the SaaSiCat framework.
|
|
3
3
|
//
|
|
4
4
|
// Sub-commands:
|
|
5
5
|
// schema apply [--prisma-schema=PATH] [--fragments=01,02,03]
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
// Inserts missing platform Prisma models into your schema.prisma.
|
|
8
8
|
// Idempotent; existing models are left untouched.
|
|
9
9
|
//
|
|
10
|
+
// schema check [--prisma-schema=PATH] [--fragments=01,02,03]
|
|
11
|
+
// Reports what your schema is missing relative to the canonical
|
|
12
|
+
// fragments. Read-only; exits 1 on drift so CI can gate on it.
|
|
13
|
+
//
|
|
10
14
|
// Spec: handoff/superadmin/QUICKSTART_SIMPLIFICATIONS.md §P5.
|
|
11
15
|
|
|
12
16
|
import { readFile, writeFile, readdir } from 'node:fs/promises';
|
|
@@ -15,7 +19,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
15
19
|
import { createRequire } from 'node:module';
|
|
16
20
|
import { spawn } from 'node:child_process';
|
|
17
21
|
|
|
18
|
-
import { applyFragmentBlocks, extractModelBlocks } from '../dist/index.js';
|
|
22
|
+
import { applyFragmentBlocks, checkSchema, extractModelBlocks } from '../dist/index.js';
|
|
19
23
|
|
|
20
24
|
const require_ = createRequire(import.meta.url);
|
|
21
25
|
|
|
@@ -46,14 +50,14 @@ function resolveFragmentsDir() {
|
|
|
46
50
|
return candidate;
|
|
47
51
|
}
|
|
48
52
|
|
|
49
|
-
async function
|
|
53
|
+
async function selectFragmentFiles(dir, filter) {
|
|
50
54
|
const files = (await readdir(dir)).filter((f) => f.endsWith('.prisma')).sort();
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
if (!filter) return files;
|
|
56
|
+
return files.filter((f) => filter.includes(f.split('-')[0]));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function loadFragments(dir, filter) {
|
|
60
|
+
const selected = await selectFragmentFiles(dir, filter);
|
|
57
61
|
const blocks = new Map();
|
|
58
62
|
for (const file of selected) {
|
|
59
63
|
const content = await readFile(join(dir, file), 'utf8');
|
|
@@ -67,17 +71,24 @@ async function loadFragments(dir, filter) {
|
|
|
67
71
|
return { files: selected, blocks };
|
|
68
72
|
}
|
|
69
73
|
|
|
70
|
-
|
|
74
|
+
function parseFragmentFilter(raw) {
|
|
75
|
+
return raw ? raw.split(',').map((s) => s.padStart(2, '0')) : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function readSchemaOrExit(args) {
|
|
71
79
|
const schemaPath = resolve(args['prisma-schema'] ?? 'prisma/schema.prisma');
|
|
72
80
|
if (!existsSync(schemaPath)) {
|
|
73
81
|
console.error(`✗ schema.prisma nicht gefunden: ${schemaPath}`);
|
|
74
82
|
process.exit(1);
|
|
75
83
|
}
|
|
84
|
+
return { schemaPath, schema: await readFile(schemaPath, 'utf8') };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function cmdSchemaApply(args) {
|
|
88
|
+
const { schemaPath, schema } = await readSchemaOrExit(args);
|
|
76
89
|
|
|
77
90
|
const fragmentsDir = resolveFragmentsDir();
|
|
78
|
-
const filter = args.fragments
|
|
79
|
-
? args.fragments.split(',').map((s) => s.padStart(2, '0'))
|
|
80
|
-
: null;
|
|
91
|
+
const filter = parseFragmentFilter(args.fragments);
|
|
81
92
|
if (!filter && !args.all) {
|
|
82
93
|
console.error(
|
|
83
94
|
'✗ Entweder --fragments=01,02,03 oder --all übergeben. ' +
|
|
@@ -94,7 +105,6 @@ async function cmdSchemaApply(args) {
|
|
|
94
105
|
process.exit(1);
|
|
95
106
|
}
|
|
96
107
|
|
|
97
|
-
const schema = await readFile(schemaPath, 'utf8');
|
|
98
108
|
const result = applyFragmentBlocks(schema, blocks, {
|
|
99
109
|
fragmentLabel: files.join(', '),
|
|
100
110
|
});
|
|
@@ -125,6 +135,82 @@ async function cmdSchemaApply(args) {
|
|
|
125
135
|
console.log(' 2. pnpm prisma migrate dev --name add_saas_platform');
|
|
126
136
|
}
|
|
127
137
|
|
|
138
|
+
const MISMATCH_LABELS = {
|
|
139
|
+
type: 'Typ',
|
|
140
|
+
optionality: 'Optionalität',
|
|
141
|
+
list: 'Liste',
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
function printCheckReport(report) {
|
|
145
|
+
if (report.missingFields.length > 0) {
|
|
146
|
+
console.log(`✗ Fehlende Felder (${report.missingFields.length}):`);
|
|
147
|
+
for (const { model, field, type } of report.missingFields) {
|
|
148
|
+
console.log(` ${`${model}.${field}`.padEnd(44)} ${type}`);
|
|
149
|
+
}
|
|
150
|
+
console.log('');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (report.missingEnumValues.length > 0) {
|
|
154
|
+
console.log(`✗ Fehlende Enum-Werte (${report.missingEnumValues.length}):`);
|
|
155
|
+
for (const entry of report.missingEnumValues) {
|
|
156
|
+
console.log(` ${entry.enum}.${entry.value}`);
|
|
157
|
+
}
|
|
158
|
+
console.log('');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (report.fieldMismatches.length > 0) {
|
|
162
|
+
console.log(`✗ Abweichende Felder (${report.fieldMismatches.length}):`);
|
|
163
|
+
for (const { model, field, reason, expected, actual } of report.fieldMismatches) {
|
|
164
|
+
const location = `${model}.${field}`.padEnd(44);
|
|
165
|
+
console.log(
|
|
166
|
+
` ${location} [${MISMATCH_LABELS[reason]}] erwartet ${expected}, vorhanden ${actual}`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
console.log('');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const absent = [...report.absentModels, ...report.absentEnums];
|
|
173
|
+
if (absent.length > 0) {
|
|
174
|
+
console.log(`→ Nicht übernommen (${absent.length}): ${absent.join(', ')}`);
|
|
175
|
+
console.log(' Kein Fehler — diese Fragmente nutzt die App nicht.');
|
|
176
|
+
console.log('');
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function cmdSchemaCheck(args) {
|
|
181
|
+
const { schemaPath, schema } = await readSchemaOrExit(args);
|
|
182
|
+
const fragmentsDir = resolveFragmentsDir();
|
|
183
|
+
const filter = parseFragmentFilter(args.fragments);
|
|
184
|
+
const files = await selectFragmentFiles(fragmentsDir, filter);
|
|
185
|
+
|
|
186
|
+
if (files.length === 0) {
|
|
187
|
+
console.error('✗ Keine Fragmente ausgewählt.');
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const fragments = [];
|
|
192
|
+
for (const file of files) {
|
|
193
|
+
fragments.push(await readFile(join(fragmentsDir, file), 'utf8'));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
console.log(`→ Prüfe ${schemaPath} gegen ${files.length} Fragment(e) aus @saasicat/spec`);
|
|
197
|
+
console.log('');
|
|
198
|
+
|
|
199
|
+
const report = checkSchema(fragments.join('\n'), schema);
|
|
200
|
+
printCheckReport(report);
|
|
201
|
+
|
|
202
|
+
const checked = `${report.checkedModelCount} Model(s), ${report.checkedEnumCount} Enum(s)`;
|
|
203
|
+
if (report.ok) {
|
|
204
|
+
console.log(`✓ Kein Drift. ${checked} geprüft.`);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
console.log(`✗ Drift gefunden (${checked} geprüft).`);
|
|
209
|
+
console.log(' Fehlende Felder und Enum-Werte ergänzen, dann migrieren.');
|
|
210
|
+
console.log(' Abweichende Felder prüfen: Plattform-Code liest sie mit dem Spec-Typ.');
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
|
|
128
214
|
function runChild(cmd, args, opts = {}) {
|
|
129
215
|
return new Promise((resolve_, reject) => {
|
|
130
216
|
const proc = spawn(cmd, args, { stdio: 'inherit', ...opts });
|
|
@@ -142,7 +228,7 @@ async function cmdSchemaMigrate(args) {
|
|
|
142
228
|
process.exit(1);
|
|
143
229
|
}
|
|
144
230
|
|
|
145
|
-
console.log(`→ Schritt 1/2:
|
|
231
|
+
console.log(`→ Schritt 1/2: saasicat schema apply ${args['fragments'] ? `--fragments=${args['fragments']}` : '--all'}`);
|
|
146
232
|
await cmdSchemaApply({
|
|
147
233
|
...args,
|
|
148
234
|
all: args['fragments'] ? undefined : true,
|
|
@@ -159,16 +245,21 @@ async function main() {
|
|
|
159
245
|
if (cmd === 'schema' && sub === 'apply') {
|
|
160
246
|
return cmdSchemaApply(parseArgs(rest));
|
|
161
247
|
}
|
|
248
|
+
if (cmd === 'schema' && sub === 'check') {
|
|
249
|
+
return cmdSchemaCheck(parseArgs(rest));
|
|
250
|
+
}
|
|
162
251
|
if (cmd === 'schema' && sub === 'migrate') {
|
|
163
252
|
return cmdSchemaMigrate(parseArgs(rest));
|
|
164
253
|
}
|
|
165
254
|
if (cmd === '--help' || cmd === '-h' || !cmd) {
|
|
166
|
-
console.log('Usage:
|
|
255
|
+
console.log('Usage: saasicat <command> [...args]');
|
|
167
256
|
console.log('');
|
|
168
257
|
console.log('Commands:');
|
|
169
258
|
console.log(' schema apply --all alle Plattform-Models einfügen');
|
|
170
259
|
console.log(' schema apply --fragments=01,02 nur diese Fragmente einfügen');
|
|
171
260
|
console.log(' schema apply --dry-run nur Diff ausgeben');
|
|
261
|
+
console.log(' schema check Drift gegen @saasicat/spec melden');
|
|
262
|
+
console.log(' schema check --fragments=01,02 nur diese Fragmente prüfen');
|
|
172
263
|
console.log(' schema migrate --name=<name> apply --all + prisma migrate dev');
|
|
173
264
|
console.log('');
|
|
174
265
|
console.log('Optional --prisma-schema=PATH (default prisma/schema.prisma).');
|
package/dist/index.cjs
CHANGED
|
@@ -67,8 +67,16 @@ __export(index_exports, {
|
|
|
67
67
|
UserPortDoctorCheck: () => UserPortDoctorCheck,
|
|
68
68
|
WhoAmIFlow: () => WhoAmIFlow,
|
|
69
69
|
applyFragmentBlocks: () => applyFragmentBlocks,
|
|
70
|
+
blockBodyLines: () => blockBodyLines,
|
|
71
|
+
checkSchema: () => checkSchema,
|
|
72
|
+
extractBlockNames: () => extractBlockNames,
|
|
73
|
+
extractBlocks: () => extractBlocks,
|
|
70
74
|
extractModelBlocks: () => extractModelBlocks,
|
|
71
|
-
extractModelNames: () => extractModelNames
|
|
75
|
+
extractModelNames: () => extractModelNames,
|
|
76
|
+
parseEnumValues: () => parseEnumValues,
|
|
77
|
+
parseFields: () => parseFields,
|
|
78
|
+
parseSchema: () => parseSchema,
|
|
79
|
+
stripLineComment: () => stripLineComment
|
|
72
80
|
});
|
|
73
81
|
module.exports = __toCommonJS(index_exports);
|
|
74
82
|
|
|
@@ -1099,51 +1107,48 @@ var PLATFORM_DOCTOR_CHECK_PROVIDERS = [
|
|
|
1099
1107
|
AdminManifestDoctorCheck
|
|
1100
1108
|
];
|
|
1101
1109
|
|
|
1102
|
-
// src/
|
|
1110
|
+
// src/prisma-blocks.ts
|
|
1103
1111
|
function stripLineComment(line) {
|
|
1104
1112
|
const commentStart = line.indexOf("//");
|
|
1105
1113
|
return commentStart === -1 ? line : line.slice(0, commentStart);
|
|
1106
1114
|
}
|
|
1107
1115
|
__name(stripLineComment, "stripLineComment");
|
|
1108
|
-
function
|
|
1116
|
+
function declarationPattern(keyword) {
|
|
1117
|
+
return new RegExp(`^\\s*${keyword}\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\{`);
|
|
1118
|
+
}
|
|
1119
|
+
__name(declarationPattern, "declarationPattern");
|
|
1120
|
+
function extractBlockNames(schema, keyword) {
|
|
1121
|
+
const pattern = declarationPattern(keyword);
|
|
1109
1122
|
const names = [];
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1112
|
-
const stripped = stripLineComment(line).trim();
|
|
1113
|
-
const match = stripped.match(/^model\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/);
|
|
1123
|
+
for (const line of schema.split("\n")) {
|
|
1124
|
+
const match = stripLineComment(line).match(pattern);
|
|
1114
1125
|
if (match) names.push(match[1]);
|
|
1115
1126
|
}
|
|
1116
1127
|
return names;
|
|
1117
1128
|
}
|
|
1118
|
-
__name(
|
|
1119
|
-
function
|
|
1129
|
+
__name(extractBlockNames, "extractBlockNames");
|
|
1130
|
+
function extractBlocks(schema, keyword) {
|
|
1131
|
+
const pattern = declarationPattern(keyword);
|
|
1120
1132
|
const blocks = /* @__PURE__ */ new Map();
|
|
1121
|
-
const lines = fragment.split("\n");
|
|
1122
1133
|
let current = null;
|
|
1123
|
-
for (const rawLine of
|
|
1134
|
+
for (const rawLine of schema.split("\n")) {
|
|
1124
1135
|
const stripped = stripLineComment(rawLine);
|
|
1136
|
+
const openCount = (stripped.match(/\{/g) ?? []).length;
|
|
1137
|
+
const closeCount = (stripped.match(/\}/g) ?? []).length;
|
|
1125
1138
|
if (!current) {
|
|
1126
|
-
const match =
|
|
1127
|
-
if (match)
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
blocks.set(current.name, current.lines.join("\n"));
|
|
1139
|
-
current = null;
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
continue;
|
|
1139
|
+
const match = stripped.match(pattern);
|
|
1140
|
+
if (!match) continue;
|
|
1141
|
+
current = {
|
|
1142
|
+
name: match[1],
|
|
1143
|
+
lines: [
|
|
1144
|
+
rawLine
|
|
1145
|
+
],
|
|
1146
|
+
depth: openCount - closeCount
|
|
1147
|
+
};
|
|
1148
|
+
} else {
|
|
1149
|
+
current.lines.push(rawLine);
|
|
1150
|
+
current.depth += openCount - closeCount;
|
|
1143
1151
|
}
|
|
1144
|
-
current.lines.push(rawLine);
|
|
1145
|
-
current.depth += (stripped.match(/\{/g) ?? []).length;
|
|
1146
|
-
current.depth -= (stripped.match(/\}/g) ?? []).length;
|
|
1147
1152
|
if (current.depth <= 0) {
|
|
1148
1153
|
blocks.set(current.name, current.lines.join("\n"));
|
|
1149
1154
|
current = null;
|
|
@@ -1151,6 +1156,23 @@ function extractModelBlocks(fragment) {
|
|
|
1151
1156
|
}
|
|
1152
1157
|
return blocks;
|
|
1153
1158
|
}
|
|
1159
|
+
__name(extractBlocks, "extractBlocks");
|
|
1160
|
+
function blockBodyLines(block) {
|
|
1161
|
+
const bodyStart = block.indexOf("{");
|
|
1162
|
+
const bodyEnd = block.lastIndexOf("}");
|
|
1163
|
+
if (bodyStart === -1 || bodyEnd <= bodyStart) return [];
|
|
1164
|
+
return block.slice(bodyStart + 1, bodyEnd).split("\n").map((line) => stripLineComment(line).trim()).filter((line) => line.length > 0 && !line.startsWith("@@"));
|
|
1165
|
+
}
|
|
1166
|
+
__name(blockBodyLines, "blockBodyLines");
|
|
1167
|
+
|
|
1168
|
+
// src/schema-apply.ts
|
|
1169
|
+
function extractModelNames(schema) {
|
|
1170
|
+
return extractBlockNames(schema, "model");
|
|
1171
|
+
}
|
|
1172
|
+
__name(extractModelNames, "extractModelNames");
|
|
1173
|
+
function extractModelBlocks(fragment) {
|
|
1174
|
+
return extractBlocks(fragment, "model");
|
|
1175
|
+
}
|
|
1154
1176
|
__name(extractModelBlocks, "extractModelBlocks");
|
|
1155
1177
|
function applyFragmentBlocks(schema, fragmentBlocks, options = {}) {
|
|
1156
1178
|
const existing = new Set(extractModelNames(schema));
|
|
@@ -1175,11 +1197,11 @@ function applyFragmentBlocks(schema, fragmentBlocks, options = {}) {
|
|
|
1175
1197
|
const header = options.fragmentLabel ? `
|
|
1176
1198
|
|
|
1177
1199
|
// ============================================================
|
|
1178
|
-
// Eingef\xFCgt durch \`
|
|
1200
|
+
// Eingef\xFCgt durch \`saasicat schema apply\` aus ${options.fragmentLabel}
|
|
1179
1201
|
// ============================================================
|
|
1180
1202
|
` : `
|
|
1181
1203
|
|
|
1182
|
-
// Eingef\xFCgt durch \`
|
|
1204
|
+
// Eingef\xFCgt durch \`saasicat schema apply\`
|
|
1183
1205
|
`;
|
|
1184
1206
|
const trimmedSchema = schema.endsWith("\n") ? schema : schema + "\n";
|
|
1185
1207
|
return {
|
|
@@ -1190,6 +1212,130 @@ function applyFragmentBlocks(schema, fragmentBlocks, options = {}) {
|
|
|
1190
1212
|
}
|
|
1191
1213
|
__name(applyFragmentBlocks, "applyFragmentBlocks");
|
|
1192
1214
|
|
|
1215
|
+
// src/schema-check.ts
|
|
1216
|
+
function renderType(signature) {
|
|
1217
|
+
return `${signature.type}${signature.list ? "[]" : ""}${signature.optional ? "?" : ""}`;
|
|
1218
|
+
}
|
|
1219
|
+
__name(renderType, "renderType");
|
|
1220
|
+
function parseFields(block) {
|
|
1221
|
+
const fields = /* @__PURE__ */ new Map();
|
|
1222
|
+
for (const line of blockBodyLines(block)) {
|
|
1223
|
+
const [name, rawType] = line.split(/\s+/);
|
|
1224
|
+
if (!rawType || name.startsWith("@")) continue;
|
|
1225
|
+
const list = rawType.includes("[]");
|
|
1226
|
+
const optional = rawType.endsWith("?");
|
|
1227
|
+
fields.set(name, {
|
|
1228
|
+
name,
|
|
1229
|
+
type: rawType.replace("[]", "").replace("?", ""),
|
|
1230
|
+
optional,
|
|
1231
|
+
list
|
|
1232
|
+
});
|
|
1233
|
+
}
|
|
1234
|
+
return fields;
|
|
1235
|
+
}
|
|
1236
|
+
__name(parseFields, "parseFields");
|
|
1237
|
+
function parseEnumValues(block) {
|
|
1238
|
+
const values = [];
|
|
1239
|
+
for (const line of blockBodyLines(block)) {
|
|
1240
|
+
for (const token of line.split(/\s+/)) {
|
|
1241
|
+
if (token.startsWith("@")) break;
|
|
1242
|
+
if (token.length > 0) values.push(token);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
return values;
|
|
1246
|
+
}
|
|
1247
|
+
__name(parseEnumValues, "parseEnumValues");
|
|
1248
|
+
function parseSchema(schema) {
|
|
1249
|
+
const models = /* @__PURE__ */ new Map();
|
|
1250
|
+
for (const [name, block] of extractBlocks(schema, "model")) {
|
|
1251
|
+
models.set(name, parseFields(block));
|
|
1252
|
+
}
|
|
1253
|
+
const enums = /* @__PURE__ */ new Map();
|
|
1254
|
+
for (const [name, block] of extractBlocks(schema, "enum")) {
|
|
1255
|
+
enums.set(name, parseEnumValues(block));
|
|
1256
|
+
}
|
|
1257
|
+
return {
|
|
1258
|
+
models,
|
|
1259
|
+
enums
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
__name(parseSchema, "parseSchema");
|
|
1263
|
+
function isDocumentedTypeSubstitution(spec, app, appEnums) {
|
|
1264
|
+
return spec.type === "String" && appEnums.has(app.type);
|
|
1265
|
+
}
|
|
1266
|
+
__name(isDocumentedTypeSubstitution, "isDocumentedTypeSubstitution");
|
|
1267
|
+
function compareFields(model, specFields, appFields, appEnums, missingFields, fieldMismatches) {
|
|
1268
|
+
for (const [name, spec] of specFields) {
|
|
1269
|
+
const app = appFields.get(name);
|
|
1270
|
+
if (!app) {
|
|
1271
|
+
missingFields.push({
|
|
1272
|
+
model,
|
|
1273
|
+
field: name,
|
|
1274
|
+
type: renderType(spec)
|
|
1275
|
+
});
|
|
1276
|
+
continue;
|
|
1277
|
+
}
|
|
1278
|
+
const mismatch = /* @__PURE__ */ __name((reason) => ({
|
|
1279
|
+
model,
|
|
1280
|
+
field: name,
|
|
1281
|
+
reason,
|
|
1282
|
+
expected: renderType(spec),
|
|
1283
|
+
actual: renderType(app)
|
|
1284
|
+
}), "mismatch");
|
|
1285
|
+
if (spec.type !== app.type && !isDocumentedTypeSubstitution(spec, app, appEnums)) {
|
|
1286
|
+
fieldMismatches.push(mismatch("type"));
|
|
1287
|
+
} else if (spec.list !== app.list) {
|
|
1288
|
+
fieldMismatches.push(mismatch("list"));
|
|
1289
|
+
} else if (!spec.optional && app.optional) {
|
|
1290
|
+
fieldMismatches.push(mismatch("optionality"));
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
__name(compareFields, "compareFields");
|
|
1295
|
+
function checkSchema(specSchema, appSchema) {
|
|
1296
|
+
const spec = parseSchema(specSchema);
|
|
1297
|
+
const app = parseSchema(appSchema);
|
|
1298
|
+
const absentModels = [];
|
|
1299
|
+
const missingFields = [];
|
|
1300
|
+
const fieldMismatches = [];
|
|
1301
|
+
for (const [model, specFields] of spec.models) {
|
|
1302
|
+
const appFields = app.models.get(model);
|
|
1303
|
+
if (!appFields) {
|
|
1304
|
+
absentModels.push(model);
|
|
1305
|
+
continue;
|
|
1306
|
+
}
|
|
1307
|
+
compareFields(model, specFields, appFields, app.enums, missingFields, fieldMismatches);
|
|
1308
|
+
}
|
|
1309
|
+
const absentEnums = [];
|
|
1310
|
+
const missingEnumValues = [];
|
|
1311
|
+
for (const [name, specValues] of spec.enums) {
|
|
1312
|
+
const appValues = app.enums.get(name);
|
|
1313
|
+
if (!appValues) {
|
|
1314
|
+
absentEnums.push(name);
|
|
1315
|
+
continue;
|
|
1316
|
+
}
|
|
1317
|
+
for (const value of specValues) {
|
|
1318
|
+
if (!appValues.includes(value)) {
|
|
1319
|
+
missingEnumValues.push({
|
|
1320
|
+
enum: name,
|
|
1321
|
+
value
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
return {
|
|
1327
|
+
absentModels,
|
|
1328
|
+
absentEnums,
|
|
1329
|
+
missingFields,
|
|
1330
|
+
missingEnumValues,
|
|
1331
|
+
fieldMismatches,
|
|
1332
|
+
checkedModelCount: spec.models.size - absentModels.length,
|
|
1333
|
+
checkedEnumCount: spec.enums.size - absentEnums.length,
|
|
1334
|
+
ok: missingFields.length === 0 && missingEnumValues.length === 0 && fieldMismatches.length === 0
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
__name(checkSchema, "checkSchema");
|
|
1338
|
+
|
|
1193
1339
|
// src/module.ts
|
|
1194
1340
|
var import_common8 = require("@nestjs/common");
|
|
1195
1341
|
var import_nest5 = require("@saasicat/nest");
|
|
@@ -2279,6 +2425,14 @@ UserCommands = _ts_decorate14([
|
|
|
2279
2425
|
UserPortDoctorCheck,
|
|
2280
2426
|
WhoAmIFlow,
|
|
2281
2427
|
applyFragmentBlocks,
|
|
2428
|
+
blockBodyLines,
|
|
2429
|
+
checkSchema,
|
|
2430
|
+
extractBlockNames,
|
|
2431
|
+
extractBlocks,
|
|
2282
2432
|
extractModelBlocks,
|
|
2283
|
-
extractModelNames
|
|
2433
|
+
extractModelNames,
|
|
2434
|
+
parseEnumValues,
|
|
2435
|
+
parseFields,
|
|
2436
|
+
parseSchema,
|
|
2437
|
+
stripLineComment
|
|
2284
2438
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -326,17 +326,38 @@ declare class AdminManifestDoctorCheck implements DoctorCheck {
|
|
|
326
326
|
*/
|
|
327
327
|
declare const PLATFORM_DOCTOR_CHECK_PROVIDERS: Array<Type<DoctorCheck>>;
|
|
328
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Cuts off a trailing `//` comment. Deliberately index-based instead of
|
|
331
|
+
* `replace(/\/\/.*$/, '')`: the regex backtracks quadratically on lines with
|
|
332
|
+
* many single slashes.
|
|
333
|
+
*/
|
|
334
|
+
declare function stripLineComment(line: string): string;
|
|
335
|
+
/** Returns the names of all top-level `<keyword> X { … }` blocks. */
|
|
336
|
+
declare function extractBlockNames(schema: string, keyword: string): string[];
|
|
337
|
+
/**
|
|
338
|
+
* Returns all top-level `<keyword> X { … }` blocks as a map
|
|
339
|
+
* `name -> complete block text incl. opening/closing braces`.
|
|
340
|
+
*
|
|
341
|
+
* Delimiting logic: we search for `^<keyword> X {` (start of line, possibly
|
|
342
|
+
* with leading whitespace) and close as soon as the brace depth drops back
|
|
343
|
+
* to 0. Braces inside comments are not counted.
|
|
344
|
+
*/
|
|
345
|
+
declare function extractBlocks(schema: string, keyword: string): Map<string, string>;
|
|
346
|
+
/**
|
|
347
|
+
* Returns a block's body lines — comments stripped, blank lines, block-level
|
|
348
|
+
* attributes (`@@index`, `@@map`) and the enclosing braces removed.
|
|
349
|
+
*
|
|
350
|
+
* Cuts between the first `{` and the last `}` rather than dropping the first
|
|
351
|
+
* and last line, so single-line blocks (`model X { id String @id }`) yield
|
|
352
|
+
* their body too.
|
|
353
|
+
*/
|
|
354
|
+
declare function blockBodyLines(block: string): string[];
|
|
355
|
+
|
|
329
356
|
/** Returns the names of all top-level `model X { ... }` blocks in the schema. */
|
|
330
357
|
declare function extractModelNames(schema: string): string[];
|
|
331
358
|
/**
|
|
332
359
|
* Returns all `model X { ... }` blocks from a fragment as a map
|
|
333
360
|
* `name -> complete block text incl. opening/closing braces`.
|
|
334
|
-
*
|
|
335
|
-
* Delimiting logic: we search for `^model X {` (start of line, possibly with
|
|
336
|
-
* leading whitespace) and close as soon as the brace depth drops back to
|
|
337
|
-
* 0. Strings/comments inside the block are, in simplified terms, not
|
|
338
|
-
* counted, which is robust for Prisma schemas (no curly braces
|
|
339
|
-
* in strings, comments only `//`-style).
|
|
340
361
|
*/
|
|
341
362
|
declare function extractModelBlocks(fragment: string): Map<string, string>;
|
|
342
363
|
interface ApplyResult {
|
|
@@ -355,6 +376,65 @@ declare function applyFragmentBlocks(schema: string, fragmentBlocks: Map<string,
|
|
|
355
376
|
fragmentLabel?: string;
|
|
356
377
|
}): ApplyResult;
|
|
357
378
|
|
|
379
|
+
interface FieldSignature {
|
|
380
|
+
name: string;
|
|
381
|
+
/** Type without `?`/`[]` modifiers, e.g. `String`, `BillingCycle`. */
|
|
382
|
+
type: string;
|
|
383
|
+
optional: boolean;
|
|
384
|
+
list: boolean;
|
|
385
|
+
}
|
|
386
|
+
interface ParsedSchema {
|
|
387
|
+
models: Map<string, Map<string, FieldSignature>>;
|
|
388
|
+
enums: Map<string, string[]>;
|
|
389
|
+
}
|
|
390
|
+
interface MissingField {
|
|
391
|
+
model: string;
|
|
392
|
+
field: string;
|
|
393
|
+
/** Rendered spec type incl. modifiers, e.g. `DateTime?`. */
|
|
394
|
+
type: string;
|
|
395
|
+
}
|
|
396
|
+
interface MissingEnumValue {
|
|
397
|
+
enum: string;
|
|
398
|
+
value: string;
|
|
399
|
+
}
|
|
400
|
+
type FieldMismatchReason = 'type' | 'optionality' | 'list';
|
|
401
|
+
interface FieldMismatch {
|
|
402
|
+
model: string;
|
|
403
|
+
field: string;
|
|
404
|
+
reason: FieldMismatchReason;
|
|
405
|
+
expected: string;
|
|
406
|
+
actual: string;
|
|
407
|
+
}
|
|
408
|
+
interface SchemaCheckReport {
|
|
409
|
+
/** Platform models the consumer does not carry — informational. */
|
|
410
|
+
absentModels: string[];
|
|
411
|
+
/** Platform enums the consumer does not carry — informational. */
|
|
412
|
+
absentEnums: string[];
|
|
413
|
+
missingFields: MissingField[];
|
|
414
|
+
missingEnumValues: MissingEnumValue[];
|
|
415
|
+
fieldMismatches: FieldMismatch[];
|
|
416
|
+
/** Models present in both schemas, i.e. actually compared. */
|
|
417
|
+
checkedModelCount: number;
|
|
418
|
+
/** Enums present in both schemas, i.e. actually compared. */
|
|
419
|
+
checkedEnumCount: number;
|
|
420
|
+
/** True when nothing that breaks platform code was found. */
|
|
421
|
+
ok: boolean;
|
|
422
|
+
}
|
|
423
|
+
/** Parses the field lines of a `model` block into signatures, keyed by name. */
|
|
424
|
+
declare function parseFields(block: string): Map<string, FieldSignature>;
|
|
425
|
+
/**
|
|
426
|
+
* Parses the members of an `enum` block. Values may share a line
|
|
427
|
+
* (`enum Role { ADMIN USER }`); a trailing `@map(…)` attribute is not a value.
|
|
428
|
+
*/
|
|
429
|
+
declare function parseEnumValues(block: string): string[];
|
|
430
|
+
/** Parses every `model` and `enum` declaration of a Prisma schema. */
|
|
431
|
+
declare function parseSchema(schema: string): ParsedSchema;
|
|
432
|
+
/**
|
|
433
|
+
* Compares a consumer schema against the canonical fragments. `specSchema` is
|
|
434
|
+
* the concatenation of the fragments the check should cover.
|
|
435
|
+
*/
|
|
436
|
+
declare function checkSchema(specSchema: string, appSchema: string): SchemaCheckReport;
|
|
437
|
+
|
|
358
438
|
interface CliContextModuleOptions {
|
|
359
439
|
config: CliContextConfig;
|
|
360
440
|
userPort: ProviderSpec<UserPort>;
|
|
@@ -545,4 +625,4 @@ declare class UserCommands extends CommandRunner {
|
|
|
545
625
|
parsePassword(val: string): string;
|
|
546
626
|
}
|
|
547
627
|
|
|
548
|
-
export { AUDIT_QUERY_PORT_TOKEN, AdminCommands, AdminManifestDoctorCheck, AdminMfaSetupCommand, AdminWhoamiCommand, type ApplyResult, AuditCommands, AuditTailCommand, AuditTailFlow, type AuditTailOptions, CLI_CONTEXT_CONFIG_TOKEN, type CheckSeverity, type CliContextConfig, CliContextModule, type CliContextModuleOptions, CliContextService, CliError, type CliIdentity, DEFAULT_MANIFEST_CHECKS, DOCTOR_CHECKS_TOKEN, DiscoveryCommands, DiscoveryScanCommand, DiscoverySnapshotDoctorCheck, type DoctorCheck, type DoctorCheckResult, DoctorCommands, DoctorFlow, type DoctorReport, MANIFEST_ACCESS_PORT_TOKEN, MANIFEST_CHECKS_TOKEN, type ManifestCheck, ManifestCheckCommand, type ManifestCheckReport, type ManifestCheckResult, ManifestCliFlow, ManifestCommands, type ManifestDiff, ManifestDumpCommand, ManifestHashCommand, ManifestValidateCommand, MfaSetupFlow, type MfaSetupOptions, type MfaSetupResult, PLATFORM_DOCTOR_CHECK_PROVIDERS, PlanCatalogDoctorCheck, USER_MANAGEMENT_PORT_TOKEN, USER_PORT_TOKEN, UserCommands, UserPortDoctorCheck, WhoAmIFlow, type WhoAmIResult, applyFragmentBlocks, extractModelBlocks, extractModelNames };
|
|
628
|
+
export { AUDIT_QUERY_PORT_TOKEN, AdminCommands, AdminManifestDoctorCheck, AdminMfaSetupCommand, AdminWhoamiCommand, type ApplyResult, AuditCommands, AuditTailCommand, AuditTailFlow, type AuditTailOptions, CLI_CONTEXT_CONFIG_TOKEN, type CheckSeverity, type CliContextConfig, CliContextModule, type CliContextModuleOptions, CliContextService, CliError, type CliIdentity, DEFAULT_MANIFEST_CHECKS, DOCTOR_CHECKS_TOKEN, DiscoveryCommands, DiscoveryScanCommand, DiscoverySnapshotDoctorCheck, type DoctorCheck, type DoctorCheckResult, DoctorCommands, DoctorFlow, type DoctorReport, type FieldMismatch, type FieldMismatchReason, type FieldSignature, MANIFEST_ACCESS_PORT_TOKEN, MANIFEST_CHECKS_TOKEN, type ManifestCheck, ManifestCheckCommand, type ManifestCheckReport, type ManifestCheckResult, ManifestCliFlow, ManifestCommands, type ManifestDiff, ManifestDumpCommand, ManifestHashCommand, ManifestValidateCommand, MfaSetupFlow, type MfaSetupOptions, type MfaSetupResult, type MissingEnumValue, type MissingField, PLATFORM_DOCTOR_CHECK_PROVIDERS, type ParsedSchema, PlanCatalogDoctorCheck, type SchemaCheckReport, USER_MANAGEMENT_PORT_TOKEN, USER_PORT_TOKEN, UserCommands, UserPortDoctorCheck, WhoAmIFlow, type WhoAmIResult, applyFragmentBlocks, blockBodyLines, checkSchema, extractBlockNames, extractBlocks, extractModelBlocks, extractModelNames, parseEnumValues, parseFields, parseSchema, stripLineComment };
|
package/dist/index.d.ts
CHANGED
|
@@ -326,17 +326,38 @@ declare class AdminManifestDoctorCheck implements DoctorCheck {
|
|
|
326
326
|
*/
|
|
327
327
|
declare const PLATFORM_DOCTOR_CHECK_PROVIDERS: Array<Type<DoctorCheck>>;
|
|
328
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Cuts off a trailing `//` comment. Deliberately index-based instead of
|
|
331
|
+
* `replace(/\/\/.*$/, '')`: the regex backtracks quadratically on lines with
|
|
332
|
+
* many single slashes.
|
|
333
|
+
*/
|
|
334
|
+
declare function stripLineComment(line: string): string;
|
|
335
|
+
/** Returns the names of all top-level `<keyword> X { … }` blocks. */
|
|
336
|
+
declare function extractBlockNames(schema: string, keyword: string): string[];
|
|
337
|
+
/**
|
|
338
|
+
* Returns all top-level `<keyword> X { … }` blocks as a map
|
|
339
|
+
* `name -> complete block text incl. opening/closing braces`.
|
|
340
|
+
*
|
|
341
|
+
* Delimiting logic: we search for `^<keyword> X {` (start of line, possibly
|
|
342
|
+
* with leading whitespace) and close as soon as the brace depth drops back
|
|
343
|
+
* to 0. Braces inside comments are not counted.
|
|
344
|
+
*/
|
|
345
|
+
declare function extractBlocks(schema: string, keyword: string): Map<string, string>;
|
|
346
|
+
/**
|
|
347
|
+
* Returns a block's body lines — comments stripped, blank lines, block-level
|
|
348
|
+
* attributes (`@@index`, `@@map`) and the enclosing braces removed.
|
|
349
|
+
*
|
|
350
|
+
* Cuts between the first `{` and the last `}` rather than dropping the first
|
|
351
|
+
* and last line, so single-line blocks (`model X { id String @id }`) yield
|
|
352
|
+
* their body too.
|
|
353
|
+
*/
|
|
354
|
+
declare function blockBodyLines(block: string): string[];
|
|
355
|
+
|
|
329
356
|
/** Returns the names of all top-level `model X { ... }` blocks in the schema. */
|
|
330
357
|
declare function extractModelNames(schema: string): string[];
|
|
331
358
|
/**
|
|
332
359
|
* Returns all `model X { ... }` blocks from a fragment as a map
|
|
333
360
|
* `name -> complete block text incl. opening/closing braces`.
|
|
334
|
-
*
|
|
335
|
-
* Delimiting logic: we search for `^model X {` (start of line, possibly with
|
|
336
|
-
* leading whitespace) and close as soon as the brace depth drops back to
|
|
337
|
-
* 0. Strings/comments inside the block are, in simplified terms, not
|
|
338
|
-
* counted, which is robust for Prisma schemas (no curly braces
|
|
339
|
-
* in strings, comments only `//`-style).
|
|
340
361
|
*/
|
|
341
362
|
declare function extractModelBlocks(fragment: string): Map<string, string>;
|
|
342
363
|
interface ApplyResult {
|
|
@@ -355,6 +376,65 @@ declare function applyFragmentBlocks(schema: string, fragmentBlocks: Map<string,
|
|
|
355
376
|
fragmentLabel?: string;
|
|
356
377
|
}): ApplyResult;
|
|
357
378
|
|
|
379
|
+
interface FieldSignature {
|
|
380
|
+
name: string;
|
|
381
|
+
/** Type without `?`/`[]` modifiers, e.g. `String`, `BillingCycle`. */
|
|
382
|
+
type: string;
|
|
383
|
+
optional: boolean;
|
|
384
|
+
list: boolean;
|
|
385
|
+
}
|
|
386
|
+
interface ParsedSchema {
|
|
387
|
+
models: Map<string, Map<string, FieldSignature>>;
|
|
388
|
+
enums: Map<string, string[]>;
|
|
389
|
+
}
|
|
390
|
+
interface MissingField {
|
|
391
|
+
model: string;
|
|
392
|
+
field: string;
|
|
393
|
+
/** Rendered spec type incl. modifiers, e.g. `DateTime?`. */
|
|
394
|
+
type: string;
|
|
395
|
+
}
|
|
396
|
+
interface MissingEnumValue {
|
|
397
|
+
enum: string;
|
|
398
|
+
value: string;
|
|
399
|
+
}
|
|
400
|
+
type FieldMismatchReason = 'type' | 'optionality' | 'list';
|
|
401
|
+
interface FieldMismatch {
|
|
402
|
+
model: string;
|
|
403
|
+
field: string;
|
|
404
|
+
reason: FieldMismatchReason;
|
|
405
|
+
expected: string;
|
|
406
|
+
actual: string;
|
|
407
|
+
}
|
|
408
|
+
interface SchemaCheckReport {
|
|
409
|
+
/** Platform models the consumer does not carry — informational. */
|
|
410
|
+
absentModels: string[];
|
|
411
|
+
/** Platform enums the consumer does not carry — informational. */
|
|
412
|
+
absentEnums: string[];
|
|
413
|
+
missingFields: MissingField[];
|
|
414
|
+
missingEnumValues: MissingEnumValue[];
|
|
415
|
+
fieldMismatches: FieldMismatch[];
|
|
416
|
+
/** Models present in both schemas, i.e. actually compared. */
|
|
417
|
+
checkedModelCount: number;
|
|
418
|
+
/** Enums present in both schemas, i.e. actually compared. */
|
|
419
|
+
checkedEnumCount: number;
|
|
420
|
+
/** True when nothing that breaks platform code was found. */
|
|
421
|
+
ok: boolean;
|
|
422
|
+
}
|
|
423
|
+
/** Parses the field lines of a `model` block into signatures, keyed by name. */
|
|
424
|
+
declare function parseFields(block: string): Map<string, FieldSignature>;
|
|
425
|
+
/**
|
|
426
|
+
* Parses the members of an `enum` block. Values may share a line
|
|
427
|
+
* (`enum Role { ADMIN USER }`); a trailing `@map(…)` attribute is not a value.
|
|
428
|
+
*/
|
|
429
|
+
declare function parseEnumValues(block: string): string[];
|
|
430
|
+
/** Parses every `model` and `enum` declaration of a Prisma schema. */
|
|
431
|
+
declare function parseSchema(schema: string): ParsedSchema;
|
|
432
|
+
/**
|
|
433
|
+
* Compares a consumer schema against the canonical fragments. `specSchema` is
|
|
434
|
+
* the concatenation of the fragments the check should cover.
|
|
435
|
+
*/
|
|
436
|
+
declare function checkSchema(specSchema: string, appSchema: string): SchemaCheckReport;
|
|
437
|
+
|
|
358
438
|
interface CliContextModuleOptions {
|
|
359
439
|
config: CliContextConfig;
|
|
360
440
|
userPort: ProviderSpec<UserPort>;
|
|
@@ -545,4 +625,4 @@ declare class UserCommands extends CommandRunner {
|
|
|
545
625
|
parsePassword(val: string): string;
|
|
546
626
|
}
|
|
547
627
|
|
|
548
|
-
export { AUDIT_QUERY_PORT_TOKEN, AdminCommands, AdminManifestDoctorCheck, AdminMfaSetupCommand, AdminWhoamiCommand, type ApplyResult, AuditCommands, AuditTailCommand, AuditTailFlow, type AuditTailOptions, CLI_CONTEXT_CONFIG_TOKEN, type CheckSeverity, type CliContextConfig, CliContextModule, type CliContextModuleOptions, CliContextService, CliError, type CliIdentity, DEFAULT_MANIFEST_CHECKS, DOCTOR_CHECKS_TOKEN, DiscoveryCommands, DiscoveryScanCommand, DiscoverySnapshotDoctorCheck, type DoctorCheck, type DoctorCheckResult, DoctorCommands, DoctorFlow, type DoctorReport, MANIFEST_ACCESS_PORT_TOKEN, MANIFEST_CHECKS_TOKEN, type ManifestCheck, ManifestCheckCommand, type ManifestCheckReport, type ManifestCheckResult, ManifestCliFlow, ManifestCommands, type ManifestDiff, ManifestDumpCommand, ManifestHashCommand, ManifestValidateCommand, MfaSetupFlow, type MfaSetupOptions, type MfaSetupResult, PLATFORM_DOCTOR_CHECK_PROVIDERS, PlanCatalogDoctorCheck, USER_MANAGEMENT_PORT_TOKEN, USER_PORT_TOKEN, UserCommands, UserPortDoctorCheck, WhoAmIFlow, type WhoAmIResult, applyFragmentBlocks, extractModelBlocks, extractModelNames };
|
|
628
|
+
export { AUDIT_QUERY_PORT_TOKEN, AdminCommands, AdminManifestDoctorCheck, AdminMfaSetupCommand, AdminWhoamiCommand, type ApplyResult, AuditCommands, AuditTailCommand, AuditTailFlow, type AuditTailOptions, CLI_CONTEXT_CONFIG_TOKEN, type CheckSeverity, type CliContextConfig, CliContextModule, type CliContextModuleOptions, CliContextService, CliError, type CliIdentity, DEFAULT_MANIFEST_CHECKS, DOCTOR_CHECKS_TOKEN, DiscoveryCommands, DiscoveryScanCommand, DiscoverySnapshotDoctorCheck, type DoctorCheck, type DoctorCheckResult, DoctorCommands, DoctorFlow, type DoctorReport, type FieldMismatch, type FieldMismatchReason, type FieldSignature, MANIFEST_ACCESS_PORT_TOKEN, MANIFEST_CHECKS_TOKEN, type ManifestCheck, ManifestCheckCommand, type ManifestCheckReport, type ManifestCheckResult, ManifestCliFlow, ManifestCommands, type ManifestDiff, ManifestDumpCommand, ManifestHashCommand, ManifestValidateCommand, MfaSetupFlow, type MfaSetupOptions, type MfaSetupResult, type MissingEnumValue, type MissingField, PLATFORM_DOCTOR_CHECK_PROVIDERS, type ParsedSchema, PlanCatalogDoctorCheck, type SchemaCheckReport, USER_MANAGEMENT_PORT_TOKEN, USER_PORT_TOKEN, UserCommands, UserPortDoctorCheck, WhoAmIFlow, type WhoAmIResult, applyFragmentBlocks, blockBodyLines, checkSchema, extractBlockNames, extractBlocks, extractModelBlocks, extractModelNames, parseEnumValues, parseFields, parseSchema, stripLineComment };
|
package/dist/index.js
CHANGED
|
@@ -1028,51 +1028,48 @@ var PLATFORM_DOCTOR_CHECK_PROVIDERS = [
|
|
|
1028
1028
|
AdminManifestDoctorCheck
|
|
1029
1029
|
];
|
|
1030
1030
|
|
|
1031
|
-
// src/
|
|
1031
|
+
// src/prisma-blocks.ts
|
|
1032
1032
|
function stripLineComment(line) {
|
|
1033
1033
|
const commentStart = line.indexOf("//");
|
|
1034
1034
|
return commentStart === -1 ? line : line.slice(0, commentStart);
|
|
1035
1035
|
}
|
|
1036
1036
|
__name(stripLineComment, "stripLineComment");
|
|
1037
|
-
function
|
|
1037
|
+
function declarationPattern(keyword) {
|
|
1038
|
+
return new RegExp(`^\\s*${keyword}\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\{`);
|
|
1039
|
+
}
|
|
1040
|
+
__name(declarationPattern, "declarationPattern");
|
|
1041
|
+
function extractBlockNames(schema, keyword) {
|
|
1042
|
+
const pattern = declarationPattern(keyword);
|
|
1038
1043
|
const names = [];
|
|
1039
|
-
const
|
|
1040
|
-
|
|
1041
|
-
const stripped = stripLineComment(line).trim();
|
|
1042
|
-
const match = stripped.match(/^model\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/);
|
|
1044
|
+
for (const line of schema.split("\n")) {
|
|
1045
|
+
const match = stripLineComment(line).match(pattern);
|
|
1043
1046
|
if (match) names.push(match[1]);
|
|
1044
1047
|
}
|
|
1045
1048
|
return names;
|
|
1046
1049
|
}
|
|
1047
|
-
__name(
|
|
1048
|
-
function
|
|
1050
|
+
__name(extractBlockNames, "extractBlockNames");
|
|
1051
|
+
function extractBlocks(schema, keyword) {
|
|
1052
|
+
const pattern = declarationPattern(keyword);
|
|
1049
1053
|
const blocks = /* @__PURE__ */ new Map();
|
|
1050
|
-
const lines = fragment.split("\n");
|
|
1051
1054
|
let current = null;
|
|
1052
|
-
for (const rawLine of
|
|
1055
|
+
for (const rawLine of schema.split("\n")) {
|
|
1053
1056
|
const stripped = stripLineComment(rawLine);
|
|
1057
|
+
const openCount = (stripped.match(/\{/g) ?? []).length;
|
|
1058
|
+
const closeCount = (stripped.match(/\}/g) ?? []).length;
|
|
1054
1059
|
if (!current) {
|
|
1055
|
-
const match =
|
|
1056
|
-
if (match)
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
blocks.set(current.name, current.lines.join("\n"));
|
|
1068
|
-
current = null;
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
continue;
|
|
1060
|
+
const match = stripped.match(pattern);
|
|
1061
|
+
if (!match) continue;
|
|
1062
|
+
current = {
|
|
1063
|
+
name: match[1],
|
|
1064
|
+
lines: [
|
|
1065
|
+
rawLine
|
|
1066
|
+
],
|
|
1067
|
+
depth: openCount - closeCount
|
|
1068
|
+
};
|
|
1069
|
+
} else {
|
|
1070
|
+
current.lines.push(rawLine);
|
|
1071
|
+
current.depth += openCount - closeCount;
|
|
1072
1072
|
}
|
|
1073
|
-
current.lines.push(rawLine);
|
|
1074
|
-
current.depth += (stripped.match(/\{/g) ?? []).length;
|
|
1075
|
-
current.depth -= (stripped.match(/\}/g) ?? []).length;
|
|
1076
1073
|
if (current.depth <= 0) {
|
|
1077
1074
|
blocks.set(current.name, current.lines.join("\n"));
|
|
1078
1075
|
current = null;
|
|
@@ -1080,6 +1077,23 @@ function extractModelBlocks(fragment) {
|
|
|
1080
1077
|
}
|
|
1081
1078
|
return blocks;
|
|
1082
1079
|
}
|
|
1080
|
+
__name(extractBlocks, "extractBlocks");
|
|
1081
|
+
function blockBodyLines(block) {
|
|
1082
|
+
const bodyStart = block.indexOf("{");
|
|
1083
|
+
const bodyEnd = block.lastIndexOf("}");
|
|
1084
|
+
if (bodyStart === -1 || bodyEnd <= bodyStart) return [];
|
|
1085
|
+
return block.slice(bodyStart + 1, bodyEnd).split("\n").map((line) => stripLineComment(line).trim()).filter((line) => line.length > 0 && !line.startsWith("@@"));
|
|
1086
|
+
}
|
|
1087
|
+
__name(blockBodyLines, "blockBodyLines");
|
|
1088
|
+
|
|
1089
|
+
// src/schema-apply.ts
|
|
1090
|
+
function extractModelNames(schema) {
|
|
1091
|
+
return extractBlockNames(schema, "model");
|
|
1092
|
+
}
|
|
1093
|
+
__name(extractModelNames, "extractModelNames");
|
|
1094
|
+
function extractModelBlocks(fragment) {
|
|
1095
|
+
return extractBlocks(fragment, "model");
|
|
1096
|
+
}
|
|
1083
1097
|
__name(extractModelBlocks, "extractModelBlocks");
|
|
1084
1098
|
function applyFragmentBlocks(schema, fragmentBlocks, options = {}) {
|
|
1085
1099
|
const existing = new Set(extractModelNames(schema));
|
|
@@ -1104,11 +1118,11 @@ function applyFragmentBlocks(schema, fragmentBlocks, options = {}) {
|
|
|
1104
1118
|
const header = options.fragmentLabel ? `
|
|
1105
1119
|
|
|
1106
1120
|
// ============================================================
|
|
1107
|
-
// Eingef\xFCgt durch \`
|
|
1121
|
+
// Eingef\xFCgt durch \`saasicat schema apply\` aus ${options.fragmentLabel}
|
|
1108
1122
|
// ============================================================
|
|
1109
1123
|
` : `
|
|
1110
1124
|
|
|
1111
|
-
// Eingef\xFCgt durch \`
|
|
1125
|
+
// Eingef\xFCgt durch \`saasicat schema apply\`
|
|
1112
1126
|
`;
|
|
1113
1127
|
const trimmedSchema = schema.endsWith("\n") ? schema : schema + "\n";
|
|
1114
1128
|
return {
|
|
@@ -1119,6 +1133,130 @@ function applyFragmentBlocks(schema, fragmentBlocks, options = {}) {
|
|
|
1119
1133
|
}
|
|
1120
1134
|
__name(applyFragmentBlocks, "applyFragmentBlocks");
|
|
1121
1135
|
|
|
1136
|
+
// src/schema-check.ts
|
|
1137
|
+
function renderType(signature) {
|
|
1138
|
+
return `${signature.type}${signature.list ? "[]" : ""}${signature.optional ? "?" : ""}`;
|
|
1139
|
+
}
|
|
1140
|
+
__name(renderType, "renderType");
|
|
1141
|
+
function parseFields(block) {
|
|
1142
|
+
const fields = /* @__PURE__ */ new Map();
|
|
1143
|
+
for (const line of blockBodyLines(block)) {
|
|
1144
|
+
const [name, rawType] = line.split(/\s+/);
|
|
1145
|
+
if (!rawType || name.startsWith("@")) continue;
|
|
1146
|
+
const list = rawType.includes("[]");
|
|
1147
|
+
const optional = rawType.endsWith("?");
|
|
1148
|
+
fields.set(name, {
|
|
1149
|
+
name,
|
|
1150
|
+
type: rawType.replace("[]", "").replace("?", ""),
|
|
1151
|
+
optional,
|
|
1152
|
+
list
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
return fields;
|
|
1156
|
+
}
|
|
1157
|
+
__name(parseFields, "parseFields");
|
|
1158
|
+
function parseEnumValues(block) {
|
|
1159
|
+
const values = [];
|
|
1160
|
+
for (const line of blockBodyLines(block)) {
|
|
1161
|
+
for (const token of line.split(/\s+/)) {
|
|
1162
|
+
if (token.startsWith("@")) break;
|
|
1163
|
+
if (token.length > 0) values.push(token);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
return values;
|
|
1167
|
+
}
|
|
1168
|
+
__name(parseEnumValues, "parseEnumValues");
|
|
1169
|
+
function parseSchema(schema) {
|
|
1170
|
+
const models = /* @__PURE__ */ new Map();
|
|
1171
|
+
for (const [name, block] of extractBlocks(schema, "model")) {
|
|
1172
|
+
models.set(name, parseFields(block));
|
|
1173
|
+
}
|
|
1174
|
+
const enums = /* @__PURE__ */ new Map();
|
|
1175
|
+
for (const [name, block] of extractBlocks(schema, "enum")) {
|
|
1176
|
+
enums.set(name, parseEnumValues(block));
|
|
1177
|
+
}
|
|
1178
|
+
return {
|
|
1179
|
+
models,
|
|
1180
|
+
enums
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
__name(parseSchema, "parseSchema");
|
|
1184
|
+
function isDocumentedTypeSubstitution(spec, app, appEnums) {
|
|
1185
|
+
return spec.type === "String" && appEnums.has(app.type);
|
|
1186
|
+
}
|
|
1187
|
+
__name(isDocumentedTypeSubstitution, "isDocumentedTypeSubstitution");
|
|
1188
|
+
function compareFields(model, specFields, appFields, appEnums, missingFields, fieldMismatches) {
|
|
1189
|
+
for (const [name, spec] of specFields) {
|
|
1190
|
+
const app = appFields.get(name);
|
|
1191
|
+
if (!app) {
|
|
1192
|
+
missingFields.push({
|
|
1193
|
+
model,
|
|
1194
|
+
field: name,
|
|
1195
|
+
type: renderType(spec)
|
|
1196
|
+
});
|
|
1197
|
+
continue;
|
|
1198
|
+
}
|
|
1199
|
+
const mismatch = /* @__PURE__ */ __name((reason) => ({
|
|
1200
|
+
model,
|
|
1201
|
+
field: name,
|
|
1202
|
+
reason,
|
|
1203
|
+
expected: renderType(spec),
|
|
1204
|
+
actual: renderType(app)
|
|
1205
|
+
}), "mismatch");
|
|
1206
|
+
if (spec.type !== app.type && !isDocumentedTypeSubstitution(spec, app, appEnums)) {
|
|
1207
|
+
fieldMismatches.push(mismatch("type"));
|
|
1208
|
+
} else if (spec.list !== app.list) {
|
|
1209
|
+
fieldMismatches.push(mismatch("list"));
|
|
1210
|
+
} else if (!spec.optional && app.optional) {
|
|
1211
|
+
fieldMismatches.push(mismatch("optionality"));
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
__name(compareFields, "compareFields");
|
|
1216
|
+
function checkSchema(specSchema, appSchema) {
|
|
1217
|
+
const spec = parseSchema(specSchema);
|
|
1218
|
+
const app = parseSchema(appSchema);
|
|
1219
|
+
const absentModels = [];
|
|
1220
|
+
const missingFields = [];
|
|
1221
|
+
const fieldMismatches = [];
|
|
1222
|
+
for (const [model, specFields] of spec.models) {
|
|
1223
|
+
const appFields = app.models.get(model);
|
|
1224
|
+
if (!appFields) {
|
|
1225
|
+
absentModels.push(model);
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
compareFields(model, specFields, appFields, app.enums, missingFields, fieldMismatches);
|
|
1229
|
+
}
|
|
1230
|
+
const absentEnums = [];
|
|
1231
|
+
const missingEnumValues = [];
|
|
1232
|
+
for (const [name, specValues] of spec.enums) {
|
|
1233
|
+
const appValues = app.enums.get(name);
|
|
1234
|
+
if (!appValues) {
|
|
1235
|
+
absentEnums.push(name);
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
for (const value of specValues) {
|
|
1239
|
+
if (!appValues.includes(value)) {
|
|
1240
|
+
missingEnumValues.push({
|
|
1241
|
+
enum: name,
|
|
1242
|
+
value
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
return {
|
|
1248
|
+
absentModels,
|
|
1249
|
+
absentEnums,
|
|
1250
|
+
missingFields,
|
|
1251
|
+
missingEnumValues,
|
|
1252
|
+
fieldMismatches,
|
|
1253
|
+
checkedModelCount: spec.models.size - absentModels.length,
|
|
1254
|
+
checkedEnumCount: spec.enums.size - absentEnums.length,
|
|
1255
|
+
ok: missingFields.length === 0 && missingEnumValues.length === 0 && fieldMismatches.length === 0
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
__name(checkSchema, "checkSchema");
|
|
1259
|
+
|
|
1122
1260
|
// src/module.ts
|
|
1123
1261
|
import { Module } from "@nestjs/common";
|
|
1124
1262
|
import { asProvider } from "@saasicat/nest";
|
|
@@ -2207,6 +2345,14 @@ export {
|
|
|
2207
2345
|
UserPortDoctorCheck,
|
|
2208
2346
|
WhoAmIFlow,
|
|
2209
2347
|
applyFragmentBlocks,
|
|
2348
|
+
blockBodyLines,
|
|
2349
|
+
checkSchema,
|
|
2350
|
+
extractBlockNames,
|
|
2351
|
+
extractBlocks,
|
|
2210
2352
|
extractModelBlocks,
|
|
2211
|
-
extractModelNames
|
|
2353
|
+
extractModelNames,
|
|
2354
|
+
parseEnumValues,
|
|
2355
|
+
parseFields,
|
|
2356
|
+
parseSchema,
|
|
2357
|
+
stripLineComment
|
|
2212
2358
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saasicat/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
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",
|
|
@@ -23,13 +23,13 @@
|
|
|
23
23
|
"bin"
|
|
24
24
|
],
|
|
25
25
|
"bin": {
|
|
26
|
-
"
|
|
26
|
+
"saasicat": "./bin/saasicat.js"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"qrcode-terminal": "^0.12.0",
|
|
30
|
-
"@saasicat/nest": "^0.
|
|
31
|
-
"@saasicat/spec": "^0.
|
|
32
|
-
"@saasicat/types": "^0.
|
|
30
|
+
"@saasicat/nest": "^0.11.0",
|
|
31
|
+
"@saasicat/spec": "^0.11.0",
|
|
32
|
+
"@saasicat/types": "^0.11.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"@nestjs/common": "^11.0.0",
|