@travetto/model-mongo 8.0.0-alpha.23 → 8.0.0-alpha.25

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.23",
3
+ "version": "8.0.0-alpha.25",
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.20",
30
- "@travetto/model": "^8.0.0-alpha.21",
31
- "@travetto/model-indexed": "^8.0.0-alpha.23",
32
- "@travetto/model-query": "^8.0.0-alpha.22",
29
+ "@travetto/config": "^8.0.0-alpha.22",
30
+ "@travetto/model": "^8.0.0-alpha.23",
31
+ "@travetto/model-indexed": "^8.0.0-alpha.25",
32
+ "@travetto/model-query": "^8.0.0-alpha.24",
33
33
  "mongodb": "^7.5.0"
34
34
  },
35
35
  "peerDependencies": {
36
- "@travetto/cli": "^8.0.0-alpha.26"
36
+ "@travetto/cli": "^8.0.0-alpha.28"
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));
@@ -259,4 +271,4 @@ export class MongoUtil {
259
271
 
260
272
  return changed;
261
273
  }
262
- }
274
+ }
package/src/service.ts CHANGED
@@ -1,40 +1,91 @@
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
11
35
  } from '@travetto/model';
12
36
  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,
37
+ type FullKeyedIndexBody,
38
+ type FullKeyedIndexWithPartialBody,
39
+ type KeyedIndexBody,
40
+ type KeyedIndexSelection,
41
+ ModelIndexedComputedIndex,
42
+ type ModelIndexedSearchOptions,
43
+ type ModelIndexedSupport,
44
+ ModelIndexedUtil,
45
+ type ModelPageOptions,
46
+ type ModelPageResult,
47
+ type SingleItemIndex,
48
+ type SortedIndex,
49
+ type SortedIndexSelection,
50
+ type SortedIndexSelectionType
21
51
  } from '@travetto/model-indexed';
22
-
23
52
  import {
24
- ShutdownManager, type Class, TypedObject, castTo, asFull, type BinaryMetadata, type ByteRange, type BinaryType,
25
- BinaryUtil, BinaryMetadataUtil, JSONUtil,
53
+ type ModelQuery,
54
+ type ModelQueryCrudSupport,
55
+ ModelQueryCrudUtil,
56
+ type ModelQueryFacet,
57
+ type ModelQueryFacetSupport,
58
+ type ModelQuerySuggestSupport,
59
+ ModelQuerySuggestUtil,
60
+ type ModelQuerySupport,
61
+ ModelQueryUtil,
62
+ type PageableModelQuery,
63
+ QueryVerifier,
64
+ type ValidStringFields,
65
+ type WhereClause
66
+ } from '@travetto/model-query';
67
+ import {
68
+ asFull,
69
+ type BinaryMetadata,
70
+ BinaryMetadataUtil,
71
+ type BinaryType,
72
+ BinaryUtil,
73
+ type ByteRange,
74
+ type Class,
75
+ castTo,
76
+ JSONUtil,
77
+ ShutdownManager,
78
+ TypedObject
26
79
  } from '@travetto/runtime';
27
- import { Injectable, PostConstruct } from '@travetto/di';
28
80
 
29
- import { MongoUtil, type WithId } from './internal/util.ts';
30
81
  import type { MongoModelConfig } from './config.ts';
82
+ import { MongoUtil, type WithId } from './internal/util.ts';
31
83
 
32
84
  type BlobRaw = GridFSFile & { metadata?: BinaryMetadata };
33
85
 
34
86
  type MongoTextSearch = RootFilterOperators<unknown>['$text'];
35
87
 
36
- const isDuplicateKeyError = (error: unknown): boolean =>
37
- error instanceof MongoServerError && error.message.includes('duplicate key error');
88
+ const isDuplicateKeyError = (error: unknown): boolean => error instanceof MongoServerError && error.message.includes('duplicate key error');
38
89
 
39
90
  export const ModelBlobNamespace = '__blobs';
40
91
 
@@ -42,30 +93,41 @@ export const ModelBlobNamespace = '__blobs';
42
93
  * Mongo-based model source
43
94
  */
44
95
  @Injectable()
