@toiroakr/lines-db 0.4.1 → 0.6.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/CHANGELOG.md +24 -0
- package/bin/cli.js +472 -406
- package/dist/index.cjs +195 -327
- package/dist/index.d.cts +64 -84
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +64 -84
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +197 -328
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/src/cli.ts +436 -152
- package/src/database.ts +296 -52
- package/src/index.ts +2 -2
- package/src/jsonl-migration.ts +24 -56
- package/src/schema.ts +37 -32
- package/src/types.ts +21 -0
- package/src/validator.test.ts +0 -507
- package/src/validator.ts +0 -441
package/src/schema.ts
CHANGED
|
@@ -2,8 +2,9 @@ import type { StandardSchema, Table, ForeignKeyDefinition, IndexDefinition } fro
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Schema options for defining constraints and indexes
|
|
5
|
+
* When Input and Output types differ, backward transformation is required
|
|
5
6
|
*/
|
|
6
|
-
export
|
|
7
|
+
export type SchemaOptions<Input extends Table, Output extends Table> = {
|
|
7
8
|
/**
|
|
8
9
|
* Primary key column
|
|
9
10
|
*/
|
|
@@ -18,13 +19,19 @@ export interface SchemaOptions {
|
|
|
18
19
|
* Indexes to create
|
|
19
20
|
*/
|
|
20
21
|
indexes?: IndexDefinition[];
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}
|
|
22
|
+
} & (Output extends Input
|
|
23
|
+
? {
|
|
24
|
+
/**
|
|
25
|
+
* Backward transformation from Output to Input (optional when output is substitutable for input)
|
|
26
|
+
*/
|
|
27
|
+
backward?: (output: Output) => Input;
|
|
28
|
+
}
|
|
29
|
+
: {
|
|
30
|
+
/**
|
|
31
|
+
* Backward transformation from Output to Input (REQUIRED when types differ)
|
|
32
|
+
*/
|
|
33
|
+
backward: (output: Output) => Input;
|
|
34
|
+
});
|
|
28
35
|
|
|
29
36
|
/**
|
|
30
37
|
* BiDirectional Schema interface
|
|
@@ -58,8 +65,7 @@ export interface BiDirectionalSchema<Input extends Table = Table, Output extends
|
|
|
58
65
|
* Define a bidirectional schema with optional backward transformation
|
|
59
66
|
*
|
|
60
67
|
* @param schema - Standard Schema for validation
|
|
61
|
-
* @param
|
|
62
|
-
* Required when schema performs transformations
|
|
68
|
+
* @param options - SchemaOptions object. When Input and Output types differ, backward transformation is required
|
|
63
69
|
*
|
|
64
70
|
* @example
|
|
65
71
|
* // No transformation - backward not needed
|
|
@@ -68,10 +74,12 @@ export interface BiDirectionalSchema<Input extends Table = Table, Output extends
|
|
|
68
74
|
* );
|
|
69
75
|
*
|
|
70
76
|
* @example
|
|
71
|
-
* // With transformation - backward
|
|
77
|
+
* // With transformation - backward REQUIRED
|
|
72
78
|
* const schema = defineSchema(
|
|
73
79
|
* v.pipe(v.string(), v.transform(Number)),
|
|
74
|
-
*
|
|
80
|
+
* {
|
|
81
|
+
* backward: (num) => String(num) // backward: number → string (REQUIRED)
|
|
82
|
+
* }
|
|
75
83
|
* );
|
|
76
84
|
*
|
|
77
85
|
* @example
|
|
@@ -88,30 +96,27 @@ export interface BiDirectionalSchema<Input extends Table = Table, Output extends
|
|
|
88
96
|
*/
|
|
89
97
|
export function defineSchema<Input extends Table, Output extends Table>(
|
|
90
98
|
schema: StandardSchema<Input, Output>,
|
|
91
|
-
|
|
99
|
+
...args: Output extends Input
|
|
100
|
+
? [options?: SchemaOptions<Input, Output>]
|
|
101
|
+
: [options: SchemaOptions<Input, Output>]
|
|
92
102
|
): BiDirectionalSchema<Input, Output> {
|
|
103
|
+
const options = args[0];
|
|
93
104
|
// Create a new object that extends the schema
|
|
94
105
|
const bidirectionalSchema = Object.create(schema) as BiDirectionalSchema<Input, Output>;
|
|
95
106
|
|
|
96
|
-
// Handle options
|
|
97
|
-
if (
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if (optionsOrBackward.foreignKeys) {
|
|
110
|
-
bidirectionalSchema.foreignKeys = optionsOrBackward.foreignKeys;
|
|
111
|
-
}
|
|
112
|
-
if (optionsOrBackward.indexes) {
|
|
113
|
-
bidirectionalSchema.indexes = optionsOrBackward.indexes;
|
|
114
|
-
}
|
|
107
|
+
// Handle options object
|
|
108
|
+
if (options) {
|
|
109
|
+
if (options.backward) {
|
|
110
|
+
bidirectionalSchema.backward = options.backward as (output: Output) => Input;
|
|
111
|
+
}
|
|
112
|
+
if (options.primaryKey) {
|
|
113
|
+
bidirectionalSchema.primaryKey = options.primaryKey;
|
|
114
|
+
}
|
|
115
|
+
if (options.foreignKeys) {
|
|
116
|
+
bidirectionalSchema.foreignKeys = options.foreignKeys;
|
|
117
|
+
}
|
|
118
|
+
if (options.indexes) {
|
|
119
|
+
bidirectionalSchema.indexes = options.indexes;
|
|
115
120
|
}
|
|
116
121
|
}
|
|
117
122
|
|
package/src/types.ts
CHANGED
|
@@ -17,6 +17,27 @@ export type { StandardSchemaV1 };
|
|
|
17
17
|
export type InferInput<T> = T extends StandardSchemaV1<infer I, unknown> ? I : never;
|
|
18
18
|
export type InferOutput<T> = T extends StandardSchemaV1<unknown, infer O> ? O : never;
|
|
19
19
|
|
|
20
|
+
// Validation result types
|
|
21
|
+
export interface ValidationResult {
|
|
22
|
+
valid: boolean;
|
|
23
|
+
errors: ValidationErrorDetail[];
|
|
24
|
+
warnings: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ValidationErrorDetail {
|
|
28
|
+
file: string;
|
|
29
|
+
tableName: string;
|
|
30
|
+
rowIndex: number;
|
|
31
|
+
issues: ReadonlyArray<StandardSchemaIssue>;
|
|
32
|
+
type?: 'schema' | 'foreignKey';
|
|
33
|
+
foreignKeyError?: {
|
|
34
|
+
column: string;
|
|
35
|
+
value: unknown;
|
|
36
|
+
referencedTable: string;
|
|
37
|
+
referencedColumn: string;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
20
41
|
export interface ForeignKeyDefinition {
|
|
21
42
|
column: string;
|
|
22
43
|
references: {
|
package/src/validator.test.ts
DELETED
|
@@ -1,507 +0,0 @@
|
|
|
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
|
-
expect(result.warnings).toHaveLength(0);
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('should detect validation errors', async () => {
|
|
53
|
-
const jsonlPath = join(testDir, 'users.jsonl');
|
|
54
|
-
const schemaPath = join(testDir, 'users.schema.ts');
|
|
55
|
-
|
|
56
|
-
await writeFile(jsonlPath, '{"id":1,"name":"Alice"}\n{"id":2}\n');
|
|
57
|
-
await writeFile(
|
|
58
|
-
schemaPath,
|
|
59
|
-
`
|
|
60
|
-
export const schema = {
|
|
61
|
-
'~standard': {
|
|
62
|
-
version: 1,
|
|
63
|
-
vendor: 'test',
|
|
64
|
-
validate: (data) => {
|
|
65
|
-
const issues = [];
|
|
66
|
-
if (!data.name) {
|
|
67
|
-
issues.push({ message: 'Missing name field', path: ['name'] });
|
|
68
|
-
}
|
|
69
|
-
return { value: data, issues };
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
`,
|
|
74
|
-
);
|
|
75
|
-
|
|
76
|
-
const validator = new Validator({ path: jsonlPath });
|
|
77
|
-
const result = await validator.validate();
|
|
78
|
-
|
|
79
|
-
expect(result.valid).toBe(false);
|
|
80
|
-
expect(result.errors).toHaveLength(1);
|
|
81
|
-
expect(result.errors[0].rowIndex).toBe(1); // 0-indexed: line 2 is index 1
|
|
82
|
-
expect(result.errors[0].tableName).toBe('users');
|
|
83
|
-
expect(result.errors[0].issues).toHaveLength(1);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('should throw when no schema exists', async () => {
|
|
87
|
-
const jsonlPath = join(testDir, 'users.jsonl');
|
|
88
|
-
await writeFile(jsonlPath, '{"id":1}\n{"id":2}\n');
|
|
89
|
-
|
|
90
|
-
const validator = new Validator({ path: jsonlPath });
|
|
91
|
-
await expect(validator.validate()).rejects.toThrow(/Schema file not found/);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
it('should handle multiple validation errors in single file', async () => {
|
|
95
|
-
const jsonlPath = join(testDir, 'users.jsonl');
|
|
96
|
-
const schemaPath = join(testDir, 'users.schema.ts');
|
|
97
|
-
|
|
98
|
-
await writeFile(jsonlPath, '{"id":1}\n{"id":2}\n{"id":3,"name":"Charlie"}\n');
|
|
99
|
-
await writeFile(
|
|
100
|
-
schemaPath,
|
|
101
|
-
`
|
|
102
|
-
export const schema = {
|
|
103
|
-
'~standard': {
|
|
104
|
-
version: 1,
|
|
105
|
-
vendor: 'test',
|
|
106
|
-
validate: (data) => {
|
|
107
|
-
const issues = [];
|
|
108
|
-
if (!data.name) {
|
|
109
|
-
issues.push({ message: 'Missing name' });
|
|
110
|
-
}
|
|
111
|
-
return { value: data, issues };
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
`,
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
const validator = new Validator({ path: jsonlPath });
|
|
119
|
-
const result = await validator.validate();
|
|
120
|
-
|
|
121
|
-
expect(result.valid).toBe(false);
|
|
122
|
-
expect(result.errors).toHaveLength(2);
|
|
123
|
-
expect(result.errors[0].rowIndex).toBe(0); // 0-indexed: line 1 is index 0
|
|
124
|
-
expect(result.errors[1].rowIndex).toBe(1); // 0-indexed: line 2 is index 1
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('should return 0-based rowIndex (first line = index 0)', async () => {
|
|
128
|
-
const jsonlPath = join(testDir, 'users.jsonl');
|
|
129
|
-
const schemaPath = join(testDir, 'users.schema.ts');
|
|
130
|
-
|
|
131
|
-
// Line 1: valid, Line 2: invalid (no name), Line 3: invalid (no name)
|
|
132
|
-
await writeFile(jsonlPath, '{"id":1,"name":"Alice"}\n{"id":2}\n{"id":3}\n');
|
|
133
|
-
await writeFile(
|
|
134
|
-
schemaPath,
|
|
135
|
-
`
|
|
136
|
-
export const schema = {
|
|
137
|
-
'~standard': {
|
|
138
|
-
version: 1,
|
|
139
|
-
vendor: 'test',
|
|
140
|
-
validate: (data) => {
|
|
141
|
-
const issues = [];
|
|
142
|
-
if (!data.name) {
|
|
143
|
-
issues.push({ message: 'Missing name field', path: ['name'] });
|
|
144
|
-
}
|
|
145
|
-
return { value: data, issues };
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
`,
|
|
150
|
-
);
|
|
151
|
-
|
|
152
|
-
const validator = new Validator({ path: jsonlPath });
|
|
153
|
-
const result = await validator.validate();
|
|
154
|
-
|
|
155
|
-
expect(result.valid).toBe(false);
|
|
156
|
-
expect(result.errors).toHaveLength(2);
|
|
157
|
-
// First error is on line 2 of the file, which is index 1 (0-based)
|
|
158
|
-
expect(result.errors[0].rowIndex).toBe(1);
|
|
159
|
-
expect(result.errors[0].file).toBe(jsonlPath);
|
|
160
|
-
// Second error is on line 3 of the file, which is index 2 (0-based)
|
|
161
|
-
expect(result.errors[1].rowIndex).toBe(2);
|
|
162
|
-
expect(result.errors[1].file).toBe(jsonlPath);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('should return 0-based rowIndex for first line error', async () => {
|
|
166
|
-
const jsonlPath = join(testDir, 'users.jsonl');
|
|
167
|
-
const schemaPath = join(testDir, 'users.schema.ts');
|
|
168
|
-
|
|
169
|
-
// First line has error
|
|
170
|
-
await writeFile(jsonlPath, '{"id":1}\n{"id":2,"name":"Bob"}\n');
|
|
171
|
-
await writeFile(
|
|
172
|
-
schemaPath,
|
|
173
|
-
`
|
|
174
|
-
export const schema = {
|
|
175
|
-
'~standard': {
|
|
176
|
-
version: 1,
|
|
177
|
-
vendor: 'test',
|
|
178
|
-
validate: (data) => {
|
|
179
|
-
const issues = [];
|
|
180
|
-
if (!data.name) {
|
|
181
|
-
issues.push({ message: 'Missing name field', path: ['name'] });
|
|
182
|
-
}
|
|
183
|
-
return { value: data, issues };
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
};
|
|
187
|
-
`,
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
const validator = new Validator({ path: jsonlPath });
|
|
191
|
-
const result = await validator.validate();
|
|
192
|
-
|
|
193
|
-
expect(result.valid).toBe(false);
|
|
194
|
-
expect(result.errors).toHaveLength(1);
|
|
195
|
-
// First line (line 1 in file) should be index 0
|
|
196
|
-
expect(result.errors[0].rowIndex).toBe(0);
|
|
197
|
-
expect(result.errors[0].file).toBe(jsonlPath);
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
describe('validate directory', () => {
|
|
202
|
-
it('should validate all JSONL files in directory', async () => {
|
|
203
|
-
const usersPath = join(testDir, 'users.jsonl');
|
|
204
|
-
const productsPath = join(testDir, 'products.jsonl');
|
|
205
|
-
const usersSchemaPath = join(testDir, 'users.schema.ts');
|
|
206
|
-
const productsSchemaPath = join(testDir, 'products.schema.ts');
|
|
207
|
-
|
|
208
|
-
await writeFile(usersPath, '{"id":1,"name":"Alice"}\n');
|
|
209
|
-
await writeFile(productsPath, '{"id":1,"name":"Product"}\n');
|
|
210
|
-
await writeFile(
|
|
211
|
-
usersSchemaPath,
|
|
212
|
-
`
|
|
213
|
-
export const schema = {
|
|
214
|
-
'~standard': {
|
|
215
|
-
version: 1,
|
|
216
|
-
vendor: 'test',
|
|
217
|
-
validate: (data) => ({ value: data, issues: [] })
|
|
218
|
-
}
|
|
219
|
-
};
|
|
220
|
-
`,
|
|
221
|
-
);
|
|
222
|
-
await writeFile(
|
|
223
|
-
productsSchemaPath,
|
|
224
|
-
`
|
|
225
|
-
export const schema = {
|
|
226
|
-
'~standard': {
|
|
227
|
-
version: 1,
|
|
228
|
-
vendor: 'test',
|
|
229
|
-
validate: (data) => ({ value: data, issues: [] })
|
|
230
|
-
}
|
|
231
|
-
};
|
|
232
|
-
`,
|
|
233
|
-
);
|
|
234
|
-
|
|
235
|
-
const validator = new Validator({ path: testDir });
|
|
236
|
-
const result = await validator.validate();
|
|
237
|
-
|
|
238
|
-
expect(result.valid).toBe(true);
|
|
239
|
-
expect(result.errors).toHaveLength(0);
|
|
240
|
-
expect(result.warnings).toHaveLength(0);
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it('should collect errors from multiple files', async () => {
|
|
244
|
-
const usersPath = join(testDir, 'users.jsonl');
|
|
245
|
-
const usersSchemaPath = join(testDir, 'users.schema.ts');
|
|
246
|
-
const productsPath = join(testDir, 'products.jsonl');
|
|
247
|
-
const productsSchemaPath = join(testDir, 'products.schema.ts');
|
|
248
|
-
|
|
249
|
-
await writeFile(usersPath, '{"id":1}\n');
|
|
250
|
-
await writeFile(
|
|
251
|
-
usersSchemaPath,
|
|
252
|
-
`
|
|
253
|
-
export const schema = {
|
|
254
|
-
'~standard': {
|
|
255
|
-
version: 1,
|
|
256
|
-
vendor: 'test',
|
|
257
|
-
validate: (data) => ({
|
|
258
|
-
value: data,
|
|
259
|
-
issues: data.name ? [] : [{ message: 'Missing name' }]
|
|
260
|
-
})
|
|
261
|
-
}
|
|
262
|
-
};
|
|
263
|
-
`,
|
|
264
|
-
);
|
|
265
|
-
|
|
266
|
-
await writeFile(productsPath, '{"id":1}\n');
|
|
267
|
-
await writeFile(
|
|
268
|
-
productsSchemaPath,
|
|
269
|
-
`
|
|
270
|
-
export const schema = {
|
|
271
|
-
'~standard': {
|
|
272
|
-
version: 1,
|
|
273
|
-
vendor: 'test',
|
|
274
|
-
validate: (data) => ({
|
|
275
|
-
value: data,
|
|
276
|
-
issues: data.price ? [] : [{ message: 'Missing price' }]
|
|
277
|
-
})
|
|
278
|
-
}
|
|
279
|
-
};
|
|
280
|
-
`,
|
|
281
|
-
);
|
|
282
|
-
|
|
283
|
-
const validator = new Validator({ path: testDir });
|
|
284
|
-
const result = await validator.validate();
|
|
285
|
-
|
|
286
|
-
expect(result.valid).toBe(false);
|
|
287
|
-
expect(result.errors).toHaveLength(2);
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it('should throw error for directory with no JSONL files', async () => {
|
|
291
|
-
await writeFile(join(testDir, 'readme.txt'), 'no jsonl files here');
|
|
292
|
-
|
|
293
|
-
const validator = new Validator({ path: testDir });
|
|
294
|
-
|
|
295
|
-
await expect(validator.validate()).rejects.toThrow('No JSONL files found');
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
it('should skip validation and warn for JSONL files without schema', async () => {
|
|
299
|
-
const usersPath = join(testDir, 'users.jsonl');
|
|
300
|
-
const productsPath = join(testDir, 'products.jsonl');
|
|
301
|
-
const usersSchemaPath = join(testDir, 'users.schema.ts');
|
|
302
|
-
|
|
303
|
-
// Only users.jsonl has a schema
|
|
304
|
-
await writeFile(usersPath, '{"id":1,"name":"Alice"}\n');
|
|
305
|
-
await writeFile(productsPath, '{"id":1,"name":"Product"}\n');
|
|
306
|
-
await writeFile(
|
|
307
|
-
usersSchemaPath,
|
|
308
|
-
`
|
|
309
|
-
export const schema = {
|
|
310
|
-
'~standard': {
|
|
311
|
-
version: 1,
|
|
312
|
-
vendor: 'test',
|
|
313
|
-
validate: (data) => ({ value: data, issues: [] })
|
|
314
|
-
}
|
|
315
|
-
};
|
|
316
|
-
`,
|
|
317
|
-
);
|
|
318
|
-
|
|
319
|
-
const validator = new Validator({ path: testDir });
|
|
320
|
-
const result = await validator.validate();
|
|
321
|
-
|
|
322
|
-
expect(result.valid).toBe(true);
|
|
323
|
-
expect(result.errors).toHaveLength(0);
|
|
324
|
-
expect(result.warnings).toHaveLength(1);
|
|
325
|
-
expect(result.warnings[0]).toContain('products');
|
|
326
|
-
expect(result.warnings[0]).toContain('schema file not found');
|
|
327
|
-
});
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
describe('error handling', () => {
|
|
331
|
-
it('should throw error for non-JSONL file', async () => {
|
|
332
|
-
const txtPath = join(testDir, 'test.txt');
|
|
333
|
-
await writeFile(txtPath, 'not a jsonl file');
|
|
334
|
-
|
|
335
|
-
const validator = new Validator({ path: txtPath });
|
|
336
|
-
|
|
337
|
-
await expect(validator.validate()).rejects.toThrow('Invalid path');
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
it('should reject async validation', async () => {
|
|
341
|
-
const jsonlPath = join(testDir, 'users.jsonl');
|
|
342
|
-
const schemaPath = join(testDir, 'users.schema.ts');
|
|
343
|
-
|
|
344
|
-
await writeFile(jsonlPath, '{"id":1}\n');
|
|
345
|
-
await writeFile(
|
|
346
|
-
schemaPath,
|
|
347
|
-
`
|
|
348
|
-
export const schema = {
|
|
349
|
-
'~standard': {
|
|
350
|
-
version: 1,
|
|
351
|
-
vendor: 'test',
|
|
352
|
-
validate: async (data) => {
|
|
353
|
-
return { value: data, issues: [] };
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
};
|
|
357
|
-
`,
|
|
358
|
-
);
|
|
359
|
-
|
|
360
|
-
const validator = new Validator({ path: jsonlPath });
|
|
361
|
-
|
|
362
|
-
await expect(validator.validate()).rejects.toThrow(
|
|
363
|
-
'Asynchronous validation is not supported',
|
|
364
|
-
);
|
|
365
|
-
});
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
describe('constraint validation', () => {
|
|
369
|
-
it('should detect primary key constraint violations', async () => {
|
|
370
|
-
const jsonlPath = join(testDir, 'users.jsonl');
|
|
371
|
-
const schemaPath = join(testDir, 'users.schema.ts');
|
|
372
|
-
|
|
373
|
-
// Write data with duplicate id
|
|
374
|
-
await writeFile(
|
|
375
|
-
jsonlPath,
|
|
376
|
-
'{"id":"1","name":"Alice","email":"alice@example.com"}\n{"id":"1","name":"Bob","email":"bob@example.com"}\n',
|
|
377
|
-
);
|
|
378
|
-
await writeFile(
|
|
379
|
-
schemaPath,
|
|
380
|
-
`
|
|
381
|
-
export const schema = {
|
|
382
|
-
'~standard': {
|
|
383
|
-
version: 1,
|
|
384
|
-
vendor: 'test',
|
|
385
|
-
validate: (data) => ({ value: data, issues: [] })
|
|
386
|
-
},
|
|
387
|
-
primaryKey: 'id'
|
|
388
|
-
};
|
|
389
|
-
`,
|
|
390
|
-
);
|
|
391
|
-
|
|
392
|
-
const validator = new Validator({ path: jsonlPath });
|
|
393
|
-
const result = await validator.validate();
|
|
394
|
-
|
|
395
|
-
expect(result.valid).toBe(false);
|
|
396
|
-
expect(result.errors).toHaveLength(1);
|
|
397
|
-
expect(result.errors[0].rowIndex).toBe(1);
|
|
398
|
-
expect(result.errors[0].issues[0].message).toContain('UNIQUE constraint failed');
|
|
399
|
-
expect(result.errors[0].issues[0].message).toContain('id');
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
it('should detect unique index constraint violations', async () => {
|
|
403
|
-
const jsonlPath = join(testDir, 'users.jsonl');
|
|
404
|
-
const schemaPath = join(testDir, 'users.schema.ts');
|
|
405
|
-
|
|
406
|
-
// Write data with duplicate email
|
|
407
|
-
await writeFile(
|
|
408
|
-
jsonlPath,
|
|
409
|
-
'{"id":"1","name":"Alice","email":"alice@example.com"}\n{"id":"2","name":"Bob","email":"alice@example.com"}\n',
|
|
410
|
-
);
|
|
411
|
-
await writeFile(
|
|
412
|
-
schemaPath,
|
|
413
|
-
`
|
|
414
|
-
export const schema = {
|
|
415
|
-
'~standard': {
|
|
416
|
-
version: 1,
|
|
417
|
-
vendor: 'test',
|
|
418
|
-
validate: (data) => ({ value: data, issues: [] })
|
|
419
|
-
},
|
|
420
|
-
primaryKey: 'id',
|
|
421
|
-
indexes: [
|
|
422
|
-
{ name: 'users_email_unique', columns: ['email'], unique: true }
|
|
423
|
-
]
|
|
424
|
-
};
|
|
425
|
-
`,
|
|
426
|
-
);
|
|
427
|
-
|
|
428
|
-
const validator = new Validator({ path: jsonlPath });
|
|
429
|
-
const result = await validator.validate();
|
|
430
|
-
|
|
431
|
-
expect(result.valid).toBe(false);
|
|
432
|
-
expect(result.errors).toHaveLength(1);
|
|
433
|
-
expect(result.errors[0].rowIndex).toBe(1);
|
|
434
|
-
expect(result.errors[0].issues[0].message).toContain('UNIQUE constraint failed');
|
|
435
|
-
expect(result.errors[0].issues[0].message).toContain('email');
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
it('should use id column as primary key when primaryKey is not specified', async () => {
|
|
439
|
-
const jsonlPath = join(testDir, 'users.jsonl');
|
|
440
|
-
const schemaPath = join(testDir, 'users.schema.ts');
|
|
441
|
-
|
|
442
|
-
// Write data with duplicate id (no primaryKey specified in schema)
|
|
443
|
-
await writeFile(jsonlPath, '{"id":"1","name":"Alice"}\n{"id":"1","name":"Bob"}\n');
|
|
444
|
-
await writeFile(
|
|
445
|
-
schemaPath,
|
|
446
|
-
`
|
|
447
|
-
export const schema = {
|
|
448
|
-
'~standard': {
|
|
449
|
-
version: 1,
|
|
450
|
-
vendor: 'test',
|
|
451
|
-
validate: (data) => ({ value: data, issues: [] })
|
|
452
|
-
}
|
|
453
|
-
// Note: no primaryKey specified
|
|
454
|
-
};
|
|
455
|
-
`,
|
|
456
|
-
);
|
|
457
|
-
|
|
458
|
-
const validator = new Validator({ path: jsonlPath });
|
|
459
|
-
const result = await validator.validate();
|
|
460
|
-
|
|
461
|
-
expect(result.valid).toBe(false);
|
|
462
|
-
expect(result.errors).toHaveLength(1);
|
|
463
|
-
expect(result.errors[0].rowIndex).toBe(1);
|
|
464
|
-
expect(result.errors[0].issues[0].message).toContain('UNIQUE constraint failed');
|
|
465
|
-
expect(result.errors[0].issues[0].message).toContain('id');
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
it('should detect multiple constraint violations in single file', async () => {
|
|
469
|
-
const jsonlPath = join(testDir, 'users.jsonl');
|
|
470
|
-
const schemaPath = join(testDir, 'users.schema.ts');
|
|
471
|
-
|
|
472
|
-
// Write data with both duplicate id and duplicate email
|
|
473
|
-
await writeFile(
|
|
474
|
-
jsonlPath,
|
|
475
|
-
'{"id":"1","name":"Alice","email":"alice@example.com"}\n{"id":"1","name":"Bob","email":"bob@example.com"}\n{"id":"2","name":"Charlie","email":"alice@example.com"}\n',
|
|
476
|
-
);
|
|
477
|
-
await writeFile(
|
|
478
|
-
schemaPath,
|
|
479
|
-
`
|
|
480
|
-
export const schema = {
|
|
481
|
-
'~standard': {
|
|
482
|
-
version: 1,
|
|
483
|
-
vendor: 'test',
|
|
484
|
-
validate: (data) => ({ value: data, issues: [] })
|
|
485
|
-
},
|
|
486
|
-
primaryKey: 'id',
|
|
487
|
-
indexes: [
|
|
488
|
-
{ name: 'users_email_unique', columns: ['email'], unique: true }
|
|
489
|
-
]
|
|
490
|
-
};
|
|
491
|
-
`,
|
|
492
|
-
);
|
|
493
|
-
|
|
494
|
-
const validator = new Validator({ path: jsonlPath });
|
|
495
|
-
const result = await validator.validate();
|
|
496
|
-
|
|
497
|
-
expect(result.valid).toBe(false);
|
|
498
|
-
expect(result.errors).toHaveLength(2);
|
|
499
|
-
// First error: duplicate id
|
|
500
|
-
expect(result.errors[0].rowIndex).toBe(1);
|
|
501
|
-
expect(result.errors[0].issues[0].message).toContain('id');
|
|
502
|
-
// Second error: duplicate email
|
|
503
|
-
expect(result.errors[1].rowIndex).toBe(2);
|
|
504
|
-
expect(result.errors[1].issues[0].message).toContain('email');
|
|
505
|
-
});
|
|
506
|
-
});
|
|
507
|
-
});
|