@fuzdev/fuz_app 0.110.1 → 0.112.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/CLAUDE.md +15 -11
- package/dist/actions/action_rpc.d.ts +3 -3
- package/dist/actions/action_rpc.d.ts.map +1 -1
- package/dist/actions/action_rpc.js +4 -6
- package/dist/actions/peer_ping.d.ts +8 -0
- package/dist/actions/peer_ping.d.ts.map +1 -1
- package/dist/actions/peer_ping.js +8 -0
- package/dist/actions/peer_request.d.ts +1 -1
- package/dist/actions/peer_request.js +1 -1
- package/dist/actions/perform_action.d.ts +16 -9
- package/dist/actions/perform_action.d.ts.map +1 -1
- package/dist/actions/perform_action.js +30 -21
- package/dist/actions/register_action_ws.d.ts +1 -1
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +88 -21
- package/dist/auth/CLAUDE.md +59 -10
- 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_actions.d.ts.map +1 -1
- package/dist/auth/account_actions.js +10 -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 +70 -16
- package/dist/auth/account_schema.d.ts +27 -9
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +23 -3
- package/dist/auth/actor_search_actions.d.ts +3 -3
- package/dist/auth/actor_search_actions.d.ts.map +1 -1
- package/dist/auth/actor_search_actions.js +10 -6
- 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 +36 -8
- 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 +16 -3
- package/dist/auth/audit_log_queries.d.ts +21 -3
- package/dist/auth/audit_log_queries.d.ts.map +1 -1
- package/dist/auth/audit_log_queries.js +38 -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/cell_actions.d.ts.map +1 -1
- package/dist/auth/cell_actions.js +3 -8
- package/dist/auth/cell_audit_actions.d.ts.map +1 -1
- package/dist/auth/cell_audit_actions.js +1 -1
- package/dist/auth/cell_field_actions.js +1 -1
- package/dist/auth/cell_grant_actions.js +1 -1
- package/dist/auth/cell_item_actions.js +1 -1
- 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 +28 -7
- 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 +34 -2
- package/dist/auth/role_grant_offer_ddl.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_ddl.js +61 -2
- package/dist/auth/role_grant_offer_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_offer_queries.js +48 -53
- 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 +58 -18
- 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 +26 -6
- package/dist/db/CLAUDE.md +34 -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 +12 -3
- package/dist/db/cell_field_queries.d.ts.map +1 -1
- package/dist/db/cell_field_queries.js +25 -7
- package/dist/db/cell_grant_queries.d.ts +10 -2
- package/dist/db/cell_grant_queries.d.ts.map +1 -1
- package/dist/db/cell_grant_queries.js +29 -11
- package/dist/db/cell_item_queries.d.ts +13 -2
- package/dist/db/cell_item_queries.d.ts.map +1 -1
- package/dist/db/cell_item_queries.js +27 -7
- package/dist/db/cell_queries.d.ts +32 -3
- package/dist/db/cell_queries.d.ts.map +1 -1
- package/dist/db/cell_queries.js +57 -22
- 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 +27 -4
- 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 +144 -0
- package/dist/db/sql_columns.d.ts.map +1 -0
- package/dist/db/sql_columns.js +164 -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 +4 -2
- package/dist/http/jsonrpc_helpers.d.ts +13 -0
- package/dist/http/jsonrpc_helpers.d.ts.map +1 -1
- package/dist/http/jsonrpc_helpers.js +13 -0
- 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/server/serve_fact_route.d.ts.map +1 -1
- package/dist/server/serve_fact_route.js +7 -2
- package/dist/testing/CLAUDE.md +55 -28
- package/dist/testing/admin_integration.d.ts +0 -3
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +0 -1
- package/dist/testing/adversarial_404.d.ts.map +1 -1
- package/dist/testing/adversarial_404.js +4 -1
- package/dist/testing/adversarial_input.d.ts.map +1 -1
- package/dist/testing/adversarial_input.js +5 -4
- package/dist/testing/app_server.d.ts +4 -3
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +11 -5
- package/dist/testing/attack_surface.d.ts +16 -0
- package/dist/testing/attack_surface.d.ts.map +1 -1
- package/dist/testing/attack_surface.js +36 -13
- package/dist/testing/audit_completeness.d.ts +0 -3
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +0 -1
- package/dist/testing/auth_apps.d.ts +9 -2
- package/dist/testing/auth_apps.d.ts.map +1 -1
- package/dist/testing/auth_apps.js +9 -2
- package/dist/testing/cross_backend/account_lifecycle.d.ts +5 -4
- package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -1
- package/dist/testing/cross_backend/account_lifecycle.js +7 -0
- package/dist/testing/cross_backend/action_manifest.d.ts +19 -3
- package/dist/testing/cross_backend/action_manifest.d.ts.map +1 -1
- package/dist/testing/cross_backend/action_manifest.js +10 -4
- package/dist/testing/cross_backend/action_manifest_parity.d.ts +9 -3
- package/dist/testing/cross_backend/action_manifest_parity.d.ts.map +1 -1
- package/dist/testing/cross_backend/action_manifest_parity.js +6 -0
- package/dist/testing/cross_backend/actor_lookup.d.ts +3 -2
- package/dist/testing/cross_backend/actor_lookup.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.d.ts +4 -2
- package/dist/testing/cross_backend/body_size.d.ts.map +1 -1
- package/dist/testing/cross_backend/body_size.js +1 -1
- package/dist/testing/cross_backend/cell_crud.d.ts +2 -2
- package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_crud.js +10 -0
- package/dist/testing/cross_backend/cell_gated_create.d.ts +3 -3
- package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_grant_role.d.ts +2 -2
- package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_relations.d.ts +2 -2
- package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
- package/dist/testing/cross_backend/expected_schema.json +1 -1
- package/dist/testing/cross_backend/fact_serving.d.ts +2 -2
- package/dist/testing/cross_backend/fact_serving.d.ts.map +1 -1
- package/dist/testing/cross_backend/make_cross_backend_project.d.ts +17 -2
- package/dist/testing/cross_backend/make_cross_backend_project.d.ts.map +1 -1
- package/dist/testing/cross_backend/make_cross_backend_project.js +23 -3
- package/dist/testing/cross_backend/origin.d.ts +4 -2
- package/dist/testing/cross_backend/origin.d.ts.map +1 -1
- package/dist/testing/cross_backend/origin.js +1 -1
- package/dist/testing/cross_backend/peer_ping_ws.d.ts.map +1 -1
- package/dist/testing/cross_backend/peer_ping_ws.js +11 -7
- package/dist/testing/cross_backend/query_shape.d.ts +6 -5
- package/dist/testing/cross_backend/query_shape.d.ts.map +1 -1
- package/dist/testing/cross_backend/ready.d.ts +2 -2
- package/dist/testing/cross_backend/ready.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_enumeration.d.ts.map +1 -1
- package/dist/testing/cross_backend/role_grant_offer_enumeration.js +7 -1
- package/dist/testing/cross_backend/setup.d.ts +26 -6
- package/dist/testing/cross_backend/setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/standard.d.ts +5 -1
- package/dist/testing/cross_backend/standard.d.ts.map +1 -1
- package/dist/testing/cross_backend/standard.js +1 -5
- package/dist/testing/cross_backend/testing_backdoor.js +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +5 -0
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/token_lifetime.d.ts.map +1 -1
- package/dist/testing/cross_backend/token_lifetime.js +6 -0
- package/dist/testing/cross_backend/token_scope_surface.d.ts +2 -2
- package/dist/testing/cross_backend/token_scope_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/wire_shapes.d.ts +22 -0
- package/dist/testing/cross_backend/wire_shapes.d.ts.map +1 -0
- package/dist/testing/cross_backend/wire_shapes.js +41 -0
- package/dist/testing/data_exposure.d.ts +0 -3
- package/dist/testing/data_exposure.d.ts.map +1 -1
- package/dist/testing/data_exposure.js +0 -1
- package/dist/testing/db.d.ts +62 -7
- package/dist/testing/db.d.ts.map +1 -1
- package/dist/testing/db.js +86 -28
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +15 -2
- package/dist/testing/round_trip.d.ts +0 -3
- package/dist/testing/round_trip.d.ts.map +1 -1
- package/dist/testing/round_trip.js +0 -4
- package/dist/testing/rpc_attack_surface.js +2 -2
- package/dist/testing/rpc_round_trip.d.ts +0 -3
- package/dist/testing/rpc_round_trip.d.ts.map +1 -1
- package/dist/testing/rpc_round_trip.js +0 -1
- package/dist/testing/schema_generators.d.ts +15 -0
- package/dist/testing/schema_generators.d.ts.map +1 -1
- package/dist/testing/schema_generators.js +45 -11
- package/dist/testing/standard.d.ts +5 -1
- package/dist/testing/standard.d.ts.map +1 -1
- package/dist/testing/standard.js +2 -7
- package/dist/timestamp.d.ts +58 -0
- package/dist/timestamp.d.ts.map +1 -0
- package/dist/timestamp.js +57 -0
- package/dist/ui/AdminOverview.svelte +5 -10
- package/dist/ui/AdminOverview.svelte.d.ts.map +1 -1
- package/dist/ui/RoleGrantOfferHistory.svelte +3 -1
- package/dist/ui/RoleGrantOfferHistory.svelte.d.ts.map +1 -1
- package/dist/ui/role_grant_offers_state.svelte.d.ts.map +1 -1
- package/dist/ui/role_grant_offers_state.svelte.js +6 -2
- package/package.json +1 -1
package/dist/db/cell_queries.js
CHANGED
|
@@ -22,18 +22,53 @@
|
|
|
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 { iso8601_timestamp_expr, 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
|
+
/** The `cell` timestamp override, by row qualifier — the one `cell_row_projection` is given. */
|
|
53
|
+
const cell_expr = iso8601_timestamp_expr(CELL_COLUMNS, ['created_at', 'updated_at', 'deleted_at']);
|
|
54
|
+
/**
|
|
55
|
+
* The full `CellRow` projection — `CELL_COLUMNS` qualified by `alias` plus
|
|
56
|
+
* the derived `grant_count` (a correlated subquery against `cell_grant`,
|
|
57
|
+
* served by `idx_cell_grant_cell`) — used by every cell-row SELECT /
|
|
58
|
+
* RETURNING. `alias` is the row qualifier: the table name (`'cell'`) for
|
|
59
|
+
* single-table reads + `RETURNING`, the query alias (`'c'`) for
|
|
60
|
+
* `query_cell_list`. Twin of `fuz_cell`'s `cell_columns(alias)`.
|
|
61
|
+
*
|
|
62
|
+
* `::int` narrows the count from `bigint` so the JS row hydrates a `number`
|
|
63
|
+
* rather than a bigint primitive — counts on a single cell are trivially
|
|
64
|
+
* within int32.
|
|
31
65
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
66
|
+
* Exported for consumers writing their own cell-row reads: `CELL_COLUMNS`
|
|
67
|
+
* alone hydrates a row without `grant_count`, which typechecks as a
|
|
68
|
+
* `CellRow` but fails at `to_cell_json` — this is the projection that
|
|
69
|
+
* actually produces one.
|
|
35
70
|
*/
|
|
36
|
-
const
|
|
71
|
+
export const cell_row_projection = (alias) => `${qualify_columns(CELL_COLUMNS, alias, cell_expr(alias))}, (SELECT COUNT(*)::int FROM cell_grant WHERE cell_id = ${alias}.id) AS grant_count`;
|
|
37
72
|
/**
|
|
38
73
|
* Insert a cell row, deriving `refs` from `data`.
|
|
39
74
|
*
|
|
@@ -53,7 +88,7 @@ export const query_cell_create = async (deps, input) => {
|
|
|
53
88
|
const row = await deps.db.query_one(`INSERT INTO cell
|
|
54
89
|
(data, kind, visibility, path, refs, parent_id, root_id, moderation, created_by)
|
|
55
90
|
VALUES ($1::jsonb, $2, COALESCE($3::cell_visibility, 'private'::cell_visibility), $4, $5::text[], $6, $7, $8, $9)
|
|
56
|
-
RETURNING
|
|
91
|
+
RETURNING ${cell_row_projection('cell')}`, [
|
|
57
92
|
JSON.stringify(input.data),
|
|
58
93
|
input.kind ?? null,
|
|
59
94
|
input.visibility ?? null,
|
|
@@ -76,7 +111,7 @@ export const query_cell_create = async (deps, input) => {
|
|
|
76
111
|
*/
|
|
77
112
|
export const query_cell_get = async (deps, id, options) => {
|
|
78
113
|
const include_deleted = options?.include_deleted === true;
|
|
79
|
-
const row = await deps.db.query_one(`SELECT
|
|
114
|
+
const row = await deps.db.query_one(`SELECT ${cell_row_projection('cell')}
|
|
80
115
|
FROM cell
|
|
81
116
|
WHERE id = $1
|
|
82
117
|
AND ($2::bool OR deleted_at IS NULL)`, [id, include_deleted]);
|
|
@@ -92,7 +127,7 @@ export const query_cell_get = async (deps, id, options) => {
|
|
|
92
127
|
* @returns the row or `null` when not found
|
|
93
128
|
*/
|
|
94
129
|
export const query_cell_get_by_path = async (deps, path) => {
|
|
95
|
-
const row = await deps.db.query_one(`SELECT
|
|
130
|
+
const row = await deps.db.query_one(`SELECT ${cell_row_projection('cell')}
|
|
96
131
|
FROM cell
|
|
97
132
|
WHERE path = $1 AND deleted_at IS NULL`, [path]);
|
|
98
133
|
return row ?? null;
|
|
@@ -111,7 +146,7 @@ export const query_cell_get_by_path = async (deps, path) => {
|
|
|
111
146
|
export const query_cell_load_many = async (deps, ids) => {
|
|
112
147
|
if (ids.length === 0)
|
|
113
148
|
return [];
|
|
114
|
-
return deps.db.query(`SELECT
|
|
149
|
+
return deps.db.query(`SELECT ${cell_row_projection('cell')}
|
|
115
150
|
FROM cell
|
|
116
151
|
WHERE id = ANY($1::uuid[]) AND deleted_at IS NULL`, [ids]);
|
|
117
152
|
};
|
|
@@ -140,7 +175,7 @@ export const query_cell_update = async (deps, id, patch) => {
|
|
|
140
175
|
visibility = CASE WHEN $9::bool THEN $10::cell_visibility ELSE visibility END,
|
|
141
176
|
updated_at = NOW()
|
|
142
177
|
WHERE id = $1 AND deleted_at IS NULL
|
|
143
|
-
RETURNING
|
|
178
|
+
RETURNING ${cell_row_projection('cell')}`, [
|
|
144
179
|
id,
|
|
145
180
|
data_provided,
|
|
146
181
|
data_provided ? JSON.stringify(patch.data) : null,
|
|
@@ -199,7 +234,7 @@ export const query_cell_set_moderation = async (deps, id, moderation, options) =
|
|
|
199
234
|
updated_by = $4,
|
|
200
235
|
updated_at = NOW()
|
|
201
236
|
WHERE id = $1 AND deleted_at IS NULL
|
|
202
|
-
RETURNING
|
|
237
|
+
RETURNING ${cell_row_projection('cell')}`, [id, moderation, options.set_visibility_public, options.updated_by ?? null]);
|
|
203
238
|
return row ?? null;
|
|
204
239
|
};
|
|
205
240
|
/**
|
|
@@ -211,11 +246,11 @@ export const query_cell_set_moderation = async (deps, id, moderation, options) =
|
|
|
211
246
|
* @param options - pagination
|
|
212
247
|
* @returns matching active rows
|
|
213
248
|
*/
|
|
214
|
-
export const query_cell_list_by_kind = async (deps, kind, options) => deps.db.query(`SELECT
|
|
249
|
+
export const query_cell_list_by_kind = async (deps, kind, options) => deps.db.query(`SELECT ${cell_row_projection('cell')}
|
|
215
250
|
FROM cell
|
|
216
251
|
WHERE kind = $1
|
|
217
252
|
AND deleted_at IS NULL
|
|
218
|
-
ORDER BY created_at DESC
|
|
253
|
+
ORDER BY cell.created_at DESC
|
|
219
254
|
LIMIT $2 OFFSET $3`, [kind, options?.limit ?? null, options?.offset ?? 0]);
|
|
220
255
|
/**
|
|
221
256
|
* List active cells created by an actor, newest first. Backed by the
|
|
@@ -226,10 +261,10 @@ export const query_cell_list_by_kind = async (deps, kind, options) => deps.db.qu
|
|
|
226
261
|
* @param options - pagination
|
|
227
262
|
* @returns matching active rows
|
|
228
263
|
*/
|
|
229
|
-
export const query_cell_list_by_creator = async (deps, actor_id, options) => deps.db.query(`SELECT
|
|
264
|
+
export const query_cell_list_by_creator = async (deps, actor_id, options) => deps.db.query(`SELECT ${cell_row_projection('cell')}
|
|
230
265
|
FROM cell
|
|
231
266
|
WHERE created_by = $1 AND deleted_at IS NULL
|
|
232
|
-
ORDER BY created_at DESC
|
|
267
|
+
ORDER BY cell.created_at DESC
|
|
233
268
|
LIMIT $2 OFFSET $3`, [actor_id, options?.limit ?? null, options?.offset ?? 0]);
|
|
234
269
|
/**
|
|
235
270
|
* Filterable list query for the generic `cell_list` RPC.
|
|
@@ -365,7 +400,7 @@ const CALLER_ROLE_GRANTS_CTE = `WITH caller_role_grants AS (
|
|
|
365
400
|
)`;
|
|
366
401
|
/** General `cell_list` SQL: cell-driven scan, full visibility predicate. */
|
|
367
402
|
const build_general_sql = (order_column, order_direction) => `${CALLER_ROLE_GRANTS_CTE}
|
|
368
|
-
SELECT
|
|
403
|
+
SELECT ${cell_row_projection('c')} FROM cell c
|
|
369
404
|
WHERE ($1::bool OR c.deleted_at IS NULL)
|
|
370
405
|
AND ($2::text IS NULL OR c.kind = $2::text)
|
|
371
406
|
AND ($14::cell_visibility IS NULL OR c.visibility = $14::cell_visibility)
|
|
@@ -403,7 +438,7 @@ const build_general_sql = (order_column, order_direction) => `${CALLER_ROLE_GRAN
|
|
|
403
438
|
* "what's shared with me" wants their grant footprint, not every cell.
|
|
404
439
|
*/
|
|
405
440
|
const build_shared_with_sql = (order_column, order_direction) => `${CALLER_ROLE_GRANTS_CTE}
|
|
406
|
-
SELECT
|
|
441
|
+
SELECT ${cell_row_projection('c')} FROM cell c
|
|
407
442
|
WHERE ($1::bool OR c.deleted_at IS NULL)
|
|
408
443
|
AND ($2::text IS NULL OR c.kind = $2::text)
|
|
409
444
|
AND ($14::cell_visibility IS NULL OR c.visibility = $14::cell_visibility)
|
|
@@ -422,7 +457,7 @@ const build_shared_with_sql = (order_column, order_direction) => `${CALLER_ROLE_
|
|
|
422
457
|
-- $6 (viewer_is_admin) intentionally not consulted: an admin
|
|
423
458
|
-- asking "what's shared with me" wants their grant footprint, not
|
|
424
459
|
-- every cell. Cast-only reference keeps the param's type known to
|
|
425
|
-
-- the planner so the shared
|
|
460
|
+
-- the planner so the shared 16-param positional layout stays valid.
|
|
426
461
|
AND $6::bool IS NOT NULL
|
|
427
462
|
ORDER BY c.${order_column} ${order_direction} NULLS LAST
|
|
428
463
|
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,29 @@
|
|
|
11
11
|
*
|
|
12
12
|
* @module
|
|
13
13
|
*/
|
|
14
|
+
import { columns_sql, iso8601_timestamp_column, 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
|
+
// `created_at` is deliberately *not* projected through
|
|
30
|
+
// `iso8601_timestamp_column` here, unlike every other table: these rows feed
|
|
31
|
+
// `PgFactStore`, whose `FactMeta.created_at` is a `Date` in the
|
|
32
|
+
// `@fuzdev/fuz_util` `FactStore` contract, and nothing serializes them. The
|
|
33
|
+
// one fact timestamp that does reach the wire — the orphan-sweep sample in
|
|
34
|
+
// `query_orphan_facts_list` — projects the ISO-8601 wire string.
|
|
35
|
+
/** `FACT_COLUMNS` minus the payload — the `FactMetaRow` projection. */
|
|
36
|
+
const FACT_META_COLUMNS = omit_columns(FACT_COLUMNS, 'bytes');
|
|
14
37
|
/**
|
|
15
38
|
* Idempotently insert a fact row.
|
|
16
39
|
*
|
|
@@ -44,7 +67,7 @@ export const query_put_fact_refs = async (deps, source_hash, target_hashes) => {
|
|
|
44
67
|
* `FactStore.get`; cheaper accessors live below.
|
|
45
68
|
*/
|
|
46
69
|
export const query_get_fact = async (deps, hash) => {
|
|
47
|
-
const row = await deps.db.query_one(`SELECT
|
|
70
|
+
const row = await deps.db.query_one(`SELECT ${columns_sql(FACT_COLUMNS)}
|
|
48
71
|
FROM fact WHERE hash = $1`, [hash]);
|
|
49
72
|
return row ?? null;
|
|
50
73
|
};
|
|
@@ -52,7 +75,7 @@ export const query_get_fact = async (deps, hash) => {
|
|
|
52
75
|
* Fetch metadata only — skips the (potentially large) `bytes` column.
|
|
53
76
|
*/
|
|
54
77
|
export const query_get_fact_meta = async (deps, hash) => {
|
|
55
|
-
const row = await deps.db.query_one(`SELECT
|
|
78
|
+
const row = await deps.db.query_one(`SELECT ${columns_sql(FACT_META_COLUMNS)}
|
|
56
79
|
FROM fact WHERE hash = $1`, [hash]);
|
|
57
80
|
return row ?? null;
|
|
58
81
|
};
|
|
@@ -120,7 +143,7 @@ export const query_orphan_facts_list = async (deps, older_than, sample_limit) =>
|
|
|
120
143
|
AND c.deleted_at IS NULL
|
|
121
144
|
)
|
|
122
145
|
AND ($1::timestamptz IS NULL OR f.created_at < $1::timestamptz)`, [older_than]);
|
|
123
|
-
const sample_rows = await deps.db.query(`SELECT hash, size, created_at, external_url
|
|
146
|
+
const sample_rows = await deps.db.query(`SELECT hash, size, ${iso8601_timestamp_column('f', 'created_at')} AS created_at, external_url
|
|
124
147
|
FROM fact f
|
|
125
148
|
WHERE NOT EXISTS (
|
|
126
149
|
SELECT 1 FROM cell c
|
|
@@ -136,7 +159,7 @@ export const query_orphan_facts_list = async (deps, older_than, sample_limit) =>
|
|
|
136
159
|
sample: sample_rows.map((r) => ({
|
|
137
160
|
hash: r.hash,
|
|
138
161
|
size: Number(r.size),
|
|
139
|
-
created_at:
|
|
162
|
+
created_at: r.created_at,
|
|
140
163
|
external_url: r.external_url
|
|
141
164
|
}))
|
|
142
165
|
};
|
|
@@ -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,144 @@
|
|
|
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` / `omit_columns`.
|
|
14
|
+
*
|
|
15
|
+
* A read that needs a column rendered as something other than the bare
|
|
16
|
+
* reference passes a `ColumnExpr` — the `expr` override, twin of the third
|
|
17
|
+
* argument to `fuz_db::qualify_columns`. Every timestamp that reaches the
|
|
18
|
+
* wire projects through `iso8601_timestamp_column` so both spines serialize
|
|
19
|
+
* the same instant to the same bytes; each query module builds its override
|
|
20
|
+
* with `iso8601_timestamp_expr` over its table const. An override costs one
|
|
21
|
+
* rule — a projected `ORDER BY` must qualify its column — stated on
|
|
22
|
+
* `qualify_columns`.
|
|
23
|
+
*
|
|
24
|
+
* Placement rule: a const lives in its table's query module. When two query
|
|
25
|
+
* modules project the same table (the revoke cascades in
|
|
26
|
+
* `auth/role_grant_queries.ts` supersede `role_grant_offer` rows while the
|
|
27
|
+
* offer module's accept path writes `role_grant` rows), it lives in the
|
|
28
|
+
* table's `*_ddl.ts` instead — the module both already import — so the query
|
|
29
|
+
* modules stay acyclic.
|
|
30
|
+
*
|
|
31
|
+
* Column arrays are literal consts; the alias passed to `qualify_columns` is
|
|
32
|
+
* the only input these helpers treat as dynamic (and validate). Each const
|
|
33
|
+
* also carries `satisfies ReadonlyArray<keyof Row>` so a column name the row
|
|
34
|
+
* type doesn't know is a compile error.
|
|
35
|
+
*
|
|
36
|
+
* @module
|
|
37
|
+
*/
|
|
38
|
+
/**
|
|
39
|
+
* Per-column expression override for a projection — return the SQL to
|
|
40
|
+
* project for `column`, or `undefined` to keep the bare column reference.
|
|
41
|
+
*
|
|
42
|
+
* Twin of the `expr` closure `fuz_db::qualify_columns` takes. The Rust side
|
|
43
|
+
* decodes rows positionally, so its overrides need no output name; TS reads
|
|
44
|
+
* rows by name, so `columns_sql` / `qualify_columns` alias every override
|
|
45
|
+
* back to its column (`… AS created_at`) — otherwise a `to_char(…)`
|
|
46
|
+
* projection would arrive under Postgres' derived name and read as
|
|
47
|
+
* `undefined`.
|
|
48
|
+
*
|
|
49
|
+
* `undefined` is the *only* decline sentinel. An empty string is a returned
|
|
50
|
+
* expression, not a decline: it renders ` AS created_at` and the query fails
|
|
51
|
+
* to parse. This is documented rather than guarded — every override in the
|
|
52
|
+
* repo is a literal lookup returning a fragment or nothing, and a malformed
|
|
53
|
+
* projection failing loud at the first query beats silently swallowing an
|
|
54
|
+
* expression the caller meant to supply.
|
|
55
|
+
*/
|
|
56
|
+
export type ColumnExpr = (column: string) => string | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* Build a `to_char({alias}.{column} AT TIME ZONE 'UTC',
|
|
59
|
+
* 'YYYY-MM-DD"T"HH24:MI:SS"Z"')` fragment for embedding in a SELECT
|
|
60
|
+
* projection, via a `ColumnExpr` override.
|
|
61
|
+
*
|
|
62
|
+
* The format literal is the spine's canonical timestamp wire shape —
|
|
63
|
+
* second-precision UTC ISO-8601, exactly 20 characters — byte-identical to
|
|
64
|
+
* `fuz_db::iso8601_timestamp_column`, so the two backends serialize the same
|
|
65
|
+
* instant to the same bytes. `to_iso8601_seconds` (`timestamp.ts`) is the
|
|
66
|
+
* same shape for stamps minted in TS rather than read from Postgres.
|
|
67
|
+
*
|
|
68
|
+
* @param alias - the table alias or name to qualify the column with, or the
|
|
69
|
+
* empty string for an unqualified reference
|
|
70
|
+
* @param column - the timestamp column name
|
|
71
|
+
* @returns the `to_char(…)` fragment
|
|
72
|
+
*/
|
|
73
|
+
export declare const iso8601_timestamp_column: (alias: string, column: string) => string;
|
|
74
|
+
/**
|
|
75
|
+
* Build a table's timestamp override: a `ColumnExpr` factory that projects
|
|
76
|
+
* each of `timestamp_columns` through `iso8601_timestamp_column` and declines
|
|
77
|
+
* every other column. The result takes the row qualifier — the empty string
|
|
78
|
+
* for a bare-table read, the alias for a JOIN — so one definition per query
|
|
79
|
+
* module serves every read in it.
|
|
80
|
+
*
|
|
81
|
+
* `timestamp_columns` is typed against `columns` (and checked at build time),
|
|
82
|
+
* so a misspelled name is a compile error and a stale one throws at module
|
|
83
|
+
* load rather than silently leaving a column projected as a `Date`.
|
|
84
|
+
*
|
|
85
|
+
* @param columns - the table's `*_COLUMNS` const
|
|
86
|
+
* @param timestamp_columns - the members of `columns` that carry timestamps
|
|
87
|
+
* @returns `(alias) => ColumnExpr`
|
|
88
|
+
* @throws Error when a name in `timestamp_columns` is not in `columns`
|
|
89
|
+
*/
|
|
90
|
+
export declare const iso8601_timestamp_expr: <T extends string>(columns: ReadonlyArray<T>, timestamp_columns: ReadonlyArray<NoInfer<T>>) => ((alias: string) => ColumnExpr);
|
|
91
|
+
/**
|
|
92
|
+
* Render a `*_COLUMNS` const as a SQL select list, in projection order.
|
|
93
|
+
*
|
|
94
|
+
* @param columns - the column names
|
|
95
|
+
* @param expr - optional per-column expression override; an overridden
|
|
96
|
+
* column is aliased back to its own name
|
|
97
|
+
* @returns `a, b, c`
|
|
98
|
+
*/
|
|
99
|
+
export declare const columns_sql: (columns: ReadonlyArray<string>, expr?: ColumnExpr) => string;
|
|
100
|
+
/**
|
|
101
|
+
* Render a `*_COLUMNS` const as a SQL select list with every column qualified
|
|
102
|
+
* by a table alias, for reads that alias the table (JOINs, the `c`-aliased
|
|
103
|
+
* `cell_list` scan).
|
|
104
|
+
*
|
|
105
|
+
* The alias is interpolated into SQL, so it must be a plain identifier —
|
|
106
|
+
* every in-repo call passes a literal, but a consumer threading a computed
|
|
107
|
+
* alias through gets a thrown error instead of a malformed (or injectable)
|
|
108
|
+
* projection.
|
|
109
|
+
*
|
|
110
|
+
* **An `expr` override costs one rule: an `ORDER BY` in a projected SELECT
|
|
111
|
+
* must qualify its column.** An override is aliased back to its own name
|
|
112
|
+
* (`… AS created_at`), and Postgres resolves a bare `ORDER BY created_at`
|
|
113
|
+
* against the *output* names first, so it would sort on the `to_char(…)`
|
|
114
|
+
* text — collapsing sub-second order into ties and losing the btree index (a
|
|
115
|
+
* `Sort` node over the expression instead of an `Index Scan`). Writing
|
|
116
|
+
* `ORDER BY <alias>.created_at` (or `<table>.created_at`) names the input
|
|
117
|
+
* column and both go away. The Rust twin needs no such rule: it decodes
|
|
118
|
+
* positionally, so `fuz_db::qualify_columns` emits no alias and its bare
|
|
119
|
+
* `ORDER BY created_at` already means the column.
|
|
120
|
+
*
|
|
121
|
+
* @param columns - the column names
|
|
122
|
+
* @param alias - the table alias or name to prefix each column with
|
|
123
|
+
* @param expr - optional per-column expression override; an overridden
|
|
124
|
+
* column is aliased back to its own name
|
|
125
|
+
* @returns `alias.a, alias.b, alias.c`
|
|
126
|
+
* @throws Error when `alias` is not a valid SQL identifier
|
|
127
|
+
*/
|
|
128
|
+
export declare const qualify_columns: (columns: ReadonlyArray<string>, alias: string, expr?: ColumnExpr) => string;
|
|
129
|
+
/**
|
|
130
|
+
* Drop named columns from a `*_COLUMNS` const, preserving projection order —
|
|
131
|
+
* for a read that deliberately narrows the row (a client-safe listing, a
|
|
132
|
+
* metadata read that skips a payload column).
|
|
133
|
+
*
|
|
134
|
+
* With an `as const` column array the omitted names are checked at compile
|
|
135
|
+
* time; the runtime check covers plain `string[]` consts (a consumer's).
|
|
136
|
+
*
|
|
137
|
+
* @param columns - the column names
|
|
138
|
+
* @param omitted - column names to leave out
|
|
139
|
+
* @returns the columns without the omitted ones, in projection order
|
|
140
|
+
* @throws Error when an omitted name isn't in the list — a typo here would
|
|
141
|
+
* silently keep the column it meant to hide
|
|
142
|
+
*/
|
|
143
|
+
export declare const omit_columns: <T extends string>(columns: ReadonlyArray<T>, ...omitted: Array<NoInfer<T>>) => ReadonlyArray<T>;
|
|
144
|
+
//# 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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAIH;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAAC;AAEhE;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,wBAAwB,GAAI,OAAO,MAAM,EAAE,QAAQ,MAAM,KAAG,MAGS,CAAC;AAEnF;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,sBAAsB,GAAI,CAAC,SAAS,MAAM,EACtD,SAAS,aAAa,CAAC,CAAC,CAAC,EACzB,mBAAmB,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAC1C,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,UAAU,CAYhC,CAAC;AAqBF;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,GAAI,SAAS,aAAa,CAAC,MAAM,CAAC,EAAE,OAAO,UAAU,KAAG,MACvB,CAAC;AAE1D;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,eAAe,GAC3B,SAAS,aAAa,CAAC,MAAM,CAAC,EAC9B,OAAO,MAAM,EACb,OAAO,UAAU,KACf,MAGF,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,164 @@
|
|
|
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` / `omit_columns`.
|
|
14
|
+
*
|
|
15
|
+
* A read that needs a column rendered as something other than the bare
|
|
16
|
+
* reference passes a `ColumnExpr` — the `expr` override, twin of the third
|
|
17
|
+
* argument to `fuz_db::qualify_columns`. Every timestamp that reaches the
|
|
18
|
+
* wire projects through `iso8601_timestamp_column` so both spines serialize
|
|
19
|
+
* the same instant to the same bytes; each query module builds its override
|
|
20
|
+
* with `iso8601_timestamp_expr` over its table const. An override costs one
|
|
21
|
+
* rule — a projected `ORDER BY` must qualify its column — stated on
|
|
22
|
+
* `qualify_columns`.
|
|
23
|
+
*
|
|
24
|
+
* Placement rule: a const lives in its table's query module. When two query
|
|
25
|
+
* modules project the same table (the revoke cascades in
|
|
26
|
+
* `auth/role_grant_queries.ts` supersede `role_grant_offer` rows while the
|
|
27
|
+
* offer module's accept path writes `role_grant` rows), it lives in the
|
|
28
|
+
* table's `*_ddl.ts` instead — the module both already import — so the query
|
|
29
|
+
* modules stay acyclic.
|
|
30
|
+
*
|
|
31
|
+
* Column arrays are literal consts; the alias passed to `qualify_columns` is
|
|
32
|
+
* the only input these helpers treat as dynamic (and validate). Each const
|
|
33
|
+
* also carries `satisfies ReadonlyArray<keyof Row>` so a column name the row
|
|
34
|
+
* type doesn't know is a compile error.
|
|
35
|
+
*
|
|
36
|
+
* @module
|
|
37
|
+
*/
|
|
38
|
+
import { assert_valid_sql_identifier } from './sql_identifier.js';
|
|
39
|
+
/**
|
|
40
|
+
* Build a `to_char({alias}.{column} AT TIME ZONE 'UTC',
|
|
41
|
+
* 'YYYY-MM-DD"T"HH24:MI:SS"Z"')` fragment for embedding in a SELECT
|
|
42
|
+
* projection, via a `ColumnExpr` override.
|
|
43
|
+
*
|
|
44
|
+
* The format literal is the spine's canonical timestamp wire shape —
|
|
45
|
+
* second-precision UTC ISO-8601, exactly 20 characters — byte-identical to
|
|
46
|
+
* `fuz_db::iso8601_timestamp_column`, so the two backends serialize the same
|
|
47
|
+
* instant to the same bytes. `to_iso8601_seconds` (`timestamp.ts`) is the
|
|
48
|
+
* same shape for stamps minted in TS rather than read from Postgres.
|
|
49
|
+
*
|
|
50
|
+
* @param alias - the table alias or name to qualify the column with, or the
|
|
51
|
+
* empty string for an unqualified reference
|
|
52
|
+
* @param column - the timestamp column name
|
|
53
|
+
* @returns the `to_char(…)` fragment
|
|
54
|
+
*/
|
|
55
|
+
export const iso8601_timestamp_column = (alias, column) => alias === ''
|
|
56
|
+
? `to_char(${column} AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`
|
|
57
|
+
: `to_char(${alias}.${column} AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"')`;
|
|
58
|
+
/**
|
|
59
|
+
* Build a table's timestamp override: a `ColumnExpr` factory that projects
|
|
60
|
+
* each of `timestamp_columns` through `iso8601_timestamp_column` and declines
|
|
61
|
+
* every other column. The result takes the row qualifier — the empty string
|
|
62
|
+
* for a bare-table read, the alias for a JOIN — so one definition per query
|
|
63
|
+
* module serves every read in it.
|
|
64
|
+
*
|
|
65
|
+
* `timestamp_columns` is typed against `columns` (and checked at build time),
|
|
66
|
+
* so a misspelled name is a compile error and a stale one throws at module
|
|
67
|
+
* load rather than silently leaving a column projected as a `Date`.
|
|
68
|
+
*
|
|
69
|
+
* @param columns - the table's `*_COLUMNS` const
|
|
70
|
+
* @param timestamp_columns - the members of `columns` that carry timestamps
|
|
71
|
+
* @returns `(alias) => ColumnExpr`
|
|
72
|
+
* @throws Error when a name in `timestamp_columns` is not in `columns`
|
|
73
|
+
*/
|
|
74
|
+
export const iso8601_timestamp_expr = (columns, timestamp_columns) => {
|
|
75
|
+
for (const t of timestamp_columns) {
|
|
76
|
+
if (!columns.includes(t)) {
|
|
77
|
+
throw new Error(`iso8601_timestamp_expr: "${t}" is not in the column list "${columns.join(', ')}"`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return (alias) => (column) => timestamp_columns.includes(column)
|
|
81
|
+
? iso8601_timestamp_column(alias, column)
|
|
82
|
+
: undefined;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Render one projection entry — the `expr` override aliased back to its
|
|
86
|
+
* column name, or `fallback` when the override declines the column
|
|
87
|
+
* (`undefined`, and only `undefined` — see `ColumnExpr`).
|
|
88
|
+
*
|
|
89
|
+
* The alias this adds is what makes the `ORDER BY` rule on `qualify_columns`
|
|
90
|
+
* necessary.
|
|
91
|
+
*
|
|
92
|
+
* @param column - the column name, and the output name an override is aliased to
|
|
93
|
+
* @param fallback - what to project when the override declines — the bare
|
|
94
|
+
* column, or the alias-qualified reference
|
|
95
|
+
* @param expr - the per-column expression override, if the read has one
|
|
96
|
+
* @returns the projection entry, `<expr> AS <column>` or `fallback`
|
|
97
|
+
*/
|
|
98
|
+
const render_column = (column, fallback, expr) => {
|
|
99
|
+
const override = expr?.(column);
|
|
100
|
+
return override === undefined ? fallback : `${override} AS ${column}`;
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Render a `*_COLUMNS` const as a SQL select list, in projection order.
|
|
104
|
+
*
|
|
105
|
+
* @param columns - the column names
|
|
106
|
+
* @param expr - optional per-column expression override; an overridden
|
|
107
|
+
* column is aliased back to its own name
|
|
108
|
+
* @returns `a, b, c`
|
|
109
|
+
*/
|
|
110
|
+
export const columns_sql = (columns, expr) => columns.map((c) => render_column(c, c, expr)).join(', ');
|
|
111
|
+
/**
|
|
112
|
+
* Render a `*_COLUMNS` const as a SQL select list with every column qualified
|
|
113
|
+
* by a table alias, for reads that alias the table (JOINs, the `c`-aliased
|
|
114
|
+
* `cell_list` scan).
|
|
115
|
+
*
|
|
116
|
+
* The alias is interpolated into SQL, so it must be a plain identifier —
|
|
117
|
+
* every in-repo call passes a literal, but a consumer threading a computed
|
|
118
|
+
* alias through gets a thrown error instead of a malformed (or injectable)
|
|
119
|
+
* projection.
|
|
120
|
+
*
|
|
121
|
+
* **An `expr` override costs one rule: an `ORDER BY` in a projected SELECT
|
|
122
|
+
* must qualify its column.** An override is aliased back to its own name
|
|
123
|
+
* (`… AS created_at`), and Postgres resolves a bare `ORDER BY created_at`
|
|
124
|
+
* against the *output* names first, so it would sort on the `to_char(…)`
|
|
125
|
+
* text — collapsing sub-second order into ties and losing the btree index (a
|
|
126
|
+
* `Sort` node over the expression instead of an `Index Scan`). Writing
|
|
127
|
+
* `ORDER BY <alias>.created_at` (or `<table>.created_at`) names the input
|
|
128
|
+
* column and both go away. The Rust twin needs no such rule: it decodes
|
|
129
|
+
* positionally, so `fuz_db::qualify_columns` emits no alias and its bare
|
|
130
|
+
* `ORDER BY created_at` already means the column.
|
|
131
|
+
*
|
|
132
|
+
* @param columns - the column names
|
|
133
|
+
* @param alias - the table alias or name to prefix each column with
|
|
134
|
+
* @param expr - optional per-column expression override; an overridden
|
|
135
|
+
* column is aliased back to its own name
|
|
136
|
+
* @returns `alias.a, alias.b, alias.c`
|
|
137
|
+
* @throws Error when `alias` is not a valid SQL identifier
|
|
138
|
+
*/
|
|
139
|
+
export const qualify_columns = (columns, alias, expr) => {
|
|
140
|
+
assert_valid_sql_identifier(alias);
|
|
141
|
+
return columns.map((c) => render_column(c, `${alias}.${c}`, expr)).join(', ');
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Drop named columns from a `*_COLUMNS` const, preserving projection order —
|
|
145
|
+
* for a read that deliberately narrows the row (a client-safe listing, a
|
|
146
|
+
* metadata read that skips a payload column).
|
|
147
|
+
*
|
|
148
|
+
* With an `as const` column array the omitted names are checked at compile
|
|
149
|
+
* time; the runtime check covers plain `string[]` consts (a consumer's).
|
|
150
|
+
*
|
|
151
|
+
* @param columns - the column names
|
|
152
|
+
* @param omitted - column names to leave out
|
|
153
|
+
* @returns the columns without the omitted ones, in projection order
|
|
154
|
+
* @throws Error when an omitted name isn't in the list — a typo here would
|
|
155
|
+
* silently keep the column it meant to hide
|
|
156
|
+
*/
|
|
157
|
+
export const omit_columns = (columns, ...omitted) => {
|
|
158
|
+
for (const o of omitted) {
|
|
159
|
+
if (!columns.includes(o)) {
|
|
160
|
+
throw new Error(`omit_columns: "${o}" is not in the column list "${columns.join(', ')}"`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return columns.filter((c) => !omitted.includes(c));
|
|
164
|
+
};
|
|
@@ -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"}
|