@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.
@@ -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 = getCollection(collection, this.#tenant.id) as Collection
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 = getCollection(collection, this.#tenant.id) as Collection
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 = getCollection(collection, this.#tenant.id) as Collection
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 = getCollection(collection, this.#tenant.id) as Collection
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 = getCollection(collection, this.#tenant.id) as Collection
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 = getCollection(collection, this.#tenant.id) as Collection
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 = getCollection(collection, this.#tenant.id) as Collection
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 = getCollection(collection, this.#tenant.id) as Collection
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 = getCollection(collection, this.#tenant.id) as Collection
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 = getCollection(collection, this.#tenant.id) as Collection
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 = getCollection(collection, this.#tenant.id) as Collection
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 = getCollection(collection, this.#tenant.id) as Collection
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 = getCollection(collection, this.#tenant.id) as Collection
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 = getCollection(collection, this.#tenant.id) as Collection
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@anteros/core",
3
3
  "module": "index.ts",
4
- "version": "0.0.45",
4
+ "version": "0.0.47",
5
5
  "type": "module",
6
6
  "description": "Anteros Core",
7
7
  "license": "MIT",
@@ -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
+ });