@type32/tauri-sqlite-orm 0.3.0 → 0.4.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/aggregates.d.ts +12 -0
- package/dist/aggregates.js +9 -0
- package/dist/builders/delete.d.ts +23 -0
- package/dist/builders/delete.js +73 -0
- package/dist/builders/index.d.ts +7 -0
- package/dist/builders/index.js +7 -0
- package/dist/builders/insert.d.ts +31 -0
- package/dist/builders/insert.js +141 -0
- package/dist/builders/query-base.d.ts +1 -0
- package/dist/builders/query-base.js +1 -0
- package/dist/builders/relations.d.ts +11 -0
- package/dist/builders/relations.js +1 -0
- package/dist/builders/select.d.ts +54 -0
- package/dist/builders/select.js +427 -0
- package/dist/builders/update.d.ts +30 -0
- package/dist/builders/update.js +124 -0
- package/dist/builders/with.d.ts +17 -0
- package/dist/builders/with.js +34 -0
- package/dist/column-helpers.d.ts +22 -0
- package/dist/column-helpers.js +17 -0
- package/dist/dialect.d.ts +21 -0
- package/dist/dialect.js +67 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.js +66 -0
- package/dist/index.d.mts +11 -6
- package/dist/index.d.ts +11 -6
- package/dist/operators.d.ts +30 -0
- package/dist/operators.js +84 -0
- package/dist/orm.d.ts +180 -0
- package/dist/orm.js +556 -0
- package/dist/relational-types.d.ts +87 -0
- package/dist/relational-types.js +1 -0
- package/dist/relations-v2.d.ts +77 -0
- package/dist/relations-v2.js +157 -0
- package/dist/serialization.d.ts +14 -0
- package/dist/serialization.js +135 -0
- package/dist/subquery.d.ts +5 -0
- package/dist/subquery.js +6 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.js +1 -0
- package/package.json +2 -1
package/dist/orm.js
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
import { Kysely, sql as kyselySql } from 'kysely';
|
|
2
|
+
import { TauriDialect } from './dialect';
|
|
3
|
+
import { SelectQueryBuilder, InsertQueryBuilder, UpdateQueryBuilder, DeleteQueryBuilder, WithQueryBuilder, } from './builders';
|
|
4
|
+
// Column class
|
|
5
|
+
export class SQLiteColumn {
|
|
6
|
+
type;
|
|
7
|
+
options;
|
|
8
|
+
_;
|
|
9
|
+
constructor(name, type, options = {}) {
|
|
10
|
+
this.type = type;
|
|
11
|
+
this.options = options;
|
|
12
|
+
this._ = {
|
|
13
|
+
name,
|
|
14
|
+
dataType: type,
|
|
15
|
+
mode: (options.mode || 'default'),
|
|
16
|
+
notNull: (options.notNull ?? false),
|
|
17
|
+
hasDefault: (options.default !== undefined || options.$defaultFn !== undefined),
|
|
18
|
+
autoincrement: (options.autoincrement ?? false),
|
|
19
|
+
enum: options.enum,
|
|
20
|
+
customType: undefined,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
notNull() {
|
|
24
|
+
return new SQLiteColumn(this._.name, this.type, { ...this.options, notNull: true, mode: this._.mode });
|
|
25
|
+
}
|
|
26
|
+
default(value) {
|
|
27
|
+
return new SQLiteColumn(this._.name, this.type, { ...this.options, default: value, mode: this._.mode });
|
|
28
|
+
}
|
|
29
|
+
$defaultFn(fn) {
|
|
30
|
+
return new SQLiteColumn(this._.name, this.type, { ...this.options, $defaultFn: fn, mode: this._.mode });
|
|
31
|
+
}
|
|
32
|
+
primaryKey() {
|
|
33
|
+
return new SQLiteColumn(this._.name, this.type, { ...this.options, primaryKey: true, notNull: true, mode: this._.mode });
|
|
34
|
+
}
|
|
35
|
+
autoincrement() {
|
|
36
|
+
return new SQLiteColumn(this._.name, this.type, { ...this.options, autoincrement: true, mode: this._.mode });
|
|
37
|
+
}
|
|
38
|
+
unique() {
|
|
39
|
+
return new SQLiteColumn(this._.name, this.type, { ...this.options, unique: true, mode: this._.mode });
|
|
40
|
+
}
|
|
41
|
+
references(ref, column, options) {
|
|
42
|
+
const columnKey = typeof column === 'string' ? column : column._.name;
|
|
43
|
+
const columnObj = typeof column === 'string' ? ref._.columns[column] : column;
|
|
44
|
+
return new SQLiteColumn(this._.name, this.type, {
|
|
45
|
+
...this.options,
|
|
46
|
+
references: {
|
|
47
|
+
table: ref,
|
|
48
|
+
column: columnObj,
|
|
49
|
+
onDelete: options?.onDelete,
|
|
50
|
+
onUpdate: options?.onUpdate,
|
|
51
|
+
},
|
|
52
|
+
mode: this._.mode
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
$onUpdateFn(fn) {
|
|
56
|
+
return new SQLiteColumn(this._.name, this.type, { ...this.options, $onUpdateFn: fn, mode: this._.mode });
|
|
57
|
+
}
|
|
58
|
+
$type() {
|
|
59
|
+
return this;
|
|
60
|
+
}
|
|
61
|
+
as(alias) {
|
|
62
|
+
// This is a placeholder for alias functionality
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export class Table {
|
|
67
|
+
_;
|
|
68
|
+
relations = {};
|
|
69
|
+
constructor(name, columns) {
|
|
70
|
+
this._ = {
|
|
71
|
+
name,
|
|
72
|
+
columns,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export const sqliteTable = (tableName, columns) => {
|
|
77
|
+
return new Table(tableName, columns);
|
|
78
|
+
};
|
|
79
|
+
export const asc = (column) => kyselySql `${kyselySql.ref(column._.name)} ASC`;
|
|
80
|
+
export const desc = (column) => kyselySql `${kyselySql.ref(column._.name)} DESC`;
|
|
81
|
+
// Main ORM Class
|
|
82
|
+
export class TauriORM {
|
|
83
|
+
db;
|
|
84
|
+
tables = new Map();
|
|
85
|
+
kysely;
|
|
86
|
+
constructor(db, schema = undefined) {
|
|
87
|
+
this.db = db;
|
|
88
|
+
this.kysely = new Kysely({ dialect: new TauriDialect(db) });
|
|
89
|
+
if (schema) {
|
|
90
|
+
for (const [, value] of Object.entries(schema)) {
|
|
91
|
+
if (value instanceof Table) {
|
|
92
|
+
this.tables.set(value._.name, value);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
buildColumnDefinition(col, forAlterTable = false) {
|
|
98
|
+
let sql = `${col._.name} ${col.type}`;
|
|
99
|
+
if (col.options.primaryKey && !forAlterTable) {
|
|
100
|
+
sql += ' PRIMARY KEY';
|
|
101
|
+
if (col._.autoincrement) {
|
|
102
|
+
sql += ' AUTOINCREMENT';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (col._.notNull)
|
|
106
|
+
sql += ' NOT NULL';
|
|
107
|
+
if (col.options.unique)
|
|
108
|
+
sql += ' UNIQUE';
|
|
109
|
+
if (col.options.default !== undefined) {
|
|
110
|
+
const value = col.options.default;
|
|
111
|
+
sql += ` DEFAULT ${typeof value === 'string' ? `'${value.replace(/'/g, "''")}'` : value}`;
|
|
112
|
+
}
|
|
113
|
+
if (col.options.references) {
|
|
114
|
+
sql += ` REFERENCES ${col.options.references.table._.name}(${col.options.references.column._.name})`;
|
|
115
|
+
if (col.options.references.onDelete) {
|
|
116
|
+
sql += ` ON DELETE ${col.options.references.onDelete.toUpperCase()}`;
|
|
117
|
+
}
|
|
118
|
+
if (col.options.references.onUpdate) {
|
|
119
|
+
sql += ` ON UPDATE ${col.options.references.onUpdate.toUpperCase()}`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return sql;
|
|
123
|
+
}
|
|
124
|
+
async checkMigration() {
|
|
125
|
+
const dbTables = await this.db.select(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`);
|
|
126
|
+
const dbTableNames = new Set(dbTables.map((t) => t.name));
|
|
127
|
+
const schemaTableNames = new Set(Array.from(this.tables.keys()));
|
|
128
|
+
const changes = {
|
|
129
|
+
tablesToCreate: [],
|
|
130
|
+
tablesToRecreate: [],
|
|
131
|
+
tablesToDrop: [],
|
|
132
|
+
columnsToAdd: [],
|
|
133
|
+
};
|
|
134
|
+
// Check tables to create
|
|
135
|
+
for (const tableName of schemaTableNames) {
|
|
136
|
+
if (!dbTableNames.has(tableName)) {
|
|
137
|
+
changes.tablesToCreate.push(tableName);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Check tables to drop
|
|
141
|
+
for (const tableName of dbTableNames) {
|
|
142
|
+
if (!schemaTableNames.has(tableName)) {
|
|
143
|
+
changes.tablesToDrop.push(tableName);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// Check for table recreation and column additions
|
|
147
|
+
for (const table of this.tables.values()) {
|
|
148
|
+
const tableName = table._.name;
|
|
149
|
+
if (!dbTableNames.has(tableName))
|
|
150
|
+
continue;
|
|
151
|
+
const existingTableInfo = await this.db.select(`PRAGMA table_info('${tableName}')`);
|
|
152
|
+
const existingIndexes = await this.db.select(`PRAGMA index_list('${tableName}')`);
|
|
153
|
+
const uniqueColumns = new Set();
|
|
154
|
+
for (const index of existingIndexes) {
|
|
155
|
+
if (index.unique === 1 && index.origin === 'u') {
|
|
156
|
+
const indexInfo = await this.db.select(`PRAGMA index_info('${index.name}')`);
|
|
157
|
+
if (indexInfo.length === 1) {
|
|
158
|
+
uniqueColumns.add(indexInfo[0].name);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const existingColumns = new Map(existingTableInfo.map(c => [c.name, c]));
|
|
163
|
+
const schemaColumns = table._.columns;
|
|
164
|
+
let needsRecreate = false;
|
|
165
|
+
for (const [colName, column] of Object.entries(schemaColumns)) {
|
|
166
|
+
const dbColName = column._.name;
|
|
167
|
+
const existing = existingColumns.get(dbColName);
|
|
168
|
+
if (!existing) {
|
|
169
|
+
if (!this.canAddColumnWithAlter(column)) {
|
|
170
|
+
needsRecreate = true;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
changes.columnsToAdd.push({ table: tableName, column: dbColName });
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
const hasUniqueInDB = uniqueColumns.has(dbColName);
|
|
177
|
+
const wantsUnique = !!column.options.unique;
|
|
178
|
+
if (hasUniqueInDB !== wantsUnique || this.hasColumnDefinitionChanged(column, existing)) {
|
|
179
|
+
needsRecreate = true;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Check for removed columns (existingCol is DB name)
|
|
185
|
+
for (const existingCol of existingColumns.keys()) {
|
|
186
|
+
const schemaHasCol = Object.values(schemaColumns).some((c) => c._.name === existingCol);
|
|
187
|
+
if (!schemaHasCol) {
|
|
188
|
+
needsRecreate = true;
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (needsRecreate) {
|
|
193
|
+
changes.tablesToRecreate.push(tableName);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const safe = changes.tablesToRecreate.length === 0 && changes.tablesToDrop.length === 0;
|
|
197
|
+
return { safe, changes };
|
|
198
|
+
}
|
|
199
|
+
async migrate(options) {
|
|
200
|
+
if (options?.dryRun) {
|
|
201
|
+
const check = await this.checkMigration();
|
|
202
|
+
console.log('[Tauri-ORM] Migration Preview (Dry Run):');
|
|
203
|
+
console.log(' Tables to create:', check.changes.tablesToCreate);
|
|
204
|
+
console.log(' Tables to recreate (DESTRUCTIVE):', check.changes.tablesToRecreate);
|
|
205
|
+
console.log(' Tables to drop:', check.changes.tablesToDrop);
|
|
206
|
+
console.log(' Columns to add:', check.changes.columnsToAdd);
|
|
207
|
+
console.log(' Safe migration:', check.safe);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const dbTables = await this.db.select(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`);
|
|
211
|
+
const dbTableNames = new Set(dbTables.map((t) => t.name));
|
|
212
|
+
const schemaTableNames = new Set(Array.from(this.tables.keys()));
|
|
213
|
+
// Create/update tables
|
|
214
|
+
for (const table of this.tables.values()) {
|
|
215
|
+
const tableName = table._.name;
|
|
216
|
+
const tableExists = dbTableNames.has(tableName);
|
|
217
|
+
if (!tableExists) {
|
|
218
|
+
// Table does not exist, create it
|
|
219
|
+
const columnsSql = Object.values(table._.columns)
|
|
220
|
+
.map((col) => this.buildColumnDefinition(col))
|
|
221
|
+
.join(', ');
|
|
222
|
+
const createSql = `CREATE TABLE ${tableName} (${columnsSql})`;
|
|
223
|
+
await this.db.execute(createSql);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
// Table exists, check for schema changes
|
|
227
|
+
const existingTableInfo = await this.db.select(`PRAGMA table_info('${tableName}')`);
|
|
228
|
+
// Get existing UNIQUE constraints from indexes
|
|
229
|
+
const existingIndexes = await this.db.select(`PRAGMA index_list('${tableName}')`);
|
|
230
|
+
const uniqueColumns = new Set();
|
|
231
|
+
for (const index of existingIndexes) {
|
|
232
|
+
if (index.unique === 1 && index.origin === 'u') {
|
|
233
|
+
const indexInfo = await this.db.select(`PRAGMA index_info('${index.name}')`);
|
|
234
|
+
if (indexInfo.length === 1) {
|
|
235
|
+
uniqueColumns.add(indexInfo[0].name);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const existingColumns = new Map(existingTableInfo.map(c => [c.name, c]));
|
|
240
|
+
const schemaColumns = table._.columns;
|
|
241
|
+
// Check if we need to recreate the table (column definition changes)
|
|
242
|
+
let needsRecreate = false;
|
|
243
|
+
const columnsToAdd = [];
|
|
244
|
+
for (const [colName, column] of Object.entries(schemaColumns)) {
|
|
245
|
+
const dbColName = column._.name;
|
|
246
|
+
const existing = existingColumns.get(dbColName);
|
|
247
|
+
if (!existing) {
|
|
248
|
+
// New column - check if it can be added with ALTER TABLE
|
|
249
|
+
if (this.canAddColumnWithAlter(column)) {
|
|
250
|
+
columnsToAdd.push(column);
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
needsRecreate = true;
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
// Existing column - check if definition changed
|
|
259
|
+
const hasUniqueInDB = uniqueColumns.has(dbColName);
|
|
260
|
+
const wantsUnique = !!column.options.unique;
|
|
261
|
+
if (hasUniqueInDB !== wantsUnique) {
|
|
262
|
+
needsRecreate = true;
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
265
|
+
if (this.hasColumnDefinitionChanged(column, existing)) {
|
|
266
|
+
needsRecreate = true;
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// Check for removed columns (existingCol is DB name)
|
|
272
|
+
if (options?.performDestructiveActions) {
|
|
273
|
+
for (const existingCol of existingColumns.keys()) {
|
|
274
|
+
const schemaHasCol = Object.values(schemaColumns).some((c) => c._.name === existingCol);
|
|
275
|
+
if (!schemaHasCol) {
|
|
276
|
+
needsRecreate = true;
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (needsRecreate) {
|
|
282
|
+
// Recreate table with new schema
|
|
283
|
+
await this.recreateTable(tableName, table);
|
|
284
|
+
}
|
|
285
|
+
else if (columnsToAdd.length > 0) {
|
|
286
|
+
// Just add new columns with ALTER TABLE
|
|
287
|
+
for (const column of columnsToAdd) {
|
|
288
|
+
const columnSql = this.buildColumnDefinition(column, true);
|
|
289
|
+
await this.db.execute(`ALTER TABLE ${tableName} ADD COLUMN ${columnSql}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// Drop extra tables if destructive actions are enabled
|
|
295
|
+
if (options?.performDestructiveActions) {
|
|
296
|
+
for (const tableName of dbTableNames) {
|
|
297
|
+
if (!schemaTableNames.has(tableName)) {
|
|
298
|
+
await this.dropTable(tableName);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
canAddColumnWithAlter(column) {
|
|
304
|
+
// SQLite ALTER TABLE ADD COLUMN has limitations:
|
|
305
|
+
// - Cannot add PRIMARY KEY
|
|
306
|
+
// - Cannot add UNIQUE (without using a workaround)
|
|
307
|
+
// - Can add NOT NULL only if column has a DEFAULT value
|
|
308
|
+
if (column.options.primaryKey)
|
|
309
|
+
return false;
|
|
310
|
+
if (column.options.unique)
|
|
311
|
+
return false;
|
|
312
|
+
if (column._.notNull && column.options.default === undefined && !column.options.$defaultFn)
|
|
313
|
+
return false;
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
hasColumnDefinitionChanged(column, existing) {
|
|
317
|
+
// Check if column type changed (normalize to uppercase)
|
|
318
|
+
if (column.type.toUpperCase() !== existing.type.toUpperCase())
|
|
319
|
+
return true;
|
|
320
|
+
// Check if NOT NULL changed
|
|
321
|
+
if (column._.notNull !== (existing.notnull === 1))
|
|
322
|
+
return true;
|
|
323
|
+
// Check if PRIMARY KEY changed
|
|
324
|
+
if (!!column.options.primaryKey !== (existing.pk === 1))
|
|
325
|
+
return true;
|
|
326
|
+
// Check if default value changed
|
|
327
|
+
const hasDefault = column.options.default !== undefined;
|
|
328
|
+
const existingHasDefault = existing.dflt_value !== null;
|
|
329
|
+
if (hasDefault !== existingHasDefault)
|
|
330
|
+
return true;
|
|
331
|
+
// Check UNIQUE constraint (requires checking indexes)
|
|
332
|
+
// For now, we'll check UNIQUE separately if needed
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
async recreateTable(tableName, table) {
|
|
336
|
+
const tempTableName = `${tableName}_new_${Date.now()}`;
|
|
337
|
+
// Create new table with updated schema
|
|
338
|
+
const columnsSql = Object.values(table._.columns)
|
|
339
|
+
.map((col) => this.buildColumnDefinition(col))
|
|
340
|
+
.join(', ');
|
|
341
|
+
await this.db.execute(`CREATE TABLE ${tempTableName} (${columnsSql})`);
|
|
342
|
+
// Copy data from old table (only columns that exist in both)
|
|
343
|
+
const oldColumns = await this.db.select(`PRAGMA table_info('${tableName}')`);
|
|
344
|
+
const oldColumnNames = oldColumns.map(c => c.name);
|
|
345
|
+
const newColumnNames = Object.values(table._.columns).map(c => c._.name);
|
|
346
|
+
const commonColumns = oldColumnNames.filter(name => newColumnNames.includes(name));
|
|
347
|
+
if (commonColumns.length > 0) {
|
|
348
|
+
const columnsList = commonColumns.join(', ');
|
|
349
|
+
await this.db.execute(`INSERT INTO ${tempTableName} (${columnsList}) SELECT ${columnsList} FROM ${tableName}`);
|
|
350
|
+
}
|
|
351
|
+
// Drop old table and rename new table
|
|
352
|
+
await this.db.execute(`DROP TABLE ${tableName}`);
|
|
353
|
+
await this.db.execute(`ALTER TABLE ${tempTableName} RENAME TO ${tableName}`);
|
|
354
|
+
}
|
|
355
|
+
select(table, columns) {
|
|
356
|
+
const internalTable = this.tables.get(table._.name);
|
|
357
|
+
if (!internalTable) {
|
|
358
|
+
console.warn(`[Tauri-ORM] Table "${table._.name}" was not passed in the schema to the ORM constructor. Relations will not be available.`);
|
|
359
|
+
return new SelectQueryBuilder(this.kysely, table, columns);
|
|
360
|
+
}
|
|
361
|
+
return new SelectQueryBuilder(this.kysely, internalTable, columns);
|
|
362
|
+
}
|
|
363
|
+
insert(table) {
|
|
364
|
+
return new InsertQueryBuilder(this.kysely, table);
|
|
365
|
+
}
|
|
366
|
+
update(table) {
|
|
367
|
+
return new UpdateQueryBuilder(this.kysely, table);
|
|
368
|
+
}
|
|
369
|
+
delete(table) {
|
|
370
|
+
return new DeleteQueryBuilder(this.kysely, table);
|
|
371
|
+
}
|
|
372
|
+
async upsert(table, data, conflictTarget) {
|
|
373
|
+
const columns = conflictTarget.map(col => table._.columns[col]);
|
|
374
|
+
return this.insert(table)
|
|
375
|
+
.values(data)
|
|
376
|
+
.onConflictDoUpdate({
|
|
377
|
+
target: columns.length === 1 ? columns[0] : columns,
|
|
378
|
+
set: data
|
|
379
|
+
})
|
|
380
|
+
.execute();
|
|
381
|
+
}
|
|
382
|
+
$with(alias) {
|
|
383
|
+
const withBuilder = new WithQueryBuilder(this.kysely);
|
|
384
|
+
return {
|
|
385
|
+
as: (query) => {
|
|
386
|
+
withBuilder.with(alias, query);
|
|
387
|
+
return withBuilder;
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
async transaction(callback) {
|
|
392
|
+
await this.db.execute('BEGIN');
|
|
393
|
+
try {
|
|
394
|
+
const result = await callback(this);
|
|
395
|
+
await this.db.execute('COMMIT');
|
|
396
|
+
return result;
|
|
397
|
+
}
|
|
398
|
+
catch (e) {
|
|
399
|
+
await this.db.execute('ROLLBACK');
|
|
400
|
+
throw e;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
rollback() {
|
|
404
|
+
throw new Error('Transaction rolled back');
|
|
405
|
+
}
|
|
406
|
+
// --- Schema detection / signature ---
|
|
407
|
+
async ensureSchemaMeta() {
|
|
408
|
+
await this.db.execute(`CREATE TABLE IF NOT EXISTS _schema_meta
|
|
409
|
+
(
|
|
410
|
+
key
|
|
411
|
+
TEXT
|
|
412
|
+
PRIMARY
|
|
413
|
+
KEY,
|
|
414
|
+
value
|
|
415
|
+
TEXT
|
|
416
|
+
NOT
|
|
417
|
+
NULL
|
|
418
|
+
)`);
|
|
419
|
+
}
|
|
420
|
+
async getSchemaMeta(key) {
|
|
421
|
+
await this.ensureSchemaMeta();
|
|
422
|
+
const rows = await this.db.select(`SELECT value
|
|
423
|
+
FROM _schema_meta
|
|
424
|
+
WHERE key = ?`, [key]);
|
|
425
|
+
return rows?.[0]?.value ?? null;
|
|
426
|
+
}
|
|
427
|
+
async setSchemaMeta(key, value) {
|
|
428
|
+
await this.ensureSchemaMeta();
|
|
429
|
+
await this.db.execute(`INSERT INTO _schema_meta(key, value)
|
|
430
|
+
VALUES (?, ?) ON CONFLICT(key) DO
|
|
431
|
+
UPDATE
|
|
432
|
+
SET value = excluded.value`, [key, value]);
|
|
433
|
+
}
|
|
434
|
+
normalizeColumn(col) {
|
|
435
|
+
return {
|
|
436
|
+
name: col._.name,
|
|
437
|
+
type: col.type,
|
|
438
|
+
pk: !!col.options.primaryKey,
|
|
439
|
+
ai: !!col._.autoincrement,
|
|
440
|
+
nn: !!col._.notNull,
|
|
441
|
+
unique: !!col.options.unique,
|
|
442
|
+
dv: col.options.default && typeof col.options.default === 'object' && col.options.default.raw
|
|
443
|
+
? { raw: col.options.default.raw }
|
|
444
|
+
: col.options.default ?? null,
|
|
445
|
+
hasDefaultFn: col.options.$defaultFn !== undefined,
|
|
446
|
+
hasOnUpdateFn: col.options.$onUpdateFn !== undefined,
|
|
447
|
+
onDelete: col.options.references?.onDelete ?? null,
|
|
448
|
+
onUpdate: col.options.references?.onUpdate ?? null,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
computeModelSignature() {
|
|
452
|
+
const entries = Array.from(this.tables.values()).map((tbl) => {
|
|
453
|
+
const cols = Object.values(tbl._.columns)
|
|
454
|
+
.map((c) => this.normalizeColumn(c))
|
|
455
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
456
|
+
return { table: tbl._.name, columns: cols };
|
|
457
|
+
});
|
|
458
|
+
entries.sort((a, b) => a.table.localeCompare(b.table));
|
|
459
|
+
return JSON.stringify(entries);
|
|
460
|
+
}
|
|
461
|
+
getSchemaSignature() {
|
|
462
|
+
return this.computeModelSignature();
|
|
463
|
+
}
|
|
464
|
+
async isSchemaDirty() {
|
|
465
|
+
const sig = this.computeModelSignature();
|
|
466
|
+
const stored = await this.getSchemaMeta('schema_signature');
|
|
467
|
+
return { dirty: sig !== stored, current: sig, stored };
|
|
468
|
+
}
|
|
469
|
+
async migrateIfDirty() {
|
|
470
|
+
const status = await this.isSchemaDirty();
|
|
471
|
+
if (status.dirty) {
|
|
472
|
+
await this.migrate();
|
|
473
|
+
await this.setSchemaMeta('schema_signature', this.computeModelSignature());
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
async doesTableExist(tableName) {
|
|
479
|
+
const result = await this.db.select(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, [tableName]);
|
|
480
|
+
return result.length > 0;
|
|
481
|
+
}
|
|
482
|
+
async dropTable(tableName) {
|
|
483
|
+
await this.db.execute(`DROP TABLE IF EXISTS ${tableName}`);
|
|
484
|
+
}
|
|
485
|
+
async doesColumnExist(tableName, columnName) {
|
|
486
|
+
const result = await this.db.select(`PRAGMA table_info('${tableName}')`);
|
|
487
|
+
return result.some((col) => col.name === columnName);
|
|
488
|
+
}
|
|
489
|
+
async renameTable(from, to) {
|
|
490
|
+
await this.db.execute(`ALTER TABLE ${from} RENAME TO ${to}`);
|
|
491
|
+
}
|
|
492
|
+
async dropColumn(tableName, columnName) {
|
|
493
|
+
await this.db.execute(`ALTER TABLE ${tableName} DROP COLUMN ${columnName}`);
|
|
494
|
+
}
|
|
495
|
+
async renameColumn(tableName, from, to) {
|
|
496
|
+
await this.db.execute(`ALTER TABLE ${tableName} RENAME COLUMN ${from} TO ${to}`);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
// Relations
|
|
500
|
+
export class Relation {
|
|
501
|
+
foreignTable;
|
|
502
|
+
constructor(foreignTable) {
|
|
503
|
+
this.foreignTable = foreignTable;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
export class OneRelation extends Relation {
|
|
507
|
+
config;
|
|
508
|
+
constructor(foreignTable, config) {
|
|
509
|
+
super(foreignTable);
|
|
510
|
+
this.config = config;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
export class ManyRelation extends Relation {
|
|
514
|
+
config;
|
|
515
|
+
constructor(foreignTable, config) {
|
|
516
|
+
super(foreignTable);
|
|
517
|
+
this.config = config;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
export const relations = (table, relationsCallback) => {
|
|
521
|
+
const builtRelations = relationsCallback({
|
|
522
|
+
one: (foreignTable, config) => {
|
|
523
|
+
return new OneRelation(foreignTable, config);
|
|
524
|
+
},
|
|
525
|
+
many: (foreignTable) => {
|
|
526
|
+
return new ManyRelation(foreignTable);
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
for (const [name, relation] of Object.entries(builtRelations)) {
|
|
530
|
+
if (relation instanceof OneRelation) {
|
|
531
|
+
table.relations[name] = {
|
|
532
|
+
type: 'one',
|
|
533
|
+
foreignTable: relation.foreignTable,
|
|
534
|
+
fields: relation.config?.fields,
|
|
535
|
+
references: relation.config?.references,
|
|
536
|
+
optional: relation.config?.optional,
|
|
537
|
+
alias: relation.config?.alias,
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
else if (relation instanceof ManyRelation) {
|
|
541
|
+
table.relations[name] = {
|
|
542
|
+
type: 'many',
|
|
543
|
+
foreignTable: relation.foreignTable,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return builtRelations;
|
|
548
|
+
};
|
|
549
|
+
// Helper functions
|
|
550
|
+
export const getTableColumns = (table) => {
|
|
551
|
+
return table._.columns;
|
|
552
|
+
};
|
|
553
|
+
export const alias = (table, alias) => {
|
|
554
|
+
// This is a placeholder for alias functionality
|
|
555
|
+
return table;
|
|
556
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { AnyTable, InferSelectModel } from './types';
|
|
2
|
+
import type { OneRelation, ManyRelation } from './orm';
|
|
3
|
+
/** Maps relations() return type to typed relation configs with foreign table preserved */
|
|
4
|
+
export type InferRelationsMap<R extends Record<string, OneRelation | ManyRelation>> = {
|
|
5
|
+
[K in keyof R]: R[K] extends OneRelation<infer T> ? R[K] extends {
|
|
6
|
+
config?: {
|
|
7
|
+
optional?: infer O;
|
|
8
|
+
};
|
|
9
|
+
} ? {
|
|
10
|
+
type: 'one';
|
|
11
|
+
foreignTable: T;
|
|
12
|
+
optional: O;
|
|
13
|
+
} : {
|
|
14
|
+
type: 'one';
|
|
15
|
+
foreignTable: T;
|
|
16
|
+
optional?: true;
|
|
17
|
+
} : R[K] extends ManyRelation<infer T> ? {
|
|
18
|
+
type: 'many';
|
|
19
|
+
foreignTable: T;
|
|
20
|
+
} : never;
|
|
21
|
+
};
|
|
22
|
+
/** With/include object shape - matches NestedInclude from select builder */
|
|
23
|
+
type WithShape = boolean | {
|
|
24
|
+
columns?: string[] | Record<string, boolean>;
|
|
25
|
+
with?: Record<string, WithShape>;
|
|
26
|
+
};
|
|
27
|
+
/** Map of table name -> relations() return type, for nested with support */
|
|
28
|
+
type RelationsByTable = Record<string, Record<string, OneRelation | ManyRelation>>;
|
|
29
|
+
/** Get relations for a table from the all-relations map */
|
|
30
|
+
type GetRelationsForTable<TTable extends AnyTable, TAllRelations extends RelationsByTable> = TAllRelations[TTable['_']['name']] extends Record<string, OneRelation | ManyRelation> ? InferRelationsMap<TAllRelations[TTable['_']['name']]> : Record<string, never>;
|
|
31
|
+
/** Infer nested relation fields when TWith has a nested `with` */
|
|
32
|
+
type InferNestedFields<TForeignTable extends AnyTable, TWith extends WithShape, TAllRelations extends RelationsByTable> = TWith extends {
|
|
33
|
+
with?: infer TW;
|
|
34
|
+
} ? TW extends Record<string, WithShape> ? InferRelationFields<TForeignTable, GetRelationsForTable<TForeignTable, TAllRelations>, TAllRelations, TW> : unknown : unknown;
|
|
35
|
+
/** Recursively build relation fields from a with object */
|
|
36
|
+
type InferRelationFields<TTable extends AnyTable, TRelationsMap extends Record<string, {
|
|
37
|
+
type: 'one' | 'many';
|
|
38
|
+
foreignTable: AnyTable;
|
|
39
|
+
}>, TAllRelations extends RelationsByTable, TWith extends Record<string, WithShape>> = {
|
|
40
|
+
[K in keyof TWith & keyof TRelationsMap]: TWith[K] extends false | undefined ? never : TRelationsMap[K] extends {
|
|
41
|
+
type: 'one';
|
|
42
|
+
foreignTable: infer T;
|
|
43
|
+
optional?: infer O;
|
|
44
|
+
} ? T extends AnyTable ? ([O] extends [false] ? InferSelectModel<T> & InferNestedFields<T, TWith[K], TAllRelations> : (InferSelectModel<T> & InferNestedFields<T, TWith[K], TAllRelations>) | null) : never : TRelationsMap[K] extends {
|
|
45
|
+
type: 'many';
|
|
46
|
+
foreignTable: infer T;
|
|
47
|
+
} ? T extends AnyTable ? (InferSelectModel<T> & InferNestedFields<T, TWith[K], TAllRelations>)[] : never : never;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Infer the result type of a select query that includes relations via `.include(with)`.
|
|
51
|
+
*
|
|
52
|
+
* Use this to type variables that hold results from relational queries, e.g.:
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* export type User = InferSelectModel<typeof schema.user>
|
|
57
|
+
*
|
|
58
|
+
* const withRelationalObject = { sessions: true, accounts: true } as const
|
|
59
|
+
* export type UserWithRelations = InferRelationalSelectModel<
|
|
60
|
+
* typeof schema.user,
|
|
61
|
+
* typeof schema.userRelations,
|
|
62
|
+
* typeof withRelationalObject
|
|
63
|
+
* >
|
|
64
|
+
* ```
|
|
65
|
+
*
|
|
66
|
+
* For nested includes, pass a map of table name -> relations as the fourth parameter:
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```ts
|
|
70
|
+
* const withNested = { sessions: { with: { user: true } } } as const
|
|
71
|
+
* type UserWithSessionsAndUser = InferRelationalSelectModel<
|
|
72
|
+
* typeof schema.user,
|
|
73
|
+
* typeof schema.userRelations,
|
|
74
|
+
* typeof withNested,
|
|
75
|
+
* { user: typeof userRelations; session: typeof sessionRelations }
|
|
76
|
+
* >
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* @param TTable - The table type (e.g. typeof schema.user)
|
|
80
|
+
* @param TRelations - The relations for this table (e.g. typeof schema.userRelations)
|
|
81
|
+
* @param TWith - The with/include object shape (use `as const` for literal inference)
|
|
82
|
+
* @param TAllRelations - Optional map of table name -> relations for nested `with` support
|
|
83
|
+
*/
|
|
84
|
+
export type InferRelationalSelectModel<TTable extends AnyTable, TRelations extends Record<string, OneRelation | ManyRelation>, TWith extends Record<string, WithShape>, TAllRelations extends RelationsByTable = {
|
|
85
|
+
[K in TTable['_']['name']]: TRelations;
|
|
86
|
+
}> = InferSelectModel<TTable> & InferRelationFields<TTable, InferRelationsMap<TRelations>, TAllRelations, TWith>;
|
|
87
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|