@cfast/db 0.3.0 → 0.4.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/dist/index.js CHANGED
@@ -63,6 +63,7 @@ function checkOperationPermissions(grants, descriptors) {
63
63
  }
64
64
 
65
65
  // src/utils.ts
66
+ import { AsyncLocalStorage } from "async_hooks";
66
67
  import { and, or } from "drizzle-orm";
67
68
  import { getTableName as getTableName2 } from "@cfast/permissions";
68
69
  function deduplicateDescriptors(descriptors) {
@@ -77,12 +78,20 @@ function deduplicateDescriptors(descriptors) {
77
78
  }
78
79
  return result;
79
80
  }
81
+ var lookupCacheStorage = new AsyncLocalStorage();
80
82
  function createLookupCache() {
81
83
  return /* @__PURE__ */ new Map();
82
84
  }
85
+ function runWithLookupCache(fn, cache = createLookupCache()) {
86
+ return lookupCacheStorage.run(cache, fn);
87
+ }
88
+ function getActiveLookupCache(fallback) {
89
+ return lookupCacheStorage.getStore() ?? fallback;
90
+ }
83
91
  async function resolveGrantLookups(grant, user, lookupDb, cache) {
84
92
  if (!grant.with) return {};
85
- const cached = cache.get(grant);
93
+ const activeCache = getActiveLookupCache(cache);
94
+ const cached = activeCache.get(grant);
86
95
  if (cached) return cached;
87
96
  const entries = Object.entries(grant.with);
88
97
  const promise = (async () => {
@@ -94,7 +103,7 @@ async function resolveGrantLookups(grant, user, lookupDb, cache) {
94
103
  );
95
104
  return resolved;
96
105
  })();
97
- cache.set(grant, promise);
106
+ activeCache.set(grant, promise);
98
107
  return promise;
99
108
  }
100
109
  async function buildPermissionFilter(grants, action, table, user, unsafe, getLookupDb, cache) {
@@ -665,6 +674,139 @@ function createCacheManager(config) {
665
674
  };
666
675
  }
667
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
+
668
810
  // src/create-db.ts
669
811
  function createDb(config) {
670
812
  const lookupCache = createLookupCache();
@@ -744,11 +886,14 @@ function buildDb(config, isUnsafe, lookupCache) {
744
886
  permissions: allPermissions,
745
887
  async run(params) {
746
888
  const p = params ?? {};
889
+ if (operations.length === 0) {
890
+ return [];
891
+ }
747
892
  if (!isUnsafe) {
748
893
  checkOperationPermissions(config.grants, allPermissions);
749
894
  }
750
895
  const batchables = operations.map((op) => getBatchable(op));
751
- const everyOpBatchable = operations.length > 0 && batchables.every((b) => b !== void 0);
896
+ const everyOpBatchable = batchables.every((b) => b !== void 0);
752
897
  if (everyOpBatchable) {
753
898
  const sharedDb = drizzle3(config.d1, { schema: config.schema });
754
899
  await Promise.all(
@@ -775,6 +920,9 @@ function buildDb(config, isUnsafe, lookupCache) {
775
920
  }
776
921
  };
777
922
  },
923
+ transaction(callback) {
924
+ return runTransaction(db, callback);
925
+ },
778
926
  cache: {
779
927
  async invalidate(options) {
780
928
  if (!cacheManager) return;
@@ -787,13 +935,32 @@ function buildDb(config, isUnsafe, lookupCache) {
787
935
  }
788
936
  }
789
937
  }
938
+ },
939
+ clearLookupCache() {
940
+ lookupCache.clear();
790
941
  }
791
942
  };
792
943
  return db;
793
944
  }
945
+ function createAppDb(config) {
946
+ const { d1, schema, cache } = config;
947
+ const getD1 = typeof d1 === "function" ? d1 : () => d1;
948
+ return (grants, user) => createDb({
949
+ d1: getD1(),
950
+ schema,
951
+ grants,
952
+ user,
953
+ cache
954
+ });
955
+ }
794
956
 
795
957
  // src/compose.ts
796
958
  function compose(operations, executor) {
959
+ if (executor.length > 0 && executor.length !== operations.length) {
960
+ throw new Error(
961
+ `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.`
962
+ );
963
+ }
797
964
  const allPermissions = deduplicateDescriptors(
798
965
  operations.flatMap((op) => op.permissions)
799
966
  );
@@ -864,7 +1031,7 @@ function wrapForTracking(target, perms) {
864
1031
  });
865
1032
  }
866
1033
  function createTrackingDb(real, perms) {
867
- return {
1034
+ const trackingDb = {
868
1035
  query: (table) => wrapForTracking(real.query(table), perms),
869
1036
  insert: (table) => wrapForTracking(real.insert(table), perms),
870
1037
  update: (table) => wrapForTracking(real.update(table), perms),
@@ -881,8 +1048,24 @@ function createTrackingDb(real, perms) {
881
1048
  }
882
1049
  };
883
1050
  },
884
- cache: real.cache
1051
+ transaction: async (callback) => {
1052
+ const trackingTx = {
1053
+ query: trackingDb.query,
1054
+ insert: trackingDb.insert,
1055
+ update: trackingDb.update,
1056
+ delete: trackingDb.delete,
1057
+ transaction: (cb) => trackingDb.transaction(cb)
1058
+ };
1059
+ try {
1060
+ await callback(trackingTx);
1061
+ } catch {
1062
+ }
1063
+ return createSentinel();
1064
+ },
1065
+ cache: real.cache,
1066
+ clearLookupCache: () => real.clearLookupCache()
885
1067
  };
1068
+ return trackingDb;
886
1069
  }
887
1070
  function composeSequentialCallback(db, callback) {
888
1071
  const collected = [];
@@ -911,11 +1094,43 @@ function composeSequentialCallback(db, callback) {
911
1094
  }
912
1095
  };
913
1096
  }
1097
+
1098
+ // src/seed.ts
1099
+ function defineSeed(config) {
1100
+ const entries = Object.freeze(
1101
+ config.entries.map((entry) => ({
1102
+ table: entry.table,
1103
+ rows: Object.freeze([...entry.rows])
1104
+ }))
1105
+ );
1106
+ return {
1107
+ entries,
1108
+ async run(db) {
1109
+ const unsafeDb = db.unsafe();
1110
+ for (const entry of entries) {
1111
+ if (entry.rows.length === 0) continue;
1112
+ const ops = entry.rows.map(
1113
+ (row) => unsafeDb.insert(entry.table).values(row)
1114
+ );
1115
+ if (ops.length === 1) {
1116
+ await ops[0].run({});
1117
+ } else {
1118
+ await unsafeDb.batch(ops).run({});
1119
+ }
1120
+ }
1121
+ }
1122
+ };
1123
+ }
914
1124
  export {
1125
+ TransactionError,
915
1126
  compose,
916
1127
  composeSequential,
917
1128
  composeSequentialCallback,
1129
+ createAppDb,
918
1130
  createDb,
1131
+ createLookupCache,
1132
+ defineSeed,
919
1133
  parseCursorParams,
920
- parseOffsetParams
1134
+ parseOffsetParams,
1135
+ runWithLookupCache
921
1136
  };