@cfast/db 0.5.0 → 0.7.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.d.ts +5 -305
- package/dist/index.js +79 -33
- package/dist/seed.d.ts +328 -0
- package/dist/seed.js +399 -0
- package/dist/types-FUFR36h1.d.ts +221 -0
- package/llms.txt +163 -16
- package/package.json +15 -5
package/dist/seed.js
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
// src/seed-generator.ts
|
|
2
|
+
import { faker } from "@faker-js/faker";
|
|
3
|
+
import { getTableColumns, getTableName } from "drizzle-orm";
|
|
4
|
+
function asTable(t) {
|
|
5
|
+
return t;
|
|
6
|
+
}
|
|
7
|
+
var columnSeedMap = /* @__PURE__ */ new WeakMap();
|
|
8
|
+
var tableSeedMap = /* @__PURE__ */ new WeakMap();
|
|
9
|
+
function getColumnSeedFn(col) {
|
|
10
|
+
const config = col.config;
|
|
11
|
+
if (config) {
|
|
12
|
+
const fn = columnSeedMap.get(config);
|
|
13
|
+
if (fn) return fn;
|
|
14
|
+
}
|
|
15
|
+
return columnSeedMap.get(col);
|
|
16
|
+
}
|
|
17
|
+
function seedConfig(column, fn) {
|
|
18
|
+
const config = column.config;
|
|
19
|
+
if (config && typeof config === "object") {
|
|
20
|
+
columnSeedMap.set(config, fn);
|
|
21
|
+
}
|
|
22
|
+
columnSeedMap.set(column, fn);
|
|
23
|
+
return column;
|
|
24
|
+
}
|
|
25
|
+
function tableSeed(table2, config) {
|
|
26
|
+
tableSeedMap.set(table2, config);
|
|
27
|
+
return table2;
|
|
28
|
+
}
|
|
29
|
+
function extractForeignKeys(table2) {
|
|
30
|
+
const fkSymbol = Object.getOwnPropertySymbols(table2).find(
|
|
31
|
+
(s) => s.toString().includes("InlineForeignKeys")
|
|
32
|
+
);
|
|
33
|
+
if (!fkSymbol) return [];
|
|
34
|
+
const fks = table2[fkSymbol];
|
|
35
|
+
const columns = getTableColumns(asTable(table2));
|
|
36
|
+
const sqlNameToKey = /* @__PURE__ */ new Map();
|
|
37
|
+
for (const [key, col] of Object.entries(columns)) {
|
|
38
|
+
sqlNameToKey.set(col.name, key);
|
|
39
|
+
}
|
|
40
|
+
const result = [];
|
|
41
|
+
for (const fk of fks) {
|
|
42
|
+
if (typeof fk?.reference !== "function") continue;
|
|
43
|
+
const ref = fk.reference();
|
|
44
|
+
if (!ref?.foreignTable || !ref.foreignColumns?.length || !ref.columns?.length) continue;
|
|
45
|
+
const colSqlName = ref.columns[0].name;
|
|
46
|
+
result.push({
|
|
47
|
+
columnName: colSqlName,
|
|
48
|
+
columnKey: sqlNameToKey.get(colSqlName) ?? colSqlName,
|
|
49
|
+
foreignTable: ref.foreignTable,
|
|
50
|
+
foreignColumnName: ref.foreignColumns[0].name
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
function findPrimaryKeyColumn(table2) {
|
|
56
|
+
const columns = getTableColumns(asTable(table2));
|
|
57
|
+
for (const [key, col] of Object.entries(columns)) {
|
|
58
|
+
if (col.primary) {
|
|
59
|
+
return { key, column: col };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return void 0;
|
|
63
|
+
}
|
|
64
|
+
function generateDefaultValue(col, isPk, isNullable) {
|
|
65
|
+
if (isPk) return faker.string.uuid();
|
|
66
|
+
if (isNullable && faker.number.int({ min: 0, max: 9 }) === 0) return null;
|
|
67
|
+
const { dataType, columnType } = col;
|
|
68
|
+
if (columnType === "SQLiteBoolean" || dataType === "boolean") {
|
|
69
|
+
return faker.datatype.boolean();
|
|
70
|
+
}
|
|
71
|
+
if (columnType === "SQLiteTimestamp" || dataType === "date") {
|
|
72
|
+
return faker.date.recent();
|
|
73
|
+
}
|
|
74
|
+
if (columnType === "SQLiteReal") {
|
|
75
|
+
return faker.number.float({ min: 0, max: 1e3, fractionDigits: 2 });
|
|
76
|
+
}
|
|
77
|
+
if (columnType === "SQLiteInteger" || dataType === "number") {
|
|
78
|
+
return faker.number.int({ min: 0, max: 1e4 });
|
|
79
|
+
}
|
|
80
|
+
if (columnType === "SQLiteText" || dataType === "string") {
|
|
81
|
+
return faker.lorem.words(3);
|
|
82
|
+
}
|
|
83
|
+
if (dataType === "buffer") {
|
|
84
|
+
return faker.string.alphanumeric(16);
|
|
85
|
+
}
|
|
86
|
+
return faker.lorem.words(2);
|
|
87
|
+
}
|
|
88
|
+
var AUTH_TABLE_NAME = "users";
|
|
89
|
+
function isAuthUsersTable(table2) {
|
|
90
|
+
return getTableName(asTable(table2)) === AUTH_TABLE_NAME;
|
|
91
|
+
}
|
|
92
|
+
function generateAuthEmail(index) {
|
|
93
|
+
const roles = ["admin", "user", "editor", "viewer", "moderator"];
|
|
94
|
+
if (index < roles.length) {
|
|
95
|
+
return `${roles[index]}@example.com`;
|
|
96
|
+
}
|
|
97
|
+
return faker.internet.email().toLowerCase();
|
|
98
|
+
}
|
|
99
|
+
function topologicalSort(tables, fkMap) {
|
|
100
|
+
const tableSet = new Set(tables);
|
|
101
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
102
|
+
const dependents = /* @__PURE__ */ new Map();
|
|
103
|
+
for (const t of tables) {
|
|
104
|
+
if (!inDegree.has(t)) inDegree.set(t, 0);
|
|
105
|
+
if (!dependents.has(t)) dependents.set(t, /* @__PURE__ */ new Set());
|
|
106
|
+
}
|
|
107
|
+
for (const t of tables) {
|
|
108
|
+
const fks = fkMap.get(t) ?? [];
|
|
109
|
+
const deps = /* @__PURE__ */ new Set();
|
|
110
|
+
for (const fk of fks) {
|
|
111
|
+
if (tableSet.has(fk.foreignTable) && fk.foreignTable !== t) {
|
|
112
|
+
deps.add(fk.foreignTable);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const tConfig = tableSeedMap.get(t);
|
|
116
|
+
if (tConfig?.per && tableSet.has(tConfig.per) && tConfig.per !== t) {
|
|
117
|
+
deps.add(tConfig.per);
|
|
118
|
+
}
|
|
119
|
+
inDegree.set(t, deps.size);
|
|
120
|
+
for (const dep of deps) {
|
|
121
|
+
if (!dependents.has(dep)) dependents.set(dep, /* @__PURE__ */ new Set());
|
|
122
|
+
dependents.get(dep).add(t);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const queue = [];
|
|
126
|
+
for (const [t, deg] of inDegree) {
|
|
127
|
+
if (deg === 0) queue.push(t);
|
|
128
|
+
}
|
|
129
|
+
const sorted = [];
|
|
130
|
+
while (queue.length > 0) {
|
|
131
|
+
const current = queue.shift();
|
|
132
|
+
sorted.push(current);
|
|
133
|
+
for (const dep of dependents.get(current) ?? []) {
|
|
134
|
+
const newDeg = (inDegree.get(dep) ?? 1) - 1;
|
|
135
|
+
inDegree.set(dep, newDeg);
|
|
136
|
+
if (newDeg === 0) queue.push(dep);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const t of tables) {
|
|
140
|
+
if (!sorted.includes(t)) sorted.push(t);
|
|
141
|
+
}
|
|
142
|
+
return sorted;
|
|
143
|
+
}
|
|
144
|
+
function getDeduplicationKeys(_table, fks) {
|
|
145
|
+
if (fks.length >= 2) {
|
|
146
|
+
return fks.map((f) => f.columnKey);
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
function createSeedEngine(schema) {
|
|
151
|
+
const tables = [];
|
|
152
|
+
for (const value of Object.values(schema)) {
|
|
153
|
+
if (isTable(value)) {
|
|
154
|
+
tables.push(value);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const fkMap = /* @__PURE__ */ new Map();
|
|
158
|
+
for (const table2 of tables) {
|
|
159
|
+
fkMap.set(table2, extractForeignKeys(table2));
|
|
160
|
+
}
|
|
161
|
+
const sorted = topologicalSort(tables, fkMap);
|
|
162
|
+
return {
|
|
163
|
+
tables: sorted,
|
|
164
|
+
fkMap,
|
|
165
|
+
/**
|
|
166
|
+
* Generate rows for all tables (or a subset).
|
|
167
|
+
* Returns a map of table -> rows[].
|
|
168
|
+
*/
|
|
169
|
+
generate(tableOverrides) {
|
|
170
|
+
const generated = /* @__PURE__ */ new Map();
|
|
171
|
+
for (const table2 of sorted) {
|
|
172
|
+
const config = tableOverrides?.get(table2) ?? tableSeedMap.get(table2);
|
|
173
|
+
const fks = fkMap.get(table2) ?? [];
|
|
174
|
+
const columns = getTableColumns(asTable(table2));
|
|
175
|
+
const pk = findPrimaryKeyColumn(table2);
|
|
176
|
+
const isAuth = isAuthUsersTable(table2);
|
|
177
|
+
const dedupKeys = getDeduplicationKeys(table2, fks);
|
|
178
|
+
const count = config?.count ?? 10;
|
|
179
|
+
const perTable = config?.per;
|
|
180
|
+
const parentRows = perTable ? generated.get(perTable) ?? [] : [void 0];
|
|
181
|
+
const allRows = [];
|
|
182
|
+
const seenCombos = /* @__PURE__ */ new Set();
|
|
183
|
+
let globalIndex = 0;
|
|
184
|
+
for (const parentRow of parentRows) {
|
|
185
|
+
for (let i = 0; i < count; i++) {
|
|
186
|
+
const ctx = {
|
|
187
|
+
parent: parentRow,
|
|
188
|
+
ref: (t) => {
|
|
189
|
+
const rows = generated.get(t);
|
|
190
|
+
if (!rows || rows.length === 0) {
|
|
191
|
+
throw new Error(
|
|
192
|
+
`seedConfig ctx.ref(${getTableName(asTable(t))}): no rows generated yet`
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
return rows[faker.number.int({ min: 0, max: rows.length - 1 })];
|
|
196
|
+
},
|
|
197
|
+
index: i,
|
|
198
|
+
all: (t) => generated.get(t) ?? []
|
|
199
|
+
};
|
|
200
|
+
const row = {};
|
|
201
|
+
for (const [key, col] of Object.entries(columns)) {
|
|
202
|
+
const colObj = col;
|
|
203
|
+
const isPk = pk?.key === key;
|
|
204
|
+
const isNullable = !colObj.notNull;
|
|
205
|
+
const customSeedFn = getColumnSeedFn(colObj);
|
|
206
|
+
if (customSeedFn) {
|
|
207
|
+
row[key] = customSeedFn(faker, ctx);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const fk = fks.find((f) => f.columnKey === key);
|
|
211
|
+
if (fk) {
|
|
212
|
+
if (perTable && parentRow && getTableName(asTable(fk.foreignTable)) === getTableName(asTable(perTable))) {
|
|
213
|
+
const foreignColumns = getTableColumns(asTable(fk.foreignTable));
|
|
214
|
+
const foreignKey = Object.entries(foreignColumns).find(
|
|
215
|
+
([, c]) => c.name === fk.foreignColumnName
|
|
216
|
+
);
|
|
217
|
+
if (foreignKey) {
|
|
218
|
+
row[key] = parentRow[foreignKey[0]];
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const refRows = generated.get(fk.foreignTable);
|
|
223
|
+
if (refRows && refRows.length > 0) {
|
|
224
|
+
const randomRef = refRows[faker.number.int({ min: 0, max: refRows.length - 1 })];
|
|
225
|
+
const foreignColumns = getTableColumns(asTable(fk.foreignTable));
|
|
226
|
+
const foreignKey = Object.entries(foreignColumns).find(
|
|
227
|
+
([, c]) => c.name === fk.foreignColumnName
|
|
228
|
+
);
|
|
229
|
+
if (foreignKey) {
|
|
230
|
+
row[key] = randomRef[foreignKey[0]];
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (isAuth && key === "email") {
|
|
236
|
+
row[key] = generateAuthEmail(globalIndex);
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
if (isAuth && key === "name") {
|
|
240
|
+
row[key] = faker.person.fullName();
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const colAny = colObj;
|
|
244
|
+
if (colAny.defaultFn || colAny.onUpdateFn) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
if (colAny.hasDefault && colAny.default !== void 0) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
row[key] = generateDefaultValue(colObj, isPk, isNullable);
|
|
251
|
+
}
|
|
252
|
+
if (dedupKeys) {
|
|
253
|
+
const comboKey = dedupKeys.map((k) => String(row[k])).join(":");
|
|
254
|
+
if (seenCombos.has(comboKey)) continue;
|
|
255
|
+
seenCombos.add(comboKey);
|
|
256
|
+
}
|
|
257
|
+
allRows.push(row);
|
|
258
|
+
globalIndex++;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
generated.set(table2, allRows);
|
|
262
|
+
}
|
|
263
|
+
return generated;
|
|
264
|
+
},
|
|
265
|
+
/**
|
|
266
|
+
* Generate and insert seed data into the database.
|
|
267
|
+
*/
|
|
268
|
+
async run(db, options) {
|
|
269
|
+
const generated = this.generate(options?.tableOverrides);
|
|
270
|
+
const unsafeDb = db.unsafe();
|
|
271
|
+
const transcriptLines = [];
|
|
272
|
+
for (const table2 of sorted) {
|
|
273
|
+
const rows = generated.get(table2);
|
|
274
|
+
if (!rows || rows.length === 0) continue;
|
|
275
|
+
const tableName = getTableName(asTable(table2));
|
|
276
|
+
if (options?.transcript) {
|
|
277
|
+
for (const row of rows) {
|
|
278
|
+
const colNames = Object.keys(row);
|
|
279
|
+
const values = colNames.map((k) => {
|
|
280
|
+
const v = row[k];
|
|
281
|
+
if (v === null) return "NULL";
|
|
282
|
+
if (typeof v === "number" || typeof v === "boolean") return String(v);
|
|
283
|
+
if (v instanceof Date) return `'${v.toISOString()}'`;
|
|
284
|
+
return `'${String(v).replace(/'/g, "''")}'`;
|
|
285
|
+
});
|
|
286
|
+
transcriptLines.push(
|
|
287
|
+
`INSERT INTO "${tableName}" (${colNames.map((c) => `"${c}"`).join(", ")}) VALUES (${values.join(", ")});`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const ops = rows.map(
|
|
292
|
+
(row) => unsafeDb.insert(table2).values(row)
|
|
293
|
+
);
|
|
294
|
+
if (ops.length === 1) {
|
|
295
|
+
await ops[0].run({});
|
|
296
|
+
} else {
|
|
297
|
+
await unsafeDb.batch(ops).run({});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (options?.transcript && transcriptLines.length > 0) {
|
|
301
|
+
try {
|
|
302
|
+
const fs = await new Function(
|
|
303
|
+
'return import("node:fs/promises")'
|
|
304
|
+
)();
|
|
305
|
+
await fs.writeFile(
|
|
306
|
+
options.transcript,
|
|
307
|
+
transcriptLines.join("\n") + "\n",
|
|
308
|
+
"utf-8"
|
|
309
|
+
);
|
|
310
|
+
} catch {
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return generated;
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function createSingleTableSeed(schema, table2, count) {
|
|
318
|
+
const engine = createSeedEngine(schema);
|
|
319
|
+
const overrides = /* @__PURE__ */ new Map();
|
|
320
|
+
overrides.set(table2, { count });
|
|
321
|
+
for (const t of engine.tables) {
|
|
322
|
+
if (t !== table2 && !overrides.has(t)) {
|
|
323
|
+
overrides.set(t, { count: 0 });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
generate: () => engine.generate(overrides),
|
|
328
|
+
run: (db, options) => engine.run(db, { ...options, tableOverrides: overrides })
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function isTable(value) {
|
|
332
|
+
if (!value || typeof value !== "object") return false;
|
|
333
|
+
const symbols = Object.getOwnPropertySymbols(value);
|
|
334
|
+
return symbols.some((s) => s.toString().includes("IsDrizzleTable"));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// src/seed.ts
|
|
338
|
+
import { SQLiteColumnBuilder } from "drizzle-orm/sqlite-core/columns/common";
|
|
339
|
+
import { sqliteTable } from "drizzle-orm/sqlite-core";
|
|
340
|
+
SQLiteColumnBuilder.prototype.seed = function(fn) {
|
|
341
|
+
const config = this.config;
|
|
342
|
+
if (config && typeof config === "object") {
|
|
343
|
+
columnSeedMap.set(config, fn);
|
|
344
|
+
}
|
|
345
|
+
columnSeedMap.set(this, fn);
|
|
346
|
+
return this;
|
|
347
|
+
};
|
|
348
|
+
function table(name, columns, extraConfig) {
|
|
349
|
+
const t = sqliteTable(name, columns, extraConfig);
|
|
350
|
+
t.seed = (config) => {
|
|
351
|
+
tableSeedMap.set(t, config);
|
|
352
|
+
return t;
|
|
353
|
+
};
|
|
354
|
+
return t;
|
|
355
|
+
}
|
|
356
|
+
async function seed(db, options) {
|
|
357
|
+
const engine = createSeedEngine(db._schema);
|
|
358
|
+
await engine.run(db, options);
|
|
359
|
+
}
|
|
360
|
+
function defineSeed(config) {
|
|
361
|
+
const entries = Object.freeze(
|
|
362
|
+
config.entries.map((entry) => ({
|
|
363
|
+
table: entry.table,
|
|
364
|
+
rows: Object.freeze([...entry.rows])
|
|
365
|
+
}))
|
|
366
|
+
);
|
|
367
|
+
return {
|
|
368
|
+
entries,
|
|
369
|
+
async run(db) {
|
|
370
|
+
const unsafeDb = db.unsafe();
|
|
371
|
+
for (const entry of entries) {
|
|
372
|
+
if (entry.rows.length === 0) continue;
|
|
373
|
+
const ops = entry.rows.map(
|
|
374
|
+
(row) => unsafeDb.insert(entry.table).values(row)
|
|
375
|
+
);
|
|
376
|
+
if (ops.length === 1) {
|
|
377
|
+
await ops[0].run({});
|
|
378
|
+
} else {
|
|
379
|
+
await unsafeDb.batch(ops).run({});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
export {
|
|
386
|
+
columnSeedMap,
|
|
387
|
+
createSeedEngine,
|
|
388
|
+
createSingleTableSeed,
|
|
389
|
+
defineSeed,
|
|
390
|
+
extractForeignKeys,
|
|
391
|
+
findPrimaryKeyColumn,
|
|
392
|
+
isTable,
|
|
393
|
+
seed,
|
|
394
|
+
seedConfig,
|
|
395
|
+
table,
|
|
396
|
+
tableSeed,
|
|
397
|
+
tableSeedMap,
|
|
398
|
+
topologicalSort
|
|
399
|
+
};
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { Grant, DrizzleTable, PermissionDescriptor } from '@cfast/permissions';
|
|
2
|
+
import { Table, TablesRelationalConfig, ExtractTablesWithRelations, TableRelationalConfig, BuildQueryResult } from 'drizzle-orm';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Augments a row type with a `_can` object containing per-action booleans.
|
|
6
|
+
*
|
|
7
|
+
* Every query result from `findMany` / `findFirst` includes `_can` when the
|
|
8
|
+
* `Db` was created with grants and a user. Each CRUD action maps to `true`
|
|
9
|
+
* (permitted for this row), `false` (denied), or a row-dependent boolean
|
|
10
|
+
* when the grant has a `where` clause.
|
|
11
|
+
*/
|
|
12
|
+
type WithCan<T> = T & {
|
|
13
|
+
_can: Record<string, boolean>;
|
|
14
|
+
};
|
|
15
|
+
type InferRow<TTable> = TTable extends {
|
|
16
|
+
$inferSelect: infer R;
|
|
17
|
+
} ? R : Record<string, unknown>;
|
|
18
|
+
/** @internal */
|
|
19
|
+
type FindTableKeyByName<TSchema extends TablesRelationalConfig, TTableName extends string> = {
|
|
20
|
+
[K in keyof TSchema]: TSchema[K]["dbName"] extends TTableName ? K : never;
|
|
21
|
+
}[keyof TSchema];
|
|
22
|
+
/** @internal */
|
|
23
|
+
type LookupTableConfig<TFullSchema extends Record<string, unknown>, TTable> = TTable extends Table<infer TTableConfig> ? FindTableKeyByName<ExtractTablesWithRelations<TFullSchema>, TTableConfig["name"]> extends infer TKey extends keyof ExtractTablesWithRelations<TFullSchema> ? ExtractTablesWithRelations<TFullSchema>[TKey] : never : never;
|
|
24
|
+
type InferQueryResult<TFullSchema extends Record<string, unknown>, TTable, TConfig> = [TFullSchema] extends [Record<string, never>] ? InferRow<TTable> : LookupTableConfig<TFullSchema, TTable> extends infer TTableConfig extends TableRelationalConfig ? BuildQueryResult<ExtractTablesWithRelations<TFullSchema>, TTableConfig, TConfig extends Record<string, unknown> ? TConfig : true> : InferRow<TTable>;
|
|
25
|
+
type Operation<TResult> = {
|
|
26
|
+
permissions: PermissionDescriptor[];
|
|
27
|
+
run: (params?: Record<string, unknown>) => Promise<TResult>;
|
|
28
|
+
};
|
|
29
|
+
type CacheBackend = "cache-api" | "kv";
|
|
30
|
+
type CacheConfig = {
|
|
31
|
+
backend: CacheBackend;
|
|
32
|
+
kv?: KVNamespace;
|
|
33
|
+
ttl?: string;
|
|
34
|
+
staleWhileRevalidate?: string;
|
|
35
|
+
exclude?: string[];
|
|
36
|
+
onHit?: (key: string, table: string) => void;
|
|
37
|
+
onMiss?: (key: string, table: string) => void;
|
|
38
|
+
onInvalidate?: (tables: string[]) => void;
|
|
39
|
+
};
|
|
40
|
+
type QueryCacheOptions = false | {
|
|
41
|
+
ttl?: string;
|
|
42
|
+
staleWhileRevalidate?: string;
|
|
43
|
+
tags?: string[];
|
|
44
|
+
};
|
|
45
|
+
type DbConfig<TSchema extends Record<string, unknown> = Record<string, unknown>> = {
|
|
46
|
+
d1: D1Database;
|
|
47
|
+
schema: TSchema;
|
|
48
|
+
grants: Grant[];
|
|
49
|
+
user: {
|
|
50
|
+
id: string;
|
|
51
|
+
} | null;
|
|
52
|
+
cache?: CacheConfig | false;
|
|
53
|
+
};
|
|
54
|
+
type FindManyOptions = {
|
|
55
|
+
columns?: Record<string, boolean>;
|
|
56
|
+
where?: unknown;
|
|
57
|
+
orderBy?: unknown;
|
|
58
|
+
limit?: number;
|
|
59
|
+
offset?: number;
|
|
60
|
+
with?: Record<string, unknown>;
|
|
61
|
+
cache?: QueryCacheOptions;
|
|
62
|
+
};
|
|
63
|
+
type FindFirstOptions = Omit<FindManyOptions, "limit" | "offset">;
|
|
64
|
+
type CursorParams = {
|
|
65
|
+
type: "cursor";
|
|
66
|
+
cursor: string | null;
|
|
67
|
+
limit: number;
|
|
68
|
+
};
|
|
69
|
+
type OffsetParams = {
|
|
70
|
+
type: "offset";
|
|
71
|
+
page: number;
|
|
72
|
+
limit: number;
|
|
73
|
+
};
|
|
74
|
+
type PaginateParams = CursorParams | OffsetParams;
|
|
75
|
+
type CursorPage<T> = {
|
|
76
|
+
items: T[];
|
|
77
|
+
nextCursor: string | null;
|
|
78
|
+
};
|
|
79
|
+
type OffsetPage<T> = {
|
|
80
|
+
items: T[];
|
|
81
|
+
total: number;
|
|
82
|
+
page: number;
|
|
83
|
+
totalPages: number;
|
|
84
|
+
};
|
|
85
|
+
type PaginateOptions = {
|
|
86
|
+
columns?: Record<string, boolean>;
|
|
87
|
+
where?: unknown;
|
|
88
|
+
orderBy?: unknown;
|
|
89
|
+
cursorColumns?: unknown[];
|
|
90
|
+
orderDirection?: "asc" | "desc";
|
|
91
|
+
with?: Record<string, unknown>;
|
|
92
|
+
cache?: QueryCacheOptions;
|
|
93
|
+
};
|
|
94
|
+
type TransactionResult<T> = {
|
|
95
|
+
result: T;
|
|
96
|
+
meta: {
|
|
97
|
+
changes: number;
|
|
98
|
+
writeResults: D1Result[];
|
|
99
|
+
};
|
|
100
|
+
};
|
|
101
|
+
type Tx<TSchema extends Record<string, unknown> = Record<string, never>> = {
|
|
102
|
+
query: <TTable extends DrizzleTable>(table: TTable) => QueryBuilder<TTable, TSchema>;
|
|
103
|
+
insert: <TTable extends DrizzleTable>(table: TTable) => InsertBuilder<TTable>;
|
|
104
|
+
update: <TTable extends DrizzleTable>(table: TTable) => UpdateBuilder<TTable>;
|
|
105
|
+
delete: <TTable extends DrizzleTable>(table: TTable) => DeleteBuilder<TTable>;
|
|
106
|
+
transaction: <T>(callback: (tx: Tx<TSchema>) => Promise<T>) => Promise<T>;
|
|
107
|
+
};
|
|
108
|
+
type Db<TSchema extends Record<string, unknown> = Record<string, never>> = {
|
|
109
|
+
query: <TTable extends DrizzleTable>(table: TTable) => QueryBuilder<TTable, TSchema>;
|
|
110
|
+
insert: <TTable extends DrizzleTable>(table: TTable) => InsertBuilder<TTable>;
|
|
111
|
+
update: <TTable extends DrizzleTable>(table: TTable) => UpdateBuilder<TTable>;
|
|
112
|
+
delete: <TTable extends DrizzleTable>(table: TTable) => DeleteBuilder<TTable>;
|
|
113
|
+
unsafe: () => Db<TSchema>;
|
|
114
|
+
batch: (operations: Operation<unknown>[]) => Operation<unknown[]>;
|
|
115
|
+
/**
|
|
116
|
+
* Runs a callback inside a transaction with atomic commit-or-rollback semantics.
|
|
117
|
+
*
|
|
118
|
+
* All writes (`tx.insert`/`tx.update`/`tx.delete`) recorded inside the callback
|
|
119
|
+
* are deferred and flushed together as a single atomic `db.batch([...])` when
|
|
120
|
+
* the callback returns successfully. If the callback throws, pending writes are
|
|
121
|
+
* discarded and the error is re-thrown — nothing reaches D1.
|
|
122
|
+
*
|
|
123
|
+
* Reads (`tx.query(...)`) execute eagerly so the caller can branch on their
|
|
124
|
+
* results. **D1 does not provide snapshot isolation across async code**, so
|
|
125
|
+
* reads inside a transaction can see concurrent writes. For concurrency-safe
|
|
126
|
+
* read-modify-write (e.g. stock decrement), combine the transaction with a
|
|
127
|
+
* relative SQL update and a WHERE guard:
|
|
128
|
+
*
|
|
129
|
+
* ```ts
|
|
130
|
+
* await db.transaction(async (tx) => {
|
|
131
|
+
* // The WHERE guard ensures the decrement only applies when stock is
|
|
132
|
+
* // still >= qty at commit time. Two concurrent transactions cannot
|
|
133
|
+
* // both oversell because D1's atomic batch re-evaluates the WHERE.
|
|
134
|
+
* await tx.update(products)
|
|
135
|
+
* .set({ stock: sql`stock - ${qty}` })
|
|
136
|
+
* .where(and(eq(products.id, pid), gte(products.stock, qty)))
|
|
137
|
+
* .run();
|
|
138
|
+
* return tx.insert(orders).values({ productId: pid, qty }).returning().run();
|
|
139
|
+
* });
|
|
140
|
+
* ```
|
|
141
|
+
*
|
|
142
|
+
* Nested `db.transaction()` calls inside the callback are flattened into the
|
|
143
|
+
* parent's pending queue so everything still commits atomically.
|
|
144
|
+
*
|
|
145
|
+
* @typeParam T - The return type of the callback.
|
|
146
|
+
* @param callback - The transaction body. Receives a `tx` handle with
|
|
147
|
+
* `query`/`insert`/`update`/`delete` methods (no `unsafe`, `batch`, or
|
|
148
|
+
* `cache` — those are intentionally off-limits inside a transaction).
|
|
149
|
+
* @returns A {@link TransactionResult} containing the callback's return value
|
|
150
|
+
* and transaction metadata (`meta.changes`, `meta.writeResults`), or rejects
|
|
151
|
+
* with whatever the callback threw (after rolling back pending writes).
|
|
152
|
+
*/
|
|
153
|
+
transaction: <T>(callback: (tx: Tx<TSchema>) => Promise<T>) => Promise<TransactionResult<T>>;
|
|
154
|
+
/** Cache control methods for manual invalidation. */
|
|
155
|
+
cache: {
|
|
156
|
+
/** Invalidate cached queries by tag names and/or table names. */
|
|
157
|
+
invalidate: (options: {
|
|
158
|
+
/** Tag names to invalidate (from {@link QueryCacheOptions} `tags`). */
|
|
159
|
+
tags?: string[];
|
|
160
|
+
/** Table names to invalidate (bumps their version counters). */
|
|
161
|
+
tables?: string[];
|
|
162
|
+
}) => Promise<void>;
|
|
163
|
+
};
|
|
164
|
+
/**
|
|
165
|
+
* Clears the per-instance `with` lookup cache so that the next query
|
|
166
|
+
* re-runs every grant lookup function.
|
|
167
|
+
*
|
|
168
|
+
* In production this is rarely needed because each request gets a fresh
|
|
169
|
+
* `Db` via `createDb()`. In tests that reuse a single `Db` across grant
|
|
170
|
+
* mutations (e.g. inserting a new friendship and then querying recipes),
|
|
171
|
+
* call this after the mutation to avoid stale lookup results.
|
|
172
|
+
*
|
|
173
|
+
* For finer-grained control, wrap each logical request in
|
|
174
|
+
* {@link runWithLookupCache} instead -- that scopes the cache via
|
|
175
|
+
* `AsyncLocalStorage` so it is automatically discarded at scope exit.
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```ts
|
|
179
|
+
* const db = createDb({ ... });
|
|
180
|
+
* await db.query(recipes).findMany().run(); // populates lookup cache
|
|
181
|
+
* await db.insert(friendGrants).values({ ... }).run(); // adds new grant
|
|
182
|
+
* db.clearLookupCache(); // drop stale lookups
|
|
183
|
+
* await db.query(recipes).findMany().run(); // sees new grant
|
|
184
|
+
* ```
|
|
185
|
+
*/
|
|
186
|
+
clearLookupCache: () => void;
|
|
187
|
+
/**
|
|
188
|
+
* The Drizzle schema this Db was created with. Exposed for the seed engine
|
|
189
|
+
* so `seed(db)` can introspect tables without a separate schema import.
|
|
190
|
+
* @internal
|
|
191
|
+
*/
|
|
192
|
+
readonly _schema: Record<string, unknown>;
|
|
193
|
+
};
|
|
194
|
+
type QueryBuilder<TTable extends DrizzleTable = DrizzleTable, TSchema extends Record<string, unknown> = Record<string, never>> = {
|
|
195
|
+
findMany: <TConfig extends FindManyOptions = Record<string, never>, TRow = InferQueryResult<TSchema, TTable, TConfig>>(options?: TConfig) => Operation<WithCan<TRow>[]>;
|
|
196
|
+
findFirst: <TConfig extends FindFirstOptions = Record<string, never>, TRow = InferQueryResult<TSchema, TTable, TConfig>>(options?: TConfig) => Operation<WithCan<TRow> | undefined>;
|
|
197
|
+
paginate: <TRow = InferRow<TTable>>(params: CursorParams | OffsetParams, options?: PaginateOptions) => Operation<CursorPage<TRow>> | Operation<OffsetPage<TRow>>;
|
|
198
|
+
};
|
|
199
|
+
type InsertBuilder<TTable extends DrizzleTable = DrizzleTable> = {
|
|
200
|
+
values: (values: Record<string, unknown>) => InsertReturningBuilder<TTable>;
|
|
201
|
+
};
|
|
202
|
+
type InsertReturningBuilder<TTable extends DrizzleTable = DrizzleTable> = Operation<void> & {
|
|
203
|
+
returning: () => Operation<InferRow<TTable>>;
|
|
204
|
+
};
|
|
205
|
+
type UpdateBuilder<TTable extends DrizzleTable = DrizzleTable> = {
|
|
206
|
+
set: (values: Record<string, unknown>) => UpdateWhereBuilder<TTable>;
|
|
207
|
+
};
|
|
208
|
+
type UpdateWhereBuilder<TTable extends DrizzleTable = DrizzleTable> = {
|
|
209
|
+
where: (condition: unknown) => UpdateReturningBuilder<TTable>;
|
|
210
|
+
};
|
|
211
|
+
type UpdateReturningBuilder<TTable extends DrizzleTable = DrizzleTable> = Operation<void> & {
|
|
212
|
+
returning: () => Operation<InferRow<TTable>>;
|
|
213
|
+
};
|
|
214
|
+
type DeleteBuilder<TTable extends DrizzleTable = DrizzleTable> = {
|
|
215
|
+
where: (condition: unknown) => DeleteReturningBuilder<TTable>;
|
|
216
|
+
};
|
|
217
|
+
type DeleteReturningBuilder<TTable extends DrizzleTable = DrizzleTable> = Operation<void> & {
|
|
218
|
+
returning: () => Operation<InferRow<TTable>>;
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
export type { CursorParams as C, DbConfig as D, FindFirstOptions as F, InferQueryResult as I, Operation as O, PaginateOptions as P, QueryBuilder as Q, TransactionResult as T, UpdateBuilder as U, WithCan as W, Db as a, OffsetParams as b, CacheBackend as c, CacheConfig as d, CursorPage as e, DeleteBuilder as f, DeleteReturningBuilder as g, FindManyOptions as h, InferRow as i, InsertBuilder as j, InsertReturningBuilder as k, OffsetPage as l, PaginateParams as m, QueryCacheOptions as n, Tx as o, UpdateReturningBuilder as p, UpdateWhereBuilder as q };
|