@travetto/model-mongo 8.0.0-alpha.24 → 8.0.0-alpha.26

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 CHANGED
@@ -13,25 +13,25 @@ npm install @travetto/model-mongo
13
13
  yarn add @travetto/model-mongo
14
14
  ```
15
15
 
16
- This module provides an [mongodb](https://mongodb.com)-based implementation for the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations."). This source allows the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") module to read, write and query against [mongodb](https://mongodb.com).. Given the dynamic nature of [mongodb](https://mongodb.com), during development when models are modified, nothing needs to be done to adapt to the latest schema.
16
+ This module provides an [mongodb](https://mongodb.com)-based implementation for the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations."). This source allows the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") module to read, write and query against [mongodb](https://mongodb.com).. Given the dynamic nature of [mongodb](https://mongodb.com), during development when models are modified, nothing needs to be done to adapt to the latest schema.
17
17
 
18
18
  Supported features:
19
- * [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#L11)
20
- * [Expiry](https://github.com/travetto/travetto/tree/main/module/model/src/types/expiry.ts#L10)
21
- * [Bulk](https://github.com/travetto/travetto/tree/main/module/model/src/types/bulk.ts#L64)
22
19
  * [Blob](https://github.com/travetto/travetto/tree/main/module/model/src/types/blob.ts#L8)
23
- * [Indexed](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/types/service.ts#L16)
20
+ * [Bulk](https://github.com/travetto/travetto/tree/main/module/model/src/types/bulk.ts#L60)
21
+ * [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#L10)
22
+ * [Expiry](https://github.com/travetto/travetto/tree/main/module/model/src/types/expiry.ts#L10)
23
+ * [Indexed](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/types/service.ts#L21)
24
24
  * [Query Crud](https://github.com/travetto/travetto/tree/main/module/model-query/src/types/crud.ts#L11)
25
25
  * [Facet](https://github.com/travetto/travetto/tree/main/module/model-query/src/types/facet.ts#L14)
26
- * [Query](https://github.com/travetto/travetto/tree/main/module/model-query/src/types/query.ts#L10)
27
26
  * [Suggest](https://github.com/travetto/travetto/tree/main/module/model-query/src/types/suggest.ts#L12)
27
+ * [Query](https://github.com/travetto/travetto/tree/main/module/model-query/src/types/query.ts#L10)
28
28
 
29
29
  Out of the box, by installing the module, everything should be wired up by default.If you need to customize any aspect of the source or config, you can override and register it with the [Dependency Injection](https://github.com/travetto/travetto/tree/main/module/di#readme "Dependency registration/management and injection support.") module.
30
30
 
31
31
  **Code: Wiring up a custom Model Source**
32
32
  ```typescript
33
33
  import { InjectableFactory } from '@travetto/di';
34
- import { MongoModelService, type MongoModelConfig } from '@travetto/model-mongo';
34
+ import { type MongoModelConfig, MongoModelService } from '@travetto/model-mongo';
35
35
 
