@toiroakr/lines-db 0.1.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/LICENSE +21 -0
- package/bin/cli.js +1373 -0
- package/dist/index.cjs +1212 -0
- package/dist/index.d.cts +486 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +486 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1181 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -0
- package/src/cli.ts +333 -0
- package/src/database.test.ts +493 -0
- package/src/database.ts +1025 -0
- package/src/directory-scanner.test.ts +91 -0
- package/src/directory-scanner.ts +38 -0
- package/src/error-formatter.ts +166 -0
- package/src/index.ts +35 -0
- package/src/jsonl-migration.ts +76 -0
- package/src/jsonl-reader.test.ts +168 -0
- package/src/jsonl-reader.ts +135 -0
- package/src/jsonl-writer.test.ts +101 -0
- package/src/jsonl-writer.ts +33 -0
- package/src/runtime.ts +34 -0
- package/src/schema-loader.test.ts +136 -0
- package/src/schema-loader.ts +64 -0
- package/src/schema.ts +135 -0
- package/src/sqlite-adapter.ts +99 -0
- package/src/type-generator.ts +201 -0
- package/src/types.ts +99 -0
- package/src/validator.test.ts +337 -0
- package/src/validator.ts +207 -0
- package/tsconfig.json +20 -0
- package/tsconfig.test.json +8 -0
- package/tsdown.config.ts +26 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { Validator } from './validator.js';
|
|
3
|
+
import { writeFile, mkdir, rm } from 'node:fs/promises';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
|
|
7
|
+
describe('Validator', () => {
|
|
8
|
+
let testDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
testDir = join(tmpdir(), `validator-test-${Date.now()}`);
|
|
12
|
+
await mkdir(testDir, { recursive: true });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
await rm(testDir, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('validate file', () => {
|
|
20
|
+
it('should validate file with valid data', async () => {
|
|
21
|
+
const jsonlPath = join(testDir, 'users.jsonl');
|
|
22
|
+
const schemaPath = join(testDir, 'users.schema.ts');
|
|
23
|
+
|
|
24
|
+
await writeFile(jsonlPath, '{"id":1,"name":"Alice"}\n{"id":2,"name":"Bob"}\n');
|
|
25
|
+
await writeFile(
|
|
26
|
+
schemaPath,
|
|
27
|
+
`
|
|
28
|
+
export const schema = {
|
|
29
|
+
'~standard': {
|
|
30
|
+
version: 1,
|
|
31
|
+
vendor: 'test',
|
|
32
|
+
validate: (data) => {
|
|
33
|
+
const issues = [];
|
|
34
|
+
if (!data.id || !data.name) {
|
|
35
|
+
issues.push({ message: 'Missing required fields' });
|
|
36
|
+
}
|
|
37
|
+
return { value: data, issues };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
`,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const validator = new Validator({ path: jsonlPath });
|
|
45
|
+
const result = await validator.validate();
|
|
46
|
+
|
|
47
|
+
expect(result.valid).toBe(true);
|
|
48
|
+
expect(result.errors).toHaveLength(0);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should detect validation errors', async () => {
|
|
52
|
+
const jsonlPath = join(testDir, 'users.jsonl');
|
|
53
|
+
const schemaPath = join(testDir, 'users.schema.ts');
|
|
54
|
+
|
|
55
|
+
await writeFile(jsonlPath, '{"id":1,"name":"Alice"}\n{"id":2}\n');
|
|
56
|
+
await writeFile(
|
|
57
|
+
schemaPath,
|
|
58
|
+
`
|
|
59
|
+
export const schema = {
|
|
60
|
+
'~standard': {
|
|
61
|
+
version: 1,
|
|
62
|
+
vendor: 'test',
|
|
63
|
+
validate: (data) => {
|
|
64
|
+
const issues = [];
|
|
65
|
+
if (!data.name) {
|
|
66
|
+
issues.push({ message: 'Missing name field', path: ['name'] });
|
|
67
|
+
}
|
|
68
|
+
return { value: data, issues };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
`,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const validator = new Validator({ path: jsonlPath });
|
|
76
|
+
const result = await validator.validate();
|
|
77
|
+
|
|
78
|
+
expect(result.valid).toBe(false);
|
|
79
|
+
expect(result.errors).toHaveLength(1);
|
|
80
|
+
expect(result.errors[0].rowIndex).toBe(2);
|
|
81
|
+
expect(result.errors[0].tableName).toBe('users');
|
|
82
|
+
expect(result.errors[0].issues).toHaveLength(1);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should throw when no schema exists', async () => {
|
|
86
|
+
const jsonlPath = join(testDir, 'users.jsonl');
|
|
87
|
+
await writeFile(jsonlPath, '{"id":1}\n{"id":2}\n');
|
|
88
|
+
|
|
89
|
+
const validator = new Validator({ path: jsonlPath });
|
|
90
|
+
await expect(validator.validate()).rejects.toThrow(/Schema file not found/);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should handle multiple validation errors in single file', async () => {
|
|
94
|
+
const jsonlPath = join(testDir, 'users.jsonl');
|
|
95
|
+
const schemaPath = join(testDir, 'users.schema.ts');
|
|
96
|
+
|
|
97
|
+
await writeFile(jsonlPath, '{"id":1}\n{"id":2}\n{"id":3,"name":"Charlie"}\n');
|
|
98
|
+
await writeFile(
|
|
99
|
+
schemaPath,
|
|
100
|
+
`
|
|
101
|
+
export const schema = {
|
|
102
|
+
'~standard': {
|
|
103
|
+
version: 1,
|
|
104
|
+
vendor: 'test',
|
|
105
|
+
validate: (data) => {
|
|
106
|
+
const issues = [];
|
|
107
|
+
if (!data.name) {
|
|
108
|
+
issues.push({ message: 'Missing name' });
|
|
109
|
+
}
|
|
110
|
+
return { value: data, issues };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
`,
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
const validator = new Validator({ path: jsonlPath });
|
|
118
|
+
const result = await validator.validate();
|
|
119
|
+
|
|
120
|
+
expect(result.valid).toBe(false);
|
|
121
|
+
expect(result.errors).toHaveLength(2);
|
|
122
|
+
expect(result.errors[0].rowIndex).toBe(1);
|
|
123
|
+
expect(result.errors[1].rowIndex).toBe(2);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should return 0-based rowIndex (first line = index 0)', async () => {
|
|
127
|
+
const jsonlPath = join(testDir, 'users.jsonl');
|
|
128
|
+
const schemaPath = join(testDir, 'users.schema.ts');
|
|
129
|
+
|
|
130
|
+
// Line 1: valid, Line 2: invalid (no name), Line 3: invalid (no name)
|
|
131
|
+
await writeFile(
|
|
132
|
+
jsonlPath,
|
|
133
|
+
'{"id":1,"name":"Alice"}\n{"id":2}\n{"id":3}\n',
|
|
134
|
+
);
|
|
135
|
+
await writeFile(
|
|
136
|
+
schemaPath,
|
|
137
|
+
`
|
|
138
|
+
export const schema = {
|
|
139
|
+
'~standard': {
|
|
140
|
+
version: 1,
|
|
141
|
+
vendor: 'test',
|
|
142
|
+
validate: (data) => {
|
|
143
|
+
const issues = [];
|
|
144
|
+
if (!data.name) {
|
|
145
|
+
issues.push({ message: 'Missing name field', path: ['name'] });
|
|
146
|
+
}
|
|
147
|
+
return { value: data, issues };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
`,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const validator = new Validator({ path: jsonlPath });
|
|
155
|
+
const result = await validator.validate();
|
|
156
|
+
|
|
157
|
+
expect(result.valid).toBe(false);
|
|
158
|
+
expect(result.errors).toHaveLength(2);
|
|
159
|
+
// First error is on line 2 of the file, which is index 1 (0-based)
|
|
160
|
+
expect(result.errors[0].rowIndex).toBe(1);
|
|
161
|
+
expect(result.errors[0].file).toBe(jsonlPath);
|
|
162
|
+
// Second error is on line 3 of the file, which is index 2 (0-based)
|
|
163
|
+
expect(result.errors[1].rowIndex).toBe(2);
|
|
164
|
+
expect(result.errors[1].file).toBe(jsonlPath);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('should return 0-based rowIndex for first line error', async () => {
|
|
168
|
+
const jsonlPath = join(testDir, 'users.jsonl');
|
|
169
|
+
const schemaPath = join(testDir, 'users.schema.ts');
|
|
170
|
+
|
|
171
|
+
// First line has error
|
|
172
|
+
await writeFile(jsonlPath, '{"id":1}\n{"id":2,"name":"Bob"}\n');
|
|
173
|
+
await writeFile(
|
|
174
|
+
schemaPath,
|
|
175
|
+
`
|
|
176
|
+
export const schema = {
|
|
177
|
+
'~standard': {
|
|
178
|
+
version: 1,
|
|
179
|
+
vendor: 'test',
|
|
180
|
+
validate: (data) => {
|
|
181
|
+
const issues = [];
|
|
182
|
+
if (!data.name) {
|
|
183
|
+
issues.push({ message: 'Missing name field', path: ['name'] });
|
|
184
|
+
}
|
|
185
|
+
return { value: data, issues };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
`,
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const validator = new Validator({ path: jsonlPath });
|
|
193
|
+
const result = await validator.validate();
|
|
194
|
+
|
|
195
|
+
expect(result.valid).toBe(false);
|
|
196
|
+
expect(result.errors).toHaveLength(1);
|
|
197
|
+
// First line (line 1 in file) should be index 0
|
|
198
|
+
expect(result.errors[0].rowIndex).toBe(0);
|
|
199
|
+
expect(result.errors[0].file).toBe(jsonlPath);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('validate directory', () => {
|
|
204
|
+
it('should validate all JSONL files in directory', async () => {
|
|
205
|
+
const usersPath = join(testDir, 'users.jsonl');
|
|
206
|
+
const productsPath = join(testDir, 'products.jsonl');
|
|
207
|
+
const usersSchemaPath = join(testDir, 'users.schema.ts');
|
|
208
|
+
const productsSchemaPath = join(testDir, 'products.schema.ts');
|
|
209
|
+
|
|
210
|
+
await writeFile(usersPath, '{"id":1,"name":"Alice"}\n');
|
|
211
|
+
await writeFile(productsPath, '{"id":1,"name":"Product"}\n');
|
|
212
|
+
await writeFile(
|
|
213
|
+
usersSchemaPath,
|
|
214
|
+
`
|
|
215
|
+
export const schema = {
|
|
216
|
+
'~standard': {
|
|
217
|
+
version: 1,
|
|
218
|
+
vendor: 'test',
|
|
219
|
+
validate: (data) => ({ value: data, issues: [] })
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
`,
|
|
223
|
+
);
|
|
224
|
+
await writeFile(
|
|
225
|
+
productsSchemaPath,
|
|
226
|
+
`
|
|
227
|
+
export const schema = {
|
|
228
|
+
'~standard': {
|
|
229
|
+
version: 1,
|
|
230
|
+
vendor: 'test',
|
|
231
|
+
validate: (data) => ({ value: data, issues: [] })
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
`,
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const validator = new Validator({ path: testDir });
|
|
238
|
+
const result = await validator.validate();
|
|
239
|
+
|
|
240
|
+
expect(result.valid).toBe(true);
|
|
241
|
+
expect(result.errors).toHaveLength(0);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should collect errors from multiple files', async () => {
|
|
245
|
+
const usersPath = join(testDir, 'users.jsonl');
|
|
246
|
+
const usersSchemaPath = join(testDir, 'users.schema.ts');
|
|
247
|
+
const productsPath = join(testDir, 'products.jsonl');
|
|
248
|
+
const productsSchemaPath = join(testDir, 'products.schema.ts');
|
|
249
|
+
|
|
250
|
+
await writeFile(usersPath, '{"id":1}\n');
|
|
251
|
+
await writeFile(
|
|
252
|
+
usersSchemaPath,
|
|
253
|
+
`
|
|
254
|
+
export const schema = {
|
|
255
|
+
'~standard': {
|
|
256
|
+
version: 1,
|
|
257
|
+
vendor: 'test',
|
|
258
|
+
validate: (data) => ({
|
|
259
|
+
value: data,
|
|
260
|
+
issues: data.name ? [] : [{ message: 'Missing name' }]
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
`,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
await writeFile(productsPath, '{"id":1}\n');
|
|
268
|
+
await writeFile(
|
|
269
|
+
productsSchemaPath,
|
|
270
|
+
`
|
|
271
|
+
export const schema = {
|
|
272
|
+
'~standard': {
|
|
273
|
+
version: 1,
|
|
274
|
+
vendor: 'test',
|
|
275
|
+
validate: (data) => ({
|
|
276
|
+
value: data,
|
|
277
|
+
issues: data.price ? [] : [{ message: 'Missing price' }]
|
|
278
|
+
})
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
`,
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
const validator = new Validator({ path: testDir });
|
|
285
|
+
const result = await validator.validate();
|
|
286
|
+
|
|
287
|
+
expect(result.valid).toBe(false);
|
|
288
|
+
expect(result.errors).toHaveLength(2);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should throw error for directory with no JSONL files', async () => {
|
|
292
|
+
await writeFile(join(testDir, 'readme.txt'), 'no jsonl files here');
|
|
293
|
+
|
|
294
|
+
const validator = new Validator({ path: testDir });
|
|
295
|
+
|
|
296
|
+
await expect(validator.validate()).rejects.toThrow('No JSONL files found');
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
describe('error handling', () => {
|
|
301
|
+
it('should throw error for non-JSONL file', async () => {
|
|
302
|
+
const txtPath = join(testDir, 'test.txt');
|
|
303
|
+
await writeFile(txtPath, 'not a jsonl file');
|
|
304
|
+
|
|
305
|
+
const validator = new Validator({ path: txtPath });
|
|
306
|
+
|
|
307
|
+
await expect(validator.validate()).rejects.toThrow('Invalid path');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('should reject async validation', async () => {
|
|
311
|
+
const jsonlPath = join(testDir, 'users.jsonl');
|
|
312
|
+
const schemaPath = join(testDir, 'users.schema.ts');
|
|
313
|
+
|
|
314
|
+
await writeFile(jsonlPath, '{"id":1}\n');
|
|
315
|
+
await writeFile(
|
|
316
|
+
schemaPath,
|
|
317
|
+
`
|
|
318
|
+
export const schema = {
|
|
319
|
+
'~standard': {
|
|
320
|
+
version: 1,
|
|
321
|
+
vendor: 'test',
|
|
322
|
+
validate: async (data) => {
|
|
323
|
+
return { value: data, issues: [] };
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
`,
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
const validator = new Validator({ path: jsonlPath });
|
|
331
|
+
|
|
332
|
+
await expect(validator.validate()).rejects.toThrow(
|
|
333
|
+
'Asynchronous validation is not supported',
|
|
334
|
+
);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
});
|
package/src/validator.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { join, basename } from 'node:path';
|
|
3
|
+
import { JsonlReader } from './jsonl-reader.js';
|
|
4
|
+
import { SchemaLoader } from './schema-loader.js';
|
|
5
|
+
import type { StandardSchemaIssue, JsonObject } from './types.js';
|
|
6
|
+
import type { BiDirectionalSchema } from './schema.js';
|
|
7
|
+
|
|
8
|
+
export interface ValidationResult {
|
|
9
|
+
valid: boolean;
|
|
10
|
+
errors: ValidationErrorDetail[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface ValidationErrorDetail {
|
|
14
|
+
file: string;
|
|
15
|
+
tableName: string;
|
|
16
|
+
rowIndex: number;
|
|
17
|
+
issues: ReadonlyArray<StandardSchemaIssue>;
|
|
18
|
+
type?: 'schema' | 'foreignKey';
|
|
19
|
+
foreignKeyError?: {
|
|
20
|
+
column: string;
|
|
21
|
+
value: unknown;
|
|
22
|
+
referencedTable: string;
|
|
23
|
+
referencedColumn: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ValidatorOptions {
|
|
28
|
+
path: string; // File or directory path
|
|
29
|
+
projectRoot?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class Validator {
|
|
33
|
+
private path: string;
|
|
34
|
+
private projectRoot: string;
|
|
35
|
+
|
|
36
|
+
constructor(options: ValidatorOptions) {
|
|
37
|
+
this.path = options.path;
|
|
38
|
+
this.projectRoot = options.projectRoot || process.cwd();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validate JSONL file(s)
|
|
43
|
+
*/
|
|
44
|
+
async validate(): Promise<ValidationResult> {
|
|
45
|
+
// Use absolute path if provided, otherwise resolve relative to projectRoot
|
|
46
|
+
const fullPath = this.path.startsWith('/') ? this.path : join(this.projectRoot, this.path);
|
|
47
|
+
const stats = await stat(fullPath);
|
|
48
|
+
|
|
49
|
+
if (stats.isDirectory()) {
|
|
50
|
+
return this.validateDirectory(fullPath);
|
|
51
|
+
} else if (stats.isFile() && fullPath.endsWith('.jsonl')) {
|
|
52
|
+
return this.validateFile(fullPath);
|
|
53
|
+
} else {
|
|
54
|
+
throw new Error(`Invalid path: ${this.path}. Must be a directory or .jsonl file.`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Validate all JSONL files in a directory
|
|
60
|
+
*/
|
|
61
|
+
private async validateDirectory(dirPath: string): Promise<ValidationResult> {
|
|
62
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
63
|
+
const jsonlFiles = entries
|
|
64
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.jsonl'))
|
|
65
|
+
.map((entry) => join(dirPath, entry.name));
|
|
66
|
+
|
|
67
|
+
if (jsonlFiles.length === 0) {
|
|
68
|
+
throw new Error(`No JSONL files found in directory: ${dirPath}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const allErrors: ValidationErrorDetail[] = [];
|
|
72
|
+
|
|
73
|
+
// First, validate schema for each file
|
|
74
|
+
for (const file of jsonlFiles) {
|
|
75
|
+
const result = await this.validateFile(file);
|
|
76
|
+
allErrors.push(...result.errors);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Then, validate foreign keys across all tables
|
|
80
|
+
const fkErrors = await this.validateForeignKeys(dirPath, jsonlFiles);
|
|
81
|
+
allErrors.push(...fkErrors);
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
valid: allErrors.length === 0,
|
|
85
|
+
errors: allErrors,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Validate foreign key constraints across all tables
|
|
91
|
+
*/
|
|
92
|
+
private async validateForeignKeys(
|
|
93
|
+
dirPath: string,
|
|
94
|
+
jsonlFiles: string[],
|
|
95
|
+
): Promise<ValidationErrorDetail[]> {
|
|
96
|
+
const errors: ValidationErrorDetail[] = [];
|
|
97
|
+
|
|
98
|
+
// Load all table data
|
|
99
|
+
const tableData = new Map<string, JsonObject[]>();
|
|
100
|
+
const tableSchemas = new Map<string, BiDirectionalSchema>();
|
|
101
|
+
|
|
102
|
+
for (const file of jsonlFiles) {
|
|
103
|
+
const tableName = basename(file, '.jsonl');
|
|
104
|
+
const data = await JsonlReader.read(file);
|
|
105
|
+
const schema = await SchemaLoader.loadSchema(file);
|
|
106
|
+
|
|
107
|
+
tableData.set(tableName, data);
|
|
108
|
+
tableSchemas.set(tableName, schema as BiDirectionalSchema);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Check foreign keys for each table
|
|
112
|
+
for (const file of jsonlFiles) {
|
|
113
|
+
const tableName = basename(file, '.jsonl');
|
|
114
|
+
const schema = tableSchemas.get(tableName);
|
|
115
|
+
const data = tableData.get(tableName);
|
|
116
|
+
|
|
117
|
+
if (!schema || !data || !schema.foreignKeys) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check each foreign key constraint
|
|
122
|
+
for (const fk of schema.foreignKeys) {
|
|
123
|
+
const referencedTable = fk.references.table;
|
|
124
|
+
const referencedData = tableData.get(referencedTable);
|
|
125
|
+
|
|
126
|
+
if (!referencedData) {
|
|
127
|
+
// Referenced table not found - skip validation
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Build index of referenced values for fast lookup
|
|
132
|
+
const referencedValues = new Set<string>();
|
|
133
|
+
for (const refRow of referencedData) {
|
|
134
|
+
// Build composite key from referenced columns
|
|
135
|
+
const keyValues = fk.references.columns.map((col) => refRow[col]);
|
|
136
|
+
const compositeKey = JSON.stringify(keyValues);
|
|
137
|
+
referencedValues.add(compositeKey);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check each row in current table
|
|
141
|
+
for (let i = 0; i < data.length; i++) {
|
|
142
|
+
const row = data[i];
|
|
143
|
+
const foreignKeyValues = fk.columns.map((col) => row[col]);
|
|
144
|
+
const compositeKey = JSON.stringify(foreignKeyValues);
|
|
145
|
+
|
|
146
|
+
// Check if foreign key value exists in referenced table
|
|
147
|
+
if (!referencedValues.has(compositeKey)) {
|
|
148
|
+
errors.push({
|
|
149
|
+
file,
|
|
150
|
+
tableName,
|
|
151
|
+
rowIndex: i, // 0-indexed, will be converted to 1-indexed in formatter
|
|
152
|
+
issues: [],
|
|
153
|
+
type: 'foreignKey',
|
|
154
|
+
foreignKeyError: {
|
|
155
|
+
column: fk.columns.join(', '),
|
|
156
|
+
value: foreignKeyValues.length === 1 ? foreignKeyValues[0] : foreignKeyValues,
|
|
157
|
+
referencedTable: referencedTable,
|
|
158
|
+
referencedColumn: fk.references.columns.join(', '),
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return errors;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Validate a single JSONL file
|
|
171
|
+
*/
|
|
172
|
+
private async validateFile(filePath: string): Promise<ValidationResult> {
|
|
173
|
+
const tableName = basename(filePath, '.jsonl');
|
|
174
|
+
const data = await JsonlReader.read(filePath);
|
|
175
|
+
|
|
176
|
+
// Try to load schema
|
|
177
|
+
const schema = await SchemaLoader.loadSchema(filePath);
|
|
178
|
+
|
|
179
|
+
const errors: ValidationErrorDetail[] = [];
|
|
180
|
+
|
|
181
|
+
// Validate each row
|
|
182
|
+
for (let i = 0; i < data.length; i++) {
|
|
183
|
+
const row = data[i];
|
|
184
|
+
const result = schema['~standard'].validate(row);
|
|
185
|
+
|
|
186
|
+
// Only synchronous validation is supported
|
|
187
|
+
if (result instanceof Promise) {
|
|
188
|
+
throw new Error('Asynchronous validation is not supported.');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (result.issues && result.issues.length > 0) {
|
|
192
|
+
errors.push({
|
|
193
|
+
file: filePath,
|
|
194
|
+
tableName,
|
|
195
|
+
rowIndex: i, // 0-indexed, will be converted to 1-indexed in formatter
|
|
196
|
+
issues: result.issues,
|
|
197
|
+
type: 'schema',
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
valid: errors.length === 0,
|
|
204
|
+
errors,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./src",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"declaration": true,
|
|
15
|
+
"declarationMap": true,
|
|
16
|
+
"sourceMap": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "dist"]
|
|
20
|
+
}
|
package/tsdown.config.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineConfig } from 'tsdown';
|
|
2
|
+
|
|
3
|
+
export default defineConfig([
|
|
4
|
+
// CLI build (ESM only)
|
|
5
|
+
{
|
|
6
|
+
entry: ['src/cli.ts'],
|
|
7
|
+
format: ['esm'],
|
|
8
|
+
platform: 'node',
|
|
9
|
+
target: 'node18',
|
|
10
|
+
clean: true,
|
|
11
|
+
shims: true,
|
|
12
|
+
outDir: 'bin',
|
|
13
|
+
dts: false,
|
|
14
|
+
treeshake: true,
|
|
15
|
+
},
|
|
16
|
+
// Library build (ESM + CJS)
|
|
17
|
+
{
|
|
18
|
+
entry: ['src/index.ts'],
|
|
19
|
+
format: ['esm', 'cjs'],
|
|
20
|
+
platform: 'node',
|
|
21
|
+
target: 'node18',
|
|
22
|
+
outDir: 'dist',
|
|
23
|
+
dts: true,
|
|
24
|
+
treeshake: true,
|
|
25
|
+
},
|
|
26
|
+
]);
|