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.
- package/LICENSE +21 -0
- package/README.md +62 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +132 -0
- package/dist/cli.js.map +1 -0
- package/dist/core/analysis/computeKeys.d.ts +13 -0
- package/dist/core/analysis/computeKeys.d.ts.map +1 -0
- package/dist/core/analysis/computeKeys.js +27 -0
- package/dist/core/analysis/computeKeys.js.map +1 -0
- package/dist/core/analysis/inferFds.d.ts +31 -0
- package/dist/core/analysis/inferFds.d.ts.map +1 -0
- package/dist/core/analysis/inferFds.js +87 -0
- package/dist/core/analysis/inferFds.js.map +1 -0
- package/dist/core/analysis/normalizeChecks/check1nf.d.ts +11 -0
- package/dist/core/analysis/normalizeChecks/check1nf.d.ts.map +1 -0
- package/dist/core/analysis/normalizeChecks/check1nf.js +91 -0
- package/dist/core/analysis/normalizeChecks/check1nf.js.map +1 -0
- package/dist/core/analysis/normalizeChecks/check2nf.d.ts +13 -0
- package/dist/core/analysis/normalizeChecks/check2nf.d.ts.map +1 -0
- package/dist/core/analysis/normalizeChecks/check2nf.js +80 -0
- package/dist/core/analysis/normalizeChecks/check2nf.js.map +1 -0
- package/dist/core/analysis/normalizeChecks/check3nf.d.ts +18 -0
- package/dist/core/analysis/normalizeChecks/check3nf.d.ts.map +1 -0
- package/dist/core/analysis/normalizeChecks/check3nf.js +71 -0
- package/dist/core/analysis/normalizeChecks/check3nf.js.map +1 -0
- package/dist/core/invariants/parse.d.ts +19 -0
- package/dist/core/invariants/parse.d.ts.map +1 -0
- package/dist/core/invariants/parse.js +73 -0
- package/dist/core/invariants/parse.js.map +1 -0
- package/dist/core/invariants/schema.d.ts +24 -0
- package/dist/core/invariants/schema.d.ts.map +1 -0
- package/dist/core/invariants/schema.js +20 -0
- package/dist/core/invariants/schema.js.map +1 -0
- package/dist/core/prismaSchema/contract.d.ts +8 -0
- package/dist/core/prismaSchema/contract.d.ts.map +1 -0
- package/dist/core/prismaSchema/contract.js +107 -0
- package/dist/core/prismaSchema/contract.js.map +1 -0
- package/dist/core/prismaSchema/parse.d.ts +8 -0
- package/dist/core/prismaSchema/parse.d.ts.map +1 -0
- package/dist/core/prismaSchema/parse.js +46 -0
- package/dist/core/prismaSchema/parse.js.map +1 -0
- package/dist/core/prismaSchema/types.d.ts +39 -0
- package/dist/core/prismaSchema/types.d.ts.map +1 -0
- package/dist/core/prismaSchema/types.js +2 -0
- package/dist/core/prismaSchema/types.js.map +1 -0
- package/dist/core/report/reportTypes.d.ts +72 -0
- package/dist/core/report/reportTypes.d.ts.map +1 -0
- package/dist/core/report/reportTypes.js +2 -0
- package/dist/core/report/reportTypes.js.map +1 -0
- package/dist/core/report/toJson.d.ts +7 -0
- package/dist/core/report/toJson.d.ts.map +1 -0
- package/dist/core/report/toJson.js +27 -0
- package/dist/core/report/toJson.js.map +1 -0
- package/dist/core/report/toText.d.ts +6 -0
- package/dist/core/report/toText.d.ts.map +1 -0
- package/dist/core/report/toText.js +46 -0
- package/dist/core/report/toText.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/util/index.d.ts +5 -0
- package/dist/util/index.d.ts.map +1 -0
- package/dist/util/index.js +15 -0
- package/dist/util/index.js.map +1 -0
- 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 @@
|
|
|
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
|
package/dist/cli.js.map
ADDED
|
@@ -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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"}
|