@toiroakr/lines-db 0.5.0 → 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/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 interface SchemaOptions {
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
- * Backward transformation from Output to Input
24
- * Required when Input and Output types differ (e.g., with transformations)
25
- */
26
- backward?: (output: Table) => Table;
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 optionsOrBackward - Optional SchemaOptions object or backward transformation function (Output → Input)
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 recommended (legacy)
77
+ * // With transformation - backward REQUIRED
72
78
  * const schema = defineSchema(
73
79
  * v.pipe(v.string(), v.transform(Number)),
74
- * (num) => String(num) // backward: number → string
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
- optionsOrBackward?: SchemaOptions | ((output: Output) => Input),
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 or backward function
97
- if (optionsOrBackward) {
98
- if (typeof optionsOrBackward === 'function') {
99
- // Legacy: backward function only
100
- bidirectionalSchema.backward = optionsOrBackward;
101
- } else {
102
- // New: options object
103
- if (optionsOrBackward.backward) {
104
- bidirectionalSchema.backward = optionsOrBackward.backward as (output: Output) => Input;
105
- }
106
- if (optionsOrBackward.primaryKey) {
107
- bidirectionalSchema.primaryKey = optionsOrBackward.primaryKey;
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: {
@@ -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
- });