@cfast/db 0.2.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/index.js CHANGED
@@ -12,14 +12,17 @@ import {
12
12
  } from "@cfast/permissions";
13
13
  import { CRUD_ACTIONS } from "@cfast/permissions";
14
14
  function resolvePermissionFilters(grants, action, table) {
15
+ const targetName = getTableName(table);
15
16
  const matching = grants.filter((g) => {
16
17
  const actionMatch = g.action === action || g.action === "manage";
17
- const tableMatch = g.subject === "all" || g.subject === table || typeof g.subject === "object" && getTableName(g.subject) === getTableName(table);
18
+ const tableMatch = g.subject === "all" || g.subject === table || getTableName(g.subject) === targetName;
18
19
  return actionMatch && tableMatch;
19
20
  });
20
21
  if (matching.length === 0) return [];
21
22
  if (matching.some((g) => !g.where)) return [];
22
- return matching.filter((g) => !!g.where).map((g) => g.where);
23
+ return matching.filter(
24
+ (g) => !!g.where
25
+ );
23
26
  }
24
27
  function grantMatchesAction(grantAction, requiredAction) {
25
28
  if (grantAction === requiredAction) return true;
@@ -60,6 +63,7 @@ function checkOperationPermissions(grants, descriptors) {
60
63
  }
61
64
 
62
65
  // src/utils.ts
66
+ import { AsyncLocalStorage } from "async_hooks";
63
67
  import { and, or } from "drizzle-orm";
64
68
  import { getTableName as getTableName2 } from "@cfast/permissions";
65
69
  function deduplicateDescriptors(descriptors) {
@@ -74,13 +78,46 @@ function deduplicateDescriptors(descriptors) {
74
78
  }
75
79
  return result;
76
80
  }
77
- function buildPermissionFilter(grants, action, table, user, unsafe) {
81
+ var lookupCacheStorage = new AsyncLocalStorage();
82
+ function createLookupCache() {
83
+ return /* @__PURE__ */ new Map();
84
+ }
85
+ function runWithLookupCache(fn, cache = createLookupCache()) {
86
+ return lookupCacheStorage.run(cache, fn);
87
+ }
88
+ function getActiveLookupCache(fallback) {
89
+ return lookupCacheStorage.getStore() ?? fallback;
90
+ }
91
+ async function resolveGrantLookups(grant, user, lookupDb, cache) {
92
+ if (!grant.with) return {};
93
+ const activeCache = getActiveLookupCache(cache);
94
+ const cached = activeCache.get(grant);
95
+ if (cached) return cached;
96
+ const entries = Object.entries(grant.with);
97
+ const promise = (async () => {
98
+ const resolved = {};
99
+ await Promise.all(
100
+ entries.map(async ([key, fn]) => {
101
+ resolved[key] = await fn(user, lookupDb);
102
+ })
103
+ );
104
+ return resolved;
105
+ })();
106
+ activeCache.set(grant, promise);
107
+ return promise;
108
+ }
109
+ async function buildPermissionFilter(grants, action, table, user, unsafe, getLookupDb, cache) {
78
110
  if (unsafe || !user) return void 0;
79
- const filters = resolvePermissionFilters(grants, action, table);
80
- if (filters.length === 0) return void 0;
111
+ const matching = resolvePermissionFilters(grants, action, table);
112
+ if (matching.length === 0) return void 0;
81
113
  const columns = table;
82
- const clauses = filters.map(
83
- (fn) => fn(columns, user)
114
+ const needsLookupDb = matching.some((g) => g.with !== void 0);
115
+ const lookupDb = needsLookupDb ? getLookupDb() : void 0;
116
+ const lookupSets = await Promise.all(
117
+ matching.map((g) => resolveGrantLookups(g, user, lookupDb, cache))
118
+ );
119
+ const clauses = matching.map(
120
+ (g, i) => g.where(columns, user, lookupSets[i])
84
121
  );
85
122
  return or(...clauses);
86
123
  }
@@ -191,12 +228,14 @@ function buildQueryOperation(config, db, tableKey, method, options) {
191
228
  if (!config.unsafe) {
192
229
  checkOperationPermissions(config.grants, permissions);
193
230
  }
194
- const permFilter = buildPermissionFilter(
231
+ const permFilter = await buildPermissionFilter(
195
232
  config.grants,
196
233
  "read",
197
234
  config.table,
198
235
  config.user,
199
- config.unsafe
236
+ config.unsafe,
237
+ config.getLookupDb,
238
+ config.lookupCache
200
239
  );
201
240
  const userWhere = options?.where;
202
241
  const combinedWhere = combineWhere(userWhere, permFilter);
@@ -243,16 +282,18 @@ function createQueryBuilder(config) {
243
282
  if (!tableKey) throw new Error("Table not found in schema");
244
283
  return tableKey;
245
284
  }
246
- function checkAndBuildWhere(extraWhere) {
285
+ async function checkAndBuildWhere(extraWhere) {
247
286
  if (!config.unsafe) {
248
287
  checkOperationPermissions(config.grants, permissions);
249
288
  }
250
- const permFilter = buildPermissionFilter(
289
+ const permFilter = await buildPermissionFilter(
251
290
  config.grants,
252
291
  "read",
253
292
  config.table,
254
293
  config.user,
255
- config.unsafe
294
+ config.unsafe,
295
+ config.getLookupDb,
296
+ config.lookupCache
256
297
  );
257
298
  return combineWhere(
258
299
  combineWhere(options?.where, permFilter),
@@ -282,7 +323,7 @@ function createQueryBuilder(config) {
282
323
  const cursorValues = decodeCursor(params.cursor);
283
324
  const direction = options?.orderDirection ?? "desc";
284
325
  const cursorWhere = cursorValues ? buildCursorWhere(cursorColumns, cursorValues, direction) : void 0;
285
- const combinedWhere = checkAndBuildWhere(cursorWhere);
326
+ const combinedWhere = await checkAndBuildWhere(cursorWhere);
286
327
  const queryOptions = buildBaseQueryOptions(combinedWhere);
287
328
  queryOptions.limit = params.limit + 1;
288
329
  const queryTable = getQueryTable(db, key);
@@ -303,7 +344,7 @@ function createQueryBuilder(config) {
303
344
  permissions,
304
345
  async run(_params) {
305
346
  const key = ensureTableKey();
306
- const combinedWhere = checkAndBuildWhere();
347
+ const combinedWhere = await checkAndBuildWhere();
307
348
  const queryOptions = buildBaseQueryOptions(combinedWhere);
308
349
  queryOptions.limit = params.limit;
309
350
  queryOptions.offset = (params.page - 1) * params.limit;
@@ -344,10 +385,11 @@ function checkIfNeeded(config, grants, permissions) {
344
385
  checkOperationPermissions(grants, permissions);
345
386
  }
346
387
  }
347
- function buildMutationWithReturning(config, permissions, tableName, buildQuery) {
388
+ function buildMutationWithReturning(config, permissions, tableName, buildQuery, prepareFn) {
348
389
  const drizzleDb = drizzle2(config.d1, { schema: config.schema });
349
390
  const runOnce = async (returning) => {
350
391
  checkIfNeeded(config, config.grants, permissions);
392
+ if (prepareFn) await prepareFn();
351
393
  const built = buildQuery(drizzleDb, returning);
352
394
  const result = await built.execute(returning);
353
395
  config.onMutate?.(tableName);
@@ -366,6 +408,7 @@ function buildMutationWithReturning(config, permissions, tableName, buildQuery)
366
408
  }
367
409
  };
368
410
  returningOp[BATCHABLE] = {
411
+ prepare: prepareFn,
369
412
  build: (sharedDb) => buildQuery(sharedDb, true).query,
370
413
  tableName,
371
414
  withResult: true
@@ -374,6 +417,7 @@ function buildMutationWithReturning(config, permissions, tableName, buildQuery)
374
417
  }
375
418
  };
376
419
  baseOp[BATCHABLE] = {
420
+ prepare: prepareFn,
377
421
  build: (sharedDb) => buildQuery(sharedDb, false).query,
378
422
  tableName,
379
423
  withResult: false
@@ -409,17 +453,31 @@ function createUpdateBuilder(config) {
409
453
  set(values) {
410
454
  return {
411
455
  where(condition) {
412
- const permFilter = buildPermissionFilter(
413
- config.grants,
414
- "update",
415
- config.table,
416
- config.user,
417
- config.unsafe
418
- );
419
- const combinedWhere = combineWhere(condition, permFilter);
456
+ let resolvedCombinedWhere;
457
+ let preparePromise;
458
+ const prepareFn = () => {
459
+ if (!preparePromise) {
460
+ preparePromise = (async () => {
461
+ const permFilter = await buildPermissionFilter(
462
+ config.grants,
463
+ "update",
464
+ config.table,
465
+ config.user,
466
+ config.unsafe,
467
+ config.getLookupDb,
468
+ config.lookupCache
469
+ );
470
+ resolvedCombinedWhere = combineWhere(
471
+ condition,
472
+ permFilter
473
+ );
474
+ })();
475
+ }
476
+ return preparePromise;
477
+ };
420
478
  const buildQuery = (sharedDb, returning) => {
421
479
  const base = sharedDb.update(config.table).set(values);
422
- if (combinedWhere) base.where(combinedWhere);
480
+ if (resolvedCombinedWhere) base.where(resolvedCombinedWhere);
423
481
  const query = returning ? base.returning() : base;
424
482
  return {
425
483
  query,
@@ -431,7 +489,13 @@ function createUpdateBuilder(config) {
431
489
  }
432
490
  };
433
491
  };
434
- return buildMutationWithReturning(config, permissions, tableName, buildQuery);
492
+ return buildMutationWithReturning(
493
+ config,
494
+ permissions,
495
+ tableName,
496
+ buildQuery,
497
+ prepareFn
498
+ );
435
499
  }
436
500
  };
437
501
  }
@@ -442,17 +506,31 @@ function createDeleteBuilder(config) {
442
506
  const tableName = getTableName2(config.table);
443
507
  return {
444
508
  where(condition) {
445
- const permFilter = buildPermissionFilter(
446
- config.grants,
447
- "delete",
448
- config.table,
449
- config.user,
450
- config.unsafe
451
- );
452
- const combinedWhere = combineWhere(condition, permFilter);
509
+ let resolvedCombinedWhere;
510
+ let preparePromise;
511
+ const prepareFn = () => {
512
+ if (!preparePromise) {
513
+ preparePromise = (async () => {
514
+ const permFilter = await buildPermissionFilter(
515
+ config.grants,
516
+ "delete",
517
+ config.table,
518
+ config.user,
519
+ config.unsafe,
520
+ config.getLookupDb,
521
+ config.lookupCache
522
+ );
523
+ resolvedCombinedWhere = combineWhere(
524
+ condition,
525
+ permFilter
526
+ );
527
+ })();
528
+ }
529
+ return preparePromise;
530
+ };
453
531
  const buildQuery = (sharedDb, returning) => {
454
532
  const base = sharedDb.delete(config.table);
455
- if (combinedWhere) base.where(combinedWhere);
533
+ if (resolvedCombinedWhere) base.where(resolvedCombinedWhere);
456
534
  const query = returning ? base.returning() : base;
457
535
  return {
458
536
  query,
@@ -464,7 +542,13 @@ function createDeleteBuilder(config) {
464
542
  }
465
543
  };
466
544
  };
467
- return buildMutationWithReturning(config, permissions, tableName, buildQuery);
545
+ return buildMutationWithReturning(
546
+ config,
547
+ permissions,
548
+ tableName,
549
+ buildQuery,
550
+ prepareFn
551
+ );
468
552
  }
469
553
  };
470
554
  }
@@ -590,16 +674,156 @@ function createCacheManager(config) {
590
674
  };
591
675
  }
592
676
 
677
+ // src/transaction.ts
678
+ var TransactionError = class extends Error {
679
+ constructor(message) {
680
+ super(message);
681
+ this.name = "TransactionError";
682
+ }
683
+ };
684
+ function wrapWriteOperation(ctx, op) {
685
+ const record = (toQueue) => {
686
+ if (ctx.closed) {
687
+ throw new TransactionError(
688
+ "Cannot run an operation after the transaction has committed or aborted."
689
+ );
690
+ }
691
+ if (ctx.aborted) {
692
+ throw new TransactionError(
693
+ "Transaction has been aborted; no further operations may run."
694
+ );
695
+ }
696
+ ctx.pending.push({ op: toQueue });
697
+ };
698
+ const proxied = {
699
+ permissions: op.permissions,
700
+ async run() {
701
+ record(op);
702
+ return void 0;
703
+ }
704
+ };
705
+ if (typeof op.returning === "function") {
706
+ proxied.returning = () => {
707
+ const returningOp = op.returning();
708
+ return {
709
+ permissions: returningOp.permissions,
710
+ async run() {
711
+ record(returningOp);
712
+ return void 0;
713
+ }
714
+ };
715
+ };
716
+ }
717
+ return proxied;
718
+ }
719
+ function createTxHandle(db, ctx) {
720
+ return {
721
+ query: (table) => {
722
+ return db.query(table);
723
+ },
724
+ insert: (table) => {
725
+ const realBuilder = db.insert(table);
726
+ return {
727
+ values: (values) => {
728
+ const realOp = realBuilder.values(values);
729
+ return wrapWriteOperation(ctx, realOp);
730
+ }
731
+ };
732
+ },
733
+ update: (table) => {
734
+ const realBuilder = db.update(table);
735
+ return {
736
+ set: (values) => {
737
+ const realSet = realBuilder.set(values);
738
+ return {
739
+ where: (condition) => {
740
+ const realOp = realSet.where(condition);
741
+ return wrapWriteOperation(ctx, realOp);
742
+ }
743
+ };
744
+ }
745
+ };
746
+ },
747
+ delete: (table) => {
748
+ const realBuilder = db.delete(table);
749
+ return {
750
+ where: (condition) => {
751
+ const realOp = realBuilder.where(condition);
752
+ return wrapWriteOperation(ctx, realOp);
753
+ }
754
+ };
755
+ },
756
+ transaction: (callback) => {
757
+ return runTransaction(db, callback, ctx);
758
+ }
759
+ };
760
+ }
761
+ async function flushWrites(db, pending) {
762
+ if (pending.length === 0) return;
763
+ const ops = pending.map((p) => p.op);
764
+ for (const op of ops) {
765
+ if (getBatchable(op) === void 0) {
766
+ throw new TransactionError(
767
+ "Internal error: a pending transaction operation was not batchable. Transactions only accept operations produced by tx.insert/update/delete."
768
+ );
769
+ }
770
+ }
771
+ await db.batch(ops).run({});
772
+ }
773
+ async function runTransaction(db, callback, parentCtx) {
774
+ if (parentCtx) {
775
+ if (parentCtx.closed || parentCtx.aborted) {
776
+ throw new TransactionError(
777
+ "Cannot start a nested transaction: parent has already committed or aborted."
778
+ );
779
+ }
780
+ const nestedTx = createTxHandle(db, parentCtx);
781
+ return callback(nestedTx);
782
+ }
783
+ const ctx = {
784
+ pending: [],
785
+ closed: false,
786
+ aborted: false,
787
+ nested: false
788
+ };
789
+ const tx = createTxHandle(db, ctx);
790
+ let result;
791
+ try {
792
+ result = await callback(tx);
793
+ } catch (err) {
794
+ ctx.aborted = true;
795
+ ctx.closed = true;
796
+ ctx.pending.length = 0;
797
+ throw err;
798
+ }
799
+ try {
800
+ await flushWrites(db, ctx.pending);
801
+ ctx.closed = true;
802
+ } catch (err) {
803
+ ctx.aborted = true;
804
+ ctx.closed = true;
805
+ throw err;
806
+ }
807
+ return result;
808
+ }
809
+
593
810
  // src/create-db.ts
594
811
  function createDb(config) {
595
- return buildDb(config, false);
812
+ const lookupCache = createLookupCache();
813
+ return buildDb(config, false, lookupCache);
596
814
  }
597
- function buildDb(config, isUnsafe) {
815
+ function buildDb(config, isUnsafe, lookupCache) {
598
816
  const cacheManager = config.cache === false ? null : createCacheManager(config.cache ?? { backend: "cache-api" });
599
817
  const onMutate = (tableName) => {
600
818
  cacheManager?.invalidateTable(tableName);
601
819
  };
602
- return {
820
+ let lookupDbCache = null;
821
+ const getLookupDb = () => {
822
+ if (lookupDbCache) return lookupDbCache;
823
+ lookupDbCache = isUnsafe ? db : buildDb(config, true, lookupCache);
824
+ return lookupDbCache;
825
+ };
826
+ const db = {
603
827
  query(table) {
604
828
  return createQueryBuilder({
605
829
  d1: config.d1,
@@ -607,7 +831,9 @@ function buildDb(config, isUnsafe) {
607
831
  grants: config.grants,
608
832
  user: config.user,
609
833
  table,
610
- unsafe: isUnsafe
834
+ unsafe: isUnsafe,
835
+ lookupCache,
836
+ getLookupDb
611
837
  });
612
838
  },
613
839
  insert(table) {
@@ -618,7 +844,9 @@ function buildDb(config, isUnsafe) {
618
844
  user: config.user,
619
845
  table,
620
846
  unsafe: isUnsafe,
621
- onMutate
847
+ onMutate,
848
+ lookupCache,
849
+ getLookupDb
622
850
  });
623
851
  },
624
852
  update(table) {
@@ -629,7 +857,9 @@ function buildDb(config, isUnsafe) {
629
857
  user: config.user,
630
858
  table,
631
859
  unsafe: isUnsafe,
632
- onMutate
860
+ onMutate,
861
+ lookupCache,
862
+ getLookupDb
633
863
  });
634
864
  },
635
865
  delete(table) {
@@ -640,11 +870,13 @@ function buildDb(config, isUnsafe) {
640
870
  user: config.user,
641
871
  table,
642
872
  unsafe: isUnsafe,
643
- onMutate
873
+ onMutate,
874
+ lookupCache,
875
+ getLookupDb
644
876
  });
645
877
  },
646
878
  unsafe() {
647
- return buildDb(config, true);
879
+ return buildDb(config, true, lookupCache);
648
880
  },
649
881
  batch(operations) {
650
882
  const allPermissions = deduplicateDescriptors(
@@ -654,13 +886,19 @@ function buildDb(config, isUnsafe) {
654
886
  permissions: allPermissions,
655
887
  async run(params) {
656
888
  const p = params ?? {};
889
+ if (operations.length === 0) {
890
+ return [];
891
+ }
657
892
  if (!isUnsafe) {
658
893
  checkOperationPermissions(config.grants, allPermissions);
659
894
  }
660
895
  const batchables = operations.map((op) => getBatchable(op));
661
- const everyOpBatchable = operations.length > 0 && batchables.every((b) => b !== void 0);
896
+ const everyOpBatchable = batchables.every((b) => b !== void 0);
662
897
  if (everyOpBatchable) {
663
898
  const sharedDb = drizzle3(config.d1, { schema: config.schema });
899
+ await Promise.all(
900
+ batchables.map((b) => b.prepare?.() ?? Promise.resolve())
901
+ );
664
902
  const items = batchables.map((b) => b.build(sharedDb));
665
903
  const batchResults = await sharedDb.batch(
666
904
  items
@@ -682,6 +920,9 @@ function buildDb(config, isUnsafe) {
682
920
  }
683
921
  };
684
922
  },
923
+ transaction(callback) {
924
+ return runTransaction(db, callback);
925
+ },
685
926
  cache: {
686
927
  async invalidate(options) {
687
928
  if (!cacheManager) return;
@@ -696,10 +937,27 @@ function buildDb(config, isUnsafe) {
696
937
  }
697
938
  }
698
939
  };
940
+ return db;
941
+ }
942
+ function createAppDb(config) {
943
+ const { d1, schema, cache } = config;
944
+ const getD1 = typeof d1 === "function" ? d1 : () => d1;
945
+ return (grants, user) => createDb({
946
+ d1: getD1(),
947
+ schema,
948
+ grants,
949
+ user,
950
+ cache
951
+ });
699
952
  }
700
953
 
701
954
  // src/compose.ts
702
955
  function compose(operations, executor) {
956
+ if (executor.length > 0 && executor.length !== operations.length) {
957
+ throw new Error(
958
+ `compose(): executor declares ${executor.length} parameter(s) but ${operations.length} operation(s) were passed. Each operation must have a corresponding run-function parameter (in array order), otherwise sub-operations will silently go uninvoked. Prefer composeSequentialCallback() for by-name binding.`
959
+ );
960
+ }
703
961
  const allPermissions = deduplicateDescriptors(
704
962
  operations.flatMap((op) => op.permissions)
705
963
  );
@@ -770,7 +1028,7 @@ function wrapForTracking(target, perms) {
770
1028
  });
771
1029
  }
772
1030
  function createTrackingDb(real, perms) {
773
- return {
1031
+ const trackingDb = {
774
1032
  query: (table) => wrapForTracking(real.query(table), perms),
775
1033
  insert: (table) => wrapForTracking(real.insert(table), perms),
776
1034
  update: (table) => wrapForTracking(real.update(table), perms),
@@ -787,8 +1045,23 @@ function createTrackingDb(real, perms) {
787
1045
  }
788
1046
  };
789
1047
  },
1048
+ transaction: async (callback) => {
1049
+ const trackingTx = {
1050
+ query: trackingDb.query,
1051
+ insert: trackingDb.insert,
1052
+ update: trackingDb.update,
1053
+ delete: trackingDb.delete,
1054
+ transaction: (cb) => trackingDb.transaction(cb)
1055
+ };
1056
+ try {
1057
+ await callback(trackingTx);
1058
+ } catch {
1059
+ }
1060
+ return createSentinel();
1061
+ },
790
1062
  cache: real.cache
791
1063
  };
1064
+ return trackingDb;
792
1065
  }
793
1066
  function composeSequentialCallback(db, callback) {
794
1067
  const collected = [];
@@ -817,11 +1090,43 @@ function composeSequentialCallback(db, callback) {
817
1090
  }
818
1091
  };
819
1092
  }
1093
+
1094
+ // src/seed.ts
1095
+ function defineSeed(config) {
1096
+ const entries = Object.freeze(
1097
+ config.entries.map((entry) => ({
1098
+ table: entry.table,
1099
+ rows: Object.freeze([...entry.rows])
1100
+ }))
1101
+ );
1102
+ return {
1103
+ entries,
1104
+ async run(db) {
1105
+ const unsafeDb = db.unsafe();
1106
+ for (const entry of entries) {
1107
+ if (entry.rows.length === 0) continue;
1108
+ const ops = entry.rows.map(
1109
+ (row) => unsafeDb.insert(entry.table).values(row)
1110
+ );
1111
+ if (ops.length === 1) {
1112
+ await ops[0].run({});
1113
+ } else {
1114
+ await unsafeDb.batch(ops).run({});
1115
+ }
1116
+ }
1117
+ }
1118
+ };
1119
+ }
820
1120
  export {
1121
+ TransactionError,
821
1122
  compose,
822
1123
  composeSequential,
823
1124
  composeSequentialCallback,
1125
+ createAppDb,
824
1126
  createDb,
1127
+ createLookupCache,
1128
+ defineSeed,
825
1129
  parseCursorParams,
826
- parseOffsetParams
1130
+ parseOffsetParams,
1131
+ runWithLookupCache
827
1132
  };