@fitzzero/quickdraw-core 5.1.2 → 5.1.3

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.
Files changed (124) hide show
  1. package/README.md +793 -57
  2. package/dist/{admin-DWWrT1h6.d.ts → admin-CS7JB_UH.d.ts} +2 -2
  3. package/dist/{adminSchemas-D4bFKsrb.d.ts → adminSchemas-29cGgrg5.d.ts} +1 -1
  4. package/dist/{auth-DXiSJZaf.d.ts → auth-DtdYjrZ9.d.ts} +11 -7
  5. package/dist/{caller-BT-Mv4bD.d.ts → caller-BTwGl7vm.d.ts} +183 -15
  6. package/dist/chunk-2FDSKLBO.js +82 -0
  7. package/dist/chunk-2FDSKLBO.js.map +1 -0
  8. package/dist/{chunk-GSIPUNJL.js → chunk-64I6MYBC.js} +139 -792
  9. package/dist/chunk-64I6MYBC.js.map +1 -0
  10. package/dist/chunk-7473HK24.js +17 -0
  11. package/dist/chunk-7473HK24.js.map +1 -0
  12. package/dist/chunk-7Y5ROPXL.js +33 -0
  13. package/dist/chunk-7Y5ROPXL.js.map +1 -0
  14. package/dist/chunk-A2RBQD66.js +7 -0
  15. package/dist/chunk-A2RBQD66.js.map +1 -0
  16. package/dist/chunk-ASAMUGHG.js +651 -0
  17. package/dist/chunk-ASAMUGHG.js.map +1 -0
  18. package/dist/{chunk-B2P7AZ25.js → chunk-CYE4UBOB.js} +6 -4
  19. package/dist/chunk-CYE4UBOB.js.map +1 -0
  20. package/dist/chunk-DNP65TD5.js +832 -0
  21. package/dist/chunk-DNP65TD5.js.map +1 -0
  22. package/dist/{chunk-5CGO426C.js → chunk-GYLWEJHW.js} +5 -1
  23. package/dist/chunk-GYLWEJHW.js.map +1 -0
  24. package/dist/{chunk-AONAFOYU.js → chunk-I3JCFS7H.js} +25 -13
  25. package/dist/chunk-I3JCFS7H.js.map +1 -0
  26. package/dist/{chunk-X5CWPX7H.js → chunk-IIQ3WMJK.js} +13 -2
  27. package/dist/chunk-IIQ3WMJK.js.map +1 -0
  28. package/dist/{chunk-6K5BY47X.js → chunk-JVTONQFI.js} +1277 -277
  29. package/dist/chunk-JVTONQFI.js.map +1 -0
  30. package/dist/{chunk-RL4VKLBX.js → chunk-KKQANRLG.js} +2 -2
  31. package/dist/{chunk-RL4VKLBX.js.map → chunk-KKQANRLG.js.map} +1 -1
  32. package/dist/{chunk-GKH3LJ7N.js → chunk-MPQAT55F.js} +38 -205
  33. package/dist/chunk-MPQAT55F.js.map +1 -0
  34. package/dist/chunk-NX2V6SIL.js +202 -0
  35. package/dist/chunk-NX2V6SIL.js.map +1 -0
  36. package/dist/{chunk-7QCUX4TK.js → chunk-NZHA4TZD.js} +72 -175
  37. package/dist/chunk-NZHA4TZD.js.map +1 -0
  38. package/dist/{chunk-MBH6IDSM.js → chunk-OCZJ4EYV.js} +40 -17
  39. package/dist/chunk-OCZJ4EYV.js.map +1 -0
  40. package/dist/{chunk-RJ5I2VO3.js → chunk-PHCURHC7.js} +6 -6
  41. package/dist/chunk-PHCURHC7.js.map +1 -0
  42. package/dist/{chunk-4YO5H5ON.js → chunk-Q4OGQM5I.js} +4 -10
  43. package/dist/chunk-Q4OGQM5I.js.map +1 -0
  44. package/dist/chunk-QSU47XEX.js +13 -0
  45. package/dist/chunk-QSU47XEX.js.map +1 -0
  46. package/dist/{chunk-LCC7HNFP.js → chunk-RZXJTDH5.js} +2 -2
  47. package/dist/{chunk-Z2LZ7IZ7.js → chunk-SN3OZVEQ.js} +5 -31
  48. package/dist/chunk-SN3OZVEQ.js.map +1 -0
  49. package/dist/{chunk-Z5ILULF2.js → chunk-SQ5VUG77.js} +4 -3
  50. package/dist/{chunk-Z5ILULF2.js.map → chunk-SQ5VUG77.js.map} +1 -1
  51. package/dist/chunk-STH5DLQP.js +1192 -0
  52. package/dist/chunk-STH5DLQP.js.map +1 -0
  53. package/dist/{chunk-NMXYENH4.js → chunk-VZFJTKIW.js} +6 -6
  54. package/dist/{chunk-753VJIUP.js → chunk-X4ZU4QH5.js} +43 -9
  55. package/dist/chunk-X4ZU4QH5.js.map +1 -0
  56. package/dist/chunk-XD7SRDUY.js +188 -0
  57. package/dist/chunk-XD7SRDUY.js.map +1 -0
  58. package/dist/chunk-Z7I6SONO.js +825 -0
  59. package/dist/chunk-Z7I6SONO.js.map +1 -0
  60. package/dist/cli/quickdraw-docs.js +131 -113
  61. package/dist/cli/quickdraw-docs.js.map +1 -1
  62. package/dist/client/core.d.ts +387 -0
  63. package/dist/client/core.js +109 -0
  64. package/dist/client/core.js.map +1 -0
  65. package/dist/client/index.d.ts +30 -193
  66. package/dist/client/index.js +56 -217
  67. package/dist/client/index.js.map +1 -1
  68. package/dist/{clientTypes-hWcPUVS9.d.ts → clientTypes-CUUNJwWs.d.ts} +3 -3
  69. package/dist/{connection-CgijdGpT.d.ts → connection-2EeQ_Et0.d.ts} +16 -9
  70. package/dist/{createServer-Cp-Wk500.d.ts → createServer-Bx5HOcYK.d.ts} +37 -9
  71. package/dist/{createTestApp-BB_PkqQ7.d.ts → createTestApp-DAMANT79.d.ts} +36 -9
  72. package/dist/{envelope-DzWyIXjj.d.ts → envelope-BMBaxppV.d.ts} +200 -8
  73. package/dist/{http-DotQ5bj8.d.ts → http-YzLAar3w.d.ts} +15 -50
  74. package/dist/index.d.ts +7 -6
  75. package/dist/index.js +61 -28
  76. package/dist/index.js.map +1 -1
  77. package/dist/{infer-BmoZsL9a.d.ts → infer-Br62cnyW.d.ts} +39 -1
  78. package/dist/{metrics-DW80Exve.d.ts → metrics-CTVpw0gu.d.ts} +2 -2
  79. package/dist/parser.js +2 -1
  80. package/dist/prisma/index.d.ts +13 -6
  81. package/dist/prisma/index.js +155 -9
  82. package/dist/prisma/index.js.map +1 -1
  83. package/dist/remote-N7OUAGOW.js +15 -0
  84. package/dist/remote-N7OUAGOW.js.map +1 -0
  85. package/dist/server/auth/index.d.ts +3 -3
  86. package/dist/server/auth/index.js +5 -3
  87. package/dist/server/auth/index.js.map +1 -1
  88. package/dist/server/index.d.ts +140 -36
  89. package/dist/server/index.js +162 -55
  90. package/dist/server/index.js.map +1 -1
  91. package/dist/server/mcp/index.d.ts +34 -18
  92. package/dist/server/mcp/index.js +29 -12
  93. package/dist/server/mcp/index.js.map +1 -1
  94. package/dist/server/otel.d.ts +3 -3
  95. package/dist/{sharing-UDLn1049.d.ts → sharing-BrsmlDv_.d.ts} +3 -3
  96. package/dist/testing/client.d.ts +13 -13
  97. package/dist/testing/client.js +17 -10
  98. package/dist/testing/client.js.map +1 -1
  99. package/dist/testing/index.d.ts +49 -12
  100. package/dist/testing/index.js +102 -19
  101. package/dist/testing/index.js.map +1 -1
  102. package/dist/testing/mock.d.ts +5 -5
  103. package/dist/testing/mock.js +15 -9
  104. package/dist/tracked-BR5b2O-U.d.ts +43 -0
  105. package/dist/{types-CYliTP65.d.ts → types-CLRCQGnO.d.ts} +4 -3
  106. package/dist/types-tMagHbjy.d.ts +151 -0
  107. package/dist/utils/index.d.ts +16 -4
  108. package/dist/utils/index.js +6 -3
  109. package/package.json +5 -1
  110. package/dist/chunk-4YO5H5ON.js.map +0 -1
  111. package/dist/chunk-5CGO426C.js.map +0 -1
  112. package/dist/chunk-6K5BY47X.js.map +0 -1
  113. package/dist/chunk-753VJIUP.js.map +0 -1
  114. package/dist/chunk-7QCUX4TK.js.map +0 -1
  115. package/dist/chunk-AONAFOYU.js.map +0 -1
  116. package/dist/chunk-B2P7AZ25.js.map +0 -1
  117. package/dist/chunk-GKH3LJ7N.js.map +0 -1
  118. package/dist/chunk-GSIPUNJL.js.map +0 -1
  119. package/dist/chunk-MBH6IDSM.js.map +0 -1
  120. package/dist/chunk-RJ5I2VO3.js.map +0 -1
  121. package/dist/chunk-X5CWPX7H.js.map +0 -1
  122. package/dist/chunk-Z2LZ7IZ7.js.map +0 -1
  123. /package/dist/{chunk-LCC7HNFP.js.map → chunk-RZXJTDH5.js.map} +0 -0
  124. /package/dist/{chunk-NMXYENH4.js.map → chunk-VZFJTKIW.js.map} +0 -0
