@toiroakr/lines-db 0.9.2 → 0.10.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/package.json CHANGED
@@ -1,28 +1,24 @@
1
1
  {
2
2
  "name": "@toiroakr/lines-db",
3
- "version": "0.9.2",
3
+ "version": "0.10.1",
4
4
  "description": "A database implementation that treats JSONL files as tables using SQLite",
5
5
  "type": "module",
6
- "main": "./dist/index.cjs",
7
- "module": "./dist/index.mjs",
8
6
  "types": "./dist/index.d.mts",
9
7
  "bin": {
10
8
  "lines-db": "./bin/cli.mjs"
11
9
  },
12
10
  "exports": {
13
11
  ".": {
14
- "import": {
15
- "types": "./dist/index.d.mts",
16
- "default": "./dist/index.mjs"
17
- },
18
- "require": {
19
- "types": "./dist/index.d.cts",
20
- "default": "./dist/index.cjs"
21
- }
12
+ "types": "./dist/index.d.mts",
13
+ "default": "./dist/index.mjs"
22
14
  }
23
15
  },
16
+ "engines": {
17
+ "node": ">=22.12.0"
18
+ },
24
19
  "scripts": {
25
20
  "build": "tsdown",
21
+ "publint": "publint",
26
22
  "typecheck": "tsc --noEmit",
27
23
  "test": "vitest run"
28
24
  },
@@ -36,31 +32,25 @@
36
32
  "license": "MIT",
37
33
  "repository": {
38
34
  "type": "git",
39
- "url": "https://github.com/toiroakr/lines-db.git"
35
+ "url": "git+https://github.com/toiroakr/lines-db.git"
40
36
  },
41
37
  "bugs": {
42
38
  "url": "https://github.com/toiroakr/lines-db/issues"
43
39
  },
44
40
  "homepage": "https://github.com/toiroakr/lines-db#readme",
45
41
  "devDependencies": {
46
- "@types/node": "24.12.2",
47
- "tsdown": "0.21.10",
48
- "type-fest": "5.6.0",
42
+ "@types/node": "24.13.2",
43
+ "politty": "0.11.0",
44
+ "publint": "0.3.21",
45
+ "tsdown": "0.22.3",
46
+ "type-fest": "5.7.0",
49
47
  "typescript": "6.0.3",
50
- "valibot": "1.3.1",
51
- "vitest": "4.1.5"
52
- },
53
- "peerDependencies": {
54
- "valibot": ">=1.0.0"
55
- },
56
- "peerDependenciesMeta": {
57
- "valibot": {
58
- "optional": true
59
- }
48
+ "valibot": "1.4.2",
49
+ "vitest": "4.1.9",
50
+ "zod": "4.4.3"
60
51
  },
61
52
  "dependencies": {
62
53
  "@standard-schema/spec": "^1.0.0",
63
- "commander": "^14.0.2",
64
- "tsx": "^4.19.2"
54
+ "amaro": "^1.1.10"
65
55
  }
66
56
  }
package/src/cli.ts CHANGED
@@ -1,14 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // Register tsx for TypeScript schema file support
3
+ // Register amaro for TypeScript schema file support
4
4
  import { register } from 'node:module';
5
- register('tsx', import.meta.url, { data: {} });
5
+ register('amaro/transform', import.meta.url);
6
6
 
7
7
  import { TypeGenerator } from './type-generator.js';
8
8
  import { LinesDB } from './database.js';
9
9
  import { ErrorFormatter } from './error-formatter.js';
10
10
  import type { ValidationError, JsonObject } from './types.js';
11
- import { Command } from 'commander';
11
+ import { z } from 'zod';
12
+ import { arg, defineCommand, runMain } from 'politty';
12
13
  import { styleText } from 'node:util';
13
14
  import { writeFile, stat, readdir } from 'node:fs/promises';
14
15
  import { basename, dirname } from 'node:path';
