@fuzdev/fuz_app 0.67.1 → 0.69.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/perform_action.d.ts.map +1 -1
- package/dist/actions/perform_action.js +10 -3
- package/dist/auth/CLAUDE.md +99 -5
- package/dist/auth/account_queries.d.ts +87 -4
- package/dist/auth/account_queries.d.ts.map +1 -1
- package/dist/auth/account_queries.js +107 -17
- package/dist/auth/account_schema.d.ts +19 -0
- package/dist/auth/account_schema.d.ts.map +1 -1
- package/dist/auth/account_schema.js +8 -0
- package/dist/auth/admin_action_specs.d.ts +170 -3
- package/dist/auth/admin_action_specs.d.ts.map +1 -1
- package/dist/auth/admin_action_specs.js +148 -4
- package/dist/auth/admin_actions.d.ts +4 -14
- package/dist/auth/admin_actions.d.ts.map +1 -1
- package/dist/auth/admin_actions.js +246 -40
- package/dist/auth/audit_log_ddl.d.ts +10 -1
- package/dist/auth/audit_log_ddl.d.ts.map +1 -1
- package/dist/auth/audit_log_ddl.js +13 -4
- package/dist/auth/audit_log_schema.d.ts +34 -1
- package/dist/auth/audit_log_schema.d.ts.map +1 -1
- package/dist/auth/audit_log_schema.js +73 -0
- package/dist/auth/auth_ddl.d.ts +2 -2
- package/dist/auth/auth_ddl.d.ts.map +1 -1
- package/dist/auth/auth_ddl.js +10 -2
- package/dist/auth/cell_action_specs.d.ts +1295 -0
- package/dist/auth/cell_action_specs.d.ts.map +1 -0
- package/dist/auth/cell_action_specs.js +397 -0
- package/dist/auth/cell_actions.d.ts +63 -0
- package/dist/auth/cell_actions.d.ts.map +1 -0
- package/dist/auth/cell_actions.js +546 -0
- package/dist/auth/cell_audit_action_specs.d.ts +131 -0
- package/dist/auth/cell_audit_action_specs.d.ts.map +1 -0
- package/dist/auth/cell_audit_action_specs.js +70 -0
- package/dist/auth/cell_audit_actions.d.ts +18 -0
- package/dist/auth/cell_audit_actions.d.ts.map +1 -0
- package/dist/auth/cell_audit_actions.js +59 -0
- package/dist/auth/cell_audit_events.d.ts +28 -0
- package/dist/auth/cell_audit_events.d.ts.map +1 -0
- package/dist/auth/cell_audit_events.js +42 -0
- package/dist/auth/cell_audit_metadata.d.ts +48 -0
- package/dist/auth/cell_audit_metadata.d.ts.map +1 -0
- package/dist/auth/cell_audit_metadata.js +46 -0
- package/dist/auth/cell_authorize.d.ts +88 -0
- package/dist/auth/cell_authorize.d.ts.map +1 -0
- package/dist/auth/cell_authorize.js +172 -0
- package/dist/auth/cell_data_schema.d.ts +44 -0
- package/dist/auth/cell_data_schema.d.ts.map +1 -0
- package/dist/auth/cell_data_schema.js +42 -0
- package/dist/auth/cell_field_action_specs.d.ts +244 -0
- package/dist/auth/cell_field_action_specs.d.ts.map +1 -0
- package/dist/auth/cell_field_action_specs.js +136 -0
- package/dist/auth/cell_field_actions.d.ts +34 -0
- package/dist/auth/cell_field_actions.d.ts.map +1 -0
- package/dist/auth/cell_field_actions.js +153 -0
- package/dist/auth/cell_field_audit_metadata.d.ts +30 -0
- package/dist/auth/cell_field_audit_metadata.d.ts.map +1 -0
- package/dist/auth/cell_field_audit_metadata.js +28 -0
- package/dist/auth/cell_grant_action_specs.d.ts +333 -0
- package/dist/auth/cell_grant_action_specs.d.ts.map +1 -0
- package/dist/auth/cell_grant_action_specs.js +148 -0
- package/dist/auth/cell_grant_actions.d.ts +50 -0
- package/dist/auth/cell_grant_actions.d.ts.map +1 -0
- package/dist/auth/cell_grant_actions.js +208 -0
- package/dist/auth/cell_grant_audit_metadata.d.ts +75 -0
- package/dist/auth/cell_grant_audit_metadata.d.ts.map +1 -0
- package/dist/auth/cell_grant_audit_metadata.js +54 -0
- package/dist/auth/cell_item_action_specs.d.ts +331 -0
- package/dist/auth/cell_item_action_specs.d.ts.map +1 -0
- package/dist/auth/cell_item_action_specs.js +182 -0
- package/dist/auth/cell_item_actions.d.ts +37 -0
- package/dist/auth/cell_item_actions.d.ts.map +1 -0
- package/dist/auth/cell_item_actions.js +204 -0
- package/dist/auth/cell_item_audit_metadata.d.ts +35 -0
- package/dist/auth/cell_item_audit_metadata.d.ts.map +1 -0
- package/dist/auth/cell_item_audit_metadata.js +32 -0
- package/dist/auth/cell_relation_visibility.d.ts +32 -0
- package/dist/auth/cell_relation_visibility.d.ts.map +1 -0
- package/dist/auth/cell_relation_visibility.js +57 -0
- package/dist/auth/deps.d.ts +9 -0
- package/dist/auth/deps.d.ts.map +1 -1
- package/dist/auth/role_grant_queries.d.ts +30 -0
- package/dist/auth/role_grant_queries.d.ts.map +1 -1
- package/dist/auth/role_grant_queries.js +54 -0
- package/dist/auth/signup_routes.d.ts +0 -3
- package/dist/auth/signup_routes.d.ts.map +1 -1
- package/dist/auth/signup_routes.js +9 -3
- package/dist/auth/standard_rpc_actions.d.ts +5 -5
- package/dist/auth/standard_rpc_actions.js +4 -4
- package/dist/db/CLAUDE.md +118 -0
- package/dist/db/cell_audit_queries.d.ts +26 -0
- package/dist/db/cell_audit_queries.d.ts.map +1 -0
- package/dist/db/cell_audit_queries.js +53 -0
- package/dist/db/cell_ddl.d.ts +151 -0
- package/dist/db/cell_ddl.d.ts.map +1 -0
- package/dist/db/cell_ddl.js +247 -0
- package/dist/db/cell_field_queries.d.ts +105 -0
- package/dist/db/cell_field_queries.d.ts.map +1 -0
- package/dist/db/cell_field_queries.js +113 -0
- package/dist/db/cell_grant_queries.d.ts +132 -0
- package/dist/db/cell_grant_queries.d.ts.map +1 -0
- package/dist/db/cell_grant_queries.js +145 -0
- package/dist/db/cell_history_ddl.d.ts +38 -0
- package/dist/db/cell_history_ddl.d.ts.map +1 -0
- package/dist/db/cell_history_ddl.js +61 -0
- package/dist/db/cell_item_queries.d.ts +107 -0
- package/dist/db/cell_item_queries.d.ts.map +1 -0
- package/dist/db/cell_item_queries.js +119 -0
- package/dist/db/cell_queries.d.ts +327 -0
- package/dist/db/cell_queries.d.ts.map +1 -0
- package/dist/db/cell_queries.js +431 -0
- package/dist/db/fact_ddl.d.ts +38 -0
- package/dist/db/fact_ddl.d.ts.map +1 -0
- package/dist/db/fact_ddl.js +71 -0
- package/dist/db/fact_queries.d.ts +140 -0
- package/dist/db/fact_queries.d.ts.map +1 -0
- package/dist/db/fact_queries.js +161 -0
- package/dist/db/fact_store.d.ts +112 -0
- package/dist/db/fact_store.d.ts.map +1 -0
- package/dist/db/fact_store.js +225 -0
- package/dist/server/app_server.d.ts +1 -7
- package/dist/server/app_server.d.ts.map +1 -1
- package/dist/server/app_server.js +1 -5
- package/dist/server/env.d.ts +2 -0
- package/dist/server/env.d.ts.map +1 -1
- package/dist/server/env.js +6 -0
- package/dist/server/fact_write.d.ts +32 -0
- package/dist/server/fact_write.d.ts.map +1 -0
- package/dist/server/fact_write.js +56 -0
- package/dist/server/file_fact_fetcher.d.ts +42 -0
- package/dist/server/file_fact_fetcher.d.ts.map +1 -0
- package/dist/server/file_fact_fetcher.js +60 -0
- package/dist/server/file_fact_url.d.ts +53 -0
- package/dist/server/file_fact_url.d.ts.map +1 -0
- package/dist/server/file_fact_url.js +52 -0
- package/dist/server/serve_fact_route.d.ts +78 -0
- package/dist/server/serve_fact_route.d.ts.map +1 -0
- package/dist/server/serve_fact_route.js +205 -0
- package/dist/testing/CLAUDE.md +142 -6
- package/dist/testing/app_server.d.ts +46 -0
- package/dist/testing/app_server.d.ts.map +1 -1
- package/dist/testing/app_server.js +67 -8
- package/dist/testing/audit_completeness.d.ts.map +1 -1
- package/dist/testing/audit_completeness.js +67 -1
- package/dist/testing/cross_backend/account_lifecycle.d.ts +10 -0
- package/dist/testing/cross_backend/account_lifecycle.d.ts.map +1 -0
- package/dist/testing/cross_backend/account_lifecycle.js +144 -0
- package/dist/testing/cross_backend/actor_lookup.d.ts +10 -0
- package/dist/testing/cross_backend/actor_lookup.d.ts.map +1 -0
- package/dist/testing/cross_backend/actor_lookup.js +83 -0
- package/dist/testing/cross_backend/actor_search.d.ts +6 -0
- package/dist/testing/cross_backend/actor_search.d.ts.map +1 -0
- package/dist/testing/cross_backend/actor_search.js +92 -0
- package/dist/testing/cross_backend/app_settings.d.ts +6 -0
- package/dist/testing/cross_backend/app_settings.d.ts.map +1 -0
- package/dist/testing/cross_backend/app_settings.js +95 -0
- package/dist/testing/cross_backend/backend_config.d.ts +1 -1
- package/dist/testing/cross_backend/capabilities.d.ts +29 -7
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +3 -1
- package/dist/testing/cross_backend/cell_cross_helpers.d.ts +39 -0
- package/dist/testing/cross_backend/cell_cross_helpers.d.ts.map +1 -0
- package/dist/testing/cross_backend/cell_cross_helpers.js +45 -0
- package/dist/testing/cross_backend/cell_crud.d.ts +4 -0
- package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -0
- package/dist/testing/cross_backend/cell_crud.js +168 -0
- package/dist/testing/cross_backend/cell_grant_role.d.ts +8 -0
- package/dist/testing/cross_backend/cell_grant_role.d.ts.map +1 -0
- package/dist/testing/cross_backend/cell_grant_role.js +102 -0
- package/dist/testing/cross_backend/cell_relations.d.ts +4 -0
- package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -0
- package/dist/testing/cross_backend/cell_relations.js +229 -0
- package/dist/testing/cross_backend/conformance_case.d.ts +144 -0
- package/dist/testing/cross_backend/conformance_case.d.ts.map +1 -0
- package/dist/testing/cross_backend/conformance_case.js +132 -0
- package/dist/testing/cross_backend/conformance_table.d.ts +46 -0
- package/dist/testing/cross_backend/conformance_table.d.ts.map +1 -0
- package/dist/testing/cross_backend/conformance_table.js +199 -0
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +6 -2
- package/dist/testing/cross_backend/default_spine_surface.d.ts +17 -9
- package/dist/testing/cross_backend/default_spine_surface.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_spine_surface.js +20 -12
- package/dist/testing/cross_backend/origin.d.ts +10 -0
- package/dist/testing/cross_backend/origin.d.ts.map +1 -0
- package/dist/testing/cross_backend/origin.js +73 -0
- package/dist/testing/cross_backend/setup.d.ts +22 -40
- package/dist/testing/cross_backend/setup.d.ts.map +1 -1
- package/dist/testing/cross_backend/setup.js +39 -5
- package/dist/testing/cross_backend/testing_reset_actions.d.ts +90 -2
- package/dist/testing/cross_backend/testing_reset_actions.d.ts.map +1 -1
- package/dist/testing/cross_backend/testing_reset_actions.js +91 -3
- package/dist/testing/cross_backend/xfail.d.ts +15 -0
- package/dist/testing/cross_backend/xfail.d.ts.map +1 -0
- package/dist/testing/cross_backend/xfail.js +37 -0
- package/dist/testing/entities.d.ts.map +1 -1
- package/dist/testing/entities.js +4 -0
- package/dist/testing/integration.d.ts +2 -3
- package/dist/testing/integration.d.ts.map +1 -1
- package/dist/testing/integration.js +20 -85
- package/dist/testing/rate_limiting.d.ts +1 -1
- package/dist/testing/rpc_helpers.d.ts +3 -3
- package/dist/testing/sse_round_trip.d.ts +1 -1
- package/dist/testing/stubs.d.ts.map +1 -1
- package/dist/testing/stubs.js +0 -1
- package/dist/testing/ws_round_trip.d.ts.map +1 -1
- package/dist/testing/ws_round_trip.js +4 -0
- package/dist/ui/AdminAccounts.svelte +84 -35
- package/dist/ui/AdminAccounts.svelte.d.ts.map +1 -1
- package/dist/ui/AdminSessions.svelte +21 -23
- package/dist/ui/AdminSessions.svelte.d.ts.map +1 -1
- package/dist/ui/CLAUDE.md +17 -26
- package/dist/ui/OpenSignupToggle.svelte +2 -5
- package/dist/ui/OpenSignupToggle.svelte.d.ts.map +1 -1
- package/dist/ui/account_sessions_state.svelte.d.ts +9 -10
- package/dist/ui/account_sessions_state.svelte.d.ts.map +1 -1
- package/dist/ui/account_sessions_state.svelte.js +7 -17
- package/dist/ui/admin_accounts_state.svelte.d.ts +41 -20
- package/dist/ui/admin_accounts_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_accounts_state.svelte.js +52 -22
- package/dist/ui/admin_invites_state.svelte.d.ts +8 -11
- package/dist/ui/admin_invites_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_invites_state.svelte.js +7 -16
- package/dist/ui/admin_rpc_adapters.d.ts +6 -2
- package/dist/ui/admin_rpc_adapters.d.ts.map +1 -1
- package/dist/ui/admin_rpc_adapters.js +5 -1
- package/dist/ui/admin_sessions_state.svelte.d.ts +6 -10
- package/dist/ui/admin_sessions_state.svelte.d.ts.map +1 -1
- package/dist/ui/admin_sessions_state.svelte.js +4 -14
- package/dist/ui/app_settings_state.svelte.d.ts +8 -12
- package/dist/ui/app_settings_state.svelte.d.ts.map +1 -1
- package/dist/ui/app_settings_state.svelte.js +6 -16
- package/dist/ui/audit_log_state.svelte.d.ts +9 -8
- package/dist/ui/audit_log_state.svelte.d.ts.map +1 -1
- package/dist/ui/audit_log_state.svelte.js +8 -20
- package/package.json +2 -2
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw queries against the `cell_grant` table.
|
|
3
|
+
*
|
|
4
|
+
* Resource-side ACL for cells: each row admits a principal at a `level`
|
|
5
|
+
* (`viewer` | `editor`). Principal is discriminated by which columns are
|
|
6
|
+
* set — `actor_id` (single actor) xor `(role, scope_id?)` (any holder
|
|
7
|
+
* of a matching role_grant). Owner is implicit on `cell.created_by` and never
|
|
8
|
+
* appears in this table.
|
|
9
|
+
*
|
|
10
|
+
* Convention: `deps: QueryDeps` first, no audit side effects, mutations
|
|
11
|
+
* return the affected row (or `null` for not-found).
|
|
12
|
+
*
|
|
13
|
+
* `query_cell_grant_create` upserts on the relevant partial unique index so
|
|
14
|
+
* re-granting the same principal updates `level` rather than producing
|
|
15
|
+
* duplicate rows. The two principal shapes use different indexes:
|
|
16
|
+
*
|
|
17
|
+
* - Actor-shaped: `idx_cell_grant_unique_actor` on `(cell_id, actor_id)`.
|
|
18
|
+
* - Role-shaped: `idx_cell_grant_unique_role_scope` on `(cell_id, role, scope_id)`
|
|
19
|
+
* with `NULLS NOT DISTINCT` so two `(role, NULL)` grants on the same cell
|
|
20
|
+
* collide.
|
|
21
|
+
*
|
|
22
|
+
* @module
|
|
23
|
+
*/
|
|
24
|
+
import { assert_row } from './assert_row.js';
|
|
25
|
+
/**
|
|
26
|
+
* Insert a grant, or update the existing row's `level` + `granted_by` when
|
|
27
|
+
* one already exists for the same `(cell_id, principal)` pair.
|
|
28
|
+
*
|
|
29
|
+
* Idempotent re-share: caller doesn't need to check existence first. The
|
|
30
|
+
* UPSERT path runs even when the existing row's level matches — handlers
|
|
31
|
+
* reading the row's prior state for audit ("create vs. update") must do
|
|
32
|
+
* so before this call.
|
|
33
|
+
*
|
|
34
|
+
* @param deps - query deps
|
|
35
|
+
* @param input - cell, level, principal, grantor
|
|
36
|
+
* @returns the inserted-or-updated row
|
|
37
|
+
* @mutates `cell_grant` - inserts or updates one row
|
|
38
|
+
*/
|
|
39
|
+
export const query_cell_grant_create = async (deps, input) => {
|
|
40
|
+
const { cell_id, level, principal, granted_by } = input;
|
|
41
|
+
if (principal.kind === 'actor') {
|
|
42
|
+
const row = await deps.db.query_one(`INSERT INTO cell_grant (cell_id, level, actor_id, granted_by)
|
|
43
|
+
VALUES ($1, $2, $3, $4)
|
|
44
|
+
ON CONFLICT (cell_id, actor_id) WHERE actor_id IS NOT NULL
|
|
45
|
+
DO UPDATE SET level = EXCLUDED.level, granted_by = EXCLUDED.granted_by
|
|
46
|
+
RETURNING *`, [cell_id, level, principal.actor_id, granted_by]);
|
|
47
|
+
return assert_row(row, 'INSERT INTO cell_grant (actor)');
|
|
48
|
+
}
|
|
49
|
+
const row = await deps.db.query_one(`INSERT INTO cell_grant (cell_id, level, role, scope_id, granted_by)
|
|
50
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
51
|
+
ON CONFLICT (cell_id, role, scope_id) WHERE role IS NOT NULL
|
|
52
|
+
DO UPDATE SET level = EXCLUDED.level, granted_by = EXCLUDED.granted_by
|
|
53
|
+
RETURNING *`, [cell_id, level, principal.role, principal.scope_id, granted_by]);
|
|
54
|
+
return assert_row(row, 'INSERT INTO cell_grant (role)');
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Fetch a grant by id.
|
|
58
|
+
*
|
|
59
|
+
* @param deps - query deps
|
|
60
|
+
* @param grant_id - grant id
|
|
61
|
+
* @returns the row or `null` when not found
|
|
62
|
+
*/
|
|
63
|
+
export const query_cell_grant_get = async (deps, grant_id) => {
|
|
64
|
+
const row = await deps.db.query_one(`SELECT * FROM cell_grant WHERE id = $1`, [
|
|
65
|
+
grant_id,
|
|
66
|
+
]);
|
|
67
|
+
return row ?? null;
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Delete a grant by id, returning the deleted row.
|
|
71
|
+
*
|
|
72
|
+
* Returning the row lets the caller audit the principal + level after the
|
|
73
|
+
* delete and (for self-revoke) recompute `still_admitted` against the
|
|
74
|
+
* remaining grants on the cell without a second fetch.
|
|
75
|
+
*
|
|
76
|
+
* @param deps - query deps
|
|
77
|
+
* @param grant_id - grant id
|
|
78
|
+
* @returns the deleted row or `null` when no row matched
|
|
79
|
+
* @mutates `cell_grant` - deletes one row
|
|
80
|
+
*/
|
|
81
|
+
export const query_cell_grant_delete = async (deps, grant_id) => {
|
|
82
|
+
const row = await deps.db.query_one(`DELETE FROM cell_grant WHERE id = $1 RETURNING *`, [grant_id]);
|
|
83
|
+
return row ?? null;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* List all grants on a cell, oldest first.
|
|
87
|
+
*
|
|
88
|
+
* Used by `cell_grant_list` (RPC) and by handlers that need grants
|
|
89
|
+
* alongside the cell row for the authorize predicate.
|
|
90
|
+
*
|
|
91
|
+
* @param deps - query deps
|
|
92
|
+
* @param cell_id - cell id
|
|
93
|
+
* @returns matching rows
|
|
94
|
+
*/
|
|
95
|
+
export const query_cell_grant_list_for_cell = async (deps, cell_id) => deps.db.query(`SELECT * FROM cell_grant
|
|
96
|
+
WHERE cell_id = $1
|
|
97
|
+
ORDER BY created_at ASC`, [cell_id]);
|
|
98
|
+
/**
|
|
99
|
+
* List all grants across a set of cells, ordered by cell then creation.
|
|
100
|
+
* Used by the strict relation-read filter to test `can_view_cell` per
|
|
101
|
+
* target in memory — the caller groups the flat result by `cell_id`.
|
|
102
|
+
* Returns **every** grant on each cell (not caller-filtered), because
|
|
103
|
+
* `can_view_cell` needs the full grant list to decide admission.
|
|
104
|
+
*
|
|
105
|
+
* @param deps - query deps
|
|
106
|
+
* @param cell_ids - cells to fetch grants for (duplicates are harmless)
|
|
107
|
+
* @returns matching grant rows (group by `cell_id` caller-side)
|
|
108
|
+
*/
|
|
109
|
+
export const query_cell_grant_list_for_cells = async (deps, cell_ids) => {
|
|
110
|
+
if (cell_ids.length === 0)
|
|
111
|
+
return [];
|
|
112
|
+
return deps.db.query(`SELECT * FROM cell_grant
|
|
113
|
+
WHERE cell_id = ANY($1::uuid[])
|
|
114
|
+
ORDER BY cell_id, created_at ASC`, [cell_ids]);
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Load grants that admit the caller (by actor or role-scoped role_grants) across
|
|
118
|
+
* multiple cells. Used to enrich `cell_list` responses with context about what
|
|
119
|
+
* granted access. Returns grants for the given cells that match the caller's
|
|
120
|
+
* identity or role_grant set.
|
|
121
|
+
*
|
|
122
|
+
* @param cell_ids - cells to fetch grants for
|
|
123
|
+
* @param caller_actor_id - actor id of the caller (null for unauth)
|
|
124
|
+
* @param role_grant_roles - active role_grant roles (parallel array)
|
|
125
|
+
* @param role_grant_scope_ids - active role_grant scope ids (parallel array, parallel to roles)
|
|
126
|
+
* @returns matching grants (may include grants the caller doesn't match; caller's
|
|
127
|
+
* list handler must filter when returning to the API)
|
|
128
|
+
*/
|
|
129
|
+
export const query_cell_grants_for_caller_in_cells = async (deps, cell_ids, caller_actor_id, role_grant_roles, role_grant_scope_ids) => {
|
|
130
|
+
if (cell_ids.length === 0) {
|
|
131
|
+
return [];
|
|
132
|
+
}
|
|
133
|
+
return deps.db.query(`SELECT g.* FROM cell_grant g
|
|
134
|
+
WHERE g.cell_id = ANY($1::uuid[])
|
|
135
|
+
AND (
|
|
136
|
+
g.actor_id = $2
|
|
137
|
+
OR EXISTS (
|
|
138
|
+
SELECT 1
|
|
139
|
+
FROM unnest($3::text[], $4::uuid[]) AS p(role, scope_id)
|
|
140
|
+
WHERE g.role = p.role
|
|
141
|
+
AND (g.scope_id IS NULL OR g.scope_id IS NOT DISTINCT FROM p.scope_id)
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
ORDER BY g.cell_id, g.created_at ASC`, [cell_ids, caller_actor_id, role_grant_roles, role_grant_scope_ids]);
|
|
145
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cell history PG schema (schema only).
|
|
3
|
+
*
|
|
4
|
+
* Lightweight references to cell state snapshots. Heavy data (serialized
|
|
5
|
+
* cell bytes) lives in the fact store; `fact_hash` points there. The
|
|
6
|
+
* snapshot lifecycle (when to serialize, hash, store, and record) is
|
|
7
|
+
* deferred to a future iteration — this only stages the table so
|
|
8
|
+
* downstream code can target a stable schema. The table ships
|
|
9
|
+
* present-but-unwritten.
|
|
10
|
+
*
|
|
11
|
+
* `fact_hash` is intentionally **not** a foreign key to `facts(hash)` —
|
|
12
|
+
* snapshots may be evicted by GC policy while history rows remain as audit
|
|
13
|
+
* traces, and federation may target facts on another instance.
|
|
14
|
+
*
|
|
15
|
+
* Depends on `CELL_MIGRATION_NS` (FK on `cell.id`).
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
import type { Migration, MigrationNamespace } from './migrate.js';
|
|
20
|
+
/** `cell_history` table — append-only log of cell snapshot references. */
|
|
21
|
+
export declare const CELL_HISTORY_SCHEMA = "\nCREATE TABLE IF NOT EXISTS cell_history (\n\tid BIGSERIAL PRIMARY KEY,\n\tcell_id UUID NOT NULL REFERENCES cell(id) ON DELETE CASCADE,\n\tfact_hash TEXT NOT NULL,\n\taction_id UUID,\n\tcreated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n)";
|
|
22
|
+
/**
|
|
23
|
+
* Cell-history indexes.
|
|
24
|
+
*
|
|
25
|
+
* - `idx_cell_history_cell`: per-cell timeline reads (newest first).
|
|
26
|
+
* - `idx_cell_history_fact`: fact → cells reverse lookup for GC liveness
|
|
27
|
+
* and provenance queries.
|
|
28
|
+
*/
|
|
29
|
+
export declare const CELL_HISTORY_INDEXES: Array<string>;
|
|
30
|
+
/** Tables created by `CELL_HISTORY_MIGRATION_NS`, in drop order. */
|
|
31
|
+
export declare const CELL_HISTORY_DROP_TABLES: readonly ["cell_history"];
|
|
32
|
+
/** Cell-history migrations. */
|
|
33
|
+
export declare const CELL_HISTORY_MIGRATIONS: Array<Migration>;
|
|
34
|
+
/** Namespace identifier for cell-history migrations. */
|
|
35
|
+
export declare const CELL_HISTORY_MIGRATION_NAMESPACE = "fuz_cell_history";
|
|
36
|
+
/** Migration namespace consumed by `run_migrations`. */
|
|
37
|
+
export declare const CELL_HISTORY_MIGRATION_NS: MigrationNamespace;
|
|
38
|
+
//# sourceMappingURL=cell_history_ddl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cell_history_ddl.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_history_ddl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,KAAK,EAAC,SAAS,EAAE,kBAAkB,EAAC,MAAM,cAAc,CAAC;AAEhE,0EAA0E;AAC1E,eAAO,MAAM,mBAAmB,gPAO9B,CAAC;AAEH;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,EAAE,KAAK,CAAC,MAAM,CAK9C,CAAC;AAEF,oEAAoE;AACpE,eAAO,MAAM,wBAAwB,2BAA4B,CAAC;AAElE,+BAA+B;AAC/B,eAAO,MAAM,uBAAuB,EAAE,KAAK,CAAC,SAAS,CAUpD,CAAC;AAEF,wDAAwD;AACxD,eAAO,MAAM,gCAAgC,qBAAqB,CAAC;AAEnE,wDAAwD;AACxD,eAAO,MAAM,yBAAyB,EAAE,kBAGvC,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cell history PG schema (schema only).
|
|
3
|
+
*
|
|
4
|
+
* Lightweight references to cell state snapshots. Heavy data (serialized
|
|
5
|
+
* cell bytes) lives in the fact store; `fact_hash` points there. The
|
|
6
|
+
* snapshot lifecycle (when to serialize, hash, store, and record) is
|
|
7
|
+
* deferred to a future iteration — this only stages the table so
|
|
8
|
+
* downstream code can target a stable schema. The table ships
|
|
9
|
+
* present-but-unwritten.
|
|
10
|
+
*
|
|
11
|
+
* `fact_hash` is intentionally **not** a foreign key to `facts(hash)` —
|
|
12
|
+
* snapshots may be evicted by GC policy while history rows remain as audit
|
|
13
|
+
* traces, and federation may target facts on another instance.
|
|
14
|
+
*
|
|
15
|
+
* Depends on `CELL_MIGRATION_NS` (FK on `cell.id`).
|
|
16
|
+
*
|
|
17
|
+
* @module
|
|
18
|
+
*/
|
|
19
|
+
/** `cell_history` table — append-only log of cell snapshot references. */
|
|
20
|
+
export const CELL_HISTORY_SCHEMA = `
|
|
21
|
+
CREATE TABLE IF NOT EXISTS cell_history (
|
|
22
|
+
id BIGSERIAL PRIMARY KEY,
|
|
23
|
+
cell_id UUID NOT NULL REFERENCES cell(id) ON DELETE CASCADE,
|
|
24
|
+
fact_hash TEXT NOT NULL,
|
|
25
|
+
action_id UUID,
|
|
26
|
+
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
27
|
+
)`;
|
|
28
|
+
/**
|
|
29
|
+
* Cell-history indexes.
|
|
30
|
+
*
|
|
31
|
+
* - `idx_cell_history_cell`: per-cell timeline reads (newest first).
|
|
32
|
+
* - `idx_cell_history_fact`: fact → cells reverse lookup for GC liveness
|
|
33
|
+
* and provenance queries.
|
|
34
|
+
*/
|
|
35
|
+
export const CELL_HISTORY_INDEXES = [
|
|
36
|
+
`CREATE INDEX IF NOT EXISTS idx_cell_history_cell
|
|
37
|
+
ON cell_history(cell_id, created_at DESC)`,
|
|
38
|
+
`CREATE INDEX IF NOT EXISTS idx_cell_history_fact
|
|
39
|
+
ON cell_history(fact_hash)`,
|
|
40
|
+
];
|
|
41
|
+
/** Tables created by `CELL_HISTORY_MIGRATION_NS`, in drop order. */
|
|
42
|
+
export const CELL_HISTORY_DROP_TABLES = ['cell_history'];
|
|
43
|
+
/** Cell-history migrations. */
|
|
44
|
+
export const CELL_HISTORY_MIGRATIONS = [
|
|
45
|
+
{
|
|
46
|
+
name: 'cell_history_v0',
|
|
47
|
+
up: async (db) => {
|
|
48
|
+
await db.query(CELL_HISTORY_SCHEMA);
|
|
49
|
+
for (const sql of CELL_HISTORY_INDEXES) {
|
|
50
|
+
await db.query(sql);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
/** Namespace identifier for cell-history migrations. */
|
|
56
|
+
export const CELL_HISTORY_MIGRATION_NAMESPACE = 'fuz_cell_history';
|
|
57
|
+
/** Migration namespace consumed by `run_migrations`. */
|
|
58
|
+
export const CELL_HISTORY_MIGRATION_NS = {
|
|
59
|
+
namespace: CELL_HISTORY_MIGRATION_NAMESPACE,
|
|
60
|
+
migrations: CELL_HISTORY_MIGRATIONS,
|
|
61
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw queries against the `cell_item` table.
|
|
3
|
+
*
|
|
4
|
+
* Ordered-child membership: each row is `(parent_id, position) → child_id`.
|
|
5
|
+
* `position` is opaque text (fractional-indexing key) — lex ordering is
|
|
6
|
+
* the contract. The PK on `(parent_id, position)` enforces one cell per
|
|
7
|
+
* slot but allows the same `child_id` to appear at multiple positions
|
|
8
|
+
* (the primitive is JSON-array-shaped — ordered multiset, not set;
|
|
9
|
+
* domain dedup rules ride on top).
|
|
10
|
+
*
|
|
11
|
+
* Reads filter both endpoints by `cell.deleted_at IS NULL` so items
|
|
12
|
+
* dangling off a soft-deleted cell don't surface.
|
|
13
|
+
*
|
|
14
|
+
* `query_cell_item_insert` returns the inserted row OR throws the
|
|
15
|
+
* underlying `23505` (Postgres unique violation) on a `(parent_id,
|
|
16
|
+
* position)` collision. Handlers convert this into the
|
|
17
|
+
* `cell_item_position_taken` JSON-RPC error so the client retries with a
|
|
18
|
+
* refreshed bracket.
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
import type { QueryDeps } from './query_deps.js';
|
|
23
|
+
import type { Uuid } from '@fuzdev/fuz_util/id.js';
|
|
24
|
+
/** Row shape returned by `cell_item` SELECTs. */
|
|
25
|
+
export interface CellItemRow {
|
|
26
|
+
parent_id: Uuid;
|
|
27
|
+
position: string;
|
|
28
|
+
child_id: Uuid;
|
|
29
|
+
created_at: Date;
|
|
30
|
+
}
|
|
31
|
+
/** Input for `query_cell_item_insert`. */
|
|
32
|
+
export interface CellItemInsertQueryInput {
|
|
33
|
+
parent_id: Uuid;
|
|
34
|
+
position: string;
|
|
35
|
+
child_id: Uuid;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Insert one item row at the caller-supplied `position`.
|
|
39
|
+
*
|
|
40
|
+
* Throws on `(parent_id, position)` collision (Postgres `23505`); handler
|
|
41
|
+
* callers detect via `is_pg_unique_violation` and surface as
|
|
42
|
+
* `cell_item_position_taken`. Helper-side jitter (`fractional_index`)
|
|
43
|
+
* makes the collision rate negligible at realistic UX concurrency, so
|
|
44
|
+
* the throw is the cold-path safety net, not the hot path.
|
|
45
|
+
*
|
|
46
|
+
* @mutates `cell_item` - inserts one row
|
|
47
|
+
*/
|
|
48
|
+
export declare const query_cell_item_insert: (deps: QueryDeps, input: CellItemInsertQueryInput) => Promise<CellItemRow>;
|
|
49
|
+
/**
|
|
50
|
+
* Fetch one item row by `(parent_id, position)`. Used by move + delete
|
|
51
|
+
* handlers to confirm the row exists before issuing the mutation.
|
|
52
|
+
*
|
|
53
|
+
* @returns the row or `null` when not found
|
|
54
|
+
*/
|
|
55
|
+
export declare const query_cell_item_get: (deps: QueryDeps, parent_id: Uuid, position: string) => Promise<CellItemRow | null>;
|
|
56
|
+
/**
|
|
57
|
+
* Move an item row from `position_old` to `position_new` (same parent).
|
|
58
|
+
*
|
|
59
|
+
* Implemented as an UPDATE on the PK; throws `23505` on collision with
|
|
60
|
+
* an existing row at `position_new` so handlers can surface
|
|
61
|
+
* `cell_item_position_taken`. The caller-supplied `position_new` is what
|
|
62
|
+
* fractional-indexing produced for the new slot — collisions are rare
|
|
63
|
+
* but the error path keeps the client truthful.
|
|
64
|
+
*
|
|
65
|
+
* @returns the updated row, or `null` when the source row was missing
|
|
66
|
+
* (raced with a deleter)
|
|
67
|
+
* @mutates `cell_item` - updates one row's `position`
|
|
68
|
+
*/
|
|
69
|
+
export declare const query_cell_item_move: (deps: QueryDeps, parent_id: Uuid, position_old: string, position_new: string) => Promise<CellItemRow | null>;
|
|
70
|
+
/**
|
|
71
|
+
* Delete one item row by `(parent_id, position)`. Returns the deleted
|
|
72
|
+
* row so callers can audit `child_id` after the delete without a
|
|
73
|
+
* pre-fetch.
|
|
74
|
+
*
|
|
75
|
+
* @returns the deleted row, or `null` when nothing matched
|
|
76
|
+
* @mutates `cell_item` - deletes one row
|
|
77
|
+
*/
|
|
78
|
+
export declare const query_cell_item_delete: (deps: QueryDeps, parent_id: Uuid, position: string) => Promise<CellItemRow | null>;
|
|
79
|
+
/**
|
|
80
|
+
* Forward items list (`parent.items[]`), ordered by lex `position`.
|
|
81
|
+
*
|
|
82
|
+
* Filters child by `deleted_at IS NULL` so items pointing at tombstoned
|
|
83
|
+
* cells don't surface; the parent filter is the caller's responsibility
|
|
84
|
+
* (gated upstream by `can_view_cell(parent)`).
|
|
85
|
+
*
|
|
86
|
+
* @param limit - optional row cap (passes through to SQL `LIMIT`)
|
|
87
|
+
*/
|
|
88
|
+
export declare const query_cell_item_list_for_parent: (deps: QueryDeps, parent_id: Uuid, options?: {
|
|
89
|
+
limit?: number;
|
|
90
|
+
position_after?: string;
|
|
91
|
+
}) => Promise<Array<CellItemRow>>;
|
|
92
|
+
/**
|
|
93
|
+
* Reverse items list (`child.lists[]`).
|
|
94
|
+
*
|
|
95
|
+
* Returns rows whose `child_id = $1`, joined to `cell` on `parent_id` so
|
|
96
|
+
* items from tombstoned parents don't surface. The caller-side authz
|
|
97
|
+
* filter (per-parent `can_view_cell`) runs after the SQL fetch — see
|
|
98
|
+
* the 2-layer authz contract on `cell_item_list({child_id})`.
|
|
99
|
+
*
|
|
100
|
+
* Bounded by `limit` (the wire `cell_item_list` cap) so a heavily
|
|
101
|
+
* inbound-linked child can't force an unbounded fetch + per-parent authz
|
|
102
|
+
* pass on the public, IP-rate-limited reverse endpoint.
|
|
103
|
+
*/
|
|
104
|
+
export declare const query_cell_item_list_for_child: (deps: QueryDeps, child_id: Uuid, options?: {
|
|
105
|
+
limit?: number;
|
|
106
|
+
}) => Promise<Array<CellItemRow>>;
|
|
107
|
+
//# sourceMappingURL=cell_item_queries.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cell_item_queries.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_item_queries.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AAGjD,iDAAiD;AACjD,MAAM,WAAW,WAAW;IAC3B,SAAS,EAAE,IAAI,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,IAAI,CAAC;IACf,UAAU,EAAE,IAAI,CAAC;CACjB;AAED,0CAA0C;AAC1C,MAAM,WAAW,wBAAwB;IACxC,SAAS,EAAE,IAAI,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,IAAI,CAAC;CACf;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,SAAS,EACf,OAAO,wBAAwB,KAC7B,OAAO,CAAC,WAAW,CAQrB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,SAAS,EACf,WAAW,IAAI,EACf,UAAU,MAAM,KACd,OAAO,CAAC,WAAW,GAAG,IAAI,CAM5B,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,GAChC,MAAM,SAAS,EACf,WAAW,IAAI,EACf,cAAc,MAAM,EACpB,cAAc,MAAM,KAClB,OAAO,CAAC,WAAW,GAAG,IAAI,CAS5B,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,GAClC,MAAM,SAAS,EACf,WAAW,IAAI,EACf,UAAU,MAAM,KACd,OAAO,CAAC,WAAW,GAAG,IAAI,CAM5B,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,+BAA+B,GAC3C,MAAM,SAAS,EACf,WAAW,IAAI,EACf,UAAU;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAAC,KACjD,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAa5B,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,8BAA8B,GAC1C,MAAM,SAAS,EACf,UAAU,IAAI,EACd,UAAU;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAC,KACxB,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,CAS3B,CAAC"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw queries against the `cell_item` table.
|
|
3
|
+
*
|
|
4
|
+
* Ordered-child membership: each row is `(parent_id, position) → child_id`.
|
|
5
|
+
* `position` is opaque text (fractional-indexing key) — lex ordering is
|
|
6
|
+
* the contract. The PK on `(parent_id, position)` enforces one cell per
|
|
7
|
+
* slot but allows the same `child_id` to appear at multiple positions
|
|
8
|
+
* (the primitive is JSON-array-shaped — ordered multiset, not set;
|
|
9
|
+
* domain dedup rules ride on top).
|
|
10
|
+
*
|
|
11
|
+
* Reads filter both endpoints by `cell.deleted_at IS NULL` so items
|
|
12
|
+
* dangling off a soft-deleted cell don't surface.
|
|
13
|
+
*
|
|
14
|
+
* `query_cell_item_insert` returns the inserted row OR throws the
|
|
15
|
+
* underlying `23505` (Postgres unique violation) on a `(parent_id,
|
|
16
|
+
* position)` collision. Handlers convert this into the
|
|
17
|
+
* `cell_item_position_taken` JSON-RPC error so the client retries with a
|
|
18
|
+
* refreshed bracket.
|
|
19
|
+
*
|
|
20
|
+
* @module
|
|
21
|
+
*/
|
|
22
|
+
import { assert_row } from './assert_row.js';
|
|
23
|
+
/**
|
|
24
|
+
* Insert one item row at the caller-supplied `position`.
|
|
25
|
+
*
|
|
26
|
+
* Throws on `(parent_id, position)` collision (Postgres `23505`); handler
|
|
27
|
+
* callers detect via `is_pg_unique_violation` and surface as
|
|
28
|
+
* `cell_item_position_taken`. Helper-side jitter (`fractional_index`)
|
|
29
|
+
* makes the collision rate negligible at realistic UX concurrency, so
|
|
30
|
+
* the throw is the cold-path safety net, not the hot path.
|
|
31
|
+
*
|
|
32
|
+
* @mutates `cell_item` - inserts one row
|
|
33
|
+
*/
|
|
34
|
+
export const query_cell_item_insert = async (deps, input) => {
|
|
35
|
+
const row = await deps.db.query_one(`INSERT INTO cell_item (parent_id, position, child_id)
|
|
36
|
+
VALUES ($1, $2, $3)
|
|
37
|
+
RETURNING *`, [input.parent_id, input.position, input.child_id]);
|
|
38
|
+
return assert_row(row, 'INSERT INTO cell_item');
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Fetch one item row by `(parent_id, position)`. Used by move + delete
|
|
42
|
+
* handlers to confirm the row exists before issuing the mutation.
|
|
43
|
+
*
|
|
44
|
+
* @returns the row or `null` when not found
|
|
45
|
+
*/
|
|
46
|
+
export const query_cell_item_get = async (deps, parent_id, position) => {
|
|
47
|
+
const row = await deps.db.query_one(`SELECT * FROM cell_item WHERE parent_id = $1 AND position = $2`, [parent_id, position]);
|
|
48
|
+
return row ?? null;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Move an item row from `position_old` to `position_new` (same parent).
|
|
52
|
+
*
|
|
53
|
+
* Implemented as an UPDATE on the PK; throws `23505` on collision with
|
|
54
|
+
* an existing row at `position_new` so handlers can surface
|
|
55
|
+
* `cell_item_position_taken`. The caller-supplied `position_new` is what
|
|
56
|
+
* fractional-indexing produced for the new slot — collisions are rare
|
|
57
|
+
* but the error path keeps the client truthful.
|
|
58
|
+
*
|
|
59
|
+
* @returns the updated row, or `null` when the source row was missing
|
|
60
|
+
* (raced with a deleter)
|
|
61
|
+
* @mutates `cell_item` - updates one row's `position`
|
|
62
|
+
*/
|
|
63
|
+
export const query_cell_item_move = async (deps, parent_id, position_old, position_new) => {
|
|
64
|
+
const row = await deps.db.query_one(`UPDATE cell_item
|
|
65
|
+
SET position = $3
|
|
66
|
+
WHERE parent_id = $1 AND position = $2
|
|
67
|
+
RETURNING *`, [parent_id, position_old, position_new]);
|
|
68
|
+
return row ?? null;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Delete one item row by `(parent_id, position)`. Returns the deleted
|
|
72
|
+
* row so callers can audit `child_id` after the delete without a
|
|
73
|
+
* pre-fetch.
|
|
74
|
+
*
|
|
75
|
+
* @returns the deleted row, or `null` when nothing matched
|
|
76
|
+
* @mutates `cell_item` - deletes one row
|
|
77
|
+
*/
|
|
78
|
+
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 RETURNING *`, [parent_id, position]);
|
|
80
|
+
return row ?? null;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Forward items list (`parent.items[]`), ordered by lex `position`.
|
|
84
|
+
*
|
|
85
|
+
* Filters child by `deleted_at IS NULL` so items pointing at tombstoned
|
|
86
|
+
* cells don't surface; the parent filter is the caller's responsibility
|
|
87
|
+
* (gated upstream by `can_view_cell(parent)`).
|
|
88
|
+
*
|
|
89
|
+
* @param limit - optional row cap (passes through to SQL `LIMIT`)
|
|
90
|
+
*/
|
|
91
|
+
export const query_cell_item_list_for_parent = async (deps, parent_id, options) => {
|
|
92
|
+
const limit = options?.limit ?? null;
|
|
93
|
+
const position_after = options?.position_after ?? null;
|
|
94
|
+
return deps.db.query(`SELECT i.* FROM cell_item i
|
|
95
|
+
JOIN cell c ON c.id = i.child_id
|
|
96
|
+
WHERE i.parent_id = $1
|
|
97
|
+
AND c.deleted_at IS NULL
|
|
98
|
+
AND ($3::text IS NULL OR i.position > $3)
|
|
99
|
+
ORDER BY i.position ASC
|
|
100
|
+
LIMIT $2`, [parent_id, limit, position_after]);
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Reverse items list (`child.lists[]`).
|
|
104
|
+
*
|
|
105
|
+
* Returns rows whose `child_id = $1`, joined to `cell` on `parent_id` so
|
|
106
|
+
* items from tombstoned parents don't surface. The caller-side authz
|
|
107
|
+
* filter (per-parent `can_view_cell`) runs after the SQL fetch — see
|
|
108
|
+
* the 2-layer authz contract on `cell_item_list({child_id})`.
|
|
109
|
+
*
|
|
110
|
+
* Bounded by `limit` (the wire `cell_item_list` cap) so a heavily
|
|
111
|
+
* inbound-linked child can't force an unbounded fetch + per-parent authz
|
|
112
|
+
* pass on the public, IP-rate-limited reverse endpoint.
|
|
113
|
+
*/
|
|
114
|
+
export const query_cell_item_list_for_child = async (deps, child_id, options) => deps.db.query(`SELECT i.* FROM cell_item i
|
|
115
|
+
JOIN cell p ON p.id = i.parent_id
|
|
116
|
+
WHERE i.child_id = $1
|
|
117
|
+
AND p.deleted_at IS NULL
|
|
118
|
+
ORDER BY i.created_at ASC
|
|
119
|
+
LIMIT $2`, [child_id, options?.limit ?? null]);
|