@gobing-ai/ts-db 0.1.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/README.md +326 -0
- package/dist/adapter.d.ts +86 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +18 -0
- package/dist/adapters/bun-sqlite.d.ts +40 -0
- package/dist/adapters/bun-sqlite.d.ts.map +1 -0
- package/dist/adapters/bun-sqlite.js +70 -0
- package/dist/adapters/d1.d.ts +48 -0
- package/dist/adapters/d1.d.ts.map +1 -0
- package/dist/adapters/d1.js +45 -0
- package/dist/base-dao.d.ts +27 -0
- package/dist/base-dao.d.ts.map +1 -0
- package/dist/base-dao.js +34 -0
- package/dist/embedded-migrations.d.ts +15 -0
- package/dist/embedded-migrations.d.ts.map +1 -0
- package/dist/embedded-migrations.js +25 -0
- package/dist/entity-dao.d.ts +143 -0
- package/dist/entity-dao.d.ts.map +1 -0
- package/dist/entity-dao.js +218 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +9 -0
- package/dist/migrate.d.ts +38 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/migrate.js +131 -0
- package/dist/queue-job-dao.d.ts +95 -0
- package/dist/queue-job-dao.d.ts.map +1 -0
- package/dist/queue-job-dao.js +211 -0
- package/dist/schema/common.d.ts +87 -0
- package/dist/schema/common.d.ts.map +1 -0
- package/dist/schema/common.js +76 -0
- package/dist/schema/index.d.ts +3 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +2 -0
- package/dist/schema/queue-jobs.d.ts +225 -0
- package/dist/schema/queue-jobs.d.ts.map +1 -0
- package/dist/schema/queue-jobs.js +18 -0
- package/dist/span-context.d.ts +2 -0
- package/dist/span-context.d.ts.map +1 -0
- package/dist/span-context.js +0 -0
- package/package.json +47 -0
- package/src/adapter.ts +109 -0
- package/src/adapters/bun-sqlite.ts +108 -0
- package/src/adapters/d1.ts +76 -0
- package/src/base-dao.ts +37 -0
- package/src/embedded-migrations.ts +32 -0
- package/src/entity-dao.ts +290 -0
- package/src/index.ts +19 -0
- package/src/migrate.ts +163 -0
- package/src/queue-job-dao.ts +317 -0
- package/src/schema/common.ts +94 -0
- package/src/schema/index.ts +2 -0
- package/src/schema/queue-jobs.ts +23 -0
- package/src/span-context.ts +1 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { and, count as countFn, eq, type SQL } from 'drizzle-orm';
|
|
2
|
+
import type { SQLiteColumn, SQLiteTable } from 'drizzle-orm/sqlite-core';
|
|
3
|
+
import type { DbClient } from './adapter';
|
|
4
|
+
import { BaseDao } from './base-dao';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Type for tables compatible with EntityDao.
|
|
8
|
+
* Must have standard columns: createdAt, updatedAt.
|
|
9
|
+
*/
|
|
10
|
+
export type EntityTable = SQLiteTable & {
|
|
11
|
+
createdAt: SQLiteColumn;
|
|
12
|
+
updatedAt: SQLiteColumn;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Type for tables with soft delete support.
|
|
17
|
+
*/
|
|
18
|
+
export type SoftDeletableTable = EntityTable & {
|
|
19
|
+
inUsed: SQLiteColumn;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Type for primary key columns.
|
|
24
|
+
*/
|
|
25
|
+
export type PKColumn = SQLiteColumn;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generic CRUD base class for entity DAOs.
|
|
29
|
+
*
|
|
30
|
+
* Provides standard create, read, update, delete operations with:
|
|
31
|
+
* - Type-safe public API using Drizzle's inference types
|
|
32
|
+
* - Automatic soft delete filtering (if table has `inUsed` column)
|
|
33
|
+
*
|
|
34
|
+
* @typeParam TTable - The table type (must extend EntityTable)
|
|
35
|
+
* @typeParam TPK - The primary key column type
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* export class UsersDao extends EntityDao<typeof users, typeof users.id> {
|
|
40
|
+
* constructor(db: DbClient) {
|
|
41
|
+
* super(db, users, users.id, 'users');
|
|
42
|
+
* }
|
|
43
|
+
*
|
|
44
|
+
* // Add entity-specific methods here
|
|
45
|
+
* async findByEmail(email: string) {
|
|
46
|
+
* return this.findBy(users.email, email);
|
|
47
|
+
* }
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export class EntityDao<TTable extends EntityTable, TPK extends SQLiteColumn> extends BaseDao {
|
|
52
|
+
constructor(
|
|
53
|
+
db: DbClient,
|
|
54
|
+
public readonly table: TTable,
|
|
55
|
+
protected readonly primaryKey: TPK,
|
|
56
|
+
protected readonly collectionName: string,
|
|
57
|
+
) {
|
|
58
|
+
super(db);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Check if the table has soft delete support (inUsed column).
|
|
63
|
+
*/
|
|
64
|
+
protected get hasSoftDelete(): boolean {
|
|
65
|
+
return 'inUsed' in this.table;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Build a where condition that filters out soft-deleted records.
|
|
70
|
+
* Returns undefined if the table doesn't support soft delete.
|
|
71
|
+
*/
|
|
72
|
+
protected get activeCondition(): SQL | undefined {
|
|
73
|
+
if (this.hasSoftDelete) {
|
|
74
|
+
return eq((this.table as unknown as SoftDeletableTable).inUsed, 1);
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Create a new record.
|
|
81
|
+
*
|
|
82
|
+
* `createdAt` and `updatedAt` are auto-filled if not provided.
|
|
83
|
+
*
|
|
84
|
+
* @param data - The data to insert (createdAt/updatedAt optional).
|
|
85
|
+
* @returns The created record.
|
|
86
|
+
*/
|
|
87
|
+
async create(
|
|
88
|
+
data: Omit<TTable['$inferInsert'], 'createdAt' | 'updatedAt'> & { createdAt?: number; updatedAt?: number },
|
|
89
|
+
): Promise<TTable['$inferSelect']> {
|
|
90
|
+
const now = this.now();
|
|
91
|
+
const record = {
|
|
92
|
+
...data,
|
|
93
|
+
createdAt: (data as Record<string, unknown>).createdAt ?? now,
|
|
94
|
+
updatedAt: (data as Record<string, unknown>).updatedAt ?? now,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
await this.db.insert(this.table).values(record);
|
|
98
|
+
|
|
99
|
+
return record as TTable['$inferSelect'];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Find a record by its primary key.
|
|
104
|
+
*
|
|
105
|
+
* @param id - The primary key value.
|
|
106
|
+
* @param includeDeleted - Whether to include soft-deleted records.
|
|
107
|
+
* @returns The record if found, otherwise undefined.
|
|
108
|
+
*/
|
|
109
|
+
async findById(id: string | number, includeDeleted = false): Promise<TTable['$inferSelect'] | undefined> {
|
|
110
|
+
const conditions = [eq(this.primaryKey, id)];
|
|
111
|
+
if (!includeDeleted && this.activeCondition) {
|
|
112
|
+
conditions.push(this.activeCondition);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const result = await this.db
|
|
116
|
+
.select()
|
|
117
|
+
.from(this.table)
|
|
118
|
+
.where(and(...conditions));
|
|
119
|
+
|
|
120
|
+
return (result as TTable['$inferSelect'][])[0];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Find all records.
|
|
125
|
+
*
|
|
126
|
+
* @param includeDeleted - Whether to include soft-deleted records.
|
|
127
|
+
* @returns Array of records.
|
|
128
|
+
*/
|
|
129
|
+
async findAll(includeDeleted = false): Promise<TTable['$inferSelect'][]> {
|
|
130
|
+
const query = this.db.select().from(this.table);
|
|
131
|
+
|
|
132
|
+
if (!includeDeleted && this.activeCondition) {
|
|
133
|
+
return query.where(this.activeCondition);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return query;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Update a record by its primary key.
|
|
141
|
+
*
|
|
142
|
+
* @param id - The primary key value.
|
|
143
|
+
* @param data - The data to update.
|
|
144
|
+
* @returns The updated record if found, otherwise undefined.
|
|
145
|
+
*/
|
|
146
|
+
async update(
|
|
147
|
+
id: string | number,
|
|
148
|
+
data: Partial<TTable['$inferInsert']>,
|
|
149
|
+
): Promise<TTable['$inferSelect'] | undefined> {
|
|
150
|
+
const now = this.now();
|
|
151
|
+
const updateData = {
|
|
152
|
+
...data,
|
|
153
|
+
updatedAt: now,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
await this.db.update(this.table).set(updateData).where(eq(this.primaryKey, id));
|
|
157
|
+
|
|
158
|
+
return this.findById(id);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Delete a record by its primary key.
|
|
163
|
+
*
|
|
164
|
+
* @param id - The primary key value.
|
|
165
|
+
* @param soft - Whether to perform a soft delete (default: true if table supports it).
|
|
166
|
+
* @returns The deleted record (for soft delete), otherwise undefined.
|
|
167
|
+
*/
|
|
168
|
+
async delete(id: string | number, soft?: boolean): Promise<TTable['$inferSelect'] | undefined> {
|
|
169
|
+
const useSoftDelete = soft ?? this.hasSoftDelete;
|
|
170
|
+
|
|
171
|
+
if (useSoftDelete && this.hasSoftDelete) {
|
|
172
|
+
const now = this.now();
|
|
173
|
+
await this.db
|
|
174
|
+
.update(this.table)
|
|
175
|
+
.set({ inUsed: 0, updatedAt: now } as Partial<TTable['$inferInsert']>)
|
|
176
|
+
.where(eq(this.primaryKey, id));
|
|
177
|
+
|
|
178
|
+
return this.findById(id, true);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
await this.db.delete(this.table).where(eq(this.primaryKey, id));
|
|
182
|
+
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Find a record by a specific column value.
|
|
188
|
+
*
|
|
189
|
+
* @param column - The column to search.
|
|
190
|
+
* @param value - The value to match.
|
|
191
|
+
* @param includeDeleted - Whether to include soft-deleted records.
|
|
192
|
+
* @returns The record if found, otherwise undefined.
|
|
193
|
+
*/
|
|
194
|
+
async findBy<TCol extends SQLiteColumn>(
|
|
195
|
+
column: TCol,
|
|
196
|
+
value: TCol['_']['data'],
|
|
197
|
+
includeDeleted = false,
|
|
198
|
+
): Promise<TTable['$inferSelect'] | undefined> {
|
|
199
|
+
const conditions = [eq(column, value)];
|
|
200
|
+
if (!includeDeleted && this.activeCondition) {
|
|
201
|
+
conditions.push(this.activeCondition);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const result = await this.db
|
|
205
|
+
.select()
|
|
206
|
+
.from(this.table)
|
|
207
|
+
.where(and(...conditions));
|
|
208
|
+
|
|
209
|
+
return (result as TTable['$inferSelect'][])[0];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Find all records matching a specific column value.
|
|
214
|
+
*
|
|
215
|
+
* @param column - The column to search.
|
|
216
|
+
* @param value - The value to match.
|
|
217
|
+
* @param includeDeleted - Whether to include soft-deleted records.
|
|
218
|
+
* @returns Array of matching records.
|
|
219
|
+
*/
|
|
220
|
+
async findAllBy<TCol extends SQLiteColumn>(
|
|
221
|
+
column: TCol,
|
|
222
|
+
value: TCol['_']['data'],
|
|
223
|
+
includeDeleted = false,
|
|
224
|
+
): Promise<TTable['$inferSelect'][]> {
|
|
225
|
+
const conditions = [eq(column, value)];
|
|
226
|
+
if (!includeDeleted && this.activeCondition) {
|
|
227
|
+
conditions.push(this.activeCondition);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return this.db
|
|
231
|
+
.select()
|
|
232
|
+
.from(this.table)
|
|
233
|
+
.where(and(...conditions));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* List records with pagination and optional filtering.
|
|
238
|
+
*
|
|
239
|
+
* @param options - List options (limit, offset, where).
|
|
240
|
+
* @returns Array of records.
|
|
241
|
+
*/
|
|
242
|
+
async list(
|
|
243
|
+
options: { limit?: number; offset?: number; where?: SQL; includeDeleted?: boolean } = {},
|
|
244
|
+
): Promise<TTable['$inferSelect'][]> {
|
|
245
|
+
const { limit = 100, offset = 0, where, includeDeleted = false } = options;
|
|
246
|
+
const conditions: SQL[] = [];
|
|
247
|
+
|
|
248
|
+
if (!includeDeleted && this.activeCondition) {
|
|
249
|
+
conditions.push(this.activeCondition);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (where) {
|
|
253
|
+
conditions.push(where);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const query = this.db.select().from(this.table);
|
|
257
|
+
|
|
258
|
+
if (conditions.length > 0) {
|
|
259
|
+
return query
|
|
260
|
+
.where(and(...conditions))
|
|
261
|
+
.limit(limit)
|
|
262
|
+
.offset(offset);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return query.limit(limit).offset(offset);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Count records in the table.
|
|
270
|
+
*
|
|
271
|
+
* @param where - Optional filter condition.
|
|
272
|
+
* @param includeDeleted - Whether to include soft-deleted records.
|
|
273
|
+
* @returns The count of matching records.
|
|
274
|
+
*/
|
|
275
|
+
async count(where?: SQL, includeDeleted = false): Promise<number> {
|
|
276
|
+
const conditions: SQL[] = [];
|
|
277
|
+
if (!includeDeleted && this.activeCondition) {
|
|
278
|
+
conditions.push(this.activeCondition);
|
|
279
|
+
}
|
|
280
|
+
if (where) {
|
|
281
|
+
conditions.push(where);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const query = this.db.select<{ value: number }>({ value: countFn() }).from(this.table);
|
|
285
|
+
|
|
286
|
+
const result = conditions.length > 0 ? await query.where(and(...conditions)) : await query;
|
|
287
|
+
|
|
288
|
+
return result[0]?.value ?? 0;
|
|
289
|
+
}
|
|
290
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { createDbAdapter, type DbAdapter, type DbAdapterConfig, type DbClient, type DbTable } from './adapter';
|
|
2
|
+
export { BunSqliteAdapter, type BunSqliteOptions } from './adapters/bun-sqlite';
|
|
3
|
+
export { D1Adapter } from './adapters/d1';
|
|
4
|
+
export { BaseDao } from './base-dao';
|
|
5
|
+
export { type EmbeddedMigration, embeddedMigrations } from './embedded-migrations';
|
|
6
|
+
export { EntityDao, type EntityTable, type PKColumn, type SoftDeletableTable } from './entity-dao';
|
|
7
|
+
export { applyMigrations, type MigrationOptions } from './migrate';
|
|
8
|
+
export { QueueJobDao, type QueueJobRecord, type QueueStats } from './queue-job-dao';
|
|
9
|
+
export {
|
|
10
|
+
appendOnlyColumns,
|
|
11
|
+
buildAppendOnlyColumns,
|
|
12
|
+
buildStandardColumns,
|
|
13
|
+
buildStandardColumnsWithSoftDelete,
|
|
14
|
+
nowTimestamp,
|
|
15
|
+
standardColumns,
|
|
16
|
+
standardColumnsWithSoftDelete,
|
|
17
|
+
} from './schema/common';
|
|
18
|
+
export { queueJobs } from './schema/queue-jobs';
|
|
19
|
+
export type { SpanContext } from './span-context';
|
package/src/migrate.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import type { FileSystem } from '@gobing-ai/ts-runtime';
|
|
3
|
+
|
|
4
|
+
import type { DbAdapter } from './adapter';
|
|
5
|
+
import { embeddedMigrations } from './embedded-migrations';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Find project root by walking up looking for bun.lock.
|
|
9
|
+
* @deprecated Use `FileSystem.getProjectRoot()` instead.
|
|
10
|
+
* @internal — only used for backward compatibility.
|
|
11
|
+
*/
|
|
12
|
+
/** @internal */
|
|
13
|
+
export function findProjectRoot(_startDir: string): string {
|
|
14
|
+
return process.cwd();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Options for configuring migration behaviour (folder path, table name).
|
|
19
|
+
*/
|
|
20
|
+
export interface MigrationOptions {
|
|
21
|
+
/** Path to migration SQL files. Default: `fs.resolve('drizzle')` */
|
|
22
|
+
migrationsFolder?: string;
|
|
23
|
+
/** Name of the migrations tracking table. Default: '__drizzle_migrations' */
|
|
24
|
+
migrationsTable?: string;
|
|
25
|
+
/** File system abstraction for path resolution. */
|
|
26
|
+
fs?: FileSystem;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Ensure the migration tracking table exists with proper SQLite types.
|
|
31
|
+
*
|
|
32
|
+
* drizzle-orm 0.45 generates `id SERIAL PRIMARY KEY` for the journal table,
|
|
33
|
+
* but SQLite doesn't recognize SERIAL as auto-increment. Pre-create with
|
|
34
|
+
* proper syntax so drizzle-orm's `CREATE TABLE IF NOT EXISTS` skips it.
|
|
35
|
+
*/
|
|
36
|
+
async function ensureJournalTable(adapter: DbAdapter, table: string): Promise<void> {
|
|
37
|
+
await adapter.exec(
|
|
38
|
+
`CREATE TABLE IF NOT EXISTS "${table}" (` +
|
|
39
|
+
'id INTEGER PRIMARY KEY AUTOINCREMENT, ' +
|
|
40
|
+
'hash text NOT NULL, ' +
|
|
41
|
+
'created_at numeric' +
|
|
42
|
+
')',
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function validateMigrationTableName(table: string): string {
|
|
47
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table)) {
|
|
48
|
+
throw new Error(`Invalid migration journal table name: ${table}`);
|
|
49
|
+
}
|
|
50
|
+
return table;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Apply migrations from embedded SQL strings (for compiled binaries).
|
|
55
|
+
*
|
|
56
|
+
* Checks the journal table and applies only migrations that haven't run yet.
|
|
57
|
+
* Each migration is executed with adapter.exec() for file-based or adapter.run() for journal tracking.
|
|
58
|
+
*/
|
|
59
|
+
async function applyEmbeddedMigrations(adapter: DbAdapter, journalTable: string): Promise<void> {
|
|
60
|
+
// Validate journal table name — this is an internal constant, never user input.
|
|
61
|
+
if (!/^__[a-z_]+$/.test(journalTable)) {
|
|
62
|
+
throw new Error(`Invalid migration journal table name: ${journalTable}`);
|
|
63
|
+
}
|
|
64
|
+
// Get already-applied hashes
|
|
65
|
+
const appliedHashes = new Set<string>();
|
|
66
|
+
try {
|
|
67
|
+
const rows = await adapter.queryAll<{ hash: string }>(`SELECT hash FROM "${journalTable}"`);
|
|
68
|
+
for (const row of rows) {
|
|
69
|
+
appliedHashes.add(row.hash);
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
// Journal table may not exist yet — will be created by ensureJournalTable
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let applied = 0;
|
|
76
|
+
for (const migration of embeddedMigrations) {
|
|
77
|
+
if (appliedHashes.has(migration.hash)) continue;
|
|
78
|
+
|
|
79
|
+
console.info(`Applying embedded migration: ${migration.tag}`);
|
|
80
|
+
|
|
81
|
+
// Split on semicolons and execute each non-empty statement
|
|
82
|
+
const statements = migration.sql
|
|
83
|
+
.split(';')
|
|
84
|
+
.map((s) => s.trim())
|
|
85
|
+
.filter((s) => s.length > 0);
|
|
86
|
+
|
|
87
|
+
for (const stmt of statements) {
|
|
88
|
+
await adapter.exec(stmt);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Record in journal
|
|
92
|
+
await adapter.run(`INSERT INTO "${journalTable}" (hash, created_at) VALUES (?, ?)`, migration.hash, Date.now());
|
|
93
|
+
applied++;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (applied > 0) {
|
|
97
|
+
console.info(`Applied ${applied} embedded migration(s)`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Apply pending migrations using drizzle-orm's built-in migrator.
|
|
103
|
+
*
|
|
104
|
+
* Tracks applied migrations in the `__drizzle_migrations` table.
|
|
105
|
+
* Safe to call on every startup — already-applied migrations are skipped.
|
|
106
|
+
*
|
|
107
|
+
* Two migration strategies:
|
|
108
|
+
* 1. **File-based** (preferred): reads SQL from a `drizzle/` folder on disk.
|
|
109
|
+
* 2. **Embedded** (fallback): uses SQL bundled in the binary when no folder exists.
|
|
110
|
+
*
|
|
111
|
+
* Only works with BunSqliteAdapter. D1 migrations should use
|
|
112
|
+
* `wrangler d1 migrations apply` instead.
|
|
113
|
+
*
|
|
114
|
+
* @param adapter - A DbAdapter instance (must be BunSqliteAdapter).
|
|
115
|
+
* @param options - Optional migration folder and table name overrides.
|
|
116
|
+
*/
|
|
117
|
+
export async function applyMigrations(adapter: DbAdapter, options?: MigrationOptions): Promise<void> {
|
|
118
|
+
const { BunSqliteAdapter } = await import('./adapters/bun-sqlite');
|
|
119
|
+
if (!(adapter instanceof BunSqliteAdapter)) {
|
|
120
|
+
console.warn('Skipping in-app migrations: only supported for bun-sqlite adapter');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const table = validateMigrationTableName(options?.migrationsTable ?? '__drizzle_migrations');
|
|
125
|
+
|
|
126
|
+
await ensureJournalTable(adapter, table);
|
|
127
|
+
|
|
128
|
+
const folder = options?.migrationsFolder ?? resolve(findProjectRoot(process.cwd()), 'drizzle');
|
|
129
|
+
|
|
130
|
+
// File-based migrations: attempt if drizzle/ folder is accessible.
|
|
131
|
+
// Use FileSystem.exists when available, otherwise try and fall back to embedded.
|
|
132
|
+
const fs = options?.fs;
|
|
133
|
+
const tryFileBased = fs?.exists(folder) ?? true; // optimistic when no fs
|
|
134
|
+
|
|
135
|
+
if (tryFileBased) {
|
|
136
|
+
try {
|
|
137
|
+
const { migrate: drizzleMigrate } = await import('drizzle-orm/bun-sqlite/migrator');
|
|
138
|
+
|
|
139
|
+
console.info(`Applying database migrations from ${folder}`);
|
|
140
|
+
|
|
141
|
+
await drizzleMigrate(adapter.getDrizzleDb(), {
|
|
142
|
+
migrationsFolder: folder,
|
|
143
|
+
...(options?.migrationsTable !== undefined ? { migrationsTable: options.migrationsTable } : {}),
|
|
144
|
+
});
|
|
145
|
+
console.info('Database migrations complete');
|
|
146
|
+
return;
|
|
147
|
+
} catch (error) {
|
|
148
|
+
// If folder doesn't exist, fall through to embedded migrations.
|
|
149
|
+
// Any other error should be thrown.
|
|
150
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
151
|
+
if (message.includes('journal') || message.includes('ENOENT') || message.includes('meta')) {
|
|
152
|
+
console.info(`File-based migrations unavailable, using embedded: ${message}`);
|
|
153
|
+
} else {
|
|
154
|
+
console.error(`[MIGRATE] drizzleMigrate failed: ${message}`);
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Fallback: embedded migrations (for compiled binaries)
|
|
161
|
+
console.info('No drizzle/ folder found — applying embedded migrations');
|
|
162
|
+
await applyEmbeddedMigrations(adapter, table);
|
|
163
|
+
}
|