@@ -45,51 +46,59 @@ function runInSandbox<T>(expression: string, context: Record<string, unknown> =
45
46
  return runInNewContext(expression, sandbox, { timeout: 1000 }) as T;
46
47
  }
47
48
 
48
- const program = new Command();
49
-
50
- program.name('@toiroakr/lines-db').description('Database utilities for JSONL files').version('1.0.0');
51
-
52
- // Generate command
53
- program
54
- .command('generate')
55
- .description('Generate TypeScript type definitions from schema files')
56
- .argument('<dataDir>', 'Directory containing JSONL and schema files')
57
- .option('-o, --output <path>', 'Output file path (default: db.ts in dataDir)')
58
- .action(async (dataDir: string, options: { output?: string }) => {
49
+ const generateCommand = defineCommand({
50
+ name: 'generate',
51
+ description: 'Generate TypeScript type definitions from schema files',
52
+ args: z.object({
53
+ dataDir: arg(z.string(), {
54
+ positional: true,
55
+ description: 'Directory containing JSONL and schema files',
56
+ }),
57
+ output: arg(z.string().optional(), {
58
+ alias: 'o',
59
+ description: 'Output file path (default: db.ts in dataDir)',
60
+ }),
61
+ }),
62
+ run: async (args) => {
59
63
  try {
60
- const generator = new TypeGenerator({ dataDir, output: options.output });
64
+ const generator = new TypeGenerator({ dataDir: args.dataDir, output: args.output });
61
65
  await generator.generate();
62
66
  console.log('Type generation completed successfully!');
63
67
  } catch (error) {
64
68
  console.error('Error:', error instanceof Error ? error.message : String(error));
65
69
  process.exit(1);
66
70
  }
67
- });
68
-
69
- // Validate command
70
- program
71
- .command('validate')
72
- .description('Validate JSONL file(s) against schema')
73
- .argument('<path>', 'File or directory path to validate')
74
- .option('-v, --verbose', 'Show verbose error output', false)
75
- .action(async (path: string, options: { verbose: boolean }) => {
71
+ },
72
+ });
73
+
74
+ const validateCommand = defineCommand({
75
+ name: 'validate',
76
+ description: 'Validate JSONL file(s) against schema',
77
+ args: z.object({
78
+ path: arg(z.string(), {
79
+ positional: true,
80
+ description: 'File or directory path to validate',
81
+ }),
82
+ verbose: arg(z.boolean().default(false), {
83
+ alias: 'v',
84
+ description: 'Show verbose error output',
85
+ }),
86
+ }),
87
+ run: async (args) => {
76
88
  try {
77
- // Determine if path is a file or directory
78
- const stats = await stat(path);
89
+ const stats = await stat(args.path);
79
90
  let dataDir: string;
80
91
  let tableName: string | undefined;
81
92
 
82
93
  if (stats.isDirectory()) {
83
- dataDir = path;
84
- // Validate all tables in directory
85
- } else if (stats.isFile() && path.endsWith('.jsonl')) {
86
- dataDir = dirname(path);
87
- tableName = basename(path, '.jsonl');
94
+ dataDir = args.path;
95
+ } else if (stats.isFile() && args.path.endsWith('.jsonl')) {
96
+ dataDir = dirname(args.path);
97
+ tableName = basename(args.path, '.jsonl');
88
98
  } else {
89
- throw new Error(`Invalid path: ${path}. Must be a directory or .jsonl file.`);
99
+ throw new Error(`Invalid path: ${args.path}. Must be a directory or .jsonl file.`);
90
100
  }
91
101
 
92
- // Use LinesDB.initialize() with detailed validation
93
102
  const db = LinesDB.create({ dataDir });
94
103
  let result;
95
104
  try {
@@ -98,21 +107,17 @@ program
98
107
  await db.close();
99
108
  }
100
109
 
101
- // Directory validation: display per-table results
102
110
  if (!tableName) {
103
- const formatter = new ErrorFormatter({ verbose: options.verbose });
111
+ const formatter = new ErrorFormatter({ verbose: args.verbose });
104
112
 
105
113
  for (const tableResult of result.tableResults) {
106
114
  if (tableResult.valid && tableResult.warnings.length === 0) {
107
- // Success
108
115
  console.log(styleText('green', `✓ ${tableResult.tableName} (${tableResult.rowCount} records)`));
109
116
  } else if (tableResult.valid && tableResult.warnings.length > 0) {
110
- // Warnings
111
117
  for (const warning of tableResult.warnings) {
112
118
  console.warn(styleText('yellow', `⚠ ${warning}`));
113
119
  }
114
120
  } else {
115
- // Errors
116
121
  const fileErrors = tableResult.errors;
117
122
  console.error(formatter.formatErrorHeader(fileErrors.length, fileErrors[0]?.file));
118
123
  console.error('');
@@ -159,7 +164,6 @@ program
159
164
  process.exit(1);
160
165
  }
161
166
  } else {
162
- // Single file validation: existing behavior
163
167
  if (result.warnings.length > 0) {
164
168
  for (const warning of result.warnings) {
165
169
  console.warn(styleText('yellow', `⚠ ${warning}`));
@@ -171,7 +175,7 @@ program
171
175
  console.log(styleText('green', '✓ All records are valid'));
172
176
  process.exit(0);
173
177
  } else {
174
- const formatter = new ErrorFormatter({ verbose: options.verbose });
178
+ const formatter = new ErrorFormatter({ verbose: args.verbose });
175
179
 
176
180
  for (const [, fileErrors] of result.errors.reduce((map, error) => {
177
181
  const errors = map.get(error.file) || [];
@@ -222,46 +226,76 @@ program
222
226
  console.error('Error:', error instanceof Error ? error.message : String(error));
223
227
  process.exit(1);
224
228
  }
225
- });
226
-
227
- // Migrate command
228
- program
229
- .command('migrate')
230
- .description('Migrate data with transformation function')
231
- .argument('<path>', 'File or directory path to migrate')
232
- .argument('<transform>', 'Transform function (e.g., "(row) => ({ ...row, age: row.age + 1 })")')
233
- .option('-f, --filter <expr>', 'Filter expression')
234
- .option('-e, --errorOutput <path>', 'Output file path for transformed data when migration fails')
235
- .option('-v, --verbose', 'Show verbose error output', false)
236
- .action(
237
- async (
238
- path: string,
239
- transformStr: string,
240
- options: { filter?: string; errorOutput?: string; verbose: boolean },
241
- ) => {
242
- try {
243
- const stats = await stat(path);
244
-
245
- if (stats.isDirectory()) {
246
- // Migrate all JSONL files in directory
247
- await migrateDirectory(path, transformStr, options);
248
- } else if (stats.isFile() && path.endsWith('.jsonl')) {
249
- // Migrate single file
250
- await migrateFile(path, transformStr, options);
251
- } else {
252
- console.error(`Error: Invalid path: ${path}. Must be a directory or .jsonl file.`);
253
- process.exit(1);
254
- }
255
- } catch (error) {
256
- if (error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT') {
257
- console.error(`Error: Path not found: ${path}`);
258
- } else {
259
- console.error(`Error: ${String(error)}`);
260
- }
229
+ },
230
+ });
231
+
232
+ const migrateCommand = defineCommand({
233
+ name: 'migrate',
234
+ description: 'Migrate data with transformation function',
235
+ args: z.object({
236
+ path: arg(z.string(), {
237
+ positional: true,
238
+ description: 'File or directory path to migrate',
239
+ }),
240
+ transform: arg(z.string(), {
241
+ positional: true,
242
+ description: 'Transform function (e.g., "(row) => ({ ...row, age: row.age + 1 })")',
243
+ }),
244
+ filter: arg(z.string().optional(), {
245
+ alias: 'f',
246
+ description: 'Filter expression',
247
+ }),
248
+ errorOutput: arg(z.string().optional(), {
249
+ alias: 'e',
250
+ description: 'Output file path for transformed data when migration fails',
251
+ }),
252
+ verbose: arg(z.boolean().default(false), {
253
+ alias: 'v',
254
+ description: 'Show verbose error output',
255
+ }),
256
+ }),
257
+ run: async (args) => {
258
+ try {
259
+ const stats = await stat(args.path);
260
+
261
+ if (stats.isDirectory()) {
262
+ await migrateDirectory(args.path, args.transform, {
263
+ filter: args.filter,
264
+ errorOutput: args.errorOutput,
265
+ verbose: args.verbose,
266
+ });
267
+ } else if (stats.isFile() && args.path.endsWith('.jsonl')) {
268
+ await migrateFile(args.path, args.transform, {
269
+ filter: args.filter,
270
+ errorOutput: args.errorOutput,
271
+ verbose: args.verbose,
272
+ });
273
+ } else {
274
+ console.error(`Error: Invalid path: ${args.path}. Must be a directory or .jsonl file.`);
261
275
  process.exit(1);
262
276
  }
263
- },
264
- );
277
+ } catch (error) {
278
+ if (error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT') {
279
+ console.error(`Error: Path not found: ${args.path}`);
280
+ } else {
281
+ console.error(`Error: ${String(error)}`);
282
+ }
283
+ process.exit(1);
284
+ }
285
+ },
286
+ });
287
+
288
+ const program = defineCommand({
289
+ name: '@toiroakr/lines-db',
290
+ description: 'Database utilities for JSONL files',
291
+ subCommands: {
292
+ generate: generateCommand,
293
+ validate: validateCommand,
294
+ migrate: migrateCommand,
295
+ },
296
+ });
297
+
298
+ runMain(program, { version: '1.0.0' });
265
299
 
266
300
  /**
267
301
  * Migrate all JSONL files in a directory
@@ -271,7 +305,6 @@ async function migrateDirectory(
271
305
  transformStr: string,
272
306
  options: { filter?: string; errorOutput?: string; verbose: boolean },
273
307
  ) {
274
- // Find all JSONL files in directory
275
308
  const entries = await readdir(dirPath, { withFileTypes: true });
276
309
  const jsonlFiles = entries
277
310
  .filter((entry) => entry.isFile() && entry.name.endsWith('.jsonl'))
@@ -284,18 +317,15 @@ async function migrateDirectory(
284
317
 
285
318
  console.log(`Found ${jsonlFiles.length} JSONL file(s) in directory`);
286
319
 
287
- // Initialize database
288
320
  const db = LinesDB.create({ dataDir: dirPath });
289
321
  const initResult = await db.initialize({ detailedValidate: true });
290
322
 
291
- // Display warnings if any
292
323
  if (initResult.warnings.length > 0) {
293
324
  for (const warning of initResult.warnings) {
294
325
  console.warn(styleText('yellow', `⚠ ${warning}`));
295
326
  }
296
327
  }
297
328
 
298
- // Check for initialization errors
299
329
  if (!initResult.valid) {
300
330
  console.error(`Error: Failed to initialize database due to validation errors:`);
301
331
  const formatter = new ErrorFormatter({ verbose: options.verbose });
@@ -324,7 +354,6 @@ async function migrateDirectory(
324
354
  console.log(`Loaded ${tableNames.length} table(s): ${tableNames.join(', ')}\n`);
325
355
 
326
356
  try {
327
- // Parse transform function
328
357
  const transform = runInSandbox<unknown>(`(${transformStr})`);
329
358
 
330
359
  if (typeof transform !== 'function') {
@@ -333,7 +362,6 @@ async function migrateDirectory(
333
362
  process.exit(1);
334
363
  }
335
364
 
336
- // Parse filter if provided
337
365
  let filter: unknown = undefined;
338
366
  if (options.filter) {
339
367
  try {
@@ -346,12 +374,10 @@ async function migrateDirectory(
346
374
  let totalRowsMigrated = 0;
347
375
  let hasErrors = false;
348
376
 
349
- // Process each table
350
377
  for (const tableName of tableNames) {
351
378
  try {
352
379
  console.log(`Processing table '${tableName}'...`);
353
380
 
354
- // Get rows to migrate
355
381
  const rowsToMigrate = filter ? db.find(tableName, filter as Parameters<typeof db.find>[1]) : db.find(tableName);
356
382
 
357
383
  if (rowsToMigrate.length === 0) {
@@ -361,10 +387,8 @@ async function migrateDirectory(
361
387
 
362
388
  console.log(` Found ${rowsToMigrate.length} row(s) to migrate`);
363
389
 
364
- // Apply transformation
365
390
  const transformedRows = rowsToMigrate.map((row) => transform(row as JsonObject));
366
391
 
367
- // Perform the migration in a transaction
368
392
  await db.transaction(async () => {
369
393
  db.batchUpdate(tableName, transformedRows as Parameters<typeof db.batchUpdate>[1], {
370
394
  validate: true,
@@ -440,7 +464,6 @@ async function migrateFile(
440
464
  transformStr: string,
441
465
  options: { filter?: string; errorOutput?: string; verbose: boolean },
442
466
  ) {
443
- // Extract table name from file path
444
467
  const fileName = filePath.split('/').pop() || '';
445
468
  const tableName = fileName.replace('.jsonl', '');
446
469
 
@@ -449,11 +472,9 @@ async function migrateFile(
449
472
  process.exit(1);
450
473
  }
451
474
 
452
- // Get directory from file path
453
475
  const lastSlashIndex = filePath.lastIndexOf('/');
454
476
  const dataDir = lastSlashIndex > 0 ? filePath.substring(0, lastSlashIndex) : '.';
455
477
 
456
- // Parse transform function first (before initialization)
457
478
  let transform: (row: JsonObject) => JsonObject;
458
479
  try {
459
480
  const parsedTransform = runInSandbox<unknown>(`(${transformStr})`);
@@ -468,18 +489,15 @@ async function migrateFile(
468
489
  process.exit(1);
469
490
  }
470
491
 
471
- // Initialize database with transform applied
472
492
  const db = LinesDB.create({ dataDir });
473
493
  const initResult = await db.initialize({ tableName, transform, detailedValidate: true });
474
494
 
475
- // Display warnings if any
476
495
  if (initResult.warnings.length > 0) {
477
496
  for (const warning of initResult.warnings) {
478
497
  console.warn(styleText('yellow', `⚠ ${warning}`));
479
498
  }
480
499
  }
481
500
 
482
- // Check for initialization errors
483
501
  if (!initResult.valid) {
484
502
  console.error(`Error: Failed to initialize database due to validation errors:`);
485
503
  const formatter = new ErrorFormatter({ verbose: options.verbose });
@@ -499,21 +517,16 @@ async function migrateFile(
499
517
  }
500
518
 
501
519
  try {
502
- // Parse filter if provided
503
520
  let filter: unknown = undefined;
504
521
  if (options.filter) {
505
522
  try {
506
- // Try JSON parse first
507
523
  filter = JSON.parse(options.filter);
508
524
  } catch {
509
- // Fall back to eval for JavaScript expressions
510
525
  filter = runInSandbox(`(${options.filter})`);
511
526
  }
512
527
  }
513
528
 
514
- // If filter is provided, we need to apply transform only to matching rows
515
529
  if (filter) {
516
- // Get rows to migrate
517
530
  let rowsToMigrate;
518
531
  try {
519
532
  rowsToMigrate = db.find(tableName, filter as Parameters<typeof db.find>[1]);
@@ -534,10 +547,8 @@ async function migrateFile(
534
547
  process.exit(0);
535
548
  }
536
549
 
537
- // Apply transformation
538
550
  const transformedRows = rowsToMigrate.map((row) => transform(row as JsonObject));
539
551
 
540
- // Perform the migration in a transaction
541
552
  try {
542
553
  await db.transaction(async () => {
543
554
  db.batchUpdate(tableName, transformedRows as Parameters<typeof db.batchUpdate>[1], {
@@ -553,7 +564,6 @@ async function migrateFile(
553
564
  } catch (error) {
554
565
  await db.close();
555
566
 
556
- // Write transformed data to error output file if --errorOutput is specified
557
567
  if (options.errorOutput) {
558
568
  try {
559
569
  const jsonlContent = transformedRows.map((row) => JSON.stringify(row)).join('\n');
@@ -577,7 +587,6 @@ async function migrateFile(
577
587
  const formatter = new ErrorFormatter({ verbose: options.verbose });
578
588
  console.error(formatter.formatMigrationFailureHeader());
579
589
 
580
- // Display detailed error information
581
590
  if (error instanceof Error && error.name === 'ValidationError') {
582
591
  const validationError = error as ValidationError & {
583
592
  validationErrors?: Array<{
@@ -588,7 +597,6 @@ async function migrateFile(
588
597
  }>;
589
598
  };
590
599
 
591
- // Display all validation errors
592
600
  if (validationError.validationErrors) {
593
601
  console.error(
594
602
  `\nFound ${validationError.validationErrors.length} validation error(s) in transformed data:\n`,
@@ -605,7 +613,6 @@ async function migrateFile(
605
613
  const formatted = formatter.formatValidationErrors(errorInfos);
606
614
  console.error(formatted);
607
615
  } else {
608
- // Fallback for single validation error (backward compatibility)
609
616
  console.error('\nValidation error:\n');
610
617
  const errorInfo = {
611
618
  file: filePath,
@@ -618,12 +625,10 @@ async function migrateFile(
618
625
  } else if (error instanceof Error) {
619
626
  console.error(`\n ${error.message}`);
620
627
 
621
- // Output stack trace for debugging
622
628
  if (options.verbose && error.stack) {
623
629
  console.error(`\nStack trace:\n${error.stack}`);
624
630
  }
625
631
 
626
- // Check if it's a SQLite constraint error
627
632
  if (
628
633
  error.message.includes('UNIQUE constraint failed') ||
629
634
  error.message.includes('FOREIGN KEY constraint failed') ||
@@ -641,8 +646,6 @@ async function migrateFile(
641
646
  process.exit(1);
642
647
  }
643
648
  } else {
644
- // No filter - all rows have been transformed during initialization
645
- // Just sync to write back to JSONL file
646
649
  try {
647
650
  const allRows = db.find(tableName);
648
651
  console.log(`Migrated ${allRows.length} row(s) in table '${tableName}'`);
@@ -665,5 +668,3 @@ async function migrateFile(
665
668
  throw error;
666
669
  }
667
670
  }
668
-
669
- program.parse();
package/tsdown.config.ts CHANGED
@@ -1,24 +1,28 @@
1
1
  import { defineConfig } from 'tsdown';
2
2
 
3
3
  export default defineConfig([
4
- // CLI build (ESM only)
4
+ // CLI build (ESM only, fully bundled)
5
5
  {
6
6
  entry: ['src/cli.ts'],
7
7
  format: ['esm'],
8
8
  platform: 'node',
9
- target: 'node18',
9
+ target: 'node22',
10
10
  clean: true,
11
11
  shims: true,
12
12
  outDir: 'bin',
13
13
  dts: false,
14
14
  treeshake: true,
15
+ deps: {
16
+ alwaysBundle: ['zod', 'politty'],
17
+ onlyBundle: false,
18
+ },
15
19
  },
16
- // Library build (ESM + CJS)
20
+ // Library build (ESM only)
17
21
  {
18
22
  entry: ['src/index.ts'],
19
- format: ['esm', 'cjs'],
23
+ format: ['esm'],
20
24
  platform: 'node',
21
- target: 'node18',
25
+ target: 'node22',
22
26
  outDir: 'dist',
23
27
  dts: true,
24
28
  treeshake: true,