@lucern/graph-primitives 0.3.0-alpha.0 → 0.3.0-alpha.10
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/{beliefDecay-Q_26RTc-.d.ts → beliefDecay-DZ6tkLYq.d.ts} +1 -1
- package/dist/beliefDecay.d.ts +1 -1
- package/dist/beliefDecay.js +188 -1144
- package/dist/beliefDecay.js.map +1 -1
- package/dist/{beliefEvidenceLinks-42FlR48t.d.ts → beliefEvidenceLinks-CWOXxxJg.d.ts} +1 -1
- package/dist/beliefEvidenceLinks.d.ts +1 -1
- package/dist/beliefEvidenceLinks.js +186 -871
- package/dist/beliefEvidenceLinks.js.map +1 -1
- package/dist/{beliefLifecycle-C-AehZgF.d.ts → beliefLifecycle-y8WLXqQj.d.ts} +1 -1
- package/dist/beliefLifecycle.d.ts +1 -1
- package/dist/confidencePropagationDispatch.d.ts +4 -4
- package/dist/confidencePropagationDispatch.js +31 -311
- package/dist/confidencePropagationDispatch.js.map +1 -1
- package/dist/{contradictions-Hdwl7zid.d.ts → contradictions-51VLsESq.d.ts} +1 -1
- package/dist/contradictions.d.ts +1 -1
- package/dist/contradictions.js +67 -800
- package/dist/contradictions.js.map +1 -1
- package/dist/debug.d.ts +4 -0
- package/dist/debug.js +34 -0
- package/dist/debug.js.map +1 -0
- package/dist/edges/contradicts.js +1 -122
- package/dist/edges/contradicts.js.map +1 -1
- package/dist/edges/dependsOn.js +14 -172
- package/dist/edges/dependsOn.js.map +1 -1
- package/dist/edges/elaborates.js +1 -49
- package/dist/edges/elaborates.js.map +1 -1
- package/dist/edges/index.js +15 -280
- package/dist/edges/index.js.map +1 -1
- package/dist/edges/informs.js +2 -65
- package/dist/edges/informs.js.map +1 -1
- package/dist/edges/propagationTypes.d.ts +2 -2
- package/dist/edges/propagationTypes.js.map +1 -1
- package/dist/edges/refutes.js +2 -65
- package/dist/edges/refutes.js.map +1 -1
- package/dist/edges/supports.js +1 -122
- package/dist/edges/supports.js.map +1 -1
- package/dist/edges/utils.d.ts +7 -7
- package/dist/edges/utils.js +2 -133
- package/dist/edges/utils.js.map +1 -1
- package/dist/embeddingTrigger.js +21 -1
- package/dist/embeddingTrigger.js.map +1 -1
- package/dist/entityBridge.js +3 -18
- package/dist/entityBridge.js.map +1 -1
- package/dist/{entityLifecycle-BkhRJ-XI.d.ts → entityLifecycle-CvgSK5FV.d.ts} +1 -1
- package/dist/entityLifecycle.d.ts +1 -1
- package/dist/entityLifecycle.js +193 -892
- package/dist/entityLifecycle.js.map +1 -1
- package/dist/{epistemicAnswers-DSP1slZ9.d.ts → epistemicAnswers-C5ib4z6_.d.ts} +1 -1
- package/dist/epistemicAnswers.d.ts +1 -1
- package/dist/epistemicAnswers.js +73 -810
- package/dist/epistemicAnswers.js.map +1 -1
- package/dist/{epistemicBeliefs-DtFVTp-k.d.ts → epistemicBeliefs-DzKjZAeC.d.ts} +3 -3
- package/dist/epistemicBeliefs.d.ts +2 -2
- package/dist/epistemicBeliefs.js +404 -1698
- package/dist/epistemicBeliefs.js.map +1 -1
- package/dist/epistemicContractHelpers.js +1 -318
- package/dist/epistemicContractHelpers.js.map +1 -1
- package/dist/epistemicContracts.d.ts +1 -1
- package/dist/epistemicContracts.js +417 -1980
- package/dist/epistemicContracts.js.map +1 -1
- package/dist/{epistemicEdges-DcA8ErUG.d.ts → epistemicEdges-CD5vxmlH.d.ts} +3 -3
- package/dist/epistemicEdges.d.ts +1 -1
- package/dist/epistemicEdges.js +248 -919
- package/dist/epistemicEdges.js.map +1 -1
- package/dist/{epistemicEvidence-Bo638XDP.d.ts → epistemicEvidence-xw6UUrwh.d.ts} +1 -1
- package/dist/epistemicEvidence.d.ts +1 -1
- package/dist/epistemicEvidence.js +229 -1087
- package/dist/epistemicEvidence.js.map +1 -1
- package/dist/{epistemicHelpers-Bd9xbaib.d.ts → epistemicHelpers-DevrYgPN.d.ts} +1 -1
- package/dist/epistemicHelpers.d.ts +1 -1
- package/dist/{epistemicLinking-CyeLOIzN.d.ts → epistemicLinking-CfE00tHJ.d.ts} +1 -1
- package/dist/epistemicLinking.d.ts +1 -1
- package/dist/epistemicLinking.js +3 -786
- package/dist/epistemicLinking.js.map +1 -1
- package/dist/{epistemicNodes-BpD6Koud.d.ts → epistemicNodes-NBrPW7fk.d.ts} +2 -2
- package/dist/epistemicNodes.d.ts +1 -1
- package/dist/epistemicNodes.js +172 -899
- package/dist/epistemicNodes.js.map +1 -1
- package/dist/{epistemicQuestions-CmEeY6zQ.d.ts → epistemicQuestions-B_nUclrH.d.ts} +1 -1
- package/dist/epistemicQuestions.d.ts +1 -1
- package/dist/epistemicQuestions.js +369 -1125
- package/dist/epistemicQuestions.js.map +1 -1
- package/dist/{epistemicSources-ZazxHOK1.d.ts → epistemicSources-dlKj58Jp.d.ts} +1 -1
- package/dist/epistemicSources.d.ts +1 -1
- package/dist/epistemicSources.js +86 -886
- package/dist/epistemicSources.js.map +1 -1
- package/dist/evaluators/index.js +417 -1980
- package/dist/evaluators/index.js.map +1 -1
- package/dist/evaluators/lintCheckerEvaluator.js.map +1 -1
- package/dist/evaluators/sentryCheckerEvaluator.js.map +1 -1
- package/dist/evaluators/shared.js +20 -1
- package/dist/evaluators/shared.js.map +1 -1
- package/dist/evaluators/testRunnerEvaluator.js +20 -1
- package/dist/evaluators/testRunnerEvaluator.js.map +1 -1
- package/dist/evaluators/tscCheckerEvaluator.js.map +1 -1
- package/dist/index.d.ts +20 -20
- package/dist/index.js +965 -3004
- package/dist/index.js.map +1 -1
- package/dist/{ontology-matching-Buhu23ss.d.ts → ontology-matching-C6rrz2VP.d.ts} +1 -1
- package/dist/ontology-matching.d.ts +1 -1
- package/dist/ontology-matching.js +1 -344
- package/dist/ontology-matching.js.map +1 -1
- package/dist/{ontologyApproval-Ba0Jjk1k.d.ts → ontologyApproval-CFYmqKmk.d.ts} +1 -1
- package/dist/ontologyApproval.d.ts +1 -1
- package/dist/ontologyApproval.js +1 -13
- package/dist/ontologyApproval.js.map +1 -1
- package/dist/ontologyDefinitions.js +6 -20
- package/dist/ontologyDefinitions.js.map +1 -1
- package/dist/ontologyHelpers.d.ts +1 -1
- package/dist/ontologyHelpers.js +4 -3
- package/dist/ontologyHelpers.js.map +1 -1
- package/dist/ontologyRegistry.js +2 -17
- package/dist/ontologyRegistry.js.map +1 -1
- package/dist/{projectionReconciliation-CxrXYGaB.d.ts → projectionReconciliation-jww2fBI0.d.ts} +1 -1
- package/dist/projectionReconciliation.d.ts +1 -1
- package/dist/projectionReconciliation.js +16 -37
- package/dist/projectionReconciliation.js.map +1 -1
- package/dist/{projectionStaleness-CAdpIsaW.d.ts → projectionStaleness-CmdbpjVK.d.ts} +1 -1
- package/dist/projectionStaleness.d.ts +1 -1
- package/dist/{questionEvidenceLinks-BdQD0TkM.d.ts → questionEvidenceLinks-DFlyPpAj.d.ts} +1 -1
- package/dist/questionEvidenceLinks.d.ts +1 -1
- package/dist/questionEvidenceLinks.js +199 -881
- package/dist/questionEvidenceLinks.js.map +1 -1
- package/dist/resolvers.js +86 -37
- package/dist/resolvers.js.map +1 -1
- package/dist/scopeResolverCompat.js +64 -7
- package/dist/scopeResolverCompat.js.map +1 -1
- package/dist/{text-matching-CMn2WnVD.d.ts → text-matching-DNg4M5Wd.d.ts} +1 -1
- package/dist/text-matching.d.ts +1 -1
- package/dist/text-matching.js +1 -244
- package/dist/text-matching.js.map +1 -1
- package/dist/topicProjectOverlay.js +56 -13
- package/dist/topicProjectOverlay.js.map +1 -1
- package/dist/topicScope.js +55 -6
- package/dist/topicScope.js.map +1 -1
- package/dist/workflowBridge.d.ts +27 -0
- package/dist/workflowBridge.js +352 -0
- package/dist/workflowBridge.js.map +1 -0
- package/dist/workspaceIsolation.js +56 -57
- package/dist/workspaceIsolation.js.map +1 -1
- package/package.json +6 -5
package/dist/epistemicEdges.js
CHANGED
|
@@ -1,818 +1,27 @@
|
|
|
1
1
|
import { v } from 'convex/values';
|
|
2
|
-
import {
|
|
2
|
+
import { checkScopeAccess, requireProjectAccess } from '@lucern/access-control/access';
|
|
3
|
+
import { permissiveReturn } from '@lucern/contracts/schema-helpers/validators';
|
|
4
|
+
import { componentsGeneric, anyApi, queryGeneric, mutationGeneric, internalMutationGeneric } from 'convex/server';
|
|
3
5
|
|
|
4
6
|
// src/epistemicEdges.ts
|
|
5
7
|
var api = anyApi;
|
|
6
8
|
componentsGeneric();
|
|
9
|
+
var internal = anyApi;
|
|
10
|
+
var internalMutation = internalMutationGeneric;
|
|
11
|
+
var mutation = mutationGeneric;
|
|
12
|
+
var query = queryGeneric;
|
|
7
13
|
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
const normalized = value.trim();
|
|
15
|
-
return normalized.length > 0 ? normalized : void 0;
|
|
16
|
-
}
|
|
17
|
-
function readStringArray(value) {
|
|
18
|
-
if (!Array.isArray(value)) {
|
|
19
|
-
return [];
|
|
20
|
-
}
|
|
21
|
-
return value.map((entry) => readNonEmptyString(entry)).filter((entry) => Boolean(entry));
|
|
22
|
-
}
|
|
23
|
-
function readMetadata(topic) {
|
|
24
|
-
return topic.metadata && typeof topic.metadata === "object" ? topic.metadata : {};
|
|
25
|
-
}
|
|
26
|
-
function readLegacyProjectId(value) {
|
|
27
|
-
if (!value) {
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
return readNonEmptyString(value[LEGACY_SCOPE_FIELD]);
|
|
31
|
-
}
|
|
32
|
-
function coerceVisibility(value) {
|
|
33
|
-
return value === "private" || value === "team" || value === "firm" || value === "external" || value === "public" ? value : void 0;
|
|
34
|
-
}
|
|
35
|
-
function coerceStatus(value) {
|
|
36
|
-
return value === "active" || value === "archived" || value === "watching" ? value : void 0;
|
|
37
|
-
}
|
|
38
|
-
function mapProjectType(topic, metadata) {
|
|
39
|
-
const explicit = readNonEmptyString(metadata.projectType);
|
|
40
|
-
if (explicit) {
|
|
41
|
-
return explicit;
|
|
42
|
-
}
|
|
43
|
-
if (topic.type === "theme") {
|
|
44
|
-
return "thematic";
|
|
45
|
-
}
|
|
46
|
-
return readNonEmptyString(topic.type) || "general";
|
|
47
|
-
}
|
|
48
|
-
function isProjectLikeTopic(topic) {
|
|
49
|
-
const metadata = readMetadata(topic);
|
|
50
|
-
return topic.type === "theme" || topic.type === "thematic" || topic.type === "deal" || topic.type === "monitoring" || readLegacyProjectId(topic) !== void 0 || readNonEmptyString(metadata.projectType) !== void 0;
|
|
51
|
-
}
|
|
52
|
-
async function resolveTopicDoc(ctx, scopeId) {
|
|
53
|
-
if (ctx?.db && typeof ctx.db.get === "function") {
|
|
54
|
-
try {
|
|
55
|
-
const directTopic = await ctx.db.get(scopeId);
|
|
56
|
-
if (directTopic) {
|
|
57
|
-
return directTopic;
|
|
58
|
-
}
|
|
59
|
-
} catch {
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
if (typeof ctx.runQuery !== "function") {
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
try {
|
|
66
|
-
const topic = await ctx.runQuery(api.topics.get, {
|
|
67
|
-
id: String(scopeId)
|
|
68
|
-
});
|
|
69
|
-
if (topic?.name !== void 0 && topic?.type !== void 0) {
|
|
70
|
-
return topic;
|
|
71
|
-
}
|
|
72
|
-
} catch {
|
|
73
|
-
}
|
|
74
|
-
try {
|
|
75
|
-
const topic = await ctx.runQuery(api.topics.getByLegacyScopeId, {
|
|
76
|
-
projectId: String(scopeId)
|
|
77
|
-
});
|
|
78
|
-
if (topic?.name !== void 0 && topic?.type !== void 0) {
|
|
79
|
-
return topic;
|
|
80
|
-
}
|
|
81
|
-
} catch {
|
|
82
|
-
}
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
function materializeTopicProjectOverlay(topic, idMode = "legacy") {
|
|
86
|
-
const metadata = readMetadata(topic);
|
|
87
|
-
const topicId = String(topic._id);
|
|
88
|
-
const legacyProjectId = readLegacyProjectId(topic) || readLegacyProjectId(metadata) || readNonEmptyString(metadata.legacyProjectId);
|
|
89
|
-
const storageProjectId = legacyProjectId || topicId;
|
|
90
|
-
const outwardId = idMode === "topic" ? topicId : storageProjectId;
|
|
91
|
-
const visibility = coerceVisibility(topic.visibility) || coerceVisibility(metadata.visibility) || "private";
|
|
92
|
-
const status = coerceStatus(topic.status) || coerceStatus(metadata.status) || "active";
|
|
93
|
-
const createdAt = typeof topic.createdAt === "number" ? topic.createdAt : typeof topic._creationTime === "number" ? topic._creationTime : 0;
|
|
94
|
-
const updatedAt = typeof topic.updatedAt === "number" ? topic.updatedAt : typeof metadata.updatedAt === "number" ? metadata.updatedAt : createdAt;
|
|
95
|
-
return {
|
|
96
|
-
...metadata,
|
|
97
|
-
_id: outwardId,
|
|
98
|
-
projectId: outwardId,
|
|
99
|
-
topicId,
|
|
100
|
-
storageProjectId,
|
|
101
|
-
legacyProjectId,
|
|
102
|
-
name: readNonEmptyString(topic.name) || "Untitled Theme",
|
|
103
|
-
type: mapProjectType(topic, metadata),
|
|
104
|
-
description: readNonEmptyString(topic.description),
|
|
105
|
-
ownerId: readNonEmptyString(metadata.ownerId) || readNonEmptyString(topic.createdBy) || "system",
|
|
106
|
-
sharedWith: readStringArray(metadata.sharedWith),
|
|
107
|
-
visibility,
|
|
108
|
-
tenantId: readNonEmptyString(topic.tenantId) || readNonEmptyString(metadata.tenantId),
|
|
109
|
-
workspaceId: readNonEmptyString(topic.workspaceId) || readNonEmptyString(metadata.workspaceId),
|
|
110
|
-
status,
|
|
111
|
-
tags: readStringArray(metadata.tags),
|
|
112
|
-
chatCount: typeof metadata.chatCount === "number" ? metadata.chatCount : 0,
|
|
113
|
-
artifactCount: typeof metadata.artifactCount === "number" ? metadata.artifactCount : 0,
|
|
114
|
-
lastActivityAt: typeof metadata.lastActivityAt === "number" ? metadata.lastActivityAt : updatedAt,
|
|
115
|
-
_creationTime: typeof topic._creationTime === "number" ? topic._creationTime : createdAt,
|
|
116
|
-
createdAt,
|
|
117
|
-
updatedAt
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
async function resolveTopicProjectOverlay(ctx, scopeId, options = {}) {
|
|
121
|
-
const topic = await resolveTopicDoc(ctx, scopeId);
|
|
122
|
-
if (!topic) {
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
if (options.projectLikeOnly !== false && !isProjectLikeTopic(topic)) {
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
return materializeTopicProjectOverlay(topic, options.idMode);
|
|
129
|
-
}
|
|
130
|
-
async function listTopicProjectOverlays(ctx, options = {}) {
|
|
131
|
-
let allTopics = [];
|
|
132
|
-
if (ctx?.db?.query && typeof ctx.db.query === "function") {
|
|
133
|
-
try {
|
|
134
|
-
allTopics = await ctx.db.query("topics").collect();
|
|
135
|
-
} catch {
|
|
136
|
-
allTopics = [];
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
if (allTopics.length === 0 && typeof ctx.runQuery === "function") {
|
|
140
|
-
allTopics = (await ctx.runQuery(api.topics.list, {}) ?? []) || [];
|
|
141
|
-
}
|
|
142
|
-
return allTopics.filter(
|
|
143
|
-
(topic) => options.projectLikeOnly === false || isProjectLikeTopic(topic)
|
|
144
|
-
).map((topic) => materializeTopicProjectOverlay(topic, options.idMode));
|
|
14
|
+
// src/debug.ts
|
|
15
|
+
function isGraphPrimitiveDebugEnabled() {
|
|
16
|
+
const env = globalThis.process?.env;
|
|
17
|
+
return env?.LUCERN_COMPAT_FALLBACK_DEBUG === "1" || env?.LUCERN_GRAPH_DEBUG === "1";
|
|
145
18
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
var PROJECT_GRANT_STATUSES = ["active", "revoked", "expired"];
|
|
149
|
-
function normalizeString(value) {
|
|
150
|
-
if (typeof value !== "string") {
|
|
19
|
+
function debugGraphPrimitiveFallback(message, context) {
|
|
20
|
+
if (!isGraphPrimitiveDebugEnabled()) {
|
|
151
21
|
return;
|
|
152
22
|
}
|
|
153
|
-
|
|
154
|
-
return trimmed.length > 0 ? trimmed : void 0;
|
|
155
|
-
}
|
|
156
|
-
async function resolveGrantScopeIds(ctx, args) {
|
|
157
|
-
const topicId = normalizeString(args.topicId);
|
|
158
|
-
const projectId = normalizeString(args.projectId);
|
|
159
|
-
for (const scopeId of [topicId, projectId]) {
|
|
160
|
-
if (!scopeId) {
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
try {
|
|
164
|
-
const overlay = await resolveTopicProjectOverlay(ctx, scopeId, {
|
|
165
|
-
idMode: "legacy",
|
|
166
|
-
projectLikeOnly: false
|
|
167
|
-
});
|
|
168
|
-
if (overlay) {
|
|
169
|
-
return {
|
|
170
|
-
topicId: normalizeString(overlay.topicId) ?? topicId,
|
|
171
|
-
projectId: normalizeString(overlay.projectId) ?? projectId ?? scopeId
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
} catch {
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
return { topicId, projectId };
|
|
178
|
-
}
|
|
179
|
-
async function normalizeProjectGrantRow(ctx, row) {
|
|
180
|
-
const scope = await resolveGrantScopeIds(ctx, {
|
|
181
|
-
topicId: row.topicId,
|
|
182
|
-
projectId: row.projectId
|
|
183
|
-
});
|
|
184
|
-
return {
|
|
185
|
-
...row,
|
|
186
|
-
...scope.topicId ? { topicId: scope.topicId } : {},
|
|
187
|
-
...scope.projectId ?? scope.topicId ? { projectId: scope.projectId ?? scope.topicId } : {}
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
async function normalizeProjectGrantRows(ctx, rows) {
|
|
191
|
-
return await Promise.all(rows.map((row) => normalizeProjectGrantRow(ctx, row)));
|
|
192
|
-
}
|
|
193
|
-
async function listProjectGrantsByPrincipal(ctx, principalId) {
|
|
194
|
-
const rows = await Promise.all(
|
|
195
|
-
PROJECT_GRANT_STATUSES.map(
|
|
196
|
-
(status) => ctx.db.query("projectGrants").withIndex(
|
|
197
|
-
"by_principal_status",
|
|
198
|
-
(q) => q.eq("principalId", principalId).eq("status", status)
|
|
199
|
-
).collect()
|
|
200
|
-
)
|
|
201
|
-
);
|
|
202
|
-
return await normalizeProjectGrantRows(ctx, rows.flat());
|
|
203
|
-
}
|
|
204
|
-
async function listProjectGrantsByGroup(ctx, groupId) {
|
|
205
|
-
const rows = await Promise.all(
|
|
206
|
-
PROJECT_GRANT_STATUSES.map(
|
|
207
|
-
(status) => ctx.db.query("projectGrants").withIndex(
|
|
208
|
-
"by_group_status",
|
|
209
|
-
(q) => q.eq("groupId", groupId).eq("status", status)
|
|
210
|
-
).collect()
|
|
211
|
-
)
|
|
212
|
-
);
|
|
213
|
-
return await normalizeProjectGrantRows(ctx, rows.flat());
|
|
214
|
-
}
|
|
215
|
-
function buildScopeMatchers(inputScopeId, resolved) {
|
|
216
|
-
return new Set(
|
|
217
|
-
[inputScopeId, resolved.topicId, resolved.projectId].map((value) => normalizeString(value)).filter((value) => Boolean(value))
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
function matchesResolvedScope(row, scopeIds) {
|
|
221
|
-
const rowTopicId = normalizeString(row.topicId);
|
|
222
|
-
const rowProjectId = normalizeString(row.projectId);
|
|
223
|
-
return rowTopicId !== void 0 && scopeIds.has(rowTopicId) || rowProjectId !== void 0 && scopeIds.has(rowProjectId);
|
|
224
|
-
}
|
|
225
|
-
async function bridgeListProjectGrantsByTopicAndPrincipal(ctx, topicId, principalId) {
|
|
226
|
-
const resolved = await resolveGrantScopeIds(ctx, { topicId });
|
|
227
|
-
const scopeIds = buildScopeMatchers(topicId, resolved);
|
|
228
|
-
const rows = await listProjectGrantsByPrincipal(ctx, principalId);
|
|
229
|
-
return rows.filter((row) => matchesResolvedScope(row, scopeIds));
|
|
230
|
-
}
|
|
231
|
-
async function bridgeListProjectGrantsByTopicAndGroup(ctx, topicId, groupId) {
|
|
232
|
-
const resolved = await resolveGrantScopeIds(ctx, { topicId });
|
|
233
|
-
const scopeIds = buildScopeMatchers(topicId, resolved);
|
|
234
|
-
const rows = await listProjectGrantsByGroup(ctx, groupId);
|
|
235
|
-
return rows.filter((row) => matchesResolvedScope(row, scopeIds));
|
|
236
|
-
}
|
|
237
|
-
async function bridgeListProjectGrantsByPrincipalStatus(ctx, principalId, status) {
|
|
238
|
-
const rows = await listProjectGrantsByPrincipal(ctx, principalId);
|
|
239
|
-
return rows.filter((row) => row.status === status);
|
|
240
|
-
}
|
|
241
|
-
async function bridgeListProjectGrantsByGroupStatus(ctx, groupId, status) {
|
|
242
|
-
const rows = await listProjectGrantsByGroup(ctx, groupId);
|
|
243
|
-
return rows.filter((row) => row.status === status);
|
|
244
|
-
}
|
|
245
|
-
async function bridgeInsertProjectGrant(ctx, value) {
|
|
246
|
-
const resolved = await resolveGrantScopeIds(ctx, value);
|
|
247
|
-
return await ctx.db.insert("projectGrants", {
|
|
248
|
-
...value,
|
|
249
|
-
...resolved.topicId ? { topicId: resolved.topicId } : {},
|
|
250
|
-
...resolved.projectId ?? resolved.topicId ? { projectId: resolved.projectId ?? resolved.topicId } : {}
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// ../access-control/src/resolvers.ts
|
|
255
|
-
async function findUserByClerkId(ctx, clerkId) {
|
|
256
|
-
const normalizedClerkId = clerkId.trim();
|
|
257
|
-
if (!normalizedClerkId) {
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
260
|
-
if (typeof ctx.runQuery === "function") {
|
|
261
|
-
try {
|
|
262
|
-
const bridgedUser = await ctx.runQuery(api.users.getUserByClerkId, {
|
|
263
|
-
clerkId: normalizedClerkId
|
|
264
|
-
});
|
|
265
|
-
if (bridgedUser) {
|
|
266
|
-
return bridgedUser;
|
|
267
|
-
}
|
|
268
|
-
} catch {
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
try {
|
|
272
|
-
const users = await ctx.db.query("users").collect();
|
|
273
|
-
return users.find((user) => String(user.clerkId ?? "") === normalizedClerkId) ?? null;
|
|
274
|
-
} catch {
|
|
275
|
-
return null;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
async function findUserByPrincipalId(ctx, principalId) {
|
|
279
|
-
const normalizedPrincipalId = principalId.trim();
|
|
280
|
-
if (!normalizedPrincipalId) {
|
|
281
|
-
return null;
|
|
282
|
-
}
|
|
283
|
-
try {
|
|
284
|
-
const users = await ctx.db.query("users").collect();
|
|
285
|
-
return users.find(
|
|
286
|
-
(user) => String(user.defaultPrincipalId ?? "") === normalizedPrincipalId
|
|
287
|
-
) ?? null;
|
|
288
|
-
} catch {
|
|
289
|
-
return null;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
async function findAgentByPrincipalId(ctx, principalId) {
|
|
293
|
-
const normalizedPrincipalId = principalId.trim();
|
|
294
|
-
if (!normalizedPrincipalId) {
|
|
295
|
-
return null;
|
|
296
|
-
}
|
|
297
|
-
if (typeof ctx.runQuery === "function") {
|
|
298
|
-
try {
|
|
299
|
-
const bridgedAgent = await ctx.runQuery(
|
|
300
|
-
api.agents.getAgentByPrincipalId,
|
|
301
|
-
{
|
|
302
|
-
principalId: normalizedPrincipalId
|
|
303
|
-
}
|
|
304
|
-
);
|
|
305
|
-
if (bridgedAgent) {
|
|
306
|
-
return bridgedAgent;
|
|
307
|
-
}
|
|
308
|
-
} catch {
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
try {
|
|
312
|
-
const agents = await ctx.db.query("agents").collect();
|
|
313
|
-
return agents.find(
|
|
314
|
-
(agent) => String(agent.principalId ?? "") === normalizedPrincipalId
|
|
315
|
-
) ?? null;
|
|
316
|
-
} catch {
|
|
317
|
-
return null;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
function defaultResolvers() {
|
|
321
|
-
return {
|
|
322
|
-
async getProject(ctx, topicId) {
|
|
323
|
-
return await resolveTopicProjectOverlay(ctx, topicId, {
|
|
324
|
-
idMode: "legacy",
|
|
325
|
-
projectLikeOnly: false
|
|
326
|
-
});
|
|
327
|
-
},
|
|
328
|
-
async listTopics(ctx) {
|
|
329
|
-
return await listTopicProjectOverlays(ctx, { idMode: "legacy" });
|
|
330
|
-
},
|
|
331
|
-
async listTopicsByOwner(ctx, ownerId) {
|
|
332
|
-
const topics = await listTopicProjectOverlays(ctx, { idMode: "legacy" });
|
|
333
|
-
return topics.filter((topic) => topic.ownerId === ownerId);
|
|
334
|
-
},
|
|
335
|
-
async listTopicsByVisibility(ctx, visibility) {
|
|
336
|
-
const topics = await listTopicProjectOverlays(ctx, { idMode: "legacy" });
|
|
337
|
-
return topics.filter((topic) => topic.visibility === visibility);
|
|
338
|
-
},
|
|
339
|
-
async listProjectGrantsByProjectAndPrincipal(ctx, topicId, principalId) {
|
|
340
|
-
return await bridgeListProjectGrantsByTopicAndPrincipal(
|
|
341
|
-
ctx,
|
|
342
|
-
topicId,
|
|
343
|
-
principalId
|
|
344
|
-
);
|
|
345
|
-
},
|
|
346
|
-
async listProjectGrantsByProjectAndGroup(ctx, topicId, groupId) {
|
|
347
|
-
return await bridgeListProjectGrantsByTopicAndGroup(ctx, topicId, groupId);
|
|
348
|
-
},
|
|
349
|
-
async listProjectGrantsByPrincipalStatus(ctx, principalId, status) {
|
|
350
|
-
return await bridgeListProjectGrantsByPrincipalStatus(
|
|
351
|
-
ctx,
|
|
352
|
-
principalId,
|
|
353
|
-
status
|
|
354
|
-
);
|
|
355
|
-
},
|
|
356
|
-
async listProjectGrantsByGroupStatus(ctx, groupId, status) {
|
|
357
|
-
return await bridgeListProjectGrantsByGroupStatus(ctx, groupId, status);
|
|
358
|
-
},
|
|
359
|
-
async insertProjectGrant(ctx, value) {
|
|
360
|
-
return await bridgeInsertProjectGrant(ctx, value);
|
|
361
|
-
},
|
|
362
|
-
async getAgentByPrincipalId(ctx, principalId) {
|
|
363
|
-
return await findAgentByPrincipalId(ctx, principalId);
|
|
364
|
-
},
|
|
365
|
-
async getUserByClerkId(ctx, clerkId) {
|
|
366
|
-
return await findUserByClerkId(ctx, clerkId);
|
|
367
|
-
},
|
|
368
|
-
async getUserByPrincipalId(ctx, principalId) {
|
|
369
|
-
return await findUserByPrincipalId(ctx, principalId);
|
|
370
|
-
}
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
var resolverOverrides = {};
|
|
374
|
-
function resolveAccessControlAppResolvers(_ctx) {
|
|
375
|
-
return {
|
|
376
|
-
...defaultResolvers(),
|
|
377
|
-
...resolverOverrides
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// ../access-control/src/principalContext.ts
|
|
382
|
-
function requireCanonicalResolvedUser(user, clerkId) {
|
|
383
|
-
const resolved = user;
|
|
384
|
-
if (!resolved) {
|
|
385
|
-
throw new Error(
|
|
386
|
-
`[AccessControl] Canonical user identity required for ${clerkId}. Sync users.upsertUser before user-bound access checks.`
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
const { mcRole, defaultTenantId, defaultWorkspaceId, defaultPrincipalId } = resolved;
|
|
390
|
-
if (mcRole !== "platform_admin" && mcRole !== "tenant_admin" && mcRole !== "workspace_admin" && mcRole !== "editor" && mcRole !== "viewer" && mcRole !== "auditor" && mcRole !== "service_agent") {
|
|
391
|
-
throw new Error(
|
|
392
|
-
`[AccessControl] Canonical MC role required for ${clerkId}. Re-sync Master Control identity before user-bound access checks.`
|
|
393
|
-
);
|
|
394
|
-
}
|
|
395
|
-
if (typeof defaultTenantId !== "string" || defaultTenantId.trim().length === 0) {
|
|
396
|
-
throw new Error(
|
|
397
|
-
`[AccessControl] Canonical home tenant required for ${clerkId}. Re-sync Master Control identity before user-bound access checks.`
|
|
398
|
-
);
|
|
399
|
-
}
|
|
400
|
-
if (typeof defaultWorkspaceId !== "string" || defaultWorkspaceId.trim().length === 0) {
|
|
401
|
-
throw new Error(
|
|
402
|
-
`[AccessControl] Canonical home workspace required for ${clerkId}. Re-sync Master Control identity before user-bound access checks.`
|
|
403
|
-
);
|
|
404
|
-
}
|
|
405
|
-
if (typeof defaultPrincipalId !== "string" || defaultPrincipalId.trim().length === 0) {
|
|
406
|
-
throw new Error(
|
|
407
|
-
`[AccessControl] Canonical federated principal required for ${clerkId}. Re-sync Master Control identity before user-bound access checks.`
|
|
408
|
-
);
|
|
409
|
-
}
|
|
410
|
-
return {
|
|
411
|
-
mcRole,
|
|
412
|
-
defaultTenantId: defaultTenantId.trim(),
|
|
413
|
-
defaultWorkspaceId: defaultWorkspaceId.trim(),
|
|
414
|
-
defaultPrincipalId: defaultPrincipalId.trim()
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
function isPrincipalIdInput(value) {
|
|
418
|
-
return value.startsWith("user:") || value.startsWith("group:") || value.startsWith("service:") || value.startsWith("agent:") || value.startsWith("external_viewer:");
|
|
419
|
-
}
|
|
420
|
-
async function resolveCanonicalUserRecord(ctx, actorId) {
|
|
421
|
-
const normalizedActorId = actorId.trim();
|
|
422
|
-
const clerkId = isPrincipalIdInput(normalizedActorId) && normalizedActorId.startsWith("user:") ? normalizedActorId.slice("user:".length) : normalizedActorId;
|
|
423
|
-
const resolvers = resolveAccessControlAppResolvers();
|
|
424
|
-
const resolvedByClerkId = await resolvers.getUserByClerkId(ctx, clerkId);
|
|
425
|
-
if (resolvedByClerkId) {
|
|
426
|
-
return {
|
|
427
|
-
resolvedUser: resolvedByClerkId,
|
|
428
|
-
clerkId,
|
|
429
|
-
contextClerkId: clerkId
|
|
430
|
-
};
|
|
431
|
-
}
|
|
432
|
-
const resolvedByPrincipalId = await resolvers.getUserByPrincipalId(
|
|
433
|
-
ctx,
|
|
434
|
-
normalizedActorId
|
|
435
|
-
);
|
|
436
|
-
return {
|
|
437
|
-
resolvedUser: resolvedByPrincipalId ?? null,
|
|
438
|
-
clerkId,
|
|
439
|
-
contextClerkId: normalizedActorId.startsWith("user:") && clerkId.length > 0 ? clerkId : normalizedActorId
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
function uniqRoles(roles) {
|
|
443
|
-
const roleSet = /* @__PURE__ */ new Set();
|
|
444
|
-
for (const role of roles) {
|
|
445
|
-
if (role === "platform_admin" || role === "tenant_admin" || role === "workspace_admin" || role === "editor" || role === "viewer" || role === "auditor" || role === "service_agent") {
|
|
446
|
-
roleSet.add(role);
|
|
447
|
-
}
|
|
448
|
-
}
|
|
449
|
-
return [...roleSet];
|
|
23
|
+
console.debug(message, context ?? {});
|
|
450
24
|
}
|
|
451
|
-
function normalizeGroupIds(value) {
|
|
452
|
-
if (!Array.isArray(value)) {
|
|
453
|
-
return [];
|
|
454
|
-
}
|
|
455
|
-
return [...new Set(
|
|
456
|
-
value.filter((entry) => typeof entry === "string").map((entry) => entry.trim()).filter(Boolean)
|
|
457
|
-
)];
|
|
458
|
-
}
|
|
459
|
-
function requireServiceAgentUser(user, actorId) {
|
|
460
|
-
const canonicalUser = requireCanonicalResolvedUser(user, actorId);
|
|
461
|
-
if (canonicalUser.mcRole !== "service_agent") {
|
|
462
|
-
throw new Error(
|
|
463
|
-
`[AccessControl] Canonical service_agent identity required for ${actorId}. Sync users.upsertUser before agent-bound access checks.`
|
|
464
|
-
);
|
|
465
|
-
}
|
|
466
|
-
return canonicalUser;
|
|
467
|
-
}
|
|
468
|
-
function requireCanonicalResolvedAgent(agent, actorId) {
|
|
469
|
-
const resolved = agent;
|
|
470
|
-
if (!resolved) {
|
|
471
|
-
throw new Error(
|
|
472
|
-
`[AccessControl] Agent "${actorId}" not found in agents or users table.`
|
|
473
|
-
);
|
|
474
|
-
}
|
|
475
|
-
if (typeof resolved.principalId !== "string" || resolved.principalId.trim().length === 0) {
|
|
476
|
-
throw new Error(
|
|
477
|
-
`[AccessControl] Canonical agent principalId required for ${actorId}.`
|
|
478
|
-
);
|
|
479
|
-
}
|
|
480
|
-
if (typeof resolved.tenantId !== "string" || resolved.tenantId.trim().length === 0) {
|
|
481
|
-
throw new Error(
|
|
482
|
-
`[AccessControl] Canonical home tenant required for ${actorId}.`
|
|
483
|
-
);
|
|
484
|
-
}
|
|
485
|
-
if (typeof resolved.workspaceId !== "string" || resolved.workspaceId.trim().length === 0) {
|
|
486
|
-
throw new Error(
|
|
487
|
-
`[AccessControl] Canonical home workspace required for ${actorId}.`
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
return {
|
|
491
|
-
principalId: resolved.principalId.trim(),
|
|
492
|
-
tenantId: resolved.tenantId.trim(),
|
|
493
|
-
workspaceId: resolved.workspaceId.trim(),
|
|
494
|
-
roles: uniqRoles(Array.isArray(resolved.roles) ? resolved.roles : []) ?? ["service_agent"],
|
|
495
|
-
groupIds: normalizeGroupIds(resolved.groupIds)
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
async function resolvePrincipalContext(ctx, actorId) {
|
|
499
|
-
if (actorId.startsWith("agent:")) {
|
|
500
|
-
const resolvers = resolveAccessControlAppResolvers();
|
|
501
|
-
const resolvedAgent = await resolvers.getAgentByPrincipalId(ctx, actorId);
|
|
502
|
-
if (resolvedAgent) {
|
|
503
|
-
const agent = requireCanonicalResolvedAgent(
|
|
504
|
-
resolvedAgent,
|
|
505
|
-
actorId
|
|
506
|
-
);
|
|
507
|
-
return {
|
|
508
|
-
principalId: agent.principalId,
|
|
509
|
-
principalType: "service",
|
|
510
|
-
clerkId: actorId,
|
|
511
|
-
tenantId: agent.tenantId,
|
|
512
|
-
workspaceId: agent.workspaceId,
|
|
513
|
-
roles: agent.roles.length > 0 ? agent.roles : ["service_agent"],
|
|
514
|
-
groupIds: agent.groupIds,
|
|
515
|
-
isPlatformAdmin: false,
|
|
516
|
-
isTenantAdmin: false,
|
|
517
|
-
isWorkspaceAdmin: false,
|
|
518
|
-
isSystemFallback: false
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
const resolvedUser2 = await resolvers.getUserByClerkId(
|
|
522
|
-
ctx,
|
|
523
|
-
actorId
|
|
524
|
-
);
|
|
525
|
-
if (!resolvedUser2) {
|
|
526
|
-
throw new Error(
|
|
527
|
-
`[AccessControl] Agent "${actorId}" not found in agents or users table.`
|
|
528
|
-
);
|
|
529
|
-
}
|
|
530
|
-
const user2 = requireServiceAgentUser(
|
|
531
|
-
resolvedUser2,
|
|
532
|
-
actorId
|
|
533
|
-
);
|
|
534
|
-
console.warn(
|
|
535
|
-
`[AccessControl] Deprecated legacy service-agent fallback for ${actorId}; migrate this principal into identity.agents.`
|
|
536
|
-
);
|
|
537
|
-
return {
|
|
538
|
-
principalId: user2.defaultPrincipalId,
|
|
539
|
-
principalType: "service",
|
|
540
|
-
clerkId: actorId,
|
|
541
|
-
tenantId: user2.defaultTenantId,
|
|
542
|
-
workspaceId: user2.defaultWorkspaceId,
|
|
543
|
-
roles: ["service_agent"],
|
|
544
|
-
groupIds: normalizeGroupIds(resolvedUser2?.principalGroupIds),
|
|
545
|
-
isPlatformAdmin: false,
|
|
546
|
-
isTenantAdmin: false,
|
|
547
|
-
isWorkspaceAdmin: false,
|
|
548
|
-
isSystemFallback: false
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
const {
|
|
552
|
-
resolvedUser,
|
|
553
|
-
contextClerkId
|
|
554
|
-
} = await resolveCanonicalUserRecord(ctx, actorId);
|
|
555
|
-
const user = requireCanonicalResolvedUser(
|
|
556
|
-
resolvedUser,
|
|
557
|
-
contextClerkId
|
|
558
|
-
);
|
|
559
|
-
if (!user.defaultPrincipalId) {
|
|
560
|
-
throw new Error(
|
|
561
|
-
`[AccessControl] Canonical federated principal required for ${contextClerkId}. Re-sync Master Control identity before user-bound access checks.`
|
|
562
|
-
);
|
|
563
|
-
}
|
|
564
|
-
if (user.mcRole === "service_agent") {
|
|
565
|
-
return {
|
|
566
|
-
principalId: user.defaultPrincipalId,
|
|
567
|
-
principalType: "service",
|
|
568
|
-
clerkId: contextClerkId,
|
|
569
|
-
tenantId: user.defaultTenantId,
|
|
570
|
-
workspaceId: user.defaultWorkspaceId,
|
|
571
|
-
roles: ["service_agent"],
|
|
572
|
-
groupIds: normalizeGroupIds(resolvedUser?.principalGroupIds),
|
|
573
|
-
isPlatformAdmin: false,
|
|
574
|
-
isTenantAdmin: false,
|
|
575
|
-
isWorkspaceAdmin: false,
|
|
576
|
-
isSystemFallback: false
|
|
577
|
-
};
|
|
578
|
-
}
|
|
579
|
-
const principalId = user.defaultPrincipalId;
|
|
580
|
-
const effectiveRole = user.mcRole;
|
|
581
|
-
const roles = effectiveRole === "platform_admin" ? ["platform_admin", "tenant_admin"] : effectiveRole === "tenant_admin" ? ["tenant_admin"] : [effectiveRole];
|
|
582
|
-
const tenantId = user.defaultTenantId;
|
|
583
|
-
const workspaceId = user.defaultWorkspaceId;
|
|
584
|
-
const isPlatformAdmin = effectiveRole === "platform_admin";
|
|
585
|
-
return {
|
|
586
|
-
principalId,
|
|
587
|
-
principalType: "user",
|
|
588
|
-
clerkId: contextClerkId,
|
|
589
|
-
tenantId,
|
|
590
|
-
workspaceId,
|
|
591
|
-
roles: uniqRoles(roles),
|
|
592
|
-
groupIds: normalizeGroupIds(resolvedUser?.principalGroupIds),
|
|
593
|
-
isPlatformAdmin,
|
|
594
|
-
isTenantAdmin: isPlatformAdmin || effectiveRole === "tenant_admin",
|
|
595
|
-
isWorkspaceAdmin: isPlatformAdmin || effectiveRole === "tenant_admin" || effectiveRole === "workspace_admin",
|
|
596
|
-
isSystemFallback: false
|
|
597
|
-
};
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
// ../access-control/src/access.ts
|
|
601
|
-
function isTopicInPrincipalTenant(topic, principalTenantId) {
|
|
602
|
-
if (!topic.tenantId) {
|
|
603
|
-
return false;
|
|
604
|
-
}
|
|
605
|
-
if (!principalTenantId) {
|
|
606
|
-
return false;
|
|
607
|
-
}
|
|
608
|
-
return String(topic.tenantId) === String(principalTenantId);
|
|
609
|
-
}
|
|
610
|
-
function isTopicInPrincipalWorkspace(topic, principalWorkspaceId) {
|
|
611
|
-
if (!topic.workspaceId) {
|
|
612
|
-
return false;
|
|
613
|
-
}
|
|
614
|
-
if (!principalWorkspaceId) {
|
|
615
|
-
return false;
|
|
616
|
-
}
|
|
617
|
-
return String(topic.workspaceId) === String(principalWorkspaceId);
|
|
618
|
-
}
|
|
619
|
-
function isLegacyUnscopedTopic(topic) {
|
|
620
|
-
return !topic.tenantId || !topic.workspaceId;
|
|
621
|
-
}
|
|
622
|
-
function isGrantScopeAlignedToTopic(topic, grant) {
|
|
623
|
-
if (topic.tenantId && grant.tenantId && String(topic.tenantId) !== String(grant.tenantId)) {
|
|
624
|
-
return false;
|
|
625
|
-
}
|
|
626
|
-
if (topic.workspaceId && grant.workspaceId && String(topic.workspaceId) !== String(grant.workspaceId)) {
|
|
627
|
-
return false;
|
|
628
|
-
}
|
|
629
|
-
return true;
|
|
630
|
-
}
|
|
631
|
-
function isGrantSourceAllowedForVisibility(visibility, source) {
|
|
632
|
-
if (source !== "external_share") {
|
|
633
|
-
return true;
|
|
634
|
-
}
|
|
635
|
-
return visibility === "external" || visibility === "public";
|
|
636
|
-
}
|
|
637
|
-
function isGrantActive(grant) {
|
|
638
|
-
if (grant.status !== "active") {
|
|
639
|
-
return false;
|
|
640
|
-
}
|
|
641
|
-
if (grant.expiresAt !== void 0 && grant.expiresAt <= Date.now()) {
|
|
642
|
-
return false;
|
|
643
|
-
}
|
|
644
|
-
return true;
|
|
645
|
-
}
|
|
646
|
-
async function hasPrincipalGrant(ctx, args) {
|
|
647
|
-
const grants = await resolveAccessControlAppResolvers().listProjectGrantsByProjectAndPrincipal(
|
|
648
|
-
ctx,
|
|
649
|
-
args.topic._id,
|
|
650
|
-
args.principalId
|
|
651
|
-
);
|
|
652
|
-
if (grants.some(
|
|
653
|
-
(grant) => isGrantActive(grant) && isGrantScopeAlignedToTopic(args.topic, grant) && isGrantSourceAllowedForVisibility(
|
|
654
|
-
args.topic.visibility,
|
|
655
|
-
grant.source
|
|
656
|
-
) && (!args.principalIsExternal || args.topic.visibility === "public" || grant.source === "external_share")
|
|
657
|
-
)) {
|
|
658
|
-
return true;
|
|
659
|
-
}
|
|
660
|
-
return false;
|
|
661
|
-
}
|
|
662
|
-
async function hasGroupGrant(ctx, args) {
|
|
663
|
-
if (args.groupIds.length === 0) {
|
|
664
|
-
return false;
|
|
665
|
-
}
|
|
666
|
-
for (const groupId of args.groupIds) {
|
|
667
|
-
const grants = await resolveAccessControlAppResolvers().listProjectGrantsByProjectAndGroup(ctx, args.topic._id, groupId);
|
|
668
|
-
if (grants.some(
|
|
669
|
-
(grant) => isGrantActive(grant) && isGrantScopeAlignedToTopic(args.topic, grant) && isGrantSourceAllowedForVisibility(
|
|
670
|
-
args.topic.visibility,
|
|
671
|
-
grant.source
|
|
672
|
-
)
|
|
673
|
-
)) {
|
|
674
|
-
return true;
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
return false;
|
|
678
|
-
}
|
|
679
|
-
function isExternalPrincipal(_ctx, _args) {
|
|
680
|
-
return false;
|
|
681
|
-
}
|
|
682
|
-
async function evaluateTopicAccessDetailed(ctx, args) {
|
|
683
|
-
if (args.legacyUserId) {
|
|
684
|
-
return {
|
|
685
|
-
hasAccess: true,
|
|
686
|
-
isAdmin: false,
|
|
687
|
-
isOwner: false,
|
|
688
|
-
isShared: false,
|
|
689
|
-
hasGrant: true,
|
|
690
|
-
isFirmVisible: true,
|
|
691
|
-
isExternalVisible: false,
|
|
692
|
-
isPublicVisible: false,
|
|
693
|
-
isTenantScopeMatch: true,
|
|
694
|
-
isWorkspaceScopeMatch: true,
|
|
695
|
-
isPrincipalExternal: false
|
|
696
|
-
};
|
|
697
|
-
}
|
|
698
|
-
const topic = await resolveAccessControlAppResolvers().getProject(
|
|
699
|
-
ctx,
|
|
700
|
-
args.topicId
|
|
701
|
-
);
|
|
702
|
-
if (!topic) {
|
|
703
|
-
return {
|
|
704
|
-
hasAccess: false,
|
|
705
|
-
isAdmin: false,
|
|
706
|
-
isOwner: false,
|
|
707
|
-
isShared: false,
|
|
708
|
-
hasGrant: false,
|
|
709
|
-
isFirmVisible: false,
|
|
710
|
-
isExternalVisible: false,
|
|
711
|
-
isPublicVisible: false,
|
|
712
|
-
isTenantScopeMatch: false,
|
|
713
|
-
isWorkspaceScopeMatch: false,
|
|
714
|
-
isPrincipalExternal: false
|
|
715
|
-
};
|
|
716
|
-
}
|
|
717
|
-
const { principalContext, legacyUserId } = args;
|
|
718
|
-
const userIsAdmin = principalContext.isPlatformAdmin;
|
|
719
|
-
const isOwner = topic.ownerId === legacyUserId;
|
|
720
|
-
const isShared = (topic.sharedWith ?? []).includes(legacyUserId);
|
|
721
|
-
const principalIsExternal = await isExternalPrincipal(ctx, {
|
|
722
|
-
groupIds: principalContext.groupIds,
|
|
723
|
-
topicTenantId: topic.tenantId,
|
|
724
|
-
topicWorkspaceId: topic.workspaceId
|
|
725
|
-
});
|
|
726
|
-
const hasPrincipalGrantResult = await hasPrincipalGrant(ctx, {
|
|
727
|
-
topic,
|
|
728
|
-
principalId: principalContext.principalId,
|
|
729
|
-
principalIsExternal
|
|
730
|
-
});
|
|
731
|
-
const hasGroupGrantResult = await hasGroupGrant(ctx, {
|
|
732
|
-
topic,
|
|
733
|
-
groupIds: principalContext.groupIds
|
|
734
|
-
});
|
|
735
|
-
const hasGrant = isShared || hasPrincipalGrantResult || hasGroupGrantResult;
|
|
736
|
-
const legacyUnscoped = isLegacyUnscopedTopic(topic);
|
|
737
|
-
const tenantScopeMatch = isTopicInPrincipalTenant(
|
|
738
|
-
topic,
|
|
739
|
-
principalContext.tenantId
|
|
740
|
-
);
|
|
741
|
-
const workspaceScopeMatch = isTopicInPrincipalWorkspace(
|
|
742
|
-
topic,
|
|
743
|
-
principalContext.workspaceId
|
|
744
|
-
);
|
|
745
|
-
const isPublicVisible = topic.visibility === "public";
|
|
746
|
-
const isFirmVisible = topic.visibility === "firm" && !legacyUnscoped && tenantScopeMatch && workspaceScopeMatch && !principalIsExternal;
|
|
747
|
-
const hasScopedGrant = hasGrant && (legacyUnscoped || tenantScopeMatch && workspaceScopeMatch);
|
|
748
|
-
const isExternalVisible = topic.visibility === "external" && hasScopedGrant;
|
|
749
|
-
const hasAccess = userIsAdmin || isOwner || hasScopedGrant || isPublicVisible || isFirmVisible;
|
|
750
|
-
return {
|
|
751
|
-
hasAccess,
|
|
752
|
-
isAdmin: userIsAdmin,
|
|
753
|
-
isOwner,
|
|
754
|
-
isShared,
|
|
755
|
-
hasGrant,
|
|
756
|
-
isFirmVisible,
|
|
757
|
-
isExternalVisible,
|
|
758
|
-
isPublicVisible,
|
|
759
|
-
isTenantScopeMatch: tenantScopeMatch,
|
|
760
|
-
isWorkspaceScopeMatch: workspaceScopeMatch,
|
|
761
|
-
isPrincipalExternal: principalIsExternal
|
|
762
|
-
};
|
|
763
|
-
}
|
|
764
|
-
async function checkTopicAccessDetailed(ctx, topicId, userId) {
|
|
765
|
-
const principalContext = await resolvePrincipalContext(ctx, userId);
|
|
766
|
-
return evaluateTopicAccessDetailed(ctx, {
|
|
767
|
-
topicId,
|
|
768
|
-
legacyUserId: userId,
|
|
769
|
-
principalContext
|
|
770
|
-
});
|
|
771
|
-
}
|
|
772
|
-
async function checkTopicAccess(ctx, topicId, userId) {
|
|
773
|
-
const result = await checkTopicAccessDetailed(ctx, topicId, userId);
|
|
774
|
-
return result.hasAccess;
|
|
775
|
-
}
|
|
776
|
-
async function checkScopeAccess(ctx, scopeId, userId) {
|
|
777
|
-
try {
|
|
778
|
-
const topic = await ctx.db.get(scopeId);
|
|
779
|
-
if (topic && topic.name !== void 0 && topic.type !== void 0) {
|
|
780
|
-
return true;
|
|
781
|
-
}
|
|
782
|
-
} catch {
|
|
783
|
-
}
|
|
784
|
-
try {
|
|
785
|
-
return await checkTopicAccess(ctx, scopeId, userId);
|
|
786
|
-
} catch {
|
|
787
|
-
return false;
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
async function requireTopicAccess(ctx, topicId, userId) {
|
|
791
|
-
const hasAccess = await checkTopicAccess(ctx, topicId, userId);
|
|
792
|
-
if (!hasAccess) {
|
|
793
|
-
throw new Error(
|
|
794
|
-
"Access denied: You don't have permission to access this topic"
|
|
795
|
-
);
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
var requireProjectAccess = requireTopicAccess;
|
|
799
|
-
var permissiveReturn = v.optional(v.any());
|
|
800
|
-
var looseJsonObject = v.record(v.string(), v.any());
|
|
801
|
-
var looseJsonArray = v.array(v.any());
|
|
802
|
-
v.union(
|
|
803
|
-
v.string(),
|
|
804
|
-
v.number(),
|
|
805
|
-
v.boolean(),
|
|
806
|
-
v.null(),
|
|
807
|
-
looseJsonObject,
|
|
808
|
-
looseJsonArray
|
|
809
|
-
);
|
|
810
|
-
var api2 = anyApi;
|
|
811
|
-
componentsGeneric();
|
|
812
|
-
var internal = anyApi;
|
|
813
|
-
var internalMutation = internalMutationGeneric;
|
|
814
|
-
var mutation = mutationGeneric;
|
|
815
|
-
var query = queryGeneric;
|
|
816
25
|
|
|
817
26
|
// src/graphTypes.ts
|
|
818
27
|
var EDGE_TYPE_TO_REL = {
|
|
@@ -1153,96 +362,125 @@ function validateEdgeLayers(edgeType, fromLayer, toLayer) {
|
|
|
1153
362
|
}
|
|
1154
363
|
|
|
1155
364
|
// src/topicProjectOverlay.ts
|
|
1156
|
-
var
|
|
1157
|
-
function
|
|
365
|
+
var LEGACY_SCOPE_FIELD = "graphScopeProjectId";
|
|
366
|
+
function readNonEmptyString(value) {
|
|
1158
367
|
if (typeof value !== "string") {
|
|
1159
368
|
return;
|
|
1160
369
|
}
|
|
1161
370
|
const normalized = value.trim();
|
|
1162
371
|
return normalized.length > 0 ? normalized : void 0;
|
|
1163
372
|
}
|
|
1164
|
-
function
|
|
373
|
+
function readStringArray(value) {
|
|
1165
374
|
if (!Array.isArray(value)) {
|
|
1166
375
|
return [];
|
|
1167
376
|
}
|
|
1168
|
-
return value.map((entry) =>
|
|
377
|
+
return value.map((entry) => readNonEmptyString(entry)).filter((entry) => Boolean(entry));
|
|
1169
378
|
}
|
|
1170
|
-
function
|
|
379
|
+
function readMetadata(topic) {
|
|
1171
380
|
return topic.metadata && typeof topic.metadata === "object" ? topic.metadata : {};
|
|
1172
381
|
}
|
|
1173
|
-
function
|
|
382
|
+
function readLegacyProjectId(value) {
|
|
1174
383
|
if (!value) {
|
|
1175
384
|
return;
|
|
1176
385
|
}
|
|
1177
|
-
return
|
|
386
|
+
return readNonEmptyString(value[LEGACY_SCOPE_FIELD]);
|
|
1178
387
|
}
|
|
1179
|
-
function
|
|
388
|
+
function coerceVisibility(value) {
|
|
1180
389
|
return value === "private" || value === "team" || value === "firm" || value === "external" || value === "public" ? value : void 0;
|
|
1181
390
|
}
|
|
1182
|
-
function
|
|
391
|
+
function coerceStatus(value) {
|
|
1183
392
|
return value === "active" || value === "archived" || value === "watching" ? value : void 0;
|
|
1184
393
|
}
|
|
1185
|
-
function
|
|
1186
|
-
const explicit =
|
|
394
|
+
function mapProjectType(topic, metadata) {
|
|
395
|
+
const explicit = readNonEmptyString(metadata.projectType);
|
|
1187
396
|
if (explicit) {
|
|
1188
397
|
return explicit;
|
|
1189
398
|
}
|
|
1190
399
|
if (topic.type === "theme") {
|
|
1191
400
|
return "thematic";
|
|
1192
401
|
}
|
|
1193
|
-
return
|
|
402
|
+
return readNonEmptyString(topic.type) || "general";
|
|
1194
403
|
}
|
|
1195
|
-
function
|
|
1196
|
-
const metadata =
|
|
1197
|
-
return topic.type === "theme" || topic.type === "thematic" || topic.type === "deal" || topic.type === "monitoring" ||
|
|
404
|
+
function isProjectLikeTopic(topic) {
|
|
405
|
+
const metadata = readMetadata(topic);
|
|
406
|
+
return topic.type === "theme" || topic.type === "thematic" || topic.type === "deal" || topic.type === "monitoring" || readLegacyProjectId(topic) !== void 0 || readNonEmptyString(metadata.projectType) !== void 0;
|
|
1198
407
|
}
|
|
1199
408
|
function isMissingLucernChildComponentError(error) {
|
|
1200
|
-
const message =
|
|
409
|
+
const message = getErrorMessage(error);
|
|
1201
410
|
return message.includes(
|
|
1202
411
|
'Child component ComponentName(Identifier("lucern")) not found'
|
|
1203
412
|
) || message.includes("Child component") && message.includes("lucern") && message.includes("not found");
|
|
1204
413
|
}
|
|
1205
|
-
|
|
414
|
+
function getErrorMessage(error) {
|
|
415
|
+
if (error instanceof Error) {
|
|
416
|
+
return error.message;
|
|
417
|
+
}
|
|
418
|
+
if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string") {
|
|
419
|
+
return error.message;
|
|
420
|
+
}
|
|
421
|
+
return "unknown error";
|
|
422
|
+
}
|
|
423
|
+
async function resolveTopicDoc(ctx, scopeId) {
|
|
1206
424
|
if (ctx?.db && typeof ctx.db.get === "function") {
|
|
1207
425
|
try {
|
|
1208
|
-
const directTopic = await ctx.db.get(
|
|
426
|
+
const directTopic = await ctx.db.get(
|
|
427
|
+
scopeId
|
|
428
|
+
);
|
|
1209
429
|
if (directTopic) {
|
|
1210
430
|
return directTopic;
|
|
1211
431
|
}
|
|
1212
|
-
} catch {
|
|
432
|
+
} catch (error) {
|
|
433
|
+
debugGraphPrimitiveFallback(
|
|
434
|
+
"[topicProjectOverlay] Failed to resolve topic by direct ID",
|
|
435
|
+
{
|
|
436
|
+
error,
|
|
437
|
+
scopeId
|
|
438
|
+
}
|
|
439
|
+
);
|
|
1213
440
|
}
|
|
1214
441
|
}
|
|
1215
442
|
if (typeof ctx.runQuery !== "function") {
|
|
1216
443
|
return null;
|
|
1217
444
|
}
|
|
1218
445
|
try {
|
|
1219
|
-
const topic = await ctx.runQuery(
|
|
446
|
+
const topic = await ctx.runQuery(api.topics.get, {
|
|
1220
447
|
id: String(scopeId)
|
|
1221
448
|
});
|
|
1222
449
|
if (topic?.name !== void 0 && topic?.type !== void 0) {
|
|
1223
450
|
return topic;
|
|
1224
451
|
}
|
|
1225
|
-
} catch {
|
|
452
|
+
} catch (error) {
|
|
453
|
+
debugGraphPrimitiveFallback(
|
|
454
|
+
"[topicProjectOverlay] Failed to resolve topic by ID query",
|
|
455
|
+
{
|
|
456
|
+
error,
|
|
457
|
+
scopeId
|
|
458
|
+
}
|
|
459
|
+
);
|
|
1226
460
|
}
|
|
1227
461
|
try {
|
|
1228
|
-
const topic = await ctx.runQuery(
|
|
462
|
+
const topic = await ctx.runQuery(api.topics.getByLegacyScopeId, {
|
|
1229
463
|
projectId: String(scopeId)
|
|
1230
464
|
});
|
|
1231
465
|
if (topic?.name !== void 0 && topic?.type !== void 0) {
|
|
1232
466
|
return topic;
|
|
1233
467
|
}
|
|
1234
|
-
} catch {
|
|
468
|
+
} catch (error) {
|
|
469
|
+
debugGraphPrimitiveFallback(
|
|
470
|
+
"[topicProjectOverlay] Failed to resolve topic by legacy scope ID",
|
|
471
|
+
{ error, scopeId }
|
|
472
|
+
);
|
|
1235
473
|
}
|
|
1236
474
|
return null;
|
|
1237
475
|
}
|
|
1238
|
-
function
|
|
1239
|
-
const metadata =
|
|
476
|
+
function materializeTopicProjectOverlay(topic, idMode = "legacy") {
|
|
477
|
+
const metadata = readMetadata(topic);
|
|
1240
478
|
const topicId = String(topic._id);
|
|
1241
|
-
const legacyProjectId =
|
|
479
|
+
const legacyProjectId = readLegacyProjectId(topic) || readLegacyProjectId(metadata) || readNonEmptyString(metadata.legacyProjectId);
|
|
1242
480
|
const storageProjectId = legacyProjectId || topicId;
|
|
1243
481
|
const outwardId = idMode === "topic" ? topicId : storageProjectId;
|
|
1244
|
-
const visibility =
|
|
1245
|
-
const status =
|
|
482
|
+
const visibility = coerceVisibility(topic.visibility) || coerceVisibility(metadata.visibility) || "private";
|
|
483
|
+
const status = coerceStatus(topic.status) || coerceStatus(metadata.status) || "active";
|
|
1246
484
|
const createdAt = typeof topic.createdAt === "number" ? topic.createdAt : typeof topic._creationTime === "number" ? topic._creationTime : 0;
|
|
1247
485
|
const updatedAt = typeof topic.updatedAt === "number" ? topic.updatedAt : typeof metadata.updatedAt === "number" ? metadata.updatedAt : createdAt;
|
|
1248
486
|
return {
|
|
@@ -1252,16 +490,16 @@ function materializeTopicProjectOverlay2(topic, idMode = "legacy") {
|
|
|
1252
490
|
topicId,
|
|
1253
491
|
storageProjectId,
|
|
1254
492
|
legacyProjectId,
|
|
1255
|
-
name:
|
|
1256
|
-
type:
|
|
1257
|
-
description:
|
|
1258
|
-
ownerId:
|
|
1259
|
-
sharedWith:
|
|
493
|
+
name: readNonEmptyString(topic.name) || "Untitled Theme",
|
|
494
|
+
type: mapProjectType(topic, metadata),
|
|
495
|
+
description: readNonEmptyString(topic.description),
|
|
496
|
+
ownerId: readNonEmptyString(metadata.ownerId) || readNonEmptyString(topic.createdBy) || "system",
|
|
497
|
+
sharedWith: readStringArray(metadata.sharedWith),
|
|
1260
498
|
visibility,
|
|
1261
|
-
tenantId:
|
|
1262
|
-
workspaceId:
|
|
499
|
+
tenantId: readNonEmptyString(topic.tenantId) || readNonEmptyString(metadata.tenantId),
|
|
500
|
+
workspaceId: readNonEmptyString(topic.workspaceId) || readNonEmptyString(metadata.workspaceId),
|
|
1263
501
|
status,
|
|
1264
|
-
tags:
|
|
502
|
+
tags: readStringArray(metadata.tags),
|
|
1265
503
|
chatCount: typeof metadata.chatCount === "number" ? metadata.chatCount : 0,
|
|
1266
504
|
artifactCount: typeof metadata.artifactCount === "number" ? metadata.artifactCount : 0,
|
|
1267
505
|
lastActivityAt: typeof metadata.lastActivityAt === "number" ? metadata.lastActivityAt : updatedAt,
|
|
@@ -1270,38 +508,42 @@ function materializeTopicProjectOverlay2(topic, idMode = "legacy") {
|
|
|
1270
508
|
updatedAt
|
|
1271
509
|
};
|
|
1272
510
|
}
|
|
1273
|
-
async function
|
|
1274
|
-
const topic = await
|
|
511
|
+
async function resolveTopicProjectOverlay(ctx, scopeId, options = {}) {
|
|
512
|
+
const topic = await resolveTopicDoc(ctx, scopeId);
|
|
1275
513
|
if (!topic) {
|
|
1276
514
|
return null;
|
|
1277
515
|
}
|
|
1278
|
-
if (options.projectLikeOnly !== false && !
|
|
516
|
+
if (options.projectLikeOnly !== false && !isProjectLikeTopic(topic)) {
|
|
1279
517
|
return null;
|
|
1280
518
|
}
|
|
1281
|
-
return
|
|
519
|
+
return materializeTopicProjectOverlay(topic, options.idMode);
|
|
1282
520
|
}
|
|
1283
|
-
async function
|
|
521
|
+
async function listTopicProjectOverlays(ctx, options = {}) {
|
|
1284
522
|
let allTopics = [];
|
|
1285
523
|
if (ctx?.db?.query && typeof ctx.db.query === "function") {
|
|
1286
524
|
try {
|
|
1287
525
|
allTopics = await ctx.db.query("topics").collect();
|
|
1288
|
-
} catch {
|
|
526
|
+
} catch (error) {
|
|
527
|
+
debugGraphPrimitiveFallback(
|
|
528
|
+
"[topicProjectOverlay] Failed to read topics table; falling back to API",
|
|
529
|
+
{ error }
|
|
530
|
+
);
|
|
1289
531
|
allTopics = [];
|
|
1290
532
|
}
|
|
1291
533
|
}
|
|
1292
534
|
if (allTopics.length === 0 && typeof ctx.runQuery === "function") {
|
|
1293
|
-
allTopics = (await ctx.runQuery(
|
|
535
|
+
allTopics = (await ctx.runQuery(api.topics.list, {}) ?? []) || [];
|
|
1294
536
|
}
|
|
1295
537
|
return allTopics.filter(
|
|
1296
|
-
(topic) => options.projectLikeOnly === false ||
|
|
1297
|
-
).map((topic) =>
|
|
538
|
+
(topic) => options.projectLikeOnly === false || isProjectLikeTopic(topic)
|
|
539
|
+
).map((topic) => materializeTopicProjectOverlay(topic, options.idMode));
|
|
1298
540
|
}
|
|
1299
541
|
async function patchTopicProjectOverlay(ctx, scopeId, value) {
|
|
1300
|
-
const topic = await
|
|
542
|
+
const topic = await resolveTopicDoc(ctx, scopeId);
|
|
1301
543
|
if (!topic) {
|
|
1302
544
|
return null;
|
|
1303
545
|
}
|
|
1304
|
-
const nextMetadata = { ...
|
|
546
|
+
const nextMetadata = { ...readMetadata(topic) };
|
|
1305
547
|
const patch = {};
|
|
1306
548
|
const topicUpdateArgs = {
|
|
1307
549
|
id: String(topic._id)
|
|
@@ -1326,7 +568,7 @@ async function patchTopicProjectOverlay(ctx, scopeId, value) {
|
|
|
1326
568
|
`patchTopicProjectOverlay cannot mutate ${key} via component-owned topics`
|
|
1327
569
|
);
|
|
1328
570
|
case "status": {
|
|
1329
|
-
const status =
|
|
571
|
+
const status = coerceStatus(rawValue);
|
|
1330
572
|
if (status) {
|
|
1331
573
|
patch.status = status;
|
|
1332
574
|
topicUpdateArgs.status = status;
|
|
@@ -1334,7 +576,7 @@ async function patchTopicProjectOverlay(ctx, scopeId, value) {
|
|
|
1334
576
|
break;
|
|
1335
577
|
}
|
|
1336
578
|
case "visibility": {
|
|
1337
|
-
const visibility =
|
|
579
|
+
const visibility = coerceVisibility(rawValue);
|
|
1338
580
|
if (visibility) {
|
|
1339
581
|
patch.visibility = visibility;
|
|
1340
582
|
topicUpdateArgs.visibility = visibility;
|
|
@@ -1342,7 +584,7 @@ async function patchTopicProjectOverlay(ctx, scopeId, value) {
|
|
|
1342
584
|
break;
|
|
1343
585
|
}
|
|
1344
586
|
case "type": {
|
|
1345
|
-
const projectType =
|
|
587
|
+
const projectType = readNonEmptyString(rawValue);
|
|
1346
588
|
if (projectType) {
|
|
1347
589
|
nextMetadata.projectType = projectType;
|
|
1348
590
|
} else {
|
|
@@ -1366,7 +608,7 @@ async function patchTopicProjectOverlay(ctx, scopeId, value) {
|
|
|
1366
608
|
topicUpdateArgs.metadata = nextMetadata;
|
|
1367
609
|
if (typeof ctx.runMutation === "function") {
|
|
1368
610
|
try {
|
|
1369
|
-
await ctx.runMutation(
|
|
611
|
+
await ctx.runMutation(api.topics.update, topicUpdateArgs);
|
|
1370
612
|
} catch (error) {
|
|
1371
613
|
if (!isMissingLucernChildComponentError(error) || !ctx?.db || typeof ctx.db.patch !== "function") {
|
|
1372
614
|
throw error;
|
|
@@ -1380,19 +622,28 @@ async function patchTopicProjectOverlay(ctx, scopeId, value) {
|
|
|
1380
622
|
"Cannot patch topic without component adapter (ctx.runMutation unavailable)"
|
|
1381
623
|
);
|
|
1382
624
|
}
|
|
1383
|
-
return
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
}
|
|
1389
|
-
);
|
|
625
|
+
return materializeTopicProjectOverlay({
|
|
626
|
+
...topic,
|
|
627
|
+
...patch,
|
|
628
|
+
metadata: nextMetadata
|
|
629
|
+
});
|
|
1390
630
|
}
|
|
1391
631
|
|
|
1392
632
|
// src/resolvers.ts
|
|
1393
633
|
function isMissingLucernChildComponentError2(error) {
|
|
1394
|
-
const message =
|
|
1395
|
-
return message.includes(
|
|
634
|
+
const message = getErrorMessage2(error);
|
|
635
|
+
return message.includes(
|
|
636
|
+
'Child component ComponentName(Identifier("lucern")) not found'
|
|
637
|
+
) || message.includes("Child component") && message.includes("lucern") && message.includes("not found");
|
|
638
|
+
}
|
|
639
|
+
function getErrorMessage2(error) {
|
|
640
|
+
if (error instanceof Error) {
|
|
641
|
+
return error.message;
|
|
642
|
+
}
|
|
643
|
+
if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string") {
|
|
644
|
+
return error.message;
|
|
645
|
+
}
|
|
646
|
+
return "unknown error";
|
|
1396
647
|
}
|
|
1397
648
|
function isAdvisoryTopicPatch(value) {
|
|
1398
649
|
const advisoryKeys = /* @__PURE__ */ new Set(["lastActivityAt", "updatedAt"]);
|
|
@@ -1406,52 +657,47 @@ async function patchProjectWithTolerance(ctx, projectId, value) {
|
|
|
1406
657
|
if (!isAdvisoryTopicPatch(value) || !isMissingLucernChildComponentError2(error)) {
|
|
1407
658
|
throw error;
|
|
1408
659
|
}
|
|
1409
|
-
console.warn(
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
660
|
+
console.warn(
|
|
661
|
+
"[lucern graph-primitives] Non-fatal advisory topic patch failure",
|
|
662
|
+
{
|
|
663
|
+
projectId,
|
|
664
|
+
keys: Object.keys(value),
|
|
665
|
+
error: getErrorMessage2(error)
|
|
666
|
+
}
|
|
667
|
+
);
|
|
1414
668
|
}
|
|
1415
669
|
}
|
|
1416
|
-
function
|
|
670
|
+
function defaultResolvers() {
|
|
1417
671
|
return {
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
async listTopics(ctx) {
|
|
1428
|
-
return await listTopicProjectOverlays2(ctx, {
|
|
1429
|
-
idMode: "legacy"
|
|
1430
|
-
});
|
|
1431
|
-
},
|
|
1432
|
-
async getFinalArtifact(ctx, artifactId) {
|
|
1433
|
-
return await ctx.db.get(artifactId);
|
|
1434
|
-
}
|
|
672
|
+
getProject: (ctx, projectId) => resolveTopicProjectOverlay(ctx, projectId, {
|
|
673
|
+
idMode: "legacy",
|
|
674
|
+
projectLikeOnly: false
|
|
675
|
+
}),
|
|
676
|
+
patchProject: (ctx, projectId, value) => patchProjectWithTolerance(ctx, projectId, value),
|
|
677
|
+
listTopics: (ctx) => listTopicProjectOverlays(ctx, {
|
|
678
|
+
idMode: "legacy"
|
|
679
|
+
}),
|
|
680
|
+
getFinalArtifact: (ctx, artifactId) => ctx.db.get(artifactId)
|
|
1435
681
|
};
|
|
1436
682
|
}
|
|
1437
|
-
var
|
|
683
|
+
var resolverOverrides = {};
|
|
1438
684
|
function resolveGraphPrimitivesAppResolvers(_ctx) {
|
|
1439
685
|
return {
|
|
1440
|
-
...
|
|
1441
|
-
...
|
|
686
|
+
...defaultResolvers(),
|
|
687
|
+
...resolverOverrides
|
|
1442
688
|
};
|
|
1443
689
|
}
|
|
1444
|
-
var
|
|
690
|
+
var LEGACY_SCOPE_FIELD2 = "graphScopeProjectId";
|
|
1445
691
|
function asMappedProjectId(topic) {
|
|
1446
692
|
if (!topic) {
|
|
1447
693
|
return;
|
|
1448
694
|
}
|
|
1449
|
-
const directLegacyProjectId = normalizeScopeValue(topic[
|
|
695
|
+
const directLegacyProjectId = normalizeScopeValue(topic[LEGACY_SCOPE_FIELD2]);
|
|
1450
696
|
if (directLegacyProjectId) {
|
|
1451
697
|
return directLegacyProjectId;
|
|
1452
698
|
}
|
|
1453
699
|
const metadata = topic.metadata || {};
|
|
1454
|
-
const candidate = metadata[
|
|
700
|
+
const candidate = metadata[LEGACY_SCOPE_FIELD2] || metadata.legacyProjectId || metadata.projectId || metadata.scopeProjectId;
|
|
1455
701
|
return candidate ? candidate : void 0;
|
|
1456
702
|
}
|
|
1457
703
|
function normalizeScopeValue(value) {
|
|
@@ -1480,9 +726,16 @@ async function findTopicsByScopeAlias(ctx, scopeId) {
|
|
|
1480
726
|
try {
|
|
1481
727
|
return await ctx.db.query("topics").withIndex(
|
|
1482
728
|
"by_graph_scope_project",
|
|
1483
|
-
(q) => q.eq(
|
|
729
|
+
(q) => q.eq(LEGACY_SCOPE_FIELD2, scopeId)
|
|
1484
730
|
).collect();
|
|
1485
|
-
} catch {
|
|
731
|
+
} catch (error) {
|
|
732
|
+
debugGraphPrimitiveFallback(
|
|
733
|
+
"[topicScope] Failed to resolve scope alias via index",
|
|
734
|
+
{
|
|
735
|
+
error,
|
|
736
|
+
scopeId
|
|
737
|
+
}
|
|
738
|
+
);
|
|
1486
739
|
const topics = await ctx.db.query("topics").collect();
|
|
1487
740
|
return topics.filter((topic) => {
|
|
1488
741
|
const normalizedGlobalId = normalizeScopeValue(topic.globalId);
|
|
@@ -1496,10 +749,17 @@ async function tryResolveHostTopicById(ctx, topicId) {
|
|
|
1496
749
|
return null;
|
|
1497
750
|
}
|
|
1498
751
|
try {
|
|
1499
|
-
return await ctx.runQuery(
|
|
752
|
+
return await ctx.runQuery(api.topics.get, {
|
|
1500
753
|
id: topicId
|
|
1501
754
|
}) ?? null;
|
|
1502
|
-
} catch {
|
|
755
|
+
} catch (error) {
|
|
756
|
+
debugGraphPrimitiveFallback(
|
|
757
|
+
"[topicScope] Failed to resolve topic by host query",
|
|
758
|
+
{
|
|
759
|
+
error,
|
|
760
|
+
topicId
|
|
761
|
+
}
|
|
762
|
+
);
|
|
1503
763
|
return null;
|
|
1504
764
|
}
|
|
1505
765
|
}
|
|
@@ -1508,10 +768,17 @@ async function tryResolveHostTopicByLegacyScope(ctx, legacyScopeId) {
|
|
|
1508
768
|
return null;
|
|
1509
769
|
}
|
|
1510
770
|
try {
|
|
1511
|
-
return await ctx.runQuery(
|
|
771
|
+
return await ctx.runQuery(api.topics.getByLegacyScopeId, {
|
|
1512
772
|
projectId: legacyScopeId
|
|
1513
773
|
}) ?? null;
|
|
1514
|
-
} catch {
|
|
774
|
+
} catch (error) {
|
|
775
|
+
debugGraphPrimitiveFallback(
|
|
776
|
+
"[topicScope] Failed to resolve topic by legacy scope",
|
|
777
|
+
{
|
|
778
|
+
error,
|
|
779
|
+
legacyScopeId
|
|
780
|
+
}
|
|
781
|
+
);
|
|
1515
782
|
return null;
|
|
1516
783
|
}
|
|
1517
784
|
}
|
|
@@ -1540,8 +807,17 @@ async function resolveTopicProjectScope(ctx, args) {
|
|
|
1540
807
|
if (args.topicId) {
|
|
1541
808
|
let topic = null;
|
|
1542
809
|
try {
|
|
1543
|
-
topic = await ctx.db.get(
|
|
1544
|
-
|
|
810
|
+
topic = await ctx.db.get(
|
|
811
|
+
args.topicId
|
|
812
|
+
);
|
|
813
|
+
} catch (error) {
|
|
814
|
+
debugGraphPrimitiveFallback(
|
|
815
|
+
"[topicScope] Failed to load topic by direct id",
|
|
816
|
+
{
|
|
817
|
+
error,
|
|
818
|
+
topicId: args.topicId
|
|
819
|
+
}
|
|
820
|
+
);
|
|
1545
821
|
}
|
|
1546
822
|
if (!topic) {
|
|
1547
823
|
topic = await tryResolveHostTopicById(ctx, String(args.topicId));
|
|
@@ -1578,7 +854,14 @@ async function resolveTopicProjectScope(ctx, args) {
|
|
|
1578
854
|
directTopic = await ctx.db.get(
|
|
1579
855
|
args.projectId
|
|
1580
856
|
);
|
|
1581
|
-
} catch {
|
|
857
|
+
} catch (error) {
|
|
858
|
+
debugGraphPrimitiveFallback(
|
|
859
|
+
"[topicScope] Failed to load direct project topic",
|
|
860
|
+
{
|
|
861
|
+
error,
|
|
862
|
+
projectId: args.projectId
|
|
863
|
+
}
|
|
864
|
+
);
|
|
1582
865
|
}
|
|
1583
866
|
if (directTopic) {
|
|
1584
867
|
const inherited = await resolveInheritedWorkspaceScope(ctx, directTopic);
|
|
@@ -1785,6 +1068,34 @@ var edgeTypeValidator = v.union(
|
|
|
1785
1068
|
v.literal("competes_with")
|
|
1786
1069
|
// Company → Company (ontological → ontological)
|
|
1787
1070
|
);
|
|
1071
|
+
function buildEdgeStatusSuccessResult() {
|
|
1072
|
+
return { success: true };
|
|
1073
|
+
}
|
|
1074
|
+
function buildEdgeNotFoundResult() {
|
|
1075
|
+
const result = {};
|
|
1076
|
+
result.success = false;
|
|
1077
|
+
result.error = "Edge not found";
|
|
1078
|
+
return result;
|
|
1079
|
+
}
|
|
1080
|
+
function buildEdgeMirrorSkippedResult() {
|
|
1081
|
+
return {
|
|
1082
|
+
success: false,
|
|
1083
|
+
reason: "source_not_in_convex"
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
function buildEdgeMirrorMissingResult() {
|
|
1087
|
+
return {
|
|
1088
|
+
success: false,
|
|
1089
|
+
reason: "not_found"
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
function buildEdgeMirrorWriteResult(edgeId, existed) {
|
|
1093
|
+
return {
|
|
1094
|
+
success: true,
|
|
1095
|
+
edgeId,
|
|
1096
|
+
existed
|
|
1097
|
+
};
|
|
1098
|
+
}
|
|
1788
1099
|
var get = query({
|
|
1789
1100
|
args: { edgeId: v.id("epistemicEdges") },
|
|
1790
1101
|
returns: permissiveReturn,
|
|
@@ -1936,7 +1247,15 @@ var getByProjectAndType = query({
|
|
|
1936
1247
|
projectId: args.projectId,
|
|
1937
1248
|
topicId: args.topicId
|
|
1938
1249
|
});
|
|
1939
|
-
} catch {
|
|
1250
|
+
} catch (error) {
|
|
1251
|
+
debugGraphPrimitiveFallback(
|
|
1252
|
+
"[epistemicEdges] Failed to resolve getAll scope",
|
|
1253
|
+
{
|
|
1254
|
+
error,
|
|
1255
|
+
projectId: args.projectId,
|
|
1256
|
+
topicId: args.topicId
|
|
1257
|
+
}
|
|
1258
|
+
);
|
|
1940
1259
|
return [];
|
|
1941
1260
|
}
|
|
1942
1261
|
const projectEdges = await collectScopedEdges(ctx, scope, 5e3);
|
|
@@ -1962,7 +1281,15 @@ var getByProject = query({
|
|
|
1962
1281
|
projectId: args.projectId,
|
|
1963
1282
|
topicId: args.topicId
|
|
1964
1283
|
});
|
|
1965
|
-
} catch {
|
|
1284
|
+
} catch (error) {
|
|
1285
|
+
debugGraphPrimitiveFallback(
|
|
1286
|
+
"[epistemicEdges] Failed to resolve getByProject scope",
|
|
1287
|
+
{
|
|
1288
|
+
error,
|
|
1289
|
+
projectId: args.projectId,
|
|
1290
|
+
topicId: args.topicId
|
|
1291
|
+
}
|
|
1292
|
+
);
|
|
1966
1293
|
return [];
|
|
1967
1294
|
}
|
|
1968
1295
|
if (args.userId) {
|
|
@@ -1976,7 +1303,11 @@ var getByProject = query({
|
|
|
1976
1303
|
}
|
|
1977
1304
|
}
|
|
1978
1305
|
const pageSize = Math.max(1, Math.min(Math.floor(args.limit ?? 500), 2e3));
|
|
1979
|
-
const edges = await collectScopedEdges(
|
|
1306
|
+
const edges = await collectScopedEdges(
|
|
1307
|
+
ctx,
|
|
1308
|
+
scope,
|
|
1309
|
+
Math.min(pageSize * 3, 6e3)
|
|
1310
|
+
);
|
|
1980
1311
|
return edges.filter((edge) => edgeMatchesWorkspaceReasoningScope(edge, scope)).slice(0, pageSize);
|
|
1981
1312
|
}
|
|
1982
1313
|
});
|
|
@@ -2210,7 +1541,7 @@ var update = mutation({
|
|
|
2210
1541
|
projectId: edge.projectId
|
|
2211
1542
|
});
|
|
2212
1543
|
}
|
|
2213
|
-
return
|
|
1544
|
+
return buildEdgeStatusSuccessResult();
|
|
2214
1545
|
}
|
|
2215
1546
|
});
|
|
2216
1547
|
var remove = mutation({
|
|
@@ -2222,7 +1553,7 @@ var remove = mutation({
|
|
|
2222
1553
|
handler: async (ctx, args) => {
|
|
2223
1554
|
const edge = await ctx.db.get(args.edgeId);
|
|
2224
1555
|
if (!edge) {
|
|
2225
|
-
return
|
|
1556
|
+
return buildEdgeNotFoundResult();
|
|
2226
1557
|
}
|
|
2227
1558
|
if (edge.projectId && args.userId) {
|
|
2228
1559
|
await requireProjectAccess(ctx, edge.projectId, args.userId);
|
|
@@ -2257,7 +1588,7 @@ var remove = mutation({
|
|
|
2257
1588
|
globalId: edge.globalId
|
|
2258
1589
|
});
|
|
2259
1590
|
await ctx.db.delete(args.edgeId);
|
|
2260
|
-
return
|
|
1591
|
+
return buildEdgeStatusSuccessResult();
|
|
2261
1592
|
}
|
|
2262
1593
|
});
|
|
2263
1594
|
var removeBetween = mutation({
|
|
@@ -2417,9 +1748,7 @@ var getLineage = query({
|
|
|
2417
1748
|
}
|
|
2418
1749
|
visited.add(nodeIdStr);
|
|
2419
1750
|
const edges = await ctx.db.query("epistemicEdges").withIndex("by_from", (q) => q.eq("fromNodeId", current.nodeId)).collect();
|
|
2420
|
-
const lineageEdges = edges.filter(
|
|
2421
|
-
(e) => e.edgeType === "derived_from"
|
|
2422
|
-
);
|
|
1751
|
+
const lineageEdges = edges.filter((e) => e.edgeType === "derived_from");
|
|
2423
1752
|
for (const edge of lineageEdges) {
|
|
2424
1753
|
if (!edge.toNodeId) {
|
|
2425
1754
|
continue;
|
|
@@ -2620,13 +1949,13 @@ var mirrorEdgeToConvex = internalMutation({
|
|
|
2620
1949
|
console.log(
|
|
2621
1950
|
`[Dual-Write] Skipping mirror - source node not in Convex: ${args.fromGlobalId}`
|
|
2622
1951
|
);
|
|
2623
|
-
return
|
|
1952
|
+
return buildEdgeMirrorSkippedResult();
|
|
2624
1953
|
}
|
|
2625
1954
|
const toNode = await ctx.db.query("epistemicNodes").withIndex("by_globalId", (q) => q.eq("globalId", args.toGlobalId)).first();
|
|
2626
1955
|
const existing = await ctx.db.query("epistemicEdges").withIndex("by_globalId", (q) => q.eq("globalId", args.globalId)).first();
|
|
2627
1956
|
if (existing) {
|
|
2628
1957
|
console.log(`[Dual-Write] Edge already exists: ${args.globalId}`);
|
|
2629
|
-
return
|
|
1958
|
+
return buildEdgeMirrorWriteResult(existing._id, true);
|
|
2630
1959
|
}
|
|
2631
1960
|
const now = Date.now();
|
|
2632
1961
|
const edgeId = await ctx.db.insert("epistemicEdges", {
|
|
@@ -2667,7 +1996,7 @@ var mirrorEdgeToConvex = internalMutation({
|
|
|
2667
1996
|
console.log(
|
|
2668
1997
|
`[Dual-Write] Mirrored edge to Convex: ${args.globalId} (${args.edgeType})`
|
|
2669
1998
|
);
|
|
2670
|
-
return
|
|
1999
|
+
return buildEdgeMirrorWriteResult(edgeId, false);
|
|
2671
2000
|
}
|
|
2672
2001
|
});
|
|
2673
2002
|
var deleteEdgeFromConvex = internalMutation({
|
|
@@ -2678,11 +2007,11 @@ var deleteEdgeFromConvex = internalMutation({
|
|
|
2678
2007
|
handler: async (ctx, args) => {
|
|
2679
2008
|
const existing = await ctx.db.query("epistemicEdges").withIndex("by_globalId", (q) => q.eq("globalId", args.globalId)).first();
|
|
2680
2009
|
if (!existing) {
|
|
2681
|
-
return
|
|
2010
|
+
return buildEdgeMirrorMissingResult();
|
|
2682
2011
|
}
|
|
2683
2012
|
await ctx.db.delete(existing._id);
|
|
2684
2013
|
console.log(`[Dual-Write] Deleted edge from Convex: ${args.globalId}`);
|
|
2685
|
-
return
|
|
2014
|
+
return buildEdgeStatusSuccessResult();
|
|
2686
2015
|
}
|
|
2687
2016
|
});
|
|
2688
2017
|
var updateEdgeInConvex = internalMutation({
|
|
@@ -2703,7 +2032,7 @@ var updateEdgeInConvex = internalMutation({
|
|
|
2703
2032
|
handler: async (ctx, args) => {
|
|
2704
2033
|
const existing = await ctx.db.query("epistemicEdges").withIndex("by_globalId", (q) => q.eq("globalId", args.globalId)).first();
|
|
2705
2034
|
if (!existing) {
|
|
2706
|
-
return
|
|
2035
|
+
return buildEdgeMirrorMissingResult();
|
|
2707
2036
|
}
|
|
2708
2037
|
const updates = {
|
|
2709
2038
|
updatedAt: Date.now()
|
|
@@ -2740,7 +2069,7 @@ var updateEdgeInConvex = internalMutation({
|
|
|
2740
2069
|
}
|
|
2741
2070
|
await ctx.db.patch(existing._id, updates);
|
|
2742
2071
|
console.log(`[Dual-Write] Updated edge in Convex: ${args.globalId}`);
|
|
2743
|
-
return
|
|
2072
|
+
return buildEdgeStatusSuccessResult();
|
|
2744
2073
|
}
|
|
2745
2074
|
});
|
|
2746
2075
|
|