@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/CHANGELOG.md +151 -0
- package/README.md +177 -8
- package/dist/index.cjs +1152 -171
- package/dist/index.d.cts +490 -78
- package/dist/index.d.ts +490 -78
- package/dist/index.js +1132 -167
- package/package.json +11 -3
package/dist/index.d.ts
CHANGED
|
@@ -54,20 +54,120 @@ 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>;
|
|
164
|
+
/**
|
|
165
|
+
* The wrapper that reads the tenant out of a query's own parameters. Spelled here rather than
|
|
166
|
+
* imported: what the exam asks about is the behaviour, and a consumer's seam is their own type.
|
|
167
|
+
*/
|
|
168
|
+
scoped?: <Params extends Readonly<Record<string, string>>, Result>(query: (handle: Handle, params: Params) => Promise<Result>) => (handle: Handle, params: Params) => Promise<Result>;
|
|
169
|
+
/** The key those parameters carry the tenant under. `tenantId` when the seam names none. */
|
|
170
|
+
tenantKey?: string;
|
|
71
171
|
};
|
|
72
172
|
/**
|
|
73
173
|
* A table under the policies under test, whose only required columns are these two.
|
|
@@ -81,16 +181,23 @@ type ProbeTable = {
|
|
|
81
181
|
schema?: string;
|
|
82
182
|
tenantColumn: string;
|
|
83
183
|
};
|
|
84
|
-
type SessionSubject = {
|
|
184
|
+
type SessionSubject<Handle = Executor> = {
|
|
85
185
|
/** A handle OUTSIDE every session: what the policies alone let through. */
|
|
86
|
-
connection:
|
|
186
|
+
connection: Handle;
|
|
87
187
|
/** The tables whose FORCE is checked. The probe table alone, when nothing else is named. */
|
|
88
188
|
guardedTables?: readonly string[];
|
|
89
189
|
probe: ProbeTable;
|
|
90
|
-
recording: () => Promise<RecordingConnection
|
|
190
|
+
recording: () => Promise<RecordingConnection<Handle>>;
|
|
91
191
|
/** Empty the probe table. Whatever role can do that — the exam's own rows are its business. */
|
|
92
192
|
reset: () => Promise<void>;
|
|
93
|
-
|
|
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>;
|
|
94
201
|
};
|
|
95
202
|
/**
|
|
96
203
|
* The tenant wall, as the source proof took it: what a query with NO predicate can still see.
|
|
@@ -98,7 +205,7 @@ type SessionSubject = {
|
|
|
98
205
|
* Every read here is unqualified on purpose. A test that filters by tenant proves its own WHERE
|
|
99
206
|
* clause works; only an unfiltered one asks what the policy lets through.
|
|
100
207
|
*/
|
|
101
|
-
declare const tenantIsolationConformance: (subject: SessionSubject) => ConformanceCase[];
|
|
208
|
+
declare const tenantIsolationConformance: <Handle>(subject: SessionSubject<Handle>) => ConformanceCase[];
|
|
102
209
|
/**
|
|
103
210
|
* The census: a read costs what it says.
|
|
104
211
|
*
|
|
@@ -107,20 +214,70 @@ declare const tenantIsolationConformance: (subject: SessionSubject) => Conforman
|
|
|
107
214
|
* a second transaction nested inside the first. Twenty scoped reads against a pool of five is how
|
|
108
215
|
* the source repo found this one.
|
|
109
216
|
*/
|
|
110
|
-
declare const statementCensusConformance: (subject: SessionSubject) => ConformanceCase[];
|
|
217
|
+
declare const statementCensusConformance: <Handle>(subject: SessionSubject<Handle>) => ConformanceCase[];
|
|
111
218
|
/**
|
|
112
219
|
* Two tenants at once on one pool — the case a suite that runs one tenant at a time never reaches,
|
|
113
220
|
* and the one a pooled connection carrying a stale setting would fail.
|
|
114
221
|
*/
|
|
115
|
-
declare const concurrentTenantsConformance: (subject: SessionSubject) => ConformanceCase[];
|
|
222
|
+
declare const concurrentTenantsConformance: <Handle>(subject: SessionSubject<Handle>) => ConformanceCase[];
|
|
223
|
+
/**
|
|
224
|
+
* A query that names its own tenant, under the key the CONSUMER's queries carry it in (#245).
|
|
225
|
+
*
|
|
226
|
+
* A seam that reads only `tenantId` sends every repo whose queries say something else back through
|
|
227
|
+
* `inTenant` with a wrapper per call site, which is the hand-written scope the seam exists to
|
|
228
|
+
* remove — and a wrapper that reads a key nobody carries names `undefined`, which is not a tenant
|
|
229
|
+
* and, before this, was not a refusal either.
|
|
230
|
+
*/
|
|
231
|
+
declare const scopedQueryConformance: <Handle>(subject: SessionSubject<Handle>) => ConformanceCase[];
|
|
116
232
|
/**
|
|
117
233
|
* ENABLE alone exempts the table's OWNER, and the owner is the role that migrates and the role an
|
|
118
234
|
* ops sweep connects as. A wall only the application is behind is a wall with a door in it.
|
|
119
235
|
*/
|
|
120
|
-
declare const forcedRowLevelSecurityConformance: (subject: SessionSubject) => ConformanceCase[];
|
|
236
|
+
declare const forcedRowLevelSecurityConformance: <Handle>(subject: SessionSubject<Handle>) => ConformanceCase[];
|
|
237
|
+
/** As much of a handle keeper as the exam asks about. */
|
|
238
|
+
type ConnectionsUnderTest<Handle = Connection> = {
|
|
239
|
+
open?: () => {
|
|
240
|
+
close: () => Promise<void>;
|
|
241
|
+
connection: Handle;
|
|
242
|
+
};
|
|
243
|
+
sessionDb: () => Handle;
|
|
244
|
+
withConnection: <Result>(run: () => Promise<Result>, release?: (closing: Promise<void>) => void) => Promise<Result>;
|
|
245
|
+
};
|
|
246
|
+
type ConnectionsSubject<Handle = Connection> = {
|
|
247
|
+
connections: ConnectionsUnderTest<Handle>;
|
|
248
|
+
/**
|
|
249
|
+
* A unit of work, by name. A durable step's handle is the one that outlives its step, and a
|
|
250
|
+
* refusal that cannot say WHICH step leaked sends a reader through every step in the workflow.
|
|
251
|
+
*/
|
|
252
|
+
perStep?: (named: string) => <Output>(body: () => Promise<Output>) => Promise<Output>;
|
|
253
|
+
/** Something any role may run. The exam asks a handle a question rather than assuming a table. */
|
|
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>;
|
|
257
|
+
};
|
|
258
|
+
/**
|
|
259
|
+
* The handle discipline: a connection belongs to the invocation that opened it and to no other.
|
|
260
|
+
*
|
|
261
|
+
* A serverless invocation hibernates and dies, so a handle held across one is a socket nobody is on
|
|
262
|
+
* the other end of. Held inside the process that opened it, the failure is a driver's own error at
|
|
263
|
+
* some later line — "Cannot use a pool after calling end on the pool" — which reads as a bug in the
|
|
264
|
+
* query rather than as a handle used out of its lifetime.
|
|
265
|
+
*/
|
|
266
|
+
declare const connectionsConformance: <Handle>(subject: ConnectionsSubject<Handle>) => ConformanceCase[];
|
|
121
267
|
/** The whole exam: what a consumer runs against their own session before depending on it. */
|
|
122
|
-
declare const sessionConformance: (subject: SessionSubject) => ConformanceCase[];
|
|
268
|
+
declare const sessionConformance: <Handle>(subject: SessionSubject<Handle>) => ConformanceCase[];
|
|
123
269
|
|
|
270
|
+
/** What one invocation may be told about itself, for the refusal a handle that outlives it raises. */
|
|
271
|
+
type InvocationOptions = {
|
|
272
|
+
/** The unit of work this frame is, so a leaked handle is named with what leaked it. */
|
|
273
|
+
named?: string;
|
|
274
|
+
/**
|
|
275
|
+
* A frame of its own even when this call is nested inside one. A durable step needs it: an
|
|
276
|
+
* entrypoint that wraps the whole run in an invocation would otherwise hand every step the RUN's
|
|
277
|
+
* handle, and workerd refuses I/O on a socket opened for another request.
|
|
278
|
+
*/
|
|
279
|
+
own?: boolean;
|
|
280
|
+
};
|
|
124
281
|
type OpenOptions = {
|
|
125
282
|
/**
|
|
126
283
|
* Every statement this handle sends, as it is sent — the transaction's own begin and commit
|
|
@@ -140,29 +297,40 @@ type OpenOptions = {
|
|
|
140
297
|
*/
|
|
141
298
|
poolSize?: number;
|
|
142
299
|
};
|
|
143
|
-
/**
|
|
144
|
-
|
|
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> = {
|
|
145
308
|
close: () => Promise<void>;
|
|
146
|
-
connection:
|
|
309
|
+
connection: Handle;
|
|
147
310
|
};
|
|
148
311
|
/** The one thing in a deployment that knows which package speaks the wire protocol. */
|
|
149
|
-
type Driver = {
|
|
312
|
+
type Driver<Handle = Connection> = {
|
|
150
313
|
migrate: (connectionString: string, migrationsFolder: string) => Promise<void>;
|
|
151
|
-
open: (connectionString: string, options?: OpenOptions) => OpenConnection
|
|
314
|
+
open: (connectionString: string, options?: OpenOptions) => OpenConnection<Handle>;
|
|
152
315
|
};
|
|
153
|
-
type Connections = {
|
|
316
|
+
type Connections<Handle = Connection> = {
|
|
154
317
|
/** Whether this call is running inside an invocation frame. */
|
|
155
318
|
inInvocation: () => boolean;
|
|
156
319
|
/** A handle outside every frame, for a caller who will close it themselves. */
|
|
157
|
-
open: () => OpenConnection
|
|
320
|
+
open: () => OpenConnection<Handle>;
|
|
158
321
|
/** The handle this invocation is using, opened on first ask. */
|
|
159
|
-
sessionDb: () =>
|
|
160
|
-
withConnection: <Result>(run: () => Promise<Result>, release?: (closing: Promise<void>) => void) => Promise<Result>;
|
|
322
|
+
sessionDb: () => Handle;
|
|
323
|
+
withConnection: <Result>(run: () => Promise<Result>, release?: (closing: Promise<void>) => void, invocation?: InvocationOptions) => Promise<Result>;
|
|
161
324
|
};
|
|
162
|
-
type ConnectionsConfig = {
|
|
325
|
+
type ConnectionsConfig<Handle = Connection> = {
|
|
163
326
|
connectionString: string;
|
|
164
|
-
driver: Driver
|
|
327
|
+
driver: Driver<Handle>;
|
|
165
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'>;
|
|
166
334
|
};
|
|
167
335
|
/**
|
|
168
336
|
* The handles of one database, and the discipline that closes them.
|
|
@@ -172,20 +340,251 @@ type ConnectionsConfig = {
|
|
|
172
340
|
* lifetime that matches, and the frame is what makes "this invocation" a thing the code can ask
|
|
173
341
|
* about rather than a thing the caller has to thread through.
|
|
174
342
|
*/
|
|
175
|
-
declare const createConnections: (config: ConnectionsConfig) => Connections
|
|
343
|
+
declare const createConnections: <Handle = Connection>(config: ConnectionsConfig<Handle>) => Connections<Handle>;
|
|
176
344
|
/**
|
|
177
345
|
* The `scope.perStep` hook a workflow engine asks for, answered with a connection per unit of work.
|
|
178
346
|
*
|
|
179
347
|
* A durable run hibernates between steps, so the handle its first step opened is dead by the fifth:
|
|
180
|
-
* each step body and each undo opens its own
|
|
181
|
-
*
|
|
348
|
+
* each step body and each undo opens its own — its OWN frame, even when the entrypoint has already
|
|
349
|
+
* wrapped the whole run in one, because a step handed the run's handle is a step doing I/O for a
|
|
350
|
+
* request that has gone. The engine's own writes — the trail, the closing batch — are not units of
|
|
351
|
+
* work and stay outside, on whatever connection the runner holds.
|
|
182
352
|
*
|
|
183
353
|
* The type is not imported. `@geonosis/workflows` is a sibling foundation and the floors forbid an
|
|
184
|
-
* edge between two of them in either direction; what crosses is the shape.
|
|
354
|
+
* edge between two of them in either direction; what crosses is the shape. `perStep` takes the
|
|
355
|
+
* step's name where the caller has one — `RunScope` calls it with none, and a frame with no name
|
|
356
|
+
* refuses by lifetime alone.
|
|
185
357
|
*/
|
|
186
358
|
declare const perStepConnection: (connections: Pick<Connections, "withConnection">) => {
|
|
187
|
-
perStep: () => <Output>(body: () => Promise<Output>) => Promise<Output>;
|
|
359
|
+
perStep: (named?: string) => <Output>(body: () => Promise<Output>) => Promise<Output>;
|
|
360
|
+
};
|
|
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;
|
|
188
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[];
|
|
189
588
|
|
|
190
589
|
/**
|
|
191
590
|
* What this seam throws when it refuses, so a caller can catch it by class (#211).
|
|
@@ -228,7 +627,19 @@ type PgClientLike = {
|
|
|
228
627
|
}>;
|
|
229
628
|
release: () => void;
|
|
230
629
|
};
|
|
231
|
-
|
|
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> = {
|
|
232
643
|
/**
|
|
233
644
|
* The table recording which migration files have been applied. Named after this package so it
|
|
234
645
|
* cannot collide with a consumer's own; a consumer whose migrator already keeps one names theirs.
|
|
@@ -236,6 +647,19 @@ type NodePostgresOptions = {
|
|
|
236
647
|
migrationsTable?: string;
|
|
237
648
|
/** The `pg` module itself, for a runtime whose copy is patched. Loaded on demand when absent. */
|
|
238
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>;
|
|
239
663
|
};
|
|
240
664
|
/**
|
|
241
665
|
* The one file in this package that names `pg`.
|
|
@@ -243,24 +667,7 @@ type NodePostgresOptions = {
|
|
|
243
667
|
* It is a factory rather than a constant because the module is loaded on demand: a consumer using
|
|
244
668
|
* the executor port with their own database library never resolves `pg` at all.
|
|
245
669
|
*/
|
|
246
|
-
declare const nodePostgresDriver:
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* The two session settings a tenant wall is built on, spelled once at the composition root.
|
|
250
|
-
*
|
|
251
|
-
* Neither has a default. A default here would be one repo's name — `app.org_id` at the repo this
|
|
252
|
-
* came from — compiled into every other repo's policies, and the seam and the policies must agree
|
|
253
|
-
* on it exactly or the wall is open while every test still passes.
|
|
254
|
-
*/
|
|
255
|
-
type SessionSettings = {
|
|
256
|
-
/** What maintenance sets to see every tenant, through a lever no request path can reach. */
|
|
257
|
-
opsSetting: string;
|
|
258
|
-
/** What the ops setting holds while the lever is pulled. */
|
|
259
|
-
opsValue?: string;
|
|
260
|
-
/** What a transaction sets to name the tenant it is open for, and what the policies read. */
|
|
261
|
-
tenantSetting: string;
|
|
262
|
-
};
|
|
263
|
-
declare const DEFAULT_OPS_VALUE = "on";
|
|
670
|
+
declare const nodePostgresDriver: NodePostgresDriverFactory;
|
|
264
671
|
|
|
265
672
|
type FreezeRegister = {
|
|
266
673
|
/** The column of that register holding the tenant a row names. */
|
|
@@ -290,6 +697,39 @@ type TenantPolicyOptions = {
|
|
|
290
697
|
/** The column holding the tenant a row belongs to. */
|
|
291
698
|
tenantColumn: string;
|
|
292
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;
|
|
293
733
|
/**
|
|
294
734
|
* The row-level-security DDL for one tenant table, as statements to apply after the migrator ran.
|
|
295
735
|
*
|
|
@@ -305,32 +745,4 @@ type TenantPolicyOptions = {
|
|
|
305
745
|
*/
|
|
306
746
|
declare const tenantPolicies: (table: string, options: TenantPolicyOptions) => string[];
|
|
307
747
|
|
|
308
|
-
|
|
309
|
-
type Query<Params, Result> = (executor: Executor, params: Params) => Promise<Result>;
|
|
310
|
-
type SessionSeam = {
|
|
311
|
-
inOps: <Result>(executor: Executor, run: (tx: Executor) => Promise<Result>) => Promise<Result>;
|
|
312
|
-
inTenant: <Result>(executor: Executor, tenantId: string, run: (tx: Executor) => Promise<Result>) => Promise<Result>;
|
|
313
|
-
scoped: <Params extends {
|
|
314
|
-
tenantId: string;
|
|
315
|
-
}, Result>(query: Query<Params, Result>) => Query<Params, Result>;
|
|
316
|
-
scopedAsOps: <Params, Result>(query: Query<Params, Result>) => Query<Params, Result>;
|
|
317
|
-
};
|
|
318
|
-
type SessionSeamConfig = {
|
|
319
|
-
/**
|
|
320
|
-
* How long a statement may run under the ops lever, as Postgres reads it. No default: a sweep's
|
|
321
|
-
* budget is a fact about one deployment's data, and the value that fits belongs to whoever runs
|
|
322
|
-
* it. Absent, the connection's own timeout stands.
|
|
323
|
-
*/
|
|
324
|
-
opsStatementTimeout?: string;
|
|
325
|
-
settings: SessionSettings;
|
|
326
|
-
};
|
|
327
|
-
/**
|
|
328
|
-
* The seam every query crosses: one transaction, the scope named as its first statement.
|
|
329
|
-
*
|
|
330
|
-
* `set_config(..., true)` is local to the transaction, which is what makes this safe on a pool —
|
|
331
|
-
* the name is gone when the transaction ends, and a connection handed to the next request carries
|
|
332
|
-
* nothing. Naming it second would leave the statements before it running under no tenant at all.
|
|
333
|
-
*/
|
|
334
|
-
declare const createSessionSeam: (config: SessionSeamConfig) => SessionSeam;
|
|
335
|
-
|
|
336
|
-
export { type AppRoleOptions, type ConformanceCase, type Connection, type Connections, type ConnectionsConfig, DEFAULT_OPS_VALUE, DbRefusal, type Driver, type Executor, type FreezeRegister, 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, createConnections, createSessionSeam, forcedRowLevelSecurityConformance, nodePostgresDriver, perStepConnection, 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 };
|