@travetto/model-mongo 8.0.0-alpha.8 → 8.0.0

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
- * [Indexed](https://github.com/travetto/travetto/tree/main/module/model/src/types/indexed.ts#L11)
23
19
  * [Blob](https://github.com/travetto/travetto/tree/main/module/model/src/types/blob.ts#L8)
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,23 +154,20 @@ 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();
167
- let creds = '';
162
+ ).toString();
163
+ let credentials = '';
168
164
  if (this.username) {
169
- creds = `${[this.username, this.password].filter(part => !!part).join(':')}@`;
165
+ credentials = `${[this.username, this.password].filter(part => !!part).join(':')}@`;
170
166
  }
171
- const url = `mongodb${this.srvRecord ? '+srv' : ''}://${creds}${hosts}/${this.namespace}?${optionString}`;
167
+ const url = `mongodb${this.srvRecord ? '+srv' : ''}://${credentials}${hosts}/${this.namespace}?${optionString}`;
172
168
  return url;
173
169
  }
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,38 +1,42 @@
1
1
  {
2
2
  "name": "@travetto/model-mongo",
3
- "version": "8.0.0-alpha.8",
4
- "type": "module",
3
+ "version": "8.0.0",
5
4
  "description": "Mongo backing for the travetto model module.",
6
5
  "keywords": [
6
+ "database",
7
7
  "mongo",
8
8
  "travetto",
9
- "database",
10
9
  "typescript"
11
10
  ],
12
11
  "homepage": "https://travetto.io",
13
12
  "license": "MIT",
14
13
  "author": {
15
- "email": "travetto.framework@gmail.com",
16
- "name": "Travetto Framework"
14
+ "name": "Travetto Framework",
15
+ "email": "travetto.framework@gmail.com"
16
+ },
17
+ "repository": {
18
+ "url": "git+https://github.com/travetto/travetto.git",
19
+ "directory": "module/model-mongo"
17
20
  },
18
21
  "files": [
19
22
  "__index__.ts",
20
23
  "src",
21
24
  "support"
22
25
  ],
26
+ "type": "module",
23
27
  "main": "__index__.ts",
24
- "repository": {
25
- "url": "git+https://github.com/travetto/travetto.git",
26
- "directory": "module/model-mongo"
28
+ "publishConfig": {
29
+ "access": "public"
27
30
  },
28
31
  "dependencies": {
29
- "@travetto/config": "^8.0.0-alpha.8",
30
- "@travetto/model": "^8.0.0-alpha.8",
31
- "@travetto/model-query": "^8.0.0-alpha.8",
32
- "mongodb": "^7.1.0"
32
+ "@travetto/config": "^8.0.0",
33
+ "@travetto/model": "^8.0.0",
34
+ "@travetto/model-indexed": "^8.0.0",
35
+ "@travetto/model-query": "^8.0.0",
36
+ "mongodb": "^7.5.0"
33
37
  },
34
38
  "peerDependencies": {
35
- "@travetto/cli": "^8.0.0-alpha.13"
39
+ "@travetto/cli": "^8.0.0"
36
40
  },
37
41
  "peerDependenciesMeta": {
38
42
  "@travetto/cli": {
@@ -41,8 +45,5 @@
41
45
  },
42
46
  "travetto": {
43
47
  "displayName": "MongoDB Model Support"
44
- },
45
- "publishConfig": {
46
- "access": "public"
47
48
  }
48
49
  }
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,20 +127,17 @@ 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();
140
- let creds = '';
135
+ ).toString();
136
+ let credentials = '';
141
137
  if (this.username) {
142
- creds = `${[this.username, this.password].filter(part => !!part).join(':')}@`;
138
+ credentials = `${[this.username, this.password].filter(part => !!part).join(':')}@`;
143
139
  }
144
- const url = `mongodb${this.srvRecord ? '+srv' : ''}://${creds}${hosts}/${this.namespace}?${optionString}`;
140
+ const url = `mongodb${this.srvRecord ? '+srv' : ''}://${credentials}${hosts}/${this.namespace}?${optionString}`;
145
141
  return url;
146
142
  }
147
- }
143
+ }
@@ -1,17 +1,22 @@
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, TypedObject, BinaryUtil } from '@travetto/runtime';
7
- import { type DistanceUnit, type PageableModelQuery, type WhereClause, ModelQueryUtil } from '@travetto/model-query';
8
- import type { ModelType, IndexField, IndexConfig } from '@travetto/model';
9
- import { DataUtil, SchemaRegistryIndex, type Point } from '@travetto/schema';
12
+ import { type IndexConfig, IndexNotSupported, type ModelType } from '@travetto/model';
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';
10
17
 