@@ -49,9 +49,10 @@ import {
49
49
  orderByOf,
50
50
  resolver,
51
51
  rowlessProblem,
52
+ seedRoomHosts,
52
53
  selectWith,
53
54
  watchAdapterClients
54
- } from "../chunk-6K5BY47X.js";
55
+ } from "../chunk-JVTONQFI.js";
55
56
  import {
56
57
  DevWarningError,
57
58
  N_PLUS_ONE_STATEMENTS,
@@ -59,14 +60,14 @@ import {
59
60
  currentRev,
60
61
  formatDevWarning,
61
62
  runBeforeAnyDispatcher
62
- } from "../chunk-Z5ILULF2.js";
63
+ } from "../chunk-SQ5VUG77.js";
63
64
  import {
64
65
  crud,
65
66
  crudSpecOf,
66
67
  sharing,
67
68
  sharingSpecOf
68
- } from "../chunk-NMXYENH4.js";
69
- import "../chunk-RL4VKLBX.js";
69
+ } from "../chunk-VZFJTKIW.js";
70
+ import "../chunk-KKQANRLG.js";
70
71
  import {
71
72
  SUBSCRIBER_LEVELS,
72
73
  abortError,
@@ -92,27 +93,42 @@ import {
92
93
  schemaOutputOf,
93
94
  serviceGrant,
94
95
  strip
95
- } from "../chunk-MBH6IDSM.js";
96
- import "../chunk-LCC7HNFP.js";
96
+ } from "../chunk-OCZJ4EYV.js";
97
+ import "../chunk-RZXJTDH5.js";
97
98
  import {
98
99
  recordSocketSession,
99
100
  toCallReply,
100
101
  unreadable
101
- } from "../chunk-RJ5I2VO3.js";
102
+ } from "../chunk-PHCURHC7.js";
102
103
  import {
103
104
  ANY_FIELD,
104
105
  modelKey,
105
106
  storageOf
106
107
  } from "../chunk-DUBU3BB3.js";
