@anteros/core 0.0.19 → 0.0.21
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/README.md +90 -5
- package/database/decorator.ts +1 -1
- package/database/mongodbadapter.ts +64 -40
- package/index.ts +1 -1
- package/lib/define.ts +60 -3
- package/lib/files.ts +115 -32
- package/lib/mcp.ts +59 -0
- package/lib/scriptWorker.ts +76 -0
- package/lib/scripts.ts +77 -12
- package/package.json +6 -3
- package/probe-cache.ts +16 -0
- package/server/api.ts +35 -28
- package/server/boot.ts +78 -16
- package/server/cluster.ts +177 -0
- package/server/config.ts +0 -1
- package/server/env.ts +6 -0
- package/server/hono.ts +192 -18
- package/server/mcp.ts +232 -0
- package/server/metrics.ts +110 -0
- package/server/security.ts +13 -9
- package/tests/api.test.ts +14 -12
- package/tests/cache.test.ts +124 -0
- package/tests/crypto.test.ts +101 -0
- package/tests/fixtures/heavy/collections/items.model.ts +8 -0
- package/tests/fixtures/heavy/scripts/heavy-import.run.ts +14 -0
- package/tests/fixtures/mcp-tenant/mcp/resources/orders.summary.resource.ts +17 -0
- package/tests/fixtures/mcp-tenant/mcp/resources/users.id.resource.ts +18 -0
- package/tests/fixtures/mcp-tenant/mcp/tools/badge.tool.ts +16 -0
- package/tests/fixtures/mcp-tenant/mcp/tools/echo.tool.ts +19 -0
- package/tests/fixtures/metrics-worker.ts +21 -0
- package/tests/hono.test.ts +211 -0
- package/tests/mcp.test.ts +186 -0
- package/tests/metrics-cluster.test.ts +103 -0
- package/tests/scripts-heavy.test.ts +49 -0
- package/tests/security.test.ts +102 -0
- package/tests/ts1011-test.ts +15 -0
- package/tests/utils.test.ts +113 -0
- package/types/api.d.ts +11 -0
- package/types/collection.d.ts +20 -5
- package/types/config.d.ts +20 -3
- package/types/hook.d.ts +16 -13
- package/types/mcp.d.ts +81 -0
- package/types/scripts.d.ts +14 -1
- package/utils/cache.ts +20 -1
- package/utils/crypto.ts +25 -8
- package/utils/func.ts +178 -18
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @
|
|
1
|
+
# @anteros/core
|
|
2
2
|
|
|
3
3
|
Server core for **multi-tenant** apps: generic **MongoDB** REST API, **Joi** validation, hooks, JWT auth, custom routes, and boot scripts. Built for **Bun** with **Hono** and **Socket.io**.
|
|
4
4
|
|
|
@@ -11,7 +11,7 @@ Server core for **multi-tenant** apps: generic **MongoDB** REST API, **Joi** val
|
|
|
11
11
|
## Installation
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
bun add @
|
|
14
|
+
bun add @anteros/core@latest
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
## Quick start
|
|
@@ -19,10 +19,9 @@ bun add @dnax/core@0.0.0-rc.13
|
|
|
19
19
|
Typical entrypoint in your app (working directory = project root that contains tenant folders):
|
|
20
20
|
|
|
21
21
|
```ts
|
|
22
|
-
import { app, define } from "@
|
|
22
|
+
import { app, define } from "@anteros/core";
|
|
23
23
|
|
|
24
24
|
await app.boot({
|
|
25
|
-
clusterMode: false,
|
|
26
25
|
server: {
|
|
27
26
|
port: 5000,
|
|
28
27
|
jwt: {
|
|
@@ -57,6 +56,7 @@ On boot, the server syncs tenants, loads collections (`*.model.ts`), registers r
|
|
|
57
56
|
| `app` | `{ boot }` — starts the application |
|
|
58
57
|
| `useRest` | REST client / per-tenant Mongo access (programmatic use or in handlers) |
|
|
59
58
|
| `v` | **Joi** (re-export), for schemas in models |
|
|
59
|
+
| `cache` | Native caching: `useMemoryCache`, `useFilesystemCache`, `useRedisCache` (Bun.Redis) |
|
|
60
60
|
| `utils` | Internal utilities exposed by the package |
|
|
61
61
|
|
|
62
62
|
## Folder layout (per tenant)
|
|
@@ -116,11 +116,96 @@ Per collection, `api.access` allows or denies each action (`boolean` or async fu
|
|
|
116
116
|
|
|
117
117
|
When `api.auth.enabled` is set, define `onLogin` / `onLogout` on the model. The JWT secret is read from `server.jwt.secret` or the **`JWT_SECRET`** environment variable.
|
|
118
118
|
|
|
119
|
+
## Health & metrics
|
|
120
|
+
|
|
121
|
+
The server exposes a built-in **`GET /health`** endpoint (zero dependency) reporting live process metrics:
|
|
122
|
+
|
|
123
|
+
```json
|
|
124
|
+
{
|
|
125
|
+
"pid": 10047,
|
|
126
|
+
"startedAt": 1720000000000,
|
|
127
|
+
"uptime": 3600,
|
|
128
|
+
"env": "production",
|
|
129
|
+
"connections": 0,
|
|
130
|
+
"pendingRequests": 3,
|
|
131
|
+
"pendingWebSockets": 7,
|
|
132
|
+
"requests": {
|
|
133
|
+
"total": 1523,
|
|
134
|
+
"active": 3,
|
|
135
|
+
"errors": 2,
|
|
136
|
+
"byMethod": { "GET": 1200, "POST": 323 },
|
|
137
|
+
"byStatus": { "200": 1500, "201": 21, "500": 2 }
|
|
138
|
+
},
|
|
139
|
+
"memory": { "rss": 13778944, "heapUsed": 112384, "heapTotal": 521216, "external": 10624 },
|
|
140
|
+
"cpu": { "percent": 12.5, "cores": 8, "user": 120000, "system": 80000 }
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
| Field | Meaning |
|
|
145
|
+
|-------|---------|
|
|
146
|
+
| `pid` / `uptime` | Process id / seconds since boot |
|
|
147
|
+
| `requests.total` / `requests.errors` | Total requests served / responses ≥ 500 |
|
|
148
|
+
| `requests.byMethod` / `requests.byStatus` | Counters split by HTTP method / status |
|
|
149
|
+
| `memory` | V8 heap + RSS usage (bytes) |
|
|
150
|
+
| `pendingRequests` | Requests currently being processed |
|
|
151
|
+
| `pendingWebSockets` | Active WebSocket connections |
|
|
152
|
+
| `cpu.percent` | % of a single core over the last sample window |
|
|
153
|
+
|
|
154
|
+
With `reusePort`, each process exposes its **own** metrics — useful to see per-worker load.
|
|
155
|
+
|
|
156
|
+
## Cluster (reusePort + multiple processes)
|
|
157
|
+
|
|
158
|
+
Set `server.reusePort: true` to enable Bun's `SO_REUSEPORT`. The framework then acts as a **master**:
|
|
159
|
+
|
|
160
|
+
- Spawns N workers (`server.workers`, default = CPU count)
|
|
161
|
+
- Each worker serves the main port (the OS round-robins connections)
|
|
162
|
+
- Workers report metrics to the master **via IPC** every 5s
|
|
163
|
+
- The master **restarts any worker that crashes** (auto-respawn)
|
|
164
|
+
- The master exposes an **aggregated `/health`** on `server.metricsPort` (default: `port + 1`)
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
await app.boot({
|
|
168
|
+
server: {
|
|
169
|
+
port: 5000,
|
|
170
|
+
reusePort: true, // cluster mode
|
|
171
|
+
workers: 4, // optional, default = CPU count
|
|
172
|
+
metricsPort: 5001, // optional, default = port + 1
|
|
173
|
+
},
|
|
174
|
+
// ...
|
|
175
|
+
});
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
curl http://localhost:5001/health # aggregated across all workers
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
```json
|
|
183
|
+
{
|
|
184
|
+
"status": "ok",
|
|
185
|
+
"master": { "pid": 1000 },
|
|
186
|
+
"workers": 4,
|
|
187
|
+
"uptime": { "min": 12, "max": 15 },
|
|
188
|
+
"requests": {
|
|
189
|
+
"total": 1523,
|
|
190
|
+
"active": 11,
|
|
191
|
+
"errors": 2,
|
|
192
|
+
"perSecond": 101,
|
|
193
|
+
"byMethod": { "GET": 1200, "POST": 323 },
|
|
194
|
+
"byStatus": { "200": 1500, "201": 21, "500": 2 }
|
|
195
|
+
},
|
|
196
|
+
"memory": { "rss": 54067200, "heapUsed": 589294 },
|
|
197
|
+
"cpu": { "percent": 25.5, "cores": 8 },
|
|
198
|
+
"pids": [1001, 1002, 1003, 1004]
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
> ⚠️ Each process has its own memory — use **Redis** for shared cache / Socket.io state.
|
|
203
|
+
|
|
119
204
|
## Server configuration
|
|
120
205
|
|
|
121
206
|
Common options when calling `app.boot`:
|
|
122
207
|
|
|
123
|
-
- `
|
|
208
|
+
- `server.reusePort` — Bun `SO_REUSEPORT` (share the port across multiple processes)
|
|
124
209
|
- `server.port`, `server.name`
|
|
125
210
|
- `server.cors` — origins, credentials, headers, and methods
|
|
126
211
|
- `server.ipRestriction` — allow/deny lists (Hono)
|
package/database/decorator.ts
CHANGED
|
@@ -10,7 +10,7 @@ export function CheckIfCollectionExists(): any {
|
|
|
10
10
|
return descriptor;
|
|
11
11
|
}
|
|
12
12
|
const originalMethod = descriptor.value;
|
|
13
|
-
descriptor.value = async function (...args: any[]) {
|
|
13
|
+
descriptor.value = async function (this: any, ...args: any[]) {
|
|
14
14
|
let collection = args[0];
|
|
15
15
|
let col = getCollection(collection, this.tenant_id)
|
|
16
16
|
if (!col) {
|
|
@@ -42,18 +42,22 @@ class MongoRest {
|
|
|
42
42
|
if (options.session) {
|
|
43
43
|
this.session = options.session;
|
|
44
44
|
}
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
// Resolve tenant: explicit tenant_id, else from request context
|
|
46
|
+
let tenantId = options.tenant_id;
|
|
47
|
+
if (!tenantId) {
|
|
48
|
+
tenantId = requestCtxStorage.get<string>('tenant_id');
|
|
49
|
+
}
|
|
50
|
+
let tenant = getTenant(tenantId ?? '-')
|
|
51
|
+
if (tenant) {
|
|
47
52
|
this.#tenant = tenant;
|
|
48
53
|
this.tenant_id = tenant.id;
|
|
49
54
|
this.db = tenant.database.db as Db;
|
|
50
55
|
this.client = tenant.database.client as MongoClient;
|
|
51
56
|
}
|
|
52
|
-
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
|
|
56
|
-
private _validate(options: { collection: string, action?: ActionsApiList, data?: any, update?: UpdateFilter<any> }) {
|
|
60
|
+
private _validate(options: { collection: string, action?: ActionsApiList, data?: any, update?: UpdateFilter<any> }): any {
|
|
57
61
|
let col = getCollection(options.collection, this.#tenant.id)
|
|
58
62
|
let schema = col?._schema_
|
|
59
63
|
let partialSchema = col?._schemaPartial_
|
|
@@ -77,6 +81,7 @@ class MongoRest {
|
|
|
77
81
|
|
|
78
82
|
if (options?.data) {
|
|
79
83
|
if (options.action == 'insertMany') {
|
|
84
|
+
const sanitized: any[] = [];
|
|
80
85
|
for (let i = 0; i < options.data.length; i++) {
|
|
81
86
|
validationResult = schema.validate(options.data[i], { allowUnknown: false })
|
|
82
87
|
if (validationResult?.error) {
|
|
@@ -85,9 +90,9 @@ class MongoRest {
|
|
|
85
90
|
status: 400
|
|
86
91
|
})
|
|
87
92
|
}
|
|
88
|
-
|
|
89
|
-
options.data[i] = validationResult.value
|
|
93
|
+
sanitized.push(validationResult.value)
|
|
90
94
|
}
|
|
95
|
+
return sanitized;
|
|
91
96
|
}
|
|
92
97
|
|
|
93
98
|
if (options.action == 'insertOne') {
|
|
@@ -98,8 +103,8 @@ class MongoRest {
|
|
|
98
103
|
status: 400
|
|
99
104
|
})
|
|
100
105
|
}
|
|
101
|
-
//
|
|
102
|
-
|
|
106
|
+
// Return sanitized value (Joi defaults applied, unknown keys handled)
|
|
107
|
+
return validationResult.value;
|
|
103
108
|
}
|
|
104
109
|
}
|
|
105
110
|
|
|
@@ -117,22 +122,38 @@ class MongoRest {
|
|
|
117
122
|
validationResult = partialSchema.validate(options.update.$setOnInsert, { allowUnknown: false })
|
|
118
123
|
}
|
|
119
124
|
|
|
125
|
+
// Faille 9: updateOne/updateMany — unflatten dot-notation ("address.zip") before Joi validation
|
|
126
|
+
if ((options.action == 'updateOne' || options.action == 'updateMany') && options.update?.$set) {
|
|
127
|
+
const unflattened = func.unflattenKeys(options.update.$set)
|
|
128
|
+
validationResult = partialSchema.validate(unflattened, { allowUnknown: false })
|
|
129
|
+
if (validationResult?.error) {
|
|
130
|
+
throw new AppError(validationResult.error.message, {
|
|
131
|
+
code: 'VALIDATION_ERROR',
|
|
132
|
+
status: 400
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
// Write sanitized (nested) value back — MongoDB accepts nested objects
|
|
136
|
+
if (validationResult.value) {
|
|
137
|
+
options.update.$set = validationResult.value
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
120
141
|
if (validationResult?.error) {
|
|
121
142
|
throw new AppError(validationResult.error.message, {
|
|
122
143
|
code: 'VALIDATION_ERROR',
|
|
123
144
|
status: 400
|
|
124
145
|
})
|
|
125
146
|
}
|
|
147
|
+
return validationResult?.value;
|
|
126
148
|
}
|
|
127
149
|
|
|
128
150
|
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
|
|
132
151
|
|
|
152
|
+
// No schema or nothing to sanitize → pass the raw data through
|
|
153
|
+
return options.data;
|
|
133
154
|
}
|
|
134
155
|
|
|
135
|
-
|
|
156
|
+
async #logActivity(data: {
|
|
136
157
|
action: string,
|
|
137
158
|
collection: string,
|
|
138
159
|
input?: any,
|
|
@@ -189,7 +210,7 @@ class MongoRest {
|
|
|
189
210
|
await this.audit.addActivities([activity])
|
|
190
211
|
}
|
|
191
212
|
|
|
192
|
-
|
|
213
|
+
async #executeWithAudit<T>(
|
|
193
214
|
action: string,
|
|
194
215
|
collection: string,
|
|
195
216
|
input: any,
|
|
@@ -254,7 +275,17 @@ class MongoRest {
|
|
|
254
275
|
try {
|
|
255
276
|
|
|
256
277
|
const col = getCollection(collection, this.#tenant.id) as Collection
|
|
257
|
-
|
|
278
|
+
// Faille 12: restrict $lookup targets to this tenant's declared collections
|
|
279
|
+
const allowedCollections = [
|
|
280
|
+
...(cfg.collections ?? [])
|
|
281
|
+
.filter(c => c._tenant_ === this.tenant_id)
|
|
282
|
+
.map(c => c.slug),
|
|
283
|
+
...(cfg.fileCollections ?? [])
|
|
284
|
+
.filter(fc => fc._tenant_ === this.tenant_id)
|
|
285
|
+
.map(fc => fc.slug),
|
|
286
|
+
'_activities_', '_locks_', '_workflows_',
|
|
287
|
+
];
|
|
288
|
+
const isSafe = func.isSafeAggregatePipeline(pipeline, allowedCollections);
|
|
258
289
|
if (!isSafe.isSafe) {
|
|
259
290
|
throw isSafe.error;
|
|
260
291
|
}
|
|
@@ -362,7 +393,7 @@ class MongoRest {
|
|
|
362
393
|
await col.hooks.beforeOperation({ rest: this, io, action, meta })
|
|
363
394
|
}
|
|
364
395
|
// Validate AFTER hooks (they may fill required fields or apply Joi defaults)
|
|
365
|
-
this._validate({ collection, action: action, data: meta.data })
|
|
396
|
+
meta.data = this._validate({ collection, action: action, data: meta.data })
|
|
366
397
|
|
|
367
398
|
await this.db.collection(collection).insertOne(func.toBson(meta.data, { col }) as any, {
|
|
368
399
|
session: this.session,
|
|
@@ -400,7 +431,7 @@ class MongoRest {
|
|
|
400
431
|
await col.hooks.beforeOperation({ rest: this, io, action, meta })
|
|
401
432
|
}
|
|
402
433
|
// Validate AFTER hooks (they may fill required fields or apply Joi defaults)
|
|
403
|
-
this._validate({ collection, action: action, data: meta.data })
|
|
434
|
+
meta.data = this._validate({ collection, action: action, data: meta.data })
|
|
404
435
|
|
|
405
436
|
await this.db.collection(collection).insertMany(func.toBson(meta.data, { col }) as any, {
|
|
406
437
|
session: this.session,
|
|
@@ -429,15 +460,13 @@ class MongoRest {
|
|
|
429
460
|
}
|
|
430
461
|
|
|
431
462
|
update = await func.buildInput(update)
|
|
463
|
+
this._validate({ collection, action: action, update: update })
|
|
464
|
+
update = func.toBson(update, { col })
|
|
432
465
|
|
|
433
466
|
const meta: any = { action, collection, update, id: _id }
|
|
434
467
|
if (col.hooks?.beforeOperation) {
|
|
435
468
|
await col.hooks.beforeOperation({ rest: this, io, action, meta })
|
|
436
469
|
}
|
|
437
|
-
// Validate and toBson AFTER hooks
|
|
438
|
-
update = meta.update;
|
|
439
|
-
this._validate({ collection, action: action, update: update })
|
|
440
|
-
update = func.toBson(update, { col })
|
|
441
470
|
|
|
442
471
|
const data = await this.db.collection(collection).findOneAndUpdate(
|
|
443
472
|
{ _id: new ObjectId(_id) },
|
|
@@ -493,18 +522,16 @@ class MongoRest {
|
|
|
493
522
|
}
|
|
494
523
|
|
|
495
524
|
|
|
496
|
-
const meta: any = { action, collection, filter, update }
|
|
497
|
-
if (col.hooks?.beforeOperation) {
|
|
498
|
-
await col.hooks.beforeOperation({ rest: this, io, action, meta })
|
|
499
|
-
}
|
|
500
|
-
// Validate and toBson AFTER hooks
|
|
501
|
-
update = meta.update;
|
|
502
|
-
filter = meta.filter;
|
|
503
525
|
this._validate({ collection, action: action, update: update.$set })
|
|
504
526
|
this._validate({ collection, action: action, update: update.$setOnInsert })
|
|
505
527
|
update = func.toBson(update, { col })
|
|
506
528
|
filter = func.toBson(filter, { col })
|
|
507
529
|
|
|
530
|
+
const meta: any = { action, collection, filter, update }
|
|
531
|
+
if (col.hooks?.beforeOperation) {
|
|
532
|
+
await col.hooks.beforeOperation({ rest: this, io, action, meta })
|
|
533
|
+
}
|
|
534
|
+
|
|
508
535
|
const data = await this.db.collection(collection).findOneAndUpdate(
|
|
509
536
|
filter,
|
|
510
537
|
{
|
|
@@ -536,15 +563,13 @@ class MongoRest {
|
|
|
536
563
|
update.$set = await func.buildInput(update.$set, { action: action, col: col })
|
|
537
564
|
}
|
|
538
565
|
update = await func.buildInput(update)
|
|
566
|
+
this._validate({ collection, action: action, update: update })
|
|
567
|
+
update = func.toBson(update, { col })
|
|
539
568
|
|
|
540
569
|
const meta: any = { action, collection, update, ids: _ids }
|
|
541
570
|
if (col.hooks?.beforeOperation) {
|
|
542
571
|
await col.hooks.beforeOperation({ rest: this, io, action, meta })
|
|
543
572
|
}
|
|
544
|
-
// Validate and toBson AFTER hooks
|
|
545
|
-
update = meta.update;
|
|
546
|
-
this._validate({ collection, action: action, update: update })
|
|
547
|
-
update = func.toBson(update, { col })
|
|
548
573
|
|
|
549
574
|
const result = await this.db.collection(collection).updateMany(
|
|
550
575
|
{ _id: { $in: _ids.map(id => new ObjectId(id)) } },
|
|
@@ -761,14 +786,14 @@ class MongoRest {
|
|
|
761
786
|
|
|
762
787
|
const token = requestCtxStorage.get<{ value: string | null, decoded: Record<string, unknown> | null, provided: boolean, expired: boolean }>('token')
|
|
763
788
|
|
|
764
|
-
return await col.actions?.[action]({
|
|
789
|
+
return await col.actions?.[action]?.({
|
|
765
790
|
rest: this,
|
|
766
|
-
|
|
791
|
+
io,
|
|
767
792
|
data,
|
|
768
793
|
error: fn.error,
|
|
769
794
|
jwt: func.jwt,
|
|
770
|
-
token: token ?? { value: null, decoded: null },
|
|
771
|
-
})
|
|
795
|
+
token: token ?? { value: null, decoded: null, provided: false, expired: false },
|
|
796
|
+
} as any)
|
|
772
797
|
}
|
|
773
798
|
|
|
774
799
|
async runService<T = any>(service: string, action: string, data?: any): Promise<T> {
|
|
@@ -785,15 +810,14 @@ class MongoRest {
|
|
|
785
810
|
|
|
786
811
|
const token = requestCtxStorage.get<{ value: string | null, decoded: Record<string, unknown> | null, provided: boolean, expired: boolean }>('token')
|
|
787
812
|
|
|
788
|
-
return await serviceInstance.actions?.[action]({
|
|
813
|
+
return await serviceInstance.actions?.[action]?.({
|
|
789
814
|
data,
|
|
790
815
|
error: fn.error,
|
|
791
816
|
io,
|
|
792
817
|
jwt: func.jwt,
|
|
793
818
|
token: token ?? { value: null, decoded: null, provided: false, expired: false },
|
|
794
819
|
rest: this,
|
|
795
|
-
|
|
796
|
-
})
|
|
820
|
+
} as any)
|
|
797
821
|
}
|
|
798
822
|
|
|
799
823
|
async stats() {
|
|
@@ -910,8 +934,8 @@ class MongoRest {
|
|
|
910
934
|
|
|
911
935
|
try {
|
|
912
936
|
const result = await this.db.collection('_locks_').findOneAndUpdate(
|
|
913
|
-
{ _id: id, expiresAt: { $lt: now } },
|
|
914
|
-
{ $set: {
|
|
937
|
+
{ _id: id as any, expiresAt: { $lt: now } },
|
|
938
|
+
{ $set: { tid: this.tenant_id, name, acquiredAt: now, expiresAt } },
|
|
915
939
|
{ upsert: true, returnDocument: 'after' }
|
|
916
940
|
);
|
|
917
941
|
if (!result) {
|
package/index.ts
CHANGED
|
@@ -11,7 +11,7 @@ import * as crypto from "./utils/crypto";
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
//
|
|
14
|
+
// BentoCache-based caching (memory L1 + filesystem/Redis L2)
|
|
15
15
|
import { useMemoryCache, useFilesystemCache, useRedisCache } from "./utils/cache";
|
|
16
16
|
const cache = {
|
|
17
17
|
useMemoryCache,
|
package/lib/define.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import type { ServerConfig } from "../types/config";
|
|
2
|
-
import type { Collection } from "../types/collection";
|
|
2
|
+
import type { Collection, CollectionAction } from "../types/collection";
|
|
3
|
+
import type { CollectionHook } from "../types/hook";
|
|
3
4
|
import type { FileCollection } from "../types/file";
|
|
4
5
|
import type { Script } from "../types/scripts";
|
|
5
6
|
import type { Route } from "../types/route";
|
|
6
7
|
import type { Service } from "../types/service";
|
|
7
8
|
import type { WebSocketHandler } from "../types/websocket";
|
|
8
9
|
import type { TenantMiddlewareConfig, GlobalMiddlewareConfig } from "../types/middleware";
|
|
10
|
+
import type { McpTool, McpResource } from "../types/mcp";
|
|
9
11
|
import { file } from "bun";
|
|
10
12
|
function Server(config: ServerConfig) {
|
|
11
|
-
config.clusterMode = config?.clusterMode ?? true;
|
|
12
13
|
config.server = {
|
|
13
14
|
...config?.server,
|
|
14
15
|
}
|
|
@@ -32,6 +33,58 @@ function Collection(collection: Collection): Collection {
|
|
|
32
33
|
}
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Defines a custom action on a collection — usable in `define.Collection({ actions: { ... } })`.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* const processOrder = define.Action(async ({ rest, data, error }) => {
|
|
42
|
+
* const order = await rest.findOne("orders", data.orderId);
|
|
43
|
+
* if (!order) throw error("Order not found", { status: 404 });
|
|
44
|
+
* return { ok: true };
|
|
45
|
+
* });
|
|
46
|
+
*
|
|
47
|
+
* define.Collection({ slug: "orders", actions: { processOrder } })
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
function Action(action: CollectionAction): CollectionAction & { _isAction_: true } {
|
|
51
|
+
return Object.assign(action, { _isAction_: true }) as CollectionAction & { _isAction_: true };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Defines a collection hook — usable as `beforeOperation` / `afterOperation`.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* const softDelete = define.Hook(async ({ rest, action, meta }) => {
|
|
60
|
+
* if (action === 'deleteOne' && meta.id) {
|
|
61
|
+
* await rest.updateOne("items", meta.id, { $set: { deletedAt: new Date().toISOString() } });
|
|
62
|
+
* }
|
|
63
|
+
* });
|
|
64
|
+
*
|
|
65
|
+
* define.Collection({ slug: "items", hooks: { beforeOperation: softDelete } })
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
function Hook(hook: CollectionHook): CollectionHook & { _isHook_: true } {
|
|
69
|
+
return Object.assign(hook, { _isHook_: true }) as CollectionHook & { _isHook_: true };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function McpTool(tool: McpTool): McpTool {
|
|
73
|
+
return {
|
|
74
|
+
...tool,
|
|
75
|
+
enabled: tool.enabled ?? true,
|
|
76
|
+
_isMcpTool_: true,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function McpResource(resource: McpResource): McpResource {
|
|
81
|
+
return {
|
|
82
|
+
...resource,
|
|
83
|
+
enabled: resource.enabled ?? true,
|
|
84
|
+
_isMcpResource_: true,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
35
88
|
function FileCollection(collection: FileCollection): FileCollection {
|
|
36
89
|
return {
|
|
37
90
|
...collection,
|
|
@@ -103,6 +156,8 @@ export const define = {
|
|
|
103
156
|
Server: Server,
|
|
104
157
|
App: Server,
|
|
105
158
|
Collection,
|
|
159
|
+
Action,
|
|
160
|
+
Hook,
|
|
106
161
|
FileCollection,
|
|
107
162
|
Script,
|
|
108
163
|
Route,
|
|
@@ -110,5 +165,7 @@ export const define = {
|
|
|
110
165
|
Workflow,
|
|
111
166
|
TenantMiddleware,
|
|
112
167
|
Middleware,
|
|
113
|
-
WebSocket
|
|
168
|
+
WebSocket,
|
|
169
|
+
McpTool,
|
|
170
|
+
McpResource
|
|
114
171
|
}
|