@fuzdev/fuz_app 0.110.0 → 0.111.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/dist/actions/action_rpc.d.ts +2 -2
- package/dist/auth/CLAUDE.md +24 -5
- package/dist/auth/account_action_specs.d.ts +0 -2
- package/dist/auth/account_action_specs.d.ts.map +1 -1
- package/dist/auth/account_queries.d.ts +22 -0
- package/dist/auth/account_queries.d.ts.map +1 -1
- package/dist/auth/account_queries.js +49 -15
- package/dist/auth/account_schema.d.ts +7 -9
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +1 -2
- package/dist/auth/admin_action_specs.d.ts +0 -2
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/api_token_queries.d.ts +9 -4
- package/dist/auth/api_token_queries.d.ts.map +1 -1
- package/dist/auth/api_token_queries.js +29 -7
- package/dist/auth/app_settings_queries.d.ts +8 -0
- package/dist/auth/app_settings_queries.d.ts.map +1 -1
- package/dist/auth/app_settings_queries.js +14 -3
- package/dist/auth/audit_log_queries.d.ts +15 -3
- package/dist/auth/audit_log_queries.d.ts.map +1 -1
- package/dist/auth/audit_log_queries.js +32 -7
- package/dist/auth/audit_log_schema.d.ts +0 -1
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/auth_ddl.d.ts +6 -0
- package/dist/auth/auth_ddl.d.ts.map +1 -1
- package/dist/auth/auth_ddl.js +9 -0
- package/dist/auth/invite_queries.d.ts +10 -0
- package/dist/auth/invite_queries.d.ts.map +1 -1
- package/dist/auth/invite_queries.js +25 -6
- package/dist/auth/migrations.d.ts.map +1 -1
- package/dist/auth/migrations.js +10 -0
- package/dist/auth/role_grant_offer_ddl.d.ts +23 -2
- package/dist/auth/role_grant_offer_ddl.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_ddl.js +43 -2
- package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_queries.js +29 -48
- package/dist/auth/role_grant_queries.d.ts +18 -0
- package/dist/auth/role_grant_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_queries.js +43 -17
- package/dist/auth/session_queries.d.ts +12 -3
- package/dist/auth/session_queries.d.ts.map +1 -1
- package/dist/auth/session_queries.js +21 -6
- package/dist/db/CLAUDE.md +15 -0
- package/dist/db/cell_audit_queries.d.ts.map +1 -1
- package/dist/db/cell_audit_queries.js +3 -1
- package/dist/db/cell_field_queries.d.ts +11 -2
- package/dist/db/cell_field_queries.d.ts.map +1 -1
- package/dist/db/cell_field_queries.js +23 -7
- package/dist/db/cell_grant_queries.d.ts +9 -1
- package/dist/db/cell_grant_queries.d.ts.map +1 -1
- package/dist/db/cell_grant_queries.js +25 -9
- package/dist/db/cell_item_queries.d.ts +12 -1
- package/dist/db/cell_item_queries.d.ts.map +1 -1
- package/dist/db/cell_item_queries.js +25 -7
- package/dist/db/cell_queries.d.ts +29 -0
- package/dist/db/cell_queries.d.ts.map +1 -1
- package/dist/db/cell_queries.js +53 -20
- package/dist/db/fact_queries.d.ts +7 -0
- package/dist/db/fact_queries.d.ts.map +1 -1
- package/dist/db/fact_queries.js +19 -2
- package/dist/db/schema_ready.d.ts +6 -1
- package/dist/db/schema_ready.d.ts.map +1 -1
- package/dist/db/schema_ready.js +6 -2
- package/dist/db/sql_columns.d.ts +67 -0
- package/dist/db/sql_columns.d.ts.map +1 -0
- package/dist/db/sql_columns.js +77 -0
- package/dist/db/sql_identifier.d.ts +6 -5
- package/dist/db/sql_identifier.d.ts.map +1 -1
- package/dist/db/sql_identifier.js +6 -5
- package/dist/http/CLAUDE.md +1 -1
- package/dist/http/route_spec.d.ts +2 -2
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +1 -2
- package/dist/testing/CLAUDE.md +2 -1
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +47 -18
- package/dist/testing/cross_backend/expected_schema.json +1 -1
- package/dist/testing/db.d.ts +18 -0
- package/dist/testing/db.d.ts.map +1 -1
- package/dist/testing/db.js +24 -1
- package/dist/ui/AdminOverview.svelte +5 -10
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/package.json +1 -1
|
@@ -20,6 +20,19 @@
|
|
|
20
20
|
* @module
|
|
21
21
|
*/
|
|
22
22
|
import { assert_row } from './assert_row.js';
|
|
23
|
+
import { columns_sql, qualify_columns } from './sql_columns.js';
|
|
24
|
+
/**
|
|
25
|
+
* The full `cell_item` column set, named explicitly so a row read fails
|
|
26
|
+
* loud on schema drift (see `CELL_COLUMNS` in `db/cell_queries.ts`). Column
|
|
27
|
+
* order mirrors the Rust twin's `item_columns(alias)` (`fuz_cell`). Keep in
|
|
28
|
+
* sync with `CellItemRow` and the `cell_item` DDL in `db/cell_ddl.ts`.
|
|
29
|
+
*/
|
|
30
|
+
export const CELL_ITEM_COLUMNS = [
|
|
31
|
+
'parent_id',
|
|
32
|
+
'position',
|
|
33
|
+
'child_id',
|
|
34
|
+
'created_at'
|
|
35
|
+
];
|
|
23
36
|
/**
|
|
24
37
|
* Insert one item row at the caller-supplied `position`.
|
|
25
38
|
*
|
|
@@ -34,7 +47,7 @@ import { assert_row } from './assert_row.js';
|
|
|
34
47
|
export const query_cell_item_insert = async (deps, input) => {
|
|
35
48
|
const row = await deps.db.query_one(`INSERT INTO cell_item (parent_id, position, child_id)
|
|
36
49
|
VALUES ($1, $2, $3)
|
|
37
|
-
RETURNING
|
|
50
|
+
RETURNING ${columns_sql(CELL_ITEM_COLUMNS)}`, [input.parent_id, input.position, input.child_id]);
|
|
38
51
|
return assert_row(row, 'INSERT INTO cell_item');
|
|
39
52
|
};
|
|
40
53
|
/**
|
|
@@ -44,7 +57,7 @@ export const query_cell_item_insert = async (deps, input) => {
|
|
|
44
57
|
* @returns the row or `null` when not found
|
|
45
58
|
*/
|
|
46
59
|
export const query_cell_item_get = async (deps, parent_id, position) => {
|
|
47
|
-
const row = await deps.db.query_one(`SELECT
|
|
60
|
+
const row = await deps.db.query_one(`SELECT ${columns_sql(CELL_ITEM_COLUMNS)} FROM cell_item WHERE parent_id = $1 AND position = $2`, [parent_id, position]);
|
|
48
61
|
return row ?? null;
|
|
49
62
|
};
|
|
50
63
|
/**
|
|
@@ -64,7 +77,7 @@ export const query_cell_item_move = async (deps, parent_id, position_old, positi
|
|
|
64
77
|
const row = await deps.db.query_one(`UPDATE cell_item
|
|
65
78
|
SET position = $3
|
|
66
79
|
WHERE parent_id = $1 AND position = $2
|
|
67
|
-
RETURNING
|
|
80
|
+
RETURNING ${columns_sql(CELL_ITEM_COLUMNS)}`, [parent_id, position_old, position_new]);
|
|
68
81
|
return row ?? null;
|
|
69
82
|
};
|
|
70
83
|
/**
|
|
@@ -76,7 +89,8 @@ export const query_cell_item_move = async (deps, parent_id, position_old, positi
|
|
|
76
89
|
* @mutates `cell_item` - deletes one row
|
|
77
90
|
*/
|
|
78
91
|
export const query_cell_item_delete = async (deps, parent_id, position) => {
|
|
79
|
-
const row = await deps.db.query_one(`DELETE FROM cell_item WHERE parent_id = $1 AND position = $2
|
|
92
|
+
const row = await deps.db.query_one(`DELETE FROM cell_item WHERE parent_id = $1 AND position = $2
|
|
93
|
+
RETURNING ${columns_sql(CELL_ITEM_COLUMNS)}`, [parent_id, position]);
|
|
80
94
|
return row ?? null;
|
|
81
95
|
};
|
|
82
96
|
/**
|
|
@@ -86,12 +100,16 @@ export const query_cell_item_delete = async (deps, parent_id, position) => {
|
|
|
86
100
|
* cells don't surface; the parent filter is the caller's responsibility
|
|
87
101
|
* (gated upstream by `can_view_cell(parent)`).
|
|
88
102
|
*
|
|
89
|
-
* @param
|
|
103
|
+
* @param deps - query deps
|
|
104
|
+
* @param parent_id - parent cell id
|
|
105
|
+
* @param options - `limit` caps the row count (passes through to SQL `LIMIT`);
|
|
106
|
+
* `position_after` resumes after a position (keyset pagination)
|
|
107
|
+
* @returns matching rows in lex `position` order
|
|
90
108
|
*/
|
|
91
109
|
export const query_cell_item_list_for_parent = async (deps, parent_id, options) => {
|
|
92
110
|
const limit = options?.limit ?? null;
|
|
93
111
|
const position_after = options?.position_after ?? null;
|
|
94
|
-
return deps.db.query(`SELECT i
|
|
112
|
+
return deps.db.query(`SELECT ${qualify_columns(CELL_ITEM_COLUMNS, 'i')} FROM cell_item i
|
|
95
113
|
JOIN cell c ON c.id = i.child_id
|
|
96
114
|
WHERE i.parent_id = $1
|
|
97
115
|
AND c.deleted_at IS NULL
|
|
@@ -111,7 +129,7 @@ export const query_cell_item_list_for_parent = async (deps, parent_id, options)
|
|
|
111
129
|
* inbound-linked child can't force an unbounded fetch + per-parent authz
|
|
112
130
|
* pass on the public, IP-rate-limited reverse endpoint.
|
|
113
131
|
*/
|
|
114
|
-
export const query_cell_item_list_for_child = async (deps, child_id, options) => deps.db.query(`SELECT i
|
|
132
|
+
export const query_cell_item_list_for_child = async (deps, child_id, options) => deps.db.query(`SELECT ${qualify_columns(CELL_ITEM_COLUMNS, 'i')} FROM cell_item i
|
|
115
133
|
JOIN cell p ON p.id = i.parent_id
|
|
116
134
|
WHERE i.child_id = $1
|
|
117
135
|
AND p.deleted_at IS NULL
|
|
@@ -56,6 +56,35 @@ export interface CellRow {
|
|
|
56
56
|
updated_by: Uuid | null;
|
|
57
57
|
grant_count: number;
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* The full `cell` column set, named explicitly so a row read fails loud on
|
|
61
|
+
* schema drift — a `SELECT *` silently omits a dropped column and the
|
|
62
|
+
* `deleted_at IS NULL`-shaped predicates on the hydrated row then misread
|
|
63
|
+
* `undefined` (see `ACCOUNT_COLUMNS` in `auth/account_queries.ts` for the
|
|
64
|
+
* outage class). Column order mirrors the Rust twin's `cell_columns(alias)`
|
|
65
|
+
* (`fuz_cell`), which decodes the same projection positionally. The derived
|
|
66
|
+
* `grant_count` is not a column — `cell_row_projection` appends it. Keep
|
|
67
|
+
* in sync with `CellRow` and the `cell` DDL in `db/cell_ddl.ts`.
|
|
68
|
+
*/
|
|
69
|
+
export declare const CELL_COLUMNS: readonly ["id", "data", "kind", "visibility", "path", "refs", "parent_id", "root_id", "moderation", "created_at", "updated_at", "deleted_at", "created_by", "updated_by"];
|
|
70
|
+
/**
|
|
71
|
+
* The full `CellRow` projection — `CELL_COLUMNS` qualified by `alias` plus
|
|
72
|
+
* the derived `grant_count` (a correlated subquery against `cell_grant`,
|
|
73
|
+
* served by `idx_cell_grant_cell`) — used by every cell-row SELECT /
|
|
74
|
+
* RETURNING. `alias` is the row qualifier: the table name (`'cell'`) for
|
|
75
|
+
* single-table reads + `RETURNING`, the query alias (`'c'`) for
|
|
76
|
+
* `query_cell_list`. Twin of `fuz_cell`'s `cell_columns(alias)`.
|
|
77
|
+
*
|
|
78
|
+
* `::int` narrows the count from `bigint` so the JS row hydrates a `number`
|
|
79
|
+
* rather than a bigint primitive — counts on a single cell are trivially
|
|
80
|
+
* within int32.
|
|
81
|
+
*
|
|
82
|
+
* Exported for consumers writing their own cell-row reads: `CELL_COLUMNS`
|
|
83
|
+
* alone hydrates a row without `grant_count`, which typechecks as a
|
|
84
|
+
* `CellRow` but fails at `to_cell_json` — this is the projection that
|
|
85
|
+
* actually produces one.
|
|
86
|
+
*/
|
|
87
|
+
export declare const cell_row_projection: (alias: string) => string;
|
|
59
88
|
/**
|
|
60
89
|
* Input for `query_cell_create`. `refs` is derived from `data`. `kind` is
|
|
61
90
|
* the write-once capability axis (`cell.kind` column) — set here at INSERT
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAC;AACrD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,wBAAwB,CAAC;AAEnD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"cell_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_queries.ts"],"names":[],"mappings":"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"}
|
package/dist/db/cell_queries.js
CHANGED
|
@@ -22,18 +22,51 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { fact_hash_extract_refs } from '@fuzdev/fuz_util/fact_hash.ts';
|
|
24
24
|
import { assert_row } from './assert_row.js';
|
|
25
|
+
import { qualify_columns } from './sql_columns.js';
|
|
25
26
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* `
|
|
29
|
-
*
|
|
30
|
-
*
|
|
27
|
+
* The full `cell` column set, named explicitly so a row read fails loud on
|
|
28
|
+
* schema drift — a `SELECT *` silently omits a dropped column and the
|
|
29
|
+
* `deleted_at IS NULL`-shaped predicates on the hydrated row then misread
|
|
30
|
+
* `undefined` (see `ACCOUNT_COLUMNS` in `auth/account_queries.ts` for the
|
|
31
|
+
* outage class). Column order mirrors the Rust twin's `cell_columns(alias)`
|
|
32
|
+
* (`fuz_cell`), which decodes the same projection positionally. The derived
|
|
33
|
+
* `grant_count` is not a column — `cell_row_projection` appends it. Keep
|
|
34
|
+
* in sync with `CellRow` and the `cell` DDL in `db/cell_ddl.ts`.
|
|
35
|
+
*/
|
|
36
|
+
export const CELL_COLUMNS = [
|
|
37
|
+
'id',
|
|
38
|
+
'data',
|
|
39
|
+
'kind',
|
|
40
|
+
'visibility',
|
|
41
|
+
'path',
|
|
42
|
+
'refs',
|
|
43
|
+
'parent_id',
|
|
44
|
+
'root_id',
|
|
45
|
+
'moderation',
|
|
46
|
+
'created_at',
|
|
47
|
+
'updated_at',
|
|
48
|
+
'deleted_at',
|
|
49
|
+
'created_by',
|
|
50
|
+
'updated_by'
|
|
51
|
+
];
|
|
52
|
+
/**
|
|
53
|
+
* The full `CellRow` projection — `CELL_COLUMNS` qualified by `alias` plus
|
|
54
|
+
* the derived `grant_count` (a correlated subquery against `cell_grant`,
|
|
55
|
+
* served by `idx_cell_grant_cell`) — used by every cell-row SELECT /
|
|
56
|
+
* RETURNING. `alias` is the row qualifier: the table name (`'cell'`) for
|
|
57
|
+
* single-table reads + `RETURNING`, the query alias (`'c'`) for
|
|
58
|
+
* `query_cell_list`. Twin of `fuz_cell`'s `cell_columns(alias)`.
|
|
59
|
+
*
|
|
60
|
+
* `::int` narrows the count from `bigint` so the JS row hydrates a `number`
|
|
61
|
+
* rather than a bigint primitive — counts on a single cell are trivially
|
|
62
|
+
* within int32.
|
|
31
63
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
64
|
+
* Exported for consumers writing their own cell-row reads: `CELL_COLUMNS`
|
|
65
|
+
* alone hydrates a row without `grant_count`, which typechecks as a
|
|
66
|
+
* `CellRow` but fails at `to_cell_json` — this is the projection that
|
|
67
|
+
* actually produces one.
|
|
35
68
|
*/
|
|
36
|
-
const
|
|
69
|
+
export const cell_row_projection = (alias) => `${qualify_columns(CELL_COLUMNS, alias)}, (SELECT COUNT(*)::int FROM cell_grant WHERE cell_id = ${alias}.id) AS grant_count`;
|
|
37
70
|
/**
|
|
38
71
|
* Insert a cell row, deriving `refs` from `data`.
|
|
39
72
|
*
|
|
@@ -53,7 +86,7 @@ export const query_cell_create = async (deps, input) => {
|
|
|
53
86
|
const row = await deps.db.query_one(`INSERT INTO cell
|
|
54
87
|
(data, kind, visibility, path, refs, parent_id, root_id, moderation, created_by)
|
|
55
88
|
VALUES ($1::jsonb, $2, COALESCE($3::cell_visibility, 'private'::cell_visibility), $4, $5::text[], $6, $7, $8, $9)
|
|
56
|
-
RETURNING
|
|
89
|
+
RETURNING ${cell_row_projection('cell')}`, [
|
|
57
90
|
JSON.stringify(input.data),
|
|
58
91
|
input.kind ?? null,
|
|
59
92
|
input.visibility ?? null,
|
|
@@ -76,7 +109,7 @@ export const query_cell_create = async (deps, input) => {
|
|
|
76
109
|
*/
|
|
77
110
|
export const query_cell_get = async (deps, id, options) => {
|
|
78
111
|
const include_deleted = options?.include_deleted === true;
|
|
79
|
-
const row = await deps.db.query_one(`SELECT
|
|
112
|
+
const row = await deps.db.query_one(`SELECT ${cell_row_projection('cell')}
|
|
80
113
|
FROM cell
|
|
81
114
|
WHERE id = $1
|
|
82
115
|
AND ($2::bool OR deleted_at IS NULL)`, [id, include_deleted]);
|
|
@@ -92,7 +125,7 @@ export const query_cell_get = async (deps, id, options) => {
|
|
|
92
125
|
* @returns the row or `null` when not found
|
|
93
126
|
*/
|
|
94
127
|
export const query_cell_get_by_path = async (deps, path) => {
|
|
95
|
-
const row = await deps.db.query_one(`SELECT
|
|
128
|
+
const row = await deps.db.query_one(`SELECT ${cell_row_projection('cell')}
|
|
96
129
|
FROM cell
|
|
97
130
|
WHERE path = $1 AND deleted_at IS NULL`, [path]);
|
|
98
131
|
return row ?? null;
|
|
@@ -111,7 +144,7 @@ export const query_cell_get_by_path = async (deps, path) => {
|
|
|
111
144
|
export const query_cell_load_many = async (deps, ids) => {
|
|
112
145
|
if (ids.length === 0)
|
|
113
146
|
return [];
|
|
114
|
-
return deps.db.query(`SELECT
|
|
147
|
+
return deps.db.query(`SELECT ${cell_row_projection('cell')}
|
|
115
148
|
FROM cell
|
|
116
149
|
WHERE id = ANY($1::uuid[]) AND deleted_at IS NULL`, [ids]);
|
|
117
150
|
};
|
|
@@ -140,7 +173,7 @@ export const query_cell_update = async (deps, id, patch) => {
|
|
|
140
173
|
visibility = CASE WHEN $9::bool THEN $10::cell_visibility ELSE visibility END,
|
|
141
174
|
updated_at = NOW()
|
|
142
175
|
WHERE id = $1 AND deleted_at IS NULL
|
|
143
|
-
RETURNING
|
|
176
|
+
RETURNING ${cell_row_projection('cell')}`, [
|
|
144
177
|
id,
|
|
145
178
|
data_provided,
|
|
146
179
|
data_provided ? JSON.stringify(patch.data) : null,
|
|
@@ -199,7 +232,7 @@ export const query_cell_set_moderation = async (deps, id, moderation, options) =
|
|
|
199
232
|
updated_by = $4,
|
|
200
233
|
updated_at = NOW()
|
|
201
234
|
WHERE id = $1 AND deleted_at IS NULL
|
|
202
|
-
RETURNING
|
|
235
|
+
RETURNING ${cell_row_projection('cell')}`, [id, moderation, options.set_visibility_public, options.updated_by ?? null]);
|
|
203
236
|
return row ?? null;
|
|
204
237
|
};
|
|
205
238
|
/**
|
|
@@ -211,7 +244,7 @@ export const query_cell_set_moderation = async (deps, id, moderation, options) =
|
|
|
211
244
|
* @param options - pagination
|
|
212
245
|
* @returns matching active rows
|
|
213
246
|
*/
|
|
214
|
-
export const query_cell_list_by_kind = async (deps, kind, options) => deps.db.query(`SELECT
|
|
247
|
+
export const query_cell_list_by_kind = async (deps, kind, options) => deps.db.query(`SELECT ${cell_row_projection('cell')}
|
|
215
248
|
FROM cell
|
|
216
249
|
WHERE kind = $1
|
|
217
250
|
AND deleted_at IS NULL
|
|
@@ -226,7 +259,7 @@ export const query_cell_list_by_kind = async (deps, kind, options) => deps.db.qu
|
|
|
226
259
|
* @param options - pagination
|
|
227
260
|
* @returns matching active rows
|
|
228
261
|
*/
|
|
229
|
-
export const query_cell_list_by_creator = async (deps, actor_id, options) => deps.db.query(`SELECT
|
|
262
|
+
export const query_cell_list_by_creator = async (deps, actor_id, options) => deps.db.query(`SELECT ${cell_row_projection('cell')}
|
|
230
263
|
FROM cell
|
|
231
264
|
WHERE created_by = $1 AND deleted_at IS NULL
|
|
232
265
|
ORDER BY created_at DESC
|
|
@@ -365,7 +398,7 @@ const CALLER_ROLE_GRANTS_CTE = `WITH caller_role_grants AS (
|
|
|
365
398
|
)`;
|
|
366
399
|
/** General `cell_list` SQL: cell-driven scan, full visibility predicate. */
|
|
367
400
|
const build_general_sql = (order_column, order_direction) => `${CALLER_ROLE_GRANTS_CTE}
|
|
368
|
-
SELECT
|
|
401
|
+
SELECT ${cell_row_projection('c')} FROM cell c
|
|
369
402
|
WHERE ($1::bool OR c.deleted_at IS NULL)
|
|
370
403
|
AND ($2::text IS NULL OR c.kind = $2::text)
|
|
371
404
|
AND ($14::cell_visibility IS NULL OR c.visibility = $14::cell_visibility)
|
|
@@ -403,7 +436,7 @@ const build_general_sql = (order_column, order_direction) => `${CALLER_ROLE_GRAN
|
|
|
403
436
|
* "what's shared with me" wants their grant footprint, not every cell.
|
|
404
437
|
*/
|
|
405
438
|
const build_shared_with_sql = (order_column, order_direction) => `${CALLER_ROLE_GRANTS_CTE}
|
|
406
|
-
SELECT
|
|
439
|
+
SELECT ${cell_row_projection('c')} FROM cell c
|
|
407
440
|
WHERE ($1::bool OR c.deleted_at IS NULL)
|
|
408
441
|
AND ($2::text IS NULL OR c.kind = $2::text)
|
|
409
442
|
AND ($14::cell_visibility IS NULL OR c.visibility = $14::cell_visibility)
|
|
@@ -422,7 +455,7 @@ const build_shared_with_sql = (order_column, order_direction) => `${CALLER_ROLE_
|
|
|
422
455
|
-- $6 (viewer_is_admin) intentionally not consulted: an admin
|
|
423
456
|
-- asking "what's shared with me" wants their grant footprint, not
|
|
424
457
|
-- every cell. Cast-only reference keeps the param's type known to
|
|
425
|
-
-- the planner so the shared
|
|
458
|
+
-- the planner so the shared 16-param positional layout stays valid.
|
|
426
459
|
AND $6::bool IS NOT NULL
|
|
427
460
|
ORDER BY c.${order_column} ${order_direction} NULLS LAST
|
|
428
461
|
LIMIT $8 OFFSET $9`;
|
|
@@ -22,6 +22,13 @@ export interface FactRow {
|
|
|
22
22
|
size: number | string;
|
|
23
23
|
created_at: Date;
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* The full `fact` column set, named explicitly so a row read fails loud on
|
|
27
|
+
* schema drift (see `ACCOUNT_COLUMNS` in `auth/account_queries.ts` for the
|
|
28
|
+
* outage class). Keep in sync with `FactRow` and the `fact` DDL in
|
|
29
|
+
* `db/fact_ddl.ts`.
|
|
30
|
+
*/
|
|
31
|
+
export declare const FACT_COLUMNS: readonly ["hash", "bytes", "external_url", "content_type", "size", "created_at"];
|
|
25
32
|
/** Subset returned by metadata-only queries (no `bytes` payload). */
|
|
26
33
|
export interface FactMetaRow {
|
|
27
34
|
hash: FactHash;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fact_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/fact_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAEjD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"fact_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/fact_queries.ts"],"names":[],"mappings":"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"}
|
package/dist/db/fact_queries.js
CHANGED
|
@@ -11,6 +11,23 @@
|
|
|
11
11
|
*
|
|
12
12
|
* @module
|
|
13
13
|
*/
|
|
14
|
+
import { columns_sql, omit_columns } from './sql_columns.js';
|
|
15
|
+
/**
|
|
16
|
+
* The full `fact` column set, named explicitly so a row read fails loud on
|
|
17
|
+
* schema drift (see `ACCOUNT_COLUMNS` in `auth/account_queries.ts` for the
|
|
18
|
+
* outage class). Keep in sync with `FactRow` and the `fact` DDL in
|
|
19
|
+
* `db/fact_ddl.ts`.
|
|
20
|
+
*/
|
|
21
|
+
export const FACT_COLUMNS = [
|
|
22
|
+
'hash',
|
|
23
|
+
'bytes',
|
|
24
|
+
'external_url',
|
|
25
|
+
'content_type',
|
|
26
|
+
'size',
|
|
27
|
+
'created_at'
|
|
28
|
+
];
|
|
29
|
+
/** `FACT_COLUMNS` minus the payload — the `FactMetaRow` projection. */
|
|
30
|
+
const FACT_META_COLUMNS = omit_columns(FACT_COLUMNS, 'bytes');
|
|
14
31
|
/**
|
|
15
32
|
* Idempotently insert a fact row.
|
|
16
33
|
*
|
|
@@ -44,7 +61,7 @@ export const query_put_fact_refs = async (deps, source_hash, target_hashes) => {
|
|
|
44
61
|
* `FactStore.get`; cheaper accessors live below.
|
|
45
62
|
*/
|
|
46
63
|
export const query_get_fact = async (deps, hash) => {
|
|
47
|
-
const row = await deps.db.query_one(`SELECT
|
|
64
|
+
const row = await deps.db.query_one(`SELECT ${columns_sql(FACT_COLUMNS)}
|
|
48
65
|
FROM fact WHERE hash = $1`, [hash]);
|
|
49
66
|
return row ?? null;
|
|
50
67
|
};
|
|
@@ -52,7 +69,7 @@ export const query_get_fact = async (deps, hash) => {
|
|
|
52
69
|
* Fetch metadata only — skips the (potentially large) `bytes` column.
|
|
53
70
|
*/
|
|
54
71
|
export const query_get_fact_meta = async (deps, hash) => {
|
|
55
|
-
const row = await deps.db.query_one(`SELECT
|
|
72
|
+
const row = await deps.db.query_one(`SELECT ${columns_sql(FACT_META_COLUMNS)}
|
|
56
73
|
FROM fact WHERE hash = $1`, [hash]);
|
|
57
74
|
return row ?? null;
|
|
58
75
|
};
|
|
@@ -47,9 +47,14 @@ export type ExpectedSchema = Record<string, ReadonlyArray<string>>;
|
|
|
47
47
|
* never-migrated DB then correctly fails readiness instead of passing on an
|
|
48
48
|
* empty expectation.
|
|
49
49
|
*
|
|
50
|
+
* @param db - the database to introspect
|
|
51
|
+
* @param options - `table` narrows the scan to one relation (the drift guard's
|
|
52
|
+
* one-table question); omitted, every `public` relation is returned
|
|
50
53
|
* @returns relation name → sorted column names
|
|
51
54
|
*/
|
|
52
|
-
export declare const query_public_columns: (db: Db
|
|
55
|
+
export declare const query_public_columns: (db: Db, options?: {
|
|
56
|
+
table?: string;
|
|
57
|
+
}) => Promise<Record<string, Array<string>>>;
|
|
53
58
|
/** Columns the live DB is missing for a table the expected schema declares. */
|
|
54
59
|
export interface MissingColumns {
|
|
55
60
|
table: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema_ready.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/schema_ready.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAElC,iFAAiF;AACjF,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC;AAOnE
|
|
1
|
+
{"version":3,"file":"schema_ready.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/schema_ready.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAElC,iFAAiF;AACjF,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC;AAOnE;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,oBAAoB,GAChC,IAAI,EAAE,EACN,UAAU;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,KAC1B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAavC,CAAC;AAEF,+EAA+E;AAC/E,MAAM,WAAW,cAAc;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;CACvB;AAED,uCAAuC;AACvC,MAAM,WAAW,iBAAiB;IACjC,EAAE,EAAE,OAAO,CAAC;IACZ,+CAA+C;IAC/C,cAAc,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC9B,6EAA6E;IAC7E,eAAe,EAAE,KAAK,CAAC,cAAc,CAAC,CAAC;CACvC;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,kBAAkB,GAC9B,IAAI,EAAE,EACN,UAAU,cAAc,KACtB,OAAO,CAAC,iBAAiB,CAmB3B,CAAC;AAEF,qEAAqE;AACrE,eAAO,MAAM,mBAAmB,GAAI,OAAO,iBAAiB,KAAG,MAO9D,CAAC;AAEF,sFAAsF;AACtF,eAAO,MAAM,WAAW;;;CAGd,CAAC"}
|
package/dist/db/schema_ready.js
CHANGED
|
@@ -44,12 +44,16 @@
|
|
|
44
44
|
* never-migrated DB then correctly fails readiness instead of passing on an
|
|
45
45
|
* empty expectation.
|
|
46
46
|
*
|
|
47
|
+
* @param db - the database to introspect
|
|
48
|
+
* @param options - `table` narrows the scan to one relation (the drift guard's
|
|
49
|
+
* one-table question); omitted, every `public` relation is returned
|
|
47
50
|
* @returns relation name → sorted column names
|
|
48
51
|
*/
|
|
49
|
-
export const query_public_columns = async (db) => {
|
|
52
|
+
export const query_public_columns = async (db, options) => {
|
|
50
53
|
const rows = await db.query(`SELECT table_name, column_name FROM information_schema.columns
|
|
51
54
|
WHERE table_schema = 'public'
|
|
52
|
-
|
|
55
|
+
AND ($1::text IS NULL OR table_name = $1)
|
|
56
|
+
ORDER BY table_name, column_name`, [options?.table ?? null]);
|
|
53
57
|
const by_table = {};
|
|
54
58
|
for (const { table_name, column_name } of rows) {
|
|
55
59
|
(by_table[table_name] ??= []).push(column_name);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for the named-column projection consts (`ACCOUNT_COLUMNS`,
|
|
3
|
+
* `CELL_COLUMNS`, …) — per-table column-name arrays that every table read
|
|
4
|
+
* projects through so a dropped or leftover column fails loud instead of
|
|
5
|
+
* vanishing under `SELECT *`.
|
|
6
|
+
*
|
|
7
|
+
* Each query module exports its table's const and derives every projection
|
|
8
|
+
* it needs from it at the read site — `columns_sql` for single-table reads,
|
|
9
|
+
* `qualify_columns` for reads that alias the table, `omit_columns` for a
|
|
10
|
+
* client-safe subset — rather than spelling a second column list, so the
|
|
11
|
+
* drift guard on the base const (`assert_columns_match_live` in
|
|
12
|
+
* `testing/db.ts`) covers every derived projection too. Twin of the Rust
|
|
13
|
+
* spine's `*_COLUMNS` consts + `fuz_db::qualify_columns`.
|
|
14
|
+
*
|
|
15
|
+
* Placement rule: a const lives in its table's query module. When two query
|
|
16
|
+
* modules project the same table (the revoke cascades in
|
|
17
|
+
* `auth/role_grant_queries.ts` supersede `role_grant_offer` rows while the
|
|
18
|
+
* offer module's accept path writes `role_grant` rows), it lives in the
|
|
19
|
+
* table's `*_ddl.ts` instead — the module both already import — so the query
|
|
20
|
+
* modules stay acyclic.
|
|
21
|
+
*
|
|
22
|
+
* Column arrays are literal consts; the alias passed to `qualify_columns` is
|
|
23
|
+
* the only input these helpers treat as dynamic (and validate). Each const
|
|
24
|
+
* also carries `satisfies ReadonlyArray<keyof Row>` so a column name the row
|
|
25
|
+
* type doesn't know is a compile error.
|
|
26
|
+
*
|
|
27
|
+
* @module
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Render a `*_COLUMNS` const as a SQL select list, in projection order.
|
|
31
|
+
*
|
|
32
|
+
* @param columns - the column names
|
|
33
|
+
* @returns `a, b, c`
|
|
34
|
+
*/
|
|
35
|
+
export declare const columns_sql: (columns: ReadonlyArray<string>) => string;
|
|
36
|
+
/**
|
|
37
|
+
* Render a `*_COLUMNS` const as a SQL select list with every column qualified
|
|
38
|
+
* by a table alias, for reads that alias the table (JOINs, the `c`-aliased
|
|
39
|
+
* `cell_list` scan).
|
|
40
|
+
*
|
|
41
|
+
* The alias is interpolated into SQL, so it must be a plain identifier —
|
|
42
|
+
* every in-repo call passes a literal, but a consumer threading a computed
|
|
43
|
+
* alias through gets a thrown error instead of a malformed (or injectable)
|
|
44
|
+
* projection.
|
|
45
|
+
*
|
|
46
|
+
* @param columns - the column names
|
|
47
|
+
* @param alias - the table alias or name to prefix each column with
|
|
48
|
+
* @returns `alias.a, alias.b, alias.c`
|
|
49
|
+
* @throws Error when `alias` is not a valid SQL identifier
|
|
50
|
+
*/
|
|
51
|
+
export declare const qualify_columns: (columns: ReadonlyArray<string>, alias: string) => string;
|
|
52
|
+
/**
|
|
53
|
+
* Drop named columns from a `*_COLUMNS` const, preserving projection order —
|
|
54
|
+
* for a read that deliberately narrows the row (a client-safe listing, a
|
|
55
|
+
* metadata read that skips a payload column).
|
|
56
|
+
*
|
|
57
|
+
* With an `as const` column array the omitted names are checked at compile
|
|
58
|
+
* time; the runtime check covers plain `string[]` consts (a consumer's).
|
|
59
|
+
*
|
|
60
|
+
* @param columns - the column names
|
|
61
|
+
* @param omitted - column names to leave out
|
|
62
|
+
* @returns the columns without the omitted ones, in projection order
|
|
63
|
+
* @throws Error when an omitted name isn't in the list — a typo here would
|
|
64
|
+
* silently keep the column it meant to hide
|
|
65
|
+
*/
|
|
66
|
+
export declare const omit_columns: <T extends string>(columns: ReadonlyArray<T>, ...omitted: Array<NoInfer<T>>) => ReadonlyArray<T>;
|
|
67
|
+
//# sourceMappingURL=sql_columns.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql_columns.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/sql_columns.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAIH;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GAAI,SAAS,aAAa,CAAC,MAAM,CAAC,KAAG,MAA4B,CAAC;AAE1F;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,eAAe,GAAI,SAAS,aAAa,CAAC,MAAM,CAAC,EAAE,OAAO,MAAM,KAAG,MAG/E,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,YAAY,GAAI,CAAC,SAAS,MAAM,EAC5C,SAAS,aAAa,CAAC,CAAC,CAAC,EACzB,GAAG,SAAS,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAC3B,aAAa,CAAC,CAAC,CAOjB,CAAC"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helpers for the named-column projection consts (`ACCOUNT_COLUMNS`,
|
|
3
|
+
* `CELL_COLUMNS`, …) — per-table column-name arrays that every table read
|
|
4
|
+
* projects through so a dropped or leftover column fails loud instead of
|
|
5
|
+
* vanishing under `SELECT *`.
|
|
6
|
+
*
|
|
7
|
+
* Each query module exports its table's const and derives every projection
|
|
8
|
+
* it needs from it at the read site — `columns_sql` for single-table reads,
|
|
9
|
+
* `qualify_columns` for reads that alias the table, `omit_columns` for a
|
|
10
|
+
* client-safe subset — rather than spelling a second column list, so the
|
|
11
|
+
* drift guard on the base const (`assert_columns_match_live` in
|
|
12
|
+
* `testing/db.ts`) covers every derived projection too. Twin of the Rust
|
|
13
|
+
* spine's `*_COLUMNS` consts + `fuz_db::qualify_columns`.
|
|
14
|
+
*
|
|
15
|
+
* Placement rule: a const lives in its table's query module. When two query
|
|
16
|
+
* modules project the same table (the revoke cascades in
|
|
17
|
+
* `auth/role_grant_queries.ts` supersede `role_grant_offer` rows while the
|
|
18
|
+
* offer module's accept path writes `role_grant` rows), it lives in the
|
|
19
|
+
* table's `*_ddl.ts` instead — the module both already import — so the query
|
|
20
|
+
* modules stay acyclic.
|
|
21
|
+
*
|
|
22
|
+
* Column arrays are literal consts; the alias passed to `qualify_columns` is
|
|
23
|
+
* the only input these helpers treat as dynamic (and validate). Each const
|
|
24
|
+
* also carries `satisfies ReadonlyArray<keyof Row>` so a column name the row
|
|
25
|
+
* type doesn't know is a compile error.
|
|
26
|
+
*
|
|
27
|
+
* @module
|
|
28
|
+
*/
|
|
29
|
+
import { assert_valid_sql_identifier } from './sql_identifier.js';
|
|
30
|
+
/**
|
|
31
|
+
* Render a `*_COLUMNS` const as a SQL select list, in projection order.
|
|
32
|
+
*
|
|
33
|
+
* @param columns - the column names
|
|
34
|
+
* @returns `a, b, c`
|
|
35
|
+
*/
|
|
36
|
+
export const columns_sql = (columns) => columns.join(', ');
|
|
37
|
+
/**
|
|
38
|
+
* Render a `*_COLUMNS` const as a SQL select list with every column qualified
|
|
39
|
+
* by a table alias, for reads that alias the table (JOINs, the `c`-aliased
|
|
40
|
+
* `cell_list` scan).
|
|
41
|
+
*
|
|
42
|
+
* The alias is interpolated into SQL, so it must be a plain identifier —
|
|
43
|
+
* every in-repo call passes a literal, but a consumer threading a computed
|
|
44
|
+
* alias through gets a thrown error instead of a malformed (or injectable)
|
|
45
|
+
* projection.
|
|
46
|
+
*
|
|
47
|
+
* @param columns - the column names
|
|
48
|
+
* @param alias - the table alias or name to prefix each column with
|
|
49
|
+
* @returns `alias.a, alias.b, alias.c`
|
|
50
|
+
* @throws Error when `alias` is not a valid SQL identifier
|
|
51
|
+
*/
|
|
52
|
+
export const qualify_columns = (columns, alias) => {
|
|
53
|
+
assert_valid_sql_identifier(alias);
|
|
54
|
+
return columns_sql(columns.map((c) => `${alias}.${c}`));
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Drop named columns from a `*_COLUMNS` const, preserving projection order —
|
|
58
|
+
* for a read that deliberately narrows the row (a client-safe listing, a
|
|
59
|
+
* metadata read that skips a payload column).
|
|
60
|
+
*
|
|
61
|
+
* With an `as const` column array the omitted names are checked at compile
|
|
62
|
+
* time; the runtime check covers plain `string[]` consts (a consumer's).
|
|
63
|
+
*
|
|
64
|
+
* @param columns - the column names
|
|
65
|
+
* @param omitted - column names to leave out
|
|
66
|
+
* @returns the columns without the omitted ones, in projection order
|
|
67
|
+
* @throws Error when an omitted name isn't in the list — a typo here would
|
|
68
|
+
* silently keep the column it meant to hide
|
|
69
|
+
*/
|
|
70
|
+
export const omit_columns = (columns, ...omitted) => {
|
|
71
|
+
for (const o of omitted) {
|
|
72
|
+
if (!columns.includes(o)) {
|
|
73
|
+
throw new Error(`omit_columns: "${o}" is not in the column list "${columns.join(', ')}"`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return columns.filter((c) => !omitted.includes(c));
|
|
77
|
+
};
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SQL identifier validation for
|
|
2
|
+
* SQL identifier validation for the places an identifier must be
|
|
3
|
+
* interpolated rather than parameterized.
|
|
3
4
|
*
|
|
4
|
-
* PostgreSQL
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* PostgreSQL parameterizes values only — table and column names in DDL
|
|
6
|
+
* (DROP TABLE, TRUNCATE, ALTER) and the alias `qualify_columns` prefixes
|
|
7
|
+
* onto a projection (`db/sql_columns.ts`) have to be string-interpolated.
|
|
8
|
+
* This validator ensures such identifiers are safe to interpolate.
|
|
8
9
|
*
|
|
9
10
|
* @module
|
|
10
11
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sql_identifier.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/sql_identifier.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"sql_identifier.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/sql_identifier.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH;;;GAGG;AACH,eAAO,MAAM,oBAAoB,QAA6B,CAAC;AAE/D;;;;;;;;;;GAUG;AACH,eAAO,MAAM,2BAA2B,GAAI,MAAM,MAAM,KAAG,MAK1D,CAAC"}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SQL identifier validation for
|
|
2
|
+
* SQL identifier validation for the places an identifier must be
|
|
3
|
+
* interpolated rather than parameterized.
|
|
3
4
|
*
|
|
4
|
-
* PostgreSQL
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* PostgreSQL parameterizes values only — table and column names in DDL
|
|
6
|
+
* (DROP TABLE, TRUNCATE, ALTER) and the alias `qualify_columns` prefixes
|
|
7
|
+
* onto a projection (`db/sql_columns.ts`) have to be string-interpolated.
|
|
8
|
+
* This validator ensures such identifiers are safe to interpolate.
|
|
8
9
|
*
|
|
9
10
|
* @module
|
|
10
11
|
*/
|
package/dist/http/CLAUDE.md
CHANGED
|
@@ -71,7 +71,7 @@ interface RouteContext {
|
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
- **`route.db`** — handler's main DB work. Wrapped in a transaction when `transaction: true` (default for non-GET); routes that opt out (`transaction: false`, e.g. signup / bootstrap) get the pool here directly and may call `route.db.transaction(...)` for their own scope.
|
|
74
|
-
- **`route.pending_effects`** — direct push for eager fire-and-forget pool writes (audit,
|
|
74
|
+
- **`route.pending_effects`** — direct push for eager fire-and-forget pool writes (audit, api-token usage tracking).
|
|
75
75
|
- **`route.post_commit_effects`** — do not push directly; reach for `emit_after_commit` from `http/pending_effects.ts`. The helper pushes a thunk that the flush middleware invokes after the handler returns, closing the microtask-ordering window that an eager `Promise.resolve().then(fn)` leaves open inside the wrapping `db.transaction`.
|
|
76
76
|
|
|
77
77
|
Pool-level fire-and-forget writes (audit logs, etc.) run through the bound
|
|
@@ -83,8 +83,8 @@ export interface RouteContext {
|
|
|
83
83
|
db: Db;
|
|
84
84
|
/**
|
|
85
85
|
* Eager fire-and-forget queue — push the in-flight `Promise<void>` for
|
|
86
|
-
* pool writes already running (audit emits,
|
|
87
|
-
*
|
|
86
|
+
* pool writes already running (audit emits, api-token usage
|
|
87
|
+
* tracking). The flush middleware drains via
|
|
88
88
|
* `flush_pending_effects` after the handler returns.
|
|
89
89
|
*/
|
|
90
90
|
pending_effects: Array<Promise<void>>;
|