@anteros/core 0.0.1-alpha.1
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 +143 -0
- package/database/collection.ts +160 -0
- package/database/decorator.ts +172 -0
- package/database/file.ts +93 -0
- package/database/mongodbadapter.ts +1128 -0
- package/database/rest.ts +14 -0
- package/database/schema.ts +160 -0
- package/database/tenant.ts +37 -0
- package/database/workflow.ts +384 -0
- package/index.ts +28 -0
- package/lib/asyncContextStorage.ts +68 -0
- package/lib/define.ts +114 -0
- package/lib/error.ts +21 -0
- package/lib/files.ts +459 -0
- package/lib/middleware.ts +66 -0
- package/lib/routes.ts +44 -0
- package/lib/scripts.ts +47 -0
- package/lib/services.ts +45 -0
- package/lib/sockets.ts +44 -0
- package/lib/workflow.ts +60 -0
- package/package.json +31 -0
- package/server/api.ts +789 -0
- package/server/boot.ts +101 -0
- package/server/config.ts +107 -0
- package/server/env.ts +16 -0
- package/server/hono.ts +176 -0
- package/server/io.ts +15 -0
- package/server/routes.ts +48 -0
- package/server/security.ts +138 -0
- package/tests/api.test.ts +281 -0
- package/tsconfig.json +36 -0
- package/types/activity.d.ts +45 -0
- package/types/api.d.ts +85 -0
- package/types/collection.d.ts +82 -0
- package/types/config.d.ts +55 -0
- package/types/field.d.ts +72 -0
- package/types/file.d.ts +120 -0
- package/types/hook.d.ts +30 -0
- package/types/middleware.d.ts +18 -0
- package/types/mongo.d.ts +61 -0
- package/types/options.d.ts +7 -0
- package/types/rest.d.ts +18 -0
- package/types/route.d.ts +19 -0
- package/types/schema.d.ts +0 -0
- package/types/scripts.d.ts +10 -0
- package/types/service.d.ts +37 -0
- package/types/task.d.ts +12 -0
- package/types/tenant.d.ts +16 -0
- package/types/token.d.ts +14 -0
- package/types/websocket.d.ts +15 -0
- package/types/workflow.d.ts +91 -0
- package/utils/cache.ts +96 -0
- package/utils/crypto.ts +226 -0
- package/utils/func.ts +1037 -0
- package/utils/index.ts +17 -0
|
@@ -0,0 +1,1128 @@
|
|
|
1
|
+
import type { Tenant } from "../types/tenant";
|
|
2
|
+
import type { MongoClientOptions, Db, ClientSession, UpdateOptions, UpdateFilter, Document, DeleteResult, ChangeStreamOptions, ChangeStream, ChangeStreamDocument, AnyBulkWriteOperation, BulkWriteOptions, BulkWriteResult, UpdateResult } from "mongodb";
|
|
3
|
+
import { MongoClient, ObjectId, AggregationCursor } from "mongodb";
|
|
4
|
+
import type { FindOptions, findOneOptions } from "../types/mongo";
|
|
5
|
+
import { sessionCtxStorage, asyncContextStorage, requestCtxStorage } from "../lib/asyncContextStorage";
|
|
6
|
+
import * as func from "../utils/func"
|
|
7
|
+
import { AppError, fn } from "../lib/error";
|
|
8
|
+
import { createWorkflow } from "./workflow";
|
|
9
|
+
import { getTenant } from "./tenant";
|
|
10
|
+
import { getCollection, getCollectionKeys } from "./collection"
|
|
11
|
+
import type { RestActions, BulkUpdateOperation, RestOptions } from "../types/rest";
|
|
12
|
+
import type Joi from "joi";
|
|
13
|
+
import type { Collection } from "../types/collection";
|
|
14
|
+
import { CheckBulkWriteOperations, CheckIfCollectionExists, CheckIfId, CheckInsertData, CheckIfArrayOfIds, CheckFilter } from "./decorator";
|
|
15
|
+
import type { ActionsApiList, updateApiOptions } from "../types/api";
|
|
16
|
+
import type { ActivityInput } from "../types/activity";
|
|
17
|
+
import * as os from 'node:os';
|
|
18
|
+
import { cfg } from '../server/config';
|
|
19
|
+
import { io } from '../server/io';
|
|
20
|
+
import type { Service } from '../types/service';
|
|
21
|
+
|
|
22
|
+
class MongoRest {
|
|
23
|
+
client!: MongoClient;
|
|
24
|
+
#isConnected: boolean = false;
|
|
25
|
+
#internal: boolean = true;
|
|
26
|
+
db!: Db;
|
|
27
|
+
tenant_id!: string;
|
|
28
|
+
#tenant!: Tenant;
|
|
29
|
+
session!: ClientSession | undefined;
|
|
30
|
+
#database?: {
|
|
31
|
+
uri: string;
|
|
32
|
+
options?: MongoClientOptions;
|
|
33
|
+
};
|
|
34
|
+
#useHook: boolean;
|
|
35
|
+
#useCustomApi: boolean;
|
|
36
|
+
constructor(options: RestOptions) {
|
|
37
|
+
this.#internal = options.internal ?? true;
|
|
38
|
+
this.#database = options.database;
|
|
39
|
+
this.#useHook = options.useHook ?? true;
|
|
40
|
+
this.#useCustomApi = options.useCustomApi ?? true;
|
|
41
|
+
if (options.session) {
|
|
42
|
+
this.session = options.session;
|
|
43
|
+
}
|
|
44
|
+
let tenant = getTenant(options?.tenant_id ?? '-')
|
|
45
|
+
if (options.tenant_id && tenant) {
|
|
46
|
+
this.#tenant = tenant;
|
|
47
|
+
this.tenant_id = tenant.id;
|
|
48
|
+
this.db = tenant.database.db as Db;
|
|
49
|
+
this.client = tenant.database.client as MongoClient;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
private _validate(options: { collection: string, action?: ActionsApiList, data?: any, update?: UpdateFilter<any> }) {
|
|
56
|
+
let col = getCollection(options.collection, this.#tenant.id)
|
|
57
|
+
let schema = col?._schema_
|
|
58
|
+
let partialSchema = col?._schemaPartial_
|
|
59
|
+
let validationResult: Joi.ValidationResult<any> | undefined = undefined
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if (!col) throw new AppError(`collection '${options?.collection || ''}' not found`, {
|
|
63
|
+
code: 'COLLECTION_NOT_FOUND',
|
|
64
|
+
status: 500
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if (!options.action) throw new AppError(`action '${options?.action || ''}' not found or required`, {
|
|
69
|
+
code: 'ACTION_REQUIRED',
|
|
70
|
+
status: 500
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
//let unauthorizedKeys = func.unauthorizedKeys(options.data, getCollectionKeys(col))
|
|
75
|
+
/* if (unauthorizedKeys.length > 0) {
|
|
76
|
+
throw new AppError(`Unauthorized keys in "${options.collection}" : ${unauthorizedKeys.join(', ')}`, {
|
|
77
|
+
code: 'UNAUTHORIZED_KEYS',
|
|
78
|
+
status: 400
|
|
79
|
+
})
|
|
80
|
+
} */
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
if (schema && partialSchema) {
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if (options?.data) {
|
|
88
|
+
if (options.action == 'insertMany') {
|
|
89
|
+
options.data.map(d => {
|
|
90
|
+
validationResult = schema.validate(d, {
|
|
91
|
+
allowUnknown: false,
|
|
92
|
+
})
|
|
93
|
+
if (validationResult && validationResult.error) {
|
|
94
|
+
throw new AppError(validationResult.error.message, {
|
|
95
|
+
code: 'VALIDATION_ERROR',
|
|
96
|
+
status: 400
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (options.action == 'insertOne') {
|
|
103
|
+
validationResult = schema.validate(options.data, {
|
|
104
|
+
allowUnknown: false,
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if (options?.update) {
|
|
111
|
+
if (options.action == 'insertOne' && options.update?.$set) {
|
|
112
|
+
validationResult = partialSchema.validate(options.update?.$set, {
|
|
113
|
+
allowUnknown: false,
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (options.action == 'insertMany' && options.update?.$set) {
|
|
118
|
+
validationResult = partialSchema.validate(options.update?.$set, {
|
|
119
|
+
allowUnknown: false,
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (options.action == "findOneAndUpdate" && options.update.$set) {
|
|
124
|
+
validationResult = partialSchema.validate(options.update.$set, {
|
|
125
|
+
allowUnknown: false,
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (options.action == "findOneAndUpdate" && options.update.$setOnInsert) {
|
|
130
|
+
validationResult = partialSchema.validate(options.update.$setOnInsert, {
|
|
131
|
+
allowUnknown: false,
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (validationResult?.error) {
|
|
137
|
+
throw new AppError(validationResult.error.message, {
|
|
138
|
+
code: 'VALIDATION_ERROR',
|
|
139
|
+
status: 400
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private async #logActivity(data: {
|
|
150
|
+
action: string,
|
|
151
|
+
collection: string,
|
|
152
|
+
input?: any,
|
|
153
|
+
result?: any,
|
|
154
|
+
error?: { message: string, code: string },
|
|
155
|
+
duration: number,
|
|
156
|
+
}) {
|
|
157
|
+
const ctxMeta = requestCtxStorage.get<Record<string, any>>('meta')
|
|
158
|
+
const traceCtx = requestCtxStorage.get<{ id: string; comment?: string; tag?: string; version?: string }>('trace')
|
|
159
|
+
const token = requestCtxStorage.get<{ value: string | null, decoded: Record<string, unknown> | null, provided: boolean, expired: boolean }>('token')
|
|
160
|
+
|
|
161
|
+
const traceId = traceCtx?.id ?? crypto.randomUUID()
|
|
162
|
+
const trace: ActivityInput['trace'] = {
|
|
163
|
+
id: traceId,
|
|
164
|
+
comment: traceCtx?.comment,
|
|
165
|
+
tag: traceCtx?.tag,
|
|
166
|
+
version: traceCtx?.version,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const { request: requestMeta, ...restMeta } = ctxMeta ?? {}
|
|
170
|
+
|
|
171
|
+
const meta: Record<string, any> = {
|
|
172
|
+
...restMeta,
|
|
173
|
+
platform: restMeta?.platform ?? os.platform(),
|
|
174
|
+
core_version: cfg.version ?? restMeta?.core_version,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Determine collection type
|
|
178
|
+
const colMeta = cfg.collections?.find(c => c.slug === data.collection && c._tenant_ === this.tenant_id)
|
|
179
|
+
?? cfg.fileCollections?.find(c => c.slug === data.collection && c._tenant_ === this.tenant_id)
|
|
180
|
+
const collectionType = colMeta && 'type' in colMeta ? colMeta.type : colMeta ? 'file' : undefined
|
|
181
|
+
|
|
182
|
+
const activity: ActivityInput = {
|
|
183
|
+
internal: requestCtxStorage.get<boolean>('internal') ?? this.#internal,
|
|
184
|
+
trace,
|
|
185
|
+
request: requestMeta,
|
|
186
|
+
meta,
|
|
187
|
+
operation: {
|
|
188
|
+
tenant: this.tenant_id,
|
|
189
|
+
action: data.action,
|
|
190
|
+
collection: data.collection,
|
|
191
|
+
collectionType,
|
|
192
|
+
status: data.error ? 'error' : 'success',
|
|
193
|
+
input: data.input,
|
|
194
|
+
result: data.result,
|
|
195
|
+
error: data.error ?? null,
|
|
196
|
+
duration: data.duration,
|
|
197
|
+
transaction: this.session?.inTransaction?.() ?? false,
|
|
198
|
+
token: token ? { decoded: token.decoded, value: null, provided: token.provided ?? true, expired: token.expired ?? false } : undefined,
|
|
199
|
+
},
|
|
200
|
+
ts: new Date(),
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
await this.audit.addActivities([activity])
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private async #executeWithAudit<T>(
|
|
207
|
+
action: string,
|
|
208
|
+
collection: string,
|
|
209
|
+
input: any,
|
|
210
|
+
fn: () => Promise<T>
|
|
211
|
+
): Promise<T> {
|
|
212
|
+
const start = Date.now()
|
|
213
|
+
try {
|
|
214
|
+
const result = await fn()
|
|
215
|
+
const duration = Date.now() - start
|
|
216
|
+
this.#logActivity({ action, collection, input, result, duration })
|
|
217
|
+
return result
|
|
218
|
+
} catch (err: any) {
|
|
219
|
+
const duration = Date.now() - start
|
|
220
|
+
this.#logActivity({ action, collection, input, error: { message: err?.message, code: err?.code || 'INTERNAL_API_ERROR' }, duration })
|
|
221
|
+
throw err instanceof AppError ? err : new AppError(err?.message || 'Internal server error', { code: 'INTERNAL_API_ERROR', status: 500 })
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async connect(options?: {
|
|
226
|
+
uri?: string;
|
|
227
|
+
options?: MongoClientOptions;
|
|
228
|
+
}): Promise<{ client: MongoClient, db: Db }> {
|
|
229
|
+
try {
|
|
230
|
+
|
|
231
|
+
if (!this.#isConnected && this.#database) {
|
|
232
|
+
this.client = new MongoClient(options?.uri ?? this.#database.uri, {
|
|
233
|
+
...options?.options || this.#database.options,
|
|
234
|
+
})
|
|
235
|
+
await this.client.connect()
|
|
236
|
+
this.db = this.client.db();
|
|
237
|
+
this.#isConnected = true;
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
client: this.client,
|
|
241
|
+
db: this.db,
|
|
242
|
+
}
|
|
243
|
+
} catch (err: any) {
|
|
244
|
+
console.error('MongoDB connection failed', err?.message);
|
|
245
|
+
throw new Error(err?.message);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
@CheckIfCollectionExists()
|
|
250
|
+
async watch(collection: string, pipeline: any[], options: ChangeStreamOptions): Promise<ChangeStream> {
|
|
251
|
+
const col = getCollection(collection, this.#tenant.id) as Collection
|
|
252
|
+
const isSafe = func.isSafeAggregatePipeline(pipeline);
|
|
253
|
+
if (!isSafe.isSafe) {
|
|
254
|
+
throw isSafe.error;
|
|
255
|
+
}
|
|
256
|
+
pipeline = await func.buildInput(pipeline)
|
|
257
|
+
pipeline = func.toBson(pipeline, { col })
|
|
258
|
+
return await this.db.collection(collection).watch(pipeline, {
|
|
259
|
+
allowDiskUse: true,
|
|
260
|
+
session: this.session,
|
|
261
|
+
...options
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
@CheckIfCollectionExists()
|
|
266
|
+
async aggregate(collection: string, pipeline: any[]): Promise<Document[]> {
|
|
267
|
+
const action = 'aggregate' as ActionsApiList
|
|
268
|
+
try {
|
|
269
|
+
|
|
270
|
+
const col = getCollection(collection, this.#tenant.id) as Collection
|
|
271
|
+
const isSafe = func.isSafeAggregatePipeline(pipeline);
|
|
272
|
+
if (!isSafe.isSafe) {
|
|
273
|
+
throw isSafe.error;
|
|
274
|
+
}
|
|
275
|
+
pipeline = func.toBson(pipeline, { col })
|
|
276
|
+
if (col.hooks?.beforeOperation) {
|
|
277
|
+
await col.hooks.beforeOperation({
|
|
278
|
+
rest: this,
|
|
279
|
+
io,
|
|
280
|
+
action: action,
|
|
281
|
+
meta: {
|
|
282
|
+
action,
|
|
283
|
+
collection: collection,
|
|
284
|
+
pipeline: pipeline,
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
let result = await this.db.collection(collection).aggregate(pipeline, {
|
|
290
|
+
session: this.session,
|
|
291
|
+
allowDiskUse: true,
|
|
292
|
+
}).toArray()
|
|
293
|
+
result = func.toJson(result)
|
|
294
|
+
|
|
295
|
+
await col.hooks?.afterOperation?.({
|
|
296
|
+
rest: this,
|
|
297
|
+
io,
|
|
298
|
+
action: action,
|
|
299
|
+
meta: {
|
|
300
|
+
action,
|
|
301
|
+
collection: collection,
|
|
302
|
+
pipeline: pipeline,
|
|
303
|
+
result: result,
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
return result as any[]
|
|
308
|
+
} catch (err: any) {
|
|
309
|
+
throw err instanceof AppError ? err : new AppError(err?.message || 'Internal server error', { code: 'INTERNAL_API_ERROR', status: 500 })
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@CheckIfCollectionExists()
|
|
315
|
+
async find(collection: string, params: FindOptions = {}, options = {}): Promise<Document[]> {
|
|
316
|
+
const action = 'find' as ActionsApiList
|
|
317
|
+
try {
|
|
318
|
+
const col = getCollection(collection, this.#tenant.id) as Collection
|
|
319
|
+
let pipeline = func.buildPipeline(params, { col: col })
|
|
320
|
+
pipeline = await func.buildInput(pipeline, { rest: this })
|
|
321
|
+
pipeline = func.toBson(pipeline, { col })
|
|
322
|
+
|
|
323
|
+
if (col.hooks?.beforeOperation) {
|
|
324
|
+
await col.hooks.beforeOperation({
|
|
325
|
+
rest: this,
|
|
326
|
+
io,
|
|
327
|
+
action: action,
|
|
328
|
+
meta: {
|
|
329
|
+
action: action,
|
|
330
|
+
collection: collection,
|
|
331
|
+
params: func.clone(params),
|
|
332
|
+
options: func.clone(options),
|
|
333
|
+
}
|
|
334
|
+
})
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
let result = await this.db.collection(collection).aggregate(pipeline, {
|
|
338
|
+
session: this.session,
|
|
339
|
+
allowDiskUse: true
|
|
340
|
+
}).toArray()
|
|
341
|
+
result = func.toJson(result)
|
|
342
|
+
|
|
343
|
+
if (col.hooks?.afterOperation) {
|
|
344
|
+
await col.hooks.afterOperation({
|
|
345
|
+
rest: this,
|
|
346
|
+
io,
|
|
347
|
+
action: action,
|
|
348
|
+
meta: {
|
|
349
|
+
action: action,
|
|
350
|
+
collection: collection,
|
|
351
|
+
params: func.clone(params),
|
|
352
|
+
options: func.clone(options),
|
|
353
|
+
result: func.clone(result),
|
|
354
|
+
}
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return result as any[]
|
|
359
|
+
} catch (err: any) {
|
|
360
|
+
throw err instanceof AppError ? err : new AppError(err?.message || 'Internal server error', { code: 'INTERNAL_API_ERROR', status: 500 })
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
@CheckIfCollectionExists()
|
|
365
|
+
async findOne(collection: string, _id: string, params?: findOneOptions): Promise<any> {
|
|
366
|
+
const action = 'findOne' as ActionsApiList
|
|
367
|
+
try {
|
|
368
|
+
const col = getCollection(collection, this.#tenant.id) as Collection
|
|
369
|
+
let pipeline = func.buildPipeline({
|
|
370
|
+
...params,
|
|
371
|
+
$match: { _id },
|
|
372
|
+
$limit: 1
|
|
373
|
+
}, { col: col })
|
|
374
|
+
pipeline = await func.buildInput(pipeline, { rest: this })
|
|
375
|
+
pipeline = func.toBson(pipeline, { col })
|
|
376
|
+
|
|
377
|
+
if (col.hooks?.beforeOperation) {
|
|
378
|
+
await col.hooks.beforeOperation({
|
|
379
|
+
rest: this,
|
|
380
|
+
io,
|
|
381
|
+
action: action,
|
|
382
|
+
meta: {
|
|
383
|
+
action: action,
|
|
384
|
+
collection: collection,
|
|
385
|
+
params: func.clone(params),
|
|
386
|
+
id: _id,
|
|
387
|
+
}
|
|
388
|
+
})
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
let result = (await this.db.collection(collection).aggregate(pipeline, {
|
|
392
|
+
session: this.session,
|
|
393
|
+
allowDiskUse: true
|
|
394
|
+
}).toArray()).at(0) ?? null
|
|
395
|
+
result = func.toJson(result)
|
|
396
|
+
|
|
397
|
+
if (col.hooks?.afterOperation) {
|
|
398
|
+
await col.hooks.afterOperation({
|
|
399
|
+
rest: this,
|
|
400
|
+
io,
|
|
401
|
+
action: action,
|
|
402
|
+
meta: {
|
|
403
|
+
action: action,
|
|
404
|
+
collection: collection,
|
|
405
|
+
result: func.clone(result),
|
|
406
|
+
params: func.clone(params),
|
|
407
|
+
id: _id,
|
|
408
|
+
}
|
|
409
|
+
})
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return result
|
|
413
|
+
} catch (err: any) {
|
|
414
|
+
throw err instanceof AppError ? err : new AppError(err?.message || 'Internal server error', { code: 'INTERNAL_API_ERROR', status: 500 })
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
@CheckIfCollectionExists()
|
|
419
|
+
@CheckInsertData('insertOne')
|
|
420
|
+
async insertOne<T>(collection: string, data: T): Promise<T & { _id: string }> {
|
|
421
|
+
const action = 'insertOne' as ActionsApiList
|
|
422
|
+
return this.#executeWithAudit(action, collection, { data }, async () => {
|
|
423
|
+
const col = getCollection(collection, this.#tenant.id) as Collection
|
|
424
|
+
|
|
425
|
+
data = await func.buildInput(data as T, {
|
|
426
|
+
action: action, col: col, rest: this
|
|
427
|
+
})
|
|
428
|
+
this._validate({ collection, action: action, data: data })
|
|
429
|
+
|
|
430
|
+
if (col.hooks?.beforeOperation) {
|
|
431
|
+
await col.hooks.beforeOperation({
|
|
432
|
+
rest: this,
|
|
433
|
+
io,
|
|
434
|
+
action,
|
|
435
|
+
meta: {
|
|
436
|
+
action,
|
|
437
|
+
collection,
|
|
438
|
+
data: func.clone(data),
|
|
439
|
+
}
|
|
440
|
+
})
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
await this.db.collection(collection).insertOne(func.toBson(data, { col }) as any, {
|
|
444
|
+
session: this.session,
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
const result: T & { _id: string } = func.toJson(data as T & { _id: string })
|
|
448
|
+
if (col?.hooks?.afterOperation) {
|
|
449
|
+
await col.hooks.afterOperation({
|
|
450
|
+
rest: this,
|
|
451
|
+
io,
|
|
452
|
+
action: action,
|
|
453
|
+
meta: {
|
|
454
|
+
action: action,
|
|
455
|
+
collection: collection,
|
|
456
|
+
data: func.clone(data),
|
|
457
|
+
result: func.clone(result),
|
|
458
|
+
}
|
|
459
|
+
})
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return result
|
|
463
|
+
})
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
@CheckIfCollectionExists()
|
|
467
|
+
@CheckInsertData('insertMany')
|
|
468
|
+
async insertMany<T>(collection: string, data: T[]): Promise<(T & { _id: string })[]> {
|
|
469
|
+
const action = 'insertMany' as ActionsApiList
|
|
470
|
+
return this.#executeWithAudit(action, collection, { count: data.length }, async () => {
|
|
471
|
+
const col = getCollection(collection, this.#tenant.id) as Collection
|
|
472
|
+
let dataInput: T[] = []
|
|
473
|
+
|
|
474
|
+
for (let d of data) {
|
|
475
|
+
d = await func.buildInput(d as T, {
|
|
476
|
+
action: action,
|
|
477
|
+
col,
|
|
478
|
+
rest: this
|
|
479
|
+
})
|
|
480
|
+
dataInput.push(d)
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
this._validate({ collection, action: action, data: dataInput })
|
|
484
|
+
|
|
485
|
+
if (col.hooks?.beforeOperation) {
|
|
486
|
+
await col.hooks.beforeOperation({
|
|
487
|
+
rest: this,
|
|
488
|
+
io,
|
|
489
|
+
action: action,
|
|
490
|
+
meta: {
|
|
491
|
+
action: action,
|
|
492
|
+
collection: collection,
|
|
493
|
+
data: func.clone(dataInput),
|
|
494
|
+
}
|
|
495
|
+
})
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
await this.db.collection(collection).insertMany(func.toBson(dataInput, { col }) as any, {
|
|
499
|
+
session: this.session,
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
const result: (T & { _id: string })[] = func.toJson(dataInput as (T & { _id: string })[])
|
|
503
|
+
|
|
504
|
+
if (col.hooks?.afterOperation) {
|
|
505
|
+
await col.hooks.afterOperation({
|
|
506
|
+
rest: this,
|
|
507
|
+
io,
|
|
508
|
+
action: action,
|
|
509
|
+
meta: {
|
|
510
|
+
action: action,
|
|
511
|
+
collection: collection,
|
|
512
|
+
result: func.clone(result),
|
|
513
|
+
}
|
|
514
|
+
})
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return result as (T & { _id: string })[]
|
|
518
|
+
})
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
@CheckIfCollectionExists()
|
|
522
|
+
@CheckIfId('updateOne')
|
|
523
|
+
async updateOne(collection: string, _id: string, update: UpdateFilter<any>) {
|
|
524
|
+
const action = 'updateOne' as ActionsApiList
|
|
525
|
+
return this.#executeWithAudit(action, collection, { id: _id, update }, async () => {
|
|
526
|
+
const col = getCollection(collection, this.#tenant.id) as Collection
|
|
527
|
+
|
|
528
|
+
if (update.$set) {
|
|
529
|
+
update.$set = await func.buildInput(update.$set, { action: action, col: col })
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
update = await func.buildInput(update)
|
|
533
|
+
this._validate({ collection, action: action, update: update })
|
|
534
|
+
update = func.toBson(update, { col })
|
|
535
|
+
|
|
536
|
+
if (col.hooks?.beforeOperation) {
|
|
537
|
+
await col.hooks.beforeOperation({
|
|
538
|
+
rest: this,
|
|
539
|
+
io,
|
|
540
|
+
action: action,
|
|
541
|
+
meta: {
|
|
542
|
+
action: action,
|
|
543
|
+
update: func.clone(update),
|
|
544
|
+
collection: collection,
|
|
545
|
+
id: _id,
|
|
546
|
+
}
|
|
547
|
+
})
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const data = await this.db.collection(collection).findOneAndUpdate(
|
|
551
|
+
{ _id: new ObjectId(_id) },
|
|
552
|
+
{ ...(update as any) },
|
|
553
|
+
{ session: this.session, returnDocument: 'after' }
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
const result = func.toJson(data)
|
|
557
|
+
|
|
558
|
+
if (col.hooks?.afterOperation) {
|
|
559
|
+
await col.hooks.afterOperation({
|
|
560
|
+
rest: this,
|
|
561
|
+
io,
|
|
562
|
+
action: action,
|
|
563
|
+
meta: {
|
|
564
|
+
action: action,
|
|
565
|
+
collection: collection,
|
|
566
|
+
update: func.clone(update),
|
|
567
|
+
id: _id,
|
|
568
|
+
result: func.clone(result),
|
|
569
|
+
}
|
|
570
|
+
})
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return result
|
|
574
|
+
})
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
@CheckIfCollectionExists()
|
|
578
|
+
@CheckFilter()
|
|
579
|
+
async findOneAndUpdate(collection: string, filter: Document, update: UpdateFilter<any>, options?: updateApiOptions): Promise<Document | null> {
|
|
580
|
+
const action = 'findOneAndUpdate' as ActionsApiList
|
|
581
|
+
return this.#executeWithAudit(action, collection, { filter, update, options }, async () => {
|
|
582
|
+
const col = getCollection(collection, this.#tenant.id) as Collection
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
if (update.$set) {
|
|
587
|
+
update.$set = await func.buildInput(update.$set, { action: action, col: col })
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
update.$setOnInsert = await func.buildInput(update.$setOnInsert || {}, {
|
|
591
|
+
action: 'insertOne',
|
|
592
|
+
col: col
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
update.$set = func.omit(update.$set, ['updatedAt'])
|
|
596
|
+
update.$setOnInsert = func.omit(update.$setOnInsert, ['updatedAt'])
|
|
597
|
+
|
|
598
|
+
update.$currentDate = {
|
|
599
|
+
updatedAt: true,
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
// clean Deep on filter
|
|
604
|
+
if (options?.cleanDeep) {
|
|
605
|
+
filter = func.cleanDeep(filter)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// if filter is empty, throw error
|
|
609
|
+
if (func.isEmpty(filter)) {
|
|
610
|
+
throw new AppError('Filter is empty', { code: 'FILTER_EMPTY', status: 400 })
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
this._validate({ collection, action: action, update: update.$set })
|
|
615
|
+
this._validate({ collection, action: action, update: update.$setOnInsert })
|
|
616
|
+
update = func.toBson(update, { col })
|
|
617
|
+
filter = func.toBson(filter, { col })
|
|
618
|
+
|
|
619
|
+
if (col.hooks?.beforeOperation) {
|
|
620
|
+
await col.hooks.beforeOperation({
|
|
621
|
+
rest: this,
|
|
622
|
+
io,
|
|
623
|
+
action: action,
|
|
624
|
+
meta: {
|
|
625
|
+
action: action,
|
|
626
|
+
collection: collection,
|
|
627
|
+
filter: func.clone(filter),
|
|
628
|
+
update: func.clone(update),
|
|
629
|
+
}
|
|
630
|
+
})
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
const data = await this.db.collection(collection).findOneAndUpdate(
|
|
638
|
+
filter,
|
|
639
|
+
{
|
|
640
|
+
...(update as any),
|
|
641
|
+
|
|
642
|
+
},
|
|
643
|
+
{ session: this.session, returnDocument: 'after', upsert: options?.upsert ?? false }
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
const result = func.toJson(data)
|
|
647
|
+
|
|
648
|
+
if (col.hooks?.afterOperation) {
|
|
649
|
+
await col.hooks.afterOperation({
|
|
650
|
+
rest: this,
|
|
651
|
+
io,
|
|
652
|
+
action: action,
|
|
653
|
+
meta: {
|
|
654
|
+
action: action,
|
|
655
|
+
collection: collection,
|
|
656
|
+
filter: func.clone(filter),
|
|
657
|
+
update: func.clone(update),
|
|
658
|
+
result: func.clone(result),
|
|
659
|
+
}
|
|
660
|
+
})
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return result as Document | null
|
|
664
|
+
})
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
@CheckIfCollectionExists()
|
|
668
|
+
@CheckIfArrayOfIds('updateMany')
|
|
669
|
+
async updateMany(collection: string, _ids: string[], update: UpdateFilter<any>): Promise<UpdateResult> {
|
|
670
|
+
const action = 'updateMany' as ActionsApiList
|
|
671
|
+
return this.#executeWithAudit(action, collection, { ids: _ids, update }, async () => {
|
|
672
|
+
const col = getCollection(collection, this.#tenant.id) as Collection
|
|
673
|
+
|
|
674
|
+
if (update.$set) {
|
|
675
|
+
update.$set = await func.buildInput(update.$set, { action: action, col: col })
|
|
676
|
+
}
|
|
677
|
+
update = await func.buildInput(update)
|
|
678
|
+
this._validate({ collection, action: action, update: update })
|
|
679
|
+
update = func.toBson(update, { col })
|
|
680
|
+
|
|
681
|
+
if (col.hooks?.beforeOperation) {
|
|
682
|
+
await col.hooks.beforeOperation({
|
|
683
|
+
rest: this,
|
|
684
|
+
io,
|
|
685
|
+
action: action,
|
|
686
|
+
meta: {
|
|
687
|
+
action: action,
|
|
688
|
+
collection: collection,
|
|
689
|
+
update: func.clone(update),
|
|
690
|
+
ids: _ids,
|
|
691
|
+
}
|
|
692
|
+
})
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const result = await this.db.collection(collection).updateMany(
|
|
696
|
+
{ _id: { $in: _ids.map(id => new ObjectId(id)) } },
|
|
697
|
+
{ ...(update as any) },
|
|
698
|
+
{ session: this.session }
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
if (col.hooks?.afterOperation) {
|
|
702
|
+
await col.hooks.afterOperation({
|
|
703
|
+
rest: this,
|
|
704
|
+
io,
|
|
705
|
+
action: action,
|
|
706
|
+
meta: {
|
|
707
|
+
action: action,
|
|
708
|
+
collection: collection,
|
|
709
|
+
update: func.clone(update),
|
|
710
|
+
ids: _ids,
|
|
711
|
+
result: func.clone(result),
|
|
712
|
+
}
|
|
713
|
+
})
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
return result as UpdateResult
|
|
717
|
+
})
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
@CheckIfCollectionExists()
|
|
721
|
+
@CheckIfId('deleteOne')
|
|
722
|
+
async deleteOne(collection: string, _id: string) {
|
|
723
|
+
const action = 'deleteOne' as ActionsApiList
|
|
724
|
+
return this.#executeWithAudit(action, collection, { id: _id }, async () => {
|
|
725
|
+
const col = getCollection(collection, this.#tenant.id) as Collection
|
|
726
|
+
|
|
727
|
+
if (col.hooks?.beforeOperation) {
|
|
728
|
+
await col.hooks.beforeOperation({
|
|
729
|
+
rest: this,
|
|
730
|
+
io,
|
|
731
|
+
action: action,
|
|
732
|
+
meta: { action: action, collection: collection, id: _id }
|
|
733
|
+
})
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
let result = await this.db.collection(collection).findOneAndDelete(
|
|
737
|
+
{ _id: new ObjectId(_id) },
|
|
738
|
+
{ session: this.session }
|
|
739
|
+
)
|
|
740
|
+
result = func.toJson(result)
|
|
741
|
+
|
|
742
|
+
if (col.hooks?.afterOperation) {
|
|
743
|
+
await col.hooks.afterOperation({
|
|
744
|
+
rest: this,
|
|
745
|
+
io,
|
|
746
|
+
action: action,
|
|
747
|
+
meta: { action: action, collection: collection, id: _id, result: func.clone(result) }
|
|
748
|
+
})
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
return result
|
|
752
|
+
})
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
|
|
756
|
+
@CheckIfCollectionExists()
|
|
757
|
+
@CheckIfArrayOfIds('deleteMany')
|
|
758
|
+
async deleteMany(collection: string, _ids: string[]): Promise<DeleteResult> {
|
|
759
|
+
const action = 'deleteMany' as ActionsApiList
|
|
760
|
+
return this.#executeWithAudit(action, collection, { ids: _ids }, async () => {
|
|
761
|
+
const col = getCollection(collection, this.#tenant.id) as Collection
|
|
762
|
+
|
|
763
|
+
if (col.hooks?.beforeOperation) {
|
|
764
|
+
await col.hooks.beforeOperation({
|
|
765
|
+
rest: this,
|
|
766
|
+
io,
|
|
767
|
+
action: action,
|
|
768
|
+
meta: { action: action, collection: collection, ids: _ids }
|
|
769
|
+
})
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
let result: DeleteResult = await this.db.collection(collection).deleteMany(
|
|
773
|
+
{ _id: { $in: _ids.map(id => new ObjectId(id)) } },
|
|
774
|
+
{ session: this.session }
|
|
775
|
+
)
|
|
776
|
+
result = func.toJson(result) as DeleteResult
|
|
777
|
+
|
|
778
|
+
if (col.hooks?.afterOperation) {
|
|
779
|
+
await col.hooks.afterOperation({
|
|
780
|
+
rest: this,
|
|
781
|
+
io,
|
|
782
|
+
action: action,
|
|
783
|
+
meta: { action: action, collection: collection, ids: _ids, result: func.clone(result) }
|
|
784
|
+
})
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
return result
|
|
788
|
+
})
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
@CheckIfCollectionExists()
|
|
793
|
+
@CheckBulkWriteOperations()
|
|
794
|
+
async bulkWrite(collection: string, operations: AnyBulkWriteOperation[], options?: BulkWriteOptions): Promise<BulkWriteResult> {
|
|
795
|
+
const action = 'bulkWrite' as ActionsApiList
|
|
796
|
+
return this.#executeWithAudit(action, collection, { count: operations.length }, async () => {
|
|
797
|
+
for (let operation of operations) {
|
|
798
|
+
const col = getCollection(collection, this.#tenant.id) as Collection
|
|
799
|
+
|
|
800
|
+
if ('insertOne' in operation) {
|
|
801
|
+
operation.insertOne.document = func.toBson(
|
|
802
|
+
await func.buildInput(operation.insertOne.document, { action: 'insertOne', col }),
|
|
803
|
+
{ col },
|
|
804
|
+
)
|
|
805
|
+
}
|
|
806
|
+
if ('updateOne' in operation) {
|
|
807
|
+
operation.updateOne.filter = func.toBson(
|
|
808
|
+
await func.buildInput(operation.updateOne.filter, { action: 'updateOne', col }),
|
|
809
|
+
{ col },
|
|
810
|
+
)
|
|
811
|
+
const updateOneDoc = operation.updateOne.update as UpdateFilter<Document>
|
|
812
|
+
if (updateOneDoc.$set) {
|
|
813
|
+
updateOneDoc.$set = await func.buildInput(updateOneDoc.$set, { action: 'updateOne', col })
|
|
814
|
+
}
|
|
815
|
+
operation.updateOne.update = func.toBson(operation.updateOne.update, { col })
|
|
816
|
+
}
|
|
817
|
+
if ('updateMany' in operation) {
|
|
818
|
+
operation.updateMany.filter = func.toBson(
|
|
819
|
+
await func.buildInput(operation.updateMany.filter, { action: 'updateMany', col }),
|
|
820
|
+
{ col },
|
|
821
|
+
)
|
|
822
|
+
const updateManyDoc = operation.updateMany.update as UpdateFilter<Document>
|
|
823
|
+
if (updateManyDoc.$set) {
|
|
824
|
+
updateManyDoc.$set = await func.buildInput(updateManyDoc.$set, { action: 'updateMany', col })
|
|
825
|
+
}
|
|
826
|
+
operation.updateMany.update = func.toBson(operation.updateMany.update, { col })
|
|
827
|
+
}
|
|
828
|
+
if ('replaceOne' in operation) {
|
|
829
|
+
operation.replaceOne.replacement = func.toBson(
|
|
830
|
+
await func.buildInput(operation.replaceOne.replacement, { action: 'replaceOne', col }),
|
|
831
|
+
{ col },
|
|
832
|
+
)
|
|
833
|
+
}
|
|
834
|
+
if ('deleteOne' in operation) {
|
|
835
|
+
operation.deleteOne.filter = func.toBson(
|
|
836
|
+
await func.buildInput(operation.deleteOne.filter, { action: 'deleteOne', col }),
|
|
837
|
+
{ col },
|
|
838
|
+
)
|
|
839
|
+
}
|
|
840
|
+
if ('deleteMany' in operation) {
|
|
841
|
+
operation.deleteMany.filter = func.toBson(
|
|
842
|
+
await func.buildInput(operation.deleteMany.filter, { action: 'deleteMany', col }),
|
|
843
|
+
{ col },
|
|
844
|
+
)
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const result = await this.db.collection(collection).bulkWrite(operations, {
|
|
849
|
+
session: this.session,
|
|
850
|
+
...options,
|
|
851
|
+
})
|
|
852
|
+
|
|
853
|
+
return result
|
|
854
|
+
})
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
@CheckIfCollectionExists()
|
|
859
|
+
async bulkUpdate(collection: string, operations: BulkUpdateOperation[], options?: BulkWriteOptions): Promise<BulkWriteResult> {
|
|
860
|
+
const action = 'bulkUpdate' as ActionsApiList
|
|
861
|
+
return this.#executeWithAudit(action, collection, { count: operations.length }, async () => {
|
|
862
|
+
const col = getCollection(collection, this.#tenant.id) as Collection
|
|
863
|
+
|
|
864
|
+
for (const operation of operations) {
|
|
865
|
+
if ('updateOne' in operation) {
|
|
866
|
+
const update = operation.updateOne.update as UpdateFilter<Document>
|
|
867
|
+
if (update.$set) {
|
|
868
|
+
update.$set = await func.buildInput(update.$set, { action: 'updateOne', col })
|
|
869
|
+
}
|
|
870
|
+
operation.updateOne.update = func.toBson(operation.updateOne.update, { col })
|
|
871
|
+
}
|
|
872
|
+
if ('updateMany' in operation) {
|
|
873
|
+
const update = operation.updateMany.update as UpdateFilter<Document>
|
|
874
|
+
if (update.$set) {
|
|
875
|
+
update.$set = await func.buildInput(update.$set, { action: 'updateMany', col })
|
|
876
|
+
}
|
|
877
|
+
operation.updateMany.update = func.toBson(operation.updateMany.update, { col })
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const result = await this.db.collection(collection).bulkWrite(operations, {
|
|
882
|
+
session: this.session,
|
|
883
|
+
...options,
|
|
884
|
+
})
|
|
885
|
+
|
|
886
|
+
return result
|
|
887
|
+
})
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
|
|
892
|
+
@CheckIfCollectionExists()
|
|
893
|
+
async countDocuments(collection: string, query: any = {}): Promise<number> {
|
|
894
|
+
const action = 'countDocuments' as ActionsApiList
|
|
895
|
+
return this.#executeWithAudit(action, collection, { query }, async () => {
|
|
896
|
+
return await this.db.collection(collection).countDocuments()
|
|
897
|
+
})
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
@CheckIfCollectionExists()
|
|
901
|
+
async dropCollection(collection: string) {
|
|
902
|
+
const action = 'dropCollection' as ActionsApiList
|
|
903
|
+
return this.#executeWithAudit(action, collection, undefined, async () => {
|
|
904
|
+
return await this.db.collection(collection).drop()
|
|
905
|
+
})
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
@CheckIfCollectionExists()
|
|
909
|
+
async dropIndex(collection: string, index: string) {
|
|
910
|
+
const action = 'dropIndex' as ActionsApiList
|
|
911
|
+
return this.#executeWithAudit(action, collection, { index }, async () => {
|
|
912
|
+
return await this.db.collection(collection).dropIndex(index)
|
|
913
|
+
})
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
@CheckIfCollectionExists()
|
|
917
|
+
async dropIndexes(collection: string) {
|
|
918
|
+
const action = 'dropIndexes' as ActionsApiList
|
|
919
|
+
return this.#executeWithAudit(action, collection, undefined, async () => {
|
|
920
|
+
return await this.db.collection(collection).dropIndexes()
|
|
921
|
+
})
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
async runAction<T = any>(collection: string, action: string, data?: any): Promise<T> {
|
|
925
|
+
const col = getCollection(collection, this.#tenant.id)
|
|
926
|
+
if (!col) {
|
|
927
|
+
throw new AppError(`Collection '${collection}' not found`, { code: 'COLLECTION_NOT_FOUND', status: 500 })
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
if (!Object.hasOwn(col?.actions ?? {}, action)) {
|
|
931
|
+
throw new AppError(`Action '${action}' not found on collection '${collection}'`, { code: 'ACTION_NOT_FOUND', status: 500 })
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const token = requestCtxStorage.get<{ value: string | null, decoded: Record<string, unknown> | null, provided: boolean, expired: boolean }>('token')
|
|
935
|
+
|
|
936
|
+
return await col.actions?.[action]({
|
|
937
|
+
rest: this,
|
|
938
|
+
io,
|
|
939
|
+
data,
|
|
940
|
+
error: fn.error,
|
|
941
|
+
jwt: func.jwt,
|
|
942
|
+
token: token ?? { value: null, decoded: null },
|
|
943
|
+
})
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
async runService<T = any>(service: string, action: string, data?: any): Promise<T> {
|
|
947
|
+
const serviceInstance = cfg.services?.find(s => s.name === service && s._tenant_ === this.tenant_id) as Service | undefined
|
|
948
|
+
if (!serviceInstance) {
|
|
949
|
+
throw new AppError(`Service '${service}' not found`, { code: 'SERVICE_NOT_FOUND', status: 500 })
|
|
950
|
+
}
|
|
951
|
+
if (!serviceInstance.enabled) {
|
|
952
|
+
throw new AppError(`Service '${service}' is not enabled`, { code: 'SERVICE_NOT_ENABLED', status: 500 })
|
|
953
|
+
}
|
|
954
|
+
if (!Object.hasOwn(serviceInstance.actions, action) || !serviceInstance.actions?.[action]) {
|
|
955
|
+
throw new AppError(`Action '${action}' not found on service '${service}'`, { code: 'SERVICE_ACTION_NOT_FOUND', status: 500 })
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const token = requestCtxStorage.get<{ value: string | null, decoded: Record<string, unknown> | null, provided: boolean, expired: boolean }>('token')
|
|
959
|
+
|
|
960
|
+
return await serviceInstance.actions?.[action]({
|
|
961
|
+
data,
|
|
962
|
+
error: fn.error,
|
|
963
|
+
io,
|
|
964
|
+
jwt: func.jwt,
|
|
965
|
+
token: token ?? { value: null, decoded: null, provided: false, expired: false },
|
|
966
|
+
rest: this,
|
|
967
|
+
io,
|
|
968
|
+
})
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
async stats() {
|
|
972
|
+
try {
|
|
973
|
+
return await this.db.stats()
|
|
974
|
+
} catch (err: any) {
|
|
975
|
+
throw err instanceof AppError ? err : new AppError(err?.message || 'Internal server error', { code: 'INTERNAL_API_ERROR', status: 500 })
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* List workflow runs from the _workflows_ collection with filtering.
|
|
981
|
+
* Works like find() but targets the _workflows_ collection by default.
|
|
982
|
+
*/
|
|
983
|
+
async workflows(params: FindOptions = {}): Promise<Document[]> {
|
|
984
|
+
try {
|
|
985
|
+
let pipeline = func.buildPipeline(params)
|
|
986
|
+
pipeline = await func.buildInput(pipeline, { rest: this })
|
|
987
|
+
pipeline = func.toBson(pipeline)
|
|
988
|
+
|
|
989
|
+
let result = await this.db.collection('_workflows_').aggregate(pipeline, {
|
|
990
|
+
session: this.session,
|
|
991
|
+
allowDiskUse: true,
|
|
992
|
+
}).toArray()
|
|
993
|
+
result = func.toJson(result)
|
|
994
|
+
return result as any[]
|
|
995
|
+
} catch (err: any) {
|
|
996
|
+
throw err instanceof AppError ? err : new AppError(err?.message || 'Internal server error', { code: 'INTERNAL_API_ERROR', status: 500 })
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
startTransaction() {
|
|
1001
|
+
if (!this.session) this.session = this.startSession()
|
|
1002
|
+
this.session.startTransaction()
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
async commitTransaction() {
|
|
1006
|
+
if (this.session) {
|
|
1007
|
+
await this.session.commitTransaction()
|
|
1008
|
+
await this.session.endSession()
|
|
1009
|
+
this.session = undefined as any
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
async abortTransaction() {
|
|
1014
|
+
if (this.session) {
|
|
1015
|
+
const session = this.session
|
|
1016
|
+
try {
|
|
1017
|
+
await session.abortTransaction()
|
|
1018
|
+
} finally {
|
|
1019
|
+
await session.endSession()
|
|
1020
|
+
this.session = undefined as any
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
|
|
1026
|
+
startSession(): ClientSession {
|
|
1027
|
+
let session = this.client.startSession()
|
|
1028
|
+
this.session = session
|
|
1029
|
+
return session
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
async endSession(): Promise<void> {
|
|
1033
|
+
if (this.session) {
|
|
1034
|
+
await this.session.endSession()
|
|
1035
|
+
this.session = undefined as any
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
startTrace(traceId?: string, extra?: { comment?: string; tag?: string; version?: string }) {
|
|
1040
|
+
requestCtxStorage.set('trace', {
|
|
1041
|
+
id: traceId ?? crypto.randomUUID(),
|
|
1042
|
+
comment: extra?.comment,
|
|
1043
|
+
tag: extra?.tag,
|
|
1044
|
+
version: extra?.version,
|
|
1045
|
+
})
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
unsetTrace() {
|
|
1049
|
+
requestCtxStorage.delete('trace')
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
get audit() {
|
|
1053
|
+
return {
|
|
1054
|
+
addActivities: async (activities: ActivityInput[]) => {
|
|
1055
|
+
await this.db.collection('_activities_').insertMany(activities)
|
|
1056
|
+
.then(e => e)
|
|
1057
|
+
.catch(err => {
|
|
1058
|
+
console.error('Error inserting activities', err)
|
|
1059
|
+
})
|
|
1060
|
+
},
|
|
1061
|
+
getActivities: async (opts = {
|
|
1062
|
+
$match: {},
|
|
1063
|
+
$limit: 100
|
|
1064
|
+
}) => {
|
|
1065
|
+
|
|
1066
|
+
let pipeline = func.buildPipeline(opts)
|
|
1067
|
+
return await this.db.collection('_activities_').aggregate(pipeline).toArray()
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Acquire a distributed lock for this tenant.
|
|
1074
|
+
* Uses MongoDB atomic findOneAndUpdate with upsert.
|
|
1075
|
+
* Only one node in a cluster can hold the lock at a time.
|
|
1076
|
+
* Returns true if the lock was acquired, false otherwise.
|
|
1077
|
+
*/
|
|
1078
|
+
async lock(name: string, ttlMs = 300_000): Promise<void> {
|
|
1079
|
+
const now = Date.now();
|
|
1080
|
+
const expiresAt = now + ttlMs;
|
|
1081
|
+
const id = `${this.tenant_id}:${name}`;
|
|
1082
|
+
|
|
1083
|
+
try {
|
|
1084
|
+
const result = await this.db.collection('_locks_').findOneAndUpdate(
|
|
1085
|
+
{ _id: id, expiresAt: { $lt: now } },
|
|
1086
|
+
{ $set: { _id: id, tid: this.tenant_id, name, acquiredAt: now, expiresAt } },
|
|
1087
|
+
{ upsert: true, returnDocument: 'after' }
|
|
1088
|
+
);
|
|
1089
|
+
if (!result) {
|
|
1090
|
+
throw new AppError(`Lock '${name}' is already held by another node`, {
|
|
1091
|
+
code: 'LOCK_ACQUISITION_FAILED',
|
|
1092
|
+
status: 409
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
} catch (err: any) {
|
|
1096
|
+
if (err instanceof AppError) throw err;
|
|
1097
|
+
// E11000 duplicate key = another node holds a valid lock
|
|
1098
|
+
if (err?.code === 11000) {
|
|
1099
|
+
throw new AppError(`Lock '${name}' is already held by another node`, {
|
|
1100
|
+
code: 'LOCK_ACQUISITION_FAILED',
|
|
1101
|
+
status: 409
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
throw err;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
/**
|
|
1109
|
+
* Release a distributed lock.
|
|
1110
|
+
*/
|
|
1111
|
+
async unlock(name: string): Promise<void> {
|
|
1112
|
+
await this.db.collection('_locks_').deleteOne({
|
|
1113
|
+
tid: this.tenant_id,
|
|
1114
|
+
name,
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* Workflow engine — run, pause, resume, cancel workflows.
|
|
1120
|
+
* Usage: rest.workflow.run('transfert', data)
|
|
1121
|
+
*/
|
|
1122
|
+
get workflow() {
|
|
1123
|
+
return createWorkflow(this.tenant_id);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
export { MongoRest }
|