108
+ import {
109
+ search,
110
+ searchSpecOf
111
+ } from "../chunk-XD7SRDUY.js";
112
+ import "../chunk-4D2DOGAW.js";
113
+ import {
114
+ ACCESS_LEVELS,
115
+ isAccessLevel
116
+ } from "../chunk-AK23XSWD.js";
117
+ import {
118
+ consoleLogger,
119
+ errorText
120
+ } from "../chunk-DKE6QO26.js";
121
+ import "../chunk-7473HK24.js";
122
+ import "../chunk-2FDSKLBO.js";
107
123
  import {
108
124
  ADMIN_FIELD_TYPES,
109
125
  ADMIN_NEVER_WRITABLE,
110
126
  MAX_CURSOR_LENGTH,
111
127
  admin,
112
128
  adminSpecOf,
113
- search,
114
- searchSpecOf
115
- } from "../chunk-GKH3LJ7N.js";
129
+ writtenData
130
+ } from "../chunk-MPQAT55F.js";
131
+ import "../chunk-A2RBQD66.js";
116
132
  import {
117
133
  MAX_ENTITY_IDS_PER_SOCKET,
118
134
  MAX_SCOPES_PER_SOCKET,
@@ -120,25 +136,19 @@ import {
120
136
  entityRoom,
121
137
  reservedRoomPrefix,
122
138
  socketIdShaped
123
- } from "../chunk-753VJIUP.js";
139
+ } from "../chunk-X4ZU4QH5.js";
124
140
  import {
125
141
  CHANNEL_DEFAULT_RATE,
142
+ isHostServed,
126
143
  isScopedStream
127
- } from "../chunk-5CGO426C.js";
128
- import "../chunk-4D2DOGAW.js";
129
- import {
130
- ACCESS_LEVELS,
131
- isAccessLevel
132
- } from "../chunk-AK23XSWD.js";
144
+ } from "../chunk-GYLWEJHW.js";
133
145
  import "../chunk-47AEXVDM.js";
