@coffre/db 0.10.1 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dialect.d.ts +6 -0
- package/dist/dialect.js +10 -1
- package/dist/log.d.ts +64 -2
- package/dist/log.js +26 -14
- package/package.json +2 -2
- package/src/dialect.ts +10 -0
- package/src/log.ts +35 -10
package/dist/dialect.d.ts
CHANGED
|
@@ -32,6 +32,12 @@ export declare function engineOf(db: Queryable): Engine;
|
|
|
32
32
|
export declare function forUpdate<Query extends {
|
|
33
33
|
for: (strength: 'update' | 'no key update') => unknown;
|
|
34
34
|
}>(db: Queryable, query: Query, strength?: 'update' | 'no key update'): ReturnType<Query['for']>;
|
|
35
|
+
/**
|
|
36
|
+
* The rows a hand-written query answers, as the driver gives them: what
|
|
37
|
+
* Postgres's `execute` and SQLite's `all` each call a result. Column names
|
|
38
|
+
* are the query's own, unmapped.
|
|
39
|
+
*/
|
|
40
|
+
export declare function rowsOf<Row>(db: Queryable, query: SQL): Promise<Row[]>;
|
|
35
41
|
/** Insert the rows whose unique keys are free; returns how many that was. */
|
|
36
42
|
export declare function insertIfAbsent(db: Queryable, table: Table, rows: object[]): Promise<number>;
|
|
37
43
|
/**
|
package/dist/dialect.js
CHANGED
|
@@ -20,6 +20,15 @@ function forUpdate(db, query, strength = "update") {
|
|
|
20
20
|
if (engineOf(db) === "sqlite") return query;
|
|
21
21
|
return query.for(strength);
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* The rows a hand-written query answers, as the driver gives them: what
|
|
25
|
+
* Postgres's `execute` and SQLite's `all` each call a result. Column names
|
|
26
|
+
* are the query's own, unmapped.
|
|
27
|
+
*/
|
|
28
|
+
async function rowsOf(db, query) {
|
|
29
|
+
if (engineOf(db) === "postgres") return (await db.execute(query)).rows;
|
|
30
|
+
return db.all(query);
|
|
31
|
+
}
|
|
23
32
|
/** Insert the rows whose unique keys are free; returns how many that was. */
|
|
24
33
|
async function insertIfAbsent(db, table, rows) {
|
|
25
34
|
return changedRows(await db.insert(table).values(rows).onConflictDoNothing());
|
|
@@ -169,4 +178,4 @@ function canonicalTimestamp(raw) {
|
|
|
169
178
|
return `${(/* @__PURE__ */ new Date(Date.UTC(+year, +month - 1, +day, +hour, +minute, +second) - offsetMinutes * 6e4)).toISOString().slice(0, 19)}.${fraction.padEnd(6, "0")}Z`;
|
|
170
179
|
}
|
|
171
180
|
//#endregion
|
|
172
|
-
export { SNAPSHOT, ahead, canonicalTimestamp, changedRows, clock, clockMillis, engineOf, forUpdate, insertIfAbsent, isUniqueViolation, isUnreachable, migrationLedger, tombstone, truth, upsert };
|
|
181
|
+
export { SNAPSHOT, ahead, canonicalTimestamp, changedRows, clock, clockMillis, engineOf, forUpdate, insertIfAbsent, isUniqueViolation, isUnreachable, migrationLedger, rowsOf, tombstone, truth, upsert };
|
package/dist/log.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { a as Transaction, i as Queryable } from "./database-DBtn3jqp.js";
|
|
2
|
-
import { SQL } from "drizzle-orm";
|
|
2
|
+
import { Placeholder, SQL } from "drizzle-orm";
|
|
3
3
|
import { LogKey, StoredEntry } from "@coffre/core/audit";
|
|
4
4
|
//#region src/log.d.ts
|
|
5
5
|
/**
|
|
@@ -75,7 +75,25 @@ export declare function lockLogHead(tx: Transaction): Promise<{
|
|
|
75
75
|
* log as it stands, which no other append can change until this one
|
|
76
76
|
* commits. It throws to refuse.
|
|
77
77
|
*/
|
|
78
|
-
export declare function appendEntries(tx: Transaction, key: LogKey, entries: readonly NewEntry[], check?: (tx: Transaction) => Promise<void
|
|
78
|
+
export declare function appendEntries(tx: Transaction, key: LogKey, entries: readonly NewEntry[], check?: (tx: Transaction) => Promise<void>,
|
|
79
|
+
/**
|
|
80
|
+
* The head as a statement of the caller's locked it, with the log's last
|
|
81
|
+
* entry and the clock as that statement read them once it held the lock:
|
|
82
|
+
* nothing to read again.
|
|
83
|
+
*/
|
|
84
|
+
locked?: HeldHead): Promise<Appended>;
|
|
85
|
+
/** A head locked by a statement of the caller's, and what it read under the lock (`appendEntries`). */
|
|
86
|
+
export type HeldHead = {
|
|
87
|
+
head: {
|
|
88
|
+
nextSeq: bigint;
|
|
89
|
+
headHash: Buffer;
|
|
90
|
+
};
|
|
91
|
+
last: {
|
|
92
|
+
seq: bigint;
|
|
93
|
+
hash: Buffer;
|
|
94
|
+
} | null;
|
|
95
|
+
at: number;
|
|
96
|
+
};
|
|
79
97
|
/** An entry's columns, as the chain's codec reads them (`StoredEntry`). */
|
|
80
98
|
export declare function entryColumns(db: Queryable): {
|
|
81
99
|
seq: import("drizzle-orm/pg-core").PgColumn<{
|
|
@@ -492,6 +510,50 @@ export declare function newestVaultEntries(db: Queryable, action: string, limit:
|
|
|
492
510
|
* of what it says, so its MAC is not checked. One indexed read.
|
|
493
511
|
*/
|
|
494
512
|
export declare function newestVaultSeq(db: Queryable, action: string): Promise<bigint | null>;
|
|
513
|
+
/**
|
|
514
|
+
* `newestVaultSeq`'s query, to run or to read inside another, as a
|
|
515
|
+
* credential's read takes the newest `policy.change` with it: one rule for
|
|
516
|
+
* what the newest is, wherever it is read.
|
|
517
|
+
*/
|
|
518
|
+
export declare function newestSeqQuery(db: Queryable, action: string | Placeholder): Omit<import("drizzle-orm/pg-core").PgSelectBase<"audit_log", {
|
|
519
|
+
seq: import("drizzle-orm/pg-core").PgColumn<{
|
|
520
|
+
name: "seq";
|
|
521
|
+
tableName: "audit_log";
|
|
522
|
+
dataType: "bigint";
|
|
523
|
+
columnType: "PgBigInt64";
|
|
524
|
+
data: bigint;
|
|
525
|
+
driverParam: string;
|
|
526
|
+
notNull: true;
|
|
527
|
+
hasDefault: false;
|
|
528
|
+
isPrimaryKey: true;
|
|
529
|
+
isAutoincrement: false;
|
|
530
|
+
hasRuntimeDefault: false;
|
|
531
|
+
enumValues: undefined;
|
|
532
|
+
baseColumn: never;
|
|
533
|
+
identity: undefined;
|
|
534
|
+
generated: undefined;
|
|
535
|
+
}, {}, {}>;
|
|
536
|
+
}, "partial", Record<"audit_log", "not-null">, false, "where" | "orderBy" | "limit", {
|
|
537
|
+
seq: bigint;
|
|
538
|
+
}[], {
|
|
539
|
+
seq: import("drizzle-orm/pg-core").PgColumn<{
|
|
540
|
+
name: "seq";
|
|
541
|
+
tableName: "audit_log";
|
|
542
|
+
dataType: "bigint";
|
|
543
|
+
columnType: "PgBigInt64";
|
|
544
|
+
data: bigint;
|
|
545
|
+
driverParam: string;
|
|
546
|
+
notNull: true;
|
|
547
|
+
hasDefault: false;
|
|
548
|
+
isPrimaryKey: true;
|
|
549
|
+
isAutoincrement: false;
|
|
550
|
+
hasRuntimeDefault: false;
|
|
551
|
+
enumValues: undefined;
|
|
552
|
+
baseColumn: never;
|
|
553
|
+
identity: undefined;
|
|
554
|
+
generated: undefined;
|
|
555
|
+
}, {}, {}>;
|
|
556
|
+
}>, "where" | "orderBy" | "limit">;
|
|
495
557
|
/** How many entries in its name the vault reads back, a page at a time, for its own newest of a kind (`newestAuthentic`). */
|
|
496
558
|
export declare const LOOK_BACK: {
|
|
497
559
|
page: number;
|
package/dist/log.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as afterCommit } from "./commits-BuW-NL3n.js";
|
|
2
|
-
import { clockMillis, forUpdate } from "./dialect.js";
|
|
2
|
+
import { clockMillis, engineOf, forUpdate } from "./dialect.js";
|
|
3
3
|
import { a as prepared, i as tablesOf } from "./database-BadNeeN-.js";
|
|
4
4
|
import { and, asc, count, desc, eq, gt, gte, lt, lte, sql } from "drizzle-orm";
|
|
5
5
|
import { GENESIS_HASH, sealEntry } from "@coffre/core/audit";
|
|
@@ -69,13 +69,13 @@ const lockedHead = prepared((db) => {
|
|
|
69
69
|
* log as it stands, which no other append can change until this one
|
|
70
70
|
* commits. It throws to refuse.
|
|
71
71
|
*/
|
|
72
|
-
async function appendEntries(tx, key, entries, check) {
|
|
72
|
+
async function appendEntries(tx, key, entries, check, locked) {
|
|
73
73
|
if (entries.length === 0) throw new Error("appendEntries called with no entries");
|
|
74
74
|
const { auditChainHead, auditLog } = tablesOf(tx);
|
|
75
|
-
const head = await lockLogHead(tx);
|
|
75
|
+
const head = locked?.head ?? await lockLogHead(tx);
|
|
76
76
|
await check?.(tx);
|
|
77
|
-
const
|
|
78
|
-
const
|
|
77
|
+
const now = locked?.at ?? (await clockNow(tx))[0].now;
|
|
78
|
+
const last = locked === void 0 ? (await lastEntry(tx))[0] : locked.last ?? void 0;
|
|
79
79
|
const expected = last === void 0 ? {
|
|
80
80
|
nextSeq: 0n,
|
|
81
81
|
headHash: GENESIS_HASH
|
|
@@ -120,11 +120,15 @@ async function appendEntries(tx, key, entries, check) {
|
|
|
120
120
|
prevHash = hash;
|
|
121
121
|
seq += 1n;
|
|
122
122
|
}
|
|
123
|
-
await tx.insert(auditLog).values(rows)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
123
|
+
if (engineOf(tx) === "postgres" && rows.length <= ROWS_AT_ONCE) await tx.execute(sql`WITH appended AS (${tx.insert(auditLog).values(rows).getSQL()})
|
|
124
|
+
UPDATE audit_chain_head SET next_seq = ${seq}, head_hash = ${prevHash} WHERE only_row`);
|
|
125
|
+
else {
|
|
126
|
+
await tx.insert(auditLog).values(rows);
|
|
127
|
+
await tx.update(auditChainHead).set({
|
|
128
|
+
nextSeq: seq,
|
|
129
|
+
headHash: prevHash
|
|
130
|
+
}).where(eq(auditChainHead.onlyRow, true));
|
|
131
|
+
}
|
|
128
132
|
const written = {
|
|
129
133
|
nextSeq: seq,
|
|
130
134
|
headHash: prevHash
|
|
@@ -136,6 +140,8 @@ async function appendEntries(tx, key, entries, check) {
|
|
|
136
140
|
occurredAt: now
|
|
137
141
|
};
|
|
138
142
|
}
|
|
143
|
+
/** Entries one statement inserts at most, at 21 parameters each: Postgres takes 65,535. */
|
|
144
|
+
const ROWS_AT_ONCE = 3e3;
|
|
139
145
|
const clockNow = prepared((db) => db.select({ now: clockMillis(db) }).from(tablesOf(db).auditChainHead));
|
|
140
146
|
const lastEntry = prepared((db) => {
|
|
141
147
|
const { auditLog } = tablesOf(db);
|
|
@@ -260,10 +266,16 @@ async function newestVaultSeq(db, action) {
|
|
|
260
266
|
const [row] = await newestSeq(db, { action });
|
|
261
267
|
return row?.seq ?? null;
|
|
262
268
|
}
|
|
263
|
-
const newestSeq = prepared((db) =>
|
|
269
|
+
const newestSeq = prepared((db) => newestSeqQuery(db, sql.placeholder("action")));
|
|
270
|
+
/**
|
|
271
|
+
* `newestVaultSeq`'s query, to run or to read inside another, as a
|
|
272
|
+
* credential's read takes the newest `policy.change` with it: one rule for
|
|
273
|
+
* what the newest is, wherever it is read.
|
|
274
|
+
*/
|
|
275
|
+
function newestSeqQuery(db, action) {
|
|
264
276
|
const { auditLog } = tablesOf(db);
|
|
265
|
-
return db.select({ seq: auditLog.seq }).from(auditLog).where(and(eq(auditLog.author, "vault"), eq(auditLog.action,
|
|
266
|
-
}
|
|
277
|
+
return db.select({ seq: auditLog.seq }).from(auditLog).where(and(eq(auditLog.author, "vault"), eq(auditLog.action, action), eq(auditLog.decision, "allow"))).orderBy(desc(auditLog.seq)).limit(1);
|
|
278
|
+
}
|
|
267
279
|
/** How many entries in its name the vault reads back, a page at a time, for its own newest of a kind (`newestAuthentic`). */
|
|
268
280
|
const LOOK_BACK = {
|
|
269
281
|
page: 32,
|
|
@@ -294,4 +306,4 @@ async function newestAuthentic(page, check) {
|
|
|
294
306
|
return "unreached";
|
|
295
307
|
}
|
|
296
308
|
//#endregion
|
|
297
|
-
export { BATCH_BYTES, LOOK_BACK, LogHeadMismatch, LogRewound, appendEntries, entryColumns, forgetLogHeads, lockLogHead, missingEntry, newestAuthentic, newestVaultEntries, newestVaultSeq, withinBytes };
|
|
309
|
+
export { BATCH_BYTES, LOOK_BACK, LogHeadMismatch, LogRewound, appendEntries, entryColumns, forgetLogHeads, lockLogHead, missingEntry, newestAuthentic, newestSeqQuery, newestVaultEntries, newestVaultSeq, withinBytes };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coffre/db",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "coffre's database: the schema, its migrations and the connections, on Postgres or SQLite.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"drizzle-orm": "0.45.2",
|
|
80
80
|
"pg": "8.16.3",
|
|
81
81
|
"pg-connection-string": "2.14.0",
|
|
82
|
-
"@coffre/core": "0.10.
|
|
82
|
+
"@coffre/core": "0.10.2"
|
|
83
83
|
},
|
|
84
84
|
"devDependencies": {
|
|
85
85
|
"@types/node": "26.1.1",
|
package/src/dialect.ts
CHANGED
|
@@ -49,6 +49,16 @@ export function forUpdate<Query extends { for: (strength: 'update' | 'no key upd
|
|
|
49
49
|
return query.for(strength) as ReturnType<Query['for']>;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
/**
|
|
53
|
+
* The rows a hand-written query answers, as the driver gives them: what
|
|
54
|
+
* Postgres's `execute` and SQLite's `all` each call a result. Column names
|
|
55
|
+
* are the query's own, unmapped.
|
|
56
|
+
*/
|
|
57
|
+
export async function rowsOf<Row>(db: Queryable, query: SQL): Promise<Row[]> {
|
|
58
|
+
if (engineOf(db) === 'postgres') return (await (db as unknown as { execute(query: SQL): Promise<{ rows: Row[] }> }).execute(query)).rows;
|
|
59
|
+
return (db as unknown as { all<R>(query: SQL): Promise<R[]> }).all<Row>(query);
|
|
60
|
+
}
|
|
61
|
+
|
|
52
62
|
/** Insert the rows whose unique keys are free; returns how many that was. */
|
|
53
63
|
export async function insertIfAbsent(db: Queryable, table: Table, rows: object[]): Promise<number> {
|
|
54
64
|
return changedRows(await db.insert(table).values(rows as never).onConflictDoNothing());
|
package/src/log.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { GENESIS_HASH, sealEntry, type LogFields, type LogKey, type StoredEntry } from '@coffre/core/audit';
|
|
2
|
-
import { and, asc, count, desc, eq, gt, gte, lt, lte, sql, type SQL } from 'drizzle-orm';
|
|
2
|
+
import { and, asc, count, desc, eq, gt, gte, lt, lte, sql, type Placeholder, type SQL } from 'drizzle-orm';
|
|
3
3
|
|
|
4
4
|
import { afterCommit } from './commits.ts';
|
|
5
5
|
import { tablesOf, type Queryable, type Transaction } from './database.ts';
|
|
6
6
|
import { prepared } from './prepared.ts';
|
|
7
|
-
import { clockMillis, forUpdate } from './dialect.ts';
|
|
7
|
+
import { clockMillis, engineOf, forUpdate } from './dialect.ts';
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Appending to the audit log, the one table both the app and the vault
|
|
@@ -124,15 +124,21 @@ export async function appendEntries(
|
|
|
124
124
|
key: LogKey,
|
|
125
125
|
entries: readonly NewEntry[],
|
|
126
126
|
check?: (tx: Transaction) => Promise<void>,
|
|
127
|
+
/**
|
|
128
|
+
* The head as a statement of the caller's locked it, with the log's last
|
|
129
|
+
* entry and the clock as that statement read them once it held the lock:
|
|
130
|
+
* nothing to read again.
|
|
131
|
+
*/
|
|
132
|
+
locked?: HeldHead,
|
|
127
133
|
): Promise<Appended> {
|
|
128
134
|
if (entries.length === 0) throw new Error('appendEntries called with no entries');
|
|
129
135
|
const { auditChainHead, auditLog } = tablesOf(tx);
|
|
130
136
|
|
|
131
137
|
// Every append queues here, if its transaction has not already.
|
|
132
|
-
const head = await lockLogHead(tx);
|
|
138
|
+
const head = locked?.head ?? (await lockLogHead(tx));
|
|
133
139
|
await check?.(tx);
|
|
134
|
-
const
|
|
135
|
-
const
|
|
140
|
+
const now = locked?.at ?? (await clockNow(tx))[0]!.now;
|
|
141
|
+
const last = locked === undefined ? (await lastEntry(tx))[0] : locked.last ?? undefined;
|
|
136
142
|
|
|
137
143
|
const expected = last === undefined ? { nextSeq: 0n, headHash: GENESIS_HASH } : { nextSeq: last.seq + 1n, headHash: last.hash };
|
|
138
144
|
if (head.nextSeq !== expected.nextSeq || !head.headHash.equals(expected.headHash)) {
|
|
@@ -179,13 +185,25 @@ export async function appendEntries(
|
|
|
179
185
|
seq += 1n;
|
|
180
186
|
}
|
|
181
187
|
|
|
182
|
-
|
|
183
|
-
|
|
188
|
+
if (engineOf(tx) === 'postgres' && rows.length <= ROWS_AT_ONCE) {
|
|
189
|
+
// The entries and the head that names the last of them, in one statement.
|
|
190
|
+
await tx.execute(sql`WITH appended AS (${tx.insert(auditLog).values(rows).getSQL()})
|
|
191
|
+
UPDATE audit_chain_head SET next_seq = ${seq}, head_hash = ${prevHash} WHERE only_row`);
|
|
192
|
+
} else {
|
|
193
|
+
await tx.insert(auditLog).values(rows);
|
|
194
|
+
await tx.update(auditChainHead).set({ nextSeq: seq, headHash: prevHash }).where(eq(auditChainHead.onlyRow, true));
|
|
195
|
+
}
|
|
184
196
|
const written = { nextSeq: seq, headHash: prevHash };
|
|
185
197
|
afterCommit(tx, () => remember(key, written));
|
|
186
198
|
return { seqStart: head.nextSeq, ...written, occurredAt: now };
|
|
187
199
|
}
|
|
188
200
|
|
|
201
|
+
/** A head locked by a statement of the caller's, and what it read under the lock (`appendEntries`). */
|
|
202
|
+
export type HeldHead = { head: { nextSeq: bigint; headHash: Buffer }; last: { seq: bigint; hash: Buffer } | null; at: number };
|
|
203
|
+
|
|
204
|
+
/** Entries one statement inserts at most, at 21 parameters each: Postgres takes 65,535. */
|
|
205
|
+
const ROWS_AT_ONCE = 3000;
|
|
206
|
+
|
|
189
207
|
const clockNow = prepared((db) => db.select({ now: clockMillis(db) }).from(tablesOf(db).auditChainHead));
|
|
190
208
|
|
|
191
209
|
const lastEntry = prepared((db) => {
|
|
@@ -325,15 +343,22 @@ export async function newestVaultSeq(db: Queryable, action: string): Promise<big
|
|
|
325
343
|
return (row?.seq as bigint | undefined) ?? null;
|
|
326
344
|
}
|
|
327
345
|
|
|
328
|
-
const newestSeq = prepared((db) =>
|
|
346
|
+
const newestSeq = prepared((db) => newestSeqQuery(db, sql.placeholder('action')));
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* `newestVaultSeq`'s query, to run or to read inside another, as a
|
|
350
|
+
* credential's read takes the newest `policy.change` with it: one rule for
|
|
351
|
+
* what the newest is, wherever it is read.
|
|
352
|
+
*/
|
|
353
|
+
export function newestSeqQuery(db: Queryable, action: string | Placeholder) {
|
|
329
354
|
const { auditLog } = tablesOf(db);
|
|
330
355
|
return db
|
|
331
356
|
.select({ seq: auditLog.seq })
|
|
332
357
|
.from(auditLog)
|
|
333
|
-
.where(and(eq(auditLog.author, 'vault'), eq(auditLog.action,
|
|
358
|
+
.where(and(eq(auditLog.author, 'vault'), eq(auditLog.action, action), eq(auditLog.decision, 'allow')))
|
|
334
359
|
.orderBy(desc(auditLog.seq))
|
|
335
360
|
.limit(1);
|
|
336
|
-
}
|
|
361
|
+
}
|
|
337
362
|
|
|
338
363
|
/** How many entries in its name the vault reads back, a page at a time, for its own newest of a kind (`newestAuthentic`). */
|
|
339
364
|
export const LOOK_BACK = { page: 32, pages: 32 };
|