prisma-schema-auditor 1.0.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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +62 -0
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -0
  5. package/dist/cli.js +132 -0
  6. package/dist/cli.js.map +1 -0
  7. package/dist/core/analysis/computeKeys.d.ts +13 -0
  8. package/dist/core/analysis/computeKeys.d.ts.map +1 -0
  9. package/dist/core/analysis/computeKeys.js +27 -0
  10. package/dist/core/analysis/computeKeys.js.map +1 -0
  11. package/dist/core/analysis/inferFds.d.ts +31 -0
  12. package/dist/core/analysis/inferFds.d.ts.map +1 -0
  13. package/dist/core/analysis/inferFds.js +87 -0
  14. package/dist/core/analysis/inferFds.js.map +1 -0
  15. package/dist/core/analysis/normalizeChecks/check1nf.d.ts +11 -0
  16. package/dist/core/analysis/normalizeChecks/check1nf.d.ts.map +1 -0
  17. package/dist/core/analysis/normalizeChecks/check1nf.js +91 -0
  18. package/dist/core/analysis/normalizeChecks/check1nf.js.map +1 -0
  19. package/dist/core/analysis/normalizeChecks/check2nf.d.ts +13 -0
  20. package/dist/core/analysis/normalizeChecks/check2nf.d.ts.map +1 -0
  21. package/dist/core/analysis/normalizeChecks/check2nf.js +80 -0
  22. package/dist/core/analysis/normalizeChecks/check2nf.js.map +1 -0
  23. package/dist/core/analysis/normalizeChecks/check3nf.d.ts +18 -0
  24. package/dist/core/analysis/normalizeChecks/check3nf.d.ts.map +1 -0
  25. package/dist/core/analysis/normalizeChecks/check3nf.js +71 -0
  26. package/dist/core/analysis/normalizeChecks/check3nf.js.map +1 -0
  27. package/dist/core/invariants/parse.d.ts +19 -0
  28. package/dist/core/invariants/parse.d.ts.map +1 -0
  29. package/dist/core/invariants/parse.js +73 -0
  30. package/dist/core/invariants/parse.js.map +1 -0
  31. package/dist/core/invariants/schema.d.ts +24 -0
  32. package/dist/core/invariants/schema.d.ts.map +1 -0
  33. package/dist/core/invariants/schema.js +20 -0
  34. package/dist/core/invariants/schema.js.map +1 -0
  35. package/dist/core/prismaSchema/contract.d.ts +8 -0
  36. package/dist/core/prismaSchema/contract.d.ts.map +1 -0
  37. package/dist/core/prismaSchema/contract.js +107 -0
  38. package/dist/core/prismaSchema/contract.js.map +1 -0
  39. package/dist/core/prismaSchema/parse.d.ts +8 -0
  40. package/dist/core/prismaSchema/parse.d.ts.map +1 -0
  41. package/dist/core/prismaSchema/parse.js +46 -0
  42. package/dist/core/prismaSchema/parse.js.map +1 -0
  43. package/dist/core/prismaSchema/types.d.ts +39 -0
  44. package/dist/core/prismaSchema/types.d.ts.map +1 -0
  45. package/dist/core/prismaSchema/types.js +2 -0
  46. package/dist/core/prismaSchema/types.js.map +1 -0
  47. package/dist/core/report/reportTypes.d.ts +72 -0
  48. package/dist/core/report/reportTypes.d.ts.map +1 -0
  49. package/dist/core/report/reportTypes.js +2 -0
  50. package/dist/core/report/reportTypes.js.map +1 -0
  51. package/dist/core/report/toJson.d.ts +7 -0
  52. package/dist/core/report/toJson.d.ts.map +1 -0
  53. package/dist/core/report/toJson.js +27 -0
  54. package/dist/core/report/toJson.js.map +1 -0
  55. package/dist/core/report/toText.d.ts +6 -0
  56. package/dist/core/report/toText.d.ts.map +1 -0
  57. package/dist/core/report/toText.js +46 -0
  58. package/dist/core/report/toText.js.map +1 -0
  59. package/dist/index.d.ts +18 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +46 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/util/index.d.ts +5 -0
  64. package/dist/util/index.d.ts.map +1 -0
  65. package/dist/util/index.js +15 -0
  66. package/dist/util/index.js.map +1 -0
  67. package/package.json +84 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 EddieRydell
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,62 @@
1
+ # prisma-schema-auditor
2
+
3
+ Static analysis for Prisma schemas: deterministic constraint contracts + normalization lint findings (1NF/2NF; 3NF+ via invariants).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install --save-dev prisma-schema-auditor
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### CLI
14
+
15
+ ```bash
16
+ # Analyze default prisma/schema.prisma
17
+ npx prisma-schema-auditor
18
+
19
+ # Specify schema path and output format
20
+ npx prisma-schema-auditor --schema ./prisma/schema.prisma --format text
21
+
22
+ # Write JSON output to file
23
+ npx prisma-schema-auditor --out audit.json --pretty
24
+
25
+ # Fail CI on warnings or errors
26
+ npx prisma-schema-auditor --fail-on warning
27
+ ```
28
+
29
+ ### Options
30
+
31
+ | Option | Description | Default |
32
+ |--------|-------------|---------|
33
+ | `--schema <path>` | Path to Prisma schema file | `prisma/schema.prisma` |
34
+ | `--invariants <path>` | Path to invariants file (JSON) | - |
35
+ | `--format <fmt>` | Output format: `json` or `text` | `json` |
36
+ | `--out <path>` | Write output to file | stdout |
37
+ | `--fail-on <severity>` | Exit 1 if findings at severity or above | - |
38
+ | `--no-timestamp` | Omit timestamp from output | `false` |
39
+ | `--pretty` | Pretty-print JSON output | `false` |
40
+
41
+ ### Exit Codes
42
+
43
+ | Code | Meaning |
44
+ |------|---------|
45
+ | 0 | OK |
46
+ | 1 | Findings at or above `--fail-on` threshold |
47
+ | 2 | CLI usage error |
48
+ | 3 | Schema parse error |
49
+
50
+ ### Programmatic API
51
+
52
+ ```typescript
53
+ import { audit } from 'prisma-schema-auditor';
54
+
55
+ const result = await audit('prisma/schema.prisma');
56
+ console.log(result.contract); // Constraint contract
57
+ console.log(result.findings); // Normalization findings
58
+ ```
59
+
60
+ ## License
61
+
62
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export declare function main(argv?: string[]): Promise<number>;
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAkCA,wBAAsB,IAAI,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAmH3D"}
package/dist/cli.js ADDED
@@ -0,0 +1,132 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from 'node:util';
3
+ import { resolve } from 'node:path';
4
+ import { existsSync, writeFileSync } from 'node:fs';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { audit } from './index.js';
7
+ import { toJson } from './core/report/toJson.js';
8
+ import { toText } from './core/report/toText.js';
9
+ /** Exit codes. */
10
+ const EXIT_OK = 0;
11
+ const EXIT_ISSUES = 1;
12
+ const EXIT_CLI_ERROR = 2;
13
+ const EXIT_PARSE_ERROR = 3;
14
+ function printUsage() {
15
+ process.stdout.write(`Usage: prisma-schema-auditor [options]
16
+
17
+ Options:
18
+ --schema <path> Path to Prisma schema file (default: prisma/schema.prisma)
19
+ --invariants <path> Path to invariants file (JSON)
20
+ --format <fmt> Output format: json | text (default: json)
21
+ --out <path> Write output to file instead of stdout
22
+ --fail-on <severity> Exit 1 if findings at this severity or above: error | warning | info
23
+ --no-timestamp Omit timestamp from output
24
+ --pretty Pretty-print JSON output
25
+ --help Show this help message
26
+ `);
27
+ }
28
+ export async function main(argv) {
29
+ let args;
30
+ try {
31
+ args = parseArgs({
32
+ args: argv,
33
+ options: {
34
+ schema: { type: 'string' },
35
+ invariants: { type: 'string' },
36
+ format: { type: 'string', default: 'json' },
37
+ out: { type: 'string' },
38
+ 'fail-on': { type: 'string' },
39
+ 'no-timestamp': { type: 'boolean', default: false },
40
+ pretty: { type: 'boolean', default: false },
41
+ help: { type: 'boolean', default: false },
42
+ },
43
+ strict: true,
44
+ });
45
+ }
46
+ catch (error) {
47
+ const detail = error instanceof Error ? error.message : 'Invalid arguments';
48
+ process.stderr.write(`Error: ${detail}. Use --help for usage.\n`);
49
+ return EXIT_CLI_ERROR;
50
+ }
51
+ if (args.values['help'] === true) {
52
+ printUsage();
53
+ return EXIT_OK;
54
+ }
55
+ // Resolve schema path
56
+ const schemaPath = resolve(typeof args.values['schema'] === 'string'
57
+ ? args.values['schema']
58
+ : 'prisma/schema.prisma');
59
+ if (!existsSync(schemaPath)) {
60
+ process.stderr.write(`Error: Schema file not found: ${schemaPath}\n`);
61
+ return EXIT_CLI_ERROR;
62
+ }
63
+ // Validate format
64
+ const format = (args.values['format'] ?? 'json');
65
+ if (format !== 'json' && format !== 'text') {
66
+ process.stderr.write(`Error: Invalid format "${format}". Must be "json" or "text".\n`);
67
+ return EXIT_CLI_ERROR;
68
+ }
69
+ const outputFormat = format;
70
+ // Validate fail-on
71
+ const failOn = args.values['fail-on'];
72
+ if (failOn !== undefined &&
73
+ failOn !== 'error' &&
74
+ failOn !== 'warning' &&
75
+ failOn !== 'info') {
76
+ process.stderr.write(`Error: Invalid --fail-on value "${failOn}". Must be "error", "warning", or "info".\n`);
77
+ return EXIT_CLI_ERROR;
78
+ }
79
+ const noTimestamp = args.values['no-timestamp'] === true;
80
+ const pretty = args.values['pretty'] === true;
81
+ // Resolve invariants path if provided
82
+ const invariantsArg = args.values['invariants'];
83
+ let invariantsPath;
84
+ if (invariantsArg !== undefined) {
85
+ invariantsPath = resolve(invariantsArg);
86
+ if (!existsSync(invariantsPath)) {
87
+ process.stderr.write(`Error: Invariants file not found: ${invariantsPath}\n`);
88
+ return EXIT_CLI_ERROR;
89
+ }
90
+ }
91
+ // Run audit
92
+ let result;
93
+ try {
94
+ result = await audit({ schemaPath, invariantsPath, noTimestamp });
95
+ }
96
+ catch (error) {
97
+ const detail = error instanceof Error ? error.message : '';
98
+ process.stderr.write(`Error: Failed to parse schema.${detail !== '' ? ` ${detail}` : ''}\n`);
99
+ return EXIT_PARSE_ERROR;
100
+ }
101
+ // Format output
102
+ const output = outputFormat === 'json' ? toJson(result, pretty) : toText(result);
103
+ // Write output
104
+ const outPath = args.values['out'];
105
+ if (outPath !== undefined) {
106
+ writeFileSync(resolve(outPath), output, 'utf-8');
107
+ }
108
+ else {
109
+ process.stdout.write(output);
110
+ process.stdout.write('\n');
111
+ }
112
+ // Check fail-on threshold
113
+ if (failOn !== undefined && result.findings.length > 0) {
114
+ const severityOrder = { info: 0, warning: 1, error: 2 };
115
+ const threshold = severityOrder[failOn];
116
+ const hasFailure = result.findings.some((f) => severityOrder[f.severity] >= threshold);
117
+ if (hasFailure) {
118
+ return EXIT_ISSUES;
119
+ }
120
+ }
121
+ return EXIT_OK;
122
+ }
123
+ if (process.argv[1] !== undefined && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
124
+ main()
125
+ .then((code) => {
126
+ process.exitCode = code;
127
+ })
128
+ .catch(() => {
129
+ process.exitCode = EXIT_PARSE_ERROR;
130
+ });
131
+ }
132
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { ConstraintContract } from '../report/reportTypes.js';
2
+ /** A candidate key for a model. */
3
+ export interface CandidateKey {
4
+ readonly fields: readonly string[];
5
+ readonly model: string;
6
+ readonly source: 'pk' | 'unique';
7
+ }
8
+ /**
9
+ * Extract candidate keys from the constraint contract for a given model.
10
+ * A candidate key is either the primary key or a unique constraint.
11
+ */
12
+ export declare function extractCandidateKeys(contract: ConstraintContract, modelName: string): readonly CandidateKey[];
13
+ //# sourceMappingURL=computeKeys.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"computeKeys.d.ts","sourceRoot":"","sources":["../../../src/core/analysis/computeKeys.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAEnE,mCAAmC;AACnC,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,IAAI,GAAG,QAAQ,CAAC;CAClC;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,kBAAkB,EAC5B,SAAS,EAAE,MAAM,GAChB,SAAS,YAAY,EAAE,CAyBzB"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Extract candidate keys from the constraint contract for a given model.
3
+ * A candidate key is either the primary key or a unique constraint.
4
+ */
5
+ export function extractCandidateKeys(contract, modelName) {
6
+ const model = contract.models.find((m) => m.name === modelName);
7
+ if (model === undefined) {
8
+ return [];
9
+ }
10
+ const keys = [];
11
+ if (model.primaryKey !== null) {
12
+ keys.push({
13
+ fields: model.primaryKey.fields,
14
+ model: modelName,
15
+ source: 'pk',
16
+ });
17
+ }
18
+ for (const uq of model.uniqueConstraints) {
19
+ keys.push({
20
+ fields: uq.fields,
21
+ model: modelName,
22
+ source: 'unique',
23
+ });
24
+ }
25
+ return keys;
26
+ }
27
+ //# sourceMappingURL=computeKeys.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"computeKeys.js","sourceRoot":"","sources":["../../../src/core/analysis/computeKeys.ts"],"names":[],"mappings":"AASA;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAClC,QAA4B,EAC5B,SAAiB;IAEjB,MAAM,KAAK,GAAG,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC;IAChE,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;QACxB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,IAAI,GAAmB,EAAE,CAAC;IAEhC,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC;QAC9B,IAAI,CAAC,IAAI,CAAC;YACR,MAAM,EAAE,KAAK,CAAC,UAAU,CAAC,MAAM;YAC/B,KAAK,EAAE,SAAS;YAChB,MAAM,EAAE,IAAI;SACb,CAAC,CAAC;IACL,CAAC;IAED,KAAK,MAAM,EAAE,IAAI,KAAK,CAAC,iBAAiB,EAAE,CAAC;QACzC,IAAI,CAAC,IAAI,CAAC;YACR,MAAM,EAAE,EAAE,CAAC,MAAM;YACjB,KAAK,EAAE,SAAS;YAChB,MAAM,EAAE,QAAQ;SACjB,CAAC,CAAC;IACL,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC"}
@@ -0,0 +1,31 @@
1
+ import type { ConstraintContract } from '../report/reportTypes.js';
2
+ /** A functional dependency: determinant → dependent fields. */
3
+ export interface FunctionalDependency {
4
+ readonly determinant: readonly string[];
5
+ readonly dependent: readonly string[];
6
+ readonly model: string;
7
+ readonly source: 'pk' | 'unique' | 'fk' | 'invariant';
8
+ }
9
+ /**
10
+ * Infer functional dependencies from the constraint contract.
11
+ *
12
+ * Rules:
13
+ * - PK → all non-PK fields (full functional dependency)
14
+ * - Unique → all non-key fields (candidate key dependency)
15
+ * - FK fields → referenced PK fields (referential dependency)
16
+ */
17
+ export declare function inferFunctionalDependencies(contract: ConstraintContract): readonly FunctionalDependency[];
18
+ /**
19
+ * Compute the attribute closure of a set of attributes under given FDs.
20
+ * Uses Armstrong's axioms (reflexivity, augmentation, transitivity).
21
+ *
22
+ * Given attributes X and a set of FDs, returns X+ (all attributes
23
+ * functionally determined by X).
24
+ */
25
+ export declare function attributeClosure(attributes: readonly string[], fds: readonly FunctionalDependency[], modelName: string): ReadonlySet<string>;
26
+ /**
27
+ * Check if a set of attributes is a superkey for a model.
28
+ * A superkey determines all attributes in the model.
29
+ */
30
+ export declare function isSuperkey(attributes: readonly string[], allFields: readonly string[], fds: readonly FunctionalDependency[], modelName: string): boolean;
31
+ //# sourceMappingURL=inferFds.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inferFds.d.ts","sourceRoot":"","sources":["../../../src/core/analysis/inferFds.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAiB,MAAM,0BAA0B,CAAC;AAElF,+DAA+D;AAC/D,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,IAAI,GAAG,QAAQ,GAAG,IAAI,GAAG,WAAW,CAAC;CACvD;AAED;;;;;;;GAOG;AACH,wBAAgB,2BAA2B,CACzC,QAAQ,EAAE,kBAAkB,GAC3B,SAAS,oBAAoB,EAAE,CAmCjC;AAoBD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,UAAU,EAAE,SAAS,MAAM,EAAE,EAC7B,GAAG,EAAE,SAAS,oBAAoB,EAAE,EACpC,SAAS,EAAE,MAAM,GAChB,WAAW,CAAC,MAAM,CAAC,CAoBrB;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,UAAU,EAAE,SAAS,MAAM,EAAE,EAC7B,SAAS,EAAE,SAAS,MAAM,EAAE,EAC5B,GAAG,EAAE,SAAS,oBAAoB,EAAE,EACpC,SAAS,EAAE,MAAM,GAChB,OAAO,CAGT"}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Infer functional dependencies from the constraint contract.
3
+ *
4
+ * Rules:
5
+ * - PK → all non-PK fields (full functional dependency)
6
+ * - Unique → all non-key fields (candidate key dependency)
7
+ * - FK fields → referenced PK fields (referential dependency)
8
+ */
9
+ export function inferFunctionalDependencies(contract) {
10
+ const fds = [];
11
+ for (const model of contract.models) {
12
+ const allFieldNames = model.fields.map((f) => f.name);
13
+ // PK → all other fields
14
+ if (model.primaryKey !== null) {
15
+ const pkFields = model.primaryKey.fields;
16
+ const dependent = allFieldNames.filter((f) => !pkFields.includes(f));
17
+ if (dependent.length > 0) {
18
+ fds.push({
19
+ determinant: pkFields,
20
+ dependent,
21
+ model: model.name,
22
+ source: 'pk',
23
+ });
24
+ }
25
+ }
26
+ // Unique → all other fields
27
+ addUniqueFds(model, allFieldNames, fds);
28
+ // FK → referenced fields (namespaced to prevent collision with local fields)
29
+ for (const fk of model.foreignKeys) {
30
+ fds.push({
31
+ determinant: fk.fields,
32
+ dependent: fk.referencedFields.map((f) => `${fk.referencedModel}.${f}`),
33
+ model: model.name,
34
+ source: 'fk',
35
+ });
36
+ }
37
+ }
38
+ return fds;
39
+ }
40
+ function addUniqueFds(model, allFieldNames, fds) {
41
+ for (const uq of model.uniqueConstraints) {
42
+ const dependent = allFieldNames.filter((f) => !uq.fields.includes(f));
43
+ if (dependent.length > 0) {
44
+ fds.push({
45
+ determinant: uq.fields,
46
+ dependent,
47
+ model: model.name,
48
+ source: 'unique',
49
+ });
50
+ }
51
+ }
52
+ }
53
+ /**
54
+ * Compute the attribute closure of a set of attributes under given FDs.
55
+ * Uses Armstrong's axioms (reflexivity, augmentation, transitivity).
56
+ *
57
+ * Given attributes X and a set of FDs, returns X+ (all attributes
58
+ * functionally determined by X).
59
+ */
60
+ export function attributeClosure(attributes, fds, modelName) {
61
+ const closure = new Set(attributes);
62
+ const modelFds = fds.filter((fd) => fd.model === modelName);
63
+ let changed = true;
64
+ while (changed) {
65
+ changed = false;
66
+ for (const fd of modelFds) {
67
+ if (fd.determinant.every((attr) => closure.has(attr))) {
68
+ for (const dep of fd.dependent) {
69
+ if (!closure.has(dep)) {
70
+ closure.add(dep);
71
+ changed = true;
72
+ }
73
+ }
74
+ }
75
+ }
76
+ }
77
+ return closure;
78
+ }
79
+ /**
80
+ * Check if a set of attributes is a superkey for a model.
81
+ * A superkey determines all attributes in the model.
82
+ */
83
+ export function isSuperkey(attributes, allFields, fds, modelName) {
84
+ const closure = attributeClosure(attributes, fds, modelName);
85
+ return allFields.every((f) => closure.has(f));
86
+ }
87
+ //# sourceMappingURL=inferFds.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inferFds.js","sourceRoot":"","sources":["../../../src/core/analysis/inferFds.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ import type { ConstraintContract, Finding } from '../../report/reportTypes.js';
2
+ /**
3
+ * Check for 1NF violations (heuristic-based).
4
+ *
5
+ * Checks:
6
+ * - NF1_LIST_IN_STRING_SUSPECTED: String fields with names suggesting list values
7
+ * - NF1_REPEATING_GROUP_SUSPECTED: Fields with numeric suffixes (phone1, phone2)
8
+ * - NF1_JSON_RELATION_SUSPECTED: Json fields suggesting embedded relations
9
+ */
10
+ export declare function check1nf(contract: ConstraintContract): readonly Finding[];
11
+ //# sourceMappingURL=check1nf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check1nf.d.ts","sourceRoot":"","sources":["../../../../src/core/analysis/normalizeChecks/check1nf.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,OAAO,EAAgC,MAAM,6BAA6B,CAAC;AAc7G;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,QAAQ,EAAE,kBAAkB,GAAG,SAAS,OAAO,EAAE,CAUzE"}
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Patterns that suggest a string field contains a delimited list of values.
3
+ * Matches field names ending in Ids, List, Csv, Array, Tags, Items, Values (case-insensitive).
4
+ */
5
+ const LIST_IN_STRING_PATTERN = /(?:ids|list|csv|array|tags|items|values)$/i;
6
+ /**
7
+ * Pattern to detect repeating groups: field names ending with a numeric suffix.
8
+ * e.g., phone1, phone2, address1, address2
9
+ */
10
+ const REPEATING_GROUP_PATTERN = /^(.+?)(\d+)$/;
11
+ /**
12
+ * Check for 1NF violations (heuristic-based).
13
+ *
14
+ * Checks:
15
+ * - NF1_LIST_IN_STRING_SUSPECTED: String fields with names suggesting list values
16
+ * - NF1_REPEATING_GROUP_SUSPECTED: Fields with numeric suffixes (phone1, phone2)
17
+ * - NF1_JSON_RELATION_SUSPECTED: Json fields suggesting embedded relations
18
+ */
19
+ export function check1nf(contract) {
20
+ const findings = [];
21
+ for (const model of contract.models) {
22
+ checkListInString(model, findings);
23
+ checkRepeatingGroups(model, findings);
24
+ checkJsonRelation(model, findings);
25
+ }
26
+ return findings;
27
+ }
28
+ function checkListInString(model, findings) {
29
+ for (const field of model.fields) {
30
+ if (field.type === 'String' && LIST_IN_STRING_PATTERN.test(field.name)) {
31
+ findings.push({
32
+ rule: 'NF1_LIST_IN_STRING_SUSPECTED',
33
+ severity: 'warning',
34
+ normalForm: '1NF',
35
+ model: model.name,
36
+ field: field.name,
37
+ message: `String field "${field.name}" may contain a delimited list of values. Consider normalizing into a separate table.`,
38
+ });
39
+ }
40
+ }
41
+ }
42
+ function checkRepeatingGroups(model, findings) {
43
+ // Group fields by their base name (strip trailing digits)
44
+ const groups = new Map();
45
+ for (const field of model.fields) {
46
+ const match = REPEATING_GROUP_PATTERN.exec(field.name);
47
+ if (match?.[1] !== undefined) {
48
+ const baseName = match[1];
49
+ const existing = groups.get(baseName);
50
+ if (existing !== undefined) {
51
+ existing.push(field);
52
+ }
53
+ else {
54
+ groups.set(baseName, [field]);
55
+ }
56
+ }
57
+ }
58
+ // Only flag groups with 2+ numbered fields of the same type
59
+ for (const [baseName, fields] of groups) {
60
+ if (fields.length >= 2) {
61
+ const firstType = fields[0]?.type;
62
+ const allSameType = firstType !== undefined && fields.every((f) => f.type === firstType);
63
+ if (allSameType) {
64
+ const fieldNames = fields.map((f) => f.name).join(', ');
65
+ findings.push({
66
+ rule: 'NF1_REPEATING_GROUP_SUSPECTED',
67
+ severity: 'warning',
68
+ normalForm: '1NF',
69
+ model: model.name,
70
+ field: null,
71
+ message: `Fields [${fieldNames}] appear to be a repeating group for "${baseName}". Consider normalizing into a separate table.`,
72
+ });
73
+ }
74
+ }
75
+ }
76
+ }
77
+ function checkJsonRelation(model, findings) {
78
+ for (const field of model.fields) {
79
+ if (field.type === 'Json') {
80
+ findings.push({
81
+ rule: 'NF1_JSON_RELATION_SUSPECTED',
82
+ severity: 'info',
83
+ normalForm: '1NF',
84
+ model: model.name,
85
+ field: field.name,
86
+ message: `Json field "${field.name}" may contain structured data that could be normalized into related tables.`,
87
+ });
88
+ }
89
+ }
90
+ }
91
+ //# sourceMappingURL=check1nf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check1nf.js","sourceRoot":"","sources":["../../../../src/core/analysis/normalizeChecks/check1nf.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ import type { ConstraintContract, Finding } from '../../report/reportTypes.js';
2
+ import type { FunctionalDependency } from '../inferFds.js';
3
+ /**
4
+ * Check for 2NF violations (heuristic-based).
5
+ *
6
+ * Checks:
7
+ * - NF2_PARTIAL_DEPENDENCY_SUSPECTED: Composite key models with fields that
8
+ * appear to depend on only part of the key (detected via FK relationships)
9
+ * - NF2_JOIN_TABLE_DUPLICATED_ATTR_SUSPECTED: Join tables (composite PK with
10
+ * only FK fields) that carry extra non-key attributes
11
+ */
12
+ export declare function check2nf(contract: ConstraintContract, fds: readonly FunctionalDependency[]): readonly Finding[];
13
+ //# sourceMappingURL=check2nf.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check2nf.d.ts","sourceRoot":"","sources":["../../../../src/core/analysis/normalizeChecks/check2nf.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,OAAO,EAAiB,MAAM,6BAA6B,CAAC;AAC9F,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAG3D;;;;;;;;GAQG;AACH,wBAAgB,QAAQ,CACtB,QAAQ,EAAE,kBAAkB,EAC5B,GAAG,EAAE,SAAS,oBAAoB,EAAE,GACnC,SAAS,OAAO,EAAE,CAcpB"}
@@ -0,0 +1,80 @@
1
+ import { extractCandidateKeys } from '../computeKeys.js';
2
+ /**
3
+ * Check for 2NF violations (heuristic-based).
4
+ *
5
+ * Checks:
6
+ * - NF2_PARTIAL_DEPENDENCY_SUSPECTED: Composite key models with fields that
7
+ * appear to depend on only part of the key (detected via FK relationships)
8
+ * - NF2_JOIN_TABLE_DUPLICATED_ATTR_SUSPECTED: Join tables (composite PK with
9
+ * only FK fields) that carry extra non-key attributes
10
+ */
11
+ export function check2nf(contract, fds) {
12
+ const findings = [];
13
+ for (const model of contract.models) {
14
+ const keys = extractCandidateKeys(contract, model.name);
15
+ const compositePk = keys.find((k) => k.source === 'pk' && k.fields.length > 1);
16
+ if (compositePk !== undefined) {
17
+ checkPartialDependency(model, compositePk.fields, fds, findings);
18
+ checkJoinTableDuplicatedAttr(model, compositePk.fields, findings);
19
+ }
20
+ }
21
+ return findings;
22
+ }
23
+ /**
24
+ * Detect possible partial dependencies in composite-key models.
25
+ *
26
+ * When FK fields form a proper subset of the composite PK, non-key attributes
27
+ * may depend on only that subset rather than the full key — a 2NF violation.
28
+ * One finding is emitted per FK subset (not per field) since without declared
29
+ * invariants we cannot attribute specific fields to the subset.
30
+ */
31
+ function checkPartialDependency(model, pkFields, fds, findings) {
32
+ const hasNonKeyFields = model.fields.some((f) => !pkFields.includes(f.name));
33
+ if (!hasNonKeyFields) {
34
+ return;
35
+ }
36
+ const fkFds = fds.filter((fd) => fd.model === model.name && fd.source === 'fk');
37
+ for (const fkFd of fkFds) {
38
+ const isProperSubset = fkFd.determinant.length < pkFields.length &&
39
+ fkFd.determinant.every((f) => pkFields.includes(f));
40
+ if (isProperSubset) {
41
+ findings.push({
42
+ rule: 'NF2_PARTIAL_DEPENDENCY_SUSPECTED',
43
+ severity: 'warning',
44
+ normalForm: '2NF',
45
+ model: model.name,
46
+ field: null,
47
+ message: `Composite-key model "${model.name}" has FK fields (${fkFd.determinant.join(', ')}) that are a proper subset of the primary key. Non-key attributes may depend on this subset rather than the full key, which would violate 2NF.`,
48
+ });
49
+ }
50
+ }
51
+ }
52
+ /**
53
+ * Detect join tables with extra attributes. A join table has a composite PK
54
+ * where all PK fields are also FK fields. Extra non-key attributes suggest
55
+ * the table should be an entity with its own identity.
56
+ */
57
+ function checkJoinTableDuplicatedAttr(model, pkFields, findings) {
58
+ // Check if all PK fields are FK fields
59
+ const fkFieldSets = model.foreignKeys.map((fk) => fk.fields).flat();
60
+ const allPkFieldsAreFk = pkFields.every((f) => fkFieldSets.includes(f));
61
+ if (!allPkFieldsAreFk) {
62
+ return;
63
+ }
64
+ // Find non-key, non-FK fields
65
+ const allFkFields = new Set(fkFieldSets);
66
+ const extraFields = model.fields
67
+ .map((f) => f.name)
68
+ .filter((name) => !pkFields.includes(name) && !allFkFields.has(name));
69
+ if (extraFields.length > 0) {
70
+ findings.push({
71
+ rule: 'NF2_JOIN_TABLE_DUPLICATED_ATTR_SUSPECTED',
72
+ severity: 'warning',
73
+ normalForm: '2NF',
74
+ model: model.name,
75
+ field: null,
76
+ message: `Join table "${model.name}" has extra attributes [${extraFields.join(', ')}] beyond its composite key. Consider whether this should be a first-class entity.`,
77
+ });
78
+ }
79
+ }
80
+ //# sourceMappingURL=check2nf.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check2nf.js","sourceRoot":"","sources":["../../../../src/core/analysis/normalizeChecks/check2nf.ts"],"names":[],"mappings":"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"}