@ai-matrx/associations 0.4.0 → 0.5.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/CHANGELOG.md +62 -0
- package/README.md +3 -3
- package/dist/core/index.cjs +244 -3
- package/dist/core/index.cjs.map +1 -1
- package/dist/core/index.d.cts +67 -3
- package/dist/core/index.d.ts +67 -3
- package/dist/core/index.js +244 -3
- package/dist/core/index.js.map +1 -1
- package/dist/index.cjs +5 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +106 -1
- package/dist/index.d.ts +106 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +537 -53
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +252 -4
- package/dist/react/index.d.ts +252 -4
- package/dist/react/index.js +489 -5
- package/dist/react/index.js.map +1 -1
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,67 @@
|
|
|
1
1
|
# Changelog — @ai-matrx/associations
|
|
2
2
|
|
|
3
|
+
## 0.5.0 — 2026-08-29 (unreleased; W6 — the comments canonical UI)
|
|
4
|
+
|
|
5
|
+
W6 per rulings C4/C18: the comments canonical UI is built IN this package —
|
|
6
|
+
a GREENFIELD build (matrx-frontend had only a 165-line service, zero
|
|
7
|
+
components), designed against the platform comments satellite
|
|
8
|
+
(`platform.comments` behind the four `cmt_*` SECURITY-DEFINER RPCs, all
|
|
9
|
+
four verified live: SECURITY DEFINER + authenticated EXECUTE on the
|
|
10
|
+
platform DB).
|
|
11
|
+
|
|
12
|
+
- **Demanded schema grows to 27 functions**: the `cmt_list` / `cmt_add` /
|
|
13
|
+
`cmt_edit` / `cmt_delete` family added to `DEMANDED_RPC_FAMILIES`,
|
|
14
|
+
regenerated from the live-generated DB contract; `assertDemandedSchema`
|
|
15
|
+
probes all four with the unparseable-uuid sentinel (every cmt_* fn takes
|
|
16
|
+
a uuid arg, so 22P02 fires before any body runs — zero side effects).
|
|
17
|
+
- **`/core` — `createCommentsService`
|
|
18
|
+
(`src/core/commentsService.ts`)** — the `cmt_*` chokepoint, behavior
|
|
19
|
+
ported from matrx-frontend `features/comments/service/commentsService.ts`
|
|
20
|
+
with the package's guard wall added in front (entity token writes-strict
|
|
21
|
+
per C18 + legacy-alias normalization, uuid wall on entityId/parentId/
|
|
22
|
+
commentId, non-empty body). Access decisions stay in the RPCs (edit is
|
|
23
|
+
author-only, delete author-or-org-member, list org-filtered) — link ≠
|
|
24
|
+
grant, zero access decisions here.
|
|
25
|
+
- **`/core` — the comment-thread cache slice on the store**
|
|
26
|
+
(`getComments` / `loadComments` / `subscribeComments` / `addComment` /
|
|
27
|
+
`editComment` / `deleteComment` / `invalidateComments`, +
|
|
28
|
+
`services.comments`; `invalidateAll` clears comment threads too): same
|
|
29
|
+
lifecycle contract as the association cache — per-key in-flight dedup,
|
|
30
|
+
ready short-circuit unless forced, pending retains previous comments AND
|
|
31
|
+
the visible error, stable frozen idle default, writes force-reload the
|
|
32
|
+
thread on success only.
|
|
33
|
+
- **Root types (`src/comments.ts`)** — `PlatformComment` / `CommentAuthor`
|
|
34
|
+
/ `CommentsEntry`, ported from `features/comments/types.ts` and renamed
|
|
35
|
+
to the package vocabulary.
|
|
36
|
+
- **NEW `authorDisplay` port (`src/ports.ts` + provider)** — the package
|
|
37
|
+
cannot resolve user profiles itself; `cmt_list` denormalizes
|
|
38
|
+
email/displayName/avatarUrl and a host with a richer profile store
|
|
39
|
+
overrides them through this optional function port. DEGRADATION: absent →
|
|
40
|
+
the denormalized fields render as-is; a row with neither displayName nor
|
|
41
|
+
email shows "Unknown user" + the initial avatar. Never blank, never a
|
|
42
|
+
crash.
|
|
43
|
+
- **`/react` — `useComments`** — the thread of comments on any entity
|
|
44
|
+
(token + id) via `useSyncExternalStore`; `currentUserId` is the LENIENT
|
|
45
|
+
identity read gating the own-comment affordances (writes stay loud
|
|
46
|
+
through the services).
|
|
47
|
+
- **`/react` — `CommentThread` (+ `CommentComposer`, `buildCommentTree`,
|
|
48
|
+
`formatRelativeTime`)** — the canonical face: threaded list
|
|
49
|
+
(oldest→newest, replies nested by `parentId`, orphaned replies promoted
|
|
50
|
+
to roots — never dropped), composer (Cmd/Ctrl+Enter submits; a failed
|
|
51
|
+
post keeps the text beside the inline error — input is never lost),
|
|
52
|
+
per-item reply, edit-own inline, delete-own as a TWO-STEP that names the
|
|
53
|
+
consequence ("Deletes this comment for everyone.") before the
|
|
54
|
+
destructive click exists, relative timestamps via
|
|
55
|
+
`Intl.RelativeTimeFormat` (+ absolute on hover, "(edited)" marker),
|
|
56
|
+
author names through the `entityDoors.EntityRef` door when bound (token
|
|
57
|
+
`user`), avatars/names through `authorDisplay`. Chrome from
|
|
58
|
+
design-system (`Button`/`Skeleton`/`cn`, peer >=0.3.0 unchanged); glyphs
|
|
59
|
+
inlined (C19 — `RefreshIcon`/`SpinnerIcon`/`CornerDownRightIcon`
|
|
60
|
+
reused, zero new icon deps).
|
|
61
|
+
- 30 new behavioral tests (11 /core service+slice, 12 jsdom face tests +
|
|
62
|
+
tree/relative-time units) → **177 total**; full gate green (`typecheck`,
|
|
63
|
+
`test`, `check:package` incl. the packed-tarball canary).
|
|
64
|
+
|
|
3
65
|
## 0.4.0 — 2026-08-29 (unreleased; W4 completion)
|
|
4
66
|
|
|
5
67
|
The three faces STOPPED at the W3+W4 build for missing design-system
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
isEntityTypeToken, // runtime guard, narrows to the union
|
|
19
19
|
type AssociationEdge, // one assoc_for_entity row (both directions)
|
|
20
20
|
type AssociationsRpcResult, // the never-throw result envelope
|
|
21
|
-
type DemandedRpcName, // the
|
|
21
|
+
type DemandedRpcName, // the 27-function demanded RPC surface
|
|
22
22
|
type AssociationsConfig, // the seven ports a host binds
|
|
23
23
|
} from "@ai-matrx/associations";
|
|
24
24
|
```
|
|
@@ -92,9 +92,9 @@ and probed (`assertDemandedSchema` in `/core`). Every violation
|
|
|
92
92
|
`(source_type, source_id, target_type, target_id, role)` `NULLS NOT DISTINCT`.
|
|
93
93
|
Type columns are FK-validated against `platform.entity_types.token`.
|
|
94
94
|
2. **RPC-only.** The client holds **no grant** on the table. Every operation goes
|
|
95
|
-
through the
|
|
95
|
+
through the 27 SECURITY DEFINER functions in `DEMANDED_RPC_NAMES` (`assoc_*`
|
|
96
96
|
×8, `conversation_file*` ×3, `agent_resource_*` ×2, `cat_*` ×5, `ues_*` ×4,
|
|
97
|
-
`reference_search_candidates`). Those readers consume the live-only
|
|
97
|
+
`reference_search_candidates`, and the W6 comments family `cmt_*` ×4). Those readers consume the live-only
|
|
98
98
|
`associations_live` view, so tombstone semantics (a trashed entity
|
|
99
99
|
soft-removes its edges; restore revives them) hold by construction — this
|
|
100
100
|
package never writes a `.is("deleted_at", null)` filter. The DB's legacy
|
package/dist/core/index.cjs
CHANGED
|
@@ -33,6 +33,7 @@ __export(core_exports, {
|
|
|
33
33
|
createAssociationsStore: () => createAssociationsStore,
|
|
34
34
|
createCandidatesService: () => createCandidatesService,
|
|
35
35
|
createCategoriesService: () => createCategoriesService,
|
|
36
|
+
createCommentsService: () => createCommentsService,
|
|
36
37
|
createEntityRegistry: () => createEntityRegistry,
|
|
37
38
|
createEntityRowsService: () => createEntityRowsService,
|
|
38
39
|
createFavoritesService: () => createFavoritesService,
|
|
@@ -2714,6 +2715,122 @@ function createAssociationHelpers(service) {
|
|
|
2714
2715
|
};
|
|
2715
2716
|
}
|
|
2716
2717
|
|
|
2718
|
+
// src/core/commentsService.ts
|
|
2719
|
+
function toComment(row) {
|
|
2720
|
+
return {
|
|
2721
|
+
id: row.id,
|
|
2722
|
+
orgId: row.organization_id ?? null,
|
|
2723
|
+
entityType: row.entity_type,
|
|
2724
|
+
entityId: row.entity_id,
|
|
2725
|
+
parentId: row.parent_id ?? null,
|
|
2726
|
+
body: row.body,
|
|
2727
|
+
createdAt: row.created_at,
|
|
2728
|
+
updatedAt: row.updated_at,
|
|
2729
|
+
createdBy: row.created_by ?? null,
|
|
2730
|
+
author: {
|
|
2731
|
+
email: row.author_email ?? null,
|
|
2732
|
+
displayName: row.author_display_name ?? null,
|
|
2733
|
+
avatarUrl: row.author_avatar_url ?? null
|
|
2734
|
+
}
|
|
2735
|
+
};
|
|
2736
|
+
}
|
|
2737
|
+
function createCommentsService(deps) {
|
|
2738
|
+
const { dataSource, identity, guards } = deps;
|
|
2739
|
+
const { ok: ok2, err: err2, mapPgError, mapPgErrorPair } = deps.rpc;
|
|
2740
|
+
return {
|
|
2741
|
+
/**
|
|
2742
|
+
* All comments on `${entityType}:${entityId}`, org-filtered by RLS
|
|
2743
|
+
* inside the RPC, ordered oldest→newest. Build the thread from each
|
|
2744
|
+
* row's `parentId` (top-level comments have `parentId === null`).
|
|
2745
|
+
*/
|
|
2746
|
+
async listForEntity(entityType, entityId) {
|
|
2747
|
+
try {
|
|
2748
|
+
identity.requireUserId();
|
|
2749
|
+
const token = guards.normalizeEntityToken(entityType);
|
|
2750
|
+
const invalid = firstError(
|
|
2751
|
+
guards.checkToken("entityType", token),
|
|
2752
|
+
guards.checkUuid("entityId", entityId)
|
|
2753
|
+
);
|
|
2754
|
+
if (invalid) return { ok: false, error: invalid };
|
|
2755
|
+
const { data, error } = await dataSource.rpc("cmt_list", {
|
|
2756
|
+
p_entity_type: token,
|
|
2757
|
+
p_entity_id: entityId
|
|
2758
|
+
});
|
|
2759
|
+
if (error) return err2(...mapPgErrorPair(error));
|
|
2760
|
+
const rows = Array.isArray(data) ? data : [];
|
|
2761
|
+
return ok2({ comments: rows.map(toComment) });
|
|
2762
|
+
} catch (e) {
|
|
2763
|
+
return { ok: false, error: mapPgError(e) };
|
|
2764
|
+
}
|
|
2765
|
+
},
|
|
2766
|
+
/**
|
|
2767
|
+
* Post `body` on `${entityType}:${entityId}`; pass `parentId` to reply.
|
|
2768
|
+
* Returns the new comment id.
|
|
2769
|
+
*/
|
|
2770
|
+
async add(args) {
|
|
2771
|
+
try {
|
|
2772
|
+
identity.requireUserId();
|
|
2773
|
+
const token = guards.normalizeEntityToken(args.entityType);
|
|
2774
|
+
const invalid = firstError(
|
|
2775
|
+
guards.checkToken("entityType", token),
|
|
2776
|
+
guards.checkUuid("entityId", args.entityId),
|
|
2777
|
+
args.parentId != null ? guards.checkUuid("parentId", args.parentId) : null
|
|
2778
|
+
);
|
|
2779
|
+
if (invalid) return { ok: false, error: invalid };
|
|
2780
|
+
if (!args.body || args.body.trim().length === 0) {
|
|
2781
|
+
return err2("invalid_argument", "comment body must be non-empty");
|
|
2782
|
+
}
|
|
2783
|
+
const { data, error } = await dataSource.rpc("cmt_add", {
|
|
2784
|
+
p_entity_type: token,
|
|
2785
|
+
p_entity_id: args.entityId,
|
|
2786
|
+
p_body: args.body,
|
|
2787
|
+
p_parent_id: args.parentId ?? void 0,
|
|
2788
|
+
p_org_id: args.orgId ?? void 0
|
|
2789
|
+
});
|
|
2790
|
+
if (error) return err2(...mapPgErrorPair(error));
|
|
2791
|
+
if (!data || typeof data !== "string") {
|
|
2792
|
+
return err2("internal", "cmt_add returned no comment id");
|
|
2793
|
+
}
|
|
2794
|
+
return ok2({ id: data });
|
|
2795
|
+
} catch (e) {
|
|
2796
|
+
return { ok: false, error: mapPgError(e) };
|
|
2797
|
+
}
|
|
2798
|
+
},
|
|
2799
|
+
/** Replace a comment's body. Author-only; the RPC rejects everyone else. */
|
|
2800
|
+
async edit(id, body) {
|
|
2801
|
+
try {
|
|
2802
|
+
identity.requireUserId();
|
|
2803
|
+
const invalid = guards.checkUuid("commentId", id);
|
|
2804
|
+
if (invalid) return { ok: false, error: invalid };
|
|
2805
|
+
if (!body || body.trim().length === 0) {
|
|
2806
|
+
return err2("invalid_argument", "comment body must be non-empty");
|
|
2807
|
+
}
|
|
2808
|
+
const { error } = await dataSource.rpc("cmt_edit", {
|
|
2809
|
+
p_id: id,
|
|
2810
|
+
p_body: body
|
|
2811
|
+
});
|
|
2812
|
+
if (error) return err2(...mapPgErrorPair(error));
|
|
2813
|
+
return ok2(null);
|
|
2814
|
+
} catch (e) {
|
|
2815
|
+
return { ok: false, error: mapPgError(e) };
|
|
2816
|
+
}
|
|
2817
|
+
},
|
|
2818
|
+
/** Soft-delete a comment. Author or org member; enforced server-side. */
|
|
2819
|
+
async remove(id) {
|
|
2820
|
+
try {
|
|
2821
|
+
identity.requireUserId();
|
|
2822
|
+
const invalid = guards.checkUuid("commentId", id);
|
|
2823
|
+
if (invalid) return { ok: false, error: invalid };
|
|
2824
|
+
const { error } = await dataSource.rpc("cmt_delete", { p_id: id });
|
|
2825
|
+
if (error) return err2(...mapPgErrorPair(error));
|
|
2826
|
+
return ok2(null);
|
|
2827
|
+
} catch (e) {
|
|
2828
|
+
return { ok: false, error: mapPgError(e) };
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
};
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2717
2834
|
// src/core/store.ts
|
|
2718
2835
|
function associationsKey(type, id) {
|
|
2719
2836
|
return `${type}:${id}`;
|
|
@@ -2730,6 +2847,12 @@ var IDLE_CATEGORIES = Object.freeze({
|
|
|
2730
2847
|
fetchedAt: null,
|
|
2731
2848
|
error: null
|
|
2732
2849
|
});
|
|
2850
|
+
var IDLE_COMMENTS = Object.freeze({
|
|
2851
|
+
status: "idle",
|
|
2852
|
+
comments: Object.freeze([]),
|
|
2853
|
+
fetchedAt: null,
|
|
2854
|
+
error: null
|
|
2855
|
+
});
|
|
2733
2856
|
function createAssociationsStore(config) {
|
|
2734
2857
|
if (!config || typeof config.dataSource?.rpc !== "function") {
|
|
2735
2858
|
throw new Error(
|
|
@@ -2763,12 +2886,16 @@ function createAssociationsStore(config) {
|
|
|
2763
2886
|
const titles = createTitlesService(deps, candidates);
|
|
2764
2887
|
const entityRows = createEntityRowsService(deps, titles);
|
|
2765
2888
|
const helpers = createAssociationHelpers(associations);
|
|
2889
|
+
const comments = createCommentsService(deps);
|
|
2766
2890
|
const assocByKey = /* @__PURE__ */ new Map();
|
|
2767
2891
|
const assocSubs = /* @__PURE__ */ new Map();
|
|
2768
2892
|
const assocInFlight = /* @__PURE__ */ new Map();
|
|
2769
2893
|
const catByDim = /* @__PURE__ */ new Map();
|
|
2770
2894
|
const catSubs = /* @__PURE__ */ new Map();
|
|
2771
2895
|
const catInFlight = /* @__PURE__ */ new Map();
|
|
2896
|
+
const cmtByKey = /* @__PURE__ */ new Map();
|
|
2897
|
+
const cmtSubs = /* @__PURE__ */ new Map();
|
|
2898
|
+
const cmtInFlight = /* @__PURE__ */ new Map();
|
|
2772
2899
|
function notifyAssoc(key) {
|
|
2773
2900
|
const subs = assocSubs.get(key);
|
|
2774
2901
|
if (subs) for (const cb of [...subs]) cb();
|
|
@@ -2785,6 +2912,14 @@ function createAssociationsStore(config) {
|
|
|
2785
2912
|
catByDim.set(dimension, entry);
|
|
2786
2913
|
notifyCat(dimension);
|
|
2787
2914
|
}
|
|
2915
|
+
function notifyCmt(key) {
|
|
2916
|
+
const subs = cmtSubs.get(key);
|
|
2917
|
+
if (subs) for (const cb of [...subs]) cb();
|
|
2918
|
+
}
|
|
2919
|
+
function setCmt(key, entry) {
|
|
2920
|
+
cmtByKey.set(key, entry);
|
|
2921
|
+
notifyCmt(key);
|
|
2922
|
+
}
|
|
2788
2923
|
async function load(type, id, opts = {}) {
|
|
2789
2924
|
const force = opts.force ?? false;
|
|
2790
2925
|
if (!type || !id) return;
|
|
@@ -2874,6 +3009,49 @@ function createAssociationsStore(config) {
|
|
|
2874
3009
|
catInFlight.set(dimension, promise);
|
|
2875
3010
|
return promise;
|
|
2876
3011
|
}
|
|
3012
|
+
async function loadComments(type, id, opts = {}) {
|
|
3013
|
+
const force = opts.force ?? false;
|
|
3014
|
+
if (!type || !id) return;
|
|
3015
|
+
const key = associationsKey(type, id);
|
|
3016
|
+
const entry = cmtByKey.get(key);
|
|
3017
|
+
if (!force && entry?.status === "ready") return;
|
|
3018
|
+
const pending = cmtInFlight.get(key);
|
|
3019
|
+
if (!force && entry?.status === "loading" && pending) return pending;
|
|
3020
|
+
const prev = cmtByKey.get(key);
|
|
3021
|
+
setCmt(key, {
|
|
3022
|
+
status: "loading",
|
|
3023
|
+
comments: prev?.comments ?? [],
|
|
3024
|
+
fetchedAt: prev?.fetchedAt ?? null,
|
|
3025
|
+
// Retain the visible failure while a retry is pending (same contract
|
|
3026
|
+
// as the association cache).
|
|
3027
|
+
error: prev?.error ?? null
|
|
3028
|
+
});
|
|
3029
|
+
const promise = (async () => {
|
|
3030
|
+
try {
|
|
3031
|
+
const res = await comments.listForEntity(type, id);
|
|
3032
|
+
if (isAssociationsRpcErr(res)) {
|
|
3033
|
+
const before = cmtByKey.get(key);
|
|
3034
|
+
setCmt(key, {
|
|
3035
|
+
status: "error",
|
|
3036
|
+
comments: before?.comments ?? [],
|
|
3037
|
+
fetchedAt: before?.fetchedAt ?? null,
|
|
3038
|
+
error: res.error.message
|
|
3039
|
+
});
|
|
3040
|
+
} else {
|
|
3041
|
+
setCmt(key, {
|
|
3042
|
+
status: "ready",
|
|
3043
|
+
comments: res.data.comments,
|
|
3044
|
+
fetchedAt: Date.now(),
|
|
3045
|
+
error: null
|
|
3046
|
+
});
|
|
3047
|
+
}
|
|
3048
|
+
} finally {
|
|
3049
|
+
cmtInFlight.delete(key);
|
|
3050
|
+
}
|
|
3051
|
+
})();
|
|
3052
|
+
cmtInFlight.set(key, promise);
|
|
3053
|
+
return promise;
|
|
3054
|
+
}
|
|
2877
3055
|
return {
|
|
2878
3056
|
// ── associations ─────────────────────────────────────────────────────
|
|
2879
3057
|
getEdges(type, id) {
|
|
@@ -2944,6 +3122,9 @@ function createAssociationsStore(config) {
|
|
|
2944
3122
|
const dims = [...catByDim.keys()];
|
|
2945
3123
|
catByDim.clear();
|
|
2946
3124
|
for (const dim of dims) notifyCat(dim);
|
|
3125
|
+
const cmtKeys = [...cmtByKey.keys()];
|
|
3126
|
+
cmtByKey.clear();
|
|
3127
|
+
for (const key of cmtKeys) notifyCmt(key);
|
|
2947
3128
|
},
|
|
2948
3129
|
// ── categories ───────────────────────────────────────────────────────
|
|
2949
3130
|
getCategories(dimension) {
|
|
@@ -3018,6 +3199,52 @@ function createAssociationsStore(config) {
|
|
|
3018
3199
|
await loadCategories(args.dimension, { force: true });
|
|
3019
3200
|
return { ok: true, id: res.data.id };
|
|
3020
3201
|
},
|
|
3202
|
+
// ── comments (W6) ────────────────────────────────────────────────────
|
|
3203
|
+
getComments(type, id) {
|
|
3204
|
+
if (!type || !id) return IDLE_COMMENTS;
|
|
3205
|
+
return cmtByKey.get(associationsKey(type, id)) ?? IDLE_COMMENTS;
|
|
3206
|
+
},
|
|
3207
|
+
loadComments,
|
|
3208
|
+
subscribeComments(key, cb) {
|
|
3209
|
+
let subs = cmtSubs.get(key);
|
|
3210
|
+
if (!subs) {
|
|
3211
|
+
subs = /* @__PURE__ */ new Set();
|
|
3212
|
+
cmtSubs.set(key, subs);
|
|
3213
|
+
}
|
|
3214
|
+
subs.add(cb);
|
|
3215
|
+
return () => {
|
|
3216
|
+
subs.delete(cb);
|
|
3217
|
+
if (subs.size === 0) cmtSubs.delete(key);
|
|
3218
|
+
};
|
|
3219
|
+
},
|
|
3220
|
+
async addComment(args) {
|
|
3221
|
+
const res = await comments.add(args);
|
|
3222
|
+
if (isAssociationsRpcErr(res)) {
|
|
3223
|
+
return { ok: false, error: res.error.message };
|
|
3224
|
+
}
|
|
3225
|
+
await loadComments(args.entityType, args.entityId, { force: true });
|
|
3226
|
+
return { ok: true, id: res.data.id };
|
|
3227
|
+
},
|
|
3228
|
+
async editComment(args) {
|
|
3229
|
+
const res = await comments.edit(args.id, args.body);
|
|
3230
|
+
if (isAssociationsRpcErr(res)) {
|
|
3231
|
+
return { ok: false, error: res.error.message };
|
|
3232
|
+
}
|
|
3233
|
+
await loadComments(args.entityType, args.entityId, { force: true });
|
|
3234
|
+
return { ok: true, id: args.id };
|
|
3235
|
+
},
|
|
3236
|
+
async deleteComment(args) {
|
|
3237
|
+
const res = await comments.remove(args.id);
|
|
3238
|
+
if (isAssociationsRpcErr(res)) {
|
|
3239
|
+
return { ok: false, error: res.error.message };
|
|
3240
|
+
}
|
|
3241
|
+
await loadComments(args.entityType, args.entityId, { force: true });
|
|
3242
|
+
return { ok: true, id: args.id };
|
|
3243
|
+
},
|
|
3244
|
+
invalidateComments(type, id) {
|
|
3245
|
+
const key = associationsKey(type, id);
|
|
3246
|
+
if (cmtByKey.delete(key)) notifyCmt(key);
|
|
3247
|
+
},
|
|
3021
3248
|
// ── seams ────────────────────────────────────────────────────────────
|
|
3022
3249
|
titles,
|
|
3023
3250
|
favorites,
|
|
@@ -3025,7 +3252,7 @@ function createAssociationsStore(config) {
|
|
|
3025
3252
|
entityRows,
|
|
3026
3253
|
helpers,
|
|
3027
3254
|
registry,
|
|
3028
|
-
services: { associations, categories },
|
|
3255
|
+
services: { associations, categories, comments },
|
|
3029
3256
|
registerEntityOverlay: registry.registerEntityOverlay,
|
|
3030
3257
|
errorSink,
|
|
3031
3258
|
identity: config.identity
|
|
@@ -3056,7 +3283,11 @@ var DEMANDED_RPC_NAMES = [
|
|
|
3056
3283
|
"ues_list",
|
|
3057
3284
|
"ues_get_bulk",
|
|
3058
3285
|
"ues_touch",
|
|
3059
|
-
"reference_search_candidates"
|
|
3286
|
+
"reference_search_candidates",
|
|
3287
|
+
"cmt_list",
|
|
3288
|
+
"cmt_add",
|
|
3289
|
+
"cmt_edit",
|
|
3290
|
+
"cmt_delete"
|
|
3060
3291
|
];
|
|
3061
3292
|
|
|
3062
3293
|
// src/core/assertDemandedSchema.ts
|
|
@@ -3121,7 +3352,17 @@ var PROBE_ARGS = {
|
|
|
3121
3352
|
ues_get_bulk: { p_entity_type: "__probe__", p_entity_ids: [BAD_UUID] },
|
|
3122
3353
|
ues_touch: { p_entity_type: "__probe__", p_entity_id: BAD_UUID },
|
|
3123
3354
|
// candidates
|
|
3124
|
-
reference_search_candidates: { p_token: "__probe__", p_limit: 1 }
|
|
3355
|
+
reference_search_candidates: { p_token: "__probe__", p_limit: 1 },
|
|
3356
|
+
// comments (W6) — every fn takes a uuid arg, so the unparseable sentinel
|
|
3357
|
+
// guarantees 22P02 before any SECURITY DEFINER body runs (no write occurs).
|
|
3358
|
+
cmt_list: { p_entity_type: "__probe__", p_entity_id: BAD_UUID },
|
|
3359
|
+
cmt_add: {
|
|
3360
|
+
p_entity_type: "__probe__",
|
|
3361
|
+
p_entity_id: BAD_UUID,
|
|
3362
|
+
p_body: "__probe__"
|
|
3363
|
+
},
|
|
3364
|
+
cmt_edit: { p_id: BAD_UUID, p_body: "__probe__" },
|
|
3365
|
+
cmt_delete: { p_id: BAD_UUID }
|
|
3125
3366
|
};
|
|
3126
3367
|
function isMissingFunctionError(error) {
|
|
3127
3368
|
if (!error || typeof error !== "object") return false;
|