@geonosis/db 0.2.0
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/LICENSE +202 -0
- package/README.md +191 -0
- package/dist/index.cjs +674 -0
- package/dist/index.d.cts +336 -0
- package/dist/index.d.ts +336 -0
- package/dist/index.js +631 -0
- package/package.json +65 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
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 });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
DEFAULT_OPS_VALUE: () => DEFAULT_OPS_VALUE,
|
|
34
|
+
DbRefusal: () => DbRefusal,
|
|
35
|
+
appConnectionString: () => appConnectionString,
|
|
36
|
+
appRoleStatements: () => appRoleStatements,
|
|
37
|
+
concurrentTenantsConformance: () => concurrentTenantsConformance,
|
|
38
|
+
createConnections: () => createConnections,
|
|
39
|
+
createSessionSeam: () => createSessionSeam,
|
|
40
|
+
forcedRowLevelSecurityConformance: () => forcedRowLevelSecurityConformance,
|
|
41
|
+
nodePostgresDriver: () => nodePostgresDriver,
|
|
42
|
+
perStepConnection: () => perStepConnection,
|
|
43
|
+
sessionConformance: () => sessionConformance,
|
|
44
|
+
statementCensusConformance: () => statementCensusConformance,
|
|
45
|
+
tenantIsolationConformance: () => tenantIsolationConformance,
|
|
46
|
+
tenantPolicies: () => tenantPolicies
|
|
47
|
+
});
|
|
48
|
+
module.exports = __toCommonJS(index_exports);
|
|
49
|
+
|
|
50
|
+
// src/app-role.ts
|
|
51
|
+
var import_node_buffer = require("buffer");
|
|
52
|
+
var import_node_crypto = require("crypto");
|
|
53
|
+
|
|
54
|
+
// src/errors.ts
|
|
55
|
+
var DbRefusal = class extends Error {
|
|
56
|
+
constructor(message, options) {
|
|
57
|
+
super(message, options);
|
|
58
|
+
this.name = "DbRefusal";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// src/identifiers.ts
|
|
63
|
+
var quoted = (name) => `"${name.replaceAll('"', '""')}"`;
|
|
64
|
+
var literal = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
65
|
+
var qualified = (table, schema) => schema === void 0 ? quoted(table) : `${quoted(schema)}.${quoted(table)}`;
|
|
66
|
+
|
|
67
|
+
// src/app-role.ts
|
|
68
|
+
var SCRAM_ITERATIONS = 4096;
|
|
69
|
+
var SCRAM_SALT_BYTES = 16;
|
|
70
|
+
var scramVerifier = (password) => {
|
|
71
|
+
const salt = (0, import_node_crypto.randomBytes)(SCRAM_SALT_BYTES);
|
|
72
|
+
const salted = (0, import_node_crypto.pbkdf2Sync)(password, salt, SCRAM_ITERATIONS, 32, "sha256");
|
|
73
|
+
const storedKey = (0, import_node_crypto.createHash)("sha256").update((0, import_node_crypto.createHmac)("sha256", salted).update("Client Key").digest()).digest();
|
|
74
|
+
const serverKey = (0, import_node_crypto.createHmac)("sha256", salted).update("Server Key").digest();
|
|
75
|
+
return `SCRAM-SHA-256$${SCRAM_ITERATIONS}:${salt.toString("base64")}$${storedKey.toString("base64")}:${import_node_buffer.Buffer.from(serverKey).toString("base64")}`;
|
|
76
|
+
};
|
|
77
|
+
var DOLLAR_TAG = "$geonosis_app_role$";
|
|
78
|
+
var appRoleStatements = (options) => {
|
|
79
|
+
if (typeof options.role !== "string" || options.role === "") {
|
|
80
|
+
throw new DbRefusal(
|
|
81
|
+
"appRoleStatements needs a `role`, and has no default: the name an application logs in as belongs to whoever runs the database, and one baked in here would be another repo's"
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
const role = quoted(options.role);
|
|
85
|
+
const schema = quoted(options.schema ?? "public");
|
|
86
|
+
return [
|
|
87
|
+
`do ${DOLLAR_TAG} begin if not exists (select from pg_roles where rolname = ${literal(options.role)}) then create role ${role} login nosuperuser nobypassrls; end if; end ${DOLLAR_TAG}`,
|
|
88
|
+
`alter role ${role} with password ${literal(scramVerifier(options.password))}`,
|
|
89
|
+
`grant usage on schema ${schema} to ${role}`,
|
|
90
|
+
`grant select, insert, update, delete on all tables in schema ${schema} to ${role}`,
|
|
91
|
+
`grant usage, select on all sequences in schema ${schema} to ${role}`,
|
|
92
|
+
`alter default privileges in schema ${schema} grant select, insert, update, delete on tables to ${role}`,
|
|
93
|
+
`alter default privileges in schema ${schema} grant usage, select on sequences to ${role}`,
|
|
94
|
+
...options.statementTimeout === void 0 ? [] : [`alter role ${role} set statement_timeout = ${literal(options.statementTimeout)}`],
|
|
95
|
+
...options.idleInTransactionTimeout === void 0 ? [] : [
|
|
96
|
+
`alter role ${role} set idle_in_transaction_session_timeout = ${literal(options.idleInTransactionTimeout)}`
|
|
97
|
+
]
|
|
98
|
+
];
|
|
99
|
+
};
|
|
100
|
+
var appConnectionString = (ownerConnectionString, credentials) => {
|
|
101
|
+
const url = new URL(ownerConnectionString);
|
|
102
|
+
url.username = credentials.role;
|
|
103
|
+
url.password = credentials.password;
|
|
104
|
+
return url.toString();
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// src/conformance.ts
|
|
108
|
+
var import_conformance = require("@geonosis/conformance");
|
|
109
|
+
var A = "tenant_conformance_a";
|
|
110
|
+
var B = "tenant_conformance_b";
|
|
111
|
+
var CONCURRENT = [
|
|
112
|
+
"tenant_conformance_1",
|
|
113
|
+
"tenant_conformance_2",
|
|
114
|
+
"tenant_conformance_3",
|
|
115
|
+
"tenant_conformance_4",
|
|
116
|
+
"tenant_conformance_5",
|
|
117
|
+
"tenant_conformance_6",
|
|
118
|
+
"tenant_conformance_7",
|
|
119
|
+
"tenant_conformance_8"
|
|
120
|
+
];
|
|
121
|
+
var probeSql = (probe) => {
|
|
122
|
+
const on = qualified(probe.name, probe.schema);
|
|
123
|
+
const tenant = quoted(probe.tenantColumn);
|
|
124
|
+
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}` },
|
|
130
|
+
tenantsOf: (answer) => (0, import_conformance.rowsOf)(answer).map((row) => row.tenant).toSorted()
|
|
131
|
+
};
|
|
132
|
+
};
|
|
133
|
+
var written = async (subject, rows) => {
|
|
134
|
+
const probe = probeSql(subject.probe);
|
|
135
|
+
for (const [tenantId, id] of rows) {
|
|
136
|
+
await subject.seam.inTenant(
|
|
137
|
+
subject.connection,
|
|
138
|
+
tenantId,
|
|
139
|
+
(tx) => tx.execute(probe.insert(tenantId, id))
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
var tenantIsolationConformance = (subject) => {
|
|
144
|
+
const probe = probeSql(subject.probe);
|
|
145
|
+
return [
|
|
146
|
+
{
|
|
147
|
+
name: "the role the session connects as is no superuser",
|
|
148
|
+
run: async () => {
|
|
149
|
+
const [row] = (0, import_conformance.rowsOf)(
|
|
150
|
+
await subject.connection.execute({
|
|
151
|
+
text: `select current_setting('is_superuser') as super`
|
|
152
|
+
})
|
|
153
|
+
);
|
|
154
|
+
(0, import_conformance.assertIs)(
|
|
155
|
+
row?.super,
|
|
156
|
+
"off",
|
|
157
|
+
"the session connects as a superuser, and a superuser is exempt from every policy on every table \u2014 the wall is not being tested, and in production it would not be there"
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: "a tenant sees only its own rows",
|
|
163
|
+
run: async () => {
|
|
164
|
+
await subject.reset();
|
|
165
|
+
await written(subject, [
|
|
166
|
+
[A, "row_a"],
|
|
167
|
+
[B, "row_b"]
|
|
168
|
+
]);
|
|
169
|
+
const seen = await subject.seam.inTenant(
|
|
170
|
+
subject.connection,
|
|
171
|
+
A,
|
|
172
|
+
(tx) => tx.execute(probe.selectAll)
|
|
173
|
+
);
|
|
174
|
+
(0, import_conformance.assertSame)(
|
|
175
|
+
probe.tenantsOf(seen),
|
|
176
|
+
[A],
|
|
177
|
+
"a read with no predicate, inside a session naming one tenant, answered with rows belonging to another"
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: "no tenant named means no rows",
|
|
183
|
+
run: async () => {
|
|
184
|
+
await subject.reset();
|
|
185
|
+
await written(subject, [
|
|
186
|
+
[A, "row_a"],
|
|
187
|
+
[B, "row_b"]
|
|
188
|
+
]);
|
|
189
|
+
(0, import_conformance.assertSame)(
|
|
190
|
+
probe.tenantsOf(await subject.connection.execute(probe.selectAll)),
|
|
191
|
+
[],
|
|
192
|
+
"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
|
+
);
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: "a tenant cannot write into another tenant's rows",
|
|
198
|
+
run: async () => {
|
|
199
|
+
await subject.reset();
|
|
200
|
+
await (0, import_conformance.assertRefuses)(
|
|
201
|
+
() => subject.seam.inTenant(
|
|
202
|
+
subject.connection,
|
|
203
|
+
B,
|
|
204
|
+
(tx) => tx.execute(probe.insert(A, "row_forged"))
|
|
205
|
+
),
|
|
206
|
+
"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
|
+
);
|
|
208
|
+
(0, import_conformance.assertSame)(
|
|
209
|
+
probe.tenantsOf(
|
|
210
|
+
await subject.seam.inOps(subject.connection, (tx) => tx.execute(probe.selectAll))
|
|
211
|
+
),
|
|
212
|
+
[],
|
|
213
|
+
"the forged write was reported as refused and the row is there anyway"
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
name: "the ops lever sees every tenant",
|
|
219
|
+
run: async () => {
|
|
220
|
+
await subject.reset();
|
|
221
|
+
await written(subject, [
|
|
222
|
+
[A, "row_a"],
|
|
223
|
+
[B, "row_b"]
|
|
224
|
+
]);
|
|
225
|
+
(0, import_conformance.assertSame)(
|
|
226
|
+
probe.tenantsOf(
|
|
227
|
+
await subject.seam.inOps(subject.connection, (tx) => tx.execute(probe.selectAll))
|
|
228
|
+
),
|
|
229
|
+
[A, B].toSorted(),
|
|
230
|
+
"maintenance could not see across tenants, so every sweep, backfill and repair has to be run once per tenant or not at all"
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
];
|
|
235
|
+
};
|
|
236
|
+
var recorderOf = async (subject) => {
|
|
237
|
+
if (typeof subject.recording !== "function") {
|
|
238
|
+
throw new import_conformance.ConformanceFailure(
|
|
239
|
+
"this subject has no `recording`, and the 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"
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
return subject.recording();
|
|
243
|
+
};
|
|
244
|
+
var counted = (statements, pattern) => statements.filter((one) => pattern.test(one)).length;
|
|
245
|
+
var statementCensusConformance = (subject) => {
|
|
246
|
+
const probe = probeSql(subject.probe);
|
|
247
|
+
return [
|
|
248
|
+
{
|
|
249
|
+
name: "a read in a session is four statements, one of them the tenant",
|
|
250
|
+
run: async () => {
|
|
251
|
+
await subject.reset();
|
|
252
|
+
const recorded = await recorderOf(subject);
|
|
253
|
+
try {
|
|
254
|
+
await subject.seam.inTenant(recorded.connection, A, (tx) => tx.execute(probe.selectAll));
|
|
255
|
+
} finally {
|
|
256
|
+
await recorded.close();
|
|
257
|
+
}
|
|
258
|
+
const { statements } = recorded;
|
|
259
|
+
(0, import_conformance.assertIs)(
|
|
260
|
+
counted(statements, /savepoint/i),
|
|
261
|
+
0,
|
|
262
|
+
`the read opened a savepoint, which means a nested transaction per query: ${statements.join(" | ")}`
|
|
263
|
+
);
|
|
264
|
+
(0, import_conformance.assertIs)(
|
|
265
|
+
counted(statements, /set_config/i),
|
|
266
|
+
1,
|
|
267
|
+
`the tenant was named ${counted(statements, /set_config/i)} times for one read: ${statements.join(" | ")}`
|
|
268
|
+
);
|
|
269
|
+
(0, import_conformance.assertThat)(
|
|
270
|
+
statements.findIndex((one) => /set_config/i.test(one)) < statements.findIndex((one) => one.includes(subject.probe.name)),
|
|
271
|
+
`the read ran before the tenant was named, so it ran under whatever the connection carried: ${statements.join(" | ")}`
|
|
272
|
+
);
|
|
273
|
+
(0, import_conformance.assertIs)(
|
|
274
|
+
statements.length,
|
|
275
|
+
4,
|
|
276
|
+
`a read in a session is the transaction, the tenant, the read and the commit: ${statements.join(" | ")}`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
name: "a second read in that session opens no savepoint and names the tenant once",
|
|
282
|
+
run: async () => {
|
|
283
|
+
await subject.reset();
|
|
284
|
+
const recorded = await recorderOf(subject);
|
|
285
|
+
try {
|
|
286
|
+
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));
|
|
289
|
+
});
|
|
290
|
+
} finally {
|
|
291
|
+
await recorded.close();
|
|
292
|
+
}
|
|
293
|
+
const { statements } = recorded;
|
|
294
|
+
(0, import_conformance.assertIs)(
|
|
295
|
+
counted(statements, /savepoint/i),
|
|
296
|
+
0,
|
|
297
|
+
`the second read opened a savepoint: a query already inside its session re-enters it rather than nesting \u2014 ${statements.join(" | ")}`
|
|
298
|
+
);
|
|
299
|
+
(0, import_conformance.assertIs)(
|
|
300
|
+
counted(statements, /set_config/i),
|
|
301
|
+
1,
|
|
302
|
+
`the tenant was named again inside a session already open for it: ${statements.join(" | ")}`
|
|
303
|
+
);
|
|
304
|
+
(0, import_conformance.assertIs)(
|
|
305
|
+
statements.length,
|
|
306
|
+
5,
|
|
307
|
+
`two reads in one session are the transaction, the tenant, both reads and the commit: ${statements.join(" | ")}`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
];
|
|
312
|
+
};
|
|
313
|
+
var concurrentTenantsConformance = (subject) => {
|
|
314
|
+
const probe = probeSql(subject.probe);
|
|
315
|
+
return [
|
|
316
|
+
{
|
|
317
|
+
name: "concurrent writes each land under their own tenant",
|
|
318
|
+
run: async () => {
|
|
319
|
+
await subject.reset();
|
|
320
|
+
await Promise.all(
|
|
321
|
+
CONCURRENT.map(
|
|
322
|
+
(tenantId, index) => subject.seam.inTenant(
|
|
323
|
+
subject.connection,
|
|
324
|
+
tenantId,
|
|
325
|
+
(tx) => tx.execute(probe.insert(tenantId, `row_${index}`))
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
);
|
|
329
|
+
(0, import_conformance.assertSame)(
|
|
330
|
+
probe.tenantsOf(
|
|
331
|
+
await subject.seam.inOps(subject.connection, (tx) => tx.execute(probe.selectAll))
|
|
332
|
+
),
|
|
333
|
+
CONCURRENT.toSorted(),
|
|
334
|
+
"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"
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
name: "concurrent reads each see one tenant only",
|
|
340
|
+
run: async () => {
|
|
341
|
+
await subject.reset();
|
|
342
|
+
await Promise.all(
|
|
343
|
+
CONCURRENT.map(
|
|
344
|
+
(tenantId, index) => subject.seam.inTenant(
|
|
345
|
+
subject.connection,
|
|
346
|
+
tenantId,
|
|
347
|
+
(tx) => tx.execute(probe.insert(tenantId, `row_${index}`))
|
|
348
|
+
)
|
|
349
|
+
)
|
|
350
|
+
);
|
|
351
|
+
const seen = await Promise.all(
|
|
352
|
+
CONCURRENT.map(
|
|
353
|
+
(tenantId) => subject.seam.inTenant(subject.connection, tenantId, (tx) => tx.execute(probe.selectAll)).then(probe.tenantsOf)
|
|
354
|
+
)
|
|
355
|
+
);
|
|
356
|
+
(0, import_conformance.assertSame)(
|
|
357
|
+
seen,
|
|
358
|
+
CONCURRENT.map((tenantId) => [tenantId]),
|
|
359
|
+
"a read saw rows of a tenant other than the one its own session named, while other tenants were reading"
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
name: "a session refuses a second tenant",
|
|
365
|
+
run: async () => {
|
|
366
|
+
await (0, import_conformance.assertRefuses)(
|
|
367
|
+
() => subject.seam.inTenant(
|
|
368
|
+
subject.connection,
|
|
369
|
+
A,
|
|
370
|
+
(tx) => subject.seam.inTenant(tx, B, (inner) => inner.execute(probe.selectAll))
|
|
371
|
+
),
|
|
372
|
+
"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
|
+
// #211/#213: the SEAM's own refusal, by class — a driver that crashed throws too.
|
|
374
|
+
DbRefusal
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
name: "the ops lever refuses a tenant query",
|
|
380
|
+
run: async () => {
|
|
381
|
+
await (0, import_conformance.assertRefuses)(
|
|
382
|
+
() => subject.seam.inOps(
|
|
383
|
+
subject.connection,
|
|
384
|
+
(tx) => subject.seam.inTenant(tx, A, (inner) => inner.execute(probe.selectAll))
|
|
385
|
+
),
|
|
386
|
+
"a tenant query reached from an ops sweep ran anyway \u2014 with no tenant named, behind the ops policy, which is every tenant",
|
|
387
|
+
DbRefusal
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
];
|
|
392
|
+
};
|
|
393
|
+
var forcedRowLevelSecurityConformance = (subject) => [
|
|
394
|
+
{
|
|
395
|
+
name: "every guarded table forces row level security",
|
|
396
|
+
run: async () => {
|
|
397
|
+
const tables = subject.guardedTables ?? [subject.probe.name];
|
|
398
|
+
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
|
+
})
|
|
403
|
+
);
|
|
404
|
+
(0, import_conformance.assertSame)(
|
|
405
|
+
rows.map((row) => row.name).toSorted(),
|
|
406
|
+
[...tables].toSorted(),
|
|
407
|
+
"a table named as guarded is not in pg_class under that name"
|
|
408
|
+
);
|
|
409
|
+
(0, import_conformance.assertSame)(
|
|
410
|
+
rows.filter((row) => row.forced !== true).map((row) => row.name),
|
|
411
|
+
[],
|
|
412
|
+
"these tables have row level security enabled but not forced, so their owner \u2014 the migrator, and every ops connection \u2014 reads and writes every tenant"
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
];
|
|
417
|
+
var sessionConformance = (subject) => [
|
|
418
|
+
...tenantIsolationConformance(subject),
|
|
419
|
+
...statementCensusConformance(subject),
|
|
420
|
+
...concurrentTenantsConformance(subject),
|
|
421
|
+
...forcedRowLevelSecurityConformance(subject)
|
|
422
|
+
];
|
|
423
|
+
|
|
424
|
+
// src/connections.ts
|
|
425
|
+
var import_node_async_hooks = require("async_hooks");
|
|
426
|
+
var invocation = new import_node_async_hooks.AsyncLocalStorage();
|
|
427
|
+
var createConnections = (config) => {
|
|
428
|
+
const opened = () => config.open === void 0 ? config.driver.open(config.connectionString) : config.driver.open(config.connectionString, config.open);
|
|
429
|
+
return {
|
|
430
|
+
inInvocation: () => invocation.getStore() !== void 0,
|
|
431
|
+
open: opened,
|
|
432
|
+
sessionDb: () => {
|
|
433
|
+
const frame = invocation.getStore();
|
|
434
|
+
if (frame === void 0) {
|
|
435
|
+
throw new DbRefusal(
|
|
436
|
+
"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
|
+
);
|
|
438
|
+
}
|
|
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;
|
|
444
|
+
},
|
|
445
|
+
withConnection: async (run, release = (closing) => void closing) => {
|
|
446
|
+
if (invocation.getStore() !== void 0) return run();
|
|
447
|
+
const held = /* @__PURE__ */ new Map();
|
|
448
|
+
try {
|
|
449
|
+
return await invocation.run(held, run);
|
|
450
|
+
} finally {
|
|
451
|
+
release(Promise.all([...held.values()].map((one) => one.close())).then(() => void 0));
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
};
|
|
455
|
+
};
|
|
456
|
+
var perStepConnection = (connections) => ({
|
|
457
|
+
perStep: () => (body) => connections.withConnection(body)
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// src/node-postgres.ts
|
|
461
|
+
var import_promises = require("fs/promises");
|
|
462
|
+
var import_node_path = require("path");
|
|
463
|
+
var loadPg = async () => {
|
|
464
|
+
let loaded;
|
|
465
|
+
try {
|
|
466
|
+
loaded = await import("pg");
|
|
467
|
+
} catch (error) {
|
|
468
|
+
throw new DbRefusal(
|
|
469
|
+
"pg is an optional peer dependency of @geonosis/db and is not installed \u2014 `npm install pg`, or pass your own copy as `nodePostgresDriver({ pg })`",
|
|
470
|
+
{ cause: error }
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
return loaded.default ?? loaded;
|
|
474
|
+
};
|
|
475
|
+
var drivingPg = (module2) => {
|
|
476
|
+
if (typeof module2?.Pool !== "function") {
|
|
477
|
+
throw new DbRefusal(
|
|
478
|
+
"what was given as `pg` has no `Pool` \u2014 @geonosis/db opens every connection through one, so whatever answered to that name is not the driver this expects"
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
return module2;
|
|
482
|
+
};
|
|
483
|
+
var values = (statement) => statement.params === void 0 ? void 0 : [...statement.params];
|
|
484
|
+
var DEFAULT_MIGRATIONS_TABLE = "geonosis_db_migrations";
|
|
485
|
+
var nodePostgresDriver = async (options = {}) => {
|
|
486
|
+
const pg = drivingPg(options.pg ?? await loadPg());
|
|
487
|
+
const ledger = quoted(options.migrationsTable ?? DEFAULT_MIGRATIONS_TABLE);
|
|
488
|
+
const open = (connectionString, opened = {}) => {
|
|
489
|
+
const say = opened.onStatement ?? (() => {
|
|
490
|
+
});
|
|
491
|
+
const pool = new pg.Pool({
|
|
492
|
+
connectionString,
|
|
493
|
+
...opened.poolSize === void 0 ? {} : { max: opened.poolSize }
|
|
494
|
+
});
|
|
495
|
+
pool.on("error", opened.onIdleError ?? (() => {
|
|
496
|
+
}));
|
|
497
|
+
const sent = async (run, statement) => {
|
|
498
|
+
say(statement.text);
|
|
499
|
+
return run(statement.text, values(statement));
|
|
500
|
+
};
|
|
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
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
};
|
|
527
|
+
return {
|
|
528
|
+
/**
|
|
529
|
+
* The `.sql` files of a folder, in name order, each in its own transaction, each recorded so
|
|
530
|
+
* the next run skips it. The smallest thing that can apply a folder: a consumer with a migrator
|
|
531
|
+
* of their own passes their own `Driver` and this is never called.
|
|
532
|
+
*/
|
|
533
|
+
migrate: async (connectionString, migrationsFolder) => {
|
|
534
|
+
const held = open(connectionString, { poolSize: 1 });
|
|
535
|
+
try {
|
|
536
|
+
await held.connection.execute({
|
|
537
|
+
text: `create table if not exists ${ledger} (name text primary key, applied_at timestamptz not null default now())`
|
|
538
|
+
});
|
|
539
|
+
const files = (await (0, import_promises.readdir)(migrationsFolder)).filter((name) => name.endsWith(".sql")).toSorted();
|
|
540
|
+
for (const name of files) {
|
|
541
|
+
const sql = await (0, import_promises.readFile)((0, import_node_path.join)(migrationsFolder, name), "utf8");
|
|
542
|
+
await held.connection.transaction(async (tx) => {
|
|
543
|
+
const applied = await tx.execute({
|
|
544
|
+
params: [name],
|
|
545
|
+
text: `select name from ${ledger} where name = $1`
|
|
546
|
+
});
|
|
547
|
+
if (applied.rows.length > 0) return;
|
|
548
|
+
await tx.execute({ text: sql });
|
|
549
|
+
await tx.execute({ params: [name], text: `insert into ${ledger} (name) values ($1)` });
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
} finally {
|
|
553
|
+
await held.close();
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
open
|
|
557
|
+
};
|
|
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
|
+
};
|
|
658
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
659
|
+
0 && (module.exports = {
|
|
660
|
+
DEFAULT_OPS_VALUE,
|
|
661
|
+
DbRefusal,
|
|
662
|
+
appConnectionString,
|
|
663
|
+
appRoleStatements,
|
|
664
|
+
concurrentTenantsConformance,
|
|
665
|
+
createConnections,
|
|
666
|
+
createSessionSeam,
|
|
667
|
+
forcedRowLevelSecurityConformance,
|
|
668
|
+
nodePostgresDriver,
|
|
669
|
+
perStepConnection,
|
|
670
|
+
sessionConformance,
|
|
671
|
+
statementCensusConformance,
|
|
672
|
+
tenantIsolationConformance,
|
|
673
|
+
tenantPolicies
|
|
674
|
+
});
|