@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/CHANGELOG.md +151 -0
- package/README.md +147 -10
- package/dist/index.cjs +998 -204
- package/dist/index.d.cts +438 -89
- package/dist/index.d.ts +438 -89
- package/dist/index.js +981 -200
- package/package.json +11 -3
package/dist/index.d.cts
CHANGED
|
@@ -54,25 +54,118 @@ type Connection = Executor & {
|
|
|
54
54
|
transaction: <Result>(run: (tx: Executor) => Promise<Result>) => Promise<Result>;
|
|
55
55
|
};
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* The two session settings a tenant wall is built on, spelled once at the composition root.
|
|
59
|
+
*
|
|
60
|
+
* Neither has a default. A default here would be one repo's name — `app.org_id` at the repo this
|
|
61
|
+
* came from — compiled into every other repo's policies, and the seam and the policies must agree
|
|
62
|
+
* on it exactly or the wall is open while every test still passes.
|
|
63
|
+
*/
|
|
64
|
+
type SessionSettings = {
|
|
65
|
+
/** What maintenance sets to see every tenant, through a lever no request path can reach. */
|
|
66
|
+
opsSetting: string;
|
|
67
|
+
/** What the ops setting holds while the lever is pulled. */
|
|
68
|
+
opsValue?: string;
|
|
69
|
+
/** What a transaction sets to name the tenant it is open for, and what the policies read. */
|
|
70
|
+
tenantSetting: string;
|
|
71
|
+
};
|
|
72
|
+
declare const DEFAULT_OPS_VALUE = "on";
|
|
73
|
+
|
|
74
|
+
/** A query that takes its session as an argument: what the seam wraps and what a provider writes. */
|
|
75
|
+
type Query<Params, Result, Handle = Executor> = (handle: Handle, params: Params) => Promise<Result>;
|
|
76
|
+
/**
|
|
77
|
+
* A statement as its parts: the SQL between the values, and the values themselves.
|
|
78
|
+
*
|
|
79
|
+
* The shape of a template literal — one more chunk than there are values — because that is what
|
|
80
|
+
* every library that places values already speaks. A driver renders it to numbered placeholders; an
|
|
81
|
+
* ORM hands it to its own tag. Nothing writes text for something else to read back out.
|
|
82
|
+
*/
|
|
83
|
+
type SqlFragment = {
|
|
84
|
+
chunks: readonly string[];
|
|
85
|
+
values: readonly unknown[];
|
|
86
|
+
};
|
|
87
|
+
/** A fragment from the SQL around one value, or from SQL alone. */
|
|
88
|
+
declare const fragment: (chunks: readonly string[], values?: readonly unknown[]) => SqlFragment;
|
|
89
|
+
/** A fragment as the executor port's statement: the values numbered where they stood. */
|
|
90
|
+
declare const asStatement: (piece: SqlFragment) => Statement;
|
|
91
|
+
/**
|
|
92
|
+
* How a session is opened and named on ONE kind of handle.
|
|
93
|
+
*
|
|
94
|
+
* The seam's own vocabulary is scopes and settings, never SQL text: a repo whose queries are an
|
|
95
|
+
* ORM's builders hands in the builders' way of saying `set_config`, and nothing in the middle
|
|
96
|
+
* writes a statement for something else to parse back.
|
|
97
|
+
*/
|
|
98
|
+
type SessionHandles<Handle> = {
|
|
99
|
+
/**
|
|
100
|
+
* Hold a handle to the lifetime of the invocation that opened it. `stillOpen` throws when that
|
|
101
|
+
* invocation has ended; a handle keeper calls this, and a seam never does.
|
|
102
|
+
*/
|
|
103
|
+
guard?: (handle: Handle, stillOpen: () => void) => Handle;
|
|
104
|
+
/** Whether this handle can open a transaction, or is one already. */
|
|
105
|
+
opens: (handle: Handle) => boolean;
|
|
106
|
+
/** Send a statement to this handle, values and all, the way this kind of handle takes one. */
|
|
107
|
+
send: (handle: Handle, statement: SqlFragment) => Promise<QueryAnswer>;
|
|
108
|
+
transaction: <Result>(handle: Handle, run: (tx: Handle) => Promise<Result>) => Promise<Result>;
|
|
109
|
+
};
|
|
110
|
+
declare const DEFAULT_TENANT_KEY = "tenantId";
|
|
111
|
+
type SessionSeam<Key extends string = typeof DEFAULT_TENANT_KEY, Handle = Executor> = {
|
|
112
|
+
inOps: <Result>(handle: Handle, run: (tx: Handle) => Promise<Result>) => Promise<Result>;
|
|
113
|
+
inTenant: <Result>(handle: Handle, tenantId: string, run: (tx: Handle) => Promise<Result>) => Promise<Result>;
|
|
114
|
+
scoped: <Params extends Readonly<Record<Key, string>>, Result>(query: Query<Params, Result, Handle>) => Query<Params, Result, Handle>;
|
|
115
|
+
scopedAsOps: <Params, Result>(query: Query<Params, Result, Handle>) => Query<Params, Result, Handle>;
|
|
116
|
+
/** Send a statement to a handle of this seam's kind — what a generated query is written over. */
|
|
117
|
+
send: (handle: Handle, statement: SqlFragment) => Promise<QueryAnswer>;
|
|
118
|
+
/** The key `scoped` reads a query's tenant out of, for whatever is generated over this seam. */
|
|
119
|
+
tenantKey: Key;
|
|
120
|
+
};
|
|
121
|
+
type SessionSeamConfig<Key extends string = typeof DEFAULT_TENANT_KEY, Handle = Executor> = {
|
|
122
|
+
/**
|
|
123
|
+
* How long a statement may run under the ops lever, as Postgres reads it. No default: a sweep's
|
|
124
|
+
* budget is a fact about one deployment's data, and the value that fits belongs to whoever runs
|
|
125
|
+
* it. Absent, the connection's own timeout stands.
|
|
126
|
+
*/
|
|
127
|
+
opsStatementTimeout?: string;
|
|
128
|
+
/** The kind of handle this seam opens sessions on. The executor port when nothing is named. */
|
|
129
|
+
over?: SessionHandles<Handle>;
|
|
130
|
+
settings: SessionSettings;
|
|
131
|
+
/**
|
|
132
|
+
* The key a query's own parameters carry its tenant under (#245). A repo whose queries say
|
|
133
|
+
* `{ orgId }` names it here instead of wrapping every call site in `inTenant` by hand.
|
|
134
|
+
*
|
|
135
|
+
* A key name rather than a selector, measured: under `tenantOf: (params) => string` nothing ties
|
|
136
|
+
* the selector's shape to the query's, and a query whose parameters carry no tenant at all
|
|
137
|
+
* compiles clean — which is the one thing `scoped` is here to make impossible.
|
|
138
|
+
*/
|
|
139
|
+
tenantKey?: Key;
|
|
140
|
+
};
|
|
141
|
+
/**
|
|
142
|
+
* The seam every query crosses: one transaction, the scope named as its first statement.
|
|
143
|
+
*
|
|
144
|
+
* `set_config(..., true)` is local to the transaction, which is what makes this safe on a pool —
|
|
145
|
+
* the name is gone when the transaction ends, and a connection handed to the next request carries
|
|
146
|
+
* nothing. Naming it second would leave the statements before it running under no tenant at all.
|
|
147
|
+
*/
|
|
148
|
+
declare const createSessionSeam: <Key extends string = typeof DEFAULT_TENANT_KEY, Handle = Executor>(config: SessionSeamConfig<Key, Handle>) => SessionSeam<Key, Handle>;
|
|
149
|
+
|
|
57
150
|
type ConformanceCase = {
|
|
58
151
|
name: string;
|
|
59
152
|
run: () => Promise<void>;
|
|
60
153
|
};
|
|
61
154
|
/** A handle that reports every statement it sends, including the ones a transaction is made of. */
|
|
62
|
-
type RecordingConnection = {
|
|
155
|
+
type RecordingConnection<Handle = Executor> = {
|
|
63
156
|
close: () => Promise<void>;
|
|
64
|
-
connection:
|
|
157
|
+
connection: Handle;
|
|
65
158
|
statements: string[];
|
|
66
159
|
};
|
|
67
160
|
/** As much of a session seam as the exam asks about. */
|
|
68
|
-
type SeamUnderTest = {
|
|
69
|
-
inOps: <Result>(
|
|
70
|
-
inTenant: <Result>(
|
|
161
|
+
type SeamUnderTest<Handle = Executor> = {
|
|
162
|
+
inOps: <Result>(handle: Handle, run: (tx: Handle) => Promise<Result>) => Promise<Result>;
|
|
163
|
+
inTenant: <Result>(handle: Handle, tenantId: string, run: (tx: Handle) => Promise<Result>) => Promise<Result>;
|
|
71
164
|
/**
|
|
72
165
|
* The wrapper that reads the tenant out of a query's own parameters. Spelled here rather than
|
|
73
166
|
* imported: what the exam asks about is the behaviour, and a consumer's seam is their own type.
|
|
74
167
|
*/
|
|
75
|
-
scoped?: <Params extends Readonly<Record<string, string>>, Result>(query: (
|
|
168
|
+
scoped?: <Params extends Readonly<Record<string, string>>, Result>(query: (handle: Handle, params: Params) => Promise<Result>) => (handle: Handle, params: Params) => Promise<Result>;
|
|
76
169
|
/** The key those parameters carry the tenant under. `tenantId` when the seam names none. */
|
|
77
170
|
tenantKey?: string;
|
|
78
171
|
};
|
|
@@ -88,16 +181,23 @@ type ProbeTable = {
|
|
|
88
181
|
schema?: string;
|
|
89
182
|
tenantColumn: string;
|
|
90
183
|
};
|
|
91
|
-
type SessionSubject = {
|
|
184
|
+
type SessionSubject<Handle = Executor> = {
|
|
92
185
|
/** A handle OUTSIDE every session: what the policies alone let through. */
|
|
93
|
-
connection:
|
|
186
|
+
connection: Handle;
|
|
94
187
|
/** The tables whose FORCE is checked. The probe table alone, when nothing else is named. */
|
|
95
188
|
guardedTables?: readonly string[];
|
|
96
189
|
probe: ProbeTable;
|
|
97
|
-
recording: () => Promise<RecordingConnection
|
|
190
|
+
recording: () => Promise<RecordingConnection<Handle>>;
|
|
98
191
|
/** Empty the probe table. Whatever role can do that — the exam's own rows are its business. */
|
|
99
192
|
reset: () => Promise<void>;
|
|
100
|
-
|
|
193
|
+
/**
|
|
194
|
+
* How the exam's own SQL reaches this handle: a `SessionHandles.send`, which every subject
|
|
195
|
+
* already has. The executor port's own when nothing is said. It takes the statement as its PARTS
|
|
196
|
+
* so a subject binds the exam's values the way its own library binds one — a subject that had to
|
|
197
|
+
* render them into text would be writing the concatenation this port's shape exists to prevent.
|
|
198
|
+
*/
|
|
199
|
+
run?: (handle: Handle, statement: SqlFragment) => Promise<QueryAnswer>;
|
|
200
|
+
seam: SeamUnderTest<Handle>;
|
|
101
201
|
};
|
|
102
202
|
/**
|
|
103
203
|
* The tenant wall, as the source proof took it: what a query with NO predicate can still see.
|
|
@@ -105,7 +205,7 @@ type SessionSubject = {
|
|
|
105
205
|
* Every read here is unqualified on purpose. A test that filters by tenant proves its own WHERE
|
|
106
206
|
* clause works; only an unfiltered one asks what the policy lets through.
|
|
107
207
|
*/
|
|
108
|
-
declare const tenantIsolationConformance: (subject: SessionSubject) => ConformanceCase[];
|
|
208
|
+
declare const tenantIsolationConformance: <Handle>(subject: SessionSubject<Handle>) => ConformanceCase[];
|
|
109
209
|
/**
|
|
110
210
|
* The census: a read costs what it says.
|
|
111
211
|
*
|
|
@@ -114,12 +214,12 @@ declare const tenantIsolationConformance: (subject: SessionSubject) => Conforman
|
|
|
114
214
|
* a second transaction nested inside the first. Twenty scoped reads against a pool of five is how
|
|
115
215
|
* the source repo found this one.
|
|
116
216
|
*/
|
|
117
|
-
declare const statementCensusConformance: (subject: SessionSubject) => ConformanceCase[];
|
|
217
|
+
declare const statementCensusConformance: <Handle>(subject: SessionSubject<Handle>) => ConformanceCase[];
|
|
118
218
|
/**
|
|
119
219
|
* Two tenants at once on one pool — the case a suite that runs one tenant at a time never reaches,
|
|
120
220
|
* and the one a pooled connection carrying a stale setting would fail.
|
|
121
221
|
*/
|
|
122
|
-
declare const concurrentTenantsConformance: (subject: SessionSubject) => ConformanceCase[];
|
|
222
|
+
declare const concurrentTenantsConformance: <Handle>(subject: SessionSubject<Handle>) => ConformanceCase[];
|
|
123
223
|
/**
|
|
124
224
|
* A query that names its own tenant, under the key the CONSUMER's queries carry it in (#245).
|
|
125
225
|
*
|
|
@@ -128,26 +228,32 @@ declare const concurrentTenantsConformance: (subject: SessionSubject) => Conform
|
|
|
128
228
|
* remove — and a wrapper that reads a key nobody carries names `undefined`, which is not a tenant
|
|
129
229
|
* and, before this, was not a refusal either.
|
|
130
230
|
*/
|
|
131
|
-
declare const scopedQueryConformance: (subject: SessionSubject) => ConformanceCase[];
|
|
231
|
+
declare const scopedQueryConformance: <Handle>(subject: SessionSubject<Handle>) => ConformanceCase[];
|
|
132
232
|
/**
|
|
133
233
|
* ENABLE alone exempts the table's OWNER, and the owner is the role that migrates and the role an
|
|
134
234
|
* ops sweep connects as. A wall only the application is behind is a wall with a door in it.
|
|
135
235
|
*/
|
|
136
|
-
declare const forcedRowLevelSecurityConformance: (subject: SessionSubject) => ConformanceCase[];
|
|
236
|
+
declare const forcedRowLevelSecurityConformance: <Handle>(subject: SessionSubject<Handle>) => ConformanceCase[];
|
|
137
237
|
/** As much of a handle keeper as the exam asks about. */
|
|
138
|
-
type ConnectionsUnderTest = {
|
|
139
|
-
|
|
238
|
+
type ConnectionsUnderTest<Handle = Connection> = {
|
|
239
|
+
open?: () => {
|
|
240
|
+
close: () => Promise<void>;
|
|
241
|
+
connection: Handle;
|
|
242
|
+
};
|
|
243
|
+
sessionDb: () => Handle;
|
|
140
244
|
withConnection: <Result>(run: () => Promise<Result>, release?: (closing: Promise<void>) => void) => Promise<Result>;
|
|
141
245
|
};
|
|
142
|
-
type ConnectionsSubject = {
|
|
143
|
-
connections: ConnectionsUnderTest
|
|
246
|
+
type ConnectionsSubject<Handle = Connection> = {
|
|
247
|
+
connections: ConnectionsUnderTest<Handle>;
|
|
144
248
|
/**
|
|
145
249
|
* A unit of work, by name. A durable step's handle is the one that outlives its step, and a
|
|
146
250
|
* refusal that cannot say WHICH step leaked sends a reader through every step in the workflow.
|
|
147
251
|
*/
|
|
148
252
|
perStep?: (named: string) => <Output>(body: () => Promise<Output>) => Promise<Output>;
|
|
149
253
|
/** Something any role may run. The exam asks a handle a question rather than assuming a table. */
|
|
150
|
-
probe?:
|
|
254
|
+
probe?: SqlFragment;
|
|
255
|
+
/** How the exam's own SQL reaches this handle. The executor port's own when nothing is said. */
|
|
256
|
+
run?: (handle: Handle, statement: SqlFragment) => Promise<QueryAnswer>;
|
|
151
257
|
};
|
|
152
258
|
/**
|
|
153
259
|
* The handle discipline: a connection belongs to the invocation that opened it and to no other.
|
|
@@ -157,9 +263,9 @@ type ConnectionsSubject = {
|
|
|
157
263
|
* some later line — "Cannot use a pool after calling end on the pool" — which reads as a bug in the
|
|
158
264
|
* query rather than as a handle used out of its lifetime.
|
|
159
265
|
*/
|
|
160
|
-
declare const connectionsConformance: (subject: ConnectionsSubject) => ConformanceCase[];
|
|
266
|
+
declare const connectionsConformance: <Handle>(subject: ConnectionsSubject<Handle>) => ConformanceCase[];
|
|
161
267
|
/** The whole exam: what a consumer runs against their own session before depending on it. */
|
|
162
|
-
declare const sessionConformance: (subject: SessionSubject) => ConformanceCase[];
|
|
268
|
+
declare const sessionConformance: <Handle>(subject: SessionSubject<Handle>) => ConformanceCase[];
|
|
163
269
|
|
|
164
270
|
/** What one invocation may be told about itself, for the refusal a handle that outlives it raises. */
|
|
165
271
|
type InvocationOptions = {
|
|
@@ -191,29 +297,40 @@ type OpenOptions = {
|
|
|
191
297
|
*/
|
|
192
298
|
poolSize?: number;
|
|
193
299
|
};
|
|
194
|
-
/**
|
|
195
|
-
|
|
300
|
+
/**
|
|
301
|
+
* A handle, and the way to give its connections back.
|
|
302
|
+
*
|
|
303
|
+
* The handle is a type parameter because a driver hands back what its consumer's queries are
|
|
304
|
+
* written against — the executor port, or the database an ORM opened — and a keeper that narrowed
|
|
305
|
+
* it would take that away again at the first `sessionDb()` (#266).
|
|
306
|
+
*/
|
|
307
|
+
type OpenConnection<Handle = Connection> = {
|
|
196
308
|
close: () => Promise<void>;
|
|
197
|
-
connection:
|
|
309
|
+
connection: Handle;
|
|
198
310
|
};
|
|
199
311
|
/** The one thing in a deployment that knows which package speaks the wire protocol. */
|
|
200
|
-
type Driver = {
|
|
312
|
+
type Driver<Handle = Connection> = {
|
|
201
313
|
migrate: (connectionString: string, migrationsFolder: string) => Promise<void>;
|
|
202
|
-
open: (connectionString: string, options?: OpenOptions) => OpenConnection
|
|
314
|
+
open: (connectionString: string, options?: OpenOptions) => OpenConnection<Handle>;
|
|
203
315
|
};
|
|
204
|
-
type Connections = {
|
|
316
|
+
type Connections<Handle = Connection> = {
|
|
205
317
|
/** Whether this call is running inside an invocation frame. */
|
|
206
318
|
inInvocation: () => boolean;
|
|
207
319
|
/** A handle outside every frame, for a caller who will close it themselves. */
|
|
208
|
-
open: () => OpenConnection
|
|
320
|
+
open: () => OpenConnection<Handle>;
|
|
209
321
|
/** The handle this invocation is using, opened on first ask. */
|
|
210
|
-
sessionDb: () =>
|
|
322
|
+
sessionDb: () => Handle;
|
|
211
323
|
withConnection: <Result>(run: () => Promise<Result>, release?: (closing: Promise<void>) => void, invocation?: InvocationOptions) => Promise<Result>;
|
|
212
324
|
};
|
|
213
|
-
type ConnectionsConfig = {
|
|
325
|
+
type ConnectionsConfig<Handle = Connection> = {
|
|
214
326
|
connectionString: string;
|
|
215
|
-
driver: Driver
|
|
327
|
+
driver: Driver<Handle>;
|
|
216
328
|
open?: OpenOptions;
|
|
329
|
+
/**
|
|
330
|
+
* The kind of handle this driver returns — the same `over` the seam was built with. Only its
|
|
331
|
+
* `guard` is read here; the executor port's own when nothing is named.
|
|
332
|
+
*/
|
|
333
|
+
over?: Pick<SessionHandles<Handle>, 'guard'>;
|
|
217
334
|
};
|
|
218
335
|
/**
|
|
219
336
|
* The handles of one database, and the discipline that closes them.
|
|
@@ -223,7 +340,7 @@ type ConnectionsConfig = {
|
|
|
223
340
|
* lifetime that matches, and the frame is what makes "this invocation" a thing the code can ask
|
|
224
341
|
* about rather than a thing the caller has to thread through.
|
|
225
342
|
*/
|
|
226
|
-
declare const createConnections: (config: ConnectionsConfig) => Connections
|
|
343
|
+
declare const createConnections: <Handle = Connection>(config: ConnectionsConfig<Handle>) => Connections<Handle>;
|
|
227
344
|
/**
|
|
228
345
|
* The `scope.perStep` hook a workflow engine asks for, answered with a connection per unit of work.
|
|
229
346
|
*
|
|
@@ -242,6 +359,233 @@ declare const perStepConnection: (connections: Pick<Connections, "withConnection
|
|
|
242
359
|
perStep: (named?: string) => <Output>(body: () => Promise<Output>) => Promise<Output>;
|
|
243
360
|
};
|
|
244
361
|
|
|
362
|
+
/**
|
|
363
|
+
* As much of a drizzle-orm Postgres database as this package uses, declared here rather than
|
|
364
|
+
* imported.
|
|
365
|
+
*
|
|
366
|
+
* drizzle-orm is an OPTIONAL peer, and a type imported from it would put it in this package's
|
|
367
|
+
* published `.d.ts` — a consumer who wanted the executor port and nothing else would then need it
|
|
368
|
+
* installed to typecheck. `tx: unknown` is what the real signature is assignable to: a transaction
|
|
369
|
+
* handle's type is the driver's own class, and a parameter narrower than `unknown` refuses it
|
|
370
|
+
* (measured against drizzle-orm 0.45.2).
|
|
371
|
+
*/
|
|
372
|
+
type DrizzleDatabase = {
|
|
373
|
+
execute: (query: never) => Promise<unknown>;
|
|
374
|
+
transaction: <Result>(run: (tx: unknown) => Promise<Result>) => Promise<Result>;
|
|
375
|
+
};
|
|
376
|
+
/**
|
|
377
|
+
* As much of drizzle-orm's `sql` as sending a statement takes.
|
|
378
|
+
*
|
|
379
|
+
* Not the TAG: the tag SPREADS an array value into a tuple — `any(($1, $2))` for one array, which
|
|
380
|
+
* is not the query anybody wrote — while `param` places whatever it is given as one parameter
|
|
381
|
+
* (measured against drizzle-orm 0.45.2). Method syntax because `join` types its chunks as drizzle's
|
|
382
|
+
* own mutable array of its own class, which no wider parameter is assignable to under strict
|
|
383
|
+
* variance.
|
|
384
|
+
*/
|
|
385
|
+
type DrizzleSql = {
|
|
386
|
+
join(chunks: unknown[], separator?: unknown): unknown;
|
|
387
|
+
param(value: unknown): unknown;
|
|
388
|
+
raw(text: string): unknown;
|
|
389
|
+
};
|
|
390
|
+
type DrizzleModuleLike = {
|
|
391
|
+
sql: DrizzleSql;
|
|
392
|
+
};
|
|
393
|
+
type DrizzleSessionOptions = {
|
|
394
|
+
/** drizzle-orm itself, for a runtime whose copy is patched. Loaded on demand when absent. */
|
|
395
|
+
drizzleOrm?: DrizzleModuleLike;
|
|
396
|
+
};
|
|
397
|
+
/**
|
|
398
|
+
* A session seam's handle port, over a drizzle database.
|
|
399
|
+
*
|
|
400
|
+
* `set_config` is built with drizzle's own `sql`, so the values are placed by the library that will
|
|
401
|
+
* send them and no statement is written for something else to parse back; and the transaction the
|
|
402
|
+
* database opens is handed to the query as it is, which is the whole point — a repo on drizzle
|
|
403
|
+
* writes its queries in builders, and a seam that hands back anything else is an adapter that repo
|
|
404
|
+
* has to write.
|
|
405
|
+
*/
|
|
406
|
+
declare const drizzleSession: <Db extends DrizzleDatabase>(options?: DrizzleSessionOptions) => Promise<SessionHandles<Db>>;
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* The columns every entity table has besides its own. Named because a repo's are named, defaulted
|
|
410
|
+
* because the DDL this package renders spells them this way.
|
|
411
|
+
*/
|
|
412
|
+
type EntityColumns = {
|
|
413
|
+
/** The body: one column, the whole of what the entity is. */
|
|
414
|
+
body?: string;
|
|
415
|
+
/** Epoch milliseconds of the row's first write (D-050). */
|
|
416
|
+
createdAt?: string;
|
|
417
|
+
/** Epoch milliseconds of its retirement, null while it is alive. */
|
|
418
|
+
retiredAt?: string;
|
|
419
|
+
tenant?: string;
|
|
420
|
+
updatedAt?: string;
|
|
421
|
+
};
|
|
422
|
+
/** A row as a read hands it back: the key it is addressed by, its body, and its clocks. */
|
|
423
|
+
type EntityRow<Body> = {
|
|
424
|
+
body: Body;
|
|
425
|
+
createdAt: number;
|
|
426
|
+
key: string;
|
|
427
|
+
updatedAt: number;
|
|
428
|
+
};
|
|
429
|
+
/** The listing index, in its own order. The last column is unique within one tenant. */
|
|
430
|
+
type EntityOrdering = readonly {
|
|
431
|
+
column: string;
|
|
432
|
+
direction?: 'asc' | 'desc';
|
|
433
|
+
}[];
|
|
434
|
+
/** A page, and where the next one starts. `nextCursor` is absent on the last page, never empty. */
|
|
435
|
+
type EntityPage<Item> = {
|
|
436
|
+
items: Item[];
|
|
437
|
+
nextCursor?: string;
|
|
438
|
+
};
|
|
439
|
+
declare const DEFAULT_ENTITY_PAGE = 50;
|
|
440
|
+
declare const MAX_ENTITY_PAGE = 100;
|
|
441
|
+
type EntityDefinition<Body, Item, Key extends string, Param extends string, Handle = Executor> = {
|
|
442
|
+
columns?: EntityColumns;
|
|
443
|
+
/**
|
|
444
|
+
* The columns kept beside the body, read OFF the body on every write. The body is the only
|
|
445
|
+
* source: a column a caller could set separately is a second answer to what the entity says.
|
|
446
|
+
*/
|
|
447
|
+
indexed?: (body: Body) => Readonly<Record<string, unknown>>;
|
|
448
|
+
item: (row: EntityRow<Body>) => Item;
|
|
449
|
+
/** The column a row is addressed by, and the parameter that names it. */
|
|
450
|
+
key: {
|
|
451
|
+
column: string;
|
|
452
|
+
param: Param;
|
|
453
|
+
};
|
|
454
|
+
order: EntityOrdering;
|
|
455
|
+
schema?: string;
|
|
456
|
+
/** The session every one of the generated verbs runs inside (D-014). */
|
|
457
|
+
seam: SessionSeam<Key, Handle>;
|
|
458
|
+
table: string;
|
|
459
|
+
};
|
|
460
|
+
type Tenanted<Key extends string> = Readonly<Record<Key, string>>;
|
|
461
|
+
type Addressed<Key extends string, Param extends string> = Tenanted<Key> & Readonly<Record<Param, string>>;
|
|
462
|
+
type Keyed<Param extends string> = Readonly<Record<Param, string>>;
|
|
463
|
+
type UpsertOutput<Body, Param extends string> = Keyed<Param> & {
|
|
464
|
+
previous: Body | null;
|
|
465
|
+
};
|
|
466
|
+
type RetireOutput<Body, Param extends string> = Keyed<Param> & {
|
|
467
|
+
retired: Body | null;
|
|
468
|
+
};
|
|
469
|
+
/**
|
|
470
|
+
* A compensating query, as the data an engine reads: what the forward step answered, and the tenant
|
|
471
|
+
* it ran for. The engine holds the connection and knows neither this entity's key nor its seam's.
|
|
472
|
+
*/
|
|
473
|
+
type EntityUndo<Output, Handle = Executor> = Query<{
|
|
474
|
+
output: Output;
|
|
475
|
+
tenantId: string;
|
|
476
|
+
}, void, Handle>;
|
|
477
|
+
type Entity<Body, Item, Key extends string, Param extends string, Handle = Executor> = {
|
|
478
|
+
get: Query<Addressed<Key, Param>, Item | null, Handle>;
|
|
479
|
+
list: Query<Tenanted<Key> & {
|
|
480
|
+
cursor?: string;
|
|
481
|
+
limit?: number;
|
|
482
|
+
}, EntityPage<Item>, Handle>;
|
|
483
|
+
listAll: Query<Tenanted<Key>, Item[], Handle>;
|
|
484
|
+
retire: Query<Addressed<Key, Param>, RetireOutput<Body, Param>, Handle>;
|
|
485
|
+
undos: {
|
|
486
|
+
retire: EntityUndo<RetireOutput<Body, Param>, Handle>;
|
|
487
|
+
upsert: EntityUndo<UpsertOutput<Body, Param>, Handle>;
|
|
488
|
+
};
|
|
489
|
+
upsert: Query<Addressed<Key, Param> & {
|
|
490
|
+
body: Body;
|
|
491
|
+
}, UpsertOutput<Body, Param>, Handle>;
|
|
492
|
+
};
|
|
493
|
+
/**
|
|
494
|
+
* One declaration per entity: one payload table, one business key, and every read, every write and
|
|
495
|
+
* both undos written once (#251).
|
|
496
|
+
*
|
|
497
|
+
* Every verb goes through the seam's `scoped`, so a tenant is named before any of them runs and
|
|
498
|
+
* there is no door here that skips one (D-014).
|
|
499
|
+
*/
|
|
500
|
+
declare const defineEntity: <Body, Item, Key extends string, Param extends string, Handle = Executor>(definition: EntityDefinition<Body, Item, Key, Param, Handle>) => Entity<Body, Item, Key, Param, Handle>;
|
|
501
|
+
/**
|
|
502
|
+
* The compensations of an entity, by name — the registry a workflow engine reads as plain data.
|
|
503
|
+
*
|
|
504
|
+
* A door rather than a property so the engine has one thing to import and this package has one
|
|
505
|
+
* thing to keep: `@geonosis/db` and an engine are sibling foundations, and what crosses between
|
|
506
|
+
* them is the shape.
|
|
507
|
+
*/
|
|
508
|
+
declare const undosOf: <Body, Item, Key extends string, Param extends string, Handle>(entity: Entity<Body, Item, Key, Param, Handle>) => Entity<Body, Item, Key, Param, Handle>["undos"];
|
|
509
|
+
|
|
510
|
+
/** As much of a generated entity as the exam asks about; a consumer's own is their own type. */
|
|
511
|
+
type EntityUnderTest<Body, Item, Handle> = Entity<Body, Item, string, string, Handle>;
|
|
512
|
+
type EntitySubject<Body, Item, Handle = Executor> = {
|
|
513
|
+
/** Two bodies for the same row: what is written first, and what replaces it. */
|
|
514
|
+
bodies: readonly [Body, Body];
|
|
515
|
+
/** The body an item carries, for an exam that cannot know what a definition maps to. */
|
|
516
|
+
bodyOf: (item: Item) => Body;
|
|
517
|
+
connection: Handle;
|
|
518
|
+
entity: EntityUnderTest<Body, Item, Handle>;
|
|
519
|
+
/** Three keys, in the order this entity's listing index puts them. */
|
|
520
|
+
keys: readonly [string, string, string];
|
|
521
|
+
/** The parameter the entity addresses a row by. */
|
|
522
|
+
keyParam: string;
|
|
523
|
+
/** Empty the table. Whatever role can do that — the exam's own rows are its business. */
|
|
524
|
+
reset: () => Promise<void>;
|
|
525
|
+
/**
|
|
526
|
+
* A handle that reports every statement it sends, the transaction's own begin and commit
|
|
527
|
+
* included. Without it the write census is untested rather than passing.
|
|
528
|
+
*/
|
|
529
|
+
recording?: () => Promise<RecordingConnection<Handle>>;
|
|
530
|
+
/** The key the entity's queries carry their tenant under. */
|
|
531
|
+
tenantKey: string;
|
|
532
|
+
/**
|
|
533
|
+
* The wall this entity's table was rendered with, and the lever that reaches past it. Supplied,
|
|
534
|
+
* the exam checks that the DDL, the policies and the session all name the SAME two settings —
|
|
535
|
+
* which no default-named table can show, because every one of the three would be wrong together.
|
|
536
|
+
*/
|
|
537
|
+
wall?: {
|
|
538
|
+
/** Every row of the table, no predicate and no tenant: what the policy alone lets through. */
|
|
539
|
+
countAll: (handle: Handle) => Promise<number>;
|
|
540
|
+
inOps: <Result>(run: (tx: Handle) => Promise<Result>) => Promise<Result>;
|
|
541
|
+
settings: {
|
|
542
|
+
opsSetting: string;
|
|
543
|
+
tenantSetting: string;
|
|
544
|
+
};
|
|
545
|
+
};
|
|
546
|
+
};
|
|
547
|
+
/**
|
|
548
|
+
* The generated verbs, the keyset walk, both undos and the wall between two tenants (#251).
|
|
549
|
+
*
|
|
550
|
+
* A page is asked for one row at a time on purpose: a walk that fits in one page proves nothing
|
|
551
|
+
* about the cursor, and a listing whose keyset predicate is wrong shows up as a row visited twice
|
|
552
|
+
* or not at all rather than as an error.
|
|
553
|
+
*/
|
|
554
|
+
declare const entityConformance: <Body, Item, Handle>(subject: EntitySubject<Body, Item, Handle>) => ConformanceCase[];
|
|
555
|
+
|
|
556
|
+
/** The tokens the DDL carries where a consumer's own name belongs, for a migrator that templates. */
|
|
557
|
+
declare const ENTITY_PLACEHOLDERS: {
|
|
558
|
+
opsSetting: string;
|
|
559
|
+
tenantSetting: string;
|
|
560
|
+
};
|
|
561
|
+
type EntityMigration = {
|
|
562
|
+
name: string;
|
|
563
|
+
statements: string[];
|
|
564
|
+
};
|
|
565
|
+
type EntityDdlOptions = {
|
|
566
|
+
columns?: EntityColumns;
|
|
567
|
+
/** The columns kept beside the body, read off it on every write. Text unless named otherwise. */
|
|
568
|
+
indexed?: readonly (string | {
|
|
569
|
+
column: string;
|
|
570
|
+
type: string;
|
|
571
|
+
})[];
|
|
572
|
+
/** The business key column a row is addressed by. */
|
|
573
|
+
key: string;
|
|
574
|
+
order: EntityOrdering;
|
|
575
|
+
schema?: string;
|
|
576
|
+
settings: SessionSettings;
|
|
577
|
+
table: string;
|
|
578
|
+
};
|
|
579
|
+
/**
|
|
580
|
+
* An entity's table and its wall, as the two files a consumer applies.
|
|
581
|
+
*
|
|
582
|
+
* The tenant column DEFAULTS to the session setting, so a write that names no tenant still lands
|
|
583
|
+
* under the one its transaction is open for — the column and the policy then say the same thing,
|
|
584
|
+
* and neither depends on a caller remembering. Rendered with `ENTITY_PLACEHOLDERS` it is the same
|
|
585
|
+
* DDL with the tokens in it, for a migrator that templates.
|
|
586
|
+
*/
|
|
587
|
+
declare const entityMigrations: (options: EntityDdlOptions) => EntityMigration[];
|
|
588
|
+
|
|
245
589
|
/**
|
|
246
590
|
* What this seam throws when it refuses, so a caller can catch it by class (#211).
|
|
247
591
|
*
|
|
@@ -283,7 +627,19 @@ type PgClientLike = {
|
|
|
283
627
|
}>;
|
|
284
628
|
release: () => void;
|
|
285
629
|
};
|
|
286
|
-
|
|
630
|
+
/** What one `open` reports its statements to, handed to the drizzle factory to wire a logger. */
|
|
631
|
+
type StatementReporting = {
|
|
632
|
+
onStatement?: (statement: string) => void;
|
|
633
|
+
};
|
|
634
|
+
/**
|
|
635
|
+
* The database this driver's handles speak through, built over the pool it opened.
|
|
636
|
+
*
|
|
637
|
+
* A factory rather than a flag: a repo's database carries its own schema and options, and a flag
|
|
638
|
+
* could only ever hand back one with neither. `pool` is declared `never` so the factory's own
|
|
639
|
+
* parameter type — whatever the `pg` copy it imports says a pool is — satisfies it without a cast.
|
|
640
|
+
*/
|
|
641
|
+
type DrizzleFactory<Db extends DrizzleDatabase> = (pool: never, reporting: StatementReporting) => Db;
|
|
642
|
+
type NodePostgresOptions<Db extends DrizzleDatabase = DrizzleDatabase> = {
|
|
287
643
|
/**
|
|
288
644
|
* The table recording which migration files have been applied. Named after this package so it
|
|
289
645
|
* cannot collide with a consumer's own; a consumer whose migrator already keeps one names theirs.
|
|
@@ -291,6 +647,19 @@ type NodePostgresOptions = {
|
|
|
291
647
|
migrationsTable?: string;
|
|
292
648
|
/** The `pg` module itself, for a runtime whose copy is patched. Loaded on demand when absent. */
|
|
293
649
|
pg?: PgModuleLike;
|
|
650
|
+
drizzle?: DrizzleFactory<Db>;
|
|
651
|
+
};
|
|
652
|
+
/**
|
|
653
|
+
* Two answers, one call: a driver whose handles are the executor port, or — given a factory — one
|
|
654
|
+
* whose handles are the DATABASE it built, so a repo on drizzle keeps its builders through the
|
|
655
|
+
* session this package opens. The drizzle signature is first because an options object without
|
|
656
|
+
* that key cannot match it.
|
|
657
|
+
*/
|
|
658
|
+
type NodePostgresDriverFactory = {
|
|
659
|
+
<Db extends DrizzleDatabase>(options: NodePostgresOptions<Db> & {
|
|
660
|
+
drizzle: DrizzleFactory<Db>;
|
|
661
|
+
}): Promise<Driver<Db>>;
|
|
662
|
+
(options?: NodePostgresOptions): Promise<Driver>;
|
|
294
663
|
};
|
|
295
664
|
/**
|
|
296
665
|
* The one file in this package that names `pg`.
|
|
@@ -298,24 +667,7 @@ type NodePostgresOptions = {
|
|
|
298
667
|
* It is a factory rather than a constant because the module is loaded on demand: a consumer using
|
|
299
668
|
* the executor port with their own database library never resolves `pg` at all.
|
|
300
669
|
*/
|
|
301
|
-
declare const nodePostgresDriver:
|
|
302
|
-
|
|
303
|
-
/**
|
|
304
|
-
* The two session settings a tenant wall is built on, spelled once at the composition root.
|
|
305
|
-
*
|
|
306
|
-
* Neither has a default. A default here would be one repo's name — `app.org_id` at the repo this
|
|
307
|
-
* came from — compiled into every other repo's policies, and the seam and the policies must agree
|
|
308
|
-
* on it exactly or the wall is open while every test still passes.
|
|
309
|
-
*/
|
|
310
|
-
type SessionSettings = {
|
|
311
|
-
/** What maintenance sets to see every tenant, through a lever no request path can reach. */
|
|
312
|
-
opsSetting: string;
|
|
313
|
-
/** What the ops setting holds while the lever is pulled. */
|
|
314
|
-
opsValue?: string;
|
|
315
|
-
/** What a transaction sets to name the tenant it is open for, and what the policies read. */
|
|
316
|
-
tenantSetting: string;
|
|
317
|
-
};
|
|
318
|
-
declare const DEFAULT_OPS_VALUE = "on";
|
|
670
|
+
declare const nodePostgresDriver: NodePostgresDriverFactory;
|
|
319
671
|
|
|
320
672
|
type FreezeRegister = {
|
|
321
673
|
/** The column of that register holding the tenant a row names. */
|
|
@@ -345,6 +697,39 @@ type TenantPolicyOptions = {
|
|
|
345
697
|
/** The column holding the tenant a row belongs to. */
|
|
346
698
|
tenantColumn: string;
|
|
347
699
|
};
|
|
700
|
+
/** One policy of the tenant wall, before either door renders it. */
|
|
701
|
+
type TenantPolicy = {
|
|
702
|
+
as: 'permissive' | 'restrictive';
|
|
703
|
+
name: string;
|
|
704
|
+
/** The predicate, in both `using` and `with check`: read one way and write the same way. */
|
|
705
|
+
predicate: string;
|
|
706
|
+
};
|
|
707
|
+
declare const tenantPolicySet: (options: TenantPolicyOptions) => TenantPolicy[];
|
|
708
|
+
/** As much of drizzle's policy vocabulary as this door uses; the consumer hands in their own. */
|
|
709
|
+
type DrizzlePolicyTools = {
|
|
710
|
+
/**
|
|
711
|
+
* `never`, measured: drizzle-orm 0.45.2 types the config as its own class with its own `SQL`
|
|
712
|
+
* inside, and a structural stand-in for it here would be this package naming a type from a
|
|
713
|
+
* library it does not depend on. What the config must actually hold is held by the reach test,
|
|
714
|
+
* which runs the real generator over a real schema.
|
|
715
|
+
*/
|
|
716
|
+
pgPolicy: (name: string, config: never) => unknown;
|
|
717
|
+
sql: {
|
|
718
|
+
raw: (text: string) => unknown;
|
|
719
|
+
};
|
|
720
|
+
};
|
|
721
|
+
/**
|
|
722
|
+
* The same wall as drizzle declarations, for a table's own definition.
|
|
723
|
+
*
|
|
724
|
+
* The tools are handed in rather than imported: a schema file already imports them, and this way
|
|
725
|
+
* the door costs nothing to a consumer who has never installed drizzle. FORCE is not among them —
|
|
726
|
+
* drizzle-orm 0.45.2 and drizzle-kit 0.31.10 contain the string `FORCE ROW LEVEL SECURITY` nowhere
|
|
727
|
+
* (measured 2026-09-02) — so `forceRowLevelSecurity` stays the statement to apply beside the
|
|
728
|
+
* generated migration, and ENABLE alone exempts the table's owner.
|
|
729
|
+
*/
|
|
730
|
+
declare const drizzleTenantPolicies: (tools: DrizzlePolicyTools, options: TenantPolicyOptions) => unknown[];
|
|
731
|
+
/** The one statement a drizzle declaration cannot say, for the table its policies were declared on. */
|
|
732
|
+
declare const forceRowLevelSecurity: (table: string, schema?: string) => string;
|
|
348
733
|
/**
|
|
349
734
|
* The row-level-security DDL for one tenant table, as statements to apply after the migrator ran.
|
|
350
735
|
*
|
|
@@ -360,40 +745,4 @@ type TenantPolicyOptions = {
|
|
|
360
745
|
*/
|
|
361
746
|
declare const tenantPolicies: (table: string, options: TenantPolicyOptions) => string[];
|
|
362
747
|
|
|
363
|
-
|
|
364
|
-
type Query<Params, Result> = (executor: Executor, params: Params) => Promise<Result>;
|
|
365
|
-
declare const DEFAULT_TENANT_KEY = "tenantId";
|
|
366
|
-
type SessionSeam<Key extends string = typeof DEFAULT_TENANT_KEY> = {
|
|
367
|
-
inOps: <Result>(executor: Executor, run: (tx: Executor) => Promise<Result>) => Promise<Result>;
|
|
368
|
-
inTenant: <Result>(executor: Executor, tenantId: string, run: (tx: Executor) => Promise<Result>) => Promise<Result>;
|
|
369
|
-
scoped: <Params extends Readonly<Record<Key, string>>, Result>(query: Query<Params, Result>) => Query<Params, Result>;
|
|
370
|
-
scopedAsOps: <Params, Result>(query: Query<Params, Result>) => Query<Params, Result>;
|
|
371
|
-
};
|
|
372
|
-
type SessionSeamConfig<Key extends string = typeof DEFAULT_TENANT_KEY> = {
|
|
373
|
-
/**
|
|
374
|
-
* How long a statement may run under the ops lever, as Postgres reads it. No default: a sweep's
|
|
375
|
-
* budget is a fact about one deployment's data, and the value that fits belongs to whoever runs
|
|
376
|
-
* it. Absent, the connection's own timeout stands.
|
|
377
|
-
*/
|
|
378
|
-
opsStatementTimeout?: string;
|
|
379
|
-
settings: SessionSettings;
|
|
380
|
-
/**
|
|
381
|
-
* The key a query's own parameters carry its tenant under (#245). A repo whose queries say
|
|
382
|
-
* `{ orgId }` names it here instead of wrapping every call site in `inTenant` by hand.
|
|
383
|
-
*
|
|
384
|
-
* A key name rather than a selector, measured: under `tenantOf: (params) => string` nothing ties
|
|
385
|
-
* the selector's shape to the query's, and a query whose parameters carry no tenant at all
|
|
386
|
-
* compiles clean — which is the one thing `scoped` is here to make impossible.
|
|
387
|
-
*/
|
|
388
|
-
tenantKey?: Key;
|
|
389
|
-
};
|
|
390
|
-
/**
|
|
391
|
-
* The seam every query crosses: one transaction, the scope named as its first statement.
|
|
392
|
-
*
|
|
393
|
-
* `set_config(..., true)` is local to the transaction, which is what makes this safe on a pool —
|
|
394
|
-
* the name is gone when the transaction ends, and a connection handed to the next request carries
|
|
395
|
-
* nothing. Naming it second would leave the statements before it running under no tenant at all.
|
|
396
|
-
*/
|
|
397
|
-
declare const createSessionSeam: <Key extends string = typeof DEFAULT_TENANT_KEY>(config: SessionSeamConfig<Key>) => SessionSeam<Key>;
|
|
398
|
-
|
|
399
|
-
export { type AppRoleOptions, type ConformanceCase, type Connection, type Connections, type ConnectionsConfig, type ConnectionsSubject, type ConnectionsUnderTest, DEFAULT_OPS_VALUE, DEFAULT_TENANT_KEY, DbRefusal, type Driver, type Executor, type FreezeRegister, type InvocationOptions, type NodePostgresOptions, type OpenConnection, type OpenOptions, type PgClientLike, type PgModuleLike, type PgPoolLike, type ProbeTable, type Query, type RecordingConnection, type SeamUnderTest, type SessionSeam, type SessionSeamConfig, type SessionSettings, type SessionSubject, type Statement, type TenantPolicyNames, type TenantPolicyOptions, appConnectionString, appRoleStatements, concurrentTenantsConformance, connectionsConformance, createConnections, createSessionSeam, forcedRowLevelSecurityConformance, nodePostgresDriver, perStepConnection, scopedQueryConformance, sessionConformance, statementCensusConformance, tenantIsolationConformance, tenantPolicies };
|
|
748
|
+
export { type AppRoleOptions, type ConformanceCase, type Connection, type Connections, type ConnectionsConfig, type ConnectionsSubject, type ConnectionsUnderTest, DEFAULT_ENTITY_PAGE, DEFAULT_OPS_VALUE, DEFAULT_TENANT_KEY, DbRefusal, type Driver, type DrizzleDatabase, type DrizzleFactory, type DrizzleModuleLike, type DrizzlePolicyTools, type DrizzleSessionOptions, type DrizzleSql, ENTITY_PLACEHOLDERS, type Entity, type EntityColumns, type EntityDdlOptions, type EntityDefinition, type EntityMigration, type EntityOrdering, type EntityPage, type EntityRow, type EntitySubject, type EntityUnderTest, type EntityUndo, type Executor, type FreezeRegister, type InvocationOptions, MAX_ENTITY_PAGE, type NodePostgresOptions, type OpenConnection, type OpenOptions, type PgClientLike, type PgModuleLike, type PgPoolLike, type ProbeTable, type Query, type RecordingConnection, type RetireOutput, type SeamUnderTest, type SessionHandles, type SessionSeam, type SessionSeamConfig, type SessionSettings, type SessionSubject, type SqlFragment, type Statement, type StatementReporting, type TenantPolicy, type TenantPolicyNames, type TenantPolicyOptions, type UpsertOutput, appConnectionString, appRoleStatements, asStatement, concurrentTenantsConformance, connectionsConformance, createConnections, createSessionSeam, defineEntity, drizzleSession, drizzleTenantPolicies, entityConformance, entityMigrations, forceRowLevelSecurity, forcedRowLevelSecurityConformance, fragment, nodePostgresDriver, perStepConnection, scopedQueryConformance, sessionConformance, statementCensusConformance, tenantIsolationConformance, tenantPolicies, tenantPolicySet, undosOf };
|