36
36
  export class Init {
37
37
  @InjectableFactory({
@@ -82,7 +82,7 @@ export class MongoModelConfig {
82
82
  */
83
83
  @Field({ type: Object })
84
84
  options: Omit<mongo.MongoClientOptions, 'cert'> & {
85
- cert?: | Buffer | string | BinaryType | (BinaryType | Buffer | string)[];
85
+ cert?: Buffer | string | BinaryType | (BinaryType | Buffer | string)[];
86
86
  } = {};
87
87
  /**
88
88
  * Allow storage modification at runtime
@@ -124,15 +124,14 @@ export class MongoModelConfig {
124
124
  if (!this.port || Number.isNaN(this.port)) {
125
125
  this.port = 27017;
126
126
  }
127
- if (!this.hosts || !this.hosts.length) {
127
+ if (!this.hosts?.length) {
128
128
  this.hosts = ['localhost'];
129
129
  }
130
130
 
131
131
  const options = this.options;
132
132
  if (options.ssl) {
133
133
  if (options.cert) {
134
- options.cert = (await Promise.all([options.cert].flat(2).map(readCert)))
135
- .map(BinaryUtil.binaryArrayToUint8Array);
134
+ options.cert = (await Promise.all([options.cert].flat(2).map(readCert))).map(BinaryUtil.binaryArrayToUint8Array);
136
135
  }
137
136
  if (options.tlsCertificateKeyFile) {
138
137
  options.tlsCertificateKeyFile = await RuntimeResources.resolve(options.tlsCertificateKeyFile);
@@ -155,15 +154,12 @@ export class MongoModelConfig {
155
154
  * Build connection URLs
156
155
  */
157
156
  get url(): string {
158
- const hosts = this.hosts!
159
- .map(host => (this.srvRecord || host.includes(':')) ? host : `${host}:${this.port ?? 27017}`)
160
- .join(',');
157
+ const hosts = this.hosts!.map(host => (this.srvRecord || host.includes(':') ? host : `${host}:${this.port ?? 27017}`)).join(',');
161
158
  const optionString = new URLSearchParams(
162
159
  Object.entries(this.options)
163
160
  .filter((pair): pair is [string, string | number | boolean] => ['string', 'number', 'boolean'].includes(typeof pair[1]))
164
161
  .map(([k, v]) => [k, `${v}`])
165
- )
166
- .toString();
162
+ ).toString();
167
163
  let creds = '';
168
164
  if (this.username) {
169
165
  creds = `${[this.username, this.password].filter(part => !!part).join(':')}@`;
@@ -174,4 +170,4 @@ export class MongoModelConfig {
174
170
  }
175
171
  ```
176
172
 
177
- Additionally, you can see that the class is registered with the [@Config](https://github.com/travetto/travetto/tree/main/module/config/src/decorator.ts#L13) annotation, and so these values can be overridden using the standard [Configuration](https://github.com/travetto/travetto/tree/main/module/config#readme "Configuration support") resolution paths.The SSL file options in `clientOptions` will automatically be resolved to files when given a path. This path can be a resource path (will attempt to lookup using [RuntimeResources](https://github.com/travetto/travetto/tree/main/module/runtime/src/resources.ts#L8)) or just a standard file path.
173
+ Additionally, you can see that the class is registered with the [@Config](https://github.com/travetto/travetto/tree/main/module/config/src/decorator.ts#L13) annotation, and so these values can be overridden using the standard [Configuration](https://github.com/travetto/travetto/tree/main/module/config#readme "Configuration support") resolution paths.The SSL file options in `clientOptions` will automatically be resolved to files when given a path. This path can be a resource path (will attempt to lookup using [RuntimeResources](https://github.com/travetto/travetto/tree/main/module/runtime/src/resources.ts#L8)) or just a standard file path.
package/__index__.ts CHANGED
@@ -1,2 +1,2 @@
1
+ export * from './src/config.ts';
1
2
  export * from './src/service.ts';
2
- export * from './src/config.ts';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model-mongo",
3
- "version": "8.0.0-alpha.24",
3
+ "version": "8.0.0-alpha.26",
4
4
  "type": "module",
5
5
  "description": "Mongo backing for the travetto model module.",
6
6
  "keywords": [
@@ -26,14 +26,14 @@
26
26
  "directory": "module/model-mongo"
27
27
  },
28
28
  "dependencies": {
29
- "@travetto/config": "^8.0.0-alpha.21",
30
- "@travetto/model": "^8.0.0-alpha.22",
31
- "@travetto/model-indexed": "^8.0.0-alpha.24",
32
- "@travetto/model-query": "^8.0.0-alpha.23",
29
+ "@travetto/config": "^8.0.0-alpha.23",
30
+ "@travetto/model": "^8.0.0-alpha.24",
31
+ "@travetto/model-indexed": "^8.0.0-alpha.26",
32
+ "@travetto/model-query": "^8.0.0-alpha.25",
33
33
  "mongodb": "^7.5.0"
34
34
  },
35
35
  "peerDependencies": {
36
- "@travetto/cli": "^8.0.0-alpha.27"
36
+ "@travetto/cli": "^8.0.0-alpha.29"
37
37
  },
38
38
  "peerDependenciesMeta": {
39
39
  "@travetto/cli": {
package/src/config.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import type mongo from 'mongodb';
2
2
 
3
- import { type TimeSpan, Runtime, RuntimeResources, BinaryUtil, CodecUtil, type BinaryType, type BinaryArray } from '@travetto/runtime';
4
3
  import { Config } from '@travetto/config';
5
- import { Field } from '@travetto/schema';
6
4
  import { PostConstruct } from '@travetto/di';
5
+ import { type BinaryArray, type BinaryType, BinaryUtil, CodecUtil, Runtime, RuntimeResources, type TimeSpan } from '@travetto/runtime';
6
+ import { Field } from '@travetto/schema';
7
7
 
8
8
  const readCert = async (input: BinaryType | string): Promise<BinaryArray> => {
9
9
  if (BinaryUtil.isBinaryType(input)) {
@@ -55,7 +55,7 @@ export class MongoModelConfig {
55
55
  */
56
56
  @Field({ type: Object })
57
57
  options: Omit<mongo.MongoClientOptions, 'cert'> & {
58
- cert?: | Buffer | string | BinaryType | (BinaryType | Buffer | string)[];
58
+ cert?: Buffer | string | BinaryType | (BinaryType | Buffer | string)[];
59
59
  } = {};
60
60
  /**
61
61
  * Allow storage modification at runtime
@@ -97,15 +97,14 @@ export class MongoModelConfig {
97
97
  if (!this.port || Number.isNaN(this.port)) {
98
98
  this.port = 27017;
99
99
  }
100
- if (!this.hosts || !this.hosts.length) {
100
+ if (!this.hosts?.length) {
101
101
  this.hosts = ['localhost'];
102
102
  }
103
103
 
104
104
  const options = this.options;
105
105
  if (options.ssl) {
106
106
  if (options.cert) {
107
- options.cert = (await Promise.all([options.cert].flat(2).map(readCert)))
108
- .map(BinaryUtil.binaryArrayToUint8Array);
107
+ options.cert = (await Promise.all([options.cert].flat(2).map(readCert))).map(BinaryUtil.binaryArrayToUint8Array);
109
108
  }
110
109
  if (options.tlsCertificateKeyFile) {
111
110
  options.tlsCertificateKeyFile = await RuntimeResources.resolve(options.tlsCertificateKeyFile);
@@ -128,15 +127,12 @@ export class MongoModelConfig {
128
127
  * Build connection URLs
129
128
  */
130
129
  get url(): string {
131
- const hosts = this.hosts!
132
- .map(host => (this.srvRecord || host.includes(':')) ? host : `${host}:${this.port ?? 27017}`)
133
- .join(',');
130
+ const hosts = this.hosts!.map(host => (this.srvRecord || host.includes(':') ? host : `${host}:${this.port ?? 27017}`)).join(',');
134
131
  const optionString = new URLSearchParams(
135
132
  Object.entries(this.options)
136
133
  .filter((pair): pair is [string, string | number | boolean] => ['string', 'number', 'boolean'].includes(typeof pair[1]))
137
134
  .map(([k, v]) => [k, `${v}`])
138
- )
139
- .toString();
135
+ ).toString();
140
136
  let creds = '';
141
137
  if (this.username) {
142
138
  creds = `${[this.username, this.password].filter(part => !!part).join(':')}@`;
@@ -144,4 +140,4 @@ export class MongoModelConfig {
144
140
  const url = `mongodb${this.srvRecord ? '+srv' : ''}://${creds}${hosts}/${this.namespace}?${optionString}`;
145
141
  return url;
146
142
  }
147
- }
143
+ }
@@ -1,13 +1,19 @@
1
1
  import {
2
- Binary, type CreateIndexesOptions, type Filter, type FindCursor, type IndexDirection, ObjectId, type WithId as MongoWithId,
3
- type IndexDescriptionInfo
2
+ Binary,
3
+ type CreateIndexesOptions,
4
+ type Filter,
5
+ type FindCursor,
6
+ type IndexDescriptionInfo,
7
+ type IndexDirection,
8
+ type WithId as MongoWithId,
9
+ ObjectId
4
10
  } from 'mongodb';
5
11
 
6
- import { RuntimeError, CodecUtil, castTo, type Class, toConcrete, BinaryUtil } from '@travetto/runtime';
7
- import { type DistanceUnit, type PageableModelQuery, type WhereClause, isModelQueryIndex, ModelQueryUtil } from '@travetto/model-query';
8
- import { type ModelType, type IndexConfig, IndexNotSupported } from '@travetto/model';
9
- import { DataUtil, SchemaRegistryIndex, type Point } from '@travetto/schema';
12
+ import { type IndexConfig, IndexNotSupported, type ModelType } from '@travetto/model';
10
13
  import { isModelIndexedIndex } from '@travetto/model-indexed';
14
+ import { type DistanceUnit, isModelQueryIndex, ModelQueryUtil, type PageableModelQuery, type WhereClause } from '@travetto/model-query';
15
+ import { BinaryUtil, type Class, CodecUtil, castTo, RuntimeError, toConcrete } from '@travetto/runtime';
16
+ import { DataUtil, type Point, SchemaRegistryIndex } from '@travetto/schema';
11
17
 
12
18
  const PointConcrete = toConcrete<Point>();
13
19
 
@@ -42,19 +48,13 @@ function flattenKeys(obj: Record<string, unknown>, prefix = ''): Record<string,
42
48
  * Basic mongo utils for conforming to the model module
43
49
  */
44
50
  export class MongoUtil {
45
-
46
51
  static namespaceIndex(cls: Class, name: string): string {
47
52
  return `${cls.Ⲑid}__${name}`.replace(/[^a-zA-Z0-9_]+/g, '_');
48
53
  }
49
54
 
50
55
  static uuid(value: string): Binary {
51
56
  try {
52
- return new Binary(
53
- BinaryUtil.binaryArrayToUint8Array(
54
- CodecUtil.fromHexString(value.replaceAll('-', ''))
55
- ),
56
- Binary.SUBTYPE_UUID
57
- );
57
+ return new Binary(BinaryUtil.binaryArrayToUint8Array(CodecUtil.fromHexString(value.replaceAll('-', ''))), Binary.SUBTYPE_UUID);
58
58
  } catch (err) {
59
59
  if (err instanceof RuntimeError && err.message === 'Invalid hex string') {
60
60
  return null!;
@@ -94,7 +94,12 @@ export class MongoUtil {
94
94
  }
95
95
 
96
96
  /**/
97
- static extractSimple<T>(base: Class<T> | undefined, item: Record<string, unknown>, path: string = '', recursive: boolean = true): Record<string, unknown> {
97
+ static extractSimple<T>(
98
+ base: Class<T> | undefined,
99
+ item: Record<string, unknown>,
100
+ path: string = '',
101
+ recursive: boolean = true
102
+ ): Record<string, unknown> {
98
103
  const fields = base ? SchemaRegistryIndex.getOptional(base)?.getFields() : undefined;
99
104
  const out: Record<string, unknown> = {};
100
105
  const sub = item;
@@ -140,7 +145,7 @@ export class MongoUtil {
140
145
  value.$regex = DataUtil.toRegex(castTo(value.$regex));
141
146
  } else if (firstKey && '$near' in value) {
142
147
  const dist: number = castTo(value.$maxDistance);
143
- const distance = dist / RADIANS_TO[(castTo<DistanceUnit>(value.$unit) ?? 'km')];
148
+ const distance = dist / RADIANS_TO[castTo<DistanceUnit>(value.$unit) ?? 'km'];
144
149
  value.$maxDistance = distance;
145
150
  delete value.$unit;
146
151
  } else if (firstKey && '$geoWithin' in value) {
@@ -187,8 +192,9 @@ export class MongoUtil {
187
192
  const name = this.namespaceIndex(cls, idx.name);
188
193
  if (isModelQueryIndex(idx)) {
189
194
  const out = idx.fields.reduce(
190
- (acc, field) => ({ ...acc, ...flattenKeys(castTo(field)) }),
191
- castTo<Record<string, -1 | 0 | 1>>({}));
195
+ (acc, field) => Object.assign(acc, { ...flattenKeys(castTo(field)) }),
196
+ castTo<Record<string, -1 | 0 | 1>>({})
197
+ );
192
198
 
193
199
  return [out, { name, unique: !!idx.unique }];
194
200
  } else if (isModelIndexedIndex(idx)) {
@@ -197,23 +203,29 @@ export class MongoUtil {
197
203
  ...idx.sortTemplate.map(({ path, value }) => [path.join('.'), value === -1 ? -1 : 1])
198
204
  ]);
199
205
  switch (idx.type) {
200
- case 'indexed:keyed': return [filter, { name, unique: idx.unique }];
201
- case 'indexed:sorted': return [filter, { name }];
206
+ case 'indexed:keyed':
207
+ return [filter, { name, unique: idx.unique }];
208
+ case 'indexed:sorted':
209
+ return [filter, { name }];
202
210
  }
203
211
  } else {
204
212
  throw new IndexNotSupported(cls, idx);
205
213
  }
206
214
  }
207
215
 
208
- static prepareCursor<T extends ModelType>(cls: Class<T>, cursor: FindCursor<T | MongoWithId<T>>, query: PageableModelQuery<T>): FindCursor<T> {
216
+ static prepareCursor<T extends ModelType>(
217
+ cls: Class<T>,
218
+ cursor: FindCursor<T | MongoWithId<T>>,
219
+ query: PageableModelQuery<T>
220
+ ): FindCursor<T> {
209
221
  if (query.select) {
210
222
  const selectKey = Object.keys(query.select)[0];
211
223
  const select = typeof selectKey === 'string' && selectKey.startsWith('$') ? query.select : this.extractSimple(cls, query.select);
212
224
  // Remove id if not explicitly defined, and selecting fields directly
213
- if (!select['_id']) {
225
+ if (!select._id) {
214
226
  const values = new Set([...Object.values(select)]);
215
227
  if (values.has(1) || values.has(true)) {
216
- select['_id'] = false;
228
+ select._id = false;
217
229
  }
218
230
  }
219
231
  cursor.project(select);
@@ -241,9 +253,9 @@ export class MongoUtil {
241
253
  existing.expireAfterSeconds !== pendingOptions.expireAfterSeconds ||
242
254
  existing.bucketSize !== pendingOptions.bucketSize;
243
255
 
244
- const existingFields = existing.textIndexVersion ?
245
- Object.fromEntries(Object.entries(existing.weights ?? {}).map(([key]) => [key, 'text'])) :
246
- existing.key;
256
+ const existingFields = existing.textIndexVersion
257
+ ? Object.fromEntries(Object.entries(existing.weights ?? {}).map(([key]) => [key, 'text']))
258
+ : existing.key;
247
259
 
248
260
  const pendingKeySet = new Set(Object.keys(pendingKey));
249
261
  const existingKeySet = new Set(Object.keys(existingFields));
@@ -253,10 +265,10 @@ export class MongoUtil {
253
265
  const overlap = [...pendingKeySet.intersection(existingKeySet)];
254
266
  changed ||= overlap.length !== pendingKeySet.size;
255
267
 
256
- for (let i = 0; i < overlap.length && !changed; i++) {
268
+ for (let i = 0; i < overlap.length && !changed; i += 1) {
257
269
  changed ||= existingFields[overlap[i]] !== pendingKey[overlap[i]];
258
270
  }
259
271
 
260
272
  return changed;
261
273
  }
262
- }
274
+ }
package/src/service.ts CHANGED
@@ -1,40 +1,102 @@
1
1
  import {
2
- type Binary, type Db, GridFSBucket, MongoClient, type GridFSFile, type Collection,
3
- type ObjectId, type RootFilterOperators, type Filter, type WithId as MongoWithId, type FindCursor,
2
+ type Binary,
3
+ type Collection,
4
+ type Db,
5
+ type Filter,
6
+ type FindCursor,
7
+ GridFSBucket,
8
+ type GridFSFile,
9
+ MongoClient,
4
10
  MongoServerError,
11
+ type WithId as MongoWithId,
12
+ type ObjectId,
13
+ type RootFilterOperators
5
14
  } from 'mongodb';
6
15
 
16
+ import { Injectable, PostConstruct } from '@travetto/di';
7
17
  import {
8
- ModelRegistryIndex, type ModelType, type OptionalId, type ModelCrudSupport, type ModelStorageSupport, type ModelExpirySupport,
9
- type ModelBulkSupport, type BulkOperation, type BulkResponse, NotFoundError, ExistsError, type ModelBlobSupport,
10
- ModelCrudUtil, ModelStorageUtil, ModelExpiryUtil, ModelBulkUtil, type ModelListOptions,
18
+ type BulkOperation,
19
+ type BulkResponse,
20
+ ExistsError,
21
+ type ModelBlobSupport,
22
+ type ModelBulkSupport,
23
+ ModelBulkUtil,
24
+ type ModelCrudSupport,
25
+ ModelCrudUtil,
26
+ type ModelExpirySupport,
27
+ ModelExpiryUtil,
28
+ type ModelListOptions,
29
+ ModelRegistryIndex,
30
+ type ModelStorageSupport,
31
+ ModelStorageUtil,
32
+ type ModelType,
33
+ NotFoundError,
34
+ type OptionalId,
35
+ UniqueError
11
36
  } from '@travetto/model';
12
37
  import {
13
- type ModelQuery, type ModelQueryCrudSupport, type ModelQueryFacetSupport, type ModelQuerySupport,
14
- type PageableModelQuery, type ValidStringFields, type WhereClause, type ModelQuerySuggestSupport,
15
- QueryVerifier, ModelQueryUtil, ModelQuerySuggestUtil, ModelQueryCrudUtil, type ModelQueryFacet,
16
- } from '@travetto/model-query';
17
- import {
18
- type ModelIndexedSupport, type KeyedIndexSelection, type KeyedIndexBody, type ModelPageOptions, ModelIndexedUtil,
19
- type SingleItemIndex, type SortedIndexSelection, type ModelPageResult, type SortedIndex, type FullKeyedIndexBody,
20
- type FullKeyedIndexWithPartialBody, ModelIndexedComputedIndex, type ModelIndexedSearchOptions, type SortedIndexSelectionType,
38
+ type FullKeyedIndexBody,
39
+ type FullKeyedIndexWithPartialBody,
40
+ type KeyedIndexBody,
41
+ type KeyedIndexSelection,
42
+ ModelIndexedComputedIndex,
43
+ type ModelIndexedSearchOptions,
44
+ type ModelIndexedSupport,
45
+ ModelIndexedUtil,
46
+ type ModelPageOptions,
47
+ type ModelPageResult,
48
+ type SingleItemIndex,
49
+ type SortedIndex,
50
+ type SortedIndexSelection,
51
+ type SortedIndexSelectionType
21
52
  } from '@travetto/model-indexed';
22
-
23
53
  import {
24
- ShutdownManager, type Class, TypedObject, castTo, asFull, type BinaryMetadata, type ByteRange, type BinaryType,
25
- BinaryUtil, BinaryMetadataUtil, JSONUtil,
54
+ type ModelQuery,
55
+ type ModelQueryCrudSupport,
56
+ ModelQueryCrudUtil,
57
+ type ModelQueryFacet,
58
+ type ModelQueryFacetSupport,
59
+ type ModelQuerySuggestSupport,
60
+ ModelQuerySuggestUtil,
61
+ type ModelQuerySupport,
62
+ ModelQueryUtil,
63
+ type PageableModelQuery,
64
+ QueryVerifier,
65
+ type ValidStringFields,
66
+ type WhereClause
67
+ } from '@travetto/model-query';
68
+ import {
69
+ asFull,
70
+ type BinaryMetadata,
71
+ BinaryMetadataUtil,
72
+ type BinaryType,
73
+ BinaryUtil,
74
+ type ByteRange,
75
+ type Class,
76
+ castTo,
77
+ JSONUtil,
78
+ ShutdownManager,
79
+ TypedObject
26
80
  } from '@travetto/runtime';
27
- import { Injectable, PostConstruct } from '@travetto/di';
28
81
 
29
- import { MongoUtil, type WithId } from './internal/util.ts';
30
82
  import type { MongoModelConfig } from './config.ts';
83
+ import { MongoUtil, type WithId } from './internal/util.ts';
31
84
 
32
85
  type BlobRaw = GridFSFile & { metadata?: BinaryMetadata };
33
86
 
34
87
  type MongoTextSearch = RootFilterOperators<unknown>['$text'];
35
88
 
36
- const isDuplicateKeyError = (error: unknown): boolean =>
37
- error instanceof MongoServerError && error.message.includes('duplicate key error');
89
+ const handleDuplicateKeyError = (cls: Class, id: string, error: unknown): unknown => {
90
+ if (error instanceof MongoServerError && error.message.includes('duplicate key error')) {
91
+ if (error.message.includes('_id_') || (error.keyPattern && '_id' in error.keyPattern)) {
92
+ return new ExistsError(cls, id);
93
+ }
94
+ const match = error.message.match(/index: ([\w$]+)/);
95
+ const constraint = match ? match[1] : error.keyPattern ? Object.keys(error.keyPattern).join(',') : 'unique';
96
+ return new UniqueError(cls, constraint, { detail: error.message });
97
+ }
98
+ return error;
99
+ };
38
100
 
39
101
  export const ModelBlobNamespace = '__blobs';
40
102
 
@@ -42,30 +104,41 @@ export const ModelBlobNamespace = '__blobs';
42
104
  * Mongo-based model source
43
105
  */
44
106
  @Injectable()
45
- export class MongoModelService implements
46
- ModelCrudSupport, ModelStorageSupport,
47
- ModelBulkSupport, ModelBlobSupport,
48
- ModelIndexedSupport, ModelQuerySupport,
49
- ModelQueryCrudSupport, ModelQueryFacetSupport,
50
- ModelQuerySuggestSupport, ModelExpirySupport {
51
-
107
+ export class MongoModelService
108
+ implements
109
+ ModelCrudSupport,
110
+ ModelStorageSupport,
111
+ ModelBulkSupport,
112
+ ModelBlobSupport,
113
+ ModelIndexedSupport,
114
+ ModelQuerySupport,
115
+ ModelQueryCrudSupport,
116
+ ModelQueryFacetSupport,
117
+ ModelQuerySuggestSupport,
118
+ ModelExpirySupport
119
+ {
52
120
  #db: Db;
53
121
  #bucket: GridFSBucket;
54
122
  idSource = ModelCrudUtil.uuidSource();
55
123
  client: MongoClient;
56
124
  config: MongoModelConfig;
57
125
 
58
- constructor(config: MongoModelConfig) { this.config = config; }
126
+ constructor(config: MongoModelConfig) {
127
+ this.config = config;
128
+ }
59
129
 
60
- async * #iterateCursor<T extends ModelType>(cls: Class<T>, cursor: FindCursor, options?: ModelListOptions & ModelPageOptions<number>): AsyncGenerator<T[]> {
130
+ async *#iterateCursor<T extends ModelType>(
131
+ cls: Class<T>,
132
+ cursor: FindCursor,
133
+ options?: ModelListOptions & ModelPageOptions<number>
134
+ ): AsyncGenerator<T[]> {
61
135
  const batchSize = options?.batchSizeHint ?? 100;
62
136
  let batch: T[] = [];
63
137
  const maxCount = options?.limit ?? Number.MAX_SAFE_INTEGER;
64
138
  for await (const item of cursor
65
139
  .batchSize(batchSize)
66
140
  .limit(maxCount)
67
- .skip(options?.offset ?? 0)
68
- ) {
141
+ .skip(options?.offset ?? 0)) {
69
142
  if (options?.abort?.aborted) {
70
143
  cursor.close();
71
144
  break;
@@ -95,8 +168,7 @@ export class MongoModelService implements
95
168
  }
96
169
 
97
170
  const where = this.getWhereFilter(cls, whereClause);
98
- let q = store.find(where, { timeout: true })
99
- .batchSize(100);
171
+ let q = store.find(where, { timeout: true }).batchSize(100);
100
172
 
101
173
  // TODO: We could cache this
102
174
  if ('sort' in idx) {
@@ -105,7 +177,7 @@ export class MongoModelService implements
105
177
  return q;
106
178
  }
107
179
 
108
- restoreId(item: { id?: string, _id?: unknown }): void {
180
+ restoreId(item: { id?: string; _id?: unknown }): void {
109
181
  if (item._id) {
110
182
  item.id ??= MongoUtil.idToString(castTo(item._id));
111
183
  delete item._id;
@@ -125,11 +197,11 @@ export class MongoModelService implements
125
197
  return item;
126
198
  }
127
199
 
128
- preUpdate<T extends OptionalId<ModelType>>(item: T & { _id?: Binary, id: string }): string;
200
+ preUpdate<T extends OptionalId<ModelType>>(item: T & { _id?: Binary; id: string }): string;
129
201
  preUpdate<T extends OptionalId<ModelType>>(item: Omit<T, 'id'> & { _id?: Binary }): undefined;
130
- preUpdate<T extends OptionalId<ModelType>>(item: T & { _id?: Binary, id: undefined }): undefined;
131
- preUpdate<T extends OptionalId<ModelType>>(item: T & { _id?: Binary, id?: string }): string | undefined {
132
- if (item && item.id) {
202
+ preUpdate<T extends OptionalId<ModelType>>(item: T & { _id?: Binary; id: undefined }): undefined;
203
+ preUpdate<T extends OptionalId<ModelType>>(item: T & { _id?: Binary; id?: string }): string | undefined {
204
+ if (item?.id) {
133
205
  const id = item.id;
134
206
  item._id = MongoUtil.uuid(id);
135
207
  if (!this.config.storeId) {
@@ -153,7 +225,7 @@ export class MongoModelService implements
153
225
  async initializeClient(): Promise<void> {
154
226
  this.client = await MongoClient.connect(this.config.url, {
155
227
  ...this.config.connectionOptions,
156
- useBigInt64: true,
228
+ useBigInt64: true
157
229
  });
158
230
  this.#db = this.client.db(this.config.namespace);
159
231
  this.#bucket = new GridFSBucket(this.#db, {
@@ -174,7 +246,7 @@ export class MongoModelService implements
174
246
  }
175
247
 
176
248
  // Storage
177
- async createStorage(): Promise<void> { }
249
+ async createStorage(): Promise<void> {}
178
250
 
179
251
  async deleteStorage(): Promise<void> {
180
252
  await this.#db.dropDatabase();
@@ -182,10 +254,7 @@ export class MongoModelService implements
182
254
 
183
255
  async upsertModel(cls: Class): Promise<void> {
184
256
  const col = await this.getStore(cls);
185
- const indices = [
186
- ...ModelRegistryIndex.getIndices(cls).map(idx => MongoUtil.getIndex(cls, idx)),
187
- ...MongoUtil.getExtraIndices(cls)
188
- ];
257
+ const indices = [...ModelRegistryIndex.getIndices(cls).map(idx => MongoUtil.getIndex(cls, idx)), ...MongoUtil.getExtraIndices(cls)];
189
258
  const existingIndices = (await col.indexes().catch(() => [])).filter(idx => idx.name !== '_id_');
190
259
 
191
260
  const pendingMap = Object.fromEntries(indices.map(pair => [pair[1].name!, pair]));
@@ -219,7 +288,7 @@ export class MongoModelService implements
219
288
  }
220
289
 
221
290
  async truncateBlob(): Promise<void> {
222
- await this.#bucket.drop().catch(() => { });
291
+ await this.#bucket.drop().catch(() => {});
223
292
  }
224
293
 
225
294
  /**
@@ -254,7 +323,7 @@ export class MongoModelService implements
254
323
  }
255
324
  return this.postUpdate(cleaned, id);
256
325
  } catch (error) {
257
- throw isDuplicateKeyError(error) ? new ExistsError(cls, id) : error;
326
+ throw handleDuplicateKeyError(cls, id, error);
258
327
  }
259
328
  }
260
329
 
@@ -275,13 +344,9 @@ export class MongoModelService implements
275
344
  const store = await this.getStore(cls);
276
345
 
277
346
  try {
278
- await store.updateOne(
279
- this.getIdFilter(cls, id, false),
280
- { $set: cleaned },
281
- { upsert: true }
282
- );
347
+ await store.updateOne(this.getIdFilter(cls, id, false), { $set: cleaned }, { upsert: true });
283
348
  } catch (error) {
284
- throw isDuplicateKeyError(error) ? new ExistsError(cls, id) : error;
349
+ throw handleDuplicateKeyError(cls, id, error);
285
350
  }
286
351
  return this.postUpdate(cleaned, id);
287
352
  }
@@ -292,24 +357,23 @@ export class MongoModelService implements
292
357
  const final = await ModelCrudUtil.prePartialUpdate(cls, item, view);
293
358
  const simple = MongoUtil.extractSimple(cls, final, undefined, false);
294
359
 
295
- const operation: Partial<T> = castTo(Object
296
- .entries(simple)
297
- .reduce<Partial<Record<'$unset' | '$set', Record<string, unknown>>>>((document, [key, value]) => {
360
+ const operation: Partial<T> = castTo(
361
+ Object.entries(simple).reduce<Partial<Record<'$unset' | '$set', Record<string, unknown>>>>((document, [key, value]) => {
298
362
  if (value === null || value === undefined) {
299
363
  (document.$unset ??= {})[key] = value;
300
364
  } else {
301
365
  (document.$set ??= {})[key] = value;
302
366
  }
303
367
  return document;
304
- }, {}));
368
+ }, {})
369
+ );
305
370
 
306
371
  const id = item.id;
307
372
 
308
- const result = await store.findOneAndUpdate(
309
- this.getIdFilter(cls, id),
310
- operation,
311
- { returnDocument: 'after', includeResultMetadata: true }
312
- );
373
+ const result = await store.findOneAndUpdate(this.getIdFilter(cls, id), operation, {
374
+ returnDocument: 'after',
375
+ includeResultMetadata: true
376
+ });
313
377
 
314
378
  if (!result.value) {
315
379
  throw new NotFoundError(cls, id);
@@ -326,7 +390,7 @@ export class MongoModelService implements
326
390
  }
327
391
  }
328
392
 
329
- async * list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]> {
393
+ async *list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]> {
330
394
  const store = await this.getStore(cls);
331
395
  const cursor = store.find(this.getWhereFilter(cls, {}), { timeout: true });
332
396
  yield* this.#iterateCursor(cls, cursor, options);
@@ -334,7 +398,10 @@ export class MongoModelService implements
334
398
 
335
399
  // Blob
336
400
  async upsertBlob(location: string, input: BinaryType, metadata?: BinaryMetadata, overwrite = true): Promise<void> {
337
- const existing = await this.getBlobMetadata(location).then(() => true, () => false);
401
+ const existing = await this.getBlobMetadata(location).then(
402
+ () => true,
403
+ () => false
404
+ );
338
405
  if (!overwrite && existing) {
339
406
  return;
340
407
  }
@@ -366,10 +433,9 @@ export class MongoModelService implements
366
433
  }
367
434
 
368
435
  async updateBlobMetadata(location: string, metadata: BinaryMetadata): Promise<void> {
369
- await this.#db.collection<{ metadata: BinaryMetadata }>(`${ModelBlobNamespace}.files`).findOneAndUpdate(
370
- { filename: location },
371
- { $set: { metadata, contentType: metadata.contentType! } },
372
- );
436
+ await this.#db
437
+ .collection<{ metadata: BinaryMetadata }>(`${ModelBlobNamespace}.files`)
438
+ .findOneAndUpdate({ filename: location }, { $set: { metadata, contentType: metadata.contentType! } });
373
439
  }
374
440
 
375
441
  // Bulk
@@ -402,7 +468,10 @@ export class MongoModelService implements
402
468
  bulk.insert(operation.insert);
403
469
  } else if (operation.upsert) {
404
470
  const id = this.preUpdate(operation.upsert);
405
- bulk.find({ _id: MongoUtil.uuid(id!) }).upsert().updateOne({ $set: operation.upsert });
471
+ bulk
472
+ .find({ _id: MongoUtil.uuid(id!) })
473
+ .upsert()
474
+ .updateOne({ $set: operation.upsert });
406
475
  } else if (operation.update) {
407
476
  const id = this.preUpdate(operation.update);
408
477
  bulk.find({ _id: MongoUtil.uuid(id) }).update({ $set: operation.update });
@@ -451,89 +520,77 @@ export class MongoModelService implements
451
520
  }
452
521
 
453
522
  // Indexed
454
- async getByIndex<
455
- T extends ModelType,
456
- K extends KeyedIndexSelection<T>,
457
- S extends SortedIndexSelection<T>
458
- >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<T> {
523
+ async getByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
524
+ cls: Class<T>,
525
+ idx: SingleItemIndex<T, K, S>,
526
+ body: FullKeyedIndexBody<T, K, S>
527
+ ): Promise<T> {
459
528
  const store = await this.getStore(cls);
460
529
 
461
530
  const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
462
531
 
463
- const result = await store.findOne(
464
- this.getWhereFilter(cls, castTo(computed.project({ sort: true, includeId: true })))
465
- );
532
+ const result = await store.findOne(this.getWhereFilter(cls, castTo(computed.project({ sort: true, includeId: true }))));
466
533
  if (!result) {
467
534
  throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
468
535
  }
469
536
  return await this.postLoad(cls, result);
470
537
  }
471
538
 
472
- async deleteByIndex<
473
- T extends ModelType,
474
- K extends KeyedIndexSelection<T>,
475
- S extends SortedIndexSelection<T>
476
- >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<void> {
539
+ async deleteByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
540
+ cls: Class<T>,
541
+ idx: SingleItemIndex<T, K, S>,
542
+ body: FullKeyedIndexBody<T, K, S>
543
+ ): Promise<void> {
477
544
  const store = await this.getStore(cls);
478
545
  const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
479
546
 
480
- const result = await store.deleteOne(
481
- this.getWhereFilter(cls, castTo(computed.project({ sort: true, includeId: true })))
482
- );
547
+ const result = await store.deleteOne(this.getWhereFilter(cls, castTo(computed.project({ sort: true, includeId: true }))));
483
548
  if (!result.deletedCount) {
484
549
  throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
485
550
  }
486
551
  }
487
552
 
488
- upsertByIndex<
489
- T extends ModelType,
490
- K extends KeyedIndexSelection<T>,
491
- S extends SortedIndexSelection<T>
492
- >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: OptionalId<T>): Promise<T> {
553
+ upsertByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
554
+ cls: Class<T>,
555
+ idx: SingleItemIndex<T, K, S>,
556
+ body: OptionalId<T>
557
+ ): Promise<T> {
493
558
  return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
494
559
  }
495
560
 
496
- updateByIndex<
497
- T extends ModelType,
498
- K extends KeyedIndexSelection<T>,
499
- S extends SortedIndexSelection<T>
500
- >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: T): Promise<T> {
561
+ updateByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
562
+ cls: Class<T>,
563
+ idx: SingleItemIndex<T, K, S>,
564
+ body: T
565
+ ): Promise<T> {
501
566
  return ModelIndexedUtil.naiveUpdate(this, cls, idx, body);
502
567
  }
503
568
 
504
- async updatePartialByIndex<
505
- T extends ModelType,
506
- K extends KeyedIndexSelection<T>,
507
- S extends SortedIndexSelection<T>
508
- >(cls: Class<T>, idx: SingleItemIndex<T, K>, body: FullKeyedIndexWithPartialBody<T, K, S>): Promise<T> {
569
+ async updatePartialByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
570
+ cls: Class<T>,
571
+ idx: SingleItemIndex<T, K>,
572
+ body: FullKeyedIndexWithPartialBody<T, K, S>
573
+ ): Promise<T> {
509
574
  const item = await ModelCrudUtil.naivePartialUpdate(cls, () => this.getByIndex(cls, idx, castTo(body)), castTo(body));
510
575
  return this.update(cls, item);
511
576
  }
512
577
 
513
- async pageByIndex<
514
- T extends ModelType,
515
- K extends KeyedIndexSelection<T>,
516
- S extends SortedIndexSelection<T>
517
- >(
578
+ async pageByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
518
579
  cls: Class<T>,
519
580
  idx: SortedIndex<T, K, S>,
520
581
  body: KeyedIndexBody<T, K>,
521
- options?: ModelPageOptions,
582
+ options?: ModelPageOptions
522
583
  ): Promise<ModelPageResult<T>> {
523
584
  {
524
585
  const offset = options?.offset ? JSONUtil.fromBase64<number>(options.offset) : 0;
525
- const cursor = (await this.#buildIndexQuery(cls, idx, body));
586
+ const cursor = await this.#buildIndexQuery(cls, idx, body);
526
587
  const batches = await Array.fromAsync(this.#iterateCursor(cls, cursor, { limit: 100, ...options, offset }));
527
588
  const items = batches.flat();
528
589
  return { items, nextOffset: items.length ? JSONUtil.toBase64(offset + items.length) : undefined };
529
590
  }
530
591
  }
531
592
 
532
- async * listByIndex<
533
- T extends ModelType,
534
- K extends KeyedIndexSelection<T>,
535
- S extends SortedIndexSelection<T>
536
- >(
593
+ async *listByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
537
594
  cls: Class<T>,
538
595
  idx: SortedIndex<T, K, S>,
539
596
  body: KeyedIndexBody<T, K>,
@@ -548,18 +605,17 @@ export class MongoModelService implements
548
605
  S extends SortedIndexSelection<T>,
549
606
  K extends KeyedIndexSelection<T>,
550
607
  B extends SortedIndexSelectionType<T, S> & string
551
- >(
552
- cls: Class<T>,
553
- idx: SortedIndex<T, K, S>,
554
- body: KeyedIndexBody<T, K>,
555
- prefix: B,
556
- options?: ModelIndexedSearchOptions
557
- ): Promise<T[]> {
558
- const cursor = (await this.#buildIndexQuery(cls, idx, body, (where) => castTo({
559
- $and: [where, {
560
- [idx.sortTemplate[0].path.join('.')]: ModelIndexedUtil.getSuggestRegex(prefix)
561
- }]
562
- })));
608
+ >(cls: Class<T>, idx: SortedIndex<T, K, S>, body: KeyedIndexBody<T, K>, prefix: B, options?: ModelIndexedSearchOptions): Promise<T[]> {
609
+ const cursor = await this.#buildIndexQuery(cls, idx, body, where =>
610
+ castTo({
611
+ $and: [
612
+ where,
613
+ {
614
+ [idx.sortTemplate[0].path.join('.')]: ModelIndexedUtil.getSuggestRegex(prefix)
615
+ }
616
+ ]
617
+ })
618
+ );
563
619
  const batches = await Array.fromAsync(this.#iterateCursor(cls, cursor, { limit: 10, ...options }));
564
620
 
565
621
  return batches.flat();
@@ -622,15 +678,14 @@ export class MongoModelService implements
622
678
  const item = await ModelCrudUtil.prePartialUpdate(cls, data);
623
679
  const col = await this.getStore(cls);
624
680
  const items = MongoUtil.extractSimple(cls, item);
625
- const final = Object.entries(items).reduce<Partial<Record<'$unset' | '$set', Record<string, unknown>>>>(
626
- (document, [key, value]) => {
627
- if (value === null || value === undefined) {
628
- (document.$unset ??= {})[key] = value;
629
- } else {
630
- (document.$set ??= {})[key] = value;
631
- }
632
- return document;
633
- }, {});
681
+ const final = Object.entries(items).reduce<Partial<Record<'$unset' | '$set', Record<string, unknown>>>>((document, [key, value]) => {
682
+ if (value === null || value === undefined) {
683
+ (document.$unset ??= {})[key] = value;
684
+ } else {
685
+ (document.$set ??= {})[key] = value;
686
+ }
687
+ return document;
688
+ }, {});
634
689
 
635
690
  const filter = MongoUtil.extractWhereFilter(cls, query.where);
636
691
  const result = await col.updateMany(filter, castTo(final));
@@ -664,7 +719,7 @@ export class MongoModelService implements
664
719
  }
665
720
  ];
666
721
 
667
- const result = await col.aggregate<{ _id: ObjectId, count: number }>(aggregations).toArray();
722
+ const result = await col.aggregate<{ _id: ObjectId; count: number }>(aggregations).toArray();
668
723
 
669
724
  return result
670
725
  .map(item => ({
@@ -675,18 +730,28 @@ export class MongoModelService implements
675
730
  }
676
731
 
677
732
  // Suggest
678
- async suggestValuesByQuery<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Promise<string[]> {
733
+ async suggestValuesByQuery<T extends ModelType>(
734
+ cls: Class<T>,
735
+ field: ValidStringFields<T>,
736
+ prefix?: string,
737
+ query?: PageableModelQuery<T>
738
+ ): Promise<string[]> {
679
739
  await QueryVerifier.verify(cls, query);
680
740
  const resolvedQuery = ModelQuerySuggestUtil.getSuggestFieldQuery<T>(cls, field, prefix, query);
681
741
  const results = await this.query<T>(cls, resolvedQuery);
682
- return ModelQuerySuggestUtil.combineSuggestResults<T, string>(cls, field, prefix, results, (a) => a, query && query.limit);
742
+ return ModelQuerySuggestUtil.combineSuggestResults<T, string>(cls, field, prefix, results, a => a, query?.limit);
683
743
  }
684
744
 
685
- async suggestByQuery<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Promise<T[]> {
745
+ async suggestByQuery<T extends ModelType>(
746
+ cls: Class<T>,
747
+ field: ValidStringFields<T>,
748
+ prefix?: string,
749
+ query?: PageableModelQuery<T>
750
+ ): Promise<T[]> {
686
751
  await QueryVerifier.verify(cls, query);
687
752
  const resolvedQuery = ModelQuerySuggestUtil.getSuggestQuery<T>(cls, field, prefix, query);
688
753
  const results = await this.query<T>(cls, resolvedQuery);
689
- return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, results, (_, b) => b, query && query.limit);
754
+ return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, results, (_, b) => b, query?.limit);
690
755
  }
691
756
 
692
757
  // Other
@@ -699,12 +764,14 @@ export class MongoModelService implements
699
764
  search = { $search: search, $language: 'en' };
700
765
  }
701
766
 
702
- (query.sort ??= []).unshift(castTo<(typeof query.sort[0])>({
703
- score: { $meta: 'textScore' }
704
- }));
767
+ (query.sort ??= []).unshift(
768
+ castTo<(typeof query.sort)[0]>({
769
+ score: { $meta: 'textScore' }
770
+ })
771
+ );
705
772
 
706
773
  const cursor = col.find(castTo({ $and: [{ $text: search }, filter] }), {});
707
774
  const items = await MongoUtil.prepareCursor(cls, cursor, query).toArray();
708
775
  return await Promise.all(items.map(item => this.postLoad(cls, item)));
709
776
  }
710
- }
777
+ }
@@ -11,4 +11,4 @@ export const service: ServiceDescriptor = {
11
11
  // Temp until mongo image fixes orbstack issue
12
12
  GLIBC_TUNABLES: 'glibc.pthread.rseq=1'
13
13
  }
14
- };
14
+ };