134
- import "../chunk-Z2LZ7IZ7.js";
135
- import {
136
- consoleLogger,
137
- errorText
138
- } from "../chunk-DKE6QO26.js";
146
+ import "../chunk-SN3OZVEQ.js";
147
+ import "../chunk-7Y5ROPXL.js";
139
148
  import {
140
149
  QuickdrawError
141
- } from "../chunk-4YO5H5ON.js";
150
+ } from "../chunk-Q4OGQM5I.js";
151
+ import "../chunk-QSU47XEX.js";
142
152
 
143
153
  // src/server/realtime/define.ts
144
154
  function isRecord(value) {
@@ -224,7 +234,7 @@ function checkChannel(service, name, def, entry, kinds, fail5) {
224
234
  });
225
235
  }
226
236
  function compileChannels(contract, value, kinds, fail5) {
227
- const declared = Object.entries(contract.channels);
237
+ const declared = Object.entries(contract.channels).filter(([, def]) => !isHostServed(def));
228
238
  if (value === void 0 && declared.length === 0) {
229
239
  return /* @__PURE__ */ new Map();
230
240
  }
@@ -242,6 +252,11 @@ function compileChannels(contract, value, kinds, fail5) {
242
252
  if (def === void 0) {
243
253
  fail5(`"${name}" is not a channel of the contract`);
244
254
  }
255
+ if (isHostServed(def)) {
256
+ fail5(
257
+ `channel "${name}" is served by the room's host (serve: "host"): its messages never reach the server, so it takes no handler`
258
+ );
259
+ }
245
260
  channels.set(name, checkChannel(contract.name, name, def, entry, admitted, fail5));
246
261
  }
247
262
  return channels;
@@ -310,6 +325,11 @@ function checkStreamOptions(name, def, entry, fail5) {
310
325
  return { computeSeed: seed, validate };
311
326
  }
312
327
  function checkStream(name, def, needs, options, fail5) {
328
+ if (isHostServed(def) && options !== void 0) {
329
+ fail5(
330
+ `streams.${name}: the stream is served by the room's host (serve: "host"), whose client pushes its items and keeps its seed: the server takes no options for it`
331
+ );
332
+ }
313
333
  const access = engineForm(def.access);
314
334
  if (typeof access === "object" && "entry" in access && !needs.hasPolicy) {
315
335
  fail5(
@@ -329,7 +349,8 @@ function checkStream(name, def, needs, options, fail5) {
329
349
  ...checkStreamOptions(name, def, options, fail5),
330
350
  volatile: def.volatile === true,
331
351
  access,
332
- room: roomAccess(def.access)
352
+ room: roomAccess(def.access),
353
+ host: isHostServed(def)
333
354
  });
334
355
  }
335
356
  function compileStreams(contract, needs, value, fail5) {
@@ -506,6 +527,7 @@ var DEFINITION_KEYS = /* @__PURE__ */ new Set([
506
527
  "channels",
507
528
  "streams",
508
529
  "onRoomLeave",
530
+ "onHostLeave",
509
531
  "adminBypass",
510
532
  "kinds"
511
533
  ]);
@@ -724,13 +746,16 @@ function buildService(runtime, contract, definition) {
724
746
  fail5("the definition must be an object");
725
747
  }
726
748
  checkKeys(definition, DEFINITION_KEYS, "the definition", fail5);
727
- const { adminBypass = true, onRoomLeave } = definition;
749
+ const { adminBypass = true, onRoomLeave, onHostLeave } = definition;
728
750
  if (typeof adminBypass !== "boolean") {
729
751
  fail5("adminBypass must be a boolean");
730
752
  }
731
753
  if (onRoomLeave !== void 0 && typeof onRoomLeave !== "function") {
732
754
  fail5("onRoomLeave must be a function of the leave and a run context");
733
755
  }
756
+ if (onHostLeave !== void 0 && typeof onHostLeave !== "function") {
757
+ fail5("onHostLeave must be a function of the leave and a run context");
758
+ }
734
759
  const data = checkServiceData(definition, runtime.kinds, fail5);
735
760
  const projections = compileProjections(checked2, definition.project, fail5);
736
761
  const collections = compileCollections(
@@ -760,7 +785,8 @@ function buildService(runtime, contract, definition) {
760
785
  definition.streams,
761
786
  fail5
762
787
  ),
763
- onRoomLeave
788
+ onRoomLeave,
789
+ onHostLeave
764
790
  });
765
791
  checkHandlers(service, fail5);
766
792
  registerRuntime(service, runtime);
@@ -801,6 +827,10 @@ function streamOf(current, contract, name) {
801
827
  },
802
828
  pushMany(...args) {
803
829
  handle().pushMany(...args);
830
+ },
831
+ // Async, so every refusal (no dispatcher, a host-served stream, a bad scope) rejects.
832
+ async subscribers(...args) {
833
+ return await handle().subscribers(...args);
804
834
  }
805
835
  });
