speckeeper 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -290,6 +290,28 @@ class EntityModel extends Model<typeof EntitySchema> {
290
290
 
291
291
  Without `deepValidation`, speckeeper still performs existence checks for all spec IDs across all configured sources.
292
292
 
293
+ #### Lookup keys (when spec ID differs from external identifier)
294
+
295
+ By default, the global scanner searches for each spec's `id` in external sources. When the external identifier differs — for example, entity ID `"user"` vs DDL table name `"users"` — define `lookupKeys` on the model to map per source type:
296
+
297
+ ```typescript
298
+ class EntityModel extends Model<typeof EntitySchema> {
299
+ readonly id = 'entity';
300
+ readonly name = 'Entity';
301
+ readonly idPrefix = 'ENT';
302
+ readonly schema = EntitySchema;
303
+
304
+ protected lookupKeys: LookupKeyConfig<Entity> = {
305
+ ddl: (spec) => spec.tableName,
306
+ openapi: (spec) => spec.schemaName ?? spec.id,
307
+ };
308
+ }
309
+ ```
310
+
311
+ With this configuration, when scanning DDL sources the scanner searches for `spec.tableName` instead of `spec.id`. If a match is found, the result is mapped back to the original spec ID for reporting and deep validation.
312
+
313
+ `lookupKeys` is optional per source type — any source type not listed falls back to `spec.id`.
314
+
293
315
  #### Custom scanners
294
316
 
295
317
  For file formats not covered by the built-in scanners, provide a custom `SourceScanner` plugin:
package/dist/cli.js CHANGED
@@ -727,7 +727,7 @@ function loadFileContent(filePath, sourceType) {
727
727
  }
728
728
  return content;
729
729
  }
730
- function runGlobalScan(sources, specIds, basePath) {
730
+ function runGlobalScan(sources, specIds, basePath, lookupKeyMap) {
731
731
  const cwd = basePath ?? process.cwd();
732
732
  const result = /* @__PURE__ */ new Map();
733
733
  const warnings = [];
@@ -740,6 +740,13 @@ function runGlobalScan(sources, specIds, basePath) {
740
740
  });
741
741
  continue;
742
742
  }
743
+ const keyToSpecId = /* @__PURE__ */ new Map();
744
+ for (const specId of specIds) {
745
+ const overrides = lookupKeyMap?.get(specId);
746
+ const key = overrides?.[source.type] ?? specId;
747
+ keyToSpecId.set(key, specId);
748
+ }
749
+ const keysToSearch = Array.from(keyToSpecId.keys());
743
750
  const allFiles = /* @__PURE__ */ new Set();
744
751
  for (const pattern of source.paths) {
745
752
  const found = glob.sync(pattern, {
@@ -768,7 +775,7 @@ function runGlobalScan(sources, specIds, basePath) {
768
775
  if (content == null) continue;
769
776
  let matches;
770
777
  try {
771
- matches = scanner.findSpecIds(content, specIds, relPath);
778
+ matches = scanner.findSpecIds(content, keysToSearch, relPath);
772
779
  } catch (e) {
773
780
  const msg = e instanceof Error ? e.message : String(e);
774
781
  warnings.push({
@@ -779,17 +786,19 @@ function runGlobalScan(sources, specIds, basePath) {
779
786
  continue;
780
787
  }
781
788
  for (const match of matches) {
789
+ const originalSpecId = keyToSpecId.get(match.specId) ?? match.specId;
782
790
  const globalMatch = {
783
791
  ...match,
792
+ specId: originalSpecId,
784
793
  sourceType: source.type,
785
794
  relation: source.relation,
786
795
  filePath: relPath
787
796
  };
788
- const existing = result.get(match.specId);
797
+ const existing = result.get(originalSpecId);
789
798
  if (existing) {
790
799
  existing.push(globalMatch);
791
800
  } else {
792
- result.set(match.specId, [globalMatch]);
801
+ result.set(originalSpecId, [globalMatch]);
793
802
  }
794
803
  }
795
804
  }
@@ -932,17 +941,35 @@ async function checkCommand(type, options) {
932
941
  const results = [];
933
942
  const allSpecIds = [];
934
943
  const specIdToModel = /* @__PURE__ */ new Map();
944
+ const lookupKeyMap = /* @__PURE__ */ new Map();
935
945
  for (const model of models) {
936
946
  const modelSpecs = getSpecsFromConfig(specs, model.id);
947
+ const modelLookupKeys = model.getLookupKeys?.();
937
948
  for (const spec of modelSpecs) {
938
949
  const specId = spec.id;
939
950
  allSpecIds.push(specId);
940
951
  specIdToModel.set(specId, { modelId: model.id, spec });
952
+ if (modelLookupKeys) {
953
+ const overrides = {};
954
+ for (const [sourceType, mapper] of Object.entries(modelLookupKeys)) {
955
+ if (typeof mapper === "function") {
956
+ overrides[sourceType] = mapper(spec);
957
+ }
958
+ }
959
+ if (Object.keys(overrides).length > 0) {
960
+ lookupKeyMap.set(specId, overrides);
961
+ }
962
+ }
941
963
  }
942
964
  }
943
965
  if (sources.length > 0 && allSpecIds.length > 0) {
944
966
  const filteredSources = checkType === "all" ? sources : sources.filter((s) => s.type === checkType);
945
- const { matches, warnings: scanWarnings } = runGlobalScan(filteredSources, allSpecIds, cwd);
967
+ const { matches, warnings: scanWarnings } = runGlobalScan(
968
+ filteredSources,
969
+ allSpecIds,
970
+ cwd,
971
+ lookupKeyMap.size > 0 ? lookupKeyMap : void 0
972
+ );
946
973
  for (const sw of scanWarnings) {
947
974
  results.push({
948
975
  type: sw.sourceType,