@saasicat/cli 0.11.0 → 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/saasicat.js CHANGED
@@ -169,6 +169,26 @@ function printCheckReport(report) {
169
169
  console.log('');
170
170
  }
171
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
+
172
192
  const absent = [...report.absentModels, ...report.absentEnums];
173
193
  if (absent.length > 0) {
174
194
  console.log(`→ Nicht übernommen (${absent.length}): ${absent.join(', ')}`);
package/dist/index.cjs CHANGED
@@ -68,11 +68,13 @@ __export(index_exports, {
68
68
  WhoAmIFlow: () => WhoAmIFlow,
69
69
  applyFragmentBlocks: () => applyFragmentBlocks,
70
70
  blockBodyLines: () => blockBodyLines,
71
+ breaksContract: () => breaksContract,
71
72
  checkSchema: () => checkSchema,
72
73
  extractBlockNames: () => extractBlockNames,
73
74
  extractBlocks: () => extractBlocks,
74
75
  extractModelBlocks: () => extractModelBlocks,
75
76
  extractModelNames: () => extractModelNames,
77
+ parseBlockAttributes: () => parseBlockAttributes,
76
78
  parseEnumValues: () => parseEnumValues,
77
79
  parseFields: () => parseFields,
78
80
  parseSchema: () => parseSchema,
@@ -1245,10 +1247,27 @@ function parseEnumValues(block) {
1245
1247
  return values;
1246
1248
  }
1247
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");
1248
1265
  function parseSchema(schema) {
1249
1266
  const models = /* @__PURE__ */ new Map();
1267
+ const modelAttributes = /* @__PURE__ */ new Map();
1250
1268
  for (const [name, block] of extractBlocks(schema, "model")) {
1251
1269
  models.set(name, parseFields(block));
1270
+ modelAttributes.set(name, parseBlockAttributes(name, block));
1252
1271
  }
1253
1272
  const enums = /* @__PURE__ */ new Map();
1254
1273
  for (const [name, block] of extractBlocks(schema, "enum")) {
@@ -1256,6 +1275,7 @@ function parseSchema(schema) {
1256
1275
  }
1257
1276
  return {
1258
1277
  models,
1278
+ modelAttributes,
1259
1279
  enums
1260
1280
  };
1261
1281
  }
@@ -1292,12 +1312,46 @@ function compareFields(model, specFields, appFields, appEnums, missingFields, fi
1292
1312
  }
1293
1313
  }
1294
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");
1295
1348
  function checkSchema(specSchema, appSchema) {
1296
1349
  const spec = parseSchema(specSchema);
1297
1350
  const app = parseSchema(appSchema);
1298
1351
  const absentModels = [];
1299
1352
  const missingFields = [];
1300
1353
  const fieldMismatches = [];
1354
+ const missingBlockAttributes = [];
1301
1355
  for (const [model, specFields] of spec.models) {
1302
1356
  const appFields = app.models.get(model);
1303
1357
  if (!appFields) {
@@ -1305,6 +1359,11 @@ function checkSchema(specSchema, appSchema) {
1305
1359
  continue;
1306
1360
  }
1307
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
+ }
1308
1367
  }
1309
1368
  const absentEnums = [];
1310
1369
  const missingEnumValues = [];
@@ -1329,9 +1388,10 @@ function checkSchema(specSchema, appSchema) {
1329
1388
  missingFields,
1330
1389
  missingEnumValues,
1331
1390
  fieldMismatches,
1391
+ missingBlockAttributes,
1332
1392
  checkedModelCount: spec.models.size - absentModels.length,
1333
1393
  checkedEnumCount: spec.enums.size - absentEnums.length,
1334
- ok: missingFields.length === 0 && missingEnumValues.length === 0 && fieldMismatches.length === 0
1394
+ ok: missingFields.length === 0 && missingEnumValues.length === 0 && fieldMismatches.length === 0 && !missingBlockAttributes.some(breaksContract)
1335
1395
  };
1336
1396
  }
1337
1397
  __name(checkSchema, "checkSchema");
@@ -2426,11 +2486,13 @@ UserCommands = _ts_decorate14([
2426
2486
  WhoAmIFlow,
2427
2487
  applyFragmentBlocks,
2428
2488
  blockBodyLines,
2489
+ breaksContract,
2429
2490
  checkSchema,
2430
2491
  extractBlockNames,
2431
2492
  extractBlocks,
2432
2493
  extractModelBlocks,
2433
2494
  extractModelNames,
2495
+ parseBlockAttributes,
2434
2496
  parseEnumValues,
2435
2497
  parseFields,
2436
2498
  parseSchema,
package/dist/index.d.cts CHANGED
@@ -383,8 +383,17 @@ interface FieldSignature {
383
383
  optional: boolean;
384
384
  list: boolean;
385
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
+ }
386
394
  interface ParsedSchema {
387
395
  models: Map<string, Map<string, FieldSignature>>;
396
+ modelAttributes: Map<string, BlockAttributes>;
388
397
  enums: Map<string, string[]>;
389
398
  }
390
399
  interface MissingField {
@@ -405,6 +414,15 @@ interface FieldMismatch {
405
414
  expected: string;
406
415
  actual: string;
407
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
+ }
408
426
  interface SchemaCheckReport {
409
427
  /** Platform models the consumer does not carry — informational. */
410
428
  absentModels: string[];
@@ -413,6 +431,11 @@ interface SchemaCheckReport {
413
431
  missingFields: MissingField[];
414
432
  missingEnumValues: MissingEnumValue[];
415
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[];
416
439
  /** Models present in both schemas, i.e. actually compared. */
417
440
  checkedModelCount: number;
418
441
  /** Enums present in both schemas, i.e. actually compared. */
@@ -427,8 +450,21 @@ declare function parseFields(block: string): Map<string, FieldSignature>;
427
450
  * (`enum Role { ADMIN USER }`); a trailing `@map(…)` attribute is not a value.
428
451
  */
429
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;
430
459
  /** Parses every `model` and `enum` declaration of a Prisma schema. */
431
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;
432
468
  /**
433
469
  * Compares a consumer schema against the canonical fragments. `specSchema` is
434
470
  * the concatenation of the fragments the check should cover.
@@ -625,4 +661,4 @@ declare class UserCommands extends CommandRunner {
625
661
  parsePassword(val: string): string;
626
662
  }
627
663
 
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 };
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
@@ -383,8 +383,17 @@ interface FieldSignature {
383
383
  optional: boolean;
384
384
  list: boolean;
385
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
+ }
386
394
  interface ParsedSchema {
387
395
  models: Map<string, Map<string, FieldSignature>>;
396
+ modelAttributes: Map<string, BlockAttributes>;
388
397
  enums: Map<string, string[]>;
389
398
  }
390
399
  interface MissingField {
@@ -405,6 +414,15 @@ interface FieldMismatch {
405
414
  expected: string;
406
415
  actual: string;
407
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
+ }
408
426
  interface SchemaCheckReport {
409
427
  /** Platform models the consumer does not carry — informational. */
410
428
  absentModels: string[];
@@ -413,6 +431,11 @@ interface SchemaCheckReport {
413
431
  missingFields: MissingField[];
414
432
  missingEnumValues: MissingEnumValue[];
415
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[];
416
439
  /** Models present in both schemas, i.e. actually compared. */
417
440
  checkedModelCount: number;
418
441
  /** Enums present in both schemas, i.e. actually compared. */
@@ -427,8 +450,21 @@ declare function parseFields(block: string): Map<string, FieldSignature>;
427
450
  * (`enum Role { ADMIN USER }`); a trailing `@map(…)` attribute is not a value.
428
451
  */
429
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;
430
459
  /** Parses every `model` and `enum` declaration of a Prisma schema. */
431
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;
432
468
  /**
433
469
  * Compares a consumer schema against the canonical fragments. `specSchema` is
434
470
  * the concatenation of the fragments the check should cover.
@@ -625,4 +661,4 @@ declare class UserCommands extends CommandRunner {
625
661
  parsePassword(val: string): string;
626
662
  }
627
663
 
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 };
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
@@ -1166,10 +1166,27 @@ function parseEnumValues(block) {
1166
1166
  return values;
1167
1167
  }
1168
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");
1169
1184
  function parseSchema(schema) {
1170
1185
  const models = /* @__PURE__ */ new Map();
1186
+ const modelAttributes = /* @__PURE__ */ new Map();
1171
1187
  for (const [name, block] of extractBlocks(schema, "model")) {
1172
1188
  models.set(name, parseFields(block));
1189
+ modelAttributes.set(name, parseBlockAttributes(name, block));
1173
1190
  }
1174
1191
  const enums = /* @__PURE__ */ new Map();
1175
1192
  for (const [name, block] of extractBlocks(schema, "enum")) {
@@ -1177,6 +1194,7 @@ function parseSchema(schema) {
1177
1194
  }
1178
1195
  return {
1179
1196
  models,
1197
+ modelAttributes,
1180
1198
  enums
1181
1199
  };
1182
1200
  }
@@ -1213,12 +1231,46 @@ function compareFields(model, specFields, appFields, appEnums, missingFields, fi
1213
1231
  }
1214
1232
  }
1215
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");
1216
1267
  function checkSchema(specSchema, appSchema) {
1217
1268
  const spec = parseSchema(specSchema);
1218
1269
  const app = parseSchema(appSchema);
1219
1270
  const absentModels = [];
1220
1271
  const missingFields = [];
1221
1272
  const fieldMismatches = [];
1273
+ const missingBlockAttributes = [];
1222
1274
  for (const [model, specFields] of spec.models) {
1223
1275
  const appFields = app.models.get(model);
1224
1276
  if (!appFields) {
@@ -1226,6 +1278,11 @@ function checkSchema(specSchema, appSchema) {
1226
1278
  continue;
1227
1279
  }
1228
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
+ }
1229
1286
  }
1230
1287
  const absentEnums = [];
1231
1288
  const missingEnumValues = [];
@@ -1250,9 +1307,10 @@ function checkSchema(specSchema, appSchema) {
1250
1307
  missingFields,
1251
1308
  missingEnumValues,
1252
1309
  fieldMismatches,
1310
+ missingBlockAttributes,
1253
1311
  checkedModelCount: spec.models.size - absentModels.length,
1254
1312
  checkedEnumCount: spec.enums.size - absentEnums.length,
1255
- ok: missingFields.length === 0 && missingEnumValues.length === 0 && fieldMismatches.length === 0
1313
+ ok: missingFields.length === 0 && missingEnumValues.length === 0 && fieldMismatches.length === 0 && !missingBlockAttributes.some(breaksContract)
1256
1314
  };
1257
1315
  }
1258
1316
  __name(checkSchema, "checkSchema");
@@ -2346,11 +2404,13 @@ export {
2346
2404
  WhoAmIFlow,
2347
2405
  applyFragmentBlocks,
2348
2406
  blockBodyLines,
2407
+ breaksContract,
2349
2408
  checkSchema,
2350
2409
  extractBlockNames,
2351
2410
  extractBlocks,
2352
2411
  extractModelBlocks,
2353
2412
  extractModelNames,
2413
+ parseBlockAttributes,
2354
2414
  parseEnumValues,
2355
2415
  parseFields,
2356
2416
  parseSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saasicat/cli",
3
- "version": "0.11.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",
@@ -27,9 +27,9 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "qrcode-terminal": "^0.12.0",
30
- "@saasicat/nest": "^0.11.0",
31
- "@saasicat/spec": "^0.11.0",
32
- "@saasicat/types": "^0.11.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",