11
18
  const PointConcrete = toConcrete<Point>();
12
19
 
13
- type IdxConfig = CreateIndexesOptions;
14
-
15
20
  /**
16
21
  * Converting units to various radians
17
22
  */
@@ -25,38 +30,31 @@ const RADIANS_TO: Record<DistanceUnit, number> = {
25
30
 
26
31
  export type WithId<T, I = unknown> = T & { _id?: I };
27
32
  export type BasicIdx = Record<string, IndexDirection>;
28
- export type PlainIdx = Record<string, -1 | 0 | 1>;
33
+
34
+ function flattenKeys(obj: Record<string, unknown>, prefix = ''): Record<string, 1 | -1> {
35
+ const out: Record<string, 1 | -1> = {};
36
+ for (const [key, value] of Object.entries(obj)) {
37
+ const path = prefix ? `${prefix}.${key}` : key;
38
+ if (typeof value === 'object' && value !== null) {
39
+ Object.assign(out, flattenKeys(castTo(value), path));
40
+ } else {
41
+ out[path] = typeof value === 'boolean' ? (value ? 1 : -1) : castTo<-1 | 1>(value);
42
+ }
43
+ }
44
+ return out;
45
+ }
29
46
 
30
47
  /**
31
48
  * Basic mongo utils for conforming to the model module
32
49
  */
33
50
  export class MongoUtil {
34
-
35
51
  static namespaceIndex(cls: Class, name: string): string {
36
52
  return `${cls.Ⲑid}__${name}`.replace(/[^a-zA-Z0-9_]+/g, '_');
37
53
  }
38
54
 
39
- static toIndex<T extends ModelType>(field: IndexField<T>): PlainIdx {
40
- const keys = [];
41
- while (typeof field !== 'number' && typeof field !== 'boolean' && Object.keys(field)) {
42
- const key = TypedObject.keys(field)[0];
43
- field = castTo(field[key]);
44
- keys.push(key);
45
- }
46
- const rf: number | boolean = castTo(field);
47
- return {
48
- [keys.join('.')]: typeof rf === 'boolean' ? (rf ? 1 : 0) : castTo<-1 | 1 | 0>(rf)
49
- };
50
- }
51
-
52
55
  static uuid(value: string): Binary {
53
56
  try {
54
- return new Binary(
55
- BinaryUtil.binaryArrayToUint8Array(
56
- CodecUtil.fromHexString(value.replaceAll('-', ''))
57
- ),
58
- Binary.SUBTYPE_UUID
59
- );
57
+ return new Binary(BinaryUtil.binaryArrayToUint8Array(CodecUtil.fromHexString(value.replaceAll('-', ''))), Binary.SUBTYPE_UUID);
60
58
  } catch (err) {
61
59
  if (err instanceof RuntimeError && err.message === 'Invalid hex string') {
62
60
  return null!;
@@ -96,7 +94,12 @@ export class MongoUtil {
96
94
  }
97
95
 
98
96
  /**/
99
- 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> {
100
103
  const fields = base ? SchemaRegistryIndex.getOptional(base)?.getFields() : undefined;
101
104
  const out: Record<string, unknown> = {};
102
105
  const sub = item;
@@ -142,7 +145,7 @@ export class MongoUtil {
142
145
  value.$regex = DataUtil.toRegex(castTo(value.$regex));
143
146
  } else if (firstKey && '$near' in value) {
144
147
  const dist: number = castTo(value.$maxDistance);
145
- const distance = dist / RADIANS_TO[(castTo<DistanceUnit>(value.$unit) ?? 'km')];
148
+ const distance = dist / RADIANS_TO[castTo<DistanceUnit>(value.$unit) ?? 'km'];
146
149
  value.$maxDistance = distance;
147
150
  delete value.$unit;
148
151
  } else if (firstKey && '$geoWithin' in value) {
@@ -166,8 +169,8 @@ export class MongoUtil {
166
169
  return out;
167
170
  }
168
171
 
169
- static getExtraIndices<T extends ModelType>(cls: Class<T>): [BasicIdx, IdxConfig][] {
170
- const out: [BasicIdx, IdxConfig][] = [];
172
+ static getExtraIndices<T extends ModelType>(cls: Class<T>): [BasicIdx, CreateIndexesOptions][] {
173
+ const out: [BasicIdx, CreateIndexesOptions][] = [];
171
174
  const textFields: string[] = [];
172
175
  SchemaRegistryIndex.visitFields(cls, (field, path) => {
173
176
  if (field.type === PointConcrete) {
@@ -185,30 +188,44 @@ export class MongoUtil {
185
188
  return out;
186
189
  }
187
190
 
188
- static getPlainIndex(idx: IndexConfig<ModelType>): PlainIdx {
189
- let out: PlainIdx = {};
190
- for (const config of idx.fields.map(value => this.toIndex(value))) {
191
- out = Object.assign(out, config);
192
- }
193
- return out;
194
- }
191
+ static getIndex(cls: Class, idx: IndexConfig): [BasicIdx, CreateIndexesOptions] {
192
+ const name = this.namespaceIndex(cls, idx.name);
193
+ if (isModelQueryIndex(idx)) {
194
+ const out = idx.fields.reduce(
195
+ (acc, field) => Object.assign(acc, { ...flattenKeys(castTo(field)) }),
196
+ castTo<Record<string, -1 | 0 | 1>>({})
197
+ );
195
198
 
196
- static getIndices<T extends ModelType>(cls: Class<T>, indices: IndexConfig<ModelType>[] = []): [BasicIdx, IdxConfig][] {
197
- return [
198
- ...indices.map(idx => [this.getPlainIndex(idx), { ...(idx.type === 'unique' ? { unique: true } : {}), name: this.namespaceIndex(cls, idx.name) }] as const),
199
- ...this.getExtraIndices(cls)
200
- ].map(idx => [...idx]);
199
+ return [out, { name, unique: !!idx.unique }];
200
+ } else if (isModelIndexedIndex(idx)) {
201
+ const filter = Object.fromEntries([
202
+ ...idx.keyTemplate.map(({ path }) => [path.join('.'), 1]),
203
+ ...idx.sortTemplate.map(({ path, value }) => [path.join('.'), value === -1 ? -1 : 1])
204
+ ]);
205
+ switch (idx.type) {
206
+ case 'indexed:keyed':
207
+ return [filter, { name, unique: idx.unique }];
208
+ case 'indexed:sorted':
209
+ return [filter, { name }];
210
+ }
211
+ } else {
212
+ throw new IndexNotSupported(cls, idx);
213
+ }
201
214
  }
202
215
 
203
- 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> {
204
221
  if (query.select) {
205
222
  const selectKey = Object.keys(query.select)[0];
206
223
  const select = typeof selectKey === 'string' && selectKey.startsWith('$') ? query.select : this.extractSimple(cls, query.select);
207
224
  // Remove id if not explicitly defined, and selecting fields directly
208
- if (!select['_id']) {
225
+ if (!select._id) {
209
226
  const values = new Set([...Object.values(select)]);
210
227
  if (values.has(1) || values.has(true)) {
211
- select['_id'] = false;
228
+ select._id = false;
212
229
  }
213
230
  }
214
231
  cursor.project(select);
@@ -236,9 +253,9 @@ export class MongoUtil {
236
253
  existing.expireAfterSeconds !== pendingOptions.expireAfterSeconds ||
237
254
  existing.bucketSize !== pendingOptions.bucketSize;
238
255
 
239
- const existingFields = existing.textIndexVersion ?
240
- Object.fromEntries(Object.entries(existing.weights ?? {}).map(([key]) => [key, 'text'])) :
241
- existing.key;
256
+ const existingFields = existing.textIndexVersion
257
+ ? Object.fromEntries(Object.entries(existing.weights ?? {}).map(([key]) => [key, 'text']))
258
+ : existing.key;
242
259
 
243
260
  const pendingKeySet = new Set(Object.keys(pendingKey));
244
261
  const existingKeySet = new Set(Object.keys(existingFields));
@@ -248,10 +265,10 @@ export class MongoUtil {
248
265
  const overlap = [...pendingKeySet.intersection(existingKeySet)];
249
266
  changed ||= overlap.length !== pendingKeySet.size;
250
267
 
251
- for (let i = 0; i < overlap.length && !changed; i++) {
268
+ for (let i = 0; i < overlap.length && !changed; i += 1) {
252
269
  changed ||= existingFields[overlap[i]] !== pendingKey[overlap[i]];
253
270
  }
254
271
 
255
272
  return changed;
256
273
  }
257
- }
274
+ }
package/src/service.ts CHANGED
@@ -1,60 +1,183 @@
1
1
  import {
2
2
  type Binary,
3
- type Db, GridFSBucket, MongoClient, type GridFSFile, type Collection,
4
- type ObjectId, type RootFilterOperators, type Filter,
3
+ type Collection,
4
+ type Db,
5
+ type Filter,
6
+ type FindCursor,
7
+ GridFSBucket,
8
+ type GridFSFile,
9
+ MongoClient,
10
+ MongoServerError,
5
11
  type WithId as MongoWithId,
12
+ type ObjectId,
13
+ type RootFilterOperators
6
14
  } from 'mongodb';
7
15
 
16
+ import { Injectable, PostConstruct } from '@travetto/di';
8
17
  import {
9
- ModelRegistryIndex, type ModelType, type OptionalId, type ModelCrudSupport, type ModelStorageSupport,
10
- type ModelExpirySupport, type ModelBulkSupport, type ModelIndexedSupport, type BulkOperation, type BulkResponse,
11
- NotFoundError, ExistsError, type ModelBlobSupport,
12
- ModelCrudUtil, ModelIndexedUtil, ModelStorageUtil, ModelExpiryUtil, ModelBulkUtil
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
13
36
  } from '@travetto/model';
14
37
  import {
15
- type ModelQuery, type ModelQueryCrudSupport, type ModelQueryFacetSupport, type ModelQuerySupport,
16
- type PageableModelQuery, type ValidStringFields, type WhereClause, type ModelQuerySuggestSupport,
17
- QueryVerifier, ModelQueryUtil, ModelQuerySuggestUtil, ModelQueryCrudUtil,
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
52
+ } from '@travetto/model-indexed';
53
+ import {
54
+ type ModelQuery,
55
+ type ModelQueryCrudSupport,
56
+ ModelQueryCrudUtil,
18
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
19
67
  } from '@travetto/model-query';
20
-
21
68
  import {
22
- ShutdownManager, type Class, type DeepPartial, TypedObject,
23
- castTo, asFull, type BinaryMetadata, type ByteRange, type BinaryType, BinaryUtil, BinaryMetadataUtil,
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
24
80
  } from '@travetto/runtime';
25
- import { Injectable, PostConstruct } from '@travetto/di';
26
81
 
27
- import { MongoUtil, type PlainIdx, type WithId } from './internal/util.ts';
28
82
  import type { MongoModelConfig } from './config.ts';
29
-
30
- const ListIndexSymbol = Symbol();
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
 
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
+ };
100
+
36
101
  export const ModelBlobNamespace = '__blobs';
37
102
 
38
103
  /**
39
104
  * Mongo-based model source
40
105
  */
41
106
  @Injectable()
42
- export class MongoModelService implements
43
- ModelCrudSupport, ModelStorageSupport,
44
- ModelBulkSupport, ModelBlobSupport,
45
- ModelIndexedSupport, ModelQuerySupport,
46
- ModelQueryCrudSupport, ModelQueryFacetSupport,
47
- ModelQuerySuggestSupport, ModelExpirySupport {
48
-
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
+ {
49
120
  #db: Db;
50
121
  #bucket: GridFSBucket;
51
122
  idSource = ModelCrudUtil.uuidSource();
52
123
  client: MongoClient;
53
124
  config: MongoModelConfig;
54
125
 
55
- constructor(config: MongoModelConfig) { this.config = config; }
126
+ constructor(config: MongoModelConfig) {
127
+ this.config = config;
128
+ }
129
+
130
+ async *#iterateCursor<T extends ModelType>(
131
+ cls: Class<T>,
132
+ cursor: FindCursor,
133
+ options?: ModelListOptions & ModelPageOptions<number>
134
+ ): AsyncGenerator<T[]> {
135
+ const batchSize = options?.batchSizeHint ?? 100;
136
+ let batch: T[] = [];
137
+ const maxCount = options?.limit ?? Number.MAX_SAFE_INTEGER;
138
+ for await (const item of cursor
139
+ .batchSize(batchSize)
140
+ .limit(maxCount)
141
+ .skip(options?.offset ?? 0)) {
142
+ if (options?.abort?.aborted) {
143
+ cursor.close();
144
+ break;
145
+ }
146
+ batch.push(item);
147
+ if (batch.length >= batchSize) {
148
+ yield await ModelCrudUtil.filterOutNotFound(batch.map(i => this.postLoad(cls, i)));
149
+ batch = [];
150
+ }
151
+ }
152
+ if (batch.length) {
153
+ yield await ModelCrudUtil.filterOutNotFound(batch.map(i => this.postLoad(cls, i)));
154
+ }
155
+ }
56
156
 
57
- restoreId(item: { id?: string, _id?: unknown }): void {
157
+ async #buildIndexQuery<T extends ModelType>(
158
+ cls: Class<T>,
159
+ idx: SortedIndex<T>,
160
+ body: KeyedIndexBody<T>,
161
+ transformWhere?: (where: WhereClause<T>) => WhereClause<T>
162
+ ): Promise<FindCursor> {
163
+ const store = await this.getStore(cls);
164
+ const computed = ModelIndexedComputedIndex.get(idx, body).validate();
165
+ let whereClause: WhereClause<T> = castTo(computed.project());
166
+ if (transformWhere) {
167
+ whereClause = transformWhere(whereClause);
168
+ }
169
+
170
+ const where = this.getWhereFilter(cls, whereClause);
171
+ let q = store.find(where, { timeout: true }).batchSize(100);
172
+
173
+ // TODO: We could cache this
174
+ if ('sort' in idx) {
175
+ q = q.sort(idx.sortTemplate.map(({ path, value }) => [path.join('.'), value] as const));
176
+ }
177
+ return q;
178
+ }
179
+
180
+ restoreId(item: { id?: string; _id?: unknown }): void {
58
181
  if (item._id) {
59
182
  item.id ??= MongoUtil.idToString(castTo(item._id));
60
183
  delete item._id;
@@ -74,11 +197,11 @@ export class MongoModelService implements
74
197
  return item;
75
198
  }
76
199
 
77
- 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;
78
201
  preUpdate<T extends OptionalId<ModelType>>(item: Omit<T, 'id'> & { _id?: Binary }): undefined;
79
- preUpdate<T extends OptionalId<ModelType>>(item: T & { _id?: Binary, id: undefined }): undefined;
80
- preUpdate<T extends OptionalId<ModelType>>(item: T & { _id?: Binary, id?: string }): string | undefined {
81
- 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) {
82
205
  const id = item.id;
83
206
  item._id = MongoUtil.uuid(id);
84
207
  if (!this.config.storeId) {
@@ -102,7 +225,7 @@ export class MongoModelService implements
102
225
  async initializeClient(): Promise<void> {
103
226
  this.client = await MongoClient.connect(this.config.url, {
104
227
  ...this.config.connectionOptions,
105
- useBigInt64: true,
228
+ useBigInt64: true
106
229
  });
107
230
  this.#db = this.client.db(this.config.namespace);
108
231
  this.#bucket = new GridFSBucket(this.#db, {
@@ -123,7 +246,7 @@ export class MongoModelService implements
123
246
  }
124
247
 
125
248
  // Storage
126
- async createStorage(): Promise<void> { }
249
+ async createStorage(): Promise<void> {}
127
250
 
128
251
  async deleteStorage(): Promise<void> {
129
252
  await this.#db.dropDatabase();
@@ -131,7 +254,7 @@ export class MongoModelService implements
131
254
 
132
255
  async upsertModel(cls: Class): Promise<void> {
133
256
  const col = await this.getStore(cls);
134
- const indices = MongoUtil.getIndices(cls, ModelRegistryIndex.getConfig(cls).indices);
257
+ const indices = [...ModelRegistryIndex.getIndices(cls).map(idx => MongoUtil.getIndex(cls, idx)), ...MongoUtil.getExtraIndices(cls)];
135
258
  const existingIndices = (await col.indexes().catch(() => [])).filter(idx => idx.name !== '_id_');
136
259
 
137
260
  const pendingMap = Object.fromEntries(indices.map(pair => [pair[1].name!, pair]));
@@ -165,7 +288,7 @@ export class MongoModelService implements
165
288
  }
166
289
 
167
290
  async truncateBlob(): Promise<void> {
168
- await this.#bucket.drop().catch(() => { });
291
+ await this.#bucket.drop().catch(() => {});
169
292
  }
170
293
 
171
294
  /**
@@ -193,11 +316,15 @@ export class MongoModelService implements
193
316
  const id = this.preUpdate(cleaned);
194
317
 
195
318
  const store = await this.getStore(cls);
196
- const result = await store.insertOne(castTo(cleaned));
197
- if (!result.insertedId) {
198
- throw new ExistsError(cls, id);
319
+ try {
320
+ const result = await store.insertOne(castTo(cleaned));
321
+ if (!result.insertedId) {
322
+ throw new ExistsError(cls, id);
323
+ }
324
+ return this.postUpdate(cleaned, id);
325
+ } catch (error) {
326
+ throw handleDuplicateKeyError(cls, id, error);
199
327
  }
200
- return this.postUpdate(cleaned, id);
201
328
  }
202
329
 
203
330
  async update<T extends ModelType>(cls: Class<T>, item: T): Promise<T> {
@@ -217,17 +344,9 @@ export class MongoModelService implements
217
344
  const store = await this.getStore(cls);
218
345
 
219
346
  try {
220
- await store.updateOne(
221
- this.getIdFilter(cls, id, false),
222
- { $set: cleaned },
223
- { upsert: true }
224
- );
347
+ await store.updateOne(this.getIdFilter(cls, id, false), { $set: cleaned }, { upsert: true });
225
348
  } catch (error) {
226
- if (error instanceof Error && error.message.includes('duplicate key error')) {
227
- throw new ExistsError(cls, id);
228
- } else {
229
- throw error;
230
- }
349
+ throw handleDuplicateKeyError(cls, id, error);
231
350
  }
232
351
  return this.postUpdate(cleaned, id);
233
352
  }
@@ -238,24 +357,23 @@ export class MongoModelService implements
238
357
  const final = await ModelCrudUtil.prePartialUpdate(cls, item, view);
239
358
  const simple = MongoUtil.extractSimple(cls, final, undefined, false);
240
359
 
241
- const operation: Partial<T> = castTo(Object
242
- .entries(simple)
243
- .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]) => {
244
362
  if (value === null || value === undefined) {
245
363
  (document.$unset ??= {})[key] = value;
246
364
  } else {
247
365
  (document.$set ??= {})[key] = value;
248
366
  }
249
367
  return document;
250
- }, {}));
368
+ }, {})
369
+ );
251
370
 
252
371
  const id = item.id;
253
372
 
254
- const result = await store.findOneAndUpdate(
255
- this.getIdFilter(cls, id),
256
- operation,
257
- { returnDocument: 'after', includeResultMetadata: true }
258
- );
373
+ const result = await store.findOneAndUpdate(this.getIdFilter(cls, id), operation, {
374
+ returnDocument: 'after',
375
+ includeResultMetadata: true
376
+ });
259
377
 
260
378
  if (!result.value) {
261
379
  throw new NotFoundError(cls, id);
@@ -272,23 +390,18 @@ export class MongoModelService implements
272
390
  }
273
391
  }
274
392
 
275
- async * list<T extends ModelType>(cls: Class<T>): AsyncIterable<T> {
393
+ async *list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]> {
276
394
  const store = await this.getStore(cls);
277
- const cursor = store.find(this.getWhereFilter(cls, {}), { timeout: true }).batchSize(100);
278
- for await (const item of cursor) {
279
- try {
280
- yield await this.postLoad(cls, item);
281
- } catch (error) {
282
- if (!(error instanceof NotFoundError)) {
283
- throw error;
284
- }
285
- }
286
- }
395
+ const cursor = store.find(this.getWhereFilter(cls, {}), { timeout: true });
396
+ yield* this.#iterateCursor(cls, cursor, options);
287
397
  }
288
398
 
289
399
  // Blob
290
400
  async upsertBlob(location: string, input: BinaryType, metadata?: BinaryMetadata, overwrite = true): Promise<void> {
291
- const existing = await this.getBlobMetadata(location).then(() => true, () => false);
401
+ const existing = await this.getBlobMetadata(location).then(
402
+ () => true,
403
+ () => false
404
+ );
292
405
  if (!overwrite && existing) {
293
406
  return;
294
407
  }
@@ -320,10 +433,9 @@ export class MongoModelService implements
320
433
  }
321
434
 
322
435
  async updateBlobMetadata(location: string, metadata: BinaryMetadata): Promise<void> {
323
- await this.#db.collection<{ metadata: BinaryMetadata }>(`${ModelBlobNamespace}.files`).findOneAndUpdate(
324
- { filename: location },
325
- { $set: { metadata, contentType: metadata.contentType! } },
326
- );
436
+ await this.#db
437
+ .collection<{ metadata: BinaryMetadata }>(`${ModelBlobNamespace}.files`)
438
+ .findOneAndUpdate({ filename: location }, { $set: { metadata, contentType: metadata.contentType! } });
327
439
  }
328
440
 
329
441
  // Bulk
@@ -356,7 +468,10 @@ export class MongoModelService implements
356
468
  bulk.insert(operation.insert);
357
469
  } else if (operation.upsert) {
358
470
  const id = this.preUpdate(operation.upsert);
359
- 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 });
360
475
  } else if (operation.update) {
361
476
  const id = this.preUpdate(operation.update);
362
477
  bulk.find({ _id: MongoUtil.uuid(id) }).update({ $set: operation.update });
@@ -405,55 +520,105 @@ export class MongoModelService implements
405
520
  }
406
521
 
407
522
  // Indexed
408
- async getByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<T> {
409
- const { key } = ModelIndexedUtil.computeIndexKey(cls, idx, body);
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> {
410
528
  const store = await this.getStore(cls);
411
- const result = await store.findOne(
412
- this.getWhereFilter(
413
- cls,
414
- castTo(ModelIndexedUtil.projectIndex(cls, idx, body))
415
- )
416
- );
529
+
530
+ const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
531
+
532
+ const result = await store.findOne(this.getWhereFilter(cls, castTo(computed.project({ sort: true, includeId: true }))));
417
533
  if (!result) {
418
- throw new NotFoundError(`${cls.name}: ${idx}`, key);
534
+ throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
419
535
  }
420
536
  return await this.postLoad(cls, result);
421
537
  }
422
538
 
423
- async deleteByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<void> {
424
- const { key } = ModelIndexedUtil.computeIndexKey(cls, idx, body);
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> {
425
544
  const store = await this.getStore(cls);
426
- const result = await store.deleteOne(
427
- this.getWhereFilter(
428
- cls,
429
- castTo(ModelIndexedUtil.projectIndex(cls, idx, body))
430
- )
431
- );
432
- if (result.deletedCount) {
433
- return;
545
+ const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
546
+
547
+ const result = await store.deleteOne(this.getWhereFilter(cls, castTo(computed.project({ sort: true, includeId: true }))));
548
+ if (!result.deletedCount) {
549
+ throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
434
550
  }
435
- throw new NotFoundError(`${cls.name}: ${idx}`, key);
436
551
  }
437
552
 
438
- async upsertByIndex<T extends ModelType>(cls: Class<T>, idx: string, 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> {
439
558
  return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
440
559
  }
441
560
 
442
- async * listByIndex<T extends ModelType>(cls: Class<T>, idx: string, body?: DeepPartial<T>): AsyncIterable<T> {
443
- const store = await this.getStore(cls);
444
- const idxConfig = ModelRegistryIndex.getIndex(cls, idx, ['sorted', 'unsorted']);
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> {
566
+ return ModelIndexedUtil.naiveUpdate(this, cls, idx, body);
567
+ }
568
+
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> {
574
+ const item = await ModelCrudUtil.naivePartialUpdate(cls, () => this.getByIndex(cls, idx, castTo(body)), castTo(body));
575
+ return this.update(cls, item);
576
+ }
577
+
578
+ async pageByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
579
+ cls: Class<T>,
580
+ idx: SortedIndex<T, K, S>,
581
+ body: KeyedIndexBody<T, K>,
582
+ options?: ModelPageOptions
583
+ ): Promise<ModelPageResult<T>> {
584
+ {
585
+ const offset = options?.offset ? JSONUtil.fromBase64<number>(options.offset) : 0;
586
+ const cursor = await this.#buildIndexQuery(cls, idx, body);
587
+ const batches = await Array.fromAsync(this.#iterateCursor(cls, cursor, { limit: 100, ...options, offset }));
588
+ const items = batches.flat();
589
+ return { items, nextOffset: items.length ? JSONUtil.toBase64(offset + items.length) : undefined };
590
+ }
591
+ }
445
592
 
446
- const where = this.getWhereFilter(
447
- cls,
448
- castTo(ModelIndexedUtil.projectIndex(cls, idx, body, { emptySortValue: { $exists: true } }))
593
+ async *listByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
594
+ cls: Class<T>,
595
+ idx: SortedIndex<T, K, S>,
596
+ body: KeyedIndexBody<T, K>,
597
+ options?: ModelListOptions
598
+ ): AsyncIterable<T[]> {
599
+ const cursor = await this.#buildIndexQuery(cls, idx, body);
600
+ yield* this.#iterateCursor(cls, cursor, options);
601
+ }
602
+
603
+ async suggestByIndex<
604
+ T extends ModelType,
605
+ S extends SortedIndexSelection<T>,
606
+ K extends KeyedIndexSelection<T>,
607
+ B extends SortedIndexSelectionType<T, S> & string
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
+ })
449
618
  );
619
+ const batches = await Array.fromAsync(this.#iterateCursor(cls, cursor, { limit: 10, ...options }));
450
620
 
451
- const sort = castTo<{ [ListIndexSymbol]: PlainIdx }>(idxConfig)[ListIndexSymbol] ??= MongoUtil.getPlainIndex(idxConfig);
452
- const cursor = store.find(where, { timeout: true }).batchSize(100).sort(castTo(sort));
453
-
454
- for await (const item of cursor) {
455
- yield await this.postLoad(cls, item);
456
- }
621
+ return batches.flat();
457
622
  }
458
623
 
459
624
  // Query
@@ -513,15 +678,14 @@ export class MongoModelService implements
513
678
  const item = await ModelCrudUtil.prePartialUpdate(cls, data);
514
679
  const col = await this.getStore(cls);
515
680
  const items = MongoUtil.extractSimple(cls, item);
516
- const final = Object.entries(items).reduce<Partial<Record<'$unset' | '$set', Record<string, unknown>>>>(
517
- (document, [key, value]) => {
518
- if (value === null || value === undefined) {
519
- (document.$unset ??= {})[key] = value;
520
- } else {
521
- (document.$set ??= {})[key] = value;
522
- }
523
- return document;
524
- }, {});
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
+ }, {});
525
689
 
526
690
  const filter = MongoUtil.extractWhereFilter(cls, query.where);
527
691
  const result = await col.updateMany(filter, castTo(final));
@@ -529,7 +693,7 @@ export class MongoModelService implements
529
693
  }
530
694
 
531
695
  // Facet
532
- async facet<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, query?: ModelQuery<T>): Promise<ModelQueryFacet[]> {
696
+ async facetByQuery<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, query?: ModelQuery<T>): Promise<ModelQueryFacet[]> {
533
697
  await QueryVerifier.verify(cls, query);
534
698
 
535
699
  const col = await this.getStore(cls);
@@ -555,7 +719,7 @@ export class MongoModelService implements
555
719
  }
556
720
  ];
557
721
 
558
- const result = await col.aggregate<{ _id: ObjectId, count: number }>(aggregations).toArray();
722
+ const result = await col.aggregate<{ _id: ObjectId; count: number }>(aggregations).toArray();
559
723
 
560
724
  return result
561
725
  .map(item => ({
@@ -566,18 +730,28 @@ export class MongoModelService implements
566
730
  }
567
731
 
568
732
  // Suggest
569
- async suggestValues<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[]> {
570
739
  await QueryVerifier.verify(cls, query);
571
740
  const resolvedQuery = ModelQuerySuggestUtil.getSuggestFieldQuery<T>(cls, field, prefix, query);
572
741
  const results = await this.query<T>(cls, resolvedQuery);
573
- 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);
574
743
  }
575
744
 
576
- async suggest<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[]> {
577
751
  await QueryVerifier.verify(cls, query);
578
752
  const resolvedQuery = ModelQuerySuggestUtil.getSuggestQuery<T>(cls, field, prefix, query);
579
753
  const results = await this.query<T>(cls, resolvedQuery);
580
- return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, results, (_, b) => b, query && query.limit);
754
+ return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, results, (_, b) => b, query?.limit);
581
755
  }
582
756
 
583
757
  // Other
@@ -590,12 +764,14 @@ export class MongoModelService implements
590
764
  search = { $search: search, $language: 'en' };
591
765
  }
592
766
 
593
- (query.sort ??= []).unshift(castTo<(typeof query.sort[0])>({
594
- score: { $meta: 'textScore' }
595
- }));
767
+ (query.sort ??= []).unshift(
768
+ castTo<(typeof query.sort)[0]>({
769
+ score: { $meta: 'textScore' }
770
+ })
771
+ );
596
772
 
597
773
  const cursor = col.find(castTo({ $and: [{ $text: search }, filter] }), {});
598
774
  const items = await MongoUtil.prepareCursor(cls, cursor, query).toArray();
599
775
  return await Promise.all(items.map(item => this.postLoad(cls, item)));
600
776
  }
601
- }
777
+ }
@@ -1,10 +1,16 @@
1
1
  import type { ServiceDescriptor } from '@travetto/cli';
2
2
 
3
- const version = process.env.MONGO_VERSION || '8.2';
3
+ const version = process.env.MONGO_VERSION || '8.3';
4
+
5
+ /* cspell:words orbstack pthread rseq glibc TUNABLES */
4
6
 
5
7
  export const service: ServiceDescriptor = {
6
8
  name: 'mongodb',
7
9
  version,
8
10
  port: 27017,
9
- image: `mongo:${version}`
10
- };
11
+ image: `mongo:${version}`,
12
+ env: {
13
+ // Temp until mongo image fixes orbstack issue
14
+ GLIBC_TUNABLES: 'glibc.pthread.rseq=1'
15
+ }
16
+ };