@fuzdev/fuz_app 0.102.0 → 0.104.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/auth/CLAUDE.md +37 -10
- package/dist/auth/cell_action_specs.d.ts +261 -35
- package/dist/auth/cell_action_specs.d.ts.map +1 -1
- package/dist/auth/cell_action_specs.js +102 -9
- package/dist/auth/cell_actions.d.ts +51 -0
- package/dist/auth/cell_actions.d.ts.map +1 -1
- package/dist/auth/cell_actions.js +168 -22
- package/dist/auth/cell_audit_events.d.ts +1 -1
- package/dist/auth/cell_audit_events.d.ts.map +1 -1
- package/dist/auth/cell_audit_events.js +3 -2
- package/dist/auth/cell_audit_metadata.d.ts +15 -0
- package/dist/auth/cell_audit_metadata.d.ts.map +1 -1
- package/dist/auth/cell_audit_metadata.js +11 -0
- package/dist/auth/cell_data_schema.d.ts +14 -16
- package/dist/auth/cell_data_schema.d.ts.map +1 -1
- package/dist/auth/cell_data_schema.js +14 -16
- package/dist/db/CLAUDE.md +19 -6
- package/dist/db/cell_ddl.d.ts +24 -2
- package/dist/db/cell_ddl.d.ts.map +1 -1
- package/dist/db/cell_ddl.js +33 -1
- package/dist/db/cell_queries.d.ts +59 -8
- package/dist/db/cell_queries.d.ts.map +1 -1
- package/dist/db/cell_queries.js +50 -12
- package/dist/testing/CLAUDE.md +37 -7
- package/dist/testing/cross_backend/capabilities.d.ts +14 -0
- package/dist/testing/cross_backend/capabilities.d.ts.map +1 -1
- package/dist/testing/cross_backend/capabilities.js +3 -0
- package/dist/testing/cross_backend/cell_crud.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_crud.js +55 -13
- package/dist/testing/cross_backend/cell_gated_create.d.ts +28 -0
- package/dist/testing/cross_backend/cell_gated_create.d.ts.map +1 -0
- package/dist/testing/cross_backend/cell_gated_create.js +213 -0
- package/dist/testing/cross_backend/cell_grant_role.js +4 -4
- package/dist/testing/cross_backend/cell_relations.d.ts.map +1 -1
- package/dist/testing/cross_backend/cell_relations.js +30 -24
- package/dist/testing/cross_backend/default_backend_configs.d.ts.map +1 -1
- package/dist/testing/cross_backend/default_backend_configs.js +7 -0
- package/dist/testing/cross_backend/expected_schema.json +4 -0
- package/dist/testing/cross_backend/fact_serving.js +2 -2
- package/dist/testing/cross_backend/full_spine_mount.d.ts.map +1 -1
- package/dist/testing/cross_backend/full_spine_mount.js +9 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/rust_spine_stub_backend_config.js +6 -3
- package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts +22 -0
- package/dist/testing/cross_backend/test_cell_gated_create_authorize.d.ts.map +1 -0
- package/dist/testing/cross_backend/test_cell_gated_create_authorize.js +75 -0
- package/dist/testing/cross_backend/ts_spine_backend_config.d.ts.map +1 -1
- package/dist/testing/cross_backend/ts_spine_backend_config.js +5 -2
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell_audit_metadata.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_audit_metadata.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAGtB;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB;;;;iBAI5B,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,sBAAsB;;;;;;iBAMjC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC"}
|
|
1
|
+
{"version":3,"file":"cell_audit_metadata.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_audit_metadata.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAGtB;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB;;;;iBAI5B,CAAC;AACH,MAAM,MAAM,iBAAiB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,iBAAiB,CAAC,CAAC;AAElE;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,sBAAsB;;;;;;iBAMjC,CAAC;AACH,MAAM,MAAM,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,sBAAsB,CAAC,CAAC;AAE5E;;;;;GAKG;AACH,eAAO,MAAM,yBAAyB;;;;;;;iBAIpC,CAAC;AACH,MAAM,MAAM,yBAAyB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,yBAAyB,CAAC,CAAC"}
|
|
@@ -44,3 +44,14 @@ export const CellCloneAuditMetadata = z.looseObject({
|
|
|
44
44
|
item_count: z.number().int().nonnegative(),
|
|
45
45
|
kind: z.string().optional(),
|
|
46
46
|
});
|
|
47
|
+
/**
|
|
48
|
+
* Metadata envelope for `cell_moderate`. `cell_id` is the moderated
|
|
49
|
+
* contribution; `root_id` is its governing root (the container whose
|
|
50
|
+
* moderation authority gated the call); `moderation` is the terminal decision
|
|
51
|
+
* applied (`'approved'` | `'rejected'`).
|
|
52
|
+
*/
|
|
53
|
+
export const CellModerateAuditMetadata = z.looseObject({
|
|
54
|
+
cell_id: Uuid,
|
|
55
|
+
root_id: Uuid,
|
|
56
|
+
moderation: z.enum(['approved', 'rejected']),
|
|
57
|
+
});
|
|
@@ -11,17 +11,19 @@
|
|
|
11
11
|
* `auth/cell_actions.ts`.
|
|
12
12
|
*
|
|
13
13
|
* **Discipline**: a field joins `CellData` only when at least two
|
|
14
|
-
* consumers in different domains read it generically. `
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* stay per-kind.
|
|
14
|
+
* consumers in different domains read it generically. `label` (list/index
|
|
15
|
+
* rendering) and `summary` (card subtitle + share-target description) meet
|
|
16
|
+
* this bar. Future candidates require evidence of generic usage — otherwise
|
|
17
|
+
* they stay per-kind.
|
|
19
18
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* `
|
|
23
|
-
*
|
|
24
|
-
*
|
|
19
|
+
* **`kind` is not in here, and is rejected if present.** Like `visibility`,
|
|
20
|
+
* a cell's `kind` is a top-level column (`cell.kind` on `CellJson` /
|
|
21
|
+
* `CellRow`), not content metadata — it is the capability / identity axis a
|
|
22
|
+
* creation authorizer gates on, write-once at birth. A stray `kind` key
|
|
23
|
+
* inside `data` is a fail-loud `ERROR_CELL_KIND_IN_DATA` at the create /
|
|
24
|
+
* update / clone-patch boundary (single source of truth). Access control
|
|
25
|
+
* (`cell.visibility`) is likewise a peer column, enforced by `can_view_cell`
|
|
26
|
+
* reading the column directly (no JSON dive).
|
|
25
27
|
*
|
|
26
28
|
* @module
|
|
27
29
|
*/
|
|
@@ -29,14 +31,10 @@ import { z } from 'zod';
|
|
|
29
31
|
/**
|
|
30
32
|
* Base cell-data shape. All fields optional; loose mode admits arbitrary
|
|
31
33
|
* additional keys so apps can attach metadata or stage new kinds without
|
|
32
|
-
* touching the wire schema.
|
|
33
|
-
*
|
|
34
|
-
* `kind` is optional because cells without a registered kind are valid —
|
|
35
|
-
* admin-curated content, in-development types, or unknown shapes pass
|
|
36
|
-
* through. Known kinds get richer validation via the per-kind sub-API.
|
|
34
|
+
* touching the wire schema. `kind` lives on the top-level `cell.kind`
|
|
35
|
+
* column, not here (see the module doc).
|
|
37
36
|
*/
|
|
38
37
|
export declare const CellData: z.ZodObject<{
|
|
39
|
-
kind: z.ZodOptional<z.ZodString>;
|
|
40
38
|
label: z.ZodOptional<z.ZodString>;
|
|
41
39
|
summary: z.ZodOptional<z.ZodString>;
|
|
42
40
|
}, z.core.$loose>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell_data_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_data_schema.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"cell_data_schema.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/auth/cell_data_schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAEtB;;;;;GAKG;AACH,eAAO,MAAM,QAAQ;;;iBAGnB,CAAC;AACH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,QAAQ,CAAC,CAAC"}
|
|
@@ -11,17 +11,19 @@
|
|
|
11
11
|
* `auth/cell_actions.ts`.
|
|
12
12
|
*
|
|
13
13
|
* **Discipline**: a field joins `CellData` only when at least two
|
|
14
|
-
* consumers in different domains read it generically. `
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* stay per-kind.
|
|
14
|
+
* consumers in different domains read it generically. `label` (list/index
|
|
15
|
+
* rendering) and `summary` (card subtitle + share-target description) meet
|
|
16
|
+
* this bar. Future candidates require evidence of generic usage — otherwise
|
|
17
|
+
* they stay per-kind.
|
|
19
18
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* `
|
|
23
|
-
*
|
|
24
|
-
*
|
|
19
|
+
* **`kind` is not in here, and is rejected if present.** Like `visibility`,
|
|
20
|
+
* a cell's `kind` is a top-level column (`cell.kind` on `CellJson` /
|
|
21
|
+
* `CellRow`), not content metadata — it is the capability / identity axis a
|
|
22
|
+
* creation authorizer gates on, write-once at birth. A stray `kind` key
|
|
23
|
+
* inside `data` is a fail-loud `ERROR_CELL_KIND_IN_DATA` at the create /
|
|
24
|
+
* update / clone-patch boundary (single source of truth). Access control
|
|
25
|
+
* (`cell.visibility`) is likewise a peer column, enforced by `can_view_cell`
|
|
26
|
+
* reading the column directly (no JSON dive).
|
|
25
27
|
*
|
|
26
28
|
* @module
|
|
27
29
|
*/
|
|
@@ -29,14 +31,10 @@ import { z } from 'zod';
|
|
|
29
31
|
/**
|
|
30
32
|
* Base cell-data shape. All fields optional; loose mode admits arbitrary
|
|
31
33
|
* additional keys so apps can attach metadata or stage new kinds without
|
|
32
|
-
* touching the wire schema.
|
|
33
|
-
*
|
|
34
|
-
* `kind` is optional because cells without a registered kind are valid —
|
|
35
|
-
* admin-curated content, in-development types, or unknown shapes pass
|
|
36
|
-
* through. Known kinds get richer validation via the per-kind sub-API.
|
|
34
|
+
* touching the wire schema. `kind` lives on the top-level `cell.kind`
|
|
35
|
+
* column, not here (see the module doc).
|
|
37
36
|
*/
|
|
38
37
|
export const CellData = z.looseObject({
|
|
39
|
-
kind: z.string().optional(),
|
|
40
38
|
label: z.string().optional(),
|
|
41
39
|
summary: z.string().optional(),
|
|
42
40
|
});
|
package/dist/db/CLAUDE.md
CHANGED
|
@@ -51,18 +51,31 @@ The wire schemas + RPC handlers + authz predicates for this layer live in
|
|
|
51
51
|
tables; single-migration `CELL_MIGRATION_NS` (namespace `fuz_cell`).
|
|
52
52
|
`cell.visibility cell_visibility NOT NULL DEFAULT 'private'` (PG ENUM
|
|
53
53
|
`('private', 'public')`) — a top-level access-control column, peer to
|
|
54
|
-
`cell_grant`, not a `data` field.
|
|
55
|
-
|
|
56
|
-
partial-
|
|
54
|
+
`cell_grant`, not a `data` field. `cell.kind text` (nullable) is the
|
|
55
|
+
write-once capability/identity axis — likewise a top-level column, not a
|
|
56
|
+
`data` field (`idx_cell_kind` partial-active). `cell.parent_id` / `root_id`
|
|
57
|
+
(nullable self-FKs to `cell.id`) are the **directory tree** — immediate
|
|
58
|
+
container + governing root (`root_id = parent.root_id ?? parent.id`, null for a
|
|
59
|
+
root), immutable in v1 (`idx_cell_root` partial-active for subtree feeds).
|
|
60
|
+
`cell.moderation text` (nullable — `pending` / `approved` / `rejected`) is the
|
|
61
|
+
approval-lifecycle marker, peer to `visibility` (partial `idx_cell_moderation_pending`
|
|
62
|
+
on `root_id WHERE moderation = 'pending'` for the mod queue), set by the create
|
|
63
|
+
authorizer's verdict + the `cell_moderate` verb, never in `data`. Nullable
|
|
64
|
+
`created_by` / `updated_by` FKs to `actor` (NULL = system origin). GIN on `data`
|
|
65
|
+
/ `refs`; global partial-unique on `path`.
|
|
57
66
|
- **`cell_history_ddl.ts`** — dormant `cell_history` table
|
|
58
67
|
(`CELL_HISTORY_MIGRATION_NS`, namespace `fuz_cell_history`), FK → `cell.id`.
|
|
59
68
|
Ships present-but-unwritten; no snapshot lifecycle yet.
|
|
60
69
|
- **`cell_queries.ts`** — `query_cell_create / get / get_by_path / update /
|
|
61
|
-
delete`, `
|
|
70
|
+
delete / set_moderation`, `_list_by_kind / _list_by_creator`, the
|
|
62
71
|
generic `query_cell_list` (filter + SQL-side visibility predicate mirroring
|
|
63
|
-
`can_view_cell`; the `
|
|
72
|
+
`can_view_cell`; the `kind` filter narrows by the `cell.kind` column, the
|
|
73
|
+
`ref` filter by `cell.refs`, plus `root_id` / `moderation` filters for the
|
|
74
|
+
per-root feed + mod queue), and
|
|
64
75
|
`query_cell_load_many` (bulk id load, no visibility filter — feeds the
|
|
65
|
-
strict relation-read filter). `
|
|
76
|
+
strict relation-read filter). `query_cell_set_moderation` is the dedicated
|
|
77
|
+
`cell_moderate` write (`moderation` is absent from `CellUpdatePatch`, so an
|
|
78
|
+
author can't self-approve via `cell_update`). `cell.refs` derived from
|
|
66
79
|
`data` via `extract_refs` on create/update. `CellRow.grant_count` is a
|
|
67
80
|
derived projection (correlated subquery on `idx_cell_grant_cell`).
|
|
68
81
|
- **`cell_grant_queries.ts`** — resource-side ACL: `query_cell_grant_create`
|
package/dist/db/cell_ddl.d.ts
CHANGED
|
@@ -62,8 +62,24 @@ export declare const CELL_VISIBILITY_TYPE = "\nDO $$ BEGIN\n\tCREATE TYPE cell_v
|
|
|
62
62
|
*
|
|
63
63
|
* `path` is the global namespace axis (no tenant/hub scoping) — globally
|
|
64
64
|
* unique on active rows via `idx_cell_path_unique`.
|
|
65
|
+
*
|
|
66
|
+
* `kind` is the capability / identity axis — a nullable top-level column
|
|
67
|
+
* (peer to `visibility` / `path`), **not** a field inside `data`. It is the
|
|
68
|
+
* discriminator a creation authorizer gates on (see `auth/cell_actions.ts`
|
|
69
|
+
* `CellCreateAuthorize`) and is **write-once**: set at INSERT and carried on
|
|
70
|
+
* no update path, so a cell's kind is fixed at birth. Content stays
|
|
71
|
+
* duck-typed in `data`; `kind` is a capability tag, not a content-type.
|
|
72
|
+
*
|
|
73
|
+
* `parent_id` / `root_id` are the **directory tree** (containment): `parent_id`
|
|
74
|
+
* is the immediate container (nullable self-FK; `NULL` = a root), `root_id` is
|
|
75
|
+
* the governing root denormalized for flat-subtree queries (`root_id =
|
|
76
|
+
* parent.root_id ?? parent.id`, so a root has `NULL`). Both are set once at
|
|
77
|
+
* create and immutable in v1 (carried on no update path). `moderation`
|
|
78
|
+
* (nullable text — `pending` / `approved` / `rejected`; `NULL` = unmoderated)
|
|
79
|
+
* is the approval-lifecycle marker, peer to `visibility` (a control field with
|
|
80
|
+
* a non-author writer — see `auth/cell_actions.ts`), never inside `data`.
|
|
65
81
|
*/
|
|
66
|
-
export declare const CELL_SCHEMA = "\nCREATE TABLE IF NOT EXISTS cell (\n\tid UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n\tdata JSONB NOT NULL,\n\tvisibility cell_visibility NOT NULL DEFAULT 'private',\n\tpath TEXT,\n\trefs TEXT[],\n\tcreated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n\tupdated_at TIMESTAMPTZ,\n\tdeleted_at TIMESTAMPTZ,\n\tcreated_by UUID REFERENCES actor(id) ON DELETE SET NULL,\n\tupdated_by UUID REFERENCES actor(id) ON DELETE SET NULL\n)";
|
|
82
|
+
export declare const CELL_SCHEMA = "\nCREATE TABLE IF NOT EXISTS cell (\n\tid UUID PRIMARY KEY DEFAULT gen_random_uuid(),\n\tdata JSONB NOT NULL,\n\tkind TEXT,\n\tvisibility cell_visibility NOT NULL DEFAULT 'private',\n\tpath TEXT,\n\trefs TEXT[],\n\tparent_id UUID REFERENCES cell(id) ON DELETE SET NULL,\n\troot_id UUID REFERENCES cell(id) ON DELETE SET NULL,\n\tmoderation TEXT,\n\tcreated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n\tupdated_at TIMESTAMPTZ,\n\tdeleted_at TIMESTAMPTZ,\n\tcreated_by UUID REFERENCES actor(id) ON DELETE SET NULL,\n\tupdated_by UUID REFERENCES actor(id) ON DELETE SET NULL\n)";
|
|
67
83
|
/**
|
|
68
84
|
* Cell indexes — all active-only, partial on `deleted_at IS NULL`.
|
|
69
85
|
*
|
|
@@ -71,9 +87,15 @@ export declare const CELL_SCHEMA = "\nCREATE TABLE IF NOT EXISTS cell (\n\tid UU
|
|
|
71
87
|
* - `idx_cell_path_unique`: global `path` uniqueness + read-side path
|
|
72
88
|
* lookup. Partial on path + active so reused paths after soft delete are
|
|
73
89
|
* allowed.
|
|
74
|
-
* - `
|
|
90
|
+
* - `idx_cell_kind`: the `cell_list` kind filter (`cell.kind = ?`) and
|
|
91
|
+
* kind-scoped scans. Active-only.
|
|
92
|
+
* - `idx_cell_data`: shape-driven queries (`data @> ...`).
|
|
75
93
|
* - `idx_cell_refs`: cells-by-fact discovery (cross-cell reference graph).
|
|
76
94
|
* - `idx_cell_created_by`: "cells this actor created" queries.
|
|
95
|
+
* - `idx_cell_root`: flat-subtree feed/scope queries by governing root
|
|
96
|
+
* (`cell.root_id = ?`). Active-only.
|
|
97
|
+
* - `idx_cell_moderation_pending`: the moderation queue — pending
|
|
98
|
+
* contributions per governing root. Partial on `moderation = 'pending'`.
|
|
77
99
|
*
|
|
78
100
|
* Parent↔child membership and named relations live in sibling tables;
|
|
79
101
|
* see `CELL_ITEM_INDEXES` / `CELL_FIELD_INDEXES` below.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell_ddl.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_ddl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAGH,OAAO,KAAK,EAAC,SAAS,EAAE,kBAAkB,EAAC,MAAM,cAAc,CAAC;AAEhE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,oIAI1B,CAAC;AAER
|
|
1
|
+
{"version":3,"file":"cell_ddl.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/db/cell_ddl.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAGH,OAAO,KAAK,EAAC,SAAS,EAAE,kBAAkB,EAAC,MAAM,cAAc,CAAC;AAEhE;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,oIAI1B,CAAC;AAER;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,eAAO,MAAM,WAAW,gkBAgBtB,CAAC;AAEH;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,MAAM,CAkBtC,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,iBAAiB,0iBAc5B,CAAC;AAEH;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,MAAM,CAW5C,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,+RAO5B,CAAC;AAEH;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,kBAAkB,EAAE,KAAK,CAAC,MAAM,CAE5C,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,qSAO3B,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,EAAE,KAAK,CAAC,MAAM,CAG3C,CAAC;AAEF,6EAA6E;AAC7E,eAAO,MAAM,gBAAgB,4DAA6D,CAAC;AAE3F,uBAAuB;AACvB,eAAO,MAAM,eAAe,EAAE,KAAK,CAAC,SAAS,CAuB5C,CAAC;AAEF,gDAAgD;AAChD,eAAO,MAAM,wBAAwB,aAAa,CAAC;AAEnD,wDAAwD;AACxD,eAAO,MAAM,iBAAiB,EAAE,kBAG/B,CAAC"}
|
package/dist/db/cell_ddl.js
CHANGED
|
@@ -65,14 +65,34 @@ END $$`;
|
|
|
65
65
|
*
|
|
66
66
|
* `path` is the global namespace axis (no tenant/hub scoping) — globally
|
|
67
67
|
* unique on active rows via `idx_cell_path_unique`.
|
|
68
|
+
*
|
|
69
|
+
* `kind` is the capability / identity axis — a nullable top-level column
|
|
70
|
+
* (peer to `visibility` / `path`), **not** a field inside `data`. It is the
|
|
71
|
+
* discriminator a creation authorizer gates on (see `auth/cell_actions.ts`
|
|
72
|
+
* `CellCreateAuthorize`) and is **write-once**: set at INSERT and carried on
|
|
73
|
+
* no update path, so a cell's kind is fixed at birth. Content stays
|
|
74
|
+
* duck-typed in `data`; `kind` is a capability tag, not a content-type.
|
|
75
|
+
*
|
|
76
|
+
* `parent_id` / `root_id` are the **directory tree** (containment): `parent_id`
|
|
77
|
+
* is the immediate container (nullable self-FK; `NULL` = a root), `root_id` is
|
|
78
|
+
* the governing root denormalized for flat-subtree queries (`root_id =
|
|
79
|
+
* parent.root_id ?? parent.id`, so a root has `NULL`). Both are set once at
|
|
80
|
+
* create and immutable in v1 (carried on no update path). `moderation`
|
|
81
|
+
* (nullable text — `pending` / `approved` / `rejected`; `NULL` = unmoderated)
|
|
82
|
+
* is the approval-lifecycle marker, peer to `visibility` (a control field with
|
|
83
|
+
* a non-author writer — see `auth/cell_actions.ts`), never inside `data`.
|
|
68
84
|
*/
|
|
69
85
|
export const CELL_SCHEMA = `
|
|
70
86
|
CREATE TABLE IF NOT EXISTS cell (
|
|
71
87
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
72
88
|
data JSONB NOT NULL,
|
|
89
|
+
kind TEXT,
|
|
73
90
|
visibility cell_visibility NOT NULL DEFAULT 'private',
|
|
74
91
|
path TEXT,
|
|
75
92
|
refs TEXT[],
|
|
93
|
+
parent_id UUID REFERENCES cell(id) ON DELETE SET NULL,
|
|
94
|
+
root_id UUID REFERENCES cell(id) ON DELETE SET NULL,
|
|
95
|
+
moderation TEXT,
|
|
76
96
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
77
97
|
updated_at TIMESTAMPTZ,
|
|
78
98
|
deleted_at TIMESTAMPTZ,
|
|
@@ -86,9 +106,15 @@ CREATE TABLE IF NOT EXISTS cell (
|
|
|
86
106
|
* - `idx_cell_path_unique`: global `path` uniqueness + read-side path
|
|
87
107
|
* lookup. Partial on path + active so reused paths after soft delete are
|
|
88
108
|
* allowed.
|
|
89
|
-
* - `
|
|
109
|
+
* - `idx_cell_kind`: the `cell_list` kind filter (`cell.kind = ?`) and
|
|
110
|
+
* kind-scoped scans. Active-only.
|
|
111
|
+
* - `idx_cell_data`: shape-driven queries (`data @> ...`).
|
|
90
112
|
* - `idx_cell_refs`: cells-by-fact discovery (cross-cell reference graph).
|
|
91
113
|
* - `idx_cell_created_by`: "cells this actor created" queries.
|
|
114
|
+
* - `idx_cell_root`: flat-subtree feed/scope queries by governing root
|
|
115
|
+
* (`cell.root_id = ?`). Active-only.
|
|
116
|
+
* - `idx_cell_moderation_pending`: the moderation queue — pending
|
|
117
|
+
* contributions per governing root. Partial on `moderation = 'pending'`.
|
|
92
118
|
*
|
|
93
119
|
* Parent↔child membership and named relations live in sibling tables;
|
|
94
120
|
* see `CELL_ITEM_INDEXES` / `CELL_FIELD_INDEXES` below.
|
|
@@ -99,12 +125,18 @@ export const CELL_INDEXES = [
|
|
|
99
125
|
`CREATE UNIQUE INDEX IF NOT EXISTS idx_cell_path_unique
|
|
100
126
|
ON cell(path)
|
|
101
127
|
WHERE path IS NOT NULL AND deleted_at IS NULL`,
|
|
128
|
+
`CREATE INDEX IF NOT EXISTS idx_cell_kind ON cell(kind)
|
|
129
|
+
WHERE deleted_at IS NULL`,
|
|
102
130
|
`CREATE INDEX IF NOT EXISTS idx_cell_data ON cell USING gin(data)
|
|
103
131
|
WHERE deleted_at IS NULL`,
|
|
104
132
|
`CREATE INDEX IF NOT EXISTS idx_cell_refs ON cell USING gin(refs)
|
|
105
133
|
WHERE refs IS NOT NULL AND deleted_at IS NULL`,
|
|
106
134
|
`CREATE INDEX IF NOT EXISTS idx_cell_created_by ON cell(created_by)
|
|
107
135
|
WHERE deleted_at IS NULL`,
|
|
136
|
+
`CREATE INDEX IF NOT EXISTS idx_cell_root ON cell(root_id)
|
|
137
|
+
WHERE deleted_at IS NULL`,
|
|
138
|
+
`CREATE INDEX IF NOT EXISTS idx_cell_moderation_pending ON cell(root_id)
|
|
139
|
+
WHERE moderation = 'pending'`,
|
|
108
140
|
];
|
|
109
141
|
/**
|
|
110
142
|
* `cell_grant` table — resource-side ACL for cells. Each row admits a
|
|
@@ -42,9 +42,13 @@ import type { CellVisibility } from '../auth/cell_action_specs.js';
|
|
|
42
42
|
export interface CellRow {
|
|
43
43
|
id: Uuid;
|
|
44
44
|
data: CellData;
|
|
45
|
+
kind: string | null;
|
|
45
46
|
visibility: CellVisibility;
|
|
46
47
|
path: string | null;
|
|
47
48
|
refs: Array<FactHash> | null;
|
|
49
|
+
parent_id: Uuid | null;
|
|
50
|
+
root_id: Uuid | null;
|
|
51
|
+
moderation: string | null;
|
|
48
52
|
created_at: Date;
|
|
49
53
|
updated_at: Date | null;
|
|
50
54
|
deleted_at: Date | null;
|
|
@@ -52,11 +56,23 @@ export interface CellRow {
|
|
|
52
56
|
updated_by: Uuid | null;
|
|
53
57
|
grant_count: number;
|
|
54
58
|
}
|
|
55
|
-
/**
|
|
59
|
+
/**
|
|
60
|
+
* Input for `query_cell_create`. `refs` is derived from `data`. `kind` is
|
|
61
|
+
* the write-once capability axis (`cell.kind` column) — set here at INSERT
|
|
62
|
+
* and absent from `CellUpdatePatch`, so it can never change post-create.
|
|
63
|
+
* `parent_id` / `root_id` / `moderation` are the directory-tree + lifecycle
|
|
64
|
+
* columns the handler derives (parent containment, governing root, the
|
|
65
|
+
* create-authorizer verdict) — likewise write-once / control-gated, never on
|
|
66
|
+
* `CellUpdatePatch`.
|
|
67
|
+
*/
|
|
56
68
|
export interface CellCreateQueryInput {
|
|
57
69
|
data: Json;
|
|
70
|
+
kind?: string | null;
|
|
58
71
|
visibility?: CellVisibility;
|
|
59
72
|
path?: string | null;
|
|
73
|
+
parent_id?: Uuid | null;
|
|
74
|
+
root_id?: Uuid | null;
|
|
75
|
+
moderation?: string | null;
|
|
60
76
|
created_by?: Uuid | null;
|
|
61
77
|
}
|
|
62
78
|
/**
|
|
@@ -153,15 +169,37 @@ export declare const query_cell_delete: (deps: QueryDeps, id: Uuid, options?: {
|
|
|
153
169
|
deleted_by?: Uuid | null;
|
|
154
170
|
}) => Promise<boolean>;
|
|
155
171
|
/**
|
|
156
|
-
*
|
|
157
|
-
*
|
|
172
|
+
* Transition a contribution's `moderation` marker (the `cell_moderate` verb's
|
|
173
|
+
* write). On `set_visibility_public` (an approval) it also flips `visibility`
|
|
174
|
+
* to `'public'` so the approved contribution goes live; a rejection leaves
|
|
175
|
+
* visibility untouched (stays private). `updated_at` / `updated_by` are stamped.
|
|
176
|
+
*
|
|
177
|
+
* `moderation` is deliberately **not** writable through `query_cell_update`
|
|
178
|
+
* (it's absent from `CellUpdatePatch`) — gating the transition on a dedicated
|
|
179
|
+
* query (with the manage-tier authority check in the handler) is what stops an
|
|
180
|
+
* author self-approving their own pending cell.
|
|
181
|
+
*
|
|
182
|
+
* @param deps - query deps
|
|
183
|
+
* @param id - cell id
|
|
184
|
+
* @param moderation - the terminal marker to write (`'approved'` | `'rejected'`)
|
|
185
|
+
* @param options - `set_visibility_public` flips visibility on approval; `updated_by` records the moderator
|
|
186
|
+
* @returns the updated row, or `null` when no active row matched (raced delete)
|
|
187
|
+
* @mutates `cell` - sets `moderation` (+ maybe `visibility`) on one row
|
|
188
|
+
*/
|
|
189
|
+
export declare const query_cell_set_moderation: (deps: QueryDeps, id: Uuid, moderation: string, options: {
|
|
190
|
+
set_visibility_public: boolean;
|
|
191
|
+
updated_by?: Uuid | null;
|
|
192
|
+
}) => Promise<CellRow | null>;
|
|
193
|
+
/**
|
|
194
|
+
* List active cells with the given `kind`, newest first. Uses the
|
|
195
|
+
* `idx_cell_kind` index (`cell.kind = ?`).
|
|
158
196
|
*
|
|
159
197
|
* @param deps - query deps
|
|
160
|
-
* @param kind - `
|
|
198
|
+
* @param kind - `cell.kind` value to match (e.g. `'collection'`, `'entry'`)
|
|
161
199
|
* @param options - pagination
|
|
162
200
|
* @returns matching active rows
|
|
163
201
|
*/
|
|
164
|
-
export declare const
|
|
202
|
+
export declare const query_cell_list_by_kind: (deps: QueryDeps, kind: string, options?: Pick<CellListOptions, "limit" | "offset">) => Promise<Array<CellRow>>;
|
|
165
203
|
/**
|
|
166
204
|
* List active cells created by an actor, newest first. Backed by the
|
|
167
205
|
* `idx_cell_created_by` partial index.
|
|
@@ -221,8 +259,8 @@ export declare const query_cell_list_by_creator: (deps: QueryDeps, actor_id: Uui
|
|
|
221
259
|
export declare const query_cell_list: (deps: QueryDeps, params: CellListParams) => Promise<Array<CellRow>>;
|
|
222
260
|
/** Parameters for `query_cell_list`. All filter dimensions are optional. */
|
|
223
261
|
export interface CellListParams {
|
|
224
|
-
/** Match `
|
|
225
|
-
|
|
262
|
+
/** Match `cell.kind = ?` (uses `idx_cell_kind`). */
|
|
263
|
+
kind?: string;
|
|
226
264
|
/**
|
|
227
265
|
* Match `cell.visibility = ?` directly on the top-level column.
|
|
228
266
|
* Additional narrowing on top of the SQL-side auth visibility
|
|
@@ -240,6 +278,19 @@ export interface CellListParams {
|
|
|
240
278
|
* literal matching.
|
|
241
279
|
*/
|
|
242
280
|
path_prefix?: string;
|
|
281
|
+
/** Scope to a directory subtree by governing root (`cell.root_id = ?`, uses `idx_cell_root`). */
|
|
282
|
+
root_id?: Uuid;
|
|
283
|
+
/**
|
|
284
|
+
* Filter by moderation lifecycle marker (`'pending'` drives the mod queue via
|
|
285
|
+
* `idx_cell_moderation_pending`).
|
|
286
|
+
*
|
|
287
|
+
* Note: the visibility predicate still applies, so a `'pending'` (private)
|
|
288
|
+
* queue surfaces only to viewers it admits — admin / owner / grant. A
|
|
289
|
+
* non-admin container manager won't see pending children here until a
|
|
290
|
+
* manager-scoped visibility branch is added, so for now the queue is an admin
|
|
291
|
+
* surface (`cell_moderate` of a known id still works regardless).
|
|
292
|
+
*/
|
|
293
|
+
moderation?: string;
|
|
243
294
|
/**
|
|
244
295
|
* Batch-fetch by id. The visibility predicate still runs, so callers
|
|
245
296
|
* passing ids they can't view simply get fewer rows back. Order of
|
|
@@ -286,7 +337,7 @@ export interface CellListParams {
|
|
|
286
337
|
/**
|
|
287
338
|
* When `true`, narrow to cells admitting the caller via a
|
|
288
339
|
* `cell_grant` row AND that the caller does not own. Authenticated
|
|
289
|
-
* only (`viewer_actor_id` must be set). Combine with `
|
|
340
|
+
* only (`viewer_actor_id` must be set). Combine with `kind` /
|
|
290
341
|
* `path_prefix` etc. to scope further.
|
|
291
342
|
*/
|
|
292
343
|
shared_with_caller_only?: boolean;
|
|
@@ -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,EAAC,SAAS,EAAC,MAAM,iBAAiB,CAAC;AAC/C,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,0BAA0B,CAAC;AACnD,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAyB,KAAK,QAAQ,EAAC,MAAM,+BAA+B,CAAC;AAGpF,OAAO,KAAK,EAAC,QAAQ,EAAC,MAAM,6BAA6B,CAAC;AAC1D,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,8BAA8B,CAAC;AAEjE;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,OAAO;IACvB,EAAE,EAAE,IAAI,CAAC;IACT,IAAI,EAAE,QAAQ,CAAC;IACf,UAAU,EAAE,cAAc,CAAC;IAC3B,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,IAAI,EAAE,KAAK,CAAC,QAAQ,CAAC,GAAG,IAAI,CAAC;IAC7B,UAAU,EAAE,IAAI,CAAC;IACjB,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,UAAU,EAAE,IAAI,GAAG,IAAI,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;CACpB;AAgBD
|
|
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
|
@@ -51,13 +51,17 @@ const grant_count_projection = (cell_alias) => `(SELECT COUNT(*)::int FROM cell_
|
|
|
51
51
|
export const query_cell_create = async (deps, input) => {
|
|
52
52
|
const refs = derive_refs(input.data);
|
|
53
53
|
const row = await deps.db.query_one(`INSERT INTO cell
|
|
54
|
-
(data, visibility, path, refs, created_by)
|
|
55
|
-
VALUES ($1::jsonb, COALESCE($
|
|
54
|
+
(data, kind, visibility, path, refs, parent_id, root_id, moderation, created_by)
|
|
55
|
+
VALUES ($1::jsonb, $2, COALESCE($3::cell_visibility, 'private'::cell_visibility), $4, $5::text[], $6, $7, $8, $9)
|
|
56
56
|
RETURNING *, ${grant_count_projection('cell')}`, [
|
|
57
57
|
JSON.stringify(input.data),
|
|
58
|
+
input.kind ?? null,
|
|
58
59
|
input.visibility ?? null,
|
|
59
60
|
input.path ?? null,
|
|
60
61
|
refs,
|
|
62
|
+
input.parent_id ?? null,
|
|
63
|
+
input.root_id ?? null,
|
|
64
|
+
input.moderation ?? null,
|
|
61
65
|
input.created_by ?? null,
|
|
62
66
|
]);
|
|
63
67
|
return assert_row(row, 'INSERT INTO cell');
|
|
@@ -171,20 +175,48 @@ export const query_cell_delete = async (deps, id, options) => {
|
|
|
171
175
|
return row !== undefined;
|
|
172
176
|
};
|
|
173
177
|
/**
|
|
174
|
-
*
|
|
175
|
-
*
|
|
178
|
+
* Transition a contribution's `moderation` marker (the `cell_moderate` verb's
|
|
179
|
+
* write). On `set_visibility_public` (an approval) it also flips `visibility`
|
|
180
|
+
* to `'public'` so the approved contribution goes live; a rejection leaves
|
|
181
|
+
* visibility untouched (stays private). `updated_at` / `updated_by` are stamped.
|
|
182
|
+
*
|
|
183
|
+
* `moderation` is deliberately **not** writable through `query_cell_update`
|
|
184
|
+
* (it's absent from `CellUpdatePatch`) — gating the transition on a dedicated
|
|
185
|
+
* query (with the manage-tier authority check in the handler) is what stops an
|
|
186
|
+
* author self-approving their own pending cell.
|
|
187
|
+
*
|
|
188
|
+
* @param deps - query deps
|
|
189
|
+
* @param id - cell id
|
|
190
|
+
* @param moderation - the terminal marker to write (`'approved'` | `'rejected'`)
|
|
191
|
+
* @param options - `set_visibility_public` flips visibility on approval; `updated_by` records the moderator
|
|
192
|
+
* @returns the updated row, or `null` when no active row matched (raced delete)
|
|
193
|
+
* @mutates `cell` - sets `moderation` (+ maybe `visibility`) on one row
|
|
194
|
+
*/
|
|
195
|
+
export const query_cell_set_moderation = async (deps, id, moderation, options) => {
|
|
196
|
+
const row = await deps.db.query_one(`UPDATE cell SET
|
|
197
|
+
moderation = $2,
|
|
198
|
+
visibility = CASE WHEN $3::bool THEN 'public'::cell_visibility ELSE visibility END,
|
|
199
|
+
updated_by = $4,
|
|
200
|
+
updated_at = NOW()
|
|
201
|
+
WHERE id = $1 AND deleted_at IS NULL
|
|
202
|
+
RETURNING *, ${grant_count_projection('cell')}`, [id, moderation, options.set_visibility_public, options.updated_by ?? null]);
|
|
203
|
+
return row ?? null;
|
|
204
|
+
};
|
|
205
|
+
/**
|
|
206
|
+
* List active cells with the given `kind`, newest first. Uses the
|
|
207
|
+
* `idx_cell_kind` index (`cell.kind = ?`).
|
|
176
208
|
*
|
|
177
209
|
* @param deps - query deps
|
|
178
|
-
* @param kind - `
|
|
210
|
+
* @param kind - `cell.kind` value to match (e.g. `'collection'`, `'entry'`)
|
|
179
211
|
* @param options - pagination
|
|
180
212
|
* @returns matching active rows
|
|
181
213
|
*/
|
|
182
|
-
export const
|
|
214
|
+
export const query_cell_list_by_kind = async (deps, kind, options) => deps.db.query(`SELECT *, ${grant_count_projection('cell')}
|
|
183
215
|
FROM cell
|
|
184
|
-
WHERE
|
|
216
|
+
WHERE kind = $1
|
|
185
217
|
AND deleted_at IS NULL
|
|
186
218
|
ORDER BY created_at DESC
|
|
187
|
-
LIMIT $2 OFFSET $3`, [
|
|
219
|
+
LIMIT $2 OFFSET $3`, [kind, options?.limit ?? null, options?.offset ?? 0]);
|
|
188
220
|
/**
|
|
189
221
|
* List active cells created by an actor, newest first. Backed by the
|
|
190
222
|
* `idx_cell_created_by` partial index.
|
|
@@ -271,7 +303,7 @@ export const query_cell_list = async (deps, params) => {
|
|
|
271
303
|
// supplied wildcards (`%`, `_`, `\`) match literally — Postgres 11+ has
|
|
272
304
|
// the function and pglite/PG 16 supports it.
|
|
273
305
|
//
|
|
274
|
-
// Two SQL shapes share the same
|
|
306
|
+
// Two SQL shapes share the same 16-param positional layout:
|
|
275
307
|
//
|
|
276
308
|
// - `shared_with_caller_only: false` — cell-driven scan with the
|
|
277
309
|
// visibility predicate (admin / public / owner / grant-admits).
|
|
@@ -284,7 +316,7 @@ export const query_cell_list = async (deps, params) => {
|
|
|
284
316
|
: build_general_sql(order_column, order_direction);
|
|
285
317
|
return deps.db.query(sql, [
|
|
286
318
|
params.include_deleted === true,
|
|
287
|
-
params.
|
|
319
|
+
params.kind ?? null,
|
|
288
320
|
params.ref ?? null,
|
|
289
321
|
params.created_by ?? null,
|
|
290
322
|
params.path_prefix ?? null,
|
|
@@ -297,6 +329,8 @@ export const query_cell_list = async (deps, params) => {
|
|
|
297
329
|
role_grant_roles,
|
|
298
330
|
role_grant_scope_ids,
|
|
299
331
|
params.visibility ?? null,
|
|
332
|
+
params.root_id ?? null,
|
|
333
|
+
params.moderation ?? null,
|
|
300
334
|
]);
|
|
301
335
|
};
|
|
302
336
|
/**
|
|
@@ -333,12 +367,14 @@ const CALLER_ROLE_GRANTS_CTE = `WITH caller_role_grants AS (
|
|
|
333
367
|
const build_general_sql = (order_column, order_direction) => `${CALLER_ROLE_GRANTS_CTE}
|
|
334
368
|
SELECT c.*, ${grant_count_projection('c')} FROM cell c
|
|
335
369
|
WHERE ($1::bool OR c.deleted_at IS NULL)
|
|
336
|
-
AND ($2::text IS NULL OR c.
|
|
370
|
+
AND ($2::text IS NULL OR c.kind = $2::text)
|
|
337
371
|
AND ($14::cell_visibility IS NULL OR c.visibility = $14::cell_visibility)
|
|
338
372
|
AND ($3::text IS NULL OR c.refs @> ARRAY[$3]::text[])
|
|
339
373
|
AND ($4::uuid IS NULL OR c.created_by = $4)
|
|
340
374
|
AND ($5::text IS NULL OR starts_with(c.path, $5))
|
|
341
375
|
AND ($10::uuid[] IS NULL OR c.id = ANY($10))
|
|
376
|
+
AND ($15::uuid IS NULL OR c.root_id = $15)
|
|
377
|
+
AND ($16::text IS NULL OR c.moderation = $16::text)
|
|
342
378
|
AND (
|
|
343
379
|
$6::bool
|
|
344
380
|
OR c.visibility = 'public'
|
|
@@ -369,12 +405,14 @@ const build_general_sql = (order_column, order_direction) => `${CALLER_ROLE_GRAN
|
|
|
369
405
|
const build_shared_with_sql = (order_column, order_direction) => `${CALLER_ROLE_GRANTS_CTE}
|
|
370
406
|
SELECT c.*, ${grant_count_projection('c')} FROM cell c
|
|
371
407
|
WHERE ($1::bool OR c.deleted_at IS NULL)
|
|
372
|
-
AND ($2::text IS NULL OR c.
|
|
408
|
+
AND ($2::text IS NULL OR c.kind = $2::text)
|
|
373
409
|
AND ($14::cell_visibility IS NULL OR c.visibility = $14::cell_visibility)
|
|
374
410
|
AND ($3::text IS NULL OR c.refs @> ARRAY[$3]::text[])
|
|
375
411
|
AND ($4::uuid IS NULL OR c.created_by = $4)
|
|
376
412
|
AND ($5::text IS NULL OR starts_with(c.path, $5))
|
|
377
413
|
AND ($10::uuid[] IS NULL OR c.id = ANY($10))
|
|
414
|
+
AND ($15::uuid IS NULL OR c.root_id = $15)
|
|
415
|
+
AND ($16::text IS NULL OR c.moderation = $16::text)
|
|
378
416
|
AND $7::uuid IS NOT NULL
|
|
379
417
|
AND c.created_by IS DISTINCT FROM $7
|
|
380
418
|
AND c.id IN (
|
package/dist/testing/CLAUDE.md
CHANGED
|
@@ -897,8 +897,9 @@ source of truth for wire-shape conformance.
|
|
|
897
897
|
|
|
898
898
|
- `testing/cross_backend/capabilities.ts` — `BackendCapabilities` vocabulary
|
|
899
899
|
(the **gating** flags `ws` / `sse` / `cell_crud` / `cell_relations` /
|
|
900
|
-
`
|
|
901
|
-
`oversized_reject_closes_connection` / `peer_request` —
|
|
900
|
+
`cell_gated_create` / `account_lifecycle` / `fact_serving` / `ready` /
|
|
901
|
+
`account_status` / `oversized_reject_closes_connection` / `peer_request` —
|
|
902
|
+
each has a `test_if`
|
|
902
903
|
reader; `peer_request` (server-initiated requests — the ActionPeer
|
|
903
904
|
`peer/ping` round-trip) is `true` for both the Rust spine and the TS spine
|
|
904
905
|
(`BackendWebsocketTransport.request_connection`); `ts_default_capabilities`
|
|
@@ -914,6 +915,15 @@ source of truth for wire-shape conformance.
|
|
|
914
915
|
parity suite — both `true` on every backend that live-mounts the full cell
|
|
915
916
|
surface (TS spine binary, in-process app, Rust stub). A backend mounting only
|
|
916
917
|
plain CRUD would declare `cell_crud: true, cell_relations: false`.
|
|
918
|
+
`cell_gated_create` gates `describe_cell_gated_create_cross_tests` +
|
|
919
|
+
`describe_cell_moderate_cross_tests` (the parent-aware-creation-authorizer +
|
|
920
|
+
`cell_moderate` parity proofs); unlike `cell_crud`/`cell_relations`
|
|
921
|
+
it is **cross-only** (`false` in-process) — `true` only on the reference
|
|
922
|
+
spine binaries that live-mount the `test_cell_gated_create_authorize` policy
|
|
923
|
+
(the TS spine `full_spine_mount` + the Rust `testing_spine_stub`), since the
|
|
924
|
+
authorizer _behavior_ adds no method/wire shape and so the schema + manifest
|
|
925
|
+
parity gates are blind to an authorizer divergence (the `cell_moderate`
|
|
926
|
+
_verb_ it pairs with is on the manifest, separately gated).
|
|
917
927
|
`account_lifecycle` gates `describe_account_lifecycle_cross_tests` (the
|
|
918
928
|
`account_delete` / `account_undelete` / `account_purge` parity suite); unlike
|
|
919
929
|
cells those verbs **are** on the declared surface (in `create_admin_actions`,
|
|
@@ -1154,7 +1164,7 @@ the stream \_can't* open on a spine without SSE — a self-cleaning tripwire for
|
|
|
1154
1164
|
the spine that should grow it, distinct from the consumer-legit capability
|
|
1155
1165
|
skip the shared suite emits.
|
|
1156
1166
|
|
|
1157
|
-
### `cross_backend/cell_crud.ts` + `cell_relations.ts` — cell parity suites
|
|
1167
|
+
### `cross_backend/cell_crud.ts` + `cell_relations.ts` + `cell_gated_create.ts` — cell parity suites
|
|
1158
1168
|
|
|
1159
1169
|
The cell-layer parity coverage is split across two sibling suites. Cells
|
|
1160
1170
|
can't ride the generic `describe_rpc_round_trip_tests` (stateful verbs need a
|
|
@@ -1189,10 +1199,30 @@ backdoor, cell).
|
|
|
1189
1199
|
holder who can `cell_get` still gets the IDOR 404). Only **actor-shaped**
|
|
1190
1200
|
grants are exercised — role-shaped principals need a closed role registry the
|
|
1191
1201
|
Rust spine deliberately lacks.
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1202
|
+
- **`describe_cell_gated_create_cross_tests`** + **`describe_cell_moderate_cross_tests`**
|
|
1203
|
+
(gate on `capabilities.cell_gated_create`, **cross-only**) — the parent-aware
|
|
1204
|
+
cell-creation authorizer (`CellCreateAuthorize`, **directory model**) +
|
|
1205
|
+
the `cell_moderate` verb parity proofs. Both spines mount the same
|
|
1206
|
+
`test_cell_gated_create_authorize` policy: a `kind: 'space'` **root** is
|
|
1207
|
+
admin-only (a non-admin → **403** `cell_create_forbidden`); a **contribution**
|
|
1208
|
+
under a space is gated by the root's `data.policy[kind] = {min_role,
|
|
1209
|
+
moderation_required}`, with `moderation` set per the verdict (`pending`+private
|
|
1210
|
+
vs `approved`+author-visibility); a **hidden** parent **404**-masks vs a
|
|
1211
|
+
**visible** one **403**s; plain parentless creates stay open. The moderate
|
|
1212
|
+
suite then proves the `pending → approved | rejected` transition: an admin
|
|
1213
|
+
(root manager) approves → public, the **author** self-approve → 403
|
|
1214
|
+
(anti-self-approval), a non-viewer → 404, reject → stays private. The
|
|
1215
|
+
authorizer _behavior_ adds no method/wire shape, so schema + manifest parity
|
|
1216
|
+
are **blind** to a divergence — these are the only gates that catch one (the
|
|
1217
|
+
`cell_moderate` verb itself is manifest-covered). In-process hook coverage is
|
|
1218
|
+
the standalone `auth/cell_create_authorize.db.test.ts` (these cross suites skip
|
|
1219
|
+
in-process, where the default app mounts no authorizer).
|
|
1220
|
+
|
|
1221
|
+
`cell_crud` / `cell_relations` gate `true` on TS + Rust (cells run on both, no
|
|
1222
|
+
`.skip`); `cell_gated_create` is cross-only (the policy is a spine-binary
|
|
1223
|
+
fixture). Cross-process wiring is `src/test/cross_backend/cell.cross.test.ts`
|
|
1224
|
+
(all the cell suites); the in-process legs (plain `gro test`) for crud/relations
|
|
1225
|
+
are `src/test/auth/cell_crud_parity.db.test.ts`
|
|
1196
1226
|
|
|
1197
1227
|
- `cell_relations_parity.db.test.ts`, sharing the full-surface
|
|
1198
1228
|
`create_cell_parity_setup` (`cell_parity_helpers.ts`) which mounts every cell
|