45
- export class MongoModelService implements
46
- ModelCrudSupport, ModelStorageSupport,
47
- ModelBulkSupport, ModelBlobSupport,
48
- ModelIndexedSupport, ModelQuerySupport,
49
- ModelQueryCrudSupport, ModelQueryFacetSupport,
50
- ModelQuerySuggestSupport, ModelExpirySupport {
51
-
96
+ export class MongoModelService
97
+ implements
98
+ ModelCrudSupport,
99
+ ModelStorageSupport,
100
+ ModelBulkSupport,
101
+ ModelBlobSupport,
102
+ ModelIndexedSupport,
103
+ ModelQuerySupport,
104
+ ModelQueryCrudSupport,
105
+ ModelQueryFacetSupport,
106
+ ModelQuerySuggestSupport,
107
+ ModelExpirySupport
108
+ {
52
109
  #db: Db;
53
110
  #bucket: GridFSBucket;
54
111
  idSource = ModelCrudUtil.uuidSource();
55
112
  client: MongoClient;
56
113
  config: MongoModelConfig;
57
114
 
58
- constructor(config: MongoModelConfig) { this.config = config; }
115
+ constructor(config: MongoModelConfig) {
116
+ this.config = config;
117
+ }
59
118
 
60
- async * #iterateCursor<T extends ModelType>(cls: Class<T>, cursor: FindCursor, options?: ModelListOptions & ModelPageOptions<number>): AsyncGenerator<T[]> {
119
+ async *#iterateCursor<T extends ModelType>(
120
+ cls: Class<T>,
121
+ cursor: FindCursor,
122
+ options?: ModelListOptions & ModelPageOptions<number>
123
+ ): AsyncGenerator<T[]> {
61
124
  const batchSize = options?.batchSizeHint ?? 100;
62
125
  let batch: T[] = [];
63
126
  const maxCount = options?.limit ?? Number.MAX_SAFE_INTEGER;
64
127
  for await (const item of cursor
65
128
  .batchSize(batchSize)
66
129
  .limit(maxCount)
67
- .skip(options?.offset ?? 0)
68
- ) {
130
+ .skip(options?.offset ?? 0)) {
69
131
  if (options?.abort?.aborted) {
70
132
  cursor.close();
71
133
  break;
@@ -95,8 +157,7 @@ export class MongoModelService implements
95
157
  }
96
158
 
97
159
  const where = this.getWhereFilter(cls, whereClause);
98
- let q = store.find(where, { timeout: true })
99
- .batchSize(100);
160
+ let q = store.find(where, { timeout: true }).batchSize(100);
100
161
 
101
162
  // TODO: We could cache this
102
163
  if ('sort' in idx) {
@@ -105,7 +166,7 @@ export class MongoModelService implements
105
166
  return q;
106
167
  }
107
168
 
108
- restoreId(item: { id?: string, _id?: unknown }): void {
169
+ restoreId(item: { id?: string; _id?: unknown }): void {
109
170
  if (item._id) {
110
171
  item.id ??= MongoUtil.idToString(castTo(item._id));
111
172
  delete item._id;
@@ -125,11 +186,11 @@ export class MongoModelService implements
125
186
  return item;
126
187
  }
127
188
 
128
- preUpdate<T extends OptionalId<ModelType>>(item: T & { _id?: Binary, id: string }): string;
189
+ preUpdate<T extends OptionalId<ModelType>>(item: T & { _id?: Binary; id: string }): string;
129
190
  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) {
191
+ preUpdate<T extends OptionalId<ModelType>>(item: T & { _id?: Binary; id: undefined }): undefined;
192
+ preUpdate<T extends OptionalId<ModelType>>(item: T & { _id?: Binary; id?: string }): string | undefined {
193
+ if (item?.id) {
133
194
  const id = item.id;
134
195
  item._id = MongoUtil.uuid(id);
135
196
  if (!this.config.storeId) {
@@ -153,7 +214,7 @@ export class MongoModelService implements
153
214
  async initializeClient(): Promise<void> {
154
215
  this.client = await MongoClient.connect(this.config.url, {
155
216
  ...this.config.connectionOptions,
156
- useBigInt64: true,
217
+ useBigInt64: true
157
218
  });
158
219
  this.#db = this.client.db(this.config.namespace);
159
220
  this.#bucket = new GridFSBucket(this.#db, {
@@ -174,7 +235,7 @@ export class MongoModelService implements
174
235
  }
175
236
 
176
237
  // Storage
177
- async createStorage(): Promise<void> { }
238
+ async createStorage(): Promise<void> {}
178
239
 
179
240
  async deleteStorage(): Promise<void> {
180
241
  await this.#db.dropDatabase();
@@ -182,10 +243,7 @@ export class MongoModelService implements
182
243
 
183
244
  async upsertModel(cls: Class): Promise<void> {
184
245
  const col = await this.getStore(cls);
185
- const indices = [
186
- ...ModelRegistryIndex.getIndices(cls).map(idx => MongoUtil.getIndex(cls, idx)),
187
- ...MongoUtil.getExtraIndices(cls)
188
- ];
246
+ const indices = [...ModelRegistryIndex.getIndices(cls).map(idx => MongoUtil.getIndex(cls, idx)), ...MongoUtil.getExtraIndices(cls)];
189
247
  const existingIndices = (await col.indexes().catch(() => [])).filter(idx => idx.name !== '_id_');
190
248
 
191
249
  const pendingMap = Object.fromEntries(indices.map(pair => [pair[1].name!, pair]));
@@ -219,7 +277,7 @@ export class MongoModelService implements
219
277
  }
220
278
 
221
279
  async truncateBlob(): Promise<void> {
222
- await this.#bucket.drop().catch(() => { });
280
+ await this.#bucket.drop().catch(() => {});
223
281
  }
224
282
 
225
283
  /**
@@ -275,11 +333,7 @@ export class MongoModelService implements
275
333
  const store = await this.getStore(cls);
276
334
 
277
335
  try {
278
- await store.updateOne(
279
- this.getIdFilter(cls, id, false),
280
- { $set: cleaned },
281
- { upsert: true }
282
- );
336
+ await store.updateOne(this.getIdFilter(cls, id, false), { $set: cleaned }, { upsert: true });
283
337
  } catch (error) {
284
338
  throw isDuplicateKeyError(error) ? new ExistsError(cls, id) : error;
285
339
  }
@@ -292,24 +346,23 @@ export class MongoModelService implements
292
346
  const final = await ModelCrudUtil.prePartialUpdate(cls, item, view);
293
347
  const simple = MongoUtil.extractSimple(cls, final, undefined, false);
294
348
 
295
- const operation: Partial<T> = castTo(Object
296
- .entries(simple)
297
- .reduce<Partial<Record<'$unset' | '$set', Record<string, unknown>>>>((document, [key, value]) => {
349
+ const operation: Partial<T> = castTo(
350
+ Object.entries(simple).reduce<Partial<Record<'$unset' | '$set', Record<string, unknown>>>>((document, [key, value]) => {
298
351
  if (value === null || value === undefined) {
299
352
  (document.$unset ??= {})[key] = value;
300
353
  } else {
301
354
  (document.$set ??= {})[key] = value;
302
355
  }
303
356
  return document;
304
- }, {}));
357
+ }, {})
358
+ );
305
359
 
306
360
  const id = item.id;
307
361
 
308
- const result = await store.findOneAndUpdate(
309
- this.getIdFilter(cls, id),
310
- operation,
311
- { returnDocument: 'after', includeResultMetadata: true }
312
- );
362
+ const result = await store.findOneAndUpdate(this.getIdFilter(cls, id), operation, {
363
+ returnDocument: 'after',
364
+ includeResultMetadata: true
365
+ });
313
366
 
314
367
  if (!result.value) {
315
368
  throw new NotFoundError(cls, id);
@@ -326,7 +379,7 @@ export class MongoModelService implements
326
379
  }
327
380
  }
328
381
 
329
- async * list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]> {
382
+ async *list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]> {
330
383
  const store = await this.getStore(cls);
331
384
  const cursor = store.find(this.getWhereFilter(cls, {}), { timeout: true });
332
385
  yield* this.#iterateCursor(cls, cursor, options);
@@ -334,7 +387,10 @@ export class MongoModelService implements
334
387
 
335
388
  // Blob
336
389
  async upsertBlob(location: string, input: BinaryType, metadata?: BinaryMetadata, overwrite = true): Promise<void> {
337
- const existing = await this.getBlobMetadata(location).then(() => true, () => false);
390
+ const existing = await this.getBlobMetadata(location).then(
391
+ () => true,
392
+ () => false
393
+ );
338
394
  if (!overwrite && existing) {
339
395
  return;
340
396
  }
@@ -366,10 +422,9 @@ export class MongoModelService implements
366
422
  }
367
423
 
368
424
  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
- );
425
+ await this.#db
426
+ .collection<{ metadata: BinaryMetadata }>(`${ModelBlobNamespace}.files`)
427
+ .findOneAndUpdate({ filename: location }, { $set: { metadata, contentType: metadata.contentType! } });
373
428
  }
374
429
 
375
430
  // Bulk
@@ -402,7 +457,10 @@ export class MongoModelService implements
402
457
  bulk.insert(operation.insert);
403
458
  } else if (operation.upsert) {
404
459
  const id = this.preUpdate(operation.upsert);
405
- bulk.find({ _id: MongoUtil.uuid(id!) }).upsert().updateOne({ $set: operation.upsert });
460
+ bulk
461
+ .find({ _id: MongoUtil.uuid(id!) })
462
+ .upsert()
463
+ .updateOne({ $set: operation.upsert });
406
464
  } else if (operation.update) {
407
465
  const id = this.preUpdate(operation.update);
408
466
  bulk.find({ _id: MongoUtil.uuid(id) }).update({ $set: operation.update });
@@ -451,89 +509,77 @@ export class MongoModelService implements
451
509
  }
452
510
 
453
511
  // 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> {
512
+ async getByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
513
+ cls: Class<T>,
514
+ idx: SingleItemIndex<T, K, S>,
515
+ body: FullKeyedIndexBody<T, K, S>
516
+ ): Promise<T> {
459
517
  const store = await this.getStore(cls);
460
518
 
461
519
  const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
462
520
 
463
- const result = await store.findOne(
464
- this.getWhereFilter(cls, castTo(computed.project({ sort: true, includeId: true })))
465
- );
521
+ const result = await store.findOne(this.getWhereFilter(cls, castTo(computed.project({ sort: true, includeId: true }))));
466
522
  if (!result) {
467
523
  throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
468
524
  }
469
525
  return await this.postLoad(cls, result);
470
526
  }
471
527
 
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> {
528
+ async deleteByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
529
+ cls: Class<T>,
530
+ idx: SingleItemIndex<T, K, S>,
531
+ body: FullKeyedIndexBody<T, K, S>
532
+ ): Promise<void> {
477
533
  const store = await this.getStore(cls);
478
534
  const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
479
535
 
480
- const result = await store.deleteOne(
481
- this.getWhereFilter(cls, castTo(computed.project({ sort: true, includeId: true })))
482
- );
536
+ const result = await store.deleteOne(this.getWhereFilter(cls, castTo(computed.project({ sort: true, includeId: true }))));
483
537
  if (!result.deletedCount) {
484
538
  throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
485
539
  }
486
540
  }
487
541
 
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> {
542
+ upsertByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
543
+ cls: Class<T>,
544
+ idx: SingleItemIndex<T, K, S>,
545
+ body: OptionalId<T>
546
+ ): Promise<T> {
493
547
  return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
494
548
  }
495
549
 
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> {
550
+ updateByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
551
+ cls: Class<T>,
552
+ idx: SingleItemIndex<T, K, S>,
553
+ body: T
554
+ ): Promise<T> {
501
555
  return ModelIndexedUtil.naiveUpdate(this, cls, idx, body);
502
556
  }
503
557
 
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> {
558
+ async updatePartialByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
559
+ cls: Class<T>,
560
+ idx: SingleItemIndex<T, K>,
561
+ body: FullKeyedIndexWithPartialBody<T, K, S>
562
+ ): Promise<T> {
509
563
  const item = await ModelCrudUtil.naivePartialUpdate(cls, () => this.getByIndex(cls, idx, castTo(body)), castTo(body));
510
564
  return this.update(cls, item);
511
565
  }
512
566
 
513
- async pageByIndex<
514
- T extends ModelType,
515
- K extends KeyedIndexSelection<T>,
516
- S extends SortedIndexSelection<T>
517
- >(
567
+ async pageByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
518
568
  cls: Class<T>,
519
569
  idx: SortedIndex<T, K, S>,
520
570
  body: KeyedIndexBody<T, K>,
521
- options?: ModelPageOptions,
571
+ options?: ModelPageOptions
522
572
  ): Promise<ModelPageResult<T>> {
523
573
  {
524
574
  const offset = options?.offset ? JSONUtil.fromBase64<number>(options.offset) : 0;
525
- const cursor = (await this.#buildIndexQuery(cls, idx, body));
575
+ const cursor = await this.#buildIndexQuery(cls, idx, body);
526
576
  const batches = await Array.fromAsync(this.#iterateCursor(cls, cursor, { limit: 100, ...options, offset }));
527
577
  const items = batches.flat();
528
578
  return { items, nextOffset: items.length ? JSONUtil.toBase64(offset + items.length) : undefined };
529
579
  }
530
580
  }
531
581
 
532
- async * listByIndex<
533
- T extends ModelType,
534
- K extends KeyedIndexSelection<T>,
535
- S extends SortedIndexSelection<T>
536
- >(
582
+ async *listByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
537
583
  cls: Class<T>,
538
584
  idx: SortedIndex<T, K, S>,
539
585
  body: KeyedIndexBody<T, K>,
@@ -548,18 +594,17 @@ export class MongoModelService implements
548
594
  S extends SortedIndexSelection<T>,
549
595
  K extends KeyedIndexSelection<T>,
550
596
  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
- })));
597
+ >(cls: Class<T>, idx: SortedIndex<T, K, S>, body: KeyedIndexBody<T, K>, prefix: B, options?: ModelIndexedSearchOptions): Promise<T[]> {
598
+ const cursor = await this.#buildIndexQuery(cls, idx, body, where =>
599
+ castTo({
600
+ $and: [
601
+ where,
602
+ {
603
+ [idx.sortTemplate[0].path.join('.')]: ModelIndexedUtil.getSuggestRegex(prefix)
604
+ }
605
+ ]
606
+ })
607
+ );
563
608
  const batches = await Array.fromAsync(this.#iterateCursor(cls, cursor, { limit: 10, ...options }));
564
609
 
565
610
  return batches.flat();
@@ -622,15 +667,14 @@ export class MongoModelService implements
622
667
  const item = await ModelCrudUtil.prePartialUpdate(cls, data);
623
668
  const col = await this.getStore(cls);
624
669
  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
- }, {});
670
+ const final = Object.entries(items).reduce<Partial<Record<'$unset' | '$set', Record<string, unknown>>>>((document, [key, value]) => {
671
+ if (value === null || value === undefined) {
672
+ (document.$unset ??= {})[key] = value;
673
+ } else {
674
+ (document.$set ??= {})[key] = value;
675
+ }
676
+ return document;
677
+ }, {});
634
678
 
635
679
  const filter = MongoUtil.extractWhereFilter(cls, query.where);
636
680
  const result = await col.updateMany(filter, castTo(final));
@@ -664,7 +708,7 @@ export class MongoModelService implements
664
708
  }
665
709
  ];
666
710
 
667
- const result = await col.aggregate<{ _id: ObjectId, count: number }>(aggregations).toArray();
711
+ const result = await col.aggregate<{ _id: ObjectId; count: number }>(aggregations).toArray();
668
712
 
669
713
  return result
670
714
  .map(item => ({
@@ -675,18 +719,28 @@ export class MongoModelService implements
675
719
  }
676
720
 
677
721
  // Suggest
678
- async suggestValuesByQuery<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Promise<string[]> {
722
+ async suggestValuesByQuery<T extends ModelType>(
723
+ cls: Class<T>,
724
+ field: ValidStringFields<T>,
725
+ prefix?: string,
726
+ query?: PageableModelQuery<T>
727
+ ): Promise<string[]> {
679
728
  await QueryVerifier.verify(cls, query);
680
729
  const resolvedQuery = ModelQuerySuggestUtil.getSuggestFieldQuery<T>(cls, field, prefix, query);
681
730
  const results = await this.query<T>(cls, resolvedQuery);
682
- return ModelQuerySuggestUtil.combineSuggestResults<T, string>(cls, field, prefix, results, (a) => a, query && query.limit);
731
+ return ModelQuerySuggestUtil.combineSuggestResults<T, string>(cls, field, prefix, results, a => a, query?.limit);
683
732
  }
684
733
 
685
- async suggestByQuery<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Promise<T[]> {
734
+ async suggestByQuery<T extends ModelType>(
735
+ cls: Class<T>,
736
+ field: ValidStringFields<T>,
737
+ prefix?: string,
738
+ query?: PageableModelQuery<T>
739
+ ): Promise<T[]> {
686
740
  await QueryVerifier.verify(cls, query);
687
741
  const resolvedQuery = ModelQuerySuggestUtil.getSuggestQuery<T>(cls, field, prefix, query);
688
742
  const results = await this.query<T>(cls, resolvedQuery);
689
- return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, results, (_, b) => b, query && query.limit);
743
+ return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, results, (_, b) => b, query?.limit);
690
744
  }
691
745
 
692
746
  // Other
@@ -699,12 +753,14 @@ export class MongoModelService implements
699
753
  search = { $search: search, $language: 'en' };
700
754
  }
701
755
 
702
- (query.sort ??= []).unshift(castTo<(typeof query.sort[0])>({
703
- score: { $meta: 'textScore' }
704
- }));
756
+ (query.sort ??= []).unshift(
757
+ castTo<(typeof query.sort)[0]>({
758
+ score: { $meta: 'textScore' }
759
+ })
760
+ );
705
761
 
706
762
  const cursor = col.find(castTo({ $and: [{ $text: search }, filter] }), {});
707
763
  const items = await MongoUtil.prepareCursor(cls, cursor, query).toArray();
708
764
  return await Promise.all(items.map(item => this.postLoad(cls, item)));
709
765
  }
710
- }
766
+ }
@@ -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
+ };