flint-orm 0.1.0 → 0.2.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/dist/index.js CHANGED
@@ -1144,6 +1144,9 @@ function flint(details) {
1144
1144
  avg: (table, column, condition) => avg(client, table, column, condition),
1145
1145
  min: (table, column, condition) => min(client, table, column, condition),
1146
1146
  max: (table, column, condition) => max(client, table, column, condition),
1147
+ $run(sql, ...params) {
1148
+ return client.prepare(sql).run(...params);
1149
+ },
1147
1150
  $client: client
1148
1151
  };
1149
1152
  }
@@ -1329,7 +1332,7 @@ function real(name) {
1329
1332
  });
1330
1333
  }
1331
1334
  // src/schema/table.ts
1332
- function table(name, columns) {
1335
+ function table(name, columns, indexFn) {
1333
1336
  const stamped = Object.create(null);
1334
1337
  for (const [key, col] of Object.entries(columns)) {
1335
1338
  const columnName = col.name || key;
@@ -1383,14 +1386,22 @@ function table(name, columns) {
1383
1386
  }
1384
1387
  stamped[key] = stampedCol;
1385
1388
  }
1386
- return Object.assign(stamped, {
1389
+ const result = Object.assign(stamped, {
1387
1390
  _: { name }
1388
1391
  });
1392
+ if (indexFn) {
1393
+ const raw = indexFn(result);
1394
+ if (raw.length > 0) {
1395
+ const tableObj = result;
1396
+ tableObj.__indexes = raw.map((item) => item.build());
1397
+ }
1398
+ }
1399
+ return result;
1389
1400
  }
1390
1401
  function toSnakeCase(str) {
1391
1402
  return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`);
1392
1403
  }
1393
- function snakeCaseTable(tableName, columns) {
1404
+ function snakeCaseTable(tableName, columns, indexFn) {
1394
1405
  const converted = {};
1395
1406
  for (const [key, col] of Object.entries(columns)) {
1396
1407
  const sqlName = toSnakeCase(key);
@@ -1445,11 +1456,141 @@ function snakeCaseTable(tableName, columns) {
1445
1456
  }
1446
1457
  converted[key] = stampedCol;
1447
1458
  }
1448
- return table(tableName, converted);
1459
+ return table(tableName, converted, indexFn);
1449
1460
  }
1450
1461
  var snakeCase = {
1451
1462
  table: snakeCaseTable
1452
1463
  };
1464
+ function index(name) {
1465
+ let columns = [];
1466
+ let isUnique = false;
1467
+ const builder = {
1468
+ on(...cols) {
1469
+ columns = cols;
1470
+ return builder;
1471
+ },
1472
+ unique() {
1473
+ isUnique = true;
1474
+ return builder;
1475
+ },
1476
+ build() {
1477
+ if (columns.length === 0) {
1478
+ throw new Error(`Index "${name}" has no columns \u2014 call .on() before returning`);
1479
+ }
1480
+ return {
1481
+ name,
1482
+ columns: columns.map((c) => c.name),
1483
+ unique: isUnique
1484
+ };
1485
+ }
1486
+ };
1487
+ return builder;
1488
+ }
1489
+ // src/sqlite/introspect.ts
1490
+ var TEXT_TYPES = new Set(["text", "varchar", "character", "char", "clob", "native character", "nvarchar", "nchar", "nyclob", "numeric"]);
1491
+ var INTEGER_TYPES = new Set(["integer", "int", "smallint", "tinyint", "bigint", "unsigned big int", "int2", "int8", "mediumint"]);
1492
+ var REAL_TYPES = new Set(["real", "float", "double", "double precision", "decimal"]);
1493
+ function normalizeType(rawType) {
1494
+ const t = rawType.toLowerCase().trim();
1495
+ const base = t.replace(/\(.*\)/, "").trim();
1496
+ if (INTEGER_TYPES.has(base))
1497
+ return "integer";
1498
+ if (REAL_TYPES.has(base))
1499
+ return "real";
1500
+ if (TEXT_TYPES.has(base))
1501
+ return "text";
1502
+ if (base === "blob")
1503
+ return "blob";
1504
+ return "text";
1505
+ }
1506
+ function parseDefault(dfltValue) {
1507
+ if (dfltValue === null || dfltValue === undefined) {
1508
+ return { hasDefault: false };
1509
+ }
1510
+ const raw = String(dfltValue);
1511
+ if (raw.toUpperCase() === "NULL") {
1512
+ return { hasDefault: true, defaultValue: null };
1513
+ }
1514
+ if (/^-?\d+$/.test(raw)) {
1515
+ return { hasDefault: true, defaultValue: Number(raw) };
1516
+ }
1517
+ if (/^-?\d+\.\d+$/.test(raw)) {
1518
+ return { hasDefault: true, defaultValue: Number(raw) };
1519
+ }
1520
+ if (raw.startsWith("'") && raw.endsWith("'") || raw.startsWith('"') && raw.endsWith('"')) {
1521
+ return { hasDefault: true, defaultValue: raw.slice(1, -1) };
1522
+ }
1523
+ return { hasDefault: true, defaultValue: raw };
1524
+ }
1525
+ function introspectColumns(client, tableName) {
1526
+ const rows = client.query(`PRAGMA table_info('${tableName}')`).all();
1527
+ const fkRows = client.query(`PRAGMA foreign_key_list('${tableName}')`).all();
1528
+ const indexRows = client.query(`PRAGMA index_list('${tableName}')`).all();
1529
+ const fkMap = new Map;
1530
+ for (const fk of fkRows) {
1531
+ fkMap.set(fk.from, { referencesTable: fk.table, referencesColumn: fk.to });
1532
+ }
1533
+ const uniqueColumns = new Set;
1534
+ for (const idx of indexRows) {
1535
+ if (idx.unique === 1 && (idx.origin === "u" || idx.origin === "c")) {
1536
+ const colInfo = client.query(`PRAGMA index_info('${idx.name}')`).all();
1537
+ if (colInfo.length === 1) {
1538
+ uniqueColumns.add(colInfo[0].name);
1539
+ }
1540
+ }
1541
+ }
1542
+ const columns = rows.map((row) => {
1543
+ const fk = fkMap.get(row.name);
1544
+ const { hasDefault, defaultValue } = parseDefault(row.dflt_value);
1545
+ const col = {
1546
+ name: row.name,
1547
+ sqlType: normalizeType(row.type),
1548
+ isPrimaryKey: row.pk === 1,
1549
+ isNotNull: row.notnull === 1,
1550
+ isUnique: uniqueColumns.has(row.name),
1551
+ hasDefault,
1552
+ defaultValue
1553
+ };
1554
+ if (fk) {
1555
+ col.referencesTable = fk.referencesTable;
1556
+ col.referencesColumn = fk.referencesColumn;
1557
+ }
1558
+ return col;
1559
+ });
1560
+ return { columns, indexRows };
1561
+ }
1562
+ function introspectIndexes(client, indexRows) {
1563
+ const indexes = [];
1564
+ for (const idx of indexRows) {
1565
+ if (idx.name.startsWith("sqlite_autoindex_"))
1566
+ continue;
1567
+ const colRows = client.query(`PRAGMA index_info('${idx.name}')`).all();
1568
+ const columns = colRows.map((c) => c.name);
1569
+ indexes.push({
1570
+ name: idx.name,
1571
+ columns,
1572
+ unique: idx.unique === 1
1573
+ });
1574
+ }
1575
+ return indexes;
1576
+ }
1577
+ function introspect(client) {
1578
+ const tableRows = client.query(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' AND name != '__flint_migrations'`).all();
1579
+ const tables = {};
1580
+ for (const { name } of tableRows) {
1581
+ const { columns, indexRows } = introspectColumns(client, name);
1582
+ const indexes = introspectIndexes(client, indexRows);
1583
+ tables[name] = {
1584
+ name,
1585
+ columns,
1586
+ indexes
1587
+ };
1588
+ }
1589
+ return {
1590
+ version: 1,
1591
+ tables
1592
+ };
1593
+ }
1453
1594
  export {
1454
1595
  text,
1455
1596
  table,
@@ -1469,7 +1610,9 @@ export {
1469
1610
  isNotNull,
1470
1611
  isNotIn,
1471
1612
  isIn,
1613
+ introspect,
1472
1614
  integer,
1615
+ index,
1473
1616
  gte,
1474
1617
  gt,
1475
1618
  glob,
package/package.json CHANGED
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "name": "flint-orm",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
+ "bin": {
5
+ "flint": "./src/cli.ts"
6
+ },
4
7
  "files": [
5
8
  "dist",
6
9
  "API.md",
@@ -21,10 +24,18 @@
21
24
  "./expressions": {
22
25
  "import": "./dist/entries/expressions.js",
23
26
  "types": "./dist/entries/expressions.d.ts"
27
+ },
28
+ "./config": {
29
+ "import": "./dist/entries/config.js",
30
+ "types": "./dist/entries/config.d.ts"
31
+ },
32
+ "./migration": {
33
+ "import": "./dist/src/migration/index.js",
34
+ "types": "./dist/src/migration/index.d.ts"
24
35
  }
25
36
  },
26
37
  "scripts": {
27
- "build": "bun build ./src/index.ts ./src/entries/table.ts ./src/entries/expressions.ts --outdir ./dist --target bun && tsc --project tsconfig.build.json",
38
+ "build": "bun build ./src/index.ts ./src/entries/table.ts ./src/entries/expressions.ts ./src/entries/config.ts --outdir ./dist --target bun && tsc --project tsconfig.build.json",
28
39
  "typecheck": "tsc --noEmit",
29
40
  "lint": "oxlint",
30
41
  "format": "oxfmt"
package/src/cli.ts ADDED
@@ -0,0 +1,306 @@
1
+ #!/usr/bin/env bun
2
+ // ---------------------------------------------------------------------------
3
+ // flint CLI — migration generation for flint-orm
4
+ // ---------------------------------------------------------------------------
5
+
6
+ import { parseArgs } from 'node:util';
7
+ import { statSync, readdirSync, existsSync, readFileSync } from 'node:fs';
8
+ import { join, resolve, isAbsolute } from 'node:path';
9
+ import { pathToFileURL } from 'node:url';
10
+ import type { AnyTable, TableDef } from './schema/table.js';
11
+ import type { SchemaState } from './migration/types.js';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Config loading
15
+ // ---------------------------------------------------------------------------
16
+
17
+ interface FlintConfig {
18
+ /** Path to the SQLite database file. */
19
+ url: string;
20
+ /** Path to schema file or directory. */
21
+ schema: string;
22
+ /** Path to migrations directory (default: ./flint). */
23
+ migrations?: string;
24
+ }
25
+
26
+ async function loadConfig(): Promise<FlintConfig> {
27
+ const configPath = resolve(process.cwd(), 'flint.config.ts');
28
+ const configUrl = pathToFileURL(configPath).href;
29
+ const mod = await import(configUrl);
30
+ return mod.default as FlintConfig;
31
+ }
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Schema discovery — import table() exports from schema path
35
+ // ---------------------------------------------------------------------------
36
+
37
+ function isTableDef(value: unknown): boolean {
38
+ return (
39
+ value !== null &&
40
+ typeof value === 'object' &&
41
+ '_' in value &&
42
+ typeof (value as Record<string, unknown>)._ === 'object' &&
43
+ typeof ((value as Record<string, Record<string, unknown>>)._ as Record<string, unknown>).name === 'string'
44
+ );
45
+ }
46
+
47
+ async function discoverTables(schemaPath: string): Promise<unknown[]> {
48
+ const abs = isAbsolute(schemaPath) ? schemaPath : resolve(process.cwd(), schemaPath);
49
+ const stat = statSync(abs);
50
+
51
+ if (stat.isFile()) {
52
+ return importTableFile(abs);
53
+ }
54
+
55
+ if (stat.isDirectory()) {
56
+ return importTableFolder(abs);
57
+ }
58
+
59
+ throw new Error(`Schema path does not exist: ${abs}`);
60
+ }
61
+
62
+ async function importTableFile(filePath: string): Promise<unknown[]> {
63
+ const url = pathToFileURL(filePath).href;
64
+ const mod = await import(url);
65
+ const tables: unknown[] = [];
66
+
67
+ for (const exportValue of Object.values(mod)) {
68
+ if (isTableDef(exportValue)) {
69
+ tables.push(exportValue);
70
+ }
71
+ }
72
+
73
+ return tables;
74
+ }
75
+
76
+ async function importTableFolder(folderPath: string): Promise<unknown[]> {
77
+ const entries = readdirSync(folderPath).filter((e) => e.endsWith('.ts'));
78
+ const tables: unknown[] = [];
79
+
80
+ for (const entry of entries) {
81
+ const filePath = join(folderPath, entry);
82
+ const discovered = await importTableFile(filePath);
83
+ tables.push(...discovered);
84
+ }
85
+
86
+ return tables;
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Commands
91
+ // ---------------------------------------------------------------------------
92
+
93
+ async function cmdGenerate(args: ReturnType<typeof parseArgs>['values'], config: FlintConfig): Promise<void> {
94
+ const name = typeof args.name === 'string' ? args.name : undefined;
95
+ const preview = args.preview === true;
96
+
97
+ console.log(`🔍 Discovering schema from: ${config.schema}`);
98
+ const tables = await discoverTables(config.schema);
99
+
100
+ if (tables.length === 0) {
101
+ console.error('❌ No table() definitions found in schema path.');
102
+ process.exit(1);
103
+ }
104
+
105
+ console.log(`📦 Found ${tables.length} table(s):`);
106
+ for (const t of tables) {
107
+ console.log(` - ${(t as Record<string, Record<string, unknown>>)._?.name ?? 'unknown'}`);
108
+ }
109
+
110
+ if (preview) {
111
+ // Dynamic import of migration functions
112
+ const { serializeSchema } = await import('./migration/serialize.js');
113
+ const { diffSchemas, emptyState } = await import('./migration/diff.js');
114
+ const { generateSQL } = await import('./migration/sql.js');
115
+
116
+ // Find latest state
117
+ const migrationsDir = resolve(process.cwd(), config.migrations ?? './flint');
118
+ let previousState: SchemaState | null = null;
119
+ if (existsSync(migrationsDir)) {
120
+ const folders = readdirSync(migrationsDir)
121
+ .filter((e) => /^\d{10}_/.test(e))
122
+ .sort()
123
+ .reverse();
124
+ for (const folder of folders) {
125
+ const statePath = join(migrationsDir, folder, 'state.json');
126
+ if (existsSync(statePath)) {
127
+ previousState = JSON.parse(readFileSync(statePath, 'utf-8'));
128
+ break;
129
+ }
130
+ }
131
+ }
132
+ if (!previousState) previousState = emptyState();
133
+
134
+ const currentState = serializeSchema(tables as AnyTable[]);
135
+ const operations = diffSchemas(previousState, currentState);
136
+
137
+ if (operations.length === 0) {
138
+ console.log('\n✅ No changes detected — schema is already up to date.');
139
+ return;
140
+ }
141
+
142
+ const sql = generateSQL(operations);
143
+ const previewLabel = name ? `${name}` : 'unnamed';
144
+ console.log(`\n--- Preview: ${previewLabel} ---`);
145
+ console.log(` Operations: ${operations.length}`);
146
+ console.log(` Migrations dir: ${config.migrations}`);
147
+ console.log(`\n--- SQL ---\n${sql}\n`);
148
+ console.log('(dry run, no files written)');
149
+ return;
150
+ }
151
+
152
+ // Dynamic import of the generate function
153
+ const { generate } = await import('./migration/generate.js');
154
+
155
+ try {
156
+ const result = generate(tables as TableDef<any>[], resolve(process.cwd(), (config.migrations ?? './flint') as string), name);
157
+ console.log(`\n✅ Migration generated: ${result.folderName}`);
158
+ console.log(` Operations: ${result.operations.length}`);
159
+ console.log(`\n--- SQL Preview ---\n${result.sql}`);
160
+ } catch (err: unknown) {
161
+ if (err instanceof Error && err.message.includes('No changes detected')) {
162
+ console.log('\n✅ No changes detected — schema is already up to date.');
163
+ } else {
164
+ throw err;
165
+ }
166
+ }
167
+ }
168
+
169
+ async function cmdMigrate(args: ReturnType<typeof parseArgs>['values'], config: FlintConfig): Promise<void> {
170
+ const { Database } = await import('bun:sqlite');
171
+ const { migrate, getMigrationStatus } = await import('./migration/migrate.js');
172
+
173
+ const migrationsDir = resolve(process.cwd(), config.migrations ?? './flint');
174
+ const dryRun = args['dry-run'] === true;
175
+ const statusOnly = args.status === true;
176
+ const name = typeof args.name === 'string' ? args.name : undefined;
177
+
178
+ // Open database from config
179
+ const dbUrl = resolve(process.cwd(), config.url);
180
+ const db = new Database(dbUrl);
181
+
182
+ try {
183
+ if (statusOnly) {
184
+ const status = getMigrationStatus(db, migrationsDir);
185
+
186
+ if (status.applied.length === 0 && status.pending.length === 0) {
187
+ console.log('✅ No migrations found.');
188
+ return;
189
+ }
190
+
191
+ if (status.applied.length > 0) {
192
+ console.log('\n📋 Applied migrations:');
193
+ for (const m of status.applied) {
194
+ console.log(` ✓ ${m.name} (${m.folderName})`);
195
+ }
196
+ }
197
+
198
+ if (status.pending.length > 0) {
199
+ console.log('\n⏳ Pending migrations:');
200
+ for (const m of status.pending) {
201
+ console.log(` ○ ${m.name} (${m.folderName})`);
202
+ }
203
+ }
204
+
205
+ console.log(`\n ${status.applied.length} applied, ${status.pending.length} pending`);
206
+ return;
207
+ }
208
+
209
+ const result = await migrate(db, {
210
+ migrationsDir,
211
+ dryRun,
212
+ });
213
+
214
+ if (result.applied.length === 0) {
215
+ console.log('\n✅ No pending migrations — database is up to date.');
216
+ } else {
217
+ console.log(`\n🚀 ${dryRun ? 'Would apply' : 'Applied'} ${result.applied.length} migration(s):`);
218
+ for (const name of result.applied) {
219
+ console.log(` ✓ ${name}`);
220
+ }
221
+ }
222
+
223
+ if (result.skipped.length > 0 && !dryRun) {
224
+ console.log(`\n⏭ Skipped ${result.skipped.length} already applied migration(s)`);
225
+ }
226
+ } finally {
227
+ db.close();
228
+ }
229
+ }
230
+
231
+ // ---------------------------------------------------------------------------
232
+ // Help
233
+ // ---------------------------------------------------------------------------
234
+
235
+ function printHelp(): void {
236
+ console.log(`
237
+ flint — migration CLI for flint-orm
238
+
239
+ Usage:
240
+ flint <command> [options]
241
+
242
+ Commands:
243
+ generate Generate a new migration from schema changes
244
+ migrate Apply pending migrations to the database
245
+
246
+ Options for generate:
247
+ --name <name> Migration name (optional)
248
+ --preview Show what would be generated without writing files
249
+
250
+ Options for migrate:
251
+ --status Show applied and pending migrations
252
+ --dry-run Show what would be applied without executing
253
+ --name <name> Apply only the named migration
254
+
255
+ Examples:
256
+ flint generate --name init_schema
257
+ flint generate --preview
258
+ flint migrate
259
+ flint migrate --status
260
+ flint migrate --dry-run
261
+ `);
262
+ }
263
+
264
+ // ---------------------------------------------------------------------------
265
+ // Main
266
+ // ---------------------------------------------------------------------------
267
+
268
+ async function main(): Promise<void> {
269
+ const { values, positionals } = parseArgs({
270
+ args: process.argv.slice(2),
271
+ options: {
272
+ name: { type: 'string', short: 'n' },
273
+ preview: { type: 'boolean', short: 'p' },
274
+ help: { type: 'boolean', short: 'h' },
275
+ status: { type: 'boolean', short: 's' },
276
+ 'dry-run': { type: 'boolean', short: 'd' },
277
+ },
278
+ allowPositionals: true,
279
+ });
280
+
281
+ if (values.help || positionals.length === 0) {
282
+ printHelp();
283
+ process.exit(0);
284
+ }
285
+
286
+ const command = positionals[0];
287
+ const config = await loadConfig();
288
+
289
+ switch (command) {
290
+ case 'generate':
291
+ await cmdGenerate(values, config);
292
+ break;
293
+ case 'migrate':
294
+ await cmdMigrate(values, config);
295
+ break;
296
+ default:
297
+ console.error(`❌ Unknown command: ${command}`);
298
+ printHelp();
299
+ process.exit(1);
300
+ }
301
+ }
302
+
303
+ main().catch((err) => {
304
+ console.error(err);
305
+ process.exit(1);
306
+ });
@@ -1,5 +0,0 @@
1
- export { eq, and, or, gt, gte, lt, lte, neq, isIn, isNotIn, isNull, isNotNull, like, glob, between } from '../query/conditions';
2
- export type { Condition } from '../query/conditions';
3
- export { sql } from '../flint';
4
- export type { SQLExpression } from '../flint';
5
- export { count, countColumn, sum, avg, min, max } from '../query/aggregates';