@anteros/core 0.0.40 → 0.0.42
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 +143 -0
- package/database/collection.ts +6 -4
- package/database/deleteLog.ts +98 -0
- package/database/file.ts +7 -6
- package/database/mongodbadapter.ts +420 -185
- package/database/replication.ts +858 -0
- package/database/schema.ts +140 -125
- package/database/tenant.ts +4 -0
- package/database/ttl.ts +68 -0
- package/database/vars.ts +405 -0
- package/database/workflow.ts +466 -240
- package/index.ts +16 -0
- package/lib/agent.ts +1327 -0
- package/lib/agentMemory.ts +586 -0
- package/lib/agents.ts +157 -0
- package/lib/ai.ts +0 -0
- package/lib/api.ts +246 -0
- package/lib/attachment.ts +351 -0
- package/lib/audit.ts +271 -0
- package/lib/auditResult.ts +133 -0
- package/lib/define.ts +62 -1
- package/lib/endpoints.ts +80 -0
- package/lib/files.ts +66 -12
- package/lib/jsonSchema.ts +183 -0
- package/lib/lifecycle.ts +147 -0
- package/lib/load.ts +17 -0
- package/lib/mcp.ts +3 -2
- package/lib/messages.ts +299 -0
- package/lib/providers.ts +867 -0
- package/lib/redact.ts +117 -0
- package/lib/routes.ts +14 -1
- package/lib/scriptWorker.ts +4 -1
- package/lib/scripts.ts +6 -1
- 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 +451 -0
- package/server/api.ts +121 -95
- package/server/boot.ts +196 -53
- package/server/flags.ts +136 -0
- package/server/hono.ts +9 -4
- package/server/mcp.ts +17 -63
- package/server/routes.ts +19 -24
- package/tests/agent-memory.test.ts +617 -0
- package/tests/agent-working-memory.test.ts +402 -0
- package/tests/agent.test.ts +1239 -0
- package/tests/agents-api.test.ts +593 -0
- package/tests/api.test.ts +51 -0
- package/tests/attachment.test.ts +241 -0
- package/tests/audit-file.test.ts +296 -0
- package/tests/audit-operations.test.ts +184 -0
- package/tests/audit-redact.test.ts +259 -0
- package/tests/audit-results.test.ts +188 -0
- package/tests/audit-retention-config.test.ts +76 -0
- package/tests/audit-run-action.test.ts +120 -0
- package/tests/audit.test.ts +320 -0
- package/tests/db-cache.test.ts +7 -7
- package/tests/endpoints.test.ts +80 -0
- package/tests/file-collection-replication.test.ts +98 -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/lc-disabled/lifecycle.ts +11 -0
- package/tests/fixtures/lc-hang/lifecycle.ts +13 -0
- package/tests/fixtures/lc-normal/lifecycle.ts +23 -0
- package/tests/fixtures/lc-throw/lifecycle.ts +6 -0
- package/tests/fixtures/repl-only/app.ts +26 -0
- package/tests/fixtures/repl-only/tenant/collections/orders.model.ts +14 -0
- package/tests/fixtures/repl-only/tenant/lifecycle.ts +14 -0
- package/tests/fixtures/repl-only/tenant/scripts/probe.run.ts +8 -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/vars-tenant/vars/config.var.ts +23 -0
- package/tests/fixtures/vars-tenant/vars/private.var.ts +9 -0
- 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/lifecycle.test.ts +96 -0
- package/tests/locks.test.ts +120 -0
- package/tests/mcp.test.ts +11 -11
- package/tests/messages.test.ts +258 -0
- package/tests/replication-control.test.ts +128 -0
- package/tests/replication-initial-sync.test.ts +86 -0
- package/tests/replication-meta.test.ts +243 -0
- package/tests/replication-only.test.ts +358 -0
- package/tests/replication.test.ts +188 -0
- package/tests/upload.test.ts +141 -8
- package/tests/vars-api.test.ts +123 -0
- package/tests/vars-replication.test.ts +96 -0
- package/tests/vars.test.ts +216 -0
- 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 +837 -0
- package/types/collection.d.ts +3 -0
- package/types/config.d.ts +80 -0
- package/types/file.d.ts +11 -8
- package/types/hook.d.ts +6 -0
- package/types/lifecycle.d.ts +77 -0
- package/types/mcp.d.ts +11 -1
- package/types/replication.d.ts +228 -0
- 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 +36 -0
- package/types/vars.d.ts +147 -0
- package/types/websocket.d.ts +6 -0
- package/types/workflow.d.ts +50 -3
- package/utils/func.ts +67 -52
package/lib/agents.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { Glob } from "bun";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
import { cfg } from "../server/config";
|
|
5
|
+
import { Agent } from "./agent";
|
|
6
|
+
import { InMemoryAgentMemory, MongoAgentMemory, RedisAgentMemory } from "./agentMemory";
|
|
7
|
+
import { importDefinition } from "./load";
|
|
8
|
+
import { logger } from "../utils/logger";
|
|
9
|
+
import type { useRest } from "../database/rest";
|
|
10
|
+
import type { AgentDefinition, TenantAgents } from "../types/agent";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Agent loader — `{tenant.dir}/agents/**\/*.agent.ts`.
|
|
14
|
+
*
|
|
15
|
+
* Each definition is instantiated once per tenant and kept in a process-wide
|
|
16
|
+
* registry keyed by `tenant:agent`. `agents.get('weather')` resolves it at
|
|
17
|
+
* runtime, binding the *calling* `rest` so tools run with the right scope.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const registry = new Map<string, Agent>();
|
|
21
|
+
const definitions: AgentDefinition[] = [];
|
|
22
|
+
|
|
23
|
+
async function syncAgents(): Promise<void> {
|
|
24
|
+
registry.clear();
|
|
25
|
+
definitions.length = 0;
|
|
26
|
+
const memories: Array<{ _tenant_: string; collection: string }> = [];
|
|
27
|
+
|
|
28
|
+
for (const tenant of cfg.tenants ?? []) {
|
|
29
|
+
const agentsPath = path.join(process.cwd(), tenant.dir, "agents");
|
|
30
|
+
if (!(await fs.exists(agentsPath))) continue;
|
|
31
|
+
if (!(await fs.stat(agentsPath)).isDirectory()) continue;
|
|
32
|
+
|
|
33
|
+
const glob = new Glob(path.join(agentsPath, "**/*.agent.ts"));
|
|
34
|
+
for await (const file of glob.scan(".")) {
|
|
35
|
+
const module = await importDefinition(file, "Agent");
|
|
36
|
+
const definition: AgentDefinition | undefined = module?.default;
|
|
37
|
+
if (!definition?._isAgent_) continue;
|
|
38
|
+
if (definition.enabled === false) continue;
|
|
39
|
+
|
|
40
|
+
// `id` and `description` are required — the `Agent` constructor refuses a
|
|
41
|
+
// definition without them, and a broken file never hides the others
|
|
42
|
+
const id = definition.id!;
|
|
43
|
+
|
|
44
|
+
const key = `${tenant.id}:${id}`;
|
|
45
|
+
const existing = registry.get(key);
|
|
46
|
+
if (existing) {
|
|
47
|
+
logger.warn(`Duplicate agent id '${id}' for tenant '${tenant.id}' — '${file}' replaces the previous definition`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const instance = new Agent({ ...definition, id }, { tenant: tenant.id });
|
|
52
|
+
registry.set(key, instance);
|
|
53
|
+
definitions.push(instance.getConfig());
|
|
54
|
+
|
|
55
|
+
// A store says which collections it writes to (a Mongo memory does):
|
|
56
|
+
// published on `cfg` so the replication engine copies them, exactly
|
|
57
|
+
// like a declared collection — the tenant chooses the name.
|
|
58
|
+
for (const collection of instance.getMemory()?.collections?.() ?? []) {
|
|
59
|
+
if (!memories.some((m) => m._tenant_ === tenant.id && m.collection === collection)) {
|
|
60
|
+
memories.push({ _tenant_: tenant.id, collection });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} catch (err: any) {
|
|
64
|
+
console.error(`Failed to load agent '${id}' of tenant '${tenant.id}': ${err?.message}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
cfg.agents = definitions;
|
|
70
|
+
cfg.agentMemories = memories;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The agents declared by a tenant, ready to run.
|
|
75
|
+
*
|
|
76
|
+
* When a `rest` is given (always the case through the `agents` member of a
|
|
77
|
+
* context), every call
|
|
78
|
+
* returns a **copy** bound to it. The registry instance is shared by every
|
|
79
|
+
* caller — binding it in place would let one request's client (and its Mongo
|
|
80
|
+
* session, hence its transaction) leak into another request running at the same
|
|
81
|
+
* time. A copy shares the declaration (instructions, tools, memory), so nothing
|
|
82
|
+
* else changes.
|
|
83
|
+
*/
|
|
84
|
+
function createAgents(tenantId: string, rest?: InstanceType<typeof useRest> | any): TenantAgents {
|
|
85
|
+
const prefix = `${tenantId}:`;
|
|
86
|
+
const bind = (agent: Agent): Agent => rest
|
|
87
|
+
? new Agent(agent.getConfig(), { rest, tenant: agent.getTenant() })
|
|
88
|
+
: agent;
|
|
89
|
+
const entries = (): Array<[string, Agent]> =>
|
|
90
|
+
[...registry.entries()].filter(([key]) => key.startsWith(prefix));
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
get: (id) => {
|
|
94
|
+
const agent = registry.get(`${prefix}${id}`);
|
|
95
|
+
return agent ? bind(agent) : undefined;
|
|
96
|
+
},
|
|
97
|
+
has: (id) => registry.has(`${prefix}${id}`),
|
|
98
|
+
ids: () => entries().map(([key]) => key.slice(prefix.length)),
|
|
99
|
+
list: () => entries().map(([, agent]) => bind(agent)),
|
|
100
|
+
reload: syncAgents,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Boot banner counters. */
|
|
105
|
+
function agentsStats(): { total: number; tenants: string[] } {
|
|
106
|
+
const tenants = new Set([...registry.keys()].map((key) => key.slice(0, key.indexOf(":"))));
|
|
107
|
+
return { total: registry.size, tenants: [...tenants] };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Process-wide agent registry, importable from **anywhere** — including a place
|
|
112
|
+
* with no `rest` at hand: module scope, a global middleware, a `Bun.cron`, a
|
|
113
|
+
* `Bun.serve` of your own.
|
|
114
|
+
*
|
|
115
|
+
* ```ts
|
|
116
|
+
* import { agents } from '@anteros/core'
|
|
117
|
+
*
|
|
118
|
+
* const support = agents.get('v1', 'support')
|
|
119
|
+
* await support.generate('Where is my order?')
|
|
120
|
+
* ```
|
|
121
|
+
*
|
|
122
|
+
* Prefer the `agents` member of a context whenever there is one (a script, a
|
|
123
|
+
* service action, a hook, a socket, a lifecycle hook, a workflow step, a route,
|
|
124
|
+
* an MCP tool): the tenant is implicit and the **calling** client is bound, so the
|
|
125
|
+
* agent's tools run with the right scope. Without a `rest`, pass one as the third
|
|
126
|
+
* argument (`agents.get(tenantId, id, rest)`) or the tools run without a database
|
|
127
|
+
* client.
|
|
128
|
+
*/
|
|
129
|
+
const agents = {
|
|
130
|
+
/** The agent, bound to `rest` when provided — `undefined` if it does not exist. */
|
|
131
|
+
get: (tenantId: string, id: string, rest?: InstanceType<typeof useRest>): Agent | undefined =>
|
|
132
|
+
createAgents(tenantId, rest).get(id),
|
|
133
|
+
has: (tenantId: string, id: string): boolean => registry.has(`${tenantId}:${id}`),
|
|
134
|
+
/** Declared agent ids of a tenant. */
|
|
135
|
+
ids: (tenantId: string): string[] => createAgents(tenantId).ids(),
|
|
136
|
+
list: (tenantId: string, rest?: InstanceType<typeof useRest>): Agent[] => createAgents(tenantId, rest).list(),
|
|
137
|
+
/** Tenant-scoped registry — the same object a context receives as `agents`. */
|
|
138
|
+
use: createAgents,
|
|
139
|
+
/** Re-scan `{tenant.dir}/agents` and rebuild the registry. */
|
|
140
|
+
reload: syncAgents,
|
|
141
|
+
/** `{ total, tenants }` — what the boot banner prints. */
|
|
142
|
+
stats: agentsStats,
|
|
143
|
+
memory: {
|
|
144
|
+
InMemory: InMemoryAgentMemory,
|
|
145
|
+
Mongo: MongoAgentMemory,
|
|
146
|
+
Redis: RedisAgentMemory,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/** Mongo collections used as agent memory by a tenant — published by the loader. */
|
|
151
|
+
function agentMemoryCollections(tenantId: string): string[] {
|
|
152
|
+
return (cfg.agentMemories ?? [])
|
|
153
|
+
.filter((memory) => memory._tenant_ === tenantId)
|
|
154
|
+
.map((memory) => memory.collection);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export { syncAgents, createAgents, agentsStats, agents, agentMemoryCollections, InMemoryAgentMemory, MongoAgentMemory, RedisAgentMemory };
|
package/lib/ai.ts
ADDED
|
File without changes
|
package/lib/api.ts
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import { createAgents } from "./agents";
|
|
2
|
+
import { endpoints } from "./endpoints";
|
|
3
|
+
import { handleDelete } from "./files";
|
|
4
|
+
import type { useRest } from "../database/rest";
|
|
5
|
+
import type { Agent } from "./agent";
|
|
6
|
+
import type { FindCallOptions, FindOptions, findOneOptions } from "../types/mongo";
|
|
7
|
+
import type { updateApiOptions } from "../types/api";
|
|
8
|
+
import type { TenantVars } from "../types/vars";
|
|
9
|
+
import type { BulkUpdateOperation } from "../types/rest";
|
|
10
|
+
import type {
|
|
11
|
+
AnyBulkWriteOperation,
|
|
12
|
+
BulkWriteOptions,
|
|
13
|
+
BulkWriteResult,
|
|
14
|
+
ChangeStream,
|
|
15
|
+
ChangeStreamOptions,
|
|
16
|
+
DeleteResult,
|
|
17
|
+
Document,
|
|
18
|
+
UpdateFilter,
|
|
19
|
+
UpdateResult,
|
|
20
|
+
} from "mongodb";
|
|
21
|
+
|
|
22
|
+
type Rest = InstanceType<typeof useRest>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The in-process facade — an `api` member in every context, next to `rest`,
|
|
26
|
+
* `io` and `agents`.
|
|
27
|
+
*
|
|
28
|
+
* It is **the same thing as `rest`, grouped the way the SDK and the HTTP API are
|
|
29
|
+
* grouped**: one namespace per family, and a **bound client** for a collection.
|
|
30
|
+
* Nothing is re-implemented — every call lands on the very same `rest` method the
|
|
31
|
+
* context already had, so audit, hooks, access rules, the session and the
|
|
32
|
+
* transaction are those of the caller.
|
|
33
|
+
*
|
|
34
|
+
* ```ts
|
|
35
|
+
* export default define.Hook(async ({ api, rest }) => {
|
|
36
|
+
* // Bound to one collection — the slug is stated once
|
|
37
|
+
* const orders = api.collection('orders');
|
|
38
|
+
* const [order] = await orders.find({ $match: { status: 'paid' }, $limit: 1 });
|
|
39
|
+
* await orders.updateOne(order._id, { $set: { status: 'shipped' } });
|
|
40
|
+
*
|
|
41
|
+
* // The families
|
|
42
|
+
* await api.vars.set('config', 'lastRun', new Date().toISOString());
|
|
43
|
+
* await api.service('analytics').run('refresh', { orderId: order._id });
|
|
44
|
+
* const url = api.files.url('invoices', 'f1.pdf', { width: 800 });
|
|
45
|
+
* await api.agent('triage')?.generate(`Classify: ${order.ref}`);
|
|
46
|
+
*
|
|
47
|
+
* // …and `rest` stays exactly what it was
|
|
48
|
+
* await rest.aggregate('orders', [{ $count: 'total' }]);
|
|
49
|
+
* });
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* Naming rule, shared with the SDK: **singular = a client bound to one resource**
|
|
53
|
+
* (`api.collection(slug)`, `api.agent(id)`), **plural = the family's operations**
|
|
54
|
+
* (`api.vars`, `api.files`).
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
/** One collection, bound to the `rest` of the context. */
|
|
58
|
+
class ApiCollection<T = any> {
|
|
59
|
+
#rest: Rest;
|
|
60
|
+
#slug: string;
|
|
61
|
+
|
|
62
|
+
constructor(rest: Rest, slug: string) {
|
|
63
|
+
this.#rest = rest;
|
|
64
|
+
this.#slug = slug;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The slug this client is bound to. */
|
|
68
|
+
getSlug(): string {
|
|
69
|
+
return this.#slug;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Reads ────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
find(params: FindOptions = {}, options: FindCallOptions = {}): Promise<Document[]> {
|
|
75
|
+
return this.#rest.find(this.#slug, params, options);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
findOne(_id: string, params?: findOneOptions): Promise<any> {
|
|
79
|
+
return this.#rest.findOne(this.#slug, _id, params);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
aggregate<T = Document>(pipeline: any[]): Promise<T[]> {
|
|
83
|
+
return this.#rest.aggregate(this.#slug, pipeline) as Promise<T[]>;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
countDocuments(query: any = {}): Promise<number> {
|
|
87
|
+
return this.#rest.countDocuments(this.#slug, query);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The collection's change stream — the caller consumes it. */
|
|
91
|
+
watch(pipeline: any[], options: ChangeStreamOptions): Promise<ChangeStream> {
|
|
92
|
+
return this.#rest.watch(this.#slug, pipeline, options);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Writes ───────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
insertOne<TBody>(data: TBody): Promise<TBody & { _id: string }> {
|
|
98
|
+
return this.#rest.insertOne(this.#slug, data);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
insertMany<TBody>(data: TBody[]): Promise<(TBody & { _id: string })[]> {
|
|
102
|
+
return this.#rest.insertMany(this.#slug, data);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
updateOne(_id: string, update: UpdateFilter<any>): Promise<UpdateResult & { previous?: any }> {
|
|
106
|
+
return this.#rest.updateOne(this.#slug, _id, update) as Promise<UpdateResult & { previous?: any }>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
findOneAndUpdate(
|
|
110
|
+
filter: Document,
|
|
111
|
+
update: UpdateFilter<any>,
|
|
112
|
+
options?: updateApiOptions,
|
|
113
|
+
): Promise<Document | null> {
|
|
114
|
+
return this.#rest.findOneAndUpdate(this.#slug, filter, update, options);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
updateMany(_ids: string[], update: UpdateFilter<any>): Promise<UpdateResult> {
|
|
118
|
+
return this.#rest.updateMany(this.#slug, _ids, update);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
deleteOne(_id: string): Promise<any> {
|
|
122
|
+
return this.#rest.deleteOne(this.#slug, _id);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
deleteMany(_ids: string[]): Promise<DeleteResult> {
|
|
126
|
+
return this.#rest.deleteMany(this.#slug, _ids);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
bulkWrite(operations: AnyBulkWriteOperation[], options?: BulkWriteOptions): Promise<BulkWriteResult> {
|
|
130
|
+
return this.#rest.bulkWrite(this.#slug, operations, options);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
bulkUpdate(operations: BulkUpdateOperation[], options?: BulkWriteOptions): Promise<BulkWriteResult> {
|
|
134
|
+
return this.#rest.bulkUpdate(this.#slug, operations, options);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── Maintenance & custom actions ─────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
dropCollection(): Promise<any> {
|
|
140
|
+
return this.#rest.dropCollection(this.#slug);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
dropIndex(index: string): Promise<any> {
|
|
144
|
+
return this.#rest.dropIndex(this.#slug, index);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
dropIndexes(): Promise<any> {
|
|
148
|
+
return this.#rest.dropIndexes(this.#slug);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** A custom action declared on the collection (`define.Action` / `actions`). */
|
|
152
|
+
runAction<T = any>(action: string, data?: any): Promise<T> {
|
|
153
|
+
return this.#rest.runAction<T>(this.#slug, action, data);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** One service of the tenant, bound to its name — `api.service('analytics')`. */
|
|
158
|
+
class ApiService {
|
|
159
|
+
#rest: Rest;
|
|
160
|
+
#name: string;
|
|
161
|
+
|
|
162
|
+
constructor(rest: Rest, name: string) {
|
|
163
|
+
this.#rest = rest;
|
|
164
|
+
this.#name = name;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The service name this client is bound to. */
|
|
168
|
+
getName(): string {
|
|
169
|
+
return this.#name;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Run one action of the service — `data` is what the handler reads as `body.data`. */
|
|
173
|
+
run<T = any>(action: string, data?: any): Promise<T> {
|
|
174
|
+
return this.#rest.runService<T>(this.#name, action, data);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The `api` member of a context — see the class doc for the convention. */
|
|
179
|
+
type Api = {
|
|
180
|
+
/** One collection, bound: `api.collection('orders').find(...)`. */
|
|
181
|
+
collection<T = any>(slug: string): ApiCollection<T>;
|
|
182
|
+
/** The tenant's variables — the same object as `rest.vars`. */
|
|
183
|
+
vars: TenantVars;
|
|
184
|
+
/** One service, bound: `api.service('analytics').run('monthly', data)`. */
|
|
185
|
+
service<T = any>(name: string): ApiService & { run<R = T>(action: string, data?: any): Promise<R> };
|
|
186
|
+
/** File collections — the URL of a stored file, and its deletion. */
|
|
187
|
+
files: {
|
|
188
|
+
url(collection: string, filename: string, transform?: {
|
|
189
|
+
width?: number;
|
|
190
|
+
height?: number;
|
|
191
|
+
format?: "webp" | "jpeg" | "png" | "avif";
|
|
192
|
+
quality?: number;
|
|
193
|
+
}): string;
|
|
194
|
+
delete(collection: string, fileId: string): Promise<{ message: string; ok: boolean }>;
|
|
195
|
+
};
|
|
196
|
+
/** One agent of the tenant, bound to the same `rest` — `undefined` if unknown. */
|
|
197
|
+
agent(id: string): Agent | undefined;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Build the `api` facade of a context. Everything delegates to `rest`, so a hook,
|
|
202
|
+
* a script or a workflow step gets the same client it already had — only the shape
|
|
203
|
+
* changes.
|
|
204
|
+
*/
|
|
205
|
+
function createApi(rest: Rest): Api {
|
|
206
|
+
const tenant = rest.tenant_id;
|
|
207
|
+
const registry = createAgents(tenant, rest);
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
collection: <T = any>(slug: string) => new ApiCollection<T>(rest, slug),
|
|
211
|
+
// `rest.vars` returns a fresh bound client on each access — keep that
|
|
212
|
+
get vars() {
|
|
213
|
+
return rest.vars;
|
|
214
|
+
},
|
|
215
|
+
service: (name: string) => new ApiService(rest, name),
|
|
216
|
+
files: {
|
|
217
|
+
/** `/api/<tenant>/files/<collection>/<file>` (+ `w`, `h`, `format`, `q`). */
|
|
218
|
+
url(collection: string, filename: string, transform?: {
|
|
219
|
+
width?: number;
|
|
220
|
+
height?: number;
|
|
221
|
+
format?: "webp" | "jpeg" | "png" | "avif";
|
|
222
|
+
quality?: number;
|
|
223
|
+
}): string {
|
|
224
|
+
const base = endpoints.file(tenant, collection, filename);
|
|
225
|
+
if (!transform) return base;
|
|
226
|
+
|
|
227
|
+
const params = new URLSearchParams();
|
|
228
|
+
if (transform.width) params.set("w", String(transform.width));
|
|
229
|
+
if (transform.height) params.set("h", String(transform.height));
|
|
230
|
+
if (transform.format) params.set("format", transform.format);
|
|
231
|
+
if (transform.quality) params.set("q", String(transform.quality));
|
|
232
|
+
|
|
233
|
+
const query = params.toString();
|
|
234
|
+
return query ? `${base}?${query}` : base;
|
|
235
|
+
},
|
|
236
|
+
/** Removes the stored binary **and** the document (the route does the same). */
|
|
237
|
+
delete(collection: string, fileId: string) {
|
|
238
|
+
return handleDelete(tenant, collection, fileId).then(() => ({ message: "File deleted", ok: true }));
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
agent: (id: string) => registry.get(id),
|
|
242
|
+
} as Api;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export { ApiCollection, ApiService, createApi };
|
|
246
|
+
export type { Api };
|