@anteros/core 0.0.45 → 0.0.47
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/mongodbadapter.ts +36 -15
- package/index.ts +2 -1
- package/package.json +1 -1
- package/tests/api-external-guards.test.ts +25 -0
|
@@ -38,6 +38,25 @@ function isProcessAlive(pid: number): boolean {
|
|
|
38
38
|
return err?.code === 'EPERM';
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve a collection definition or fail with the framework's own error.
|
|
44
|
+
*
|
|
45
|
+
* The `@CheckIfCollectionExists()` guard runs before the method, but the definition set
|
|
46
|
+
* can be replaced between the guard and the second lookup (a reload, or a deferred
|
|
47
|
+
* factory such as the query cache). Never cast `getCollection()` to `Collection` — a
|
|
48
|
+
* vanished definition would turn into `null is not an object (evaluating 'col.hooks')`.
|
|
49
|
+
*/
|
|
50
|
+
function requireCollection(collectionName: string, tenantId: string): Collection {
|
|
51
|
+
const col = getCollection(collectionName, tenantId)
|
|
52
|
+
if (!col) {
|
|
53
|
+
throw new AppError(`collection '${collectionName}' not found`, {
|
|
54
|
+
code: 'COLLECTION_NOT_FOUND',
|
|
55
|
+
status: 500,
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
return col
|
|
59
|
+
}
|
|
41
60
|
import { MongoClient, ObjectId, AggregationCursor } from "mongodb";
|
|
42
61
|
import type { FindOptions, findOneOptions } from "../types/mongo";
|
|
43
62
|
import type { FindCallOptions } from "../types/mongo";
|
|
@@ -387,7 +406,7 @@ class MongoRest {
|
|
|
387
406
|
func.boundMatchOperators(pipeline)
|
|
388
407
|
// Audited with the parameters only — the change stream itself is a live object
|
|
389
408
|
return this.#executeWithAudit('watch', collection, { pipeline, options }, async () => {
|
|
390
|
-
const col =
|
|
409
|
+
const col = requireCollection(collection, this.#tenant.id)
|
|
391
410
|
const isSafe = func.isSafeAggregatePipeline(pipeline);
|
|
392
411
|
if (!isSafe.isSafe) {
|
|
393
412
|
throw isSafe.error;
|
|
@@ -408,7 +427,7 @@ class MongoRest {
|
|
|
408
427
|
const action = 'aggregate' as ActionsApiList
|
|
409
428
|
return this.#executeReadWithAudit(action, collection, { pipeline }, async () => {
|
|
410
429
|
try {
|
|
411
|
-
const col =
|
|
430
|
+
const col = requireCollection(collection, this.#tenant.id)
|
|
412
431
|
// Flaw 12: restrict $lookup targets to this tenant's declared collections
|
|
413
432
|
const allowedCollections = [
|
|
414
433
|
...(cfg.collections ?? [])
|
|
@@ -489,7 +508,7 @@ class MongoRest {
|
|
|
489
508
|
const countPromise = withCount
|
|
490
509
|
? this.countDocuments(collection, params.$match ?? {})
|
|
491
510
|
: null;
|
|
492
|
-
const col =
|
|
511
|
+
const col = requireCollection(collection, this.#tenant.id)
|
|
493
512
|
let pipeline = func.buildPipeline({ ...params, $limit: params.$limit ?? 0 }, { col: col })
|
|
494
513
|
pipeline = await func.buildInput(pipeline, { rest: this })
|
|
495
514
|
pipeline = func.toBson(pipeline, { col })
|
|
@@ -565,7 +584,7 @@ class MongoRest {
|
|
|
565
584
|
// `break`/`return()` — which triggers the `finally` below). Parameters only.
|
|
566
585
|
const start = Date.now();
|
|
567
586
|
let failure: any = null;
|
|
568
|
-
const col =
|
|
587
|
+
const col = requireCollection(collection, this.#tenant.id)
|
|
569
588
|
const allowedCollections = [
|
|
570
589
|
...(cfg.collections ?? []).filter((c) => c._tenant_ === this.tenant_id).map((c) => c.slug),
|
|
571
590
|
...(cfg.fileCollections ?? []).filter((fc) => fc._tenant_ === this.tenant_id).map((fc) => fc.slug),
|
|
@@ -624,11 +643,13 @@ class MongoRest {
|
|
|
624
643
|
@CheckIfCollectionExists()
|
|
625
644
|
async find(collection: string, params: FindOptions = {}, options: FindCallOptions = {}): Promise<Document[]> {
|
|
626
645
|
const action = 'find' as ActionsApiList
|
|
646
|
+
// Resolve once, before any await: a cache-miss factory may run later, after the
|
|
647
|
+
// definition set was replaced — the call keeps the collection it started with.
|
|
648
|
+
const col = requireCollection(collection, this.#tenant.id)
|
|
627
649
|
return this.#executeReadWithAudit(action, collection, { params, options }, async () => {
|
|
628
650
|
try {
|
|
629
651
|
// Real query — hooks + pipeline + toArray
|
|
630
652
|
const run = async (): Promise<Document[]> => {
|
|
631
|
-
const col = getCollection(collection, this.#tenant.id) as Collection
|
|
632
653
|
let pipeline = func.buildPipeline(params, { col: col })
|
|
633
654
|
pipeline = await func.buildInput(pipeline, { rest: this })
|
|
634
655
|
pipeline = func.toBson(pipeline, { col })
|
|
@@ -669,7 +690,7 @@ class MongoRest {
|
|
|
669
690
|
const action = 'findOne' as ActionsApiList
|
|
670
691
|
return this.#executeReadWithAudit(action, collection, { id: _id, params }, async () => {
|
|
671
692
|
try {
|
|
672
|
-
const col =
|
|
693
|
+
const col = requireCollection(collection, this.#tenant.id)
|
|
673
694
|
let pipeline = func.buildPipeline({
|
|
674
695
|
...params,
|
|
675
696
|
$match: { _id },
|
|
@@ -707,7 +728,7 @@ class MongoRest {
|
|
|
707
728
|
async insertOne<T>(collection: string, data: T): Promise<T & { _id: string }> {
|
|
708
729
|
const action = 'insertOne' as ActionsApiList
|
|
709
730
|
return this.#executeWithAudit(action, collection, { data }, async () => {
|
|
710
|
-
const col =
|
|
731
|
+
const col = requireCollection(collection, this.#tenant.id)
|
|
711
732
|
|
|
712
733
|
data = await func.buildInput(data as T, {
|
|
713
734
|
action: action, col: col, rest: this
|
|
@@ -744,7 +765,7 @@ class MongoRest {
|
|
|
744
765
|
async insertMany<T>(collection: string, data: T[]): Promise<(T & { _id: string })[]> {
|
|
745
766
|
const action = 'insertMany' as ActionsApiList
|
|
746
767
|
return this.#executeWithAudit(action, collection, { count: data.length }, async () => {
|
|
747
|
-
const col =
|
|
768
|
+
const col = requireCollection(collection, this.#tenant.id)
|
|
748
769
|
let dataInput: T[] = []
|
|
749
770
|
|
|
750
771
|
for (let d of data) {
|
|
@@ -790,7 +811,7 @@ class MongoRest {
|
|
|
790
811
|
async updateOne(collection: string, _id: string, update: UpdateFilter<any>) {
|
|
791
812
|
const action = 'updateOne' as ActionsApiList
|
|
792
813
|
return this.#executeWithAudit(action, collection, { id: _id, update }, async () => {
|
|
793
|
-
const col =
|
|
814
|
+
const col = requireCollection(collection, this.#tenant.id)
|
|
794
815
|
|
|
795
816
|
if (update.$set) {
|
|
796
817
|
update.$set = await func.buildInput(update.$set, { action: action, col: col })
|
|
@@ -832,7 +853,7 @@ class MongoRest {
|
|
|
832
853
|
func.assertSafeFilter(filter)
|
|
833
854
|
const action = 'findOneAndUpdate' as ActionsApiList
|
|
834
855
|
return this.#executeWithAudit(action, collection, { filter, update, options }, async () => {
|
|
835
|
-
const col =
|
|
856
|
+
const col = requireCollection(collection, this.#tenant.id)
|
|
836
857
|
|
|
837
858
|
|
|
838
859
|
|
|
@@ -902,7 +923,7 @@ class MongoRest {
|
|
|
902
923
|
async updateMany(collection: string, _ids: string[], update: UpdateFilter<any>): Promise<UpdateResult> {
|
|
903
924
|
const action = 'updateMany' as ActionsApiList
|
|
904
925
|
return this.#executeWithAudit(action, collection, { ids: _ids, update }, async () => {
|
|
905
|
-
const col =
|
|
926
|
+
const col = requireCollection(collection, this.#tenant.id)
|
|
906
927
|
|
|
907
928
|
if (update.$set) {
|
|
908
929
|
update.$set = await func.buildInput(update.$set, { action: action, col: col })
|
|
@@ -940,7 +961,7 @@ class MongoRest {
|
|
|
940
961
|
async deleteOne(collection: string, _id: string) {
|
|
941
962
|
const action = 'deleteOne' as ActionsApiList
|
|
942
963
|
return this.#executeWithAudit(action, collection, { id: _id }, async () => {
|
|
943
|
-
const col =
|
|
964
|
+
const col = requireCollection(collection, this.#tenant.id)
|
|
944
965
|
|
|
945
966
|
const meta: any = { action, collection, id: _id }
|
|
946
967
|
if (col.hooks?.beforeOperation) {
|
|
@@ -969,7 +990,7 @@ class MongoRest {
|
|
|
969
990
|
async deleteMany(collection: string, _ids: string[]): Promise<DeleteResult> {
|
|
970
991
|
const action = 'deleteMany' as ActionsApiList
|
|
971
992
|
return this.#executeWithAudit(action, collection, { ids: _ids }, async () => {
|
|
972
|
-
const col =
|
|
993
|
+
const col = requireCollection(collection, this.#tenant.id)
|
|
973
994
|
|
|
974
995
|
const meta: any = { action, collection, ids: _ids }
|
|
975
996
|
if (col.hooks?.beforeOperation) {
|
|
@@ -1000,7 +1021,7 @@ class MongoRest {
|
|
|
1000
1021
|
const action = 'bulkWrite' as ActionsApiList
|
|
1001
1022
|
return this.#executeWithAudit(action, collection, { count: operations.length }, async () => {
|
|
1002
1023
|
for (let operation of operations) {
|
|
1003
|
-
const col =
|
|
1024
|
+
const col = requireCollection(collection, this.#tenant.id)
|
|
1004
1025
|
|
|
1005
1026
|
if ('insertOne' in operation) {
|
|
1006
1027
|
operation.insertOne.document = func.toBson(
|
|
@@ -1066,7 +1087,7 @@ class MongoRest {
|
|
|
1066
1087
|
assertBulkFilters(operations);
|
|
1067
1088
|
const action = 'bulkUpdate' as ActionsApiList
|
|
1068
1089
|
return this.#executeWithAudit(action, collection, { count: operations.length }, async () => {
|
|
1069
|
-
const col =
|
|
1090
|
+
const col = requireCollection(collection, this.#tenant.id)
|
|
1070
1091
|
|
|
1071
1092
|
for (const operation of operations) {
|
|
1072
1093
|
if ('updateOne' in operation) {
|
package/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { evaluateAgent } from "./lib/eval";
|
|
|
10
10
|
import { endpoints } from "./lib/endpoints";
|
|
11
11
|
import * as v from "joi";
|
|
12
12
|
import utils from "./utils";
|
|
13
|
+
import {ObjectId} from "mongodb"
|
|
13
14
|
|
|
14
15
|
|
|
15
16
|
|
|
@@ -49,6 +50,7 @@ export {
|
|
|
49
50
|
agents,
|
|
50
51
|
evaluateAgent,
|
|
51
52
|
endpoints,
|
|
53
|
+
ObjectId
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
export type {
|
|
@@ -68,4 +70,3 @@ export type {
|
|
|
68
70
|
AgentEvalGrader,
|
|
69
71
|
AgentEvalReport,
|
|
70
72
|
} from "./lib/eval";
|
|
71
|
-
|
package/package.json
CHANGED
|
@@ -277,3 +277,28 @@ describe("in-process `rest` — `$rename` cannot target a read-only field (#43)"
|
|
|
277
277
|
expect((await rest.findOne("people", doc._id))?.name).toBe("ren");
|
|
278
278
|
});
|
|
279
279
|
});
|
|
280
|
+
|
|
281
|
+
describe("collection definitions — a definition that disappears mid-call", () => {
|
|
282
|
+
it("fails with COLLECTION_NOT_FOUND instead of a TypeError on `col.hooks`", async () => {
|
|
283
|
+
const saved = (cfg as any).collections;
|
|
284
|
+
let reads = 0;
|
|
285
|
+
Object.defineProperty(cfg, "collections", {
|
|
286
|
+
configurable: true,
|
|
287
|
+
get() {
|
|
288
|
+
reads += 1;
|
|
289
|
+
return reads === 1 ? saved : [];
|
|
290
|
+
},
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const code = await internal.find("people").then(
|
|
295
|
+
() => "resolved",
|
|
296
|
+
(error: any) => error?.code,
|
|
297
|
+
);
|
|
298
|
+
// One read for the guard, one for the method's own (checked) resolution.
|
|
299
|
+
expect({ reads, code }).toEqual({ reads: 2, code: "COLLECTION_NOT_FOUND" });
|
|
300
|
+
} finally {
|
|
301
|
+
Object.defineProperty(cfg, "collections", { configurable: true, writable: true, value: saved });
|
|
302
|
+
}
|
|
303
|
+
});
|
|
304
|
+
});
|