@anteros/core 0.0.41 → 0.0.44
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/database/audit.ts +9 -59
- package/database/collection.ts +3 -1
- package/database/deleteLog.ts +67 -3
- package/database/file.ts +4 -3
- package/database/mongodbadapter.ts +103 -42
- package/database/replication.ts +724 -58
- package/database/ttl.ts +68 -0
- package/database/workflow.ts +466 -240
- package/index.ts +6 -0
- package/lib/agent.ts +1567 -0
- package/lib/agentMemory.ts +689 -0
- package/lib/agents.ts +180 -0
- package/lib/ai.ts +0 -0
- package/lib/api.ts +246 -0
- package/lib/attachment.ts +351 -0
- package/lib/auditResult.ts +13 -0
- package/lib/define.ts +43 -1
- package/lib/endpoints.ts +80 -0
- package/lib/files.ts +2 -1
- package/lib/jsonSchema.ts +183 -0
- package/lib/lifecycle.ts +7 -1
- package/lib/load.ts +17 -0
- package/lib/mcp.ts +3 -2
- package/lib/mcpClient.ts +128 -0
- package/lib/messages.ts +299 -0
- package/lib/providers.ts +970 -0
- package/lib/routes.ts +14 -1
- package/lib/scriptWorker.ts +4 -1
- package/lib/scripts.ts +6 -1
- package/lib/skills.ts +481 -0
- package/lib/sockets.ts +3 -1
- package/lib/workflow.ts +209 -37
- package/lib/zip.ts +109 -0
- package/package.json +1 -1
- package/server/access.ts +96 -0
- package/server/agents.ts +472 -0
- package/server/api.ts +80 -65
- package/server/boot.ts +11 -0
- package/server/hono.ts +8 -3
- package/server/mcp.ts +17 -63
- package/server/routes.ts +19 -24
- package/tests/agent-mcp.test.ts +116 -0
- package/tests/agent-memory.test.ts +617 -0
- package/tests/agent-provider.test.ts +288 -0
- package/tests/agent-runtime.test.ts +369 -0
- package/tests/agent-skills.test.ts +177 -0
- package/tests/agent-summarize.test.ts +124 -0
- package/tests/agent-working-memory.test.ts +404 -0
- package/tests/agent.test.ts +1241 -0
- package/tests/agents-api.test.ts +593 -0
- package/tests/api-private-fields.test.ts +196 -0
- package/tests/api.test.ts +51 -0
- package/tests/attachment.test.ts +241 -0
- package/tests/audit-operations.test.ts +1 -1
- package/tests/audit-redact.test.ts +29 -3
- package/tests/audit-run-action.test.ts +1 -1
- package/tests/endpoints.test.ts +80 -0
- package/tests/file-collection-replication.test.ts +2 -0
- package/tests/fixtures/agent-tenant/agents/support.agent.ts +9 -0
- package/tests/fixtures/agent-tenant/agents/weather.agent.ts +21 -0
- package/tests/fixtures/agents-api/agents/assistant.agent.ts +46 -0
- package/tests/fixtures/agents-api/agents/plain.agent.ts +23 -0
- package/tests/fixtures/agents-api/agents/private.agent.ts +15 -0
- package/tests/fixtures/agents-api/collections/notes.model.ts +26 -0
- package/tests/fixtures/agents-api/routes/probe.route.ts +21 -0
- package/tests/fixtures/agents-api/services/probe.service.ts +19 -0
- package/tests/fixtures/attachment/data.csv +2 -0
- package/tests/fixtures/fake-provider.ts +128 -0
- package/tests/fixtures/repl-only/app.ts +3 -0
- package/tests/fixtures/replication-meta/agents/chat.agent.ts +16 -0
- package/tests/fixtures/replication-meta/collections/counters.model.ts +13 -0
- package/tests/fixtures/replication-meta/collections/orders.model.ts +9 -0
- package/tests/fixtures/replication-meta/workflows/run.workflow.ts +8 -0
- package/tests/fixtures/skills-tenant/agents/docs.agent.ts +23 -0
- package/tests/fixtures/skills-tenant/agents/incomplete.agent.ts +14 -0
- package/tests/fixtures/skills-tenant/broken/bad-name.md +6 -0
- package/tests/fixtures/skills-tenant/broken/dup-a.md +6 -0
- package/tests/fixtures/skills-tenant/broken/dup-b.md +6 -0
- package/tests/fixtures/skills-tenant/broken/empty-body.md +4 -0
- package/tests/fixtures/skills-tenant/broken/long-compat.md +7 -0
- package/tests/fixtures/skills-tenant/broken/long-description.md +6 -0
- package/tests/fixtures/skills-tenant/broken/mismatch/SKILL.md +6 -0
- package/tests/fixtures/skills-tenant/broken/no-description.md +5 -0
- package/tests/fixtures/skills-tenant/broken/no-name.md +5 -0
- package/tests/fixtures/skills-tenant/broken/plain.md +4 -0
- package/tests/fixtures/skills-tenant/broken/unknown-key.md +9 -0
- package/tests/fixtures/skills-tenant/docs/pdf-forms.md +18 -0
- package/tests/fixtures/skills-tenant/guide/ml.md +9 -0
- package/tests/fixtures/skills-tenant/guides/pdf/SKILL.md +9 -0
- package/tests/fixtures/skills-tenant/runbooks/incident.md +9 -0
- package/tests/fixtures/skills-tenant/runbooks/notes.txt +2 -0
- package/tests/fixtures/vars-tenant/vars/private.var.ts +4 -1
- package/tests/fixtures/workflow-engine/workflows/failing.workflow.ts +34 -0
- package/tests/fixtures/workflow-engine/workflows/flaky.workflow.ts +23 -0
- package/tests/fixtures/workflow-engine/workflows/hang.workflow.ts +17 -0
- package/tests/fixtures/workflow-engine/workflows/simple.workflow.ts +19 -0
- package/tests/fixtures/workflow-engine/workflows/slow.workflow.ts +23 -0
- package/tests/fixtures/workflow-engine/workflows/versioned.workflow.ts +19 -0
- package/tests/fixtures/workflow-tenant/workflows/broken.workflow.ts +3 -0
- package/tests/fixtures/workflow-tenant/workflows/disabled.workflow.ts +8 -0
- package/tests/fixtures/workflow-tenant/workflows/invoice.workflow.ts +21 -0
- package/tests/fixtures/workflow-tenant/workflows/no-id.workflow.ts +7 -0
- package/tests/fixtures/workflow-tenant/workflows/order-duplicate.workflow.ts +13 -0
- package/tests/fixtures/workflow-tenant/workflows/order.workflow.ts +17 -0
- package/tests/hono.test.ts +6 -3
- package/tests/locks.test.ts +120 -0
- package/tests/mcp.test.ts +11 -11
- package/tests/messages.test.ts +258 -0
- package/tests/replication-change-stream.test.ts +240 -0
- package/tests/replication-control.test.ts +4 -2
- package/tests/replication-initial-sync.test.ts +2 -0
- package/tests/replication-meta.test.ts +246 -0
- package/tests/replication.test.ts +2 -0
- package/tests/upload.test.ts +44 -12
- package/tests/vars-api.test.ts +1 -1
- package/tests/vars-replication.test.ts +5 -2
- package/tests/workflow-engine.test.ts +340 -0
- package/tests/workflow-retention.test.ts +103 -0
- package/tests/workflow.test.ts +186 -0
- package/types/agent.d.ts +1067 -0
- package/types/config.d.ts +16 -0
- package/types/file.d.ts +7 -7
- package/types/hook.d.ts +6 -0
- package/types/lifecycle.d.ts +6 -0
- package/types/mcp.d.ts +11 -1
- package/types/replication.d.ts +70 -4
- package/types/route.d.ts +7 -1
- package/types/scripts.d.ts +6 -0
- package/types/service.d.ts +7 -1
- package/types/tenant.d.ts +13 -0
- package/types/vars.d.ts +2 -2
- package/types/websocket.d.ts +6 -0
- package/types/workflow.d.ts +50 -3
package/database/audit.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Db } from "mongodb";
|
|
2
2
|
import { cfg } from "../server/config";
|
|
3
3
|
import type { Tenant } from "../types/tenant";
|
|
4
|
-
import {
|
|
4
|
+
import { syncTtlRetention, type RetentionResult } from "./ttl";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Collection holding the audit trail — one document per database operation
|
|
@@ -35,8 +35,6 @@ const AUDIT_INDEXES: { name: string; key: Record<string, 1 | -1> }[] = [
|
|
|
35
35
|
{ name: '_audit_trace_', key: { 'trace.id': 1 } },
|
|
36
36
|
]
|
|
37
37
|
|
|
38
|
-
type RetentionResult = 'applied' | 'dropped' | 'unchanged' | 'skipped'
|
|
39
|
-
|
|
40
38
|
/**
|
|
41
39
|
* One-off migration: `_activities_` → `_audit_`.
|
|
42
40
|
*
|
|
@@ -90,67 +88,19 @@ async function ensureAuditIndexes(db: Db): Promise<string[]> {
|
|
|
90
88
|
}
|
|
91
89
|
|
|
92
90
|
/**
|
|
93
|
-
* Declarative retention (MongoDB TTL on `ts`):
|
|
91
|
+
* Declarative retention (MongoDB TTL on `ts`) — shared TTL helper:
|
|
94
92
|
* - duration string (`'90d'`, `'24h'`) → ensure the TTL index with that expiry
|
|
95
93
|
* - `false` → explicitly disabled: drop the TTL index
|
|
96
94
|
* - omitted → untouched (entries are kept forever)
|
|
97
95
|
*/
|
|
98
96
|
async function syncAuditRetention(db: Db, retention?: string | false): Promise<RetentionResult> {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (retention === false) {
|
|
108
|
-
if (!existing) return 'unchanged'
|
|
109
|
-
await col.dropIndex(AUDIT_TTL_INDEX)
|
|
110
|
-
console.log(`🧹 Audit retention disabled — ${AUDIT_TTL_INDEX} dropped`)
|
|
111
|
-
return 'dropped'
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const parsed = parseDuration(retention)
|
|
115
|
-
if (parsed === null) {
|
|
116
|
-
console.error(`Invalid audit retention '${retention}' — expected a duration like '90d' or '24h'`)
|
|
117
|
-
return 'skipped'
|
|
118
|
-
}
|
|
119
|
-
const expireAfterSeconds = Math.round(parsed / 1000)
|
|
120
|
-
|
|
121
|
-
if (!existing) {
|
|
122
|
-
await col.createIndex({ ts: 1 }, {
|
|
123
|
-
name: AUDIT_TTL_INDEX,
|
|
124
|
-
expireAfterSeconds,
|
|
125
|
-
background: true,
|
|
126
|
-
})
|
|
127
|
-
console.log(`🧹 Audit retention enabled — entries expire after ${retention}`)
|
|
128
|
-
return 'applied'
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (existing.expireAfterSeconds === expireAfterSeconds) return 'unchanged'
|
|
132
|
-
|
|
133
|
-
// `expireAfterSeconds` cannot be updated through createIndex: collMod does it
|
|
134
|
-
// instantly, dropping/recreating the index is only a fallback for older servers
|
|
135
|
-
try {
|
|
136
|
-
await db.command({
|
|
137
|
-
collMod: AUDIT_COLLECTION,
|
|
138
|
-
index: { name: AUDIT_TTL_INDEX, expireAfterSeconds },
|
|
139
|
-
})
|
|
140
|
-
} catch {
|
|
141
|
-
await col.dropIndex(AUDIT_TTL_INDEX)
|
|
142
|
-
await col.createIndex({ ts: 1 }, {
|
|
143
|
-
name: AUDIT_TTL_INDEX,
|
|
144
|
-
expireAfterSeconds,
|
|
145
|
-
background: true,
|
|
146
|
-
})
|
|
147
|
-
}
|
|
148
|
-
console.log(`🧹 Audit retention updated — entries expire after ${retention}`)
|
|
149
|
-
return 'applied'
|
|
150
|
-
} catch (err: any) {
|
|
151
|
-
console.error(`Audit retention failed (${retention})`, err?.message)
|
|
152
|
-
return 'skipped'
|
|
153
|
-
}
|
|
97
|
+
return syncTtlRetention(db, {
|
|
98
|
+
collection: AUDIT_COLLECTION,
|
|
99
|
+
index: AUDIT_TTL_INDEX,
|
|
100
|
+
field: 'ts',
|
|
101
|
+
retention,
|
|
102
|
+
label: 'Audit',
|
|
103
|
+
})
|
|
154
104
|
}
|
|
155
105
|
|
|
156
106
|
/**
|
package/database/collection.ts
CHANGED
|
@@ -5,6 +5,7 @@ import fs from "fs/promises"
|
|
|
5
5
|
import type { Collection } from "../types/collection"
|
|
6
6
|
import type { IndexDescription } from "mongodb"
|
|
7
7
|
import type { Field } from "../types/field"
|
|
8
|
+
import { importDefinition } from "../lib/load"
|
|
8
9
|
import { getTenant } from "./tenant"
|
|
9
10
|
import { buildSchema } from "./schema"
|
|
10
11
|
import type { FileCollection } from "../types/file"
|
|
@@ -38,7 +39,8 @@ async function syncCollections() {
|
|
|
38
39
|
if (isDirectory) {
|
|
39
40
|
const glob = new Glob(path.join(COLLECTIONS_PATH, '**/*.model.ts'))
|
|
40
41
|
for await (let file of glob.scan('.')) {
|
|
41
|
-
|
|
42
|
+
// A broken model file is skipped, never fatal for the others
|
|
43
|
+
let collectionModule = await importDefinition(file, 'collection')
|
|
42
44
|
if (collectionModule?.default?._isCollection_) {
|
|
43
45
|
collections.push({
|
|
44
46
|
...collectionModule?.default,
|
package/database/deleteLog.ts
CHANGED
|
@@ -1,14 +1,78 @@
|
|
|
1
1
|
import type { Db } from "mongodb";
|
|
2
|
+
import { cfg } from "../server/config";
|
|
3
|
+
import type { ReplicationMetaName } from "../types/replication";
|
|
2
4
|
|
|
3
5
|
/** Collection holding the replication state (`_replication_`). */
|
|
4
6
|
const MARKERS = "_replication_";
|
|
5
7
|
|
|
8
|
+
/** Meta collection slug → the name `replication.exclude` knows it by. */
|
|
9
|
+
const META_BY_SLUG: Record<string, ReplicationMetaName> = {
|
|
10
|
+
[MARKERS]: "replication",
|
|
11
|
+
_audit_: "audit",
|
|
12
|
+
_workflows_: "workflows",
|
|
13
|
+
_locks_: "locks",
|
|
14
|
+
_vars_: "vars",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** Replication is armed: enabled, with at least one usable destination. */
|
|
18
|
+
function replicationReady(tenantId: string): boolean {
|
|
19
|
+
const tenant: any = (cfg.tenants ?? []).find((candidate) => candidate.id === tenantId);
|
|
20
|
+
const replication = tenant?.replication;
|
|
21
|
+
|
|
22
|
+
if (!replication || replication.enabled === false) return false;
|
|
23
|
+
return (replication.destinations ?? []).some((destination: any) =>
|
|
24
|
+
destination?.enabled !== false && destination?.id && destination?.uri);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** `replication.exclude` names this collection. */
|
|
28
|
+
function isExcluded(tenantId: string, name: string): boolean {
|
|
29
|
+
const tenant: any = (cfg.tenants ?? []).find((candidate) => candidate.id === tenantId);
|
|
30
|
+
return (tenant?.replication?.exclude ?? []).includes(name);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Whether the framework replicates a **meta** collection (`_audit_`, `_vars_`…)
|
|
35
|
+
* for this tenant.
|
|
36
|
+
*/
|
|
37
|
+
function replicatesMetaCollection(tenantId: string, name: ReplicationMetaName): boolean {
|
|
38
|
+
return replicationReady(tenantId) && !isExcluded(tenantId, name);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The same question, by **collection slug** — used by the stores that delete on
|
|
43
|
+
* their own instead of going through the collection hooks (agent memory, and the
|
|
44
|
+
* variables / file paths mirror this).
|
|
45
|
+
*
|
|
46
|
+
* It answers exactly what `replicatedCollectionsFor()` would: a meta collection,
|
|
47
|
+
* an agent memory collection (`exclude: ['memory']` covers them all — the tenant
|
|
48
|
+
* names them), or a declared collection that opted in. A collection nobody
|
|
49
|
+
* replicates gets no tombstone: it would never be flushed and would only pile up
|
|
50
|
+
* in `_replication_`.
|
|
51
|
+
*/
|
|
52
|
+
function replicatesCollection(tenantId: string, slug: string): boolean {
|
|
53
|
+
const asMeta = META_BY_SLUG[slug];
|
|
54
|
+
if (asMeta) return replicatesMetaCollection(tenantId, asMeta);
|
|
55
|
+
if (!replicationReady(tenantId)) return false;
|
|
56
|
+
|
|
57
|
+
const isAgentMemory = (cfg.agentMemories ?? [])
|
|
58
|
+
.some((memory) => memory._tenant_ === tenantId && memory.collection === slug);
|
|
59
|
+
if (isAgentMemory) return !isExcluded(tenantId, "memory");
|
|
60
|
+
|
|
61
|
+
const declared = [...(cfg.collections ?? []), ...(cfg.fileCollections ?? [])]
|
|
62
|
+
.some((collection: any) =>
|
|
63
|
+
collection._tenant_ === tenantId
|
|
64
|
+
&& collection.slug === slug
|
|
65
|
+
&& collection.replication?.enabled);
|
|
66
|
+
|
|
67
|
+
return declared && !isExcluded(tenantId, slug);
|
|
68
|
+
}
|
|
69
|
+
|
|
6
70
|
/**
|
|
7
71
|
* Upsert delete tombstones consumed by the replication engine.
|
|
8
72
|
*
|
|
9
73
|
* Some collections bypass the collection hooks when deleting (variables,
|
|
10
|
-
* file-collection documents) — those paths call this directly so
|
|
11
|
-
* deletions still propagate to the destinations.
|
|
74
|
+
* agent memory, file-collection documents) — those paths call this directly so
|
|
75
|
+
* their deletions still propagate to the destinations.
|
|
12
76
|
*/
|
|
13
77
|
async function writeDeleteMarkers(
|
|
14
78
|
db: Db,
|
|
@@ -31,4 +95,4 @@ async function writeDeleteMarkers(
|
|
|
31
95
|
await db.collection(MARKERS).bulkWrite(ops as any, { ordered: false }).catch(() => { /* ignore */ });
|
|
32
96
|
}
|
|
33
97
|
|
|
34
|
-
export { writeDeleteMarkers };
|
|
98
|
+
export { writeDeleteMarkers, replicatesMetaCollection, replicatesCollection };
|
package/database/file.ts
CHANGED
|
@@ -4,6 +4,7 @@ import path from "path"
|
|
|
4
4
|
import fs from "fs/promises"
|
|
5
5
|
import type { FileCollection } from "../types/file"
|
|
6
6
|
import { getTenant } from "./tenant"
|
|
7
|
+
import { importDefinition } from "../lib/load"
|
|
7
8
|
import type { IndexDescription } from "mongodb"
|
|
8
9
|
|
|
9
10
|
async function syncFileCollections() {
|
|
@@ -18,7 +19,7 @@ async function syncFileCollections() {
|
|
|
18
19
|
if (filesExist && (await fs.stat(FILES_PATH)).isDirectory()) {
|
|
19
20
|
const globFiles = new Glob(path.join(FILES_PATH, '**/*.file.ts'))
|
|
20
21
|
for await (let file of globFiles.scan('.')) {
|
|
21
|
-
let fileModule = await
|
|
22
|
+
let fileModule = await importDefinition(file, 'file collection')
|
|
22
23
|
if (fileModule?.default?._isFileCollection_) {
|
|
23
24
|
fileCollections.push({
|
|
24
25
|
...fileModule?.default,
|
|
@@ -30,7 +31,7 @@ async function syncFileCollections() {
|
|
|
30
31
|
// Also scan *.model.ts in files/ with _isFileCollection_
|
|
31
32
|
const globModels = new Glob(path.join(FILES_PATH, '**/*.model.ts'))
|
|
32
33
|
for await (let file of globModels.scan('.')) {
|
|
33
|
-
let fileModule = await
|
|
34
|
+
let fileModule = await importDefinition(file, 'file collection')
|
|
34
35
|
if (fileModule?.default?._isFileCollection_) {
|
|
35
36
|
fileCollections.push({
|
|
36
37
|
...fileModule?.default,
|
|
@@ -46,7 +47,7 @@ async function syncFileCollections() {
|
|
|
46
47
|
if (collectionsExist && (await fs.stat(COLLECTIONS_PATH)).isDirectory()) {
|
|
47
48
|
const globFiles = new Glob(path.join(COLLECTIONS_PATH, '**/*.file.ts'))
|
|
48
49
|
for await (let file of globFiles.scan('.')) {
|
|
49
|
-
let fileModule = await
|
|
50
|
+
let fileModule = await importDefinition(file, 'file collection')
|
|
50
51
|
if (fileModule?.default?._isFileCollection_) {
|
|
51
52
|
fileCollections.push({
|
|
52
53
|
...fileModule?.default,
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
|
|
2
2
|
import type { Tenant } from "../types/tenant";
|
|
3
3
|
import type { MongoClientOptions, Db, ClientSession, UpdateOptions, UpdateFilter, Document, DeleteResult, ChangeStreamOptions, ChangeStream, ChangeStreamDocument, AnyBulkWriteOperation, BulkWriteOptions, BulkWriteResult, UpdateResult } from "mongodb";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Is a process still running **on this host**? `EPERM` means it exists but belongs
|
|
7
|
+
* to another user — which still counts as alive.
|
|
8
|
+
*/
|
|
9
|
+
function isProcessAlive(pid: number): boolean {
|
|
10
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
11
|
+
try {
|
|
12
|
+
process.kill(pid, 0);
|
|
13
|
+
return true;
|
|
14
|
+
} catch (err: any) {
|
|
15
|
+
return err?.code === 'EPERM';
|
|
16
|
+
}
|
|
17
|
+
}
|
|
4
18
|
import { MongoClient, ObjectId, AggregationCursor } from "mongodb";
|
|
5
19
|
import type { FindOptions, findOneOptions } from "../types/mongo";
|
|
6
20
|
import type { FindCallOptions } from "../types/mongo";
|
|
@@ -28,6 +42,8 @@ import { io } from '../server/io';
|
|
|
28
42
|
import type { Service } from '../types/service';
|
|
29
43
|
import { createVars } from "./vars";
|
|
30
44
|
import type { TenantVars } from "../types/vars";
|
|
45
|
+
import { createAgents } from "../lib/agents";
|
|
46
|
+
import { createApi } from "../lib/api";
|
|
31
47
|
|
|
32
48
|
class MongoRest {
|
|
33
49
|
client!: MongoClient;
|
|
@@ -163,6 +179,15 @@ class MongoRest {
|
|
|
163
179
|
return options.data;
|
|
164
180
|
}
|
|
165
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Context handed to a collection hook. `agents` is the tenant-scoped registry,
|
|
184
|
+
* bound to `this` — so a hook runs an agent exactly like it reads a collection:
|
|
185
|
+
* `await agents.get('support')?.generate(...)`.
|
|
186
|
+
*/
|
|
187
|
+
#hookCtx(action: any, meta: any) {
|
|
188
|
+
return { rest: this, io, action, meta, agents: createAgents(this.tenant_id, this), api: createApi(this) };
|
|
189
|
+
}
|
|
190
|
+
|
|
166
191
|
async #logActivity(data: {
|
|
167
192
|
action: string,
|
|
168
193
|
collection: string,
|
|
@@ -377,7 +402,7 @@ class MongoRest {
|
|
|
377
402
|
pipeline = func.toBson(pipeline, { col })
|
|
378
403
|
const meta: any = { action, collection, pipeline }
|
|
379
404
|
if (col.hooks?.beforeOperation) {
|
|
380
|
-
await col.hooks.beforeOperation(
|
|
405
|
+
await col.hooks.beforeOperation(this.#hookCtx(action, meta))
|
|
381
406
|
}
|
|
382
407
|
|
|
383
408
|
let result = await this.db.collection(collection).aggregate(pipeline, {
|
|
@@ -387,7 +412,7 @@ class MongoRest {
|
|
|
387
412
|
result = func.toJson(result)
|
|
388
413
|
meta.result = result
|
|
389
414
|
|
|
390
|
-
await col.hooks?.afterOperation?.(
|
|
415
|
+
await col.hooks?.afterOperation?.(this.#hookCtx(action, meta))
|
|
391
416
|
|
|
392
417
|
return result as any[]
|
|
393
418
|
} catch (err: any) {
|
|
@@ -585,7 +610,7 @@ class MongoRest {
|
|
|
585
610
|
|
|
586
611
|
const meta: any = { action, collection, params, options }
|
|
587
612
|
if (col.hooks?.beforeOperation) {
|
|
588
|
-
await col.hooks.beforeOperation(
|
|
613
|
+
await col.hooks.beforeOperation(this.#hookCtx(action, meta))
|
|
589
614
|
}
|
|
590
615
|
|
|
591
616
|
let result = await this.db.collection(collection).aggregate(pipeline, {
|
|
@@ -596,7 +621,7 @@ class MongoRest {
|
|
|
596
621
|
meta.result = result
|
|
597
622
|
|
|
598
623
|
if (col.hooks?.afterOperation) {
|
|
599
|
-
await col.hooks.afterOperation(
|
|
624
|
+
await col.hooks.afterOperation(this.#hookCtx(action, meta))
|
|
600
625
|
}
|
|
601
626
|
|
|
602
627
|
return result as any[]
|
|
@@ -630,7 +655,7 @@ class MongoRest {
|
|
|
630
655
|
|
|
631
656
|
const meta: any = { action, collection, params, id: _id }
|
|
632
657
|
if (col.hooks?.beforeOperation) {
|
|
633
|
-
await col.hooks.beforeOperation(
|
|
658
|
+
await col.hooks.beforeOperation(this.#hookCtx(action, meta))
|
|
634
659
|
}
|
|
635
660
|
|
|
636
661
|
let result = (await this.db.collection(collection).aggregate(pipeline, {
|
|
@@ -641,7 +666,7 @@ class MongoRest {
|
|
|
641
666
|
meta.result = result
|
|
642
667
|
|
|
643
668
|
if (col.hooks?.afterOperation) {
|
|
644
|
-
await col.hooks.afterOperation(
|
|
669
|
+
await col.hooks.afterOperation(this.#hookCtx(action, meta))
|
|
645
670
|
}
|
|
646
671
|
|
|
647
672
|
return result
|
|
@@ -665,7 +690,7 @@ class MongoRest {
|
|
|
665
690
|
|
|
666
691
|
const meta: any = { action, collection, data }
|
|
667
692
|
if (col.hooks?.beforeOperation) {
|
|
668
|
-
await col.hooks.beforeOperation(
|
|
693
|
+
await col.hooks.beforeOperation(this.#hookCtx(action, meta))
|
|
669
694
|
}
|
|
670
695
|
// Validate AFTER hooks (they may fill required fields or apply Joi defaults)
|
|
671
696
|
meta.data = this._validate({ collection, action: action, data: meta.data })
|
|
@@ -681,7 +706,7 @@ class MongoRest {
|
|
|
681
706
|
const result: T & { _id: string } = { ...func.toJson(meta.data as any), _id: (inserted.insertedId as ObjectId).toHexString() }
|
|
682
707
|
meta.result = result
|
|
683
708
|
if (col?.hooks?.afterOperation) {
|
|
684
|
-
await col.hooks.afterOperation(
|
|
709
|
+
await col.hooks.afterOperation(this.#hookCtx(action, meta))
|
|
685
710
|
}
|
|
686
711
|
|
|
687
712
|
return result
|
|
@@ -708,7 +733,7 @@ class MongoRest {
|
|
|
708
733
|
|
|
709
734
|
const meta: any = { action, collection, data: dataInput }
|
|
710
735
|
if (col.hooks?.beforeOperation) {
|
|
711
|
-
await col.hooks.beforeOperation(
|
|
736
|
+
await col.hooks.beforeOperation(this.#hookCtx(action, meta))
|
|
712
737
|
}
|
|
713
738
|
// Validate AFTER hooks (they may fill required fields or apply Joi defaults)
|
|
714
739
|
meta.data = this._validate({ collection, action: action, data: meta.data })
|
|
@@ -727,7 +752,7 @@ class MongoRest {
|
|
|
727
752
|
meta.result = result
|
|
728
753
|
|
|
729
754
|
if (col.hooks?.afterOperation) {
|
|
730
|
-
await col.hooks.afterOperation(
|
|
755
|
+
await col.hooks.afterOperation(this.#hookCtx(action, meta))
|
|
731
756
|
}
|
|
732
757
|
|
|
733
758
|
return result as (T & { _id: string })[]
|
|
@@ -754,7 +779,7 @@ class MongoRest {
|
|
|
754
779
|
|
|
755
780
|
const meta: any = { action, collection, update, id: _id }
|
|
756
781
|
if (col.hooks?.beforeOperation) {
|
|
757
|
-
await col.hooks.beforeOperation(
|
|
782
|
+
await col.hooks.beforeOperation(this.#hookCtx(action, meta))
|
|
758
783
|
}
|
|
759
784
|
|
|
760
785
|
const data = await this.db.collection(collection).findOneAndUpdate(
|
|
@@ -767,7 +792,7 @@ class MongoRest {
|
|
|
767
792
|
meta.result = result
|
|
768
793
|
|
|
769
794
|
if (col.hooks?.afterOperation) {
|
|
770
|
-
await col.hooks.afterOperation(
|
|
795
|
+
await col.hooks.afterOperation(this.#hookCtx(action, meta))
|
|
771
796
|
}
|
|
772
797
|
|
|
773
798
|
return result
|
|
@@ -821,7 +846,7 @@ class MongoRest {
|
|
|
821
846
|
|
|
822
847
|
const meta: any = { action, collection, filter, update }
|
|
823
848
|
if (col.hooks?.beforeOperation) {
|
|
824
|
-
await col.hooks.beforeOperation(
|
|
849
|
+
await col.hooks.beforeOperation(this.#hookCtx(action, meta))
|
|
825
850
|
}
|
|
826
851
|
|
|
827
852
|
const data = await this.db.collection(collection).findOneAndUpdate(
|
|
@@ -837,7 +862,7 @@ class MongoRest {
|
|
|
837
862
|
meta.result = result
|
|
838
863
|
|
|
839
864
|
if (col.hooks?.afterOperation) {
|
|
840
|
-
await col.hooks.afterOperation(
|
|
865
|
+
await col.hooks.afterOperation(this.#hookCtx(action, meta))
|
|
841
866
|
}
|
|
842
867
|
|
|
843
868
|
return result as Document | null
|
|
@@ -863,7 +888,7 @@ class MongoRest {
|
|
|
863
888
|
|
|
864
889
|
const meta: any = { action, collection, update, ids: _ids }
|
|
865
890
|
if (col.hooks?.beforeOperation) {
|
|
866
|
-
await col.hooks.beforeOperation(
|
|
891
|
+
await col.hooks.beforeOperation(this.#hookCtx(action, meta))
|
|
867
892
|
}
|
|
868
893
|
|
|
869
894
|
const result = await this.db.collection(collection).updateMany(
|
|
@@ -874,7 +899,7 @@ class MongoRest {
|
|
|
874
899
|
meta.result = result
|
|
875
900
|
|
|
876
901
|
if (col.hooks?.afterOperation) {
|
|
877
|
-
await col.hooks.afterOperation(
|
|
902
|
+
await col.hooks.afterOperation(this.#hookCtx(action, meta))
|
|
878
903
|
}
|
|
879
904
|
|
|
880
905
|
return result as UpdateResult
|
|
@@ -891,7 +916,7 @@ class MongoRest {
|
|
|
891
916
|
|
|
892
917
|
const meta: any = { action, collection, id: _id }
|
|
893
918
|
if (col.hooks?.beforeOperation) {
|
|
894
|
-
await col.hooks.beforeOperation(
|
|
919
|
+
await col.hooks.beforeOperation(this.#hookCtx(action, meta))
|
|
895
920
|
}
|
|
896
921
|
|
|
897
922
|
let result = await this.db.collection(collection).findOneAndDelete(
|
|
@@ -902,7 +927,7 @@ class MongoRest {
|
|
|
902
927
|
meta.result = result
|
|
903
928
|
|
|
904
929
|
if (col.hooks?.afterOperation) {
|
|
905
|
-
await col.hooks.afterOperation(
|
|
930
|
+
await col.hooks.afterOperation(this.#hookCtx(action, meta))
|
|
906
931
|
}
|
|
907
932
|
|
|
908
933
|
return result
|
|
@@ -920,7 +945,7 @@ class MongoRest {
|
|
|
920
945
|
|
|
921
946
|
const meta: any = { action, collection, ids: _ids }
|
|
922
947
|
if (col.hooks?.beforeOperation) {
|
|
923
|
-
await col.hooks.beforeOperation(
|
|
948
|
+
await col.hooks.beforeOperation(this.#hookCtx(action, meta))
|
|
924
949
|
}
|
|
925
950
|
|
|
926
951
|
let result: DeleteResult = await this.db.collection(collection).deleteMany(
|
|
@@ -931,7 +956,7 @@ class MongoRest {
|
|
|
931
956
|
meta.result = result
|
|
932
957
|
|
|
933
958
|
if (col.hooks?.afterOperation) {
|
|
934
|
-
await col.hooks.afterOperation(
|
|
959
|
+
await col.hooks.afterOperation(this.#hookCtx(action, meta))
|
|
935
960
|
}
|
|
936
961
|
|
|
937
962
|
return result
|
|
@@ -1094,6 +1119,8 @@ class MongoRest {
|
|
|
1094
1119
|
return await col.actions?.[action]?.({
|
|
1095
1120
|
rest: this,
|
|
1096
1121
|
io,
|
|
1122
|
+
agents: createAgents(this.tenant_id, this),
|
|
1123
|
+
api: createApi(this),
|
|
1097
1124
|
data,
|
|
1098
1125
|
error: fn.error,
|
|
1099
1126
|
jwt: func.jwt,
|
|
@@ -1125,6 +1152,8 @@ class MongoRest {
|
|
|
1125
1152
|
data,
|
|
1126
1153
|
error: fn.error,
|
|
1127
1154
|
io,
|
|
1155
|
+
agents: createAgents(this.tenant_id, this),
|
|
1156
|
+
api: createApi(this),
|
|
1128
1157
|
jwt: func.jwt,
|
|
1129
1158
|
token: token ?? { value: null, decoded: null, provided: false, expired: false },
|
|
1130
1159
|
rest: this,
|
|
@@ -1335,39 +1364,71 @@ class MongoRest {
|
|
|
1335
1364
|
const now = Date.now();
|
|
1336
1365
|
const expiresAt = now + ttlMs;
|
|
1337
1366
|
const id = `${this.tenant_id}:${name}`;
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1367
|
+
const hostname = os.hostname();
|
|
1368
|
+
const col = this.db.collection('_locks_');
|
|
1369
|
+
|
|
1370
|
+
// A lock is free when it expired, or when it is held by a process that is
|
|
1371
|
+
// gone **on this host** (Bun `reusePort` / forked processes: a crashed
|
|
1372
|
+
// worker must not block a run for the whole TTL). A remote owner cannot be
|
|
1373
|
+
// probed — its TTL is the only guarantee.
|
|
1374
|
+
const stale = (lock: any): boolean => {
|
|
1375
|
+
if ((lock.expiresAt ?? 0) < now) return true;
|
|
1376
|
+
if (lock.hostname === hostname && typeof lock.pid === 'number') {
|
|
1377
|
+
if (lock.pid === process.pid) return false; // ours: not stale
|
|
1378
|
+
return !isProcessAlive(lock.pid);
|
|
1350
1379
|
}
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1380
|
+
return false;
|
|
1381
|
+
};
|
|
1382
|
+
|
|
1383
|
+
const claim = async (): Promise<boolean> => {
|
|
1384
|
+
const existing: any = await col.findOne({ _id: id as any });
|
|
1385
|
+
|
|
1386
|
+
if (!existing) {
|
|
1387
|
+
try {
|
|
1388
|
+
await col.insertOne({
|
|
1389
|
+
_id: id as any,
|
|
1390
|
+
tid: this.tenant_id,
|
|
1391
|
+
name,
|
|
1392
|
+
acquiredAt: now,
|
|
1393
|
+
expiresAt,
|
|
1394
|
+
pid: process.pid,
|
|
1395
|
+
hostname,
|
|
1396
|
+
} as any);
|
|
1397
|
+
return true;
|
|
1398
|
+
} catch (err: any) {
|
|
1399
|
+
if (err?.code === 11000) return false; // another node won the race
|
|
1400
|
+
throw err;
|
|
1401
|
+
}
|
|
1359
1402
|
}
|
|
1360
|
-
|
|
1403
|
+
|
|
1404
|
+
if (!stale(existing)) return false;
|
|
1405
|
+
|
|
1406
|
+
// Compare-and-set on `expiresAt` (+ owner pid): another node may take it
|
|
1407
|
+
// over in the same instant, only one of us matches
|
|
1408
|
+
const res = await col.updateOne(
|
|
1409
|
+
{ _id: id as any, expiresAt: existing.expiresAt ?? null, pid: existing.pid ?? null },
|
|
1410
|
+
{ $set: { tid: this.tenant_id, name, acquiredAt: now, expiresAt, pid: process.pid, hostname } },
|
|
1411
|
+
);
|
|
1412
|
+
return (res.matchedCount ?? 0) > 0;
|
|
1413
|
+
};
|
|
1414
|
+
|
|
1415
|
+
if (!(await claim())) {
|
|
1416
|
+
throw new AppError(`Lock '${name}' is already held by another node`, {
|
|
1417
|
+
code: 'LOCK_ACQUISITION_FAILED',
|
|
1418
|
+
status: 409,
|
|
1419
|
+
});
|
|
1361
1420
|
}
|
|
1362
1421
|
}
|
|
1363
1422
|
|
|
1364
1423
|
/**
|
|
1365
|
-
* Release a distributed lock
|
|
1424
|
+
* Release a distributed lock — only when **we** own it: another node may have
|
|
1425
|
+
* taken it over after our process was considered gone.
|
|
1366
1426
|
*/
|
|
1367
1427
|
async unlock(name: string): Promise<void> {
|
|
1368
1428
|
await this.db.collection('_locks_').deleteOne({
|
|
1369
1429
|
tid: this.tenant_id,
|
|
1370
1430
|
name,
|
|
1431
|
+
$or: [{ pid: process.pid }, { pid: { $exists: false } }],
|
|
1371
1432
|
});
|
|
1372
1433
|
}
|
|
1373
1434
|
|