806
836
  }
@@ -2133,7 +2163,8 @@ async function authorizeScope(run, collection, scope) {
2133
2163
  const rule = {
2134
2164
  service: runtime.service,
2135
2165
  anchor: collection.anchor?.name,
2136
- access: collection.access
2166
+ access: collection.access,
2167
+ requires: collection.requires
2137
2168
  };
2138
2169
  const allowed = await allowedScopes(runtime.access, rule, principal, [scope]);
2139
2170
  if (!allowed.has(scope)) {
@@ -3558,6 +3589,45 @@ async function checkWrite(call, context, data) {
3558
3589
  ["data"]
3559
3590
  );
3560
3591
  }
3592
+ function preparedIssues(call, columns) {
3593
+ const never = Object.keys(columns).filter((name) => ADMIN_NEVER_WRITABLE.includes(name)).map((name) => ({
3594
+ path: ["data", name],
3595
+ message: `"${name}" is not writable; the database sets it`
3596
+ }));
3597
+ return [...never, ...columnValueIssues(call.runtime.service, columns, ["data"])];
3598
+ }
3599
+ async function prepareWrite(call, context, write, ctx, db) {
3600
+ const { prepare } = context;
3601
+ const data = write.method === "adminDelete" ? {} : write.data;
3602
+ if (prepare === void 0) {
3603
+ return data;
3604
+ }
3605
+ const returned = await checked(() => prepare(write, ctx, db));
3606
+ if (write.method === "adminDelete" || returned === void 0) {
3607
+ return data;
3608
+ }
3609
+ const { name } = call.runtime.service;
3610
+ if (!isPlainObject(returned)) {
3611
+ throw new QuickdrawError(
3612
+ "INTERNAL",
3613
+ `The admin kit's prepare for ${name} must return column values, or nothing`
3614
+ );
3615
+ }
3616
+ const columns = writtenData(returned);
3617
+ const issues = preparedIssues(call, columns);
3618
+ if (issues.length > 0) {
3619
+ throw new QuickdrawError(
3620
+ "VALIDATION",
3621
+ `The admin kit's prepare for ${name} returned data the kit does not write`,
3622
+ { issues }
3623
+ );
3624
+ }
3625
+ const level = rowLevel(context.form, "Moderate");
3626
+ await checkAnchorColumns(call.runtime, call.principal, columns, level, {
3627
+ creating: write.method === "adminCreate"
3628
+ });
3629
+ return { ...data, ...columns };
3630
+ }
3561
3631
  function isValueRefusal(error) {
3562
3632
  if (!(error instanceof Error)) {
3563
3633
  return false;
@@ -3608,37 +3678,65 @@ function committed(context, args, write) {
3608
3678
  failed(error);
3609
3679
  }
3610
3680
  }
3611
- async function written(context, call, args, method, perform) {
3612
- const { onWrite, onCommitted } = context;
3613
- const attempt = async (table, before) => {
3681
+ async function rowBefore(context, call, table, write) {
3682
+ const readsAsItDeletes = write.method === "adminDelete" && context.prepare === void 0;
3683
+ if (write.method === "adminCreate" || readsAsItDeletes) {
3684
+ return void 0;
3685
+ }
3686
+ const row = await table.findUnique({ where: { id: write.id }, select: call.projection.select });
3687
+ return requireRow(row, `No such ${call.model}`);
3688
+ }
3689
+ function prepareCall(write, before) {
3690
+ if (write.method === "adminCreate") {
3691
+ return Object.freeze({ method: write.method, data: Object.freeze({ ...write.data }) });
3692
+ }
3693
+ if (write.method === "adminUpdate") {
3694
+ const data = Object.freeze({ ...write.data });
3695
+ return Object.freeze({ method: write.method, id: write.id, data, before });
3696
+ }
3697
+ return Object.freeze({ method: write.method, id: write.id, before });
3698
+ }
3699
+ async function written(context, call, args, write, perform) {
3700
+ const { prepare, onWrite, onCommitted } = context;
3701
+ const data = write.method === "adminDelete" ? {} : write.data;
3702
+ const attempt = async (table, values, before) => {
3614
3703
  try {
3615
- return await perform(table, before);
3704
+ return await perform({ table, values, before });
3616
3705
  } catch (error) {
3617
3706
  throw writeRefusal(error);
3618
3707
  }
3619
3708
  };
3620
- if (onWrite === void 0 && onCommitted === void 0) {
3621
- return (await attempt(call.table, false)).reply;
3709
+ if (prepare === void 0 && onWrite === void 0 && onCommitted === void 0) {
3710
+ return (await attempt(call.table, data, false)).reply;
3622
3711
  }
3623
- let write;
3712
+ const ctx = args.ctx;
3713
+ let heard;
3624
3714
  const reply = await inKitTransaction(
3625
3715
  args.db,
3626
3716
  async (tx) => {
3627
- const done = await attempt(tableOf2(tx, call.model), method !== "adminCreate");
3717
+ const table = tableOf2(tx, call.model);
3628
3718
  const whole = (row) => row ? rowWhole(call, row) : null;
3629
- write = {
3630
- method,
3719
+ const first = await rowBefore(context, call, table, write);
3720
+ const before = first === void 0 ? void 0 : whole(first);
3721
+ const values = await prepareWrite(call, context, prepareCall(write, before), ctx, tx);
3722
+ const done = await attempt(
3723
+ table,
3724
+ values,
3725
+ write.method === "adminDelete" && first === void 0
3726
+ );
3727
+ heard = {
3728
+ method: write.method,
3631
3729
  id: done.id,
3632
- ...method === "adminCreate" ? {} : { before: whole(done.before) },
3730
+ ...write.method === "adminCreate" ? {} : { before: before ?? whole(done.before) },
3633
3731
  after: whole(done.after)
3634
3732
  };
3635
- await onWrite?.(write, args.ctx, tx);
3733
+ await onWrite?.(heard, ctx, tx);
3636
3734
  return done.reply;
3637
3735
  },
3638
- { owner: "The admin kit's writes with onWrite or onCommitted" }
3736
+ { owner: "The admin kit's writes with prepare, onWrite or onCommitted" }
3639
3737
  );
3640
- if (write !== void 0) {
3641
- committed(context, args, write);
3738
+ if (heard !== void 0) {
3739
+ committed(context, args, heard);
3642
3740
  }
3643
3741
  return reply;
3644
3742
  }
@@ -3657,8 +3755,10 @@ function adminCreateHandler(context) {
3657
3755
  const call = adminCall(args.ctx, args.db, context.fieldsFor);
3658
3756
  const { data } = args.input;
3659
3757
  await checkWrite(call, context, data);
3660
- return await written(context, call, args, "adminCreate", async (table) => {
3661
- const after = await table.create({ data, select: call.projection.select });
3758
+ const { select } = call.projection;
3759
+ const write = { method: "adminCreate", data };
3760
+ return await written(context, call, args, write, async ({ table, values }) => {
3761
+ const after = await table.create({ data: values, select });
3662
3762
  return { id: idOf(after), after, reply: rowOut(call, after) };
3663
3763
  });
3664
3764
  };
@@ -3673,10 +3773,10 @@ function adminUpdateHandler(context) {
3673
3773
  return await readRow(call, id);
3674
3774
  }
3675
3775
  const { select } = call.projection;
3676
- return await written(context, call, args, "adminUpdate", async (table, withBefore) => {
3677
- const before = withBefore ? await table.findUnique({ where: { id }, select }) : void 0;
3678
- const after = await table.update({ where: { id }, data, select });
3679
- return { id, before, after, reply: rowOut(call, after) };
3776
+ const write = { method: "adminUpdate", id, data };
3777
+ return await written(context, call, args, write, async ({ table, values }) => {
3778
+ const after = await table.update({ where: { id }, data: values, select });
3779
+ return { id, after, reply: rowOut(call, after) };
3680
3780
  });
3681
3781
  };
3682
3782
  return handler;
@@ -3685,10 +3785,11 @@ function adminDeleteHandler(context) {
3685
3785
  const handler = async (args) => {
3686
3786
  const call = adminCall(args.ctx, args.db, context.fieldsFor);
3687
3787
  const { id } = args.input;
3688
- return await written(context, call, args, "adminDelete", async (table, withBefore) => {
3689
- const select = withBefore ? call.projection.select : { id: true };
3690
- const before = await table.delete({ where: { id }, select });
3691
- return { id, ...withBefore ? { before } : {}, after: null, reply: null };
3788
+ const write = { method: "adminDelete", id };
3789
+ return await written(context, call, args, write, async ({ table, before }) => {
3790
+ const select = before ? call.projection.select : { id: true };
3791
+ const row = await table.delete({ where: { id }, select });
3792
+ return { id, ...before ? { before: row } : {}, after: null, reply: null };
3692
3793
  });
3693
3794
  };
3694
3795
  return handler;
@@ -3739,6 +3840,7 @@ var OPTION_KEYS3 = [
3739
3840
  "fieldOverrides",
3740
3841
  "rowless",
3741
3842
  "grants",
3843
+ "prepare",
3742
3844
  "onWrite",
3743
3845
  "onCommitted"
3744
3846
  ];
@@ -3776,7 +3878,7 @@ function checkOptions3(options) {
3776
3878
  }
3777
3879
  if (!isRecord11(options)) {
3778
3880
  fail4(
3779
- "options must be { access?, displayName?, hiddenFields?, editable?, fieldOverrides?, rowless?, grants?, onWrite?, onCommitted? }"
3881
+ "options must be { access?, displayName?, hiddenFields?, editable?, fieldOverrides?, rowless?, grants?, prepare?, onWrite?, onCommitted? }"
3780
3882
  );
3781
3883
  }
3782
3884
  const unknownKey = Object.keys(options).find((key) => !OPTION_KEYS3.includes(key));
@@ -3884,7 +3986,10 @@ function handlers4(contract, options) {
3884
3986
  const rowless = rowlessMethods(checked2.rowless, names, fail4);
3885
3987
  const fieldsFor = fieldsByColumn(contract, kit, checked2);
3886
3988
  const fields = fieldsFor(void 0);
3887
- const { onWrite, onCommitted } = checked2;
3989
+ const { prepare, onWrite, onCommitted } = checked2;
3990
+ if (prepare !== void 0 && typeof prepare !== "function") {
3991
+ fail4("prepare must be a function of (call, ctx, db)");
3992
+ }
3888
3993
  if (onWrite !== void 0 && typeof onWrite !== "function") {
3889
3994
  fail4("onWrite must be a function of (write, ctx, db)");
3890
3995
  }
@@ -3892,6 +3997,7 @@ function handlers4(contract, options) {
3892
3997
  fail4("onCommitted must be a function of (write, ctx)");
3893
3998
  }
3894
3999
  const hooks = {
4000
+ prepare,
3895
4001
  onWrite,
3896
4002
  onCommitted
3897
4003
  };
@@ -3970,6 +4076,7 @@ async function setupRedisAdapter(io, options = {}) {
3970
4076
  await Promise.all([pubClient.connect(), subClient.connect()]);
3971
4077
  io.adapter(createAdapter(pubClient, subClient, { key }));
3972
4078
  watchAdapterClients(io, logger);
4079
+ seedRoomHosts(io);
3973
4080
  logger.info(`Redis adapter connected to ${host}:${port}`);
3974
4081
  const cleanup = async () => {
3975
4082
  try {