@cfast/db 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +686 -0
- package/dist/index.d.ts +636 -0
- package/dist/index.js +628 -0
- package/package.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
// src/query-builder.ts
|
|
2
|
+
import { count } from "drizzle-orm";
|
|
3
|
+
import { drizzle } from "drizzle-orm/d1";
|
|
4
|
+
|
|
5
|
+
// src/permissions.ts
|
|
6
|
+
import {
|
|
7
|
+
ForbiddenError,
|
|
8
|
+
getTableName
|
|
9
|
+
} from "@cfast/permissions";
|
|
10
|
+
import { CRUD_ACTIONS } from "@cfast/permissions";
|
|
11
|
+
function resolvePermissionFilters(grants, action, table) {
|
|
12
|
+
const matching = grants.filter((g) => {
|
|
13
|
+
const actionMatch = g.action === action || g.action === "manage";
|
|
14
|
+
const tableMatch = g.subject === "all" || g.subject === table || typeof g.subject === "object" && getTableName(g.subject) === getTableName(table);
|
|
15
|
+
return actionMatch && tableMatch;
|
|
16
|
+
});
|
|
17
|
+
if (matching.length === 0) return [];
|
|
18
|
+
if (matching.some((g) => !g.where)) return [];
|
|
19
|
+
return matching.filter((g) => !!g.where).map((g) => g.where);
|
|
20
|
+
}
|
|
21
|
+
function grantMatchesAction(grantAction, requiredAction) {
|
|
22
|
+
if (grantAction === requiredAction) return true;
|
|
23
|
+
if (grantAction === "manage") return true;
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
function grantMatchesTable(grantSubject, requiredTable) {
|
|
27
|
+
if (grantSubject === "all") return true;
|
|
28
|
+
if (grantSubject === requiredTable) return true;
|
|
29
|
+
return getTableName(grantSubject) === getTableName(requiredTable);
|
|
30
|
+
}
|
|
31
|
+
function hasGrantFor(grants, action, table) {
|
|
32
|
+
return grants.some(
|
|
33
|
+
(g) => grantMatchesAction(g.action, action) && grantMatchesTable(g.subject, table)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
function hasManagePermission(grants, table) {
|
|
37
|
+
if (hasGrantFor(grants, "manage", table)) return true;
|
|
38
|
+
return CRUD_ACTIONS.every((action) => hasGrantFor(grants, action, table));
|
|
39
|
+
}
|
|
40
|
+
function checkOperationPermissions(grants, descriptors) {
|
|
41
|
+
if (descriptors.length === 0) return;
|
|
42
|
+
for (const descriptor of descriptors) {
|
|
43
|
+
let permitted;
|
|
44
|
+
if (descriptor.action === "manage") {
|
|
45
|
+
permitted = hasManagePermission(grants, descriptor.table);
|
|
46
|
+
} else {
|
|
47
|
+
permitted = hasGrantFor(grants, descriptor.action, descriptor.table);
|
|
48
|
+
}
|
|
49
|
+
if (!permitted) {
|
|
50
|
+
throw new ForbiddenError({
|
|
51
|
+
action: descriptor.action,
|
|
52
|
+
table: descriptor.table,
|
|
53
|
+
descriptors: [descriptor]
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/utils.ts
|
|
60
|
+
import { and, or } from "drizzle-orm";
|
|
61
|
+
import { getTableName as getTableName2 } from "@cfast/permissions";
|
|
62
|
+
function deduplicateDescriptors(descriptors) {
|
|
63
|
+
const seen = /* @__PURE__ */ new Set();
|
|
64
|
+
const result = [];
|
|
65
|
+
for (const d of descriptors) {
|
|
66
|
+
const key = `${d.action}:${getTableName2(d.table)}`;
|
|
67
|
+
if (!seen.has(key)) {
|
|
68
|
+
seen.add(key);
|
|
69
|
+
result.push(d);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
function buildPermissionFilter(grants, action, table, user, unsafe) {
|
|
75
|
+
if (unsafe || !user) return void 0;
|
|
76
|
+
const filters = resolvePermissionFilters(grants, action, table);
|
|
77
|
+
if (filters.length === 0) return void 0;
|
|
78
|
+
const columns = table;
|
|
79
|
+
const clauses = filters.map(
|
|
80
|
+
(fn) => fn(columns, user)
|
|
81
|
+
);
|
|
82
|
+
return or(...clauses);
|
|
83
|
+
}
|
|
84
|
+
function combineWhere(userCondition, permFilter) {
|
|
85
|
+
if (permFilter && userCondition) return and(userCondition, permFilter);
|
|
86
|
+
if (permFilter) return permFilter.getSQL();
|
|
87
|
+
if (userCondition) return userCondition.getSQL();
|
|
88
|
+
return void 0;
|
|
89
|
+
}
|
|
90
|
+
function makePermissions(unsafe, action, table) {
|
|
91
|
+
return unsafe ? [] : [{ action, table }];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/paginate.ts
|
|
95
|
+
import { and as and2, or as or2, lt, gt, eq } from "drizzle-orm";
|
|
96
|
+
function parseIntParam(raw, fallback) {
|
|
97
|
+
if (raw == null) return fallback;
|
|
98
|
+
const n = Number(raw);
|
|
99
|
+
return Number.isNaN(n) ? fallback : n;
|
|
100
|
+
}
|
|
101
|
+
function clamp(value, min, max) {
|
|
102
|
+
return Math.min(Math.max(value, min), max);
|
|
103
|
+
}
|
|
104
|
+
function parseCursorParams(request, options) {
|
|
105
|
+
const defaultLimit = options?.defaultLimit ?? 20;
|
|
106
|
+
const maxLimit = options?.maxLimit ?? 100;
|
|
107
|
+
const url = new URL(request.url);
|
|
108
|
+
const cursor = url.searchParams.get("cursor");
|
|
109
|
+
const limit = clamp(parseIntParam(url.searchParams.get("limit"), defaultLimit), 1, maxLimit);
|
|
110
|
+
return { type: "cursor", cursor, limit };
|
|
111
|
+
}
|
|
112
|
+
function parseOffsetParams(request, options) {
|
|
113
|
+
const defaultLimit = options?.defaultLimit ?? 20;
|
|
114
|
+
const maxLimit = options?.maxLimit ?? 100;
|
|
115
|
+
const url = new URL(request.url);
|
|
116
|
+
const page = Math.max(parseIntParam(url.searchParams.get("page"), 1), 1);
|
|
117
|
+
const limit = clamp(parseIntParam(url.searchParams.get("limit"), defaultLimit), 1, maxLimit);
|
|
118
|
+
return { type: "offset", page, limit };
|
|
119
|
+
}
|
|
120
|
+
function encodeCursor(values) {
|
|
121
|
+
return btoa(JSON.stringify({ v: values }));
|
|
122
|
+
}
|
|
123
|
+
function decodeCursor(cursor) {
|
|
124
|
+
if (cursor === null) return null;
|
|
125
|
+
try {
|
|
126
|
+
const parsed = JSON.parse(atob(cursor));
|
|
127
|
+
if (typeof parsed === "object" && parsed !== null && "v" in parsed) {
|
|
128
|
+
const record = parsed;
|
|
129
|
+
if (Array.isArray(record["v"])) {
|
|
130
|
+
return record["v"];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function buildCursorWhere(cursorColumns, cursorValues, direction = "desc") {
|
|
139
|
+
const compare = direction === "desc" ? lt : gt;
|
|
140
|
+
if (cursorColumns.length === 1) {
|
|
141
|
+
return compare(cursorColumns[0], cursorValues[0]);
|
|
142
|
+
}
|
|
143
|
+
const conditions = [];
|
|
144
|
+
for (let i = 0; i < cursorColumns.length; i++) {
|
|
145
|
+
const eqParts = [];
|
|
146
|
+
for (let j = 0; j < i; j++) {
|
|
147
|
+
eqParts.push(eq(cursorColumns[j], cursorValues[j]));
|
|
148
|
+
}
|
|
149
|
+
eqParts.push(compare(cursorColumns[i], cursorValues[i]));
|
|
150
|
+
conditions.push(and2(...eqParts));
|
|
151
|
+
}
|
|
152
|
+
return or2(...conditions);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/query-builder.ts
|
|
156
|
+
function getTableKey(schema, table) {
|
|
157
|
+
for (const [key, val] of Object.entries(schema)) {
|
|
158
|
+
if (val === table) return key;
|
|
159
|
+
}
|
|
160
|
+
return void 0;
|
|
161
|
+
}
|
|
162
|
+
function getQueryTable(db, key) {
|
|
163
|
+
return db.query[key];
|
|
164
|
+
}
|
|
165
|
+
function buildQueryOperation(config, db, tableKey, method, options) {
|
|
166
|
+
const permissions = makePermissions(config.unsafe, "read", config.table);
|
|
167
|
+
return {
|
|
168
|
+
permissions,
|
|
169
|
+
async run(_params) {
|
|
170
|
+
if (!config.unsafe) {
|
|
171
|
+
checkOperationPermissions(config.grants, permissions);
|
|
172
|
+
}
|
|
173
|
+
const permFilter = buildPermissionFilter(
|
|
174
|
+
config.grants,
|
|
175
|
+
"read",
|
|
176
|
+
config.table,
|
|
177
|
+
config.user,
|
|
178
|
+
config.unsafe
|
|
179
|
+
);
|
|
180
|
+
const userWhere = options?.where;
|
|
181
|
+
const combinedWhere = combineWhere(userWhere, permFilter);
|
|
182
|
+
const queryOptions = { ...options };
|
|
183
|
+
if (combinedWhere) {
|
|
184
|
+
queryOptions.where = combinedWhere;
|
|
185
|
+
}
|
|
186
|
+
delete queryOptions.cache;
|
|
187
|
+
const queryTable = getQueryTable(db, tableKey);
|
|
188
|
+
const result = await queryTable[method](queryOptions);
|
|
189
|
+
return method === "findFirst" ? result ?? void 0 : result;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function createQueryBuilder(config) {
|
|
194
|
+
const db = drizzle(config.d1, { schema: config.schema });
|
|
195
|
+
const tableKey = getTableKey(config.schema, config.table);
|
|
196
|
+
return {
|
|
197
|
+
findMany(options) {
|
|
198
|
+
if (!tableKey) {
|
|
199
|
+
return {
|
|
200
|
+
permissions: makePermissions(config.unsafe, "read", config.table),
|
|
201
|
+
async run() {
|
|
202
|
+
throw new Error("Table not found in schema");
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
return buildQueryOperation(config, db, tableKey, "findMany", options);
|
|
207
|
+
},
|
|
208
|
+
findFirst(options) {
|
|
209
|
+
if (!tableKey) {
|
|
210
|
+
return {
|
|
211
|
+
permissions: makePermissions(config.unsafe, "read", config.table),
|
|
212
|
+
async run() {
|
|
213
|
+
throw new Error("Table not found in schema");
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
return buildQueryOperation(config, db, tableKey, "findFirst", options);
|
|
218
|
+
},
|
|
219
|
+
paginate(params, options) {
|
|
220
|
+
const permissions = makePermissions(config.unsafe, "read", config.table);
|
|
221
|
+
function ensureTableKey() {
|
|
222
|
+
if (!tableKey) throw new Error("Table not found in schema");
|
|
223
|
+
return tableKey;
|
|
224
|
+
}
|
|
225
|
+
function checkAndBuildWhere(extraWhere) {
|
|
226
|
+
if (!config.unsafe) {
|
|
227
|
+
checkOperationPermissions(config.grants, permissions);
|
|
228
|
+
}
|
|
229
|
+
const permFilter = buildPermissionFilter(
|
|
230
|
+
config.grants,
|
|
231
|
+
"read",
|
|
232
|
+
config.table,
|
|
233
|
+
config.user,
|
|
234
|
+
config.unsafe
|
|
235
|
+
);
|
|
236
|
+
return combineWhere(
|
|
237
|
+
combineWhere(options?.where, permFilter),
|
|
238
|
+
extraWhere
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
function buildBaseQueryOptions(where) {
|
|
242
|
+
const qo = {};
|
|
243
|
+
if (options?.columns) qo.columns = options.columns;
|
|
244
|
+
if (options?.orderBy) qo.orderBy = options.orderBy;
|
|
245
|
+
if (options?.with) qo.with = options.with;
|
|
246
|
+
if (where) qo.where = where;
|
|
247
|
+
return qo;
|
|
248
|
+
}
|
|
249
|
+
if (params.type === "cursor") {
|
|
250
|
+
const cursorColumns = options?.cursorColumns ?? [];
|
|
251
|
+
return {
|
|
252
|
+
permissions,
|
|
253
|
+
async run(_params) {
|
|
254
|
+
const key = ensureTableKey();
|
|
255
|
+
const cursorValues = decodeCursor(params.cursor);
|
|
256
|
+
const direction = options?.orderDirection ?? "desc";
|
|
257
|
+
const cursorWhere = cursorValues ? buildCursorWhere(cursorColumns, cursorValues, direction) : void 0;
|
|
258
|
+
const combinedWhere = checkAndBuildWhere(cursorWhere);
|
|
259
|
+
const queryOptions = buildBaseQueryOptions(combinedWhere);
|
|
260
|
+
queryOptions.limit = params.limit + 1;
|
|
261
|
+
const queryTable = getQueryTable(db, key);
|
|
262
|
+
const rows = await queryTable.findMany(queryOptions);
|
|
263
|
+
const hasMore = rows.length > params.limit;
|
|
264
|
+
const items = hasMore ? rows.slice(0, params.limit) : rows;
|
|
265
|
+
let nextCursor = null;
|
|
266
|
+
if (hasMore && items.length > 0) {
|
|
267
|
+
const lastItem = items[items.length - 1];
|
|
268
|
+
const values = cursorColumns.map((col) => lastItem[col.name]);
|
|
269
|
+
nextCursor = encodeCursor(values);
|
|
270
|
+
}
|
|
271
|
+
return { items, nextCursor };
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
return {
|
|
276
|
+
permissions,
|
|
277
|
+
async run(_params) {
|
|
278
|
+
const key = ensureTableKey();
|
|
279
|
+
const combinedWhere = checkAndBuildWhere();
|
|
280
|
+
const queryOptions = buildBaseQueryOptions(combinedWhere);
|
|
281
|
+
queryOptions.limit = params.limit;
|
|
282
|
+
queryOptions.offset = (params.page - 1) * params.limit;
|
|
283
|
+
const countQuery = db.select({ count: count() }).from(config.table).$dynamic();
|
|
284
|
+
if (combinedWhere) countQuery.where(combinedWhere);
|
|
285
|
+
const queryTable = getQueryTable(db, key);
|
|
286
|
+
const [items, countResult] = await Promise.all([
|
|
287
|
+
queryTable.findMany(queryOptions),
|
|
288
|
+
countQuery
|
|
289
|
+
]);
|
|
290
|
+
const total = countResult[0]?.count ?? 0;
|
|
291
|
+
return {
|
|
292
|
+
items,
|
|
293
|
+
total,
|
|
294
|
+
page: params.page,
|
|
295
|
+
totalPages: Math.ceil(total / params.limit)
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/mutate-builder.ts
|
|
304
|
+
import { drizzle as drizzle2 } from "drizzle-orm/d1";
|
|
305
|
+
function checkIfNeeded(config, grants, permissions) {
|
|
306
|
+
if (!config.unsafe) {
|
|
307
|
+
checkOperationPermissions(grants, permissions);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
function buildMutationWithReturning(config, permissions, tableName, execute) {
|
|
311
|
+
return {
|
|
312
|
+
permissions,
|
|
313
|
+
async run(_params) {
|
|
314
|
+
checkIfNeeded(config, config.grants, permissions);
|
|
315
|
+
await execute(false);
|
|
316
|
+
config.onMutate?.(tableName);
|
|
317
|
+
},
|
|
318
|
+
returning() {
|
|
319
|
+
return {
|
|
320
|
+
permissions,
|
|
321
|
+
async run(_params) {
|
|
322
|
+
checkIfNeeded(config, config.grants, permissions);
|
|
323
|
+
const result = await execute(true);
|
|
324
|
+
config.onMutate?.(tableName);
|
|
325
|
+
return result;
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
function createInsertBuilder(config) {
|
|
332
|
+
const db = drizzle2(config.d1, { schema: config.schema });
|
|
333
|
+
const permissions = makePermissions(config.unsafe, "create", config.table);
|
|
334
|
+
const tableName = getTableName2(config.table);
|
|
335
|
+
return {
|
|
336
|
+
values(values) {
|
|
337
|
+
return buildMutationWithReturning(config, permissions, tableName, async (returning) => {
|
|
338
|
+
const query = db.insert(config.table).values(values);
|
|
339
|
+
if (returning) {
|
|
340
|
+
return query.returning().get();
|
|
341
|
+
}
|
|
342
|
+
await query.run();
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
function createUpdateBuilder(config) {
|
|
348
|
+
const db = drizzle2(config.d1, { schema: config.schema });
|
|
349
|
+
const permissions = makePermissions(config.unsafe, "update", config.table);
|
|
350
|
+
const tableName = getTableName2(config.table);
|
|
351
|
+
return {
|
|
352
|
+
set(values) {
|
|
353
|
+
return {
|
|
354
|
+
where(condition) {
|
|
355
|
+
const permFilter = buildPermissionFilter(
|
|
356
|
+
config.grants,
|
|
357
|
+
"update",
|
|
358
|
+
config.table,
|
|
359
|
+
config.user,
|
|
360
|
+
config.unsafe
|
|
361
|
+
);
|
|
362
|
+
const combinedWhere = combineWhere(condition, permFilter);
|
|
363
|
+
return buildMutationWithReturning(config, permissions, tableName, async (returning) => {
|
|
364
|
+
const query = db.update(config.table).set(values);
|
|
365
|
+
if (combinedWhere) query.where(combinedWhere);
|
|
366
|
+
if (returning) {
|
|
367
|
+
return query.returning().get();
|
|
368
|
+
}
|
|
369
|
+
await query.run();
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function createDeleteBuilder(config) {
|
|
377
|
+
const db = drizzle2(config.d1, { schema: config.schema });
|
|
378
|
+
const permissions = makePermissions(config.unsafe, "delete", config.table);
|
|
379
|
+
const tableName = getTableName2(config.table);
|
|
380
|
+
return {
|
|
381
|
+
where(condition) {
|
|
382
|
+
const permFilter = buildPermissionFilter(
|
|
383
|
+
config.grants,
|
|
384
|
+
"delete",
|
|
385
|
+
config.table,
|
|
386
|
+
config.user,
|
|
387
|
+
config.unsafe
|
|
388
|
+
);
|
|
389
|
+
const combinedWhere = combineWhere(condition, permFilter);
|
|
390
|
+
return buildMutationWithReturning(config, permissions, tableName, async (returning) => {
|
|
391
|
+
const query = db.delete(config.table);
|
|
392
|
+
if (combinedWhere) query.where(combinedWhere);
|
|
393
|
+
if (returning) {
|
|
394
|
+
return query.returning().get();
|
|
395
|
+
}
|
|
396
|
+
await query.run();
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// src/cache.ts
|
|
403
|
+
function parseTtl(ttl) {
|
|
404
|
+
const match = ttl.match(/^(\d+)(s|m|h)$/);
|
|
405
|
+
if (!match) return 60;
|
|
406
|
+
const value = parseInt(match[1], 10);
|
|
407
|
+
switch (match[2]) {
|
|
408
|
+
case "s":
|
|
409
|
+
return value;
|
|
410
|
+
case "m":
|
|
411
|
+
return value * 60;
|
|
412
|
+
case "h":
|
|
413
|
+
return value * 3600;
|
|
414
|
+
default:
|
|
415
|
+
return 60;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function simpleHash(str) {
|
|
419
|
+
let hash = 0;
|
|
420
|
+
for (let i = 0; i < str.length; i++) {
|
|
421
|
+
const char = str.charCodeAt(i);
|
|
422
|
+
hash = (hash << 5) - hash + char | 0;
|
|
423
|
+
}
|
|
424
|
+
return (hash >>> 0).toString(36);
|
|
425
|
+
}
|
|
426
|
+
function createCacheManager(config) {
|
|
427
|
+
const tableVersions = /* @__PURE__ */ new Map();
|
|
428
|
+
const tagToKeys = /* @__PURE__ */ new Map();
|
|
429
|
+
const defaultTtl = config.ttl ?? "60s";
|
|
430
|
+
const excludedTables = new Set(config.exclude ?? []);
|
|
431
|
+
return {
|
|
432
|
+
generateKey(sql, role, tableVersion) {
|
|
433
|
+
return `cfast:${role}:v${tableVersion}:${simpleHash(sql)}`;
|
|
434
|
+
},
|
|
435
|
+
getTableVersion(table) {
|
|
436
|
+
return tableVersions.get(table) ?? 0;
|
|
437
|
+
},
|
|
438
|
+
invalidateTable(table) {
|
|
439
|
+
const current = tableVersions.get(table) ?? 0;
|
|
440
|
+
tableVersions.set(table, current + 1);
|
|
441
|
+
config.onInvalidate?.([table]);
|
|
442
|
+
},
|
|
443
|
+
async invalidateTags(tags) {
|
|
444
|
+
if (config.backend === "kv" && config.kv) {
|
|
445
|
+
for (const tag of tags) {
|
|
446
|
+
const keys = tagToKeys.get(tag);
|
|
447
|
+
if (keys) {
|
|
448
|
+
for (const key of keys) {
|
|
449
|
+
await config.kv.delete(key);
|
|
450
|
+
}
|
|
451
|
+
tagToKeys.delete(tag);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
},
|
|
456
|
+
isExcluded(table) {
|
|
457
|
+
return excludedTables.has(table);
|
|
458
|
+
},
|
|
459
|
+
getTtlSeconds(override) {
|
|
460
|
+
return parseTtl(override ?? defaultTtl);
|
|
461
|
+
},
|
|
462
|
+
async get(key, tableName) {
|
|
463
|
+
if (config.backend === "cache-api") {
|
|
464
|
+
const cache = await caches.open("cfast-db");
|
|
465
|
+
const response = await cache.match(
|
|
466
|
+
new Request(`https://cfast-cache/${key}`)
|
|
467
|
+
);
|
|
468
|
+
if (response) {
|
|
469
|
+
config.onHit?.(key, tableName);
|
|
470
|
+
return response.json();
|
|
471
|
+
}
|
|
472
|
+
config.onMiss?.(key, tableName);
|
|
473
|
+
return void 0;
|
|
474
|
+
}
|
|
475
|
+
if (config.backend === "kv" && config.kv) {
|
|
476
|
+
const value = await config.kv.get(key, "json");
|
|
477
|
+
if (value !== null) {
|
|
478
|
+
config.onHit?.(key, tableName);
|
|
479
|
+
return value;
|
|
480
|
+
}
|
|
481
|
+
config.onMiss?.(key, tableName);
|
|
482
|
+
return void 0;
|
|
483
|
+
}
|
|
484
|
+
return void 0;
|
|
485
|
+
},
|
|
486
|
+
async set(key, value, _tableName, options) {
|
|
487
|
+
if (options === false) return;
|
|
488
|
+
const ttl = this.getTtlSeconds(
|
|
489
|
+
typeof options === "object" ? options?.ttl : void 0
|
|
490
|
+
);
|
|
491
|
+
if (config.backend === "cache-api") {
|
|
492
|
+
const cache = await caches.open("cfast-db");
|
|
493
|
+
let cacheControl = `max-age=${ttl}`;
|
|
494
|
+
if (typeof options === "object" && options?.staleWhileRevalidate) {
|
|
495
|
+
const swr = parseTtl(options.staleWhileRevalidate);
|
|
496
|
+
cacheControl = `max-age=${ttl}, stale-while-revalidate=${swr}`;
|
|
497
|
+
}
|
|
498
|
+
await cache.put(
|
|
499
|
+
new Request(`https://cfast-cache/${key}`),
|
|
500
|
+
new Response(JSON.stringify(value), {
|
|
501
|
+
headers: {
|
|
502
|
+
"Content-Type": "application/json",
|
|
503
|
+
"Cache-Control": cacheControl
|
|
504
|
+
}
|
|
505
|
+
})
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
if (config.backend === "kv" && config.kv) {
|
|
509
|
+
await config.kv.put(key, JSON.stringify(value), {
|
|
510
|
+
expirationTtl: ttl
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
if (typeof options === "object" && options?.tags) {
|
|
514
|
+
for (const tag of options.tags) {
|
|
515
|
+
if (!tagToKeys.has(tag)) tagToKeys.set(tag, /* @__PURE__ */ new Set());
|
|
516
|
+
tagToKeys.get(tag).add(key);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// src/create-db.ts
|
|
524
|
+
function createDb(config) {
|
|
525
|
+
return buildDb(config, false);
|
|
526
|
+
}
|
|
527
|
+
function buildDb(config, isUnsafe) {
|
|
528
|
+
const cacheManager = config.cache === false ? null : createCacheManager(config.cache ?? { backend: "cache-api" });
|
|
529
|
+
const onMutate = (tableName) => {
|
|
530
|
+
cacheManager?.invalidateTable(tableName);
|
|
531
|
+
};
|
|
532
|
+
return {
|
|
533
|
+
query(table) {
|
|
534
|
+
return createQueryBuilder({
|
|
535
|
+
d1: config.d1,
|
|
536
|
+
schema: config.schema,
|
|
537
|
+
grants: config.grants,
|
|
538
|
+
user: config.user,
|
|
539
|
+
table,
|
|
540
|
+
unsafe: isUnsafe
|
|
541
|
+
});
|
|
542
|
+
},
|
|
543
|
+
insert(table) {
|
|
544
|
+
return createInsertBuilder({
|
|
545
|
+
d1: config.d1,
|
|
546
|
+
schema: config.schema,
|
|
547
|
+
grants: config.grants,
|
|
548
|
+
user: config.user,
|
|
549
|
+
table,
|
|
550
|
+
unsafe: isUnsafe,
|
|
551
|
+
onMutate
|
|
552
|
+
});
|
|
553
|
+
},
|
|
554
|
+
update(table) {
|
|
555
|
+
return createUpdateBuilder({
|
|
556
|
+
d1: config.d1,
|
|
557
|
+
schema: config.schema,
|
|
558
|
+
grants: config.grants,
|
|
559
|
+
user: config.user,
|
|
560
|
+
table,
|
|
561
|
+
unsafe: isUnsafe,
|
|
562
|
+
onMutate
|
|
563
|
+
});
|
|
564
|
+
},
|
|
565
|
+
delete(table) {
|
|
566
|
+
return createDeleteBuilder({
|
|
567
|
+
d1: config.d1,
|
|
568
|
+
schema: config.schema,
|
|
569
|
+
grants: config.grants,
|
|
570
|
+
user: config.user,
|
|
571
|
+
table,
|
|
572
|
+
unsafe: isUnsafe,
|
|
573
|
+
onMutate
|
|
574
|
+
});
|
|
575
|
+
},
|
|
576
|
+
unsafe() {
|
|
577
|
+
return buildDb(config, true);
|
|
578
|
+
},
|
|
579
|
+
batch(operations) {
|
|
580
|
+
const allPermissions = deduplicateDescriptors(
|
|
581
|
+
operations.flatMap((op) => op.permissions)
|
|
582
|
+
);
|
|
583
|
+
return {
|
|
584
|
+
permissions: allPermissions,
|
|
585
|
+
async run(params) {
|
|
586
|
+
const results = [];
|
|
587
|
+
for (const op of operations) {
|
|
588
|
+
results.push(await op.run(params));
|
|
589
|
+
}
|
|
590
|
+
return results;
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
},
|
|
594
|
+
cache: {
|
|
595
|
+
async invalidate(options) {
|
|
596
|
+
if (!cacheManager) return;
|
|
597
|
+
if (options.tags) {
|
|
598
|
+
await cacheManager.invalidateTags(options.tags);
|
|
599
|
+
}
|
|
600
|
+
if (options.tables) {
|
|
601
|
+
for (const table of options.tables) {
|
|
602
|
+
cacheManager.invalidateTable(table);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/compose.ts
|
|
611
|
+
function compose(operations, executor) {
|
|
612
|
+
const allPermissions = deduplicateDescriptors(
|
|
613
|
+
operations.flatMap((op) => op.permissions)
|
|
614
|
+
);
|
|
615
|
+
return {
|
|
616
|
+
permissions: allPermissions,
|
|
617
|
+
async run(_params) {
|
|
618
|
+
const runs = operations.map((op) => op.run);
|
|
619
|
+
return executor(...runs);
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
export {
|
|
624
|
+
compose,
|
|
625
|
+
createDb,
|
|
626
|
+
parseCursorParams,
|
|
627
|
+
parseOffsetParams
|
|
628
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cfast/db",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Permission-aware Drizzle queries for Cloudflare D1",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/DanielMSchmidt/cfast.git",
|
|
9
|
+
"directory": "packages/db"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "dist/index.js",
|
|
13
|
+
"types": "dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts"
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist"
|
|
22
|
+
],
|
|
23
|
+
"sideEffects": false,
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"drizzle-orm": ">=0.35"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@cfast/permissions": "0.0.1"
|
|
32
|
+
},
|
|
33
|
+
"peerDependenciesMeta": {
|
|
34
|
+
"@cloudflare/workers-types": {
|
|
35
|
+
"optional": true
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@cloudflare/workers-types": "^4.20260305.1",
|
|
40
|
+
"drizzle-orm": "^0.45.1",
|
|
41
|
+
"tsup": "^8",
|
|
42
|
+
"typescript": "^5.7",
|
|
43
|
+
"vitest": "^4.1.0"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
47
|
+
"dev": "tsup src/index.ts --format esm --dts --watch",
|
|
48
|
+
"typecheck": "tsc --noEmit",
|
|
49
|
+
"lint": "eslint src/",
|
|
50
|
+
"test": "vitest run"
|
|
51
|
+
}
|
|
52
|
+
}
|