@anchrd/intel-api 0.3.0 → 0.3.2
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/adapters/cloudflare/cloudflare.js +12 -12
- package/dist/adapters/db/db-flows.js +417 -188
- package/dist/adapters/db/db-grants.d.ts +47 -0
- package/dist/adapters/db/db-grants.js +125 -0
- package/dist/adapters/db/db-indexing.js +14 -3
- package/dist/adapters/db/db.js +236 -202
- package/dist/flows/flows.d.ts +31 -1
- package/dist/flows/flows.js +988 -74
- package/dist/flows/flows.types.d.ts +104 -30
- package/dist/http/http.js +154 -46
- package/dist/indexing/indexing.js +14 -3
- package/dist/knowledge/document-links/document-links.d.ts +15 -0
- package/dist/knowledge/document-links/document-links.js +52 -0
- package/dist/knowledge/knowledge.js +357 -73
- package/dist/knowledge/knowledge.types.d.ts +60 -27
- package/dist/mcp/mcp.js +168 -74
- package/dist/shared/csv/csv.d.ts +13 -0
- package/dist/shared/csv/csv.js +85 -0
- package/dist/shared/gate-authorization/gate-authorization.d.ts +1 -0
- package/dist/shared/gate-authorization/gate-authorization.js +7 -0
- package/dist/tools/tools.js +14 -1
- package/migrations/0002_flows_in_the_knowledge_tree.sql +34 -0
- package/migrations/0003_folder_permissions.sql +211 -0
- package/migrations/0004_subflow_runs.sql +14 -0
- package/migrations/0005_flow_node_cleanup.sql +28 -0
- package/migrations/0005_tables_in_the_knowledge_tree.sql +39 -0
- package/migrations/0006_links_are_written_in_the_text.sql +20 -0
- package/package.json +1 -1
package/dist/adapters/db/db.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
import { nodeVerbBindings, nodeVerbQuery, subtreeBindings, subtreeCte } from "./db-grants.js";
|
|
2
|
+
const grantColumns = `id, node_id, principal_type, principal_id, verb, expires_at,
|
|
3
|
+
created_by, created_at`;
|
|
4
|
+
const linkColumns = `link.id, link.source_node_id, link.target_node_id, link.relation,
|
|
5
|
+
link.origin, link.label, link.created_by, link.created_at`;
|
|
1
6
|
const nodeColumns = `n.id, n.parent_id, n.kind, n.title, n.description, n.context_policy,
|
|
2
7
|
n.owner_id, n.current_version_id, n.created_at, n.updated_at, n.archived_at`;
|
|
3
8
|
function mapNode(row) {
|
|
@@ -45,9 +50,9 @@ function mapGrant(row) {
|
|
|
45
50
|
}
|
|
46
51
|
return {
|
|
47
52
|
id: row.id,
|
|
48
|
-
resourceId: row.
|
|
53
|
+
resourceId: row.node_id,
|
|
49
54
|
principal,
|
|
50
|
-
|
|
55
|
+
verb: row.verb,
|
|
51
56
|
expiresAt: row.expires_at,
|
|
52
57
|
createdBy: row.created_by,
|
|
53
58
|
createdAt: row.created_at,
|
|
@@ -59,6 +64,7 @@ function mapLink(row) {
|
|
|
59
64
|
sourceNodeId: row.source_node_id,
|
|
60
65
|
targetNodeId: row.target_node_id,
|
|
61
66
|
relation: row.relation,
|
|
67
|
+
origin: row.origin,
|
|
62
68
|
label: row.label,
|
|
63
69
|
createdBy: row.created_by,
|
|
64
70
|
createdAt: row.created_at,
|
|
@@ -72,40 +78,39 @@ function toFtsQuery(query) {
|
|
|
72
78
|
.join(" AND ");
|
|
73
79
|
}
|
|
74
80
|
export function createKnowledgeRepository(deps) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
FROM knowledge_nodes child
|
|
93
|
-
JOIN allowed parent ON child.parent_id = parent.id
|
|
94
|
-
)`;
|
|
81
|
+
// Reading is one verb among four; the tree walk itself lives in db-grants.ts so Flows asks the
|
|
82
|
+
// same question of the same table.
|
|
83
|
+
const visibleCte = subtreeCte;
|
|
84
|
+
const readBindings = (actor) => subtreeBindings(actor, "read", deps.now().toISOString());
|
|
85
|
+
/**
|
|
86
|
+
* One statement for the level the tree reads and for the bounded read the relation graph makes,
|
|
87
|
+
* so the visibility predicate cannot drift between them.
|
|
88
|
+
*
|
|
89
|
+
* ⚠️ In the bounded form `LIMIT` follows the `WHERE`, so the cut falls among the rows this actor
|
|
90
|
+
* may see and never before them, and `COUNT(*) OVER ()` counts those same rows alone (#30).
|
|
91
|
+
*/
|
|
92
|
+
const visibleChildren = (bounded) => `${visibleCte}
|
|
93
|
+
SELECT ${nodeColumns}${bounded ? ", COUNT(*) OVER () AS total" : ""}
|
|
94
|
+
FROM knowledge_nodes n
|
|
95
|
+
JOIN allowed ON allowed.id = n.id
|
|
96
|
+
WHERE n.parent_id IS ? AND (? = 1 OR n.archived_at IS NULL)
|
|
97
|
+
ORDER BY CASE n.kind WHEN 'folder' THEN 0 ELSE 1 END, lower(n.title), n.id${bounded ? " LIMIT ?" : ""}`;
|
|
95
98
|
return {
|
|
96
99
|
async listVisible(actor, input) {
|
|
97
|
-
const now = deps.now().toISOString();
|
|
98
100
|
const result = await deps.db
|
|
99
|
-
.prepare(
|
|
100
|
-
|
|
101
|
-
FROM knowledge_nodes n
|
|
102
|
-
JOIN allowed ON allowed.id = n.id
|
|
103
|
-
WHERE n.parent_id IS ? AND (? = 1 OR n.archived_at IS NULL)
|
|
104
|
-
ORDER BY CASE n.kind WHEN 'folder' THEN 0 ELSE 1 END, lower(n.title), n.id`)
|
|
105
|
-
.bind(actor.id, actor.id, actor.email, now, input.parentId, input.includeArchived ? 1 : 0)
|
|
101
|
+
.prepare(visibleChildren(false))
|
|
102
|
+
.bind(...readBindings(actor), input.parentId, input.includeArchived ? 1 : 0)
|
|
106
103
|
.all();
|
|
107
104
|
return (result.results ?? []).map(mapNode);
|
|
108
105
|
},
|
|
106
|
+
async listVisibleBounded(actor, input) {
|
|
107
|
+
const result = await deps.db
|
|
108
|
+
.prepare(visibleChildren(true))
|
|
109
|
+
.bind(...readBindings(actor), input.parentId, 0, input.limit)
|
|
110
|
+
.all();
|
|
111
|
+
const rows = result.results ?? [];
|
|
112
|
+
return { items: rows.map(mapNode), total: rows[0]?.total ?? 0 };
|
|
113
|
+
},
|
|
109
114
|
async getVisible(actor, nodeId) {
|
|
110
115
|
const row = await deps.db
|
|
111
116
|
.prepare(`${visibleCte}
|
|
@@ -113,65 +118,14 @@ export function createKnowledgeRepository(deps) {
|
|
|
113
118
|
FROM knowledge_nodes n
|
|
114
119
|
JOIN allowed ON allowed.id = n.id
|
|
115
120
|
WHERE n.id = ?`)
|
|
116
|
-
.bind(actor
|
|
121
|
+
.bind(...readBindings(actor), nodeId)
|
|
117
122
|
.first();
|
|
118
123
|
return row ? mapNode(row) : null;
|
|
119
124
|
},
|
|
120
|
-
async
|
|
125
|
+
async can(actor, nodeId, verb) {
|
|
121
126
|
const row = await deps.db
|
|
122
|
-
.prepare(
|
|
123
|
-
|
|
124
|
-
UNION ALL
|
|
125
|
-
SELECT parent.id, parent.parent_id, parent.owner_id
|
|
126
|
-
FROM knowledge_nodes parent
|
|
127
|
-
JOIN ancestors child ON child.parent_id = parent.id
|
|
128
|
-
)
|
|
129
|
-
SELECT 1 AS allowed
|
|
130
|
-
FROM ancestors
|
|
131
|
-
WHERE owner_id = ?
|
|
132
|
-
OR EXISTS (
|
|
133
|
-
SELECT 1 FROM resource_grants grant_row
|
|
134
|
-
WHERE grant_row.resource_type = 'knowledge'
|
|
135
|
-
AND grant_row.resource_id = ancestors.id
|
|
136
|
-
AND (
|
|
137
|
-
(grant_row.principal_type = 'user' AND grant_row.principal_id = ?)
|
|
138
|
-
OR (grant_row.principal_type = 'email' AND lower(grant_row.principal_id) = lower(?))
|
|
139
|
-
OR (grant_row.principal_type = 'organization' AND grant_row.principal_id = '*')
|
|
140
|
-
)
|
|
141
|
-
AND grant_row.role IN ('editor', 'manager')
|
|
142
|
-
AND (grant_row.expires_at IS NULL OR grant_row.expires_at > ?)
|
|
143
|
-
)
|
|
144
|
-
LIMIT 1`)
|
|
145
|
-
.bind(nodeId, actor.id, actor.id, actor.email, deps.now().toISOString())
|
|
146
|
-
.first();
|
|
147
|
-
return row?.allowed === 1;
|
|
148
|
-
},
|
|
149
|
-
async canManage(actor, nodeId) {
|
|
150
|
-
const row = await deps.db
|
|
151
|
-
.prepare(`WITH RECURSIVE ancestors(id, parent_id, owner_id) AS (
|
|
152
|
-
SELECT id, parent_id, owner_id FROM knowledge_nodes WHERE id = ?
|
|
153
|
-
UNION ALL
|
|
154
|
-
SELECT parent.id, parent.parent_id, parent.owner_id
|
|
155
|
-
FROM knowledge_nodes parent
|
|
156
|
-
JOIN ancestors child ON child.parent_id = parent.id
|
|
157
|
-
)
|
|
158
|
-
SELECT 1 AS allowed
|
|
159
|
-
FROM ancestors
|
|
160
|
-
WHERE owner_id = ?
|
|
161
|
-
OR EXISTS (
|
|
162
|
-
SELECT 1 FROM resource_grants grant_row
|
|
163
|
-
WHERE grant_row.resource_type = 'knowledge'
|
|
164
|
-
AND grant_row.resource_id = ancestors.id
|
|
165
|
-
AND (
|
|
166
|
-
(grant_row.principal_type = 'user' AND grant_row.principal_id = ?)
|
|
167
|
-
OR (grant_row.principal_type = 'email' AND lower(grant_row.principal_id) = lower(?))
|
|
168
|
-
OR (grant_row.principal_type = 'organization' AND grant_row.principal_id = '*')
|
|
169
|
-
)
|
|
170
|
-
AND grant_row.role = 'manager'
|
|
171
|
-
AND (grant_row.expires_at IS NULL OR grant_row.expires_at > ?)
|
|
172
|
-
)
|
|
173
|
-
LIMIT 1`)
|
|
174
|
-
.bind(nodeId, actor.id, actor.id, actor.email, deps.now().toISOString())
|
|
127
|
+
.prepare(nodeVerbQuery)
|
|
128
|
+
.bind(...nodeVerbBindings(nodeId, actor, verb, deps.now().toISOString()))
|
|
175
129
|
.first();
|
|
176
130
|
return row?.allowed === 1;
|
|
177
131
|
},
|
|
@@ -228,7 +182,7 @@ export function createKnowledgeRepository(deps) {
|
|
|
228
182
|
FROM knowledge_nodes n
|
|
229
183
|
JOIN allowed ON allowed.id = n.id
|
|
230
184
|
WHERE n.id = ?`)
|
|
231
|
-
.bind(
|
|
185
|
+
.bind(...readBindings({ id: input.actorId, email: "" }), replayedKey.resource_id)
|
|
232
186
|
.first();
|
|
233
187
|
if (replayed)
|
|
234
188
|
return mapNode(replayed);
|
|
@@ -245,6 +199,22 @@ export function createKnowledgeRepository(deps) {
|
|
|
245
199
|
.first();
|
|
246
200
|
return row ? mapVersion(row) : null;
|
|
247
201
|
},
|
|
202
|
+
async listVersionContentKeys(nodeId) {
|
|
203
|
+
const result = await deps.db
|
|
204
|
+
.prepare(`SELECT content_key FROM knowledge_versions
|
|
205
|
+
WHERE node_id = ? ORDER BY sequence`)
|
|
206
|
+
.bind(nodeId)
|
|
207
|
+
.all();
|
|
208
|
+
return (result.results ?? []).map((row) => row.content_key);
|
|
209
|
+
},
|
|
210
|
+
async firstVersionContentKey(nodeId) {
|
|
211
|
+
const row = await deps.db
|
|
212
|
+
.prepare(`SELECT content_key FROM knowledge_versions
|
|
213
|
+
WHERE node_id = ? ORDER BY sequence LIMIT 1`)
|
|
214
|
+
.bind(nodeId)
|
|
215
|
+
.first();
|
|
216
|
+
return row?.content_key ?? null;
|
|
217
|
+
},
|
|
248
218
|
async listVersions(nodeId) {
|
|
249
219
|
const result = await deps.db
|
|
250
220
|
.prepare(`SELECT id, node_id, sequence, content_key, media_type, content_hash,
|
|
@@ -429,122 +399,191 @@ export function createKnowledgeRepository(deps) {
|
|
|
429
399
|
.first();
|
|
430
400
|
return row?.current_version_id === version.id ? "saved" : "conflict";
|
|
431
401
|
},
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
async
|
|
444
|
-
const
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
JOIN allowed source_access ON source_access.id = link.source_node_id
|
|
450
|
-
JOIN allowed target_access ON target_access.id = link.target_node_id
|
|
451
|
-
WHERE link.source_node_id = ? OR link.target_node_id = ?
|
|
452
|
-
ORDER BY link.created_at, link.id`)
|
|
453
|
-
.bind(actor.id, actor.id, actor.email, deps.now().toISOString(), nodeId, nodeId)
|
|
454
|
-
.all();
|
|
455
|
-
return (result.results ?? []).map(mapLink);
|
|
456
|
-
},
|
|
457
|
-
async getLinkVisible(actor, linkId) {
|
|
458
|
-
const row = await deps.db
|
|
459
|
-
.prepare(`${visibleCte}
|
|
460
|
-
SELECT link.id, link.source_node_id, link.target_node_id, link.relation, link.label,
|
|
461
|
-
link.created_by, link.created_at
|
|
462
|
-
FROM knowledge_links link
|
|
463
|
-
JOIN allowed source_access ON source_access.id = link.source_node_id
|
|
464
|
-
JOIN allowed target_access ON target_access.id = link.target_node_id
|
|
465
|
-
WHERE link.id = ?`)
|
|
466
|
-
.bind(actor.id, actor.id, actor.email, deps.now().toISOString(), linkId)
|
|
467
|
-
.first();
|
|
468
|
-
return row ? mapLink(row) : null;
|
|
469
|
-
},
|
|
470
|
-
async findLinkVisible(actor, input) {
|
|
471
|
-
const row = await deps.db
|
|
472
|
-
.prepare(`${visibleCte}
|
|
473
|
-
SELECT link.id, link.source_node_id, link.target_node_id, link.relation, link.label,
|
|
474
|
-
link.created_by, link.created_at
|
|
475
|
-
FROM knowledge_links link
|
|
476
|
-
JOIN allowed source_access ON source_access.id = link.source_node_id
|
|
477
|
-
JOIN allowed target_access ON target_access.id = link.target_node_id
|
|
478
|
-
WHERE link.source_node_id = ? AND link.target_node_id = ? AND link.relation = ?`)
|
|
479
|
-
.bind(actor.id, actor.id, actor.email, deps.now().toISOString(), input.sourceNodeId, input.targetNodeId, input.relation)
|
|
402
|
+
/**
|
|
403
|
+
* One appended segment of a table (#40).
|
|
404
|
+
*
|
|
405
|
+
* ⚠️ The sequence is computed inside the INSERT, never read out and sent back in. Two appends
|
|
406
|
+
* that arrive together would otherwise both read the same `MAX(sequence)` and the second would
|
|
407
|
+
* lose to `UNIQUE (node_id, sequence)` — the very loss the ticket forbids.
|
|
408
|
+
*
|
|
409
|
+
* ⚠️ `current_version_id` only ever moves forward. It names the newest segment, and indexing
|
|
410
|
+
* keys off it; letting a slower append pull it back would leave the index describing a table
|
|
411
|
+
* that is missing its last rows.
|
|
412
|
+
*/
|
|
413
|
+
async appendTableVersion(input) {
|
|
414
|
+
const version = input.version;
|
|
415
|
+
const stored = async () => await deps.db
|
|
416
|
+
.prepare(`SELECT id, node_id, sequence, content_key, media_type, content_hash,
|
|
417
|
+
size, created_by, created_at FROM knowledge_versions WHERE id = ?`)
|
|
418
|
+
.bind(version.id)
|
|
480
419
|
.first();
|
|
481
|
-
return row ? mapLink(row) : null;
|
|
482
|
-
},
|
|
483
|
-
async insertLink(input) {
|
|
484
|
-
const link = input.link;
|
|
485
420
|
try {
|
|
486
421
|
await deps.db.batch([
|
|
487
422
|
deps.db
|
|
488
|
-
.prepare(`INSERT INTO
|
|
489
|
-
id,
|
|
490
|
-
|
|
491
|
-
|
|
423
|
+
.prepare(`INSERT INTO knowledge_versions (
|
|
424
|
+
id, node_id, sequence, content_key, media_type, content_hash, size,
|
|
425
|
+
created_by, created_at
|
|
426
|
+
) SELECT ?, ?, COALESCE(MAX(sequence), 0) + 1, ?, ?, ?, ?, ?, ?
|
|
427
|
+
FROM knowledge_versions WHERE node_id = ?`)
|
|
428
|
+
.bind(version.id, version.nodeId, version.contentKey, version.mediaType, version.contentHash, version.size, version.createdBy, version.createdAt, version.nodeId),
|
|
429
|
+
deps.db
|
|
430
|
+
.prepare(`UPDATE knowledge_nodes
|
|
431
|
+
SET current_version_id = ?, updated_at = ?
|
|
432
|
+
WHERE id = ?
|
|
433
|
+
AND EXISTS (SELECT 1 FROM knowledge_versions WHERE id = ?)
|
|
434
|
+
AND (
|
|
435
|
+
current_version_id IS NULL
|
|
436
|
+
OR COALESCE((
|
|
437
|
+
SELECT sequence FROM knowledge_versions
|
|
438
|
+
WHERE id = knowledge_nodes.current_version_id
|
|
439
|
+
), 0) < (SELECT sequence FROM knowledge_versions WHERE id = ?)
|
|
440
|
+
)`)
|
|
441
|
+
.bind(version.id, version.createdAt, version.nodeId, version.id, version.id),
|
|
492
442
|
deps.db
|
|
493
443
|
.prepare(`INSERT INTO idempotency_keys (
|
|
494
444
|
actor_id, operation, idempotency_key, resource_id, created_at
|
|
495
|
-
)
|
|
496
|
-
|
|
445
|
+
) SELECT ?, 'knowledge.append', ?, ?, ?
|
|
446
|
+
WHERE EXISTS (SELECT 1 FROM knowledge_versions WHERE id = ?)`)
|
|
447
|
+
.bind(input.actorId, input.idempotencyKey, version.id, version.createdAt, version.id),
|
|
448
|
+
deps.db
|
|
449
|
+
.prepare(`INSERT INTO knowledge_index_state (
|
|
450
|
+
version_id, status, attempt_count, last_error, updated_at
|
|
451
|
+
) SELECT ?, 'pending', 0, NULL, ?
|
|
452
|
+
WHERE EXISTS (SELECT 1 FROM knowledge_versions WHERE id = ?)`)
|
|
453
|
+
.bind(version.id, version.createdAt, version.id),
|
|
497
454
|
deps.db
|
|
498
455
|
.prepare(`INSERT INTO audit_events (
|
|
499
456
|
id, actor_id, action, resource_type, resource_id, metadata_json, occurred_at
|
|
500
|
-
)
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
}), link.createdAt),
|
|
457
|
+
) SELECT ?, ?, 'knowledge.append', 'knowledge', ?, json_object(
|
|
458
|
+
'versionId', ?, 'size', ?
|
|
459
|
+
), ?
|
|
460
|
+
WHERE EXISTS (SELECT 1 FROM knowledge_versions WHERE id = ?)`)
|
|
461
|
+
.bind(input.auditId, input.actorId, version.nodeId, version.id, version.size, version.createdAt, version.id),
|
|
506
462
|
]);
|
|
507
|
-
return link;
|
|
508
463
|
}
|
|
509
464
|
catch (error) {
|
|
465
|
+
// The same key twice at the same moment: the first write owns the row, and this call
|
|
466
|
+
// answers with it rather than with a failure the caller cannot act on.
|
|
510
467
|
const replayed = await deps.db
|
|
511
468
|
.prepare(`SELECT resource_id FROM idempotency_keys
|
|
512
|
-
WHERE actor_id = ? AND operation = 'knowledge.
|
|
469
|
+
WHERE actor_id = ? AND operation = 'knowledge.append' AND idempotency_key = ?`)
|
|
513
470
|
.bind(input.actorId, input.idempotencyKey)
|
|
514
471
|
.first();
|
|
515
|
-
const row =
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
472
|
+
const row = replayed
|
|
473
|
+
? await deps.db
|
|
474
|
+
.prepare(`SELECT id, node_id, sequence, content_key, media_type, content_hash,
|
|
475
|
+
size, created_by, created_at FROM knowledge_versions WHERE id = ?`)
|
|
476
|
+
.bind(replayed.resource_id)
|
|
477
|
+
.first()
|
|
478
|
+
: null;
|
|
521
479
|
if (row)
|
|
522
|
-
return
|
|
480
|
+
return mapVersion(row);
|
|
523
481
|
throw error;
|
|
524
482
|
}
|
|
483
|
+
const row = await stored();
|
|
484
|
+
if (!row)
|
|
485
|
+
throw new Error("Knowledge table append did not store a version");
|
|
486
|
+
return mapVersion(row);
|
|
525
487
|
},
|
|
526
|
-
async
|
|
527
|
-
const
|
|
528
|
-
.prepare(
|
|
529
|
-
|
|
488
|
+
async listGrants(resourceId) {
|
|
489
|
+
const result = await deps.db
|
|
490
|
+
.prepare(`SELECT ${grantColumns} FROM tree_grants
|
|
491
|
+
WHERE node_id = ?
|
|
492
|
+
ORDER BY principal_type, principal_id, verb`)
|
|
493
|
+
.bind(resourceId)
|
|
494
|
+
.all();
|
|
495
|
+
return (result.results ?? []).map(mapGrant);
|
|
496
|
+
},
|
|
497
|
+
// ⚠️ `UNION`, never `UNION ALL`: a ring in `parent_id` must end the recursion rather than the
|
|
498
|
+
// database, the same reason every other walk of this tree gives.
|
|
499
|
+
async organizationExecuteReaches(nodeId, exceptGrantId) {
|
|
500
|
+
const row = await deps.db
|
|
501
|
+
.prepare(`WITH RECURSIVE ancestors(id, parent_id) AS (
|
|
502
|
+
SELECT id, parent_id FROM knowledge_nodes WHERE id = ?
|
|
503
|
+
UNION
|
|
504
|
+
SELECT parent.id, parent.parent_id
|
|
505
|
+
FROM knowledge_nodes parent
|
|
506
|
+
JOIN ancestors child ON child.parent_id = parent.id
|
|
507
|
+
)
|
|
508
|
+
SELECT 1 AS reaches
|
|
509
|
+
FROM tree_grants grant_row
|
|
510
|
+
JOIN ancestors ON ancestors.id = grant_row.node_id
|
|
511
|
+
WHERE grant_row.id <> ?
|
|
512
|
+
AND grant_row.principal_type = 'organization'
|
|
513
|
+
AND grant_row.verb = 'execute'
|
|
514
|
+
AND (grant_row.expires_at IS NULL OR grant_row.expires_at > ?)
|
|
515
|
+
LIMIT 1`)
|
|
516
|
+
.bind(nodeId, exceptGrantId, deps.now().toISOString())
|
|
530
517
|
.first();
|
|
531
|
-
|
|
518
|
+
return row?.reaches === 1;
|
|
519
|
+
},
|
|
520
|
+
async listLinksVisible(actor, nodeId) {
|
|
521
|
+
const result = await deps.db
|
|
522
|
+
.prepare(`${visibleCte}
|
|
523
|
+
SELECT ${linkColumns}
|
|
524
|
+
FROM knowledge_links link
|
|
525
|
+
JOIN allowed source_access ON source_access.id = link.source_node_id
|
|
526
|
+
JOIN allowed target_access ON target_access.id = link.target_node_id
|
|
527
|
+
WHERE link.source_node_id = ? OR link.target_node_id = ?
|
|
528
|
+
ORDER BY link.created_at, link.id`)
|
|
529
|
+
.bind(...readBindings(actor), nodeId, nodeId)
|
|
530
|
+
.all();
|
|
531
|
+
return (result.results ?? []).map(mapLink);
|
|
532
|
+
},
|
|
533
|
+
/**
|
|
534
|
+
* The titles of the nodes among these that this actor may see (#41).
|
|
535
|
+
*
|
|
536
|
+
* ⚠️ The same `allowed` walk as every other read, joined rather than filtered afterwards, and
|
|
537
|
+
* archived nodes are out. An unreachable or deleted target contributes no row at all — no
|
|
538
|
+
* placeholder, no id echoed back — because a row is already the statement "something is here".
|
|
539
|
+
*/
|
|
540
|
+
async resolveVisibleTitles(actor, nodeIds) {
|
|
541
|
+
const uniqueIds = [...new Set(nodeIds)].slice(0, 200);
|
|
542
|
+
if (uniqueIds.length === 0)
|
|
543
|
+
return [];
|
|
544
|
+
const rows = [];
|
|
545
|
+
for (let offset = 0; offset < uniqueIds.length; offset += 96) {
|
|
546
|
+
const ids = uniqueIds.slice(offset, offset + 96);
|
|
547
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
548
|
+
const result = await deps.db
|
|
549
|
+
.prepare(`${visibleCte}
|
|
550
|
+
SELECT n.id, n.title
|
|
551
|
+
FROM knowledge_nodes n
|
|
552
|
+
JOIN allowed ON allowed.id = n.id
|
|
553
|
+
WHERE n.id IN (${placeholders}) AND n.archived_at IS NULL`)
|
|
554
|
+
.bind(...readBindings(actor), ...ids)
|
|
555
|
+
.all();
|
|
556
|
+
rows.push(...(result.results ?? []));
|
|
557
|
+
}
|
|
558
|
+
return rows.map((row) => ({ nodeId: row.id, title: row.title }));
|
|
559
|
+
},
|
|
560
|
+
/**
|
|
561
|
+
* The `text` links of one document, replaced wholesale by what the saved text says (#41).
|
|
562
|
+
*
|
|
563
|
+
* ⚠️ `origin = 'text'` in the delete, and nowhere else. Links made in the removed dialog are
|
|
564
|
+
* `manual`; saving a document must not take away a relationship somebody entered before the
|
|
565
|
+
* text became the place to write one.
|
|
566
|
+
*
|
|
567
|
+
* The whole set is written in one batch, so a save never leaves the graph half-rewritten. There
|
|
568
|
+
* is no idempotency row: the operation is a replacement, and replaying it writes the same set.
|
|
569
|
+
*/
|
|
570
|
+
async replaceTextLinks(input) {
|
|
532
571
|
await deps.db.batch([
|
|
533
572
|
deps.db
|
|
534
|
-
.prepare("DELETE FROM knowledge_links WHERE
|
|
535
|
-
.bind(input.
|
|
536
|
-
deps.db
|
|
537
|
-
.prepare(`INSERT INTO
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
573
|
+
.prepare("DELETE FROM knowledge_links WHERE source_node_id = ? AND origin = 'text'")
|
|
574
|
+
.bind(input.sourceNodeId),
|
|
575
|
+
...input.links.map((link) => deps.db
|
|
576
|
+
.prepare(`INSERT INTO knowledge_links (
|
|
577
|
+
id, source_node_id, target_node_id, relation, origin, label, created_by, created_at
|
|
578
|
+
) VALUES (?, ?, ?, 'references', 'text', NULL, ?, ?)
|
|
579
|
+
ON CONFLICT (source_node_id, target_node_id, relation) DO NOTHING`)
|
|
580
|
+
.bind(link.id, input.sourceNodeId, link.targetNodeId, input.actorId, input.occurredAt)),
|
|
541
581
|
deps.db
|
|
542
582
|
.prepare(`INSERT INTO audit_events (
|
|
543
583
|
id, actor_id, action, resource_type, resource_id, metadata_json, occurred_at
|
|
544
|
-
) VALUES (?, ?, 'knowledge.
|
|
545
|
-
.bind(input.auditId, input.actorId, input.sourceNodeId, JSON.stringify({
|
|
584
|
+
) VALUES (?, ?, 'knowledge.link', 'knowledge', ?, ?, ?)`)
|
|
585
|
+
.bind(input.auditId, input.actorId, input.sourceNodeId, JSON.stringify({ targetNodeIds: input.links.map((link) => link.targetNodeId) }), input.occurredAt),
|
|
546
586
|
]);
|
|
547
|
-
return deleted;
|
|
548
587
|
},
|
|
549
588
|
async graphVisible(actor, input) {
|
|
550
589
|
const nodeResult = await deps.db
|
|
@@ -555,7 +594,7 @@ export function createKnowledgeRepository(deps) {
|
|
|
555
594
|
WHERE n.archived_at IS NULL
|
|
556
595
|
ORDER BY lower(n.title), n.id
|
|
557
596
|
LIMIT ?`)
|
|
558
|
-
.bind(actor
|
|
597
|
+
.bind(...readBindings(actor), input.limit)
|
|
559
598
|
.all();
|
|
560
599
|
const nodes = (nodeResult.results ?? []).map(mapNode);
|
|
561
600
|
if (nodes.length === 0)
|
|
@@ -568,13 +607,12 @@ export function createKnowledgeRepository(deps) {
|
|
|
568
607
|
ORDER BY lower(n.title), n.id
|
|
569
608
|
LIMIT ?
|
|
570
609
|
)
|
|
571
|
-
SELECT
|
|
572
|
-
link.created_by, link.created_at
|
|
610
|
+
SELECT ${linkColumns}
|
|
573
611
|
FROM knowledge_links link
|
|
574
612
|
JOIN graph_nodes source_node ON source_node.id = link.source_node_id
|
|
575
613
|
JOIN graph_nodes target_node ON target_node.id = link.target_node_id
|
|
576
614
|
ORDER BY link.created_at, link.id`)
|
|
577
|
-
.bind(actor
|
|
615
|
+
.bind(...readBindings(actor), input.limit)
|
|
578
616
|
.all();
|
|
579
617
|
return { nodes, links: (linkResult.results ?? []).map(mapLink) };
|
|
580
618
|
},
|
|
@@ -589,32 +627,31 @@ export function createKnowledgeRepository(deps) {
|
|
|
589
627
|
try {
|
|
590
628
|
await deps.db.batch([
|
|
591
629
|
deps.db
|
|
592
|
-
|
|
593
|
-
|
|
630
|
+
// The verb is part of the key, so re-granting the same verb only refreshes its expiry
|
|
631
|
+
// and never turns one verb into another.
|
|
632
|
+
.prepare(`INSERT INTO tree_grants (
|
|
633
|
+
id, node_id, principal_type, principal_id, verb,
|
|
594
634
|
expires_at, created_by, created_at
|
|
595
|
-
) VALUES (?,
|
|
596
|
-
ON CONFLICT (
|
|
597
|
-
DO UPDATE SET
|
|
598
|
-
.bind(grant.id, grant.resourceId, principalType, principalId, grant.
|
|
635
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
636
|
+
ON CONFLICT (node_id, principal_type, principal_id, verb)
|
|
637
|
+
DO UPDATE SET expires_at = excluded.expires_at`)
|
|
638
|
+
.bind(grant.id, grant.resourceId, principalType, principalId, grant.verb, grant.expiresAt, grant.createdBy, grant.createdAt),
|
|
599
639
|
deps.db
|
|
600
640
|
.prepare(`INSERT INTO idempotency_keys (
|
|
601
641
|
actor_id, operation, idempotency_key, resource_id, created_at
|
|
602
|
-
) SELECT ?, 'knowledge.share', ?, id, ? FROM
|
|
603
|
-
WHERE
|
|
604
|
-
|
|
605
|
-
.bind(input.actorId, input.idempotencyKey, grant.createdAt, grant.resourceId, principalType, principalId),
|
|
642
|
+
) SELECT ?, 'knowledge.share', ?, id, ? FROM tree_grants
|
|
643
|
+
WHERE node_id = ? AND principal_type = ? AND principal_id = ? AND verb = ?`)
|
|
644
|
+
.bind(input.actorId, input.idempotencyKey, grant.createdAt, grant.resourceId, principalType, principalId, grant.verb),
|
|
606
645
|
deps.db
|
|
607
646
|
.prepare(`INSERT INTO audit_events (
|
|
608
647
|
id, actor_id, action, resource_type, resource_id, metadata_json, occurred_at
|
|
609
648
|
) VALUES (?, ?, 'knowledge.share', 'knowledge', ?, ?, ?)`)
|
|
610
|
-
.bind(input.auditId, input.actorId, grant.resourceId, JSON.stringify({ principalType,
|
|
649
|
+
.bind(input.auditId, input.actorId, grant.resourceId, JSON.stringify({ principalType, verb: grant.verb }), grant.createdAt),
|
|
611
650
|
]);
|
|
612
651
|
const stored = await deps.db
|
|
613
|
-
.prepare(`SELECT
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
AND principal_type = ? AND principal_id = ?`)
|
|
617
|
-
.bind(grant.resourceId, principalType, principalId)
|
|
652
|
+
.prepare(`SELECT ${grantColumns} FROM tree_grants
|
|
653
|
+
WHERE node_id = ? AND principal_type = ? AND principal_id = ? AND verb = ?`)
|
|
654
|
+
.bind(grant.resourceId, principalType, principalId, grant.verb)
|
|
618
655
|
.first();
|
|
619
656
|
if (!stored)
|
|
620
657
|
throw new Error("Knowledge grant disappeared after upsert");
|
|
@@ -628,8 +665,7 @@ export function createKnowledgeRepository(deps) {
|
|
|
628
665
|
.first();
|
|
629
666
|
if (replayed) {
|
|
630
667
|
const row = await deps.db
|
|
631
|
-
.prepare(`SELECT
|
|
632
|
-
created_by, created_at FROM resource_grants WHERE id = ?`)
|
|
668
|
+
.prepare(`SELECT ${grantColumns} FROM tree_grants WHERE id = ?`)
|
|
633
669
|
.bind(replayed.resource_id)
|
|
634
670
|
.first();
|
|
635
671
|
if (row)
|
|
@@ -646,13 +682,11 @@ export function createKnowledgeRepository(deps) {
|
|
|
646
682
|
actor_id, operation, idempotency_key, resource_id, created_at
|
|
647
683
|
) SELECT ?, 'knowledge.revoke', ?,
|
|
648
684
|
(CASE WHEN EXISTS (
|
|
649
|
-
SELECT 1 FROM
|
|
650
|
-
WHERE id = ? AND resource_type = 'knowledge' AND resource_id = ?
|
|
685
|
+
SELECT 1 FROM tree_grants WHERE id = ? AND node_id = ?
|
|
651
686
|
) THEN '1:' ELSE '0:' END) || ?, ?`)
|
|
652
687
|
.bind(input.actorId, input.idempotencyKey, input.grantId, input.resourceId, input.grantId, input.occurredAt),
|
|
653
688
|
deps.db
|
|
654
|
-
.prepare(
|
|
655
|
-
WHERE id = ? AND resource_type = 'knowledge' AND resource_id = ?`)
|
|
689
|
+
.prepare("DELETE FROM tree_grants WHERE id = ? AND node_id = ?")
|
|
656
690
|
.bind(input.grantId, input.resourceId),
|
|
657
691
|
deps.db
|
|
658
692
|
.prepare(`INSERT INTO audit_events (
|
|
@@ -693,7 +727,7 @@ export function createKnowledgeRepository(deps) {
|
|
|
693
727
|
AND n.archived_at IS NULL
|
|
694
728
|
ORDER BY rank, n.updated_at DESC
|
|
695
729
|
LIMIT ?`)
|
|
696
|
-
.bind(actor
|
|
730
|
+
.bind(...readBindings(actor), query, input.limit)
|
|
697
731
|
.all();
|
|
698
732
|
return (result.results ?? []).map((row) => ({
|
|
699
733
|
nodeId: row.node_id,
|
|
@@ -724,7 +758,7 @@ export function createKnowledgeRepository(deps) {
|
|
|
724
758
|
JOIN knowledge_fts ON knowledge_fts.version_id = v.id
|
|
725
759
|
JOIN allowed ON allowed.id = n.id
|
|
726
760
|
WHERE n.id IN (${placeholders}) AND n.archived_at IS NULL`)
|
|
727
|
-
.bind(actor
|
|
761
|
+
.bind(...readBindings(actor), ...ids)
|
|
728
762
|
.all();
|
|
729
763
|
rows.push(...(result.results ?? []));
|
|
730
764
|
}
|
package/dist/flows/flows.d.ts
CHANGED
|
@@ -1,4 +1,34 @@
|
|
|
1
|
-
import type { FlowGraph } from "@anchrd/intel-contract";
|
|
1
|
+
import type { FlowGraph, FlowNode } from "@anchrd/intel-contract";
|
|
2
2
|
import type { CompiledFlow, FlowDeps, FlowService } from "./flows.types.js";
|
|
3
|
+
type SubflowNode = Extract<FlowNode, {
|
|
4
|
+
kind: "subflow";
|
|
5
|
+
}>;
|
|
6
|
+
type KnowledgeStepNode = Extract<FlowNode, {
|
|
7
|
+
kind: "knowledge";
|
|
8
|
+
}>;
|
|
9
|
+
type ToolStepNode = Extract<FlowNode, {
|
|
10
|
+
kind: "tool";
|
|
11
|
+
}>;
|
|
12
|
+
/**
|
|
13
|
+
* One place reads a graph for each kind of step it contains, and everything else is derived from
|
|
14
|
+
* these three. The publish-time rule, the freeze, the sidebar, the relation graph, the requirements
|
|
15
|
+
* list and the repository's cycle walk all ask about the same nodes; a second `kind === "…"` walk
|
|
16
|
+
* beside them would drift quietly, because both would keep returning something plausible.
|
|
17
|
+
*/
|
|
18
|
+
export declare function subflowNodes(graph: FlowGraph): SubflowNode[];
|
|
19
|
+
export declare function knowledgeNodes(graph: FlowGraph): KnowledgeStepNode[];
|
|
20
|
+
export declare function toolNodes(graph: FlowGraph): ToolStepNode[];
|
|
21
|
+
export declare function calleeIds(graph: FlowGraph): string[];
|
|
22
|
+
/**
|
|
23
|
+
* The Knowledge documents a graph names and the tools it calls, flattened and without repetition.
|
|
24
|
+
* The requirements list, the publish-time check and the run's first tool check read this one answer,
|
|
25
|
+
* so they cannot disagree about what a flow touches. A caller that needs to know *which step* names
|
|
26
|
+
* a document reads the node lists above instead — the relation graph draws exactly that edge.
|
|
27
|
+
*/
|
|
28
|
+
export declare function graphReferences(graph: FlowGraph): {
|
|
29
|
+
knowledge: string[];
|
|
30
|
+
tools: string[];
|
|
31
|
+
};
|
|
3
32
|
export declare function compileFlow(graph: FlowGraph): CompiledFlow;
|
|
4
33
|
export declare function createFlows(deps: FlowDeps): FlowService;
|
|
34
|
+
export {};
|