@geonosis/db 0.3.0 → 0.5.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.cjs CHANGED
@@ -5,9 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __export = (target, all) => {
9
- for (var name in all)
10
- __defProp(target, name, { get: all[name], enumerable: true });
8
+ var __export = (target, all2) => {
9
+ for (var name in all2)
10
+ __defProp(target, name, { get: all2[name], enumerable: true });
11
11
  };
12
12
  var __copyProps = (to, from, except, desc) => {
13
13
  if (from && typeof from === "object" || typeof from === "function") {
@@ -30,23 +30,36 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ DEFAULT_ENTITY_PAGE: () => DEFAULT_ENTITY_PAGE,
33
34
  DEFAULT_OPS_VALUE: () => DEFAULT_OPS_VALUE,
34
35
  DEFAULT_TENANT_KEY: () => DEFAULT_TENANT_KEY,
35
36
  DbRefusal: () => DbRefusal,
37
+ ENTITY_PLACEHOLDERS: () => ENTITY_PLACEHOLDERS,
38
+ MAX_ENTITY_PAGE: () => MAX_ENTITY_PAGE,
36
39
  appConnectionString: () => appConnectionString,
37
40
  appRoleStatements: () => appRoleStatements,
41
+ asStatement: () => asStatement,
38
42
  concurrentTenantsConformance: () => concurrentTenantsConformance,
39
43
  connectionsConformance: () => connectionsConformance,
40
44
  createConnections: () => createConnections,
41
45
  createSessionSeam: () => createSessionSeam,
46
+ defineEntity: () => defineEntity,
47
+ drizzleSession: () => drizzleSession,
48
+ drizzleTenantPolicies: () => drizzleTenantPolicies,
49
+ entityConformance: () => entityConformance,
50
+ entityMigrations: () => entityMigrations,
51
+ forceRowLevelSecurity: () => forceRowLevelSecurity,
42
52
  forcedRowLevelSecurityConformance: () => forcedRowLevelSecurityConformance,
53
+ fragment: () => fragment,
43
54
  nodePostgresDriver: () => nodePostgresDriver,
44
55
  perStepConnection: () => perStepConnection,
45
56
  scopedQueryConformance: () => scopedQueryConformance,
46
57
  sessionConformance: () => sessionConformance,
47
58
  statementCensusConformance: () => statementCensusConformance,
48
59
  tenantIsolationConformance: () => tenantIsolationConformance,
49
- tenantPolicies: () => tenantPolicies
60
+ tenantPolicies: () => tenantPolicies,
61
+ tenantPolicySet: () => tenantPolicySet,
62
+ undosOf: () => undosOf
50
63
  });
51
64
  module.exports = __toCommonJS(index_exports);
52
65
 
@@ -64,7 +77,7 @@ var DbRefusal = class extends Error {
64
77
 
65
78
  // src/identifiers.ts
66
79
  var quoted = (name) => `"${name.replaceAll('"', '""')}"`;
67
- var literal = (value) => `'${value.replaceAll("'", "''")}'`;
80
+ var literal = (value2) => `'${value2.replaceAll("'", "''")}'`;
68
81
  var qualified = (table, schema) => schema === void 0 ? quoted(table) : `${quoted(schema)}.${quoted(table)}`;
69
82
 
70
83
  // src/app-role.ts
@@ -109,6 +122,123 @@ var appConnectionString = (ownerConnectionString, credentials) => {
109
122
 
110
123
  // src/conformance.ts
111
124
  var import_conformance = require("@geonosis/conformance");
125
+
126
+ // src/settings.ts
127
+ var DEFAULT_OPS_VALUE = "on";
128
+ var CUSTOM_SETTING = /^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*$/;
129
+ var readSettings = (settings) => {
130
+ for (const named of ["opsSetting", "tenantSetting"]) {
131
+ const name = settings[named];
132
+ if (typeof name !== "string" || name === "") {
133
+ throw new DbRefusal(
134
+ `${named} is required and has no default: the name of a session setting is the one thing this package cannot pick for you, because the policies and the seam have to agree on it exactly`
135
+ );
136
+ }
137
+ if (!CUSTOM_SETTING.test(name)) {
138
+ throw new DbRefusal(
139
+ `${named} is "${name}", and Postgres reads a setting an application defines as \`prefix.name\` \u2014 anything else is rejected as unrecognised the first time a session sets it`
140
+ );
141
+ }
142
+ }
143
+ return { ...settings, opsValue: settings.opsValue ?? DEFAULT_OPS_VALUE };
144
+ };
145
+
146
+ // src/session.ts
147
+ var OPS = /* @__PURE__ */ Symbol("the ops lever");
148
+ var scopeOf = /* @__PURE__ */ new WeakMap();
149
+ var describe = (scope) => scope === OPS ? "the ops lever" : `tenant ${JSON.stringify(scope)}`;
150
+ var fragment = (chunks, values2 = []) => ({ chunks, values: values2 });
151
+ var asStatement = (piece) => ({
152
+ params: [...piece.values],
153
+ text: piece.chunks.map((chunk, at) => at === 0 ? chunk : `$${at}${chunk}`).join("")
154
+ });
155
+ var setConfigFragment = (settings) => {
156
+ if (settings.length === 0) return fragment(["select 1"]);
157
+ const chunks = ["select set_config("];
158
+ for (let at = 0; at < settings.length; at += 1) {
159
+ chunks.push(", ", at === settings.length - 1 ? ", true)" : ", true), set_config(");
160
+ }
161
+ return fragment(
162
+ chunks,
163
+ settings.flatMap((one) => [one.name, one.value])
164
+ );
165
+ };
166
+ var executorHandles = {
167
+ guard: (executor, stillOpen) => ({
168
+ // Spread, so what a driver put on the handle BESIDES the port reaches the query that was
169
+ // given it: a keeper that rebuilds the object hands back less than `open()` returned.
170
+ ...executor,
171
+ execute: async (statement) => {
172
+ stillOpen();
173
+ return executor.execute(statement);
174
+ },
175
+ ...typeof executor.transaction === "function" ? {
176
+ transaction: async (run) => {
177
+ stillOpen();
178
+ return executor.transaction(run);
179
+ }
180
+ } : {}
181
+ }),
182
+ opens: (executor) => typeof executor.transaction === "function",
183
+ send: (executor, statement) => executor.execute(asStatement(statement)),
184
+ transaction: (executor, run) => executor.transaction(run)
185
+ };
186
+ var DEFAULT_TENANT_KEY = "tenantId";
187
+ var STATEMENT_TIMEOUT = "statement_timeout";
188
+ var createSessionSeam = (config) => {
189
+ const settings = readSettings(config.settings);
190
+ const tenantKey = config.tenantKey ?? DEFAULT_TENANT_KEY;
191
+ const over = config.over ?? executorHandles;
192
+ if (tenantKey === "") {
193
+ throw new DbRefusal(
194
+ "tenantKey is empty, and it is the property the seam reads a query\u2019s tenant out of: name the key your queries carry, or leave it out for `tenantId`"
195
+ );
196
+ }
197
+ const contextOf = (scope) => {
198
+ if (scope !== OPS) return [{ name: settings.tenantSetting, value: scope }];
199
+ const lever = { name: settings.opsSetting, value: settings.opsValue };
200
+ return config.opsStatementTimeout === void 0 ? [lever] : [lever, { name: STATEMENT_TIMEOUT, value: config.opsStatementTimeout }];
201
+ };
202
+ const opened = async (handle, scope, run) => {
203
+ const current = scopeOf.get(handle);
204
+ if (current === scope) return run(handle);
205
+ if (current !== void 0) {
206
+ throw new DbRefusal(
207
+ `this transaction is open for ${describe(current)} and was asked for ${describe(scope)}: a transaction names one scope, and the second query would run behind the first one's policy`
208
+ );
209
+ }
210
+ if (!over.opens(handle)) {
211
+ throw new DbRefusal(
212
+ `this handle is in no session and cannot open a transaction, so ${describe(scope)} has nothing to name \u2014 pass the connection, or run it inside a session already open for that scope`
213
+ );
214
+ }
215
+ return over.transaction(handle, async (tx) => {
216
+ await over.send(tx, setConfigFragment(contextOf(scope)));
217
+ scopeOf.set(tx, scope);
218
+ return run(tx);
219
+ });
220
+ };
221
+ const named = (params) => {
222
+ const carried = params[tenantKey];
223
+ if (typeof carried !== "string" || carried === "") {
224
+ throw new DbRefusal(
225
+ `this query is scoped by "${tenantKey}" and its parameters carry no such value: the session would name no tenant at all, which is not a refusal but a query running outside every policy. Pass the tenant under "${tenantKey}", or build the seam with the key your own queries carry`
226
+ );
227
+ }
228
+ return carried;
229
+ };
230
+ return {
231
+ inOps: (handle, run) => opened(handle, OPS, run),
232
+ inTenant: (handle, tenantId, run) => opened(handle, tenantId, run),
233
+ scoped: (query) => async (handle, params) => opened(handle, named(params), (tx) => query(tx, params)),
234
+ scopedAsOps: (query) => (handle, params) => opened(handle, OPS, (tx) => query(tx, params)),
235
+ send: (handle, statement) => over.send(handle, statement),
236
+ tenantKey
237
+ };
238
+ };
239
+
240
+ // src/conformance.ts
241
+ var asking = (subject) => subject.run ?? ((handle, statement) => handle.execute(asStatement(statement)));
112
242
  var A = "tenant_conformance_a";
113
243
  var B = "tenant_conformance_b";
114
244
  var CONCURRENT = [
@@ -125,34 +255,45 @@ var probeSql = (probe) => {
125
255
  const on = qualified(probe.name, probe.schema);
126
256
  const tenant = quoted(probe.tenantColumn);
127
257
  return {
128
- insert: (tenantId, id) => ({
129
- params: [tenantId, id],
130
- text: `insert into ${on} (${tenant}, ${quoted(probe.idColumn)}) values ($1, $2)`
131
- }),
132
- selectAll: { text: `select ${tenant} as tenant from ${on}` },
258
+ insert: (tenantId, id) => fragment(
259
+ [`insert into ${on} (${tenant}, ${quoted(probe.idColumn)}) values (`, ", ", ")"],
260
+ [tenantId, id]
261
+ ),
262
+ selectAll: fragment([`select ${tenant} as tenant from ${on}`]),
263
+ selectBound: (ids) => fragment(
264
+ [
265
+ `select ${tenant} as tenant from ${on} where ${quoted(probe.idColumn)} = any(`,
266
+ ") and ",
267
+ " > 0"
268
+ ],
269
+ [ids, ids.length]
270
+ ),
133
271
  tenantsOf: (answer) => (0, import_conformance.rowsOf)(answer).map((row) => row.tenant).toSorted()
134
272
  };
135
273
  };
136
274
  var written = async (subject, rows) => {
137
275
  const probe = probeSql(subject.probe);
276
+ const ask = asking(subject);
138
277
  for (const [tenantId, id] of rows) {
139
278
  await subject.seam.inTenant(
140
279
  subject.connection,
141
280
  tenantId,
142
- (tx) => tx.execute(probe.insert(tenantId, id))
281
+ (tx) => ask(tx, probe.insert(tenantId, id))
143
282
  );
144
283
  }
145
284
  };
146
285
  var tenantIsolationConformance = (subject) => {
286
+ const ask = asking(subject);
147
287
  const probe = probeSql(subject.probe);
148
288
  return [
149
289
  {
150
290
  name: "the role the session connects as is no superuser",
151
291
  run: async () => {
152
292
  const [row] = (0, import_conformance.rowsOf)(
153
- await subject.connection.execute({
154
- text: `select current_setting('is_superuser') as super`
155
- })
293
+ await ask(
294
+ subject.connection,
295
+ fragment([`select current_setting('is_superuser') as super`])
296
+ )
156
297
  );
157
298
  (0, import_conformance.assertIs)(
158
299
  row?.super,
@@ -172,7 +313,7 @@ var tenantIsolationConformance = (subject) => {
172
313
  const seen = await subject.seam.inTenant(
173
314
  subject.connection,
174
315
  A,
175
- (tx) => tx.execute(probe.selectAll)
316
+ (tx) => ask(tx, probe.selectAll)
176
317
  );
177
318
  (0, import_conformance.assertSame)(
178
319
  probe.tenantsOf(seen),
@@ -190,7 +331,7 @@ var tenantIsolationConformance = (subject) => {
190
331
  [B, "row_b"]
191
332
  ]);
192
333
  (0, import_conformance.assertSame)(
193
- probe.tenantsOf(await subject.connection.execute(probe.selectAll)),
334
+ probe.tenantsOf(await ask(subject.connection, probe.selectAll)),
194
335
  [],
195
336
  "a read outside every session answered with rows: the policy defaults to visible, so a query that forgets its session sees the whole table"
196
337
  );
@@ -204,19 +345,40 @@ var tenantIsolationConformance = (subject) => {
204
345
  () => subject.seam.inTenant(
205
346
  subject.connection,
206
347
  B,
207
- (tx) => tx.execute(probe.insert(A, "row_forged"))
348
+ (tx) => ask(tx, probe.insert(A, "row_forged"))
208
349
  ),
209
350
  "a session naming one tenant wrote a row belonging to another \u2014 the isolation policy has a `using` clause and no `with check`, so it reads one way and writes any way"
210
351
  );
211
352
  (0, import_conformance.assertSame)(
212
353
  probe.tenantsOf(
213
- await subject.seam.inOps(subject.connection, (tx) => tx.execute(probe.selectAll))
354
+ await subject.seam.inOps(subject.connection, (tx) => ask(tx, probe.selectAll))
214
355
  ),
215
356
  [],
216
357
  "the forged write was reported as refused and the row is there anyway"
217
358
  );
218
359
  }
219
360
  },
361
+ {
362
+ name: "a value the exam binds reaches the database as a parameter, an array included",
363
+ run: async () => {
364
+ await subject.reset();
365
+ await written(subject, [
366
+ [A, "row_a"],
367
+ [A, "row_b"],
368
+ [B, "row_c"]
369
+ ]);
370
+ const seen = await subject.seam.inTenant(
371
+ subject.connection,
372
+ A,
373
+ (tx) => ask(tx, probe.selectBound(["row_a", "row_b"]))
374
+ );
375
+ (0, import_conformance.assertSame)(
376
+ probe.tenantsOf(seen),
377
+ [A, A],
378
+ "a read whose values were BOUND did not answer with the rows they name: a handle that renders a parameter into the statement\u2019s text \u2014 an array spliced into a list, a string quoted by hand \u2014 is a handle whose queries are built by concatenation, which is the injection this port\u2019s shape exists to make impossible"
379
+ );
380
+ }
381
+ },
220
382
  {
221
383
  name: "the ops lever sees every tenant",
222
384
  run: async () => {
@@ -227,7 +389,7 @@ var tenantIsolationConformance = (subject) => {
227
389
  ]);
228
390
  (0, import_conformance.assertSame)(
229
391
  probe.tenantsOf(
230
- await subject.seam.inOps(subject.connection, (tx) => tx.execute(probe.selectAll))
392
+ await subject.seam.inOps(subject.connection, (tx) => ask(tx, probe.selectAll))
231
393
  ),
232
394
  [A, B].toSorted(),
233
395
  "maintenance could not see across tenants, so every sweep, backfill and repair has to be run once per tenant or not at all"
@@ -246,6 +408,7 @@ var recorderOf = async (subject) => {
246
408
  };
247
409
  var counted = (statements, pattern) => statements.filter((one) => pattern.test(one)).length;
248
410
  var statementCensusConformance = (subject) => {
411
+ const ask = asking(subject);
249
412
  const probe = probeSql(subject.probe);
250
413
  return [
251
414
  {
@@ -254,7 +417,7 @@ var statementCensusConformance = (subject) => {
254
417
  await subject.reset();
255
418
  const recorded = await recorderOf(subject);
256
419
  try {
257
- await subject.seam.inTenant(recorded.connection, A, (tx) => tx.execute(probe.selectAll));
420
+ await subject.seam.inTenant(recorded.connection, A, (tx) => ask(tx, probe.selectAll));
258
421
  } finally {
259
422
  await recorded.close();
260
423
  }
@@ -287,8 +450,8 @@ var statementCensusConformance = (subject) => {
287
450
  const recorded = await recorderOf(subject);
288
451
  try {
289
452
  await subject.seam.inTenant(recorded.connection, A, async (tx) => {
290
- await tx.execute(probe.selectAll);
291
- await subject.seam.inTenant(tx, A, (inner) => inner.execute(probe.selectAll));
453
+ await ask(tx, probe.selectAll);
454
+ await subject.seam.inTenant(tx, A, (inner) => ask(inner, probe.selectAll));
292
455
  });
293
456
  } finally {
294
457
  await recorded.close();
@@ -314,6 +477,7 @@ var statementCensusConformance = (subject) => {
314
477
  ];
315
478
  };
316
479
  var concurrentTenantsConformance = (subject) => {
480
+ const ask = asking(subject);
317
481
  const probe = probeSql(subject.probe);
318
482
  return [
319
483
  {
@@ -325,13 +489,13 @@ var concurrentTenantsConformance = (subject) => {
325
489
  (tenantId, index) => subject.seam.inTenant(
326
490
  subject.connection,
327
491
  tenantId,
328
- (tx) => tx.execute(probe.insert(tenantId, `row_${index}`))
492
+ (tx) => ask(tx, probe.insert(tenantId, `row_${index}`))
329
493
  )
330
494
  )
331
495
  );
332
496
  (0, import_conformance.assertSame)(
333
497
  probe.tenantsOf(
334
- await subject.seam.inOps(subject.connection, (tx) => tx.execute(probe.selectAll))
498
+ await subject.seam.inOps(subject.connection, (tx) => ask(tx, probe.selectAll))
335
499
  ),
336
500
  CONCURRENT.toSorted(),
337
501
  "writes made at the same moment did not each land under the tenant they named: the setting is not local to its transaction, so one connection carried another request's tenant"
@@ -347,13 +511,13 @@ var concurrentTenantsConformance = (subject) => {
347
511
  (tenantId, index) => subject.seam.inTenant(
348
512
  subject.connection,
349
513
  tenantId,
350
- (tx) => tx.execute(probe.insert(tenantId, `row_${index}`))
514
+ (tx) => ask(tx, probe.insert(tenantId, `row_${index}`))
351
515
  )
352
516
  )
353
517
  );
354
518
  const seen = await Promise.all(
355
519
  CONCURRENT.map(
356
- (tenantId) => subject.seam.inTenant(subject.connection, tenantId, (tx) => tx.execute(probe.selectAll)).then(probe.tenantsOf)
520
+ (tenantId) => subject.seam.inTenant(subject.connection, tenantId, (tx) => ask(tx, probe.selectAll)).then(probe.tenantsOf)
357
521
  )
358
522
  );
359
523
  (0, import_conformance.assertSame)(
@@ -370,7 +534,7 @@ var concurrentTenantsConformance = (subject) => {
370
534
  () => subject.seam.inTenant(
371
535
  subject.connection,
372
536
  A,
373
- (tx) => subject.seam.inTenant(tx, B, (inner) => inner.execute(probe.selectAll))
537
+ (tx) => subject.seam.inTenant(tx, B, (inner) => ask(inner, probe.selectAll))
374
538
  ),
375
539
  "a session open for one tenant accepted a query for another: whichever setting wins, one of the two queries runs behind the wrong policy",
376
540
  // #211/#213: the SEAM's own refusal, by class — a driver that crashed throws too.
@@ -384,7 +548,7 @@ var concurrentTenantsConformance = (subject) => {
384
548
  await (0, import_conformance.assertRefuses)(
385
549
  () => subject.seam.inOps(
386
550
  subject.connection,
387
- (tx) => subject.seam.inTenant(tx, A, (inner) => inner.execute(probe.selectAll))
551
+ (tx) => subject.seam.inTenant(tx, A, (inner) => ask(inner, probe.selectAll))
388
552
  ),
389
553
  "a tenant query reached from an ops sweep ran anyway \u2014 with no tenant named, behind the ops policy, which is every tenant",
390
554
  DbRefusal
@@ -403,10 +567,11 @@ var scopedOf = (subject) => {
403
567
  return scoped;
404
568
  };
405
569
  var scopedQueryConformance = (subject) => {
570
+ const ask = asking(subject);
406
571
  const probe = probeSql(subject.probe);
407
572
  const key = subject.seam.tenantKey ?? "tenantId";
408
573
  const insert = () => scopedOf(subject)(
409
- (tx, params) => tx.execute(probe.insert(params[key] ?? "", "row_scoped"))
574
+ (tx, params) => ask(tx, probe.insert(params[key] ?? "", "row_scoped"))
410
575
  );
411
576
  return [
412
577
  {
@@ -416,7 +581,7 @@ var scopedQueryConformance = (subject) => {
416
581
  await insert()(subject.connection, { [key]: A });
417
582
  (0, import_conformance.assertSame)(
418
583
  probe.tenantsOf(
419
- await subject.seam.inOps(subject.connection, (tx) => tx.execute(probe.selectAll))
584
+ await subject.seam.inOps(subject.connection, (tx) => ask(tx, probe.selectAll))
420
585
  ),
421
586
  [A],
422
587
  `the row a query carrying its tenant under "${key}" wrote did not land under that tenant: the seam read the key it wanted rather than the one the query carries, and what it named instead was nothing`
@@ -453,10 +618,16 @@ var forcedRowLevelSecurityConformance = (subject) => [
453
618
  run: async () => {
454
619
  const tables = subject.guardedTables ?? [subject.probe.name];
455
620
  const rows = (0, import_conformance.rowsOf)(
456
- await subject.connection.execute({
457
- params: [tables],
458
- text: "select relname as name, relforcerowsecurity as forced from pg_class where relname = any($1)"
459
- })
621
+ await asking(subject)(
622
+ subject.connection,
623
+ fragment(
624
+ [
625
+ "select relname as name, relforcerowsecurity as forced from pg_class where relname = any(",
626
+ ")"
627
+ ],
628
+ [tables]
629
+ )
630
+ )
460
631
  );
461
632
  (0, import_conformance.assertSame)(
462
633
  rows.map((row) => row.name).toSorted(),
@@ -471,6 +642,15 @@ var forcedRowLevelSecurityConformance = (subject) => [
471
642
  }
472
643
  }
473
644
  ];
645
+ var surfaceOf = (handle) => {
646
+ const found = /* @__PURE__ */ new Set();
647
+ for (let at = handle; at !== null && at !== Object.prototype; at = Object.getPrototypeOf(at)) {
648
+ for (const name of Object.getOwnPropertyNames(at)) {
649
+ if (name !== "constructor") found.add(name);
650
+ }
651
+ }
652
+ return [...found].toSorted();
653
+ };
474
654
  var perStepOf = (subject) => {
475
655
  const perStep = subject.perStep;
476
656
  if (typeof perStep !== "function") {
@@ -481,16 +661,43 @@ var perStepOf = (subject) => {
481
661
  return perStep;
482
662
  };
483
663
  var connectionsConformance = (subject) => {
484
- const probe = subject.probe ?? { text: "select 1" };
664
+ const ask = asking(subject);
665
+ const probe = subject.probe ?? fragment(["select 1"]);
485
666
  const CHARGE = "charge";
486
667
  const SHIP = "ship";
487
668
  return [
488
669
  {
489
670
  name: "a handle inside its invocation answers",
490
671
  run: () => subject.connections.withConnection(async () => {
491
- await subject.connections.sessionDb().execute(probe);
672
+ await ask(subject.connections.sessionDb(), probe);
492
673
  })
493
674
  },
675
+ {
676
+ name: "a handle keeps everything the driver put on it, so a query reaches what open() returned",
677
+ run: async () => {
678
+ const opening = subject.connections.open;
679
+ if (typeof opening !== "function") {
680
+ throw new import_conformance.ConformanceFailure(
681
+ 'this subject has no `open`, and "the handle a query is given is the one the driver opened" cannot be asked without the two to compare: supply it \u2014 or this claim is untested rather than passing'
682
+ );
683
+ }
684
+ const held = opening();
685
+ try {
686
+ const opened = surfaceOf(held.connection);
687
+ await subject.connections.withConnection(() => {
688
+ const given = surfaceOf(subject.connections.sessionDb());
689
+ (0, import_conformance.assertSame)(
690
+ opened.filter((name) => !given.includes(name)),
691
+ [],
692
+ "the handle inside the invocation answers to less than the one the driver returned: a keeper that rebuilds the handle out of the port hands a query something its own library never made, and everything the driver attached \u2014 an ORM database, a logger, a schema \u2014 is undefined from inside every scoped query"
693
+ );
694
+ return Promise.resolve();
695
+ });
696
+ } finally {
697
+ await held.close();
698
+ }
699
+ }
700
+ },
494
701
  {
495
702
  name: "a handle asked for outside every invocation is refused, naming what to wrap it in",
496
703
  run: () => (0, import_conformance.assertRefuses)(
@@ -505,10 +712,10 @@ var connectionsConformance = (subject) => {
505
712
  let held;
506
713
  await subject.connections.withConnection(async () => {
507
714
  held = subject.connections.sessionDb();
508
- await held.execute(probe);
715
+ await ask(held, probe);
509
716
  });
510
717
  await (0, import_conformance.assertRefuses)(
511
- () => held.execute(probe),
718
+ () => ask(held, probe),
512
719
  "a handle answered a query after the invocation that opened it had ended: its pool is closed or closing, and what comes back is the driver\u2019s own error at some later line rather than a refusal naming the lifetime that was crossed",
513
720
  DbRefusal
514
721
  );
@@ -524,9 +731,9 @@ var connectionsConformance = (subject) => {
524
731
  ofTheStep !== ofTheRun,
525
732
  "a step inside a run was handed the RUN\u2019s handle: a workflow entrypoint that opens an invocation around the run body hands every step a connection opened for another request, and workerd refuses to do I/O on it \u2014 the step joined the frame it was nested in rather than opening its own"
526
733
  );
527
- await ofTheStep.execute(probe);
734
+ await ask(ofTheStep, probe);
528
735
  });
529
- await ofTheRun.execute(probe);
736
+ await ask(ofTheRun, probe);
530
737
  })
531
738
  },
532
739
  {
@@ -536,11 +743,11 @@ var connectionsConformance = (subject) => {
536
743
  let charged;
537
744
  await perStep(CHARGE)(async () => {
538
745
  charged = subject.connections.sessionDb();
539
- await charged.execute(probe);
746
+ await ask(charged, probe);
540
747
  });
541
748
  await perStep(SHIP)(
542
749
  () => (0, import_conformance.assertRefuses)(
543
- () => charged.execute(probe),
750
+ () => ask(charged, probe),
544
751
  `the handle the "${CHARGE}" step opened answered a query inside the "${SHIP}" step: a durable run hibernates between its steps, so that handle is a dead socket, and the step that leaked it has to be in the refusal or the search starts at the top of the workflow`,
545
752
  CHARGE
546
753
  )
@@ -561,25 +768,14 @@ var sessionConformance = (subject) => [
561
768
  var import_node_async_hooks = require("async_hooks");
562
769
  var invocation = new import_node_async_hooks.AsyncLocalStorage();
563
770
  var whose = (frame) => frame.named === void 0 ? "an invocation" : `the "${frame.named}" unit of work`;
564
- var guarded = (frame, connection) => {
565
- const stillOpen = () => {
566
- if (frame.live) return;
567
- throw new DbRefusal(
568
- `this handle was opened in ${whose(frame)} and that invocation has ended: its connection is closed or closing, so a query on it now is a query on a dead socket. Open the handle inside the unit of work that uses it \u2014 a durable run hibernates between its steps, and a handle carried across one belongs to a request that is gone.`
569
- );
570
- };
571
- return {
572
- execute: async (statement) => {
573
- stillOpen();
574
- return connection.execute(statement);
575
- },
576
- transaction: async (run) => {
577
- stillOpen();
578
- return connection.transaction(run);
579
- }
580
- };
771
+ var stillOpenIn = (frame) => () => {
772
+ if (frame.live) return;
773
+ throw new DbRefusal(
774
+ `this handle was opened in ${whose(frame)} and that invocation has ended: its connection is closed or closing, so a query on it now is a query on a dead socket. Open the handle inside the unit of work that uses it \u2014 a durable run hibernates between its steps, and a handle carried across one belongs to a request that is gone.`
775
+ );
581
776
  };
582
777
  var createConnections = (config) => {
778
+ const guard = config.over?.guard ?? executorHandles.guard;
583
779
  const opened = () => config.open === void 0 ? config.driver.open(config.connectionString) : config.driver.open(config.connectionString, config.open);
584
780
  return {
585
781
  inInvocation: () => invocation.getStore() !== void 0,
@@ -594,7 +790,10 @@ var createConnections = (config) => {
594
790
  const known = frame.held.get(config.connectionString);
595
791
  if (known !== void 0) return known.guarded;
596
792
  const open = opened();
597
- const held = { guarded: guarded(frame, open.connection), open };
793
+ const held = {
794
+ guarded: guard(open.connection, stillOpenIn(frame)),
795
+ open
796
+ };
598
797
  frame.held.set(config.connectionString, held);
599
798
  return held.guarded;
600
799
  },
@@ -625,6 +824,701 @@ var perStepConnection = (connections) => ({
625
824
  })
626
825
  });
627
826
 
827
+ // src/drizzle.ts
828
+ var loadDrizzle = async () => {
829
+ let loaded;
830
+ try {
831
+ loaded = await import("drizzle-orm");
832
+ } catch (error) {
833
+ throw new DbRefusal(
834
+ "drizzle-orm is an optional peer dependency of @geonosis/db and is not installed \u2014 `npm install drizzle-orm`, or pass your own copy as `drizzleSession({ drizzleOrm })`",
835
+ { cause: error }
836
+ );
837
+ }
838
+ return loaded.default ?? loaded;
839
+ };
840
+ var speaking = (module2) => {
841
+ const sql2 = module2?.sql;
842
+ if (typeof sql2?.join !== "function" || typeof sql2.param !== "function") {
843
+ throw new DbRefusal(
844
+ "what was given as drizzle-orm has no `sql` with `join`, `param` and `raw` \u2014 a statement is built out of those three, and whatever answered to that name is not the library this expects"
845
+ );
846
+ }
847
+ return sql2;
848
+ };
849
+ var database = (db) => {
850
+ if (typeof db?.execute !== "function" || typeof db.transaction !== "function") {
851
+ throw new DbRefusal(
852
+ "what was given as a drizzle database has no `execute` and `transaction` \u2014 a session is opened through the second and named through the first"
853
+ );
854
+ }
855
+ return db;
856
+ };
857
+ var guarding = (db, stillOpen) => new Proxy(db, {
858
+ get: (held, property) => {
859
+ stillOpen();
860
+ const value2 = Reflect.get(held, property);
861
+ return typeof value2 === "function" ? value2.bind(held) : value2;
862
+ }
863
+ });
864
+ var drizzleSession = async (options = {}) => {
865
+ const sql2 = speaking(options.drizzleOrm ?? await loadDrizzle());
866
+ const built = (statement) => sql2.join(
867
+ statement.chunks.flatMap(
868
+ (chunk, at) => at === 0 ? [sql2.raw(chunk)] : [sql2.param(statement.values[at - 1]), sql2.raw(chunk)]
869
+ )
870
+ );
871
+ return {
872
+ guard: guarding,
873
+ opens: (db) => typeof db?.transaction === "function",
874
+ send: async (db, statement) => await database(db).execute(built(statement)),
875
+ transaction: (db, run) => database(db).transaction((tx) => run(tx))
876
+ };
877
+ };
878
+
879
+ // src/entity.ts
880
+ var import_conformance2 = require("@geonosis/conformance");
881
+ var then = (before, after2) => ({
882
+ chunks: [
883
+ ...before.chunks.slice(0, -1),
884
+ `${before.chunks.at(-1) ?? ""}${after2.chunks[0] ?? ""}`,
885
+ ...after2.chunks.slice(1)
886
+ ],
887
+ values: [...before.values, ...after2.values]
888
+ });
889
+ var all = (pieces) => pieces.reduce((built, piece) => then(built, piece), fragment([""]));
890
+ var value = (held) => fragment(["", ""], [held]);
891
+ var sql = (text) => fragment([text]);
892
+ var DEFAULT_COLUMNS = {
893
+ body: "body",
894
+ createdAt: "created_at",
895
+ retiredAt: "retired_at",
896
+ tenant: "tenant_id",
897
+ updatedAt: "updated_at"
898
+ };
899
+ var DEFAULT_ENTITY_PAGE = 50;
900
+ var MAX_ENTITY_PAGE = 100;
901
+ var CURSOR = "cursor_";
902
+ var paged = (limit) => Math.min(Math.max(limit ?? DEFAULT_ENTITY_PAGE, 1), MAX_ENTITY_PAGE);
903
+ var stampOf = (order) => order.map((one) => `${one.column}:${one.direction ?? "asc"}`).join(",");
904
+ var readCursor = (cursor, order) => {
905
+ let held;
906
+ try {
907
+ held = JSON.parse(decodeURIComponent(cursor));
908
+ } catch {
909
+ throw new DbRefusal(
910
+ "this cursor was not minted by this listing: a page is reached by the key of the row before it, and that key is not readable here \u2014 an unreadable cursor answered with the first page again is a caller silently reading the list twice"
911
+ );
912
+ }
913
+ const [stamp, ...values2] = Array.isArray(held) ? held : [];
914
+ if (stamp !== stampOf(order) || values2.length !== order.length) {
915
+ throw new DbRefusal(
916
+ `this cursor was minted under another ordering or another width, and the rows after it are another list's: this one is ordered by ${stampOf(order)}`
917
+ );
918
+ }
919
+ return values2;
920
+ };
921
+ var after = (order, values2) => all([
922
+ sql(" and ("),
923
+ all(
924
+ order.map(
925
+ (column, depth) => all([
926
+ sql(depth === 0 ? "(" : " or ("),
927
+ all(
928
+ order.slice(0, depth).map(
929
+ (tied, at) => all([sql(`${quoted(tied.column)} = `), value(values2[at]), sql(" and ")])
930
+ )
931
+ ),
932
+ sql(`${quoted(column.column)} ${column.direction === "desc" ? "<" : ">"} `),
933
+ value(values2[depth]),
934
+ sql(")")
935
+ ])
936
+ )
937
+ ),
938
+ sql(")")
939
+ ]);
940
+ var defineEntity = (definition) => {
941
+ const columns = { ...DEFAULT_COLUMNS, ...definition.columns };
942
+ const { key, order, seam, table } = definition;
943
+ const on = qualified(table, definition.schema);
944
+ const tenantKey = seam.tenantKey;
945
+ const send = (handle, statement) => seam.send(handle, statement);
946
+ const keyed = (params) => params[key.param];
947
+ const withKey = (params, rest) => ({ [key.param]: params[key.param], ...rest });
948
+ const tenantOf = (params) => params[tenantKey];
949
+ const selected = [
950
+ `${quoted(key.column)} as "key"`,
951
+ `${quoted(columns.body)} as "body"`,
952
+ `${quoted(columns.createdAt)} as "createdAt"`,
953
+ `${quoted(columns.updatedAt)} as "updatedAt"`,
954
+ ...order.map((one, at) => `${quoted(one.column)} as ${quoted(`${CURSOR}${at}`)}`)
955
+ ].join(", ");
956
+ const alive = (tenant) => all([
957
+ sql(`select ${selected} from ${on} where ${quoted(columns.tenant)} = `),
958
+ value(tenant),
959
+ sql(` and ${quoted(columns.retiredAt)} is null`)
960
+ ]);
961
+ const ordered = order.map((one) => `${quoted(one.column)} ${one.direction === "desc" ? "desc" : "asc"}`).join(", ");
962
+ const itemsOf = (rows) => rows.map((row) => definition.item(row));
963
+ const cursorOf = (row) => encodeURIComponent(
964
+ JSON.stringify([stampOf(order), ...order.map((_, at) => row[`${CURSOR}${at}`])])
965
+ );
966
+ const get = async (handle, params) => {
967
+ const [row] = (0, import_conformance2.rowsOf)(
968
+ await send(
969
+ handle,
970
+ all([
971
+ alive(tenantOf(params)),
972
+ sql(` and ${quoted(key.column)} = `),
973
+ value(keyed(params)),
974
+ sql(" limit 1")
975
+ ])
976
+ )
977
+ );
978
+ return row === void 0 ? null : definition.item(row);
979
+ };
980
+ const listAll = async (handle, params) => itemsOf(
981
+ (0, import_conformance2.rowsOf)(
982
+ await send(handle, then(alive(tenantOf(params)), sql(` order by ${ordered}`)))
983
+ )
984
+ );
985
+ const list = async (handle, params) => {
986
+ const limit = paged(params.limit);
987
+ const rows = (0, import_conformance2.rowsOf)(
988
+ await send(
989
+ handle,
990
+ all([
991
+ alive(tenantOf(params)),
992
+ params.cursor === void 0 ? sql("") : after(order, readCursor(params.cursor, order)),
993
+ sql(` order by ${ordered} limit `),
994
+ value(limit + 1)
995
+ ])
996
+ )
997
+ );
998
+ const page = rows.slice(0, limit);
999
+ const last = page.at(-1);
1000
+ return {
1001
+ items: itemsOf(page),
1002
+ ...rows.length > limit && last !== void 0 ? { nextCursor: cursorOf(last) } : {}
1003
+ };
1004
+ };
1005
+ const upsert = async (handle, params) => {
1006
+ const now = Date.now();
1007
+ const indexed = definition.indexed?.(params.body) ?? {};
1008
+ const names = Object.keys(indexed);
1009
+ const tenant = tenantOf(params);
1010
+ const [previous] = (0, import_conformance2.rowsOf)(
1011
+ await send(
1012
+ handle,
1013
+ all([
1014
+ sql(
1015
+ `select ${quoted(columns.body)} as "body", ${quoted(columns.retiredAt)} as "retiredAt" from ${on} where ${quoted(columns.tenant)} = `
1016
+ ),
1017
+ value(tenant),
1018
+ sql(` and ${quoted(key.column)} = `),
1019
+ value(keyed(params)),
1020
+ sql(" for update")
1021
+ ])
1022
+ )
1023
+ );
1024
+ const written2 = [
1025
+ { column: columns.body, held: params.body },
1026
+ { column: columns.updatedAt, held: now },
1027
+ ...names.map((name) => ({ column: name, held: indexed[name] }))
1028
+ ];
1029
+ await send(
1030
+ handle,
1031
+ all([
1032
+ sql(
1033
+ `insert into ${on} (${[columns.tenant, key.column, columns.createdAt, columns.retiredAt, ...written2.map((one) => one.column)].map(quoted).join(", ")}) values (`
1034
+ ),
1035
+ value(tenant),
1036
+ sql(", "),
1037
+ value(keyed(params)),
1038
+ sql(", "),
1039
+ value(now),
1040
+ sql(", null"),
1041
+ all(written2.map((one) => then(sql(", "), value(one.held)))),
1042
+ sql(
1043
+ `) on conflict (${quoted(columns.tenant)}, ${quoted(key.column)}) do update set ${quoted(columns.retiredAt)} = null, ${written2.map((one) => `${quoted(one.column)} = excluded.${quoted(one.column)}`).join(", ")}`
1044
+ )
1045
+ ])
1046
+ );
1047
+ return withKey(params, {
1048
+ previous: previous === void 0 || previous.retiredAt !== null ? null : previous.body
1049
+ });
1050
+ };
1051
+ const retire = async (handle, params) => {
1052
+ const now = Date.now();
1053
+ const [row] = (0, import_conformance2.rowsOf)(
1054
+ await send(
1055
+ handle,
1056
+ all([
1057
+ sql(`update ${on} set ${quoted(columns.retiredAt)} = `),
1058
+ value(now),
1059
+ sql(`, ${quoted(columns.updatedAt)} = `),
1060
+ value(now),
1061
+ sql(` where ${quoted(columns.tenant)} = `),
1062
+ value(tenantOf(params)),
1063
+ sql(` and ${quoted(key.column)} = `),
1064
+ value(keyed(params)),
1065
+ sql(
1066
+ ` and ${quoted(columns.retiredAt)} is null returning ${quoted(columns.body)} as "body"`
1067
+ )
1068
+ ])
1069
+ )
1070
+ );
1071
+ return withKey(params, { retired: row === void 0 ? null : row.body });
1072
+ };
1073
+ const surface = {
1074
+ get: seam.scoped(get),
1075
+ list: seam.scoped(list),
1076
+ listAll: seam.scoped(listAll),
1077
+ retire: seam.scoped(retire),
1078
+ upsert: seam.scoped(upsert)
1079
+ };
1080
+ const addressed = (output, tenantId) => ({ [tenantKey]: tenantId, [key.param]: output[key.param] });
1081
+ return {
1082
+ ...surface,
1083
+ // A write is undone by writing what was there before it, which is what the forward verb
1084
+ // answered with: nothing else has to be remembered anywhere.
1085
+ undos: {
1086
+ retire: async (handle, { output, tenantId }) => {
1087
+ if (output.retired === null) return;
1088
+ await surface.upsert(handle, { ...addressed(output, tenantId), body: output.retired });
1089
+ },
1090
+ upsert: async (handle, { output, tenantId }) => {
1091
+ await (output.previous === null ? surface.retire(handle, addressed(output, tenantId)) : surface.upsert(handle, {
1092
+ ...addressed(output, tenantId),
1093
+ body: output.previous
1094
+ }));
1095
+ }
1096
+ }
1097
+ };
1098
+ };
1099
+ var undosOf = (entity) => entity.undos;
1100
+
1101
+ // src/entity-conformance.ts
1102
+ var import_conformance3 = require("@geonosis/conformance");
1103
+ var A2 = "tenant_entity_a";
1104
+ var B2 = "tenant_entity_b";
1105
+ var asking2 = (subject) => {
1106
+ const at = (tenantId, key) => ({
1107
+ [subject.tenantKey]: tenantId,
1108
+ ...key === void 0 ? {} : { [subject.keyParam]: key }
1109
+ });
1110
+ return {
1111
+ at,
1112
+ get: (tenantId, key) => subject.entity.get(subject.connection, at(tenantId, key)),
1113
+ list: (tenantId, page = {}) => subject.entity.list(subject.connection, { ...at(tenantId), ...page }),
1114
+ listAll: (tenantId) => subject.entity.listAll(subject.connection, at(tenantId)),
1115
+ retire: (tenantId, key) => subject.entity.retire(subject.connection, at(tenantId, key)),
1116
+ upsert: (tenantId, key, body) => subject.entity.upsert(subject.connection, { ...at(tenantId, key), body })
1117
+ };
1118
+ };
1119
+ var entityConformance = (subject) => {
1120
+ const ask = asking2(subject);
1121
+ const [first, second] = subject.bodies;
1122
+ const [one, two, three] = subject.keys;
1123
+ const written2 = async (tenantId) => {
1124
+ for (const key of subject.keys) await ask.upsert(tenantId, key, first);
1125
+ };
1126
+ return [
1127
+ {
1128
+ name: "a row written is read back by its key",
1129
+ run: async () => {
1130
+ await subject.reset();
1131
+ await ask.upsert(A2, one, first);
1132
+ const found = await ask.get(A2, one);
1133
+ (0, import_conformance3.assertThat)(found !== null, "the row that was just written was read back as nothing");
1134
+ (0, import_conformance3.assertSame)(
1135
+ subject.bodyOf(found),
1136
+ first,
1137
+ "the row read back is not the body that was written"
1138
+ );
1139
+ }
1140
+ },
1141
+ {
1142
+ name: "a key nothing was written under is nothing, not an empty row",
1143
+ run: async () => {
1144
+ await subject.reset();
1145
+ (0, import_conformance3.assertIs)(
1146
+ await ask.get(A2, one),
1147
+ null,
1148
+ "a key with no row behind it answered with something: a read that invents an empty row is a caller writing over nothing"
1149
+ );
1150
+ }
1151
+ },
1152
+ {
1153
+ name: "an upsert answers with the body it replaced, and with nothing when it replaced nothing",
1154
+ run: async () => {
1155
+ await subject.reset();
1156
+ const created = await ask.upsert(A2, one, first);
1157
+ (0, import_conformance3.assertIs)(
1158
+ created.previous,
1159
+ null,
1160
+ "a write over an empty key answered with a previous body: its undo would restore a row that never existed"
1161
+ );
1162
+ const replaced = await ask.upsert(A2, one, second);
1163
+ (0, import_conformance3.assertSame)(
1164
+ replaced.previous,
1165
+ first,
1166
+ "a write over a living row did not answer with what it replaced, so nothing can put it back"
1167
+ );
1168
+ }
1169
+ },
1170
+ {
1171
+ name: "every living row of one tenant is listed, in the listing order",
1172
+ run: async () => {
1173
+ await subject.reset();
1174
+ await written2(A2);
1175
+ (0, import_conformance3.assertIs)(
1176
+ (await ask.listAll(A2)).length,
1177
+ 3,
1178
+ "the listing answered with a different number of rows than were written"
1179
+ );
1180
+ }
1181
+ },
1182
+ {
1183
+ name: "a page carries a cursor while rows remain, and none on the last page",
1184
+ run: async () => {
1185
+ await subject.reset();
1186
+ await written2(A2);
1187
+ const page = await ask.list(A2, { limit: 2 });
1188
+ (0, import_conformance3.assertIs)(page.items.length, 2, "a page of two answered with another number of rows");
1189
+ (0, import_conformance3.assertThat)(
1190
+ page.nextCursor !== void 0,
1191
+ "a page with rows behind it carried no cursor, so nothing can reach them"
1192
+ );
1193
+ const last = await ask.list(A2, { cursor: page.nextCursor, limit: 2 });
1194
+ (0, import_conformance3.assertIs)(last.items.length, 1, "the page after the cursor answered with the wrong rows");
1195
+ (0, import_conformance3.assertIs)(
1196
+ last.nextCursor,
1197
+ void 0,
1198
+ "the last page carried a cursor: a caller who follows it asks for a page that is not there, and a list that never ends is how a walk becomes a loop"
1199
+ );
1200
+ }
1201
+ },
1202
+ {
1203
+ name: "a walk one row at a time visits every row exactly once",
1204
+ run: async () => {
1205
+ await subject.reset();
1206
+ await written2(A2);
1207
+ const seen = [];
1208
+ let cursor;
1209
+ for (let page = 0; page < subject.keys.length + 1; page += 1) {
1210
+ const answered = await ask.list(A2, { cursor, limit: 1 });
1211
+ seen.push(...answered.items);
1212
+ cursor = answered.nextCursor;
1213
+ if (cursor === void 0) break;
1214
+ }
1215
+ (0, import_conformance3.assertIs)(
1216
+ seen.length,
1217
+ subject.keys.length,
1218
+ "the walk saw a different number of rows than the table holds: a keyset that repeats a row or skips one is a page boundary landing on a tie"
1219
+ );
1220
+ }
1221
+ },
1222
+ {
1223
+ name: "a retired row is gone from every read",
1224
+ run: async () => {
1225
+ await subject.reset();
1226
+ await ask.upsert(A2, one, first);
1227
+ const retired = await ask.retire(A2, one);
1228
+ (0, import_conformance3.assertSame)(retired.retired, first, "a retire did not answer with the body it took away");
1229
+ (0, import_conformance3.assertIs)(await ask.get(A2, one), null, "a retired row was still read back by its key");
1230
+ (0, import_conformance3.assertIs)((await ask.listAll(A2)).length, 0, "a retired row was still listed");
1231
+ }
1232
+ },
1233
+ {
1234
+ name: "the undo of an upsert puts back the body that was there",
1235
+ run: async () => {
1236
+ await subject.reset();
1237
+ await ask.upsert(A2, one, first);
1238
+ const replaced = await ask.upsert(A2, one, second);
1239
+ await subject.entity.undos.upsert(subject.connection, {
1240
+ output: replaced,
1241
+ tenantId: A2
1242
+ });
1243
+ const found = await ask.get(A2, one);
1244
+ (0, import_conformance3.assertSame)(
1245
+ found === null ? null : subject.bodyOf(found),
1246
+ first,
1247
+ "the compensation for a write left the new body in place: a run that failed after this step is a run whose write stood"
1248
+ );
1249
+ }
1250
+ },
1251
+ {
1252
+ name: "the undo of an upsert that created the row retires it",
1253
+ run: async () => {
1254
+ await subject.reset();
1255
+ const created = await ask.upsert(A2, one, first);
1256
+ await subject.entity.undos.upsert(subject.connection, { output: created, tenantId: A2 });
1257
+ (0, import_conformance3.assertIs)(
1258
+ await ask.get(A2, one),
1259
+ null,
1260
+ "the compensation for a write that created a row left the row behind, because it restored a body that was never there"
1261
+ );
1262
+ }
1263
+ },
1264
+ {
1265
+ name: "the undo of a retire writes the row back",
1266
+ run: async () => {
1267
+ await subject.reset();
1268
+ await ask.upsert(A2, one, first);
1269
+ const retired = await ask.retire(A2, one);
1270
+ await subject.entity.undos.retire(subject.connection, { output: retired, tenantId: A2 });
1271
+ const found = await ask.get(A2, one);
1272
+ (0, import_conformance3.assertSame)(
1273
+ found === null ? null : subject.bodyOf(found),
1274
+ first,
1275
+ "the compensation for a retire did not bring the row back"
1276
+ );
1277
+ }
1278
+ },
1279
+ {
1280
+ name: "the undo of a retire that retired nothing writes nothing",
1281
+ run: async () => {
1282
+ await subject.reset();
1283
+ const retired = await ask.retire(A2, one);
1284
+ await subject.entity.undos.retire(subject.connection, { output: retired, tenantId: A2 });
1285
+ (0, import_conformance3.assertIs)(
1286
+ (await ask.listAll(A2)).length,
1287
+ 0,
1288
+ "the compensation for a retire that found no row wrote one: an undo that invents a row is worse than one that does nothing"
1289
+ );
1290
+ }
1291
+ },
1292
+ {
1293
+ name: "a write in a session is five statements, the row it replaces locked before the insert",
1294
+ run: async () => {
1295
+ const recording = subject.recording;
1296
+ if (typeof recording !== "function") {
1297
+ throw new import_conformance3.ConformanceFailure(
1298
+ "this subject has no `recording`, and the write census is a count of what reached the server: supply a handle that reports the statements it sends \u2014 a driver log hook is usually one line \u2014 or this claim is untested rather than passing"
1299
+ );
1300
+ }
1301
+ await subject.reset();
1302
+ const recorded = await recording();
1303
+ try {
1304
+ await subject.entity.upsert(recorded.connection, {
1305
+ ...ask.at(A2, one),
1306
+ body: first
1307
+ });
1308
+ } finally {
1309
+ await recorded.close();
1310
+ }
1311
+ const { statements } = recorded;
1312
+ const found = (pattern) => statements.filter((sent) => pattern.test(sent)).length;
1313
+ (0, import_conformance3.assertIs)(
1314
+ found(/savepoint/i),
1315
+ 0,
1316
+ `the write opened a savepoint, which means a nested transaction per statement: ${statements.join(" | ")}`
1317
+ );
1318
+ (0, import_conformance3.assertIs)(
1319
+ found(/set_config/i),
1320
+ 1,
1321
+ `the tenant was named ${found(/set_config/i)} times for one write: ${statements.join(" | ")}`
1322
+ );
1323
+ (0, import_conformance3.assertThat)(
1324
+ statements.findIndex((sent) => /for update/i.test(sent)) < statements.findIndex((sent) => /on conflict/i.test(sent)),
1325
+ `the row was written before it was read under a lock, or never locked at all: at READ COMMITTED the pre-image is then whatever a concurrent commit left, and the undo restores a version this write never replaced \u2014 ${statements.join(" | ")}`
1326
+ );
1327
+ (0, import_conformance3.assertIs)(
1328
+ statements.length,
1329
+ 5,
1330
+ `a write in a session is the transaction, the tenant, the locked read, the write and the commit: a sixth is a round trip somebody added \u2014 ${statements.join(" | ")}`
1331
+ );
1332
+ }
1333
+ },
1334
+ {
1335
+ name: "the wall on this entity\u2019s table reads the setting names it was rendered with",
1336
+ run: async () => {
1337
+ const wall = subject.wall;
1338
+ if (wall === void 0) {
1339
+ throw new import_conformance3.ConformanceFailure(
1340
+ 'this subject names no `wall`, and "the DDL, the policies and the session name the same two settings" is the claim a table rendered with the defaults cannot make: supply the settings, the ops lever and an unfiltered count \u2014 or this claim is untested rather than passing'
1341
+ );
1342
+ }
1343
+ await subject.reset();
1344
+ await ask.upsert(A2, one, first);
1345
+ await ask.upsert(B2, two, second);
1346
+ (0, import_conformance3.assertIs)(
1347
+ (await ask.listAll(A2)).length,
1348
+ 1,
1349
+ `a tenant saw more than its own rows on a table whose isolation policy reads "${wall.settings.tenantSetting}": the DDL was rendered with one setting name and the session names another, so the policy compares against nothing`
1350
+ );
1351
+ (0, import_conformance3.assertIs)(
1352
+ await wall.inOps((tx) => wall.countAll(tx)),
1353
+ 2,
1354
+ `the ops lever saw ${await wall.inOps((tx) => wall.countAll(tx))} of 2 rows on a table whose maintenance policy reads "${wall.settings.opsSetting}": every sweep, backfill and repair on this entity has to be run once per tenant or not at all`
1355
+ );
1356
+ }
1357
+ },
1358
+ {
1359
+ name: "one tenant never reaches another\u2019s rows through any verb",
1360
+ run: async () => {
1361
+ await subject.reset();
1362
+ await ask.upsert(A2, one, first);
1363
+ await ask.upsert(B2, two, second);
1364
+ (0, import_conformance3.assertIs)(await ask.get(B2, one), null, "a tenant read a row belonging to another by its key");
1365
+ (0, import_conformance3.assertIs)(
1366
+ (await ask.listAll(B2)).length,
1367
+ 1,
1368
+ "a tenant listed rows belonging to another: the verbs are scoped, the wall is not, or both"
1369
+ );
1370
+ (0, import_conformance3.assertIs)(
1371
+ (await ask.retire(B2, one)).retired,
1372
+ null,
1373
+ "a tenant retired a row belonging to another, and was told what it took away"
1374
+ );
1375
+ (0, import_conformance3.assertThat)(
1376
+ await ask.get(A2, one) !== null,
1377
+ "the row of the first tenant is gone after the second tried to retire it"
1378
+ );
1379
+ (0, import_conformance3.assertIs)(await ask.get(A2, three), null, "a key nobody wrote answered with a row");
1380
+ }
1381
+ }
1382
+ ];
1383
+ };
1384
+
1385
+ // src/policies.ts
1386
+ var settingRead = (name) => `(select current_setting(${literal(name)}, true))`;
1387
+ var both = (predicate) => `using (${predicate}) with check (${predicate})`;
1388
+ var DEFAULT_NAMES = {
1389
+ freeze: "not_frozen",
1390
+ isolation: "tenant_isolation",
1391
+ opsMaintenance: "ops_maintenance"
1392
+ };
1393
+ var TOKEN = /^\{\{[A-Za-z][A-Za-z0-9_]*\}\}$/;
1394
+ var templated = (settings) => TOKEN.test(settings.tenantSetting) || TOKEN.test(settings.opsSetting);
1395
+ var readForDdl = (settings) => templated(settings) ? { opsValue: settings.opsValue ?? DEFAULT_OPS_VALUE, ...settings } : readSettings(settings);
1396
+ var tenantPolicySet = (options) => {
1397
+ const settings = readForDdl(options.settings);
1398
+ const names = { ...DEFAULT_NAMES, ...options.names };
1399
+ const tenant = settingRead(settings.tenantSetting);
1400
+ const opsIsOn = `${settingRead(settings.opsSetting)} = ${literal(settings.opsValue)}`;
1401
+ const frozen = options.freeze;
1402
+ return [
1403
+ {
1404
+ as: "permissive",
1405
+ name: names.isolation,
1406
+ predicate: `${quoted(options.tenantColumn)} = ${tenant}`
1407
+ },
1408
+ { as: "permissive", name: names.opsMaintenance, predicate: opsIsOn },
1409
+ ...frozen === void 0 ? [] : [
1410
+ {
1411
+ as: "restrictive",
1412
+ name: names.freeze,
1413
+ predicate: `${opsIsOn} or not exists (select 1 from ${quoted(frozen.table)} where ${quoted(frozen.tenantColumn)} = ${tenant})`
1414
+ }
1415
+ ]
1416
+ ];
1417
+ };
1418
+ var drizzleTenantPolicies = (tools, options) => tenantPolicySet(options).map(
1419
+ (policy) => tools.pgPolicy(policy.name, {
1420
+ as: policy.as,
1421
+ for: "all",
1422
+ using: tools.sql.raw(policy.predicate),
1423
+ withCheck: tools.sql.raw(policy.predicate)
1424
+ })
1425
+ );
1426
+ var forceRowLevelSecurity = (table, schema) => `alter table ${qualified(table, schema)} force row level security`;
1427
+ var tenantPolicies = (table, options) => {
1428
+ const on = qualified(table, options.schema);
1429
+ return [
1430
+ `alter table ${on} enable row level security`,
1431
+ forceRowLevelSecurity(table, options.schema),
1432
+ ...tenantPolicySet(options).flatMap((policy) => [
1433
+ `drop policy if exists ${quoted(policy.name)} on ${on}`,
1434
+ `create policy ${quoted(policy.name)} on ${on} as ${policy.as} for all ${both(policy.predicate)}`
1435
+ ])
1436
+ ];
1437
+ };
1438
+
1439
+ // src/entity-ddl.ts
1440
+ var ENTITY_PLACEHOLDERS = {
1441
+ opsSetting: "{{opsSetting}}",
1442
+ tenantSetting: "{{tenantSetting}}"
1443
+ };
1444
+ var DEFAULT_COLUMNS2 = {
1445
+ body: "body",
1446
+ createdAt: "created_at",
1447
+ retiredAt: "retired_at",
1448
+ tenant: "tenant_id",
1449
+ updatedAt: "updated_at"
1450
+ };
1451
+ var NAME = /^[A-Za-z_][A-Za-z0-9_$]*$/;
1452
+ var readName = (name) => {
1453
+ if (!NAME.test(name)) {
1454
+ throw new DbRefusal(
1455
+ `"${name}" is not a name this DDL can write: it is inlined into every statement inside double quotes, and a name outside \`letter_or_underscore followed by letters, digits, underscores or $\` could close them`
1456
+ );
1457
+ }
1458
+ return name;
1459
+ };
1460
+ var accepted = (because, rules, statement) => `-- ${because}
1461
+ -- squawk-ignore ${rules.join(", ")}
1462
+ ${statement}`;
1463
+ var TIMEOUTS_BELONG_TO_THE_DEPLOYMENT = "lock_timeout and statement_timeout are one deployment\u2019s numbers, and a value written into shipped DDL imposes that deployment\u2019s budget on every consumer: set them in the session that migrates.";
1464
+ var IN_ONE_TRANSACTION = "The migrator applies this file inside a transaction, which is also why CONCURRENTLY is not available here; the index is created in the same migration as its table, over no rows.";
1465
+ var RERUNNABLE = "This file is applied inside a transaction and every statement in it is idempotent, so a run that fails part way through leaves nothing behind and a rerun is a no-op.";
1466
+ var settingRead2 = (name) => `current_setting(${literal(name)}, true)`;
1467
+ var columnOf = (one) => typeof one === "string" ? `${quoted(readName(one))} text` : `${quoted(readName(one.column))} ${one.type}`;
1468
+ var entityTable = (options) => {
1469
+ const columns = { ...DEFAULT_COLUMNS2, ...options.columns };
1470
+ const table = readName(options.table);
1471
+ const on = qualified(table, options.schema);
1472
+ const key = readName(options.key);
1473
+ const listing = options.order.map((one) => `${quoted(readName(one.column))} ${one.direction === "desc" ? "desc" : "asc"}`).join(", ");
1474
+ return [
1475
+ `create table if not exists ${on} (
1476
+ ${quoted(columns.tenant)} text not null default ${settingRead2(options.settings.tenantSetting)},
1477
+ ${quoted(key)} text not null,
1478
+ ${quoted(columns.body)} jsonb not null,
1479
+ ${quoted(columns.createdAt)} bigint not null,
1480
+ ${quoted(columns.updatedAt)} bigint not null,
1481
+ ${quoted(columns.retiredAt)} bigint,${(options.indexed ?? []).map((one) => `
1482
+ ${columnOf(one)},`).join("")}
1483
+ constraint ${quoted(`${table}_pkey`)} primary key (${quoted(columns.tenant)}, ${quoted(key)})
1484
+ )`,
1485
+ accepted(
1486
+ `${IN_ONE_TRANSACTION} ${TIMEOUTS_BELONG_TO_THE_DEPLOYMENT}`,
1487
+ ["require-concurrent-index-creation", "require-lock-timeout", "require-statement-timeout"],
1488
+ `create index if not exists ${quoted(`${table}_listing_idx`)} on ${on} (${quoted(columns.tenant)}, ${listing})`
1489
+ )
1490
+ ];
1491
+ };
1492
+ var both2 = (predicate) => `using (${predicate}) with check (${predicate})`;
1493
+ var entityPolicies = (options) => {
1494
+ const columns = { ...DEFAULT_COLUMNS2, ...options.columns };
1495
+ const on = qualified(readName(options.table), options.schema);
1496
+ const wall = {
1497
+ settings: options.settings,
1498
+ tenantColumn: columns.tenant,
1499
+ ...options.schema === void 0 ? {} : { schema: options.schema }
1500
+ };
1501
+ return [
1502
+ accepted(
1503
+ `${RERUNNABLE} ${TIMEOUTS_BELONG_TO_THE_DEPLOYMENT}`,
1504
+ ["prefer-robust-stmts", "require-lock-timeout", "require-statement-timeout"],
1505
+ `alter table ${on} enable row level security`
1506
+ ),
1507
+ accepted(RERUNNABLE, ["prefer-robust-stmts"], `alter table ${on} force row level security`),
1508
+ ...tenantPolicySet(wall).flatMap((policy) => [
1509
+ `drop policy if exists ${quoted(policy.name)} on ${on}`,
1510
+ `create policy ${quoted(policy.name)} on ${on} as ${policy.as} for all ${both2(policy.predicate)}`
1511
+ ])
1512
+ ];
1513
+ };
1514
+ var entityMigrations = (options) => {
1515
+ const table = readName(options.table);
1516
+ return [
1517
+ { name: `0001_${table}.sql`, statements: entityTable(options) },
1518
+ { name: `0002_${table}_policies.sql`, statements: entityPolicies(options) }
1519
+ ];
1520
+ };
1521
+
628
1522
  // src/node-postgres.ts
629
1523
  var import_promises = require("fs/promises");
630
1524
  var import_node_path = require("path");
@@ -650,10 +1544,10 @@ var drivingPg = (module2) => {
650
1544
  };
651
1545
  var values = (statement) => statement.params === void 0 ? void 0 : [...statement.params];
652
1546
  var DEFAULT_MIGRATIONS_TABLE = "geonosis_db_migrations";
653
- var nodePostgresDriver = async (options = {}) => {
1547
+ var nodePostgresDriver = (async (options = {}) => {
654
1548
  const pg = drivingPg(options.pg ?? await loadPg());
655
1549
  const ledger = quoted(options.migrationsTable ?? DEFAULT_MIGRATIONS_TABLE);
656
- const open = (connectionString, opened = {}) => {
1550
+ const openPort = (connectionString, opened = {}) => {
657
1551
  const say = opened.onStatement ?? (() => {
658
1552
  });
659
1553
  const pool = new pg.Pool({
@@ -666,31 +1560,33 @@ var nodePostgresDriver = async (options = {}) => {
666
1560
  say(statement.text);
667
1561
  return run(statement.text, values(statement));
668
1562
  };
669
- return {
670
- close: () => pool.end(),
671
- connection: {
672
- execute: (statement) => sent((text, params) => pool.query(text, params), statement),
673
- transaction: async (run) => {
674
- const client = await pool.connect();
675
- try {
676
- say("begin");
677
- await client.query("begin");
678
- const result = await run({
679
- execute: (statement) => sent((text, params) => client.query(text, params), statement)
680
- });
681
- say("commit");
682
- await client.query("commit");
683
- return result;
684
- } catch (error) {
685
- say("rollback");
686
- await client.query("rollback").catch(() => void 0);
687
- throw error;
688
- } finally {
689
- client.release();
690
- }
1563
+ const overThePort = {
1564
+ execute: (statement) => sent((text, params) => pool.query(text, params), statement),
1565
+ transaction: async (run) => {
1566
+ const client = await pool.connect();
1567
+ try {
1568
+ say("begin");
1569
+ await client.query("begin");
1570
+ const result = await run({
1571
+ execute: (statement) => sent((text, params) => client.query(text, params), statement)
1572
+ });
1573
+ say("commit");
1574
+ await client.query("commit");
1575
+ return result;
1576
+ } catch (error) {
1577
+ say("rollback");
1578
+ await client.query("rollback").catch(() => void 0);
1579
+ throw error;
1580
+ } finally {
1581
+ client.release();
691
1582
  }
692
1583
  }
693
1584
  };
1585
+ return { close: () => pool.end(), connection: overThePort, pool };
1586
+ };
1587
+ const open = (connectionString, opened = {}) => {
1588
+ const held = openPort(connectionString, opened);
1589
+ return options.drizzle === void 0 ? held : { close: held.close, connection: options.drizzle(held.pool, opened) };
694
1590
  };
695
1591
  return {
696
1592
  /**
@@ -699,21 +1595,21 @@ var nodePostgresDriver = async (options = {}) => {
699
1595
  * of their own passes their own `Driver` and this is never called.
700
1596
  */
701
1597
  migrate: async (connectionString, migrationsFolder) => {
702
- const held = open(connectionString, { poolSize: 1 });
1598
+ const held = openPort(connectionString, { poolSize: 1 });
703
1599
  try {
704
1600
  await held.connection.execute({
705
1601
  text: `create table if not exists ${ledger} (name text primary key, applied_at timestamptz not null default now())`
706
1602
  });
707
1603
  const files = (await (0, import_promises.readdir)(migrationsFolder)).filter((name) => name.endsWith(".sql")).toSorted();
708
1604
  for (const name of files) {
709
- const sql = await (0, import_promises.readFile)((0, import_node_path.join)(migrationsFolder, name), "utf8");
1605
+ const sql2 = await (0, import_promises.readFile)((0, import_node_path.join)(migrationsFolder, name), "utf8");
710
1606
  await held.connection.transaction(async (tx) => {
711
1607
  const applied = await tx.execute({
712
1608
  params: [name],
713
1609
  text: `select name from ${ledger} where name = $1`
714
1610
  });
715
1611
  if (applied.rows.length > 0) return;
716
- await tx.execute({ text: sql });
1612
+ await tx.execute({ text: sql2 });
717
1613
  await tx.execute({ params: [name], text: `insert into ${ledger} (name) values ($1)` });
718
1614
  });
719
1615
  }
@@ -723,139 +1619,37 @@ var nodePostgresDriver = async (options = {}) => {
723
1619
  },
724
1620
  open
725
1621
  };
726
- };
727
-
728
- // src/settings.ts
729
- var DEFAULT_OPS_VALUE = "on";
730
- var CUSTOM_SETTING = /^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*$/;
731
- var readSettings = (settings) => {
732
- for (const named of ["opsSetting", "tenantSetting"]) {
733
- const name = settings[named];
734
- if (typeof name !== "string" || name === "") {
735
- throw new DbRefusal(
736
- `${named} is required and has no default: the name of a session setting is the one thing this package cannot pick for you, because the policies and the seam have to agree on it exactly`
737
- );
738
- }
739
- if (!CUSTOM_SETTING.test(name)) {
740
- throw new DbRefusal(
741
- `${named} is "${name}", and Postgres reads a setting an application defines as \`prefix.name\` \u2014 anything else is rejected as unrecognised the first time a session sets it`
742
- );
743
- }
744
- }
745
- return { ...settings, opsValue: settings.opsValue ?? DEFAULT_OPS_VALUE };
746
- };
747
-
748
- // src/policies.ts
749
- var settingRead = (name) => `(select current_setting(${literal(name)}, true))`;
750
- var both = (predicate) => `using (${predicate}) with check (${predicate})`;
751
- var DEFAULT_NAMES = {
752
- freeze: "not_frozen",
753
- isolation: "tenant_isolation",
754
- opsMaintenance: "ops_maintenance"
755
- };
756
- var tenantPolicies = (table, options) => {
757
- const settings = readSettings(options.settings);
758
- const names = { ...DEFAULT_NAMES, ...options.names };
759
- const on = qualified(table, options.schema);
760
- const tenant = settingRead(settings.tenantSetting);
761
- const opsIsOn = `${settingRead(settings.opsSetting)} = ${literal(settings.opsValue)}`;
762
- const policy = (name, as, predicate) => [
763
- `drop policy if exists ${quoted(name)} on ${on}`,
764
- `create policy ${quoted(name)} on ${on} as ${as} for all ${both(predicate)}`
765
- ];
766
- const frozen = options.freeze;
767
- const notFrozen = frozen === void 0 ? [] : policy(
768
- names.freeze,
769
- "restrictive",
770
- `${opsIsOn} or not exists (select 1 from ${quoted(frozen.table)} where ${quoted(frozen.tenantColumn)} = ${tenant})`
771
- );
772
- return [
773
- `alter table ${on} enable row level security`,
774
- `alter table ${on} force row level security`,
775
- ...policy(names.isolation, "permissive", `${quoted(options.tenantColumn)} = ${tenant}`),
776
- ...policy(names.opsMaintenance, "permissive", opsIsOn),
777
- ...notFrozen
778
- ];
779
- };
780
-
781
- // src/session.ts
782
- var OPS = /* @__PURE__ */ Symbol("the ops lever");
783
- var scopeOf = /* @__PURE__ */ new WeakMap();
784
- var describe = (scope) => scope === OPS ? "the ops lever" : `tenant ${JSON.stringify(scope)}`;
785
- var canOpen = (executor) => typeof executor.transaction === "function";
786
- var DEFAULT_TENANT_KEY = "tenantId";
787
- var createSessionSeam = (config) => {
788
- const settings = readSettings(config.settings);
789
- const tenantKey = config.tenantKey ?? DEFAULT_TENANT_KEY;
790
- if (tenantKey === "") {
791
- throw new DbRefusal(
792
- "tenantKey is empty, and it is the property the seam reads a query\u2019s tenant out of: name the key your queries carry, or leave it out for `tenantId`"
793
- );
794
- }
795
- const contextOf = (scope) => {
796
- if (scope !== OPS) {
797
- return { params: [settings.tenantSetting, scope], text: "select set_config($1, $2, true)" };
798
- }
799
- return config.opsStatementTimeout === void 0 ? {
800
- params: [settings.opsSetting, settings.opsValue],
801
- text: "select set_config($1, $2, true)"
802
- } : {
803
- params: [settings.opsSetting, settings.opsValue, config.opsStatementTimeout],
804
- text: "select set_config($1, $2, true), set_config('statement_timeout', $3, true)"
805
- };
806
- };
807
- const opened = async (executor, scope, run) => {
808
- const current = scopeOf.get(executor);
809
- if (current === scope) return run(executor);
810
- if (current !== void 0) {
811
- throw new DbRefusal(
812
- `this transaction is open for ${describe(current)} and was asked for ${describe(scope)}: a transaction names one scope, and the second query would run behind the first one's policy`
813
- );
814
- }
815
- if (!canOpen(executor)) {
816
- throw new DbRefusal(
817
- `this executor is in no session and cannot open a transaction, so ${describe(scope)} has nothing to name \u2014 pass the connection, or run it inside a session already open for that scope`
818
- );
819
- }
820
- return executor.transaction(async (tx) => {
821
- await tx.execute(contextOf(scope));
822
- scopeOf.set(tx, scope);
823
- return run(tx);
824
- });
825
- };
826
- const named = (params) => {
827
- const carried = params[tenantKey];
828
- if (typeof carried !== "string" || carried === "") {
829
- throw new DbRefusal(
830
- `this query is scoped by "${tenantKey}" and its parameters carry no such value: the session would name no tenant at all, which is not a refusal but a query running outside every policy. Pass the tenant under "${tenantKey}", or build the seam with the key your own queries carry`
831
- );
832
- }
833
- return carried;
834
- };
835
- return {
836
- inOps: (executor, run) => opened(executor, OPS, run),
837
- inTenant: (executor, tenantId, run) => opened(executor, tenantId, run),
838
- scoped: (query) => async (executor, params) => opened(executor, named(params), (tx) => query(tx, params)),
839
- scopedAsOps: (query) => (executor, params) => opened(executor, OPS, (tx) => query(tx, params))
840
- };
841
- };
1622
+ });
842
1623
  // Annotate the CommonJS export names for ESM import in node:
843
1624
  0 && (module.exports = {
1625
+ DEFAULT_ENTITY_PAGE,
844
1626
  DEFAULT_OPS_VALUE,
845
1627
  DEFAULT_TENANT_KEY,
846
1628
  DbRefusal,
1629
+ ENTITY_PLACEHOLDERS,
1630
+ MAX_ENTITY_PAGE,
847
1631
  appConnectionString,
848
1632
  appRoleStatements,
1633
+ asStatement,
849
1634
  concurrentTenantsConformance,
850
1635
  connectionsConformance,
851
1636
  createConnections,
852
1637
  createSessionSeam,
1638
+ defineEntity,
1639
+ drizzleSession,
1640
+ drizzleTenantPolicies,
1641
+ entityConformance,
1642
+ entityMigrations,
1643
+ forceRowLevelSecurity,
853
1644
  forcedRowLevelSecurityConformance,
1645
+ fragment,
854
1646
  nodePostgresDriver,
855
1647
  perStepConnection,
856
1648
  scopedQueryConformance,
857
1649
  sessionConformance,
858
1650
  statementCensusConformance,
859
1651
  tenantIsolationConformance,
860
- tenantPolicies
1652
+ tenantPolicies,
1653
+ tenantPolicySet,
1654
+ undosOf
861
1655
  });