@geonosis/db 0.2.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,20 +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,
35
+ DEFAULT_TENANT_KEY: () => DEFAULT_TENANT_KEY,
34
36
  DbRefusal: () => DbRefusal,
37
+ ENTITY_PLACEHOLDERS: () => ENTITY_PLACEHOLDERS,
38
+ MAX_ENTITY_PAGE: () => MAX_ENTITY_PAGE,
35
39
  appConnectionString: () => appConnectionString,
36
40
  appRoleStatements: () => appRoleStatements,
41
+ asStatement: () => asStatement,
37
42
  concurrentTenantsConformance: () => concurrentTenantsConformance,
43
+ connectionsConformance: () => connectionsConformance,
38
44
  createConnections: () => createConnections,
39
45
  createSessionSeam: () => createSessionSeam,
46
+ defineEntity: () => defineEntity,
47
+ drizzleSession: () => drizzleSession,
48
+ drizzleTenantPolicies: () => drizzleTenantPolicies,
49
+ entityConformance: () => entityConformance,
50
+ entityMigrations: () => entityMigrations,
51
+ forceRowLevelSecurity: () => forceRowLevelSecurity,
40
52
  forcedRowLevelSecurityConformance: () => forcedRowLevelSecurityConformance,
53
+ fragment: () => fragment,
41
54
  nodePostgresDriver: () => nodePostgresDriver,
42
55
  perStepConnection: () => perStepConnection,
56
+ scopedQueryConformance: () => scopedQueryConformance,
43
57
  sessionConformance: () => sessionConformance,
44
58
  statementCensusConformance: () => statementCensusConformance,
45
59
  tenantIsolationConformance: () => tenantIsolationConformance,
46
- tenantPolicies: () => tenantPolicies
60
+ tenantPolicies: () => tenantPolicies,
61
+ tenantPolicySet: () => tenantPolicySet,
62
+ undosOf: () => undosOf
47
63
  });
48
64
  module.exports = __toCommonJS(index_exports);
49
65
 
@@ -61,7 +77,7 @@ var DbRefusal = class extends Error {
61
77
 
62
78
  // src/identifiers.ts
63
79
  var quoted = (name) => `"${name.replaceAll('"', '""')}"`;
64
- var literal = (value) => `'${value.replaceAll("'", "''")}'`;
80
+ var literal = (value2) => `'${value2.replaceAll("'", "''")}'`;
65
81
  var qualified = (table, schema) => schema === void 0 ? quoted(table) : `${quoted(schema)}.${quoted(table)}`;
66
82
 
67
83
  // src/app-role.ts
@@ -106,6 +122,123 @@ var appConnectionString = (ownerConnectionString, credentials) => {
106
122
 
107
123
  // src/conformance.ts
108
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)));
109
242
  var A = "tenant_conformance_a";
110
243
  var B = "tenant_conformance_b";
111
244
  var CONCURRENT = [
@@ -122,34 +255,45 @@ var probeSql = (probe) => {
122
255
  const on = qualified(probe.name, probe.schema);
123
256
  const tenant = quoted(probe.tenantColumn);
124
257
  return {
125
- insert: (tenantId, id) => ({
126
- params: [tenantId, id],
127
- text: `insert into ${on} (${tenant}, ${quoted(probe.idColumn)}) values ($1, $2)`
128
- }),
129
- 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
+ ),
130
271
  tenantsOf: (answer) => (0, import_conformance.rowsOf)(answer).map((row) => row.tenant).toSorted()
131
272
  };
132
273
  };
133
274
  var written = async (subject, rows) => {
134
275
  const probe = probeSql(subject.probe);
276
+ const ask = asking(subject);
135
277
  for (const [tenantId, id] of rows) {
136
278
  await subject.seam.inTenant(
137
279
  subject.connection,
138
280
  tenantId,
139
- (tx) => tx.execute(probe.insert(tenantId, id))
281
+ (tx) => ask(tx, probe.insert(tenantId, id))
140
282
  );
141
283
  }
142
284
  };
143
285
  var tenantIsolationConformance = (subject) => {
286
+ const ask = asking(subject);
144
287
  const probe = probeSql(subject.probe);
145
288
  return [
146
289
  {
147
290
  name: "the role the session connects as is no superuser",
148
291
  run: async () => {
149
292
  const [row] = (0, import_conformance.rowsOf)(
150
- await subject.connection.execute({
151
- text: `select current_setting('is_superuser') as super`
152
- })
293
+ await ask(
294
+ subject.connection,
295
+ fragment([`select current_setting('is_superuser') as super`])
296
+ )
153
297
  );
154
298
  (0, import_conformance.assertIs)(
155
299
  row?.super,
@@ -169,7 +313,7 @@ var tenantIsolationConformance = (subject) => {
169
313
  const seen = await subject.seam.inTenant(
170
314
  subject.connection,
171
315
  A,
172
- (tx) => tx.execute(probe.selectAll)
316
+ (tx) => ask(tx, probe.selectAll)
173
317
  );
174
318
  (0, import_conformance.assertSame)(
175
319
  probe.tenantsOf(seen),
@@ -187,7 +331,7 @@ var tenantIsolationConformance = (subject) => {
187
331
  [B, "row_b"]
188
332
  ]);
189
333
  (0, import_conformance.assertSame)(
190
- probe.tenantsOf(await subject.connection.execute(probe.selectAll)),
334
+ probe.tenantsOf(await ask(subject.connection, probe.selectAll)),
191
335
  [],
192
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"
193
337
  );
@@ -201,19 +345,40 @@ var tenantIsolationConformance = (subject) => {
201
345
  () => subject.seam.inTenant(
202
346
  subject.connection,
203
347
  B,
204
- (tx) => tx.execute(probe.insert(A, "row_forged"))
348
+ (tx) => ask(tx, probe.insert(A, "row_forged"))
205
349
  ),
206
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"
207
351
  );
208
352
  (0, import_conformance.assertSame)(
209
353
  probe.tenantsOf(
210
- await subject.seam.inOps(subject.connection, (tx) => tx.execute(probe.selectAll))
354
+ await subject.seam.inOps(subject.connection, (tx) => ask(tx, probe.selectAll))
211
355
  ),
212
356
  [],
213
357
  "the forged write was reported as refused and the row is there anyway"
214
358
  );
215
359
  }
216
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
+ },
217
382
  {
218
383
  name: "the ops lever sees every tenant",
219
384
  run: async () => {
@@ -224,7 +389,7 @@ var tenantIsolationConformance = (subject) => {
224
389
  ]);
225
390
  (0, import_conformance.assertSame)(
226
391
  probe.tenantsOf(
227
- await subject.seam.inOps(subject.connection, (tx) => tx.execute(probe.selectAll))
392
+ await subject.seam.inOps(subject.connection, (tx) => ask(tx, probe.selectAll))
228
393
  ),
229
394
  [A, B].toSorted(),
230
395
  "maintenance could not see across tenants, so every sweep, backfill and repair has to be run once per tenant or not at all"
@@ -243,6 +408,7 @@ var recorderOf = async (subject) => {
243
408
  };
244
409
  var counted = (statements, pattern) => statements.filter((one) => pattern.test(one)).length;
245
410
  var statementCensusConformance = (subject) => {
411
+ const ask = asking(subject);
246
412
  const probe = probeSql(subject.probe);
247
413
  return [
248
414
  {
@@ -251,7 +417,7 @@ var statementCensusConformance = (subject) => {
251
417
  await subject.reset();
252
418
  const recorded = await recorderOf(subject);
253
419
  try {
254
- 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));
255
421
  } finally {
256
422
  await recorded.close();
257
423
  }
@@ -284,8 +450,8 @@ var statementCensusConformance = (subject) => {
284
450
  const recorded = await recorderOf(subject);
285
451
  try {
286
452
  await subject.seam.inTenant(recorded.connection, A, async (tx) => {
287
- await tx.execute(probe.selectAll);
288
- 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));
289
455
  });
290
456
  } finally {
291
457
  await recorded.close();
@@ -311,6 +477,7 @@ var statementCensusConformance = (subject) => {
311
477
  ];
312
478
  };
313
479
  var concurrentTenantsConformance = (subject) => {
480
+ const ask = asking(subject);
314
481
  const probe = probeSql(subject.probe);
315
482
  return [
316
483
  {
@@ -322,13 +489,13 @@ var concurrentTenantsConformance = (subject) => {
322
489
  (tenantId, index) => subject.seam.inTenant(
323
490
  subject.connection,
324
491
  tenantId,
325
- (tx) => tx.execute(probe.insert(tenantId, `row_${index}`))
492
+ (tx) => ask(tx, probe.insert(tenantId, `row_${index}`))
326
493
  )
327
494
  )
328
495
  );
329
496
  (0, import_conformance.assertSame)(
330
497
  probe.tenantsOf(
331
- await subject.seam.inOps(subject.connection, (tx) => tx.execute(probe.selectAll))
498
+ await subject.seam.inOps(subject.connection, (tx) => ask(tx, probe.selectAll))
332
499
  ),
333
500
  CONCURRENT.toSorted(),
334
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"
@@ -344,13 +511,13 @@ var concurrentTenantsConformance = (subject) => {
344
511
  (tenantId, index) => subject.seam.inTenant(
345
512
  subject.connection,
346
513
  tenantId,
347
- (tx) => tx.execute(probe.insert(tenantId, `row_${index}`))
514
+ (tx) => ask(tx, probe.insert(tenantId, `row_${index}`))
348
515
  )
349
516
  )
350
517
  );
351
518
  const seen = await Promise.all(
352
519
  CONCURRENT.map(
353
- (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)
354
521
  )
355
522
  );
356
523
  (0, import_conformance.assertSame)(
@@ -367,7 +534,7 @@ var concurrentTenantsConformance = (subject) => {
367
534
  () => subject.seam.inTenant(
368
535
  subject.connection,
369
536
  A,
370
- (tx) => subject.seam.inTenant(tx, B, (inner) => inner.execute(probe.selectAll))
537
+ (tx) => subject.seam.inTenant(tx, B, (inner) => ask(inner, probe.selectAll))
371
538
  ),
372
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",
373
540
  // #211/#213: the SEAM's own refusal, by class — a driver that crashed throws too.
@@ -381,7 +548,7 @@ var concurrentTenantsConformance = (subject) => {
381
548
  await (0, import_conformance.assertRefuses)(
382
549
  () => subject.seam.inOps(
383
550
  subject.connection,
384
- (tx) => subject.seam.inTenant(tx, A, (inner) => inner.execute(probe.selectAll))
551
+ (tx) => subject.seam.inTenant(tx, A, (inner) => ask(inner, probe.selectAll))
385
552
  ),
386
553
  "a tenant query reached from an ops sweep ran anyway \u2014 with no tenant named, behind the ops policy, which is every tenant",
387
554
  DbRefusal
@@ -390,16 +557,77 @@ var concurrentTenantsConformance = (subject) => {
390
557
  }
391
558
  ];
392
559
  };
560
+ var scopedOf = (subject) => {
561
+ const scoped = subject.seam.scoped;
562
+ if (typeof scoped !== "function") {
563
+ throw new import_conformance.ConformanceFailure(
564
+ 'this seam has no `scoped`, and "a query names its own tenant" is the claim that keeps every call site from naming one by hand: supply the wrapper \u2014 or this claim is untested rather than passing'
565
+ );
566
+ }
567
+ return scoped;
568
+ };
569
+ var scopedQueryConformance = (subject) => {
570
+ const ask = asking(subject);
571
+ const probe = probeSql(subject.probe);
572
+ const key = subject.seam.tenantKey ?? "tenantId";
573
+ const insert = () => scopedOf(subject)(
574
+ (tx, params) => ask(tx, probe.insert(params[key] ?? "", "row_scoped"))
575
+ );
576
+ return [
577
+ {
578
+ name: "a query scoped by its own parameters lands under the tenant they name",
579
+ run: async () => {
580
+ await subject.reset();
581
+ await insert()(subject.connection, { [key]: A });
582
+ (0, import_conformance.assertSame)(
583
+ probe.tenantsOf(
584
+ await subject.seam.inOps(subject.connection, (tx) => ask(tx, probe.selectAll))
585
+ ),
586
+ [A],
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`
588
+ );
589
+ }
590
+ },
591
+ {
592
+ name: "a scoped query for a second tenant inside a session is refused",
593
+ run: async () => {
594
+ await subject.reset();
595
+ await (0, import_conformance.assertRefuses)(
596
+ () => subject.seam.inTenant(subject.connection, B, (tx) => insert()(tx, { [key]: A })),
597
+ "a scoped query naming one tenant ran inside a session open for another: the wrapper opened nothing and the write went in behind the session it found",
598
+ DbRefusal
599
+ );
600
+ }
601
+ },
602
+ {
603
+ name: "a scoped query whose parameters carry no tenant is refused by that key\u2019s name",
604
+ run: async () => {
605
+ await subject.reset();
606
+ await (0, import_conformance.assertRefuses)(
607
+ () => insert()(subject.connection, {}),
608
+ `a query whose parameters carry no "${key}" was run anyway \u2014 with no tenant named at all, which is a query outside every session rather than a refusal`,
609
+ key
610
+ );
611
+ }
612
+ }
613
+ ];
614
+ };
393
615
  var forcedRowLevelSecurityConformance = (subject) => [
394
616
  {
395
617
  name: "every guarded table forces row level security",
396
618
  run: async () => {
397
619
  const tables = subject.guardedTables ?? [subject.probe.name];
398
620
  const rows = (0, import_conformance.rowsOf)(
399
- await subject.connection.execute({
400
- params: [tables],
401
- text: "select relname as name, relforcerowsecurity as forced from pg_class where relname = any($1)"
402
- })
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
+ )
403
631
  );
404
632
  (0, import_conformance.assertSame)(
405
633
  rows.map((row) => row.name).toSorted(),
@@ -414,17 +642,140 @@ var forcedRowLevelSecurityConformance = (subject) => [
414
642
  }
415
643
  }
416
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
+ };
654
+ var perStepOf = (subject) => {
655
+ const perStep = subject.perStep;
656
+ if (typeof perStep !== "function") {
657
+ throw new import_conformance.ConformanceFailure(
658
+ 'this subject has no `perStep`, and "a step\u2019s handle is not the next step\u2019s" is a claim about a durable run that hibernates between its steps: supply the per-step scope, named \u2014 or this claim is untested rather than passing'
659
+ );
660
+ }
661
+ return perStep;
662
+ };
663
+ var connectionsConformance = (subject) => {
664
+ const ask = asking(subject);
665
+ const probe = subject.probe ?? fragment(["select 1"]);
666
+ const CHARGE = "charge";
667
+ const SHIP = "ship";
668
+ return [
669
+ {
670
+ name: "a handle inside its invocation answers",
671
+ run: () => subject.connections.withConnection(async () => {
672
+ await ask(subject.connections.sessionDb(), probe);
673
+ })
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
+ },
701
+ {
702
+ name: "a handle asked for outside every invocation is refused, naming what to wrap it in",
703
+ run: () => (0, import_conformance.assertRefuses)(
704
+ () => Promise.resolve(subject.connections.sessionDb()),
705
+ "a handle was opened for a caller in no invocation, and nothing will ever close it: one leak per call, deferred to whoever reads the comment about it",
706
+ DbRefusal
707
+ )
708
+ },
709
+ {
710
+ name: "a handle used after its invocation ended is refused by name",
711
+ run: async () => {
712
+ let held;
713
+ await subject.connections.withConnection(async () => {
714
+ held = subject.connections.sessionDb();
715
+ await ask(held, probe);
716
+ });
717
+ await (0, import_conformance.assertRefuses)(
718
+ () => ask(held, probe),
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",
720
+ DbRefusal
721
+ );
722
+ }
723
+ },
724
+ {
725
+ name: "a step inside a run gets a handle of its own, because workerd refuses I/O on another request\u2019s socket",
726
+ run: () => subject.connections.withConnection(async () => {
727
+ const ofTheRun = subject.connections.sessionDb();
728
+ await perStepOf(subject)(CHARGE)(async () => {
729
+ const ofTheStep = subject.connections.sessionDb();
730
+ (0, import_conformance.assertThat)(
731
+ ofTheStep !== ofTheRun,
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"
733
+ );
734
+ await ask(ofTheStep, probe);
735
+ });
736
+ await ask(ofTheRun, probe);
737
+ })
738
+ },
739
+ {
740
+ name: "a handle from one step is refused inside another, named with the step it came from",
741
+ run: async () => {
742
+ const perStep = perStepOf(subject);
743
+ let charged;
744
+ await perStep(CHARGE)(async () => {
745
+ charged = subject.connections.sessionDb();
746
+ await ask(charged, probe);
747
+ });
748
+ await perStep(SHIP)(
749
+ () => (0, import_conformance.assertRefuses)(
750
+ () => ask(charged, probe),
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`,
752
+ CHARGE
753
+ )
754
+ );
755
+ }
756
+ }
757
+ ];
758
+ };
417
759
  var sessionConformance = (subject) => [
418
760
  ...tenantIsolationConformance(subject),
419
761
  ...statementCensusConformance(subject),
420
762
  ...concurrentTenantsConformance(subject),
763
+ ...scopedQueryConformance(subject),
421
764
  ...forcedRowLevelSecurityConformance(subject)
422
765
  ];
423
766
 
424
767
  // src/connections.ts
425
768
  var import_node_async_hooks = require("async_hooks");
426
769
  var invocation = new import_node_async_hooks.AsyncLocalStorage();
770
+ var whose = (frame) => frame.named === void 0 ? "an invocation" : `the "${frame.named}" unit of work`;
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
+ );
776
+ };
427
777
  var createConnections = (config) => {
778
+ const guard = config.over?.guard ?? executorHandles.guard;
428
779
  const opened = () => config.open === void 0 ? config.driver.open(config.connectionString) : config.driver.open(config.connectionString, config.open);
429
780
  return {
430
781
  inInvocation: () => invocation.getStore() !== void 0,
@@ -436,27 +787,738 @@ var createConnections = (config) => {
436
787
  "sessionDb() was called outside withConnection(), where a handle opened for it would be closed by nobody. Wrap the invocation \u2014 withConnection(() => \u2026, (closing) => ctx.waitUntil(closing)) \u2014 or call open() and close what you were given."
437
788
  );
438
789
  }
439
- const known = frame.get(config.connectionString);
440
- if (known !== void 0) return known.connection;
441
- const held = opened();
442
- frame.set(config.connectionString, held);
443
- return held.connection;
790
+ const known = frame.held.get(config.connectionString);
791
+ if (known !== void 0) return known.guarded;
792
+ const open = opened();
793
+ const held = {
794
+ guarded: guard(open.connection, stillOpenIn(frame)),
795
+ open
796
+ };
797
+ frame.held.set(config.connectionString, held);
798
+ return held.guarded;
444
799
  },
445
- withConnection: async (run, release = (closing) => void closing) => {
446
- if (invocation.getStore() !== void 0) return run();
447
- const held = /* @__PURE__ */ new Map();
800
+ withConnection: async (run, release = (closing) => void closing, options) => {
801
+ if (options?.own !== true && invocation.getStore() !== void 0) return run();
802
+ const frame = {
803
+ held: /* @__PURE__ */ new Map(),
804
+ live: true,
805
+ ...options?.named === void 0 ? {} : { named: options.named }
806
+ };
448
807
  try {
449
- return await invocation.run(held, run);
808
+ return await invocation.run(frame, run);
450
809
  } finally {
451
- release(Promise.all([...held.values()].map((one) => one.close())).then(() => void 0));
810
+ frame.live = false;
811
+ release(
812
+ Promise.all([...frame.held.values()].map((one) => one.open.close())).then(
813
+ () => void 0
814
+ )
815
+ );
452
816
  }
453
817
  }
454
818
  };
455
819
  };
456
820
  var perStepConnection = (connections) => ({
457
- perStep: () => (body) => connections.withConnection(body)
821
+ perStep: (named) => (body) => connections.withConnection(body, void 0, {
822
+ own: true,
823
+ ...named === void 0 ? {} : { named }
824
+ })
458
825
  });
459
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
+
460
1522
  // src/node-postgres.ts
461
1523
  var import_promises = require("fs/promises");
462
1524
  var import_node_path = require("path");
@@ -482,10 +1544,10 @@ var drivingPg = (module2) => {
482
1544
  };
483
1545
  var values = (statement) => statement.params === void 0 ? void 0 : [...statement.params];
484
1546
  var DEFAULT_MIGRATIONS_TABLE = "geonosis_db_migrations";
485
- var nodePostgresDriver = async (options = {}) => {
1547
+ var nodePostgresDriver = (async (options = {}) => {
486
1548
  const pg = drivingPg(options.pg ?? await loadPg());
487
1549
  const ledger = quoted(options.migrationsTable ?? DEFAULT_MIGRATIONS_TABLE);
488
- const open = (connectionString, opened = {}) => {
1550
+ const openPort = (connectionString, opened = {}) => {
489
1551
  const say = opened.onStatement ?? (() => {
490
1552
  });
491
1553
  const pool = new pg.Pool({
@@ -498,31 +1560,33 @@ var nodePostgresDriver = async (options = {}) => {
498
1560
  say(statement.text);
499
1561
  return run(statement.text, values(statement));
500
1562
  };
501
- return {
502
- close: () => pool.end(),
503
- connection: {
504
- execute: (statement) => sent((text, params) => pool.query(text, params), statement),
505
- transaction: async (run) => {
506
- const client = await pool.connect();
507
- try {
508
- say("begin");
509
- await client.query("begin");
510
- const result = await run({
511
- execute: (statement) => sent((text, params) => client.query(text, params), statement)
512
- });
513
- say("commit");
514
- await client.query("commit");
515
- return result;
516
- } catch (error) {
517
- say("rollback");
518
- await client.query("rollback").catch(() => void 0);
519
- throw error;
520
- } finally {
521
- client.release();
522
- }
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();
523
1582
  }
524
1583
  }
525
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) };
526
1590
  };
527
1591
  return {
528
1592
  /**
@@ -531,21 +1595,21 @@ var nodePostgresDriver = async (options = {}) => {
531
1595
  * of their own passes their own `Driver` and this is never called.
532
1596
  */
533
1597
  migrate: async (connectionString, migrationsFolder) => {
534
- const held = open(connectionString, { poolSize: 1 });
1598
+ const held = openPort(connectionString, { poolSize: 1 });
535
1599
  try {
536
1600
  await held.connection.execute({
537
1601
  text: `create table if not exists ${ledger} (name text primary key, applied_at timestamptz not null default now())`
538
1602
  });
539
1603
  const files = (await (0, import_promises.readdir)(migrationsFolder)).filter((name) => name.endsWith(".sql")).toSorted();
540
1604
  for (const name of files) {
541
- 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");
542
1606
  await held.connection.transaction(async (tx) => {
543
1607
  const applied = await tx.execute({
544
1608
  params: [name],
545
1609
  text: `select name from ${ledger} where name = $1`
546
1610
  });
547
1611
  if (applied.rows.length > 0) return;
548
- await tx.execute({ text: sql });
1612
+ await tx.execute({ text: sql2 });
549
1613
  await tx.execute({ params: [name], text: `insert into ${ledger} (name) values ($1)` });
550
1614
  });
551
1615
  }
@@ -555,120 +1619,37 @@ var nodePostgresDriver = async (options = {}) => {
555
1619
  },
556
1620
  open
557
1621
  };
558
- };
559
-
560
- // src/settings.ts
561
- var DEFAULT_OPS_VALUE = "on";
562
- var CUSTOM_SETTING = /^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*$/;
563
- var readSettings = (settings) => {
564
- for (const named of ["opsSetting", "tenantSetting"]) {
565
- const name = settings[named];
566
- if (typeof name !== "string" || name === "") {
567
- throw new DbRefusal(
568
- `${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`
569
- );
570
- }
571
- if (!CUSTOM_SETTING.test(name)) {
572
- throw new DbRefusal(
573
- `${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`
574
- );
575
- }
576
- }
577
- return { ...settings, opsValue: settings.opsValue ?? DEFAULT_OPS_VALUE };
578
- };
579
-
580
- // src/policies.ts
581
- var settingRead = (name) => `(select current_setting(${literal(name)}, true))`;
582
- var both = (predicate) => `using (${predicate}) with check (${predicate})`;
583
- var DEFAULT_NAMES = {
584
- freeze: "not_frozen",
585
- isolation: "tenant_isolation",
586
- opsMaintenance: "ops_maintenance"
587
- };
588
- var tenantPolicies = (table, options) => {
589
- const settings = readSettings(options.settings);
590
- const names = { ...DEFAULT_NAMES, ...options.names };
591
- const on = qualified(table, options.schema);
592
- const tenant = settingRead(settings.tenantSetting);
593
- const opsIsOn = `${settingRead(settings.opsSetting)} = ${literal(settings.opsValue)}`;
594
- const policy = (name, as, predicate) => [
595
- `drop policy if exists ${quoted(name)} on ${on}`,
596
- `create policy ${quoted(name)} on ${on} as ${as} for all ${both(predicate)}`
597
- ];
598
- const frozen = options.freeze;
599
- const notFrozen = frozen === void 0 ? [] : policy(
600
- names.freeze,
601
- "restrictive",
602
- `${opsIsOn} or not exists (select 1 from ${quoted(frozen.table)} where ${quoted(frozen.tenantColumn)} = ${tenant})`
603
- );
604
- return [
605
- `alter table ${on} enable row level security`,
606
- `alter table ${on} force row level security`,
607
- ...policy(names.isolation, "permissive", `${quoted(options.tenantColumn)} = ${tenant}`),
608
- ...policy(names.opsMaintenance, "permissive", opsIsOn),
609
- ...notFrozen
610
- ];
611
- };
612
-
613
- // src/session.ts
614
- var OPS = /* @__PURE__ */ Symbol("the ops lever");
615
- var scopeOf = /* @__PURE__ */ new WeakMap();
616
- var describe = (scope) => scope === OPS ? "the ops lever" : `tenant ${JSON.stringify(scope)}`;
617
- var canOpen = (executor) => typeof executor.transaction === "function";
618
- var createSessionSeam = (config) => {
619
- const settings = readSettings(config.settings);
620
- const contextOf = (scope) => {
621
- if (scope !== OPS) {
622
- return { params: [settings.tenantSetting, scope], text: "select set_config($1, $2, true)" };
623
- }
624
- return config.opsStatementTimeout === void 0 ? {
625
- params: [settings.opsSetting, settings.opsValue],
626
- text: "select set_config($1, $2, true)"
627
- } : {
628
- params: [settings.opsSetting, settings.opsValue, config.opsStatementTimeout],
629
- text: "select set_config($1, $2, true), set_config('statement_timeout', $3, true)"
630
- };
631
- };
632
- const opened = async (executor, scope, run) => {
633
- const current = scopeOf.get(executor);
634
- if (current === scope) return run(executor);
635
- if (current !== void 0) {
636
- throw new DbRefusal(
637
- `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`
638
- );
639
- }
640
- if (!canOpen(executor)) {
641
- throw new DbRefusal(
642
- `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`
643
- );
644
- }
645
- return executor.transaction(async (tx) => {
646
- await tx.execute(contextOf(scope));
647
- scopeOf.set(tx, scope);
648
- return run(tx);
649
- });
650
- };
651
- return {
652
- inOps: (executor, run) => opened(executor, OPS, run),
653
- inTenant: (executor, tenantId, run) => opened(executor, tenantId, run),
654
- scoped: (query) => (executor, params) => opened(executor, params.tenantId, (tx) => query(tx, params)),
655
- scopedAsOps: (query) => (executor, params) => opened(executor, OPS, (tx) => query(tx, params))
656
- };
657
- };
1622
+ });
658
1623
  // Annotate the CommonJS export names for ESM import in node:
659
1624
  0 && (module.exports = {
1625
+ DEFAULT_ENTITY_PAGE,
660
1626
  DEFAULT_OPS_VALUE,
1627
+ DEFAULT_TENANT_KEY,
661
1628
  DbRefusal,
1629
+ ENTITY_PLACEHOLDERS,
1630
+ MAX_ENTITY_PAGE,
662
1631
  appConnectionString,
663
1632
  appRoleStatements,
1633
+ asStatement,
664
1634
  concurrentTenantsConformance,
1635
+ connectionsConformance,
665
1636
  createConnections,
666
1637
  createSessionSeam,
1638
+ defineEntity,
1639
+ drizzleSession,
1640
+ drizzleTenantPolicies,
1641
+ entityConformance,
1642
+ entityMigrations,
1643
+ forceRowLevelSecurity,
667
1644
  forcedRowLevelSecurityConformance,
1645
+ fragment,
668
1646
  nodePostgresDriver,
669
1647
  perStepConnection,
1648
+ scopedQueryConformance,
670
1649
  sessionConformance,
671
1650
  statementCensusConformance,
672
1651
  tenantIsolationConformance,
673
- tenantPolicies
1652
+ tenantPolicies,
1653
+ tenantPolicySet,
1654
+ undosOf
674
1655
  });