@saasicat/cli 0.10.1 → 0.12.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} +127 -16
- package/dist/index.cjs +250 -34
- package/dist/index.d.cts +123 -7
- package/dist/index.d.ts +123 -7
- package/dist/index.js +239 -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,102 @@ 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 breaking = report.missingBlockAttributes.filter((a) => a.kind !== 'index');
|
|
173
|
+
if (breaking.length > 0) {
|
|
174
|
+
console.log(`✗ Fehlende Constraints (${breaking.length}):`);
|
|
175
|
+
for (const { model, kind, expected, actual } of breaking) {
|
|
176
|
+
const suffix = kind === 'map' ? ` — vorhanden: @@map("${actual}")` : '';
|
|
177
|
+
console.log(` ${model.padEnd(28)} ${expected}${suffix}`);
|
|
178
|
+
}
|
|
179
|
+
console.log('');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const missingIndexes = report.missingBlockAttributes.filter((a) => a.kind === 'index');
|
|
183
|
+
if (missingIndexes.length > 0) {
|
|
184
|
+
console.log(`→ Fehlende Indizes (${missingIndexes.length}):`);
|
|
185
|
+
for (const { model, expected } of missingIndexes) {
|
|
186
|
+
console.log(` ${model.padEnd(28)} ${expected}`);
|
|
187
|
+
}
|
|
188
|
+
console.log(' Kein Fehler — kostet Query-Zeit, bricht aber nichts.');
|
|
189
|
+
console.log('');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const absent = [...report.absentModels, ...report.absentEnums];
|
|
193
|
+
if (absent.length > 0) {
|
|
194
|
+
console.log(`→ Nicht übernommen (${absent.length}): ${absent.join(', ')}`);
|
|
195
|
+
console.log(' Kein Fehler — diese Fragmente nutzt die App nicht.');
|
|
196
|
+
console.log('');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function cmdSchemaCheck(args) {
|
|
201
|
+
const { schemaPath, schema } = await readSchemaOrExit(args);
|
|
202
|
+
const fragmentsDir = resolveFragmentsDir();
|
|
203
|
+
const filter = parseFragmentFilter(args.fragments);
|
|
204
|
+
const files = await selectFragmentFiles(fragmentsDir, filter);
|
|
205
|
+
|
|
206
|
+
if (files.length === 0) {
|
|
207
|
+
console.error('✗ Keine Fragmente ausgewählt.');
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const fragments = [];
|
|
212
|
+
for (const file of files) {
|
|
213
|
+
fragments.push(await readFile(join(fragmentsDir, file), 'utf8'));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
console.log(`→ Prüfe ${schemaPath} gegen ${files.length} Fragment(e) aus @saasicat/spec`);
|
|
217
|
+
console.log('');
|
|
218
|
+
|
|
219
|
+
const report = checkSchema(fragments.join('\n'), schema);
|
|
220
|
+
printCheckReport(report);
|
|
221
|
+
|
|
222
|
+
const checked = `${report.checkedModelCount} Model(s), ${report.checkedEnumCount} Enum(s)`;
|
|
223
|
+
if (report.ok) {
|
|
224
|
+
console.log(`✓ Kein Drift. ${checked} geprüft.`);
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
console.log(`✗ Drift gefunden (${checked} geprüft).`);
|
|
229
|
+
console.log(' Fehlende Felder und Enum-Werte ergänzen, dann migrieren.');
|
|
230
|
+
console.log(' Abweichende Felder prüfen: Plattform-Code liest sie mit dem Spec-Typ.');
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
|
|
128
234
|
function runChild(cmd, args, opts = {}) {
|
|
129
235
|
return new Promise((resolve_, reject) => {
|
|
130
236
|
const proc = spawn(cmd, args, { stdio: 'inherit', ...opts });
|
|
@@ -142,7 +248,7 @@ async function cmdSchemaMigrate(args) {
|
|
|
142
248
|
process.exit(1);
|
|
143
249
|
}
|
|
144
250
|
|
|
145
|
-
console.log(`→ Schritt 1/2:
|
|
251
|
+
console.log(`→ Schritt 1/2: saasicat schema apply ${args['fragments'] ? `--fragments=${args['fragments']}` : '--all'}`);
|
|
146
252
|
await cmdSchemaApply({
|
|
147
253
|
...args,
|
|
148
254
|
all: args['fragments'] ? undefined : true,
|
|
@@ -159,16 +265,21 @@ async function main() {
|
|
|
159
265
|
if (cmd === 'schema' && sub === 'apply') {
|
|
160
266
|
return cmdSchemaApply(parseArgs(rest));
|
|
161
267
|
}
|
|
268
|
+
if (cmd === 'schema' && sub === 'check') {
|
|
269
|
+
return cmdSchemaCheck(parseArgs(rest));
|
|
270
|
+
}
|
|
162
271
|
if (cmd === 'schema' && sub === 'migrate') {
|
|
163
272
|
return cmdSchemaMigrate(parseArgs(rest));
|
|
164
273
|
}
|
|
165
274
|
if (cmd === '--help' || cmd === '-h' || !cmd) {
|
|
166
|
-
console.log('Usage:
|
|
275
|
+
console.log('Usage: saasicat <command> [...args]');
|
|
167
276
|
console.log('');
|
|
168
277
|
console.log('Commands:');
|
|
169
278
|
console.log(' schema apply --all alle Plattform-Models einfügen');
|
|
170
279
|
console.log(' schema apply --fragments=01,02 nur diese Fragmente einfügen');
|
|
171
280
|
console.log(' schema apply --dry-run nur Diff ausgeben');
|
|
281
|
+
console.log(' schema check Drift gegen @saasicat/spec melden');
|
|
282
|
+
console.log(' schema check --fragments=01,02 nur diese Fragmente prüfen');
|
|
172
283
|
console.log(' schema migrate --name=<name> apply --all + prisma migrate dev');
|
|
173
284
|
console.log('');
|
|
174
285
|
console.log('Optional --prisma-schema=PATH (default prisma/schema.prisma).');
|
package/dist/index.cjs
CHANGED
|
@@ -67,8 +67,18 @@ __export(index_exports, {
|
|
|
67
67
|
UserPortDoctorCheck: () => UserPortDoctorCheck,
|
|
68
68
|
WhoAmIFlow: () => WhoAmIFlow,
|
|
69
69
|
applyFragmentBlocks: () => applyFragmentBlocks,
|
|
70
|
+
blockBodyLines: () => blockBodyLines,
|
|
71
|
+
breaksContract: () => breaksContract,
|
|
72
|
+
checkSchema: () => checkSchema,
|
|
73
|
+
extractBlockNames: () => extractBlockNames,
|
|
74
|
+
extractBlocks: () => extractBlocks,
|
|
70
75
|
extractModelBlocks: () => extractModelBlocks,
|
|
71
|
-
extractModelNames: () => extractModelNames
|
|
76
|
+
extractModelNames: () => extractModelNames,
|
|
77
|
+
parseBlockAttributes: () => parseBlockAttributes,
|
|
78
|
+
parseEnumValues: () => parseEnumValues,
|
|
79
|
+
parseFields: () => parseFields,
|
|
80
|
+
parseSchema: () => parseSchema,
|
|
81
|
+
stripLineComment: () => stripLineComment
|
|
72
82
|
});
|
|
73
83
|
module.exports = __toCommonJS(index_exports);
|
|
74
84
|
|
|
@@ -1099,51 +1109,48 @@ var PLATFORM_DOCTOR_CHECK_PROVIDERS = [
|
|
|
1099
1109
|
AdminManifestDoctorCheck
|
|
1100
1110
|
];
|
|
1101
1111
|
|
|
1102
|
-
// src/
|
|
1112
|
+
// src/prisma-blocks.ts
|
|
1103
1113
|
function stripLineComment(line) {
|
|
1104
1114
|
const commentStart = line.indexOf("//");
|
|
1105
1115
|
return commentStart === -1 ? line : line.slice(0, commentStart);
|
|
1106
1116
|
}
|
|
1107
1117
|
__name(stripLineComment, "stripLineComment");
|
|
1108
|
-
function
|
|
1118
|
+
function declarationPattern(keyword) {
|
|
1119
|
+
return new RegExp(`^\\s*${keyword}\\s+([A-Za-z_][A-Za-z0-9_]*)\\s*\\{`);
|
|
1120
|
+
}
|
|
1121
|
+
__name(declarationPattern, "declarationPattern");
|
|
1122
|
+
function extractBlockNames(schema, keyword) {
|
|
1123
|
+
const pattern = declarationPattern(keyword);
|
|
1109
1124
|
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*\{/);
|
|
1125
|
+
for (const line of schema.split("\n")) {
|
|
1126
|
+
const match = stripLineComment(line).match(pattern);
|
|
1114
1127
|
if (match) names.push(match[1]);
|
|
1115
1128
|
}
|
|
1116
1129
|
return names;
|
|
1117
1130
|
}
|
|
1118
|
-
__name(
|
|
1119
|
-
function
|
|
1131
|
+
__name(extractBlockNames, "extractBlockNames");
|
|
1132
|
+
function extractBlocks(schema, keyword) {
|
|
1133
|
+
const pattern = declarationPattern(keyword);
|
|
1120
1134
|
const blocks = /* @__PURE__ */ new Map();
|
|
1121
|
-
const lines = fragment.split("\n");
|
|
1122
1135
|
let current = null;
|
|
1123
|
-
for (const rawLine of
|
|
1136
|
+
for (const rawLine of schema.split("\n")) {
|
|
1124
1137
|
const stripped = stripLineComment(rawLine);
|
|
1138
|
+
const openCount = (stripped.match(/\{/g) ?? []).length;
|
|
1139
|
+
const closeCount = (stripped.match(/\}/g) ?? []).length;
|
|
1125
1140
|
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;
|
|
1141
|
+
const match = stripped.match(pattern);
|
|
1142
|
+
if (!match) continue;
|
|
1143
|
+
current = {
|
|
1144
|
+
name: match[1],
|
|
1145
|
+
lines: [
|
|
1146
|
+
rawLine
|
|
1147
|
+
],
|
|
1148
|
+
depth: openCount - closeCount
|
|
1149
|
+
};
|
|
1150
|
+
} else {
|
|
1151
|
+
current.lines.push(rawLine);
|
|
1152
|
+
current.depth += openCount - closeCount;
|
|
1143
1153
|
}
|
|
1144
|
-
current.lines.push(rawLine);
|
|
1145
|
-
current.depth += (stripped.match(/\{/g) ?? []).length;
|
|
1146
|
-
current.depth -= (stripped.match(/\}/g) ?? []).length;
|
|
1147
1154
|
if (current.depth <= 0) {
|
|
1148
1155
|
blocks.set(current.name, current.lines.join("\n"));
|
|
1149
1156
|
current = null;
|
|
@@ -1151,6 +1158,23 @@ function extractModelBlocks(fragment) {
|
|
|
1151
1158
|
}
|
|
1152
1159
|
return blocks;
|
|
1153
1160
|
}
|
|
1161
|
+
__name(extractBlocks, "extractBlocks");
|
|
1162
|
+
function blockBodyLines(block) {
|
|
1163
|
+
const bodyStart = block.indexOf("{");
|
|
1164
|
+
const bodyEnd = block.lastIndexOf("}");
|
|
1165
|
+
if (bodyStart === -1 || bodyEnd <= bodyStart) return [];
|
|
1166
|
+
return block.slice(bodyStart + 1, bodyEnd).split("\n").map((line) => stripLineComment(line).trim()).filter((line) => line.length > 0 && !line.startsWith("@@"));
|
|
1167
|
+
}
|
|
1168
|
+
__name(blockBodyLines, "blockBodyLines");
|
|
1169
|
+
|
|
1170
|
+
// src/schema-apply.ts
|
|
1171
|
+
function extractModelNames(schema) {
|
|
1172
|
+
return extractBlockNames(schema, "model");
|
|
1173
|
+
}
|
|
1174
|
+
__name(extractModelNames, "extractModelNames");
|
|
1175
|
+
function extractModelBlocks(fragment) {
|
|
1176
|
+
return extractBlocks(fragment, "model");
|
|
1177
|
+
}
|
|
1154
1178
|
__name(extractModelBlocks, "extractModelBlocks");
|
|
1155
1179
|
function applyFragmentBlocks(schema, fragmentBlocks, options = {}) {
|
|
1156
1180
|
const existing = new Set(extractModelNames(schema));
|
|
@@ -1175,11 +1199,11 @@ function applyFragmentBlocks(schema, fragmentBlocks, options = {}) {
|
|
|
1175
1199
|
const header = options.fragmentLabel ? `
|
|
1176
1200
|
|
|
1177
1201
|
// ============================================================
|
|
1178
|
-
// Eingef\xFCgt durch \`
|
|
1202
|
+
// Eingef\xFCgt durch \`saasicat schema apply\` aus ${options.fragmentLabel}
|
|
1179
1203
|
// ============================================================
|
|
1180
1204
|
` : `
|
|
1181
1205
|
|
|
1182
|
-
// Eingef\xFCgt durch \`
|
|
1206
|
+
// Eingef\xFCgt durch \`saasicat schema apply\`
|
|
1183
1207
|
`;
|
|
1184
1208
|
const trimmedSchema = schema.endsWith("\n") ? schema : schema + "\n";
|
|
1185
1209
|
return {
|
|
@@ -1190,6 +1214,188 @@ function applyFragmentBlocks(schema, fragmentBlocks, options = {}) {
|
|
|
1190
1214
|
}
|
|
1191
1215
|
__name(applyFragmentBlocks, "applyFragmentBlocks");
|
|
1192
1216
|
|
|
1217
|
+
// src/schema-check.ts
|
|
1218
|
+
function renderType(signature) {
|
|
1219
|
+
return `${signature.type}${signature.list ? "[]" : ""}${signature.optional ? "?" : ""}`;
|
|
1220
|
+
}
|
|
1221
|
+
__name(renderType, "renderType");
|
|
1222
|
+
function parseFields(block) {
|
|
1223
|
+
const fields = /* @__PURE__ */ new Map();
|
|
1224
|
+
for (const line of blockBodyLines(block)) {
|
|
1225
|
+
const [name, rawType] = line.split(/\s+/);
|
|
1226
|
+
if (!rawType || name.startsWith("@")) continue;
|
|
1227
|
+
const list = rawType.includes("[]");
|
|
1228
|
+
const optional = rawType.endsWith("?");
|
|
1229
|
+
fields.set(name, {
|
|
1230
|
+
name,
|
|
1231
|
+
type: rawType.replace("[]", "").replace("?", ""),
|
|
1232
|
+
optional,
|
|
1233
|
+
list
|
|
1234
|
+
});
|
|
1235
|
+
}
|
|
1236
|
+
return fields;
|
|
1237
|
+
}
|
|
1238
|
+
__name(parseFields, "parseFields");
|
|
1239
|
+
function parseEnumValues(block) {
|
|
1240
|
+
const values = [];
|
|
1241
|
+
for (const line of blockBodyLines(block)) {
|
|
1242
|
+
for (const token of line.split(/\s+/)) {
|
|
1243
|
+
if (token.startsWith("@")) break;
|
|
1244
|
+
if (token.length > 0) values.push(token);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
return values;
|
|
1248
|
+
}
|
|
1249
|
+
__name(parseEnumValues, "parseEnumValues");
|
|
1250
|
+
function attributeFieldLists(block, attribute) {
|
|
1251
|
+
const pattern = new RegExp(`@@${attribute}\\(([^)]*\\])`, "g");
|
|
1252
|
+
return new Set([
|
|
1253
|
+
...block.matchAll(pattern)
|
|
1254
|
+
].map((match) => match[1].replace(/\s+/g, "")));
|
|
1255
|
+
}
|
|
1256
|
+
__name(attributeFieldLists, "attributeFieldLists");
|
|
1257
|
+
function parseBlockAttributes(name, block) {
|
|
1258
|
+
return {
|
|
1259
|
+
indexes: attributeFieldLists(block, "index"),
|
|
1260
|
+
uniques: attributeFieldLists(block, "unique"),
|
|
1261
|
+
map: block.match(/@@map\("([^"]+)"\)/)?.[1] ?? name
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
__name(parseBlockAttributes, "parseBlockAttributes");
|
|
1265
|
+
function parseSchema(schema) {
|
|
1266
|
+
const models = /* @__PURE__ */ new Map();
|
|
1267
|
+
const modelAttributes = /* @__PURE__ */ new Map();
|
|
1268
|
+
for (const [name, block] of extractBlocks(schema, "model")) {
|
|
1269
|
+
models.set(name, parseFields(block));
|
|
1270
|
+
modelAttributes.set(name, parseBlockAttributes(name, block));
|
|
1271
|
+
}
|
|
1272
|
+
const enums = /* @__PURE__ */ new Map();
|
|
1273
|
+
for (const [name, block] of extractBlocks(schema, "enum")) {
|
|
1274
|
+
enums.set(name, parseEnumValues(block));
|
|
1275
|
+
}
|
|
1276
|
+
return {
|
|
1277
|
+
models,
|
|
1278
|
+
modelAttributes,
|
|
1279
|
+
enums
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
__name(parseSchema, "parseSchema");
|
|
1283
|
+
function isDocumentedTypeSubstitution(spec, app, appEnums) {
|
|
1284
|
+
return spec.type === "String" && appEnums.has(app.type);
|
|
1285
|
+
}
|
|
1286
|
+
__name(isDocumentedTypeSubstitution, "isDocumentedTypeSubstitution");
|
|
1287
|
+
function compareFields(model, specFields, appFields, appEnums, missingFields, fieldMismatches) {
|
|
1288
|
+
for (const [name, spec] of specFields) {
|
|
1289
|
+
const app = appFields.get(name);
|
|
1290
|
+
if (!app) {
|
|
1291
|
+
missingFields.push({
|
|
1292
|
+
model,
|
|
1293
|
+
field: name,
|
|
1294
|
+
type: renderType(spec)
|
|
1295
|
+
});
|
|
1296
|
+
continue;
|
|
1297
|
+
}
|
|
1298
|
+
const mismatch = /* @__PURE__ */ __name((reason) => ({
|
|
1299
|
+
model,
|
|
1300
|
+
field: name,
|
|
1301
|
+
reason,
|
|
1302
|
+
expected: renderType(spec),
|
|
1303
|
+
actual: renderType(app)
|
|
1304
|
+
}), "mismatch");
|
|
1305
|
+
if (spec.type !== app.type && !isDocumentedTypeSubstitution(spec, app, appEnums)) {
|
|
1306
|
+
fieldMismatches.push(mismatch("type"));
|
|
1307
|
+
} else if (spec.list !== app.list) {
|
|
1308
|
+
fieldMismatches.push(mismatch("list"));
|
|
1309
|
+
} else if (!spec.optional && app.optional) {
|
|
1310
|
+
fieldMismatches.push(mismatch("optionality"));
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
__name(compareFields, "compareFields");
|
|
1315
|
+
function compareBlockAttributes(model, spec, app, out) {
|
|
1316
|
+
if (spec.map !== app.map) {
|
|
1317
|
+
out.push({
|
|
1318
|
+
model,
|
|
1319
|
+
kind: "map",
|
|
1320
|
+
expected: `@@map("${spec.map}")`,
|
|
1321
|
+
actual: app.map
|
|
1322
|
+
});
|
|
1323
|
+
}
|
|
1324
|
+
for (const fields of spec.uniques) {
|
|
1325
|
+
if (!app.uniques.has(fields)) {
|
|
1326
|
+
out.push({
|
|
1327
|
+
model,
|
|
1328
|
+
kind: "unique",
|
|
1329
|
+
expected: `@@unique(${fields})`
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
for (const fields of spec.indexes) {
|
|
1334
|
+
if (!app.indexes.has(fields)) {
|
|
1335
|
+
out.push({
|
|
1336
|
+
model,
|
|
1337
|
+
kind: "index",
|
|
1338
|
+
expected: `@@index(${fields})`
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
__name(compareBlockAttributes, "compareBlockAttributes");
|
|
1344
|
+
function breaksContract(attribute) {
|
|
1345
|
+
return attribute.kind !== "index";
|
|
1346
|
+
}
|
|
1347
|
+
__name(breaksContract, "breaksContract");
|
|
1348
|
+
function checkSchema(specSchema, appSchema) {
|
|
1349
|
+
const spec = parseSchema(specSchema);
|
|
1350
|
+
const app = parseSchema(appSchema);
|
|
1351
|
+
const absentModels = [];
|
|
1352
|
+
const missingFields = [];
|
|
1353
|
+
const fieldMismatches = [];
|
|
1354
|
+
const missingBlockAttributes = [];
|
|
1355
|
+
for (const [model, specFields] of spec.models) {
|
|
1356
|
+
const appFields = app.models.get(model);
|
|
1357
|
+
if (!appFields) {
|
|
1358
|
+
absentModels.push(model);
|
|
1359
|
+
continue;
|
|
1360
|
+
}
|
|
1361
|
+
compareFields(model, specFields, appFields, app.enums, missingFields, fieldMismatches);
|
|
1362
|
+
const specAttrs = spec.modelAttributes.get(model);
|
|
1363
|
+
const appAttrs = app.modelAttributes.get(model);
|
|
1364
|
+
if (specAttrs && appAttrs) {
|
|
1365
|
+
compareBlockAttributes(model, specAttrs, appAttrs, missingBlockAttributes);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
const absentEnums = [];
|
|
1369
|
+
const missingEnumValues = [];
|
|
1370
|
+
for (const [name, specValues] of spec.enums) {
|
|
1371
|
+
const appValues = app.enums.get(name);
|
|
1372
|
+
if (!appValues) {
|
|
1373
|
+
absentEnums.push(name);
|
|
1374
|
+
continue;
|
|
1375
|
+
}
|
|
1376
|
+
for (const value of specValues) {
|
|
1377
|
+
if (!appValues.includes(value)) {
|
|
1378
|
+
missingEnumValues.push({
|
|
1379
|
+
enum: name,
|
|
1380
|
+
value
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
return {
|
|
1386
|
+
absentModels,
|
|
1387
|
+
absentEnums,
|
|
1388
|
+
missingFields,
|
|
1389
|
+
missingEnumValues,
|
|
1390
|
+
fieldMismatches,
|
|
1391
|
+
missingBlockAttributes,
|
|
1392
|
+
checkedModelCount: spec.models.size - absentModels.length,
|
|
1393
|
+
checkedEnumCount: spec.enums.size - absentEnums.length,
|
|
1394
|
+
ok: missingFields.length === 0 && missingEnumValues.length === 0 && fieldMismatches.length === 0 && !missingBlockAttributes.some(breaksContract)
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
__name(checkSchema, "checkSchema");
|
|
1398
|
+
|
|
1193
1399
|
// src/module.ts
|
|
1194
1400
|
var import_common8 = require("@nestjs/common");
|
|
1195
1401
|
var import_nest5 = require("@saasicat/nest");
|
|
@@ -2279,6 +2485,16 @@ UserCommands = _ts_decorate14([
|
|
|
2279
2485
|
UserPortDoctorCheck,
|
|
2280
2486
|
WhoAmIFlow,
|
|
2281
2487
|
applyFragmentBlocks,
|
|
2488
|
+
blockBodyLines,
|
|
2489
|
+
breaksContract,
|
|
2490
|
+
checkSchema,
|
|
2491
|
+
extractBlockNames,
|
|
2492
|
+
extractBlocks,
|
|
2282
2493
|
extractModelBlocks,
|
|
2283
|
-
extractModelNames
|
|
2494
|
+
extractModelNames,
|
|
2495
|
+
parseBlockAttributes,
|
|
2496
|
+
parseEnumValues,
|
|
2497
|
+
parseFields,
|
|
2498
|
+
parseSchema,
|
|
2499
|
+
stripLineComment
|
|
2284
2500
|
});
|
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,101 @@ 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 BlockAttributes {
|
|
387
|
+
/** Field lists of `@@index`, normalised to `[a,b]` — attribute options dropped. */
|
|
388
|
+
indexes: Set<string>;
|
|
389
|
+
/** Same for `@@unique`. */
|
|
390
|
+
uniques: Set<string>;
|
|
391
|
+
/** `@@map` target, or the model name when unmapped. */
|
|
392
|
+
map: string;
|
|
393
|
+
}
|
|
394
|
+
interface ParsedSchema {
|
|
395
|
+
models: Map<string, Map<string, FieldSignature>>;
|
|
396
|
+
modelAttributes: Map<string, BlockAttributes>;
|
|
397
|
+
enums: Map<string, string[]>;
|
|
398
|
+
}
|
|
399
|
+
interface MissingField {
|
|
400
|
+
model: string;
|
|
401
|
+
field: string;
|
|
402
|
+
/** Rendered spec type incl. modifiers, e.g. `DateTime?`. */
|
|
403
|
+
type: string;
|
|
404
|
+
}
|
|
405
|
+
interface MissingEnumValue {
|
|
406
|
+
enum: string;
|
|
407
|
+
value: string;
|
|
408
|
+
}
|
|
409
|
+
type FieldMismatchReason = 'type' | 'optionality' | 'list';
|
|
410
|
+
interface FieldMismatch {
|
|
411
|
+
model: string;
|
|
412
|
+
field: string;
|
|
413
|
+
reason: FieldMismatchReason;
|
|
414
|
+
expected: string;
|
|
415
|
+
actual: string;
|
|
416
|
+
}
|
|
417
|
+
type BlockAttributeKind = 'index' | 'unique' | 'map';
|
|
418
|
+
interface MissingBlockAttribute {
|
|
419
|
+
model: string;
|
|
420
|
+
kind: BlockAttributeKind;
|
|
421
|
+
/** Rendered attribute, e.g. `@@index([planId, validFrom])`. */
|
|
422
|
+
expected: string;
|
|
423
|
+
/** For `map`: what the consumer maps to instead. */
|
|
424
|
+
actual?: string;
|
|
425
|
+
}
|
|
426
|
+
interface SchemaCheckReport {
|
|
427
|
+
/** Platform models the consumer does not carry — informational. */
|
|
428
|
+
absentModels: string[];
|
|
429
|
+
/** Platform enums the consumer does not carry — informational. */
|
|
430
|
+
absentEnums: string[];
|
|
431
|
+
missingFields: MissingField[];
|
|
432
|
+
missingEnumValues: MissingEnumValue[];
|
|
433
|
+
fieldMismatches: FieldMismatch[];
|
|
434
|
+
/**
|
|
435
|
+
* Block-level attributes the spec declares and the consumer lacks:
|
|
436
|
+
* `@@index`, `@@unique`, and a diverging `@@map`.
|
|
437
|
+
*/
|
|
438
|
+
missingBlockAttributes: MissingBlockAttribute[];
|
|
439
|
+
/** Models present in both schemas, i.e. actually compared. */
|
|
440
|
+
checkedModelCount: number;
|
|
441
|
+
/** Enums present in both schemas, i.e. actually compared. */
|
|
442
|
+
checkedEnumCount: number;
|
|
443
|
+
/** True when nothing that breaks platform code was found. */
|
|
444
|
+
ok: boolean;
|
|
445
|
+
}
|
|
446
|
+
/** Parses the field lines of a `model` block into signatures, keyed by name. */
|
|
447
|
+
declare function parseFields(block: string): Map<string, FieldSignature>;
|
|
448
|
+
/**
|
|
449
|
+
* Parses the members of an `enum` block. Values may share a line
|
|
450
|
+
* (`enum Role { ADMIN USER }`); a trailing `@map(…)` attribute is not a value.
|
|
451
|
+
*/
|
|
452
|
+
declare function parseEnumValues(block: string): string[];
|
|
453
|
+
/**
|
|
454
|
+
* Parses the block-level attributes of a `model`. Comparing these is what
|
|
455
|
+
* catches a missing index or unique constraint — differences the field-level
|
|
456
|
+
* comparison is blind to.
|
|
457
|
+
*/
|
|
458
|
+
declare function parseBlockAttributes(name: string, block: string): BlockAttributes;
|
|
459
|
+
/** Parses every `model` and `enum` declaration of a Prisma schema. */
|
|
460
|
+
declare function parseSchema(schema: string): ParsedSchema;
|
|
461
|
+
/**
|
|
462
|
+
* A missing index costs query time; a missing `@@unique` or a diverging
|
|
463
|
+
* `@@map` breaks correctness — the platform relies on the constraint holding,
|
|
464
|
+
* and on finding the table under its canonical name. Only the latter two fail
|
|
465
|
+
* the check.
|
|
466
|
+
*/
|
|
467
|
+
declare function breaksContract(attribute: MissingBlockAttribute): boolean;
|
|
468
|
+
/**
|
|
469
|
+
* Compares a consumer schema against the canonical fragments. `specSchema` is
|
|
470
|
+
* the concatenation of the fragments the check should cover.
|
|
471
|
+
*/
|
|
472
|
+
declare function checkSchema(specSchema: string, appSchema: string): SchemaCheckReport;
|
|
473
|
+
|
|
358
474
|
interface CliContextModuleOptions {
|
|
359
475
|
config: CliContextConfig;
|
|
360
476
|
userPort: ProviderSpec<UserPort>;
|
|
@@ -545,4 +661,4 @@ declare class UserCommands extends CommandRunner {
|
|
|
545
661
|
parsePassword(val: string): string;
|
|
546
662
|
}
|
|
547
663
|
|
|
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 };
|
|
664
|
+
export { AUDIT_QUERY_PORT_TOKEN, AdminCommands, AdminManifestDoctorCheck, AdminMfaSetupCommand, AdminWhoamiCommand, type ApplyResult, AuditCommands, AuditTailCommand, AuditTailFlow, type AuditTailOptions, type BlockAttributeKind, type BlockAttributes, 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 MissingBlockAttribute, 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, breaksContract, checkSchema, extractBlockNames, extractBlocks, extractModelBlocks, extractModelNames, parseBlockAttributes, 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,101 @@ 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 BlockAttributes {
|
|
387
|
+
/** Field lists of `@@index`, normalised to `[a,b]` — attribute options dropped. */
|
|
388
|
+
indexes: Set<string>;
|
|
389
|
+
/** Same for `@@unique`. */
|
|
390
|
+
uniques: Set<string>;
|
|
391
|
+
/** `@@map` target, or the model name when unmapped. */
|
|
392
|
+
map: string;
|
|
393
|
+
}
|
|
394
|
+
interface ParsedSchema {
|
|
395
|
+
models: Map<string, Map<string, FieldSignature>>;
|
|
396
|
+
modelAttributes: Map<string, BlockAttributes>;
|
|
397
|
+
enums: Map<string, string[]>;
|
|
398
|
+
}
|
|
399
|
+
interface MissingField {
|
|
400
|
+
model: string;
|
|
401
|
+
field: string;
|
|
402
|
+
/** Rendered spec type incl. modifiers, e.g. `DateTime?`. */
|
|
403
|
+
type: string;
|
|
404
|
+
}
|
|
405
|
+
interface MissingEnumValue {
|
|
406
|
+
enum: string;
|
|
407
|
+
value: string;
|
|
408
|
+
}
|
|
409
|
+
type FieldMismatchReason = 'type' | 'optionality' | 'list';
|
|
410
|
+
interface FieldMismatch {
|
|
411
|
+
model: string;
|
|
412
|
+
field: string;
|
|
413
|
+
reason: FieldMismatchReason;
|
|
414
|
+
expected: string;
|
|
415
|
+
actual: string;
|
|
416
|
+
}
|
|
417
|
+
type BlockAttributeKind = 'index' | 'unique' | 'map';
|
|
418
|
+
interface MissingBlockAttribute {
|
|
419
|
+
model: string;
|
|
420
|
+
kind: BlockAttributeKind;
|
|
421
|
+
/** Rendered attribute, e.g. `@@index([planId, validFrom])`. */
|
|
422
|
+
expected: string;
|
|
423
|
+
/** For `map`: what the consumer maps to instead. */
|
|
424
|
+
actual?: string;
|
|
425
|
+
}
|
|
426
|
+
interface SchemaCheckReport {
|
|
427
|
+
/** Platform models the consumer does not carry — informational. */
|
|
428
|
+
absentModels: string[];
|
|
429
|
+
/** Platform enums the consumer does not carry — informational. */
|
|
430
|
+
absentEnums: string[];
|
|
431
|
+
missingFields: MissingField[];
|
|
432
|
+
missingEnumValues: MissingEnumValue[];
|
|
433
|
+
fieldMismatches: FieldMismatch[];
|
|
434
|
+
/**
|
|
435
|
+
* Block-level attributes the spec declares and the consumer lacks:
|
|
436
|
+
* `@@index`, `@@unique`, and a diverging `@@map`.
|
|
437
|
+
*/
|
|
438
|
+
missingBlockAttributes: MissingBlockAttribute[];
|
|
439
|
+
/** Models present in both schemas, i.e. actually compared. */
|
|
440
|
+
checkedModelCount: number;
|
|
441
|
+
/** Enums present in both schemas, i.e. actually compared. */
|
|
442
|
+
checkedEnumCount: number;
|
|
443
|
+
/** True when nothing that breaks platform code was found. */
|
|
444
|
+
ok: boolean;
|
|
445
|
+
}
|
|
446
|
+
/** Parses the field lines of a `model` block into signatures, keyed by name. */
|
|
447
|
+
declare function parseFields(block: string): Map<string, FieldSignature>;
|
|
448
|
+
/**
|
|
449
|
+
* Parses the members of an `enum` block. Values may share a line
|
|
450
|
+
* (`enum Role { ADMIN USER }`); a trailing `@map(…)` attribute is not a value.
|
|
451
|
+
*/
|
|
452
|
+
declare function parseEnumValues(block: string): string[];
|
|
453
|
+
/**
|
|
454
|
+
* Parses the block-level attributes of a `model`. Comparing these is what
|
|
455
|
+
* catches a missing index or unique constraint — differences the field-level
|
|
456
|
+
* comparison is blind to.
|
|
457
|
+
*/
|
|
458
|
+
declare function parseBlockAttributes(name: string, block: string): BlockAttributes;
|
|
459
|
+
/** Parses every `model` and `enum` declaration of a Prisma schema. */
|
|
460
|
+
declare function parseSchema(schema: string): ParsedSchema;
|
|
461
|
+
/**
|
|
462
|
+
* A missing index costs query time; a missing `@@unique` or a diverging
|
|
463
|
+
* `@@map` breaks correctness — the platform relies on the constraint holding,
|
|
464
|
+
* and on finding the table under its canonical name. Only the latter two fail
|
|
465
|
+
* the check.
|
|
466
|
+
*/
|
|
467
|
+
declare function breaksContract(attribute: MissingBlockAttribute): boolean;
|
|
468
|
+
/**
|
|
469
|
+
* Compares a consumer schema against the canonical fragments. `specSchema` is
|
|
470
|
+
* the concatenation of the fragments the check should cover.
|
|
471
|
+
*/
|
|
472
|
+
declare function checkSchema(specSchema: string, appSchema: string): SchemaCheckReport;
|
|
473
|
+
|
|
358
474
|
interface CliContextModuleOptions {
|
|
359
475
|
config: CliContextConfig;
|
|
360
476
|
userPort: ProviderSpec<UserPort>;
|
|
@@ -545,4 +661,4 @@ declare class UserCommands extends CommandRunner {
|
|
|
545
661
|
parsePassword(val: string): string;
|
|
546
662
|
}
|
|
547
663
|
|
|
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 };
|
|
664
|
+
export { AUDIT_QUERY_PORT_TOKEN, AdminCommands, AdminManifestDoctorCheck, AdminMfaSetupCommand, AdminWhoamiCommand, type ApplyResult, AuditCommands, AuditTailCommand, AuditTailFlow, type AuditTailOptions, type BlockAttributeKind, type BlockAttributes, 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 MissingBlockAttribute, 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, breaksContract, checkSchema, extractBlockNames, extractBlocks, extractModelBlocks, extractModelNames, parseBlockAttributes, 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,188 @@ 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 attributeFieldLists(block, attribute) {
|
|
1170
|
+
const pattern = new RegExp(`@@${attribute}\\(([^)]*\\])`, "g");
|
|
1171
|
+
return new Set([
|
|
1172
|
+
...block.matchAll(pattern)
|
|
1173
|
+
].map((match) => match[1].replace(/\s+/g, "")));
|
|
1174
|
+
}
|
|
1175
|
+
__name(attributeFieldLists, "attributeFieldLists");
|
|
1176
|
+
function parseBlockAttributes(name, block) {
|
|
1177
|
+
return {
|
|
1178
|
+
indexes: attributeFieldLists(block, "index"),
|
|
1179
|
+
uniques: attributeFieldLists(block, "unique"),
|
|
1180
|
+
map: block.match(/@@map\("([^"]+)"\)/)?.[1] ?? name
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
__name(parseBlockAttributes, "parseBlockAttributes");
|
|
1184
|
+
function parseSchema(schema) {
|
|
1185
|
+
const models = /* @__PURE__ */ new Map();
|
|
1186
|
+
const modelAttributes = /* @__PURE__ */ new Map();
|
|
1187
|
+
for (const [name, block] of extractBlocks(schema, "model")) {
|
|
1188
|
+
models.set(name, parseFields(block));
|
|
1189
|
+
modelAttributes.set(name, parseBlockAttributes(name, block));
|
|
1190
|
+
}
|
|
1191
|
+
const enums = /* @__PURE__ */ new Map();
|
|
1192
|
+
for (const [name, block] of extractBlocks(schema, "enum")) {
|
|
1193
|
+
enums.set(name, parseEnumValues(block));
|
|
1194
|
+
}
|
|
1195
|
+
return {
|
|
1196
|
+
models,
|
|
1197
|
+
modelAttributes,
|
|
1198
|
+
enums
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
__name(parseSchema, "parseSchema");
|
|
1202
|
+
function isDocumentedTypeSubstitution(spec, app, appEnums) {
|
|
1203
|
+
return spec.type === "String" && appEnums.has(app.type);
|
|
1204
|
+
}
|
|
1205
|
+
__name(isDocumentedTypeSubstitution, "isDocumentedTypeSubstitution");
|
|
1206
|
+
function compareFields(model, specFields, appFields, appEnums, missingFields, fieldMismatches) {
|
|
1207
|
+
for (const [name, spec] of specFields) {
|
|
1208
|
+
const app = appFields.get(name);
|
|
1209
|
+
if (!app) {
|
|
1210
|
+
missingFields.push({
|
|
1211
|
+
model,
|
|
1212
|
+
field: name,
|
|
1213
|
+
type: renderType(spec)
|
|
1214
|
+
});
|
|
1215
|
+
continue;
|
|
1216
|
+
}
|
|
1217
|
+
const mismatch = /* @__PURE__ */ __name((reason) => ({
|
|
1218
|
+
model,
|
|
1219
|
+
field: name,
|
|
1220
|
+
reason,
|
|
1221
|
+
expected: renderType(spec),
|
|
1222
|
+
actual: renderType(app)
|
|
1223
|
+
}), "mismatch");
|
|
1224
|
+
if (spec.type !== app.type && !isDocumentedTypeSubstitution(spec, app, appEnums)) {
|
|
1225
|
+
fieldMismatches.push(mismatch("type"));
|
|
1226
|
+
} else if (spec.list !== app.list) {
|
|
1227
|
+
fieldMismatches.push(mismatch("list"));
|
|
1228
|
+
} else if (!spec.optional && app.optional) {
|
|
1229
|
+
fieldMismatches.push(mismatch("optionality"));
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
__name(compareFields, "compareFields");
|
|
1234
|
+
function compareBlockAttributes(model, spec, app, out) {
|
|
1235
|
+
if (spec.map !== app.map) {
|
|
1236
|
+
out.push({
|
|
1237
|
+
model,
|
|
1238
|
+
kind: "map",
|
|
1239
|
+
expected: `@@map("${spec.map}")`,
|
|
1240
|
+
actual: app.map
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
for (const fields of spec.uniques) {
|
|
1244
|
+
if (!app.uniques.has(fields)) {
|
|
1245
|
+
out.push({
|
|
1246
|
+
model,
|
|
1247
|
+
kind: "unique",
|
|
1248
|
+
expected: `@@unique(${fields})`
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
for (const fields of spec.indexes) {
|
|
1253
|
+
if (!app.indexes.has(fields)) {
|
|
1254
|
+
out.push({
|
|
1255
|
+
model,
|
|
1256
|
+
kind: "index",
|
|
1257
|
+
expected: `@@index(${fields})`
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
__name(compareBlockAttributes, "compareBlockAttributes");
|
|
1263
|
+
function breaksContract(attribute) {
|
|
1264
|
+
return attribute.kind !== "index";
|
|
1265
|
+
}
|
|
1266
|
+
__name(breaksContract, "breaksContract");
|
|
1267
|
+
function checkSchema(specSchema, appSchema) {
|
|
1268
|
+
const spec = parseSchema(specSchema);
|
|
1269
|
+
const app = parseSchema(appSchema);
|
|
1270
|
+
const absentModels = [];
|
|
1271
|
+
const missingFields = [];
|
|
1272
|
+
const fieldMismatches = [];
|
|
1273
|
+
const missingBlockAttributes = [];
|
|
1274
|
+
for (const [model, specFields] of spec.models) {
|
|
1275
|
+
const appFields = app.models.get(model);
|
|
1276
|
+
if (!appFields) {
|
|
1277
|
+
absentModels.push(model);
|
|
1278
|
+
continue;
|
|
1279
|
+
}
|
|
1280
|
+
compareFields(model, specFields, appFields, app.enums, missingFields, fieldMismatches);
|
|
1281
|
+
const specAttrs = spec.modelAttributes.get(model);
|
|
1282
|
+
const appAttrs = app.modelAttributes.get(model);
|
|
1283
|
+
if (specAttrs && appAttrs) {
|
|
1284
|
+
compareBlockAttributes(model, specAttrs, appAttrs, missingBlockAttributes);
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
const absentEnums = [];
|
|
1288
|
+
const missingEnumValues = [];
|
|
1289
|
+
for (const [name, specValues] of spec.enums) {
|
|
1290
|
+
const appValues = app.enums.get(name);
|
|
1291
|
+
if (!appValues) {
|
|
1292
|
+
absentEnums.push(name);
|
|
1293
|
+
continue;
|
|
1294
|
+
}
|
|
1295
|
+
for (const value of specValues) {
|
|
1296
|
+
if (!appValues.includes(value)) {
|
|
1297
|
+
missingEnumValues.push({
|
|
1298
|
+
enum: name,
|
|
1299
|
+
value
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
return {
|
|
1305
|
+
absentModels,
|
|
1306
|
+
absentEnums,
|
|
1307
|
+
missingFields,
|
|
1308
|
+
missingEnumValues,
|
|
1309
|
+
fieldMismatches,
|
|
1310
|
+
missingBlockAttributes,
|
|
1311
|
+
checkedModelCount: spec.models.size - absentModels.length,
|
|
1312
|
+
checkedEnumCount: spec.enums.size - absentEnums.length,
|
|
1313
|
+
ok: missingFields.length === 0 && missingEnumValues.length === 0 && fieldMismatches.length === 0 && !missingBlockAttributes.some(breaksContract)
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
__name(checkSchema, "checkSchema");
|
|
1317
|
+
|
|
1122
1318
|
// src/module.ts
|
|
1123
1319
|
import { Module } from "@nestjs/common";
|
|
1124
1320
|
import { asProvider } from "@saasicat/nest";
|
|
@@ -2207,6 +2403,16 @@ export {
|
|
|
2207
2403
|
UserPortDoctorCheck,
|
|
2208
2404
|
WhoAmIFlow,
|
|
2209
2405
|
applyFragmentBlocks,
|
|
2406
|
+
blockBodyLines,
|
|
2407
|
+
breaksContract,
|
|
2408
|
+
checkSchema,
|
|
2409
|
+
extractBlockNames,
|
|
2410
|
+
extractBlocks,
|
|
2210
2411
|
extractModelBlocks,
|
|
2211
|
-
extractModelNames
|
|
2412
|
+
extractModelNames,
|
|
2413
|
+
parseBlockAttributes,
|
|
2414
|
+
parseEnumValues,
|
|
2415
|
+
parseFields,
|
|
2416
|
+
parseSchema,
|
|
2417
|
+
stripLineComment
|
|
2212
2418
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@saasicat/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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.12.0",
|
|
31
|
+
"@saasicat/spec": "^0.12.0",
|
|
32
|
+
"@saasicat/types": "^0.12.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"@nestjs/common": "^11.0.0",
|