@travetto/model-mongo 5.0.0-rc.8 → 5.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
@@ -17,10 +17,10 @@ This module provides an [mongodb](https://mongodb.com)-based implementation for
17
17
 
18
18
  Supported features:
19
19
  * [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/service/crud.ts#L11)
20
- * [Streaming](https://github.com/travetto/travetto/tree/main/module/model/src/service/stream.ts#L3)
21
20
  * [Expiry](https://github.com/travetto/travetto/tree/main/module/model/src/service/expiry.ts#L11)
22
21
  * [Bulk](https://github.com/travetto/travetto/tree/main/module/model/src/service/bulk.ts#L19)
23
22
  * [Indexed](https://github.com/travetto/travetto/tree/main/module/model/src/service/indexed.ts#L12)
23
+ * [Blob](https://github.com/travetto/travetto/tree/main/module/model/src/service/blob.ts#L8)
24
24
  * [Query Crud](https://github.com/travetto/travetto/tree/main/module/model-query/src/service/crud.ts#L11)
25
25
  * [Facet](https://github.com/travetto/travetto/tree/main/module/model-query/src/service/facet.ts#L12)
26
26
  * [Query](https://github.com/travetto/travetto/tree/main/module/model-query/src/service/query.ts#L10)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model-mongo",
3
- "version": "5.0.0-rc.8",
3
+ "version": "5.0.0",
4
4
  "description": "Mongo backing for the travetto model module.",
5
5
  "keywords": [
6
6
  "mongo",
@@ -25,13 +25,13 @@
25
25
  "directory": "module/model-mongo"
26
26
  },
27
27
  "dependencies": {
28
- "@travetto/config": "^5.0.0-rc.8",
29
- "@travetto/model": "^5.0.0-rc.8",
30
- "@travetto/model-query": "^5.0.0-rc.8",
28
+ "@travetto/config": "^5.0.0",
29
+ "@travetto/model": "^5.0.0",
30
+ "@travetto/model-query": "^5.0.0",
31
31
  "mongodb": "^6.8.0"
32
32
  },
33
33
  "peerDependencies": {
34
- "@travetto/command": "^5.0.0-rc.8"
34
+ "@travetto/command": "^5.0.0"
35
35
  },
36
36
  "peerDependenciesMeta": {
37
37
  "@travetto/command": {
@@ -1,7 +1,7 @@
1
1
  import { Binary, ObjectId } from 'mongodb';
2
2
 
3
- import { Class } from '@travetto/runtime';
4
- import { DistanceUnit, ModelQuery, Query, WhereClause } from '@travetto/model-query';
3
+ import { castTo, Class, TypedObject } from '@travetto/runtime';
4
+ import { DistanceUnit, WhereClause } from '@travetto/model-query';
5
5
  import type { ModelType, IndexField } from '@travetto/model';
6
6
  import { DataUtil, SchemaRegistry } from '@travetto/schema';
7
7
  import { ModelQueryUtil } from '@travetto/model-query/src/internal/service/query';
@@ -18,8 +18,8 @@ const RADIANS_TO: Record<DistanceUnit, number> = {
18
18
  rad: 1
19
19
  };
20
20
 
21
- export type WithId<T> = T & { _id?: Binary };
22
- const isWithId = <T extends ModelType>(o: T): o is WithId<T> => o && '_id' in o;
21
+ export type WithId<T, I = unknown> = T & { _id?: I };
22
+ const isWithId = <T extends ModelType, I = unknown>(o: T): o is WithId<T, I> => o && '_id' in o;
23
23
 
24
24
  /**
25
25
  * Basic mongo utils for conforming to the model module
@@ -29,20 +29,18 @@ export class MongoUtil {
29
29
  static toIndex<T extends ModelType>(f: IndexField<T>): Record<string, number> {
30
30
  const keys = [];
31
31
  while (typeof f !== 'number' && typeof f !== 'boolean' && Object.keys(f)) {
32
- const key = Object.keys(f)[0];
33
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
34
- f = f[key as keyof typeof f] as IndexField<T>;
32
+ const key = TypedObject.keys(f)[0];
33
+ f = castTo(f[key]);
35
34
  keys.push(key);
36
35
  }
37
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
38
- const rf = f as unknown as (number | boolean);
36
+ const rf: number | boolean = castTo(f);
39
37
  return {
40
38
  [keys.join('.')]: typeof rf === 'boolean' ? (rf ? 1 : 0) : rf
41
39
  };
42
40
  }
43
41
 
44
42
  static uuid(val: string): Binary {
45
- return new Binary(Buffer.from(val.replace(/-/g, ''), 'hex'), Binary.SUBTYPE_UUID);
43
+ return new Binary(Buffer.from(val.replaceAll('-', ''), 'hex'), Binary.SUBTYPE_UUID);
46
44
  }
47
45
 
48
46
  static idToString(id: string | ObjectId | Binary): string {
@@ -64,87 +62,45 @@ export class MongoUtil {
64
62
 
65
63
  static preInsertId<T extends ModelType>(item: T): T {
66
64
  if (item && item.id) {
67
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
68
- const itemWithId = item as WithId<T>;
65
+ const itemWithId: WithId<T> = castTo(item);
69
66
  itemWithId._id = this.uuid(item.id);
70
67
  }
71
68
  return item;
72
69
  }
73
70
 
74
- static prepareQuery<T extends ModelType, U extends Query<T> | ModelQuery<T>>(cls: Class<T>, query: U, checkExpiry = true): {
75
- query: U & { where: WhereClause<T> };
76
- filter: Record<string, unknown>;
77
- } {
78
- const q = ModelQueryUtil.getQueryAndVerify(cls, query, checkExpiry);
79
- return {
80
- query: q,
81
- filter: q.where ? this.extractWhereClause(cls, q.where) : {}
82
- };
71
+ static extractWhereFilter<T extends ModelType, U extends WhereClause<T>>(cls: Class<T>, where?: U, checkExpiry = true): Record<string, unknown> {
72
+ where = castTo(ModelQueryUtil.getWhereClause(cls, where, checkExpiry));
73
+ return where ? this.extractWhereClause(cls, where) : {};
83
74
  }
84
75
 
85
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
86
- static has$And = (o: unknown): o is ({ $and: WhereClause<unknown>[] }) => !!o && '$and' in (o as object);
87
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
88
- static has$Or = (o: unknown): o is ({ $or: WhereClause<unknown>[] }) => !!o && '$or' in (o as object);
89
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
90
- static has$Not = (o: unknown): o is ({ $not: WhereClause<unknown> }) => !!o && '$not' in (o as object);
91
-
92
76
  /**
93
77
  * Build mongo where clause
94
78
  */
95
79
  static extractWhereClause<T>(cls: Class<T>, o: WhereClause<T>): Record<string, unknown> {
96
- if (this.has$And(o)) {
80
+ if (ModelQueryUtil.has$And(o)) {
97
81
  return { $and: o.$and.map(x => this.extractWhereClause<T>(cls, x)) };
98
- } else if (this.has$Or(o)) {
82
+ } else if (ModelQueryUtil.has$Or(o)) {
99
83
  return { $or: o.$or.map(x => this.extractWhereClause<T>(cls, x)) };
100
- } else if (this.has$Not(o)) {
84
+ } else if (ModelQueryUtil.has$Not(o)) {
101
85
  return { $nor: [this.extractWhereClause<T>(cls, o.$not)] };
102
86
  } else {
103
87
  return this.extractSimple(cls, o);
104
88
  }
105
89
  }
106
90
 
107
- /**
108
- * Convert ids from '_id' to 'id'
109
- */
110
- static replaceId(v: Record<string, unknown>): Record<string, Binary>;
111
- static replaceId(v: string[]): Binary[];
112
- static replaceId(v: string): Binary;
113
- static replaceId(v: unknown): undefined;
114
- static replaceId(v: string | string[] | Record<string, unknown> | unknown): unknown {
115
- if (typeof v === 'string') {
116
- return this.uuid(v);
117
- } else if (Array.isArray(v)) {
118
- return v.map(x => this.replaceId(x));
119
- } else if (DataUtil.isPlainObject(v)) {
120
- const out: Record<string, Binary> = {};
121
- for (const [k, el] of Object.entries(v)) {
122
- const found = this.replaceId(el);
123
- if (found) {
124
- out[k] = found;
125
- }
126
- }
127
- return out;
128
- } else {
129
- return v;
130
- }
131
- }
132
-
133
91
  /**/
134
92
  static extractSimple<T>(base: Class<T> | undefined, o: Record<string, unknown>, path: string = '', recursive: boolean = true): Record<string, unknown> {
135
93
  const schema = base ? SchemaRegistry.get(base) : undefined;
136
94
  const out: Record<string, unknown> = {};
137
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
138
- const sub = o as Record<string, unknown>;
95
+ const sub = o;
139
96
  const keys = Object.keys(sub);
140
97
  for (const key of keys) {
141
98
  const subpath = `${path}${key}`;
142
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
143
- const v = sub[key] as Record<string, unknown>;
99
+ const v: Record<string, unknown> = castTo(sub[key]);
144
100
  const subField = schema?.views[AllViewⲐ].schema[key];
145
101
 
146
102
  if (subpath === 'id') { // Handle ids directly
147
- out._id = this.replaceId(v);
103
+ out._id = typeof v === 'string' ? this.uuid(v) : v;
148
104
  } else {
149
105
  const isPlain = v && DataUtil.isPlainObject(v);
150
106
  const firstKey = isPlain ? Object.keys(v)[0] : '';
@@ -170,18 +126,14 @@ export class MongoUtil {
170
126
  v.$nin = [null, []];
171
127
  }
172
128
  } else if (firstKey === '$regex') {
173
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
174
- v.$regex = DataUtil.toRegex(v.$regex as string | RegExp);
129
+ v.$regex = DataUtil.toRegex(castTo(v.$regex));
175
130
  } else if (firstKey && '$near' in v) {
176
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
177
- const dist = v.$maxDistance as number;
178
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
179
- const distance = dist / RADIANS_TO[(v.$unit as DistanceUnit ?? 'km')];
131
+ const dist: number = castTo(v.$maxDistance);
132
+ const distance = dist / RADIANS_TO[(castTo<DistanceUnit>(v.$unit) ?? 'km')];
180
133
  v.$maxDistance = distance;
181
134
  delete v.$unit;
182
135
  } else if (firstKey && '$geoWithin' in v) {
183
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
184
- const coords = v.$geoWithin as [number, number][];
136
+ const coords: [number, number][] = castTo(v.$geoWithin);
185
137
  const first = coords[0];
186
138
  const last = coords[coords.length - 1];
187
139
  // Connect if not
package/src/service.ts CHANGED
@@ -1,25 +1,26 @@
1
- // Wildcard import needed here due to packaging issues
2
1
  import {
3
2
  type Db, GridFSBucket, MongoClient, type Sort, type CreateIndexesOptions,
4
- type GridFSFile, type IndexSpecification, type Collection, type ObjectId, type Filter, type Document
3
+ type GridFSFile, type IndexSpecification, type Collection, ObjectId,
4
+ Binary
5
5
  } from 'mongodb';
6
- import { Readable } from 'node:stream';
7
6
  import { pipeline } from 'node:stream/promises';
7
+ import { Readable } from 'node:stream';
8
8
 
9
9
  import {
10
- ModelRegistry, ModelType, OptionalId,
11
- ModelCrudSupport, ModelStorageSupport, ModelStreamSupport,
12
- ModelExpirySupport, ModelBulkSupport, ModelIndexedSupport,
13
- StreamMeta, BulkOp, BulkResponse,
14
- NotFoundError, ExistsError, IndexConfig,
15
- StreamRange
10
+ ModelRegistry, ModelType, OptionalId, ModelCrudSupport, ModelStorageSupport,
11
+ ModelExpirySupport, ModelBulkSupport, ModelIndexedSupport, BulkOp, BulkResponse,
12
+ NotFoundError, ExistsError, IndexConfig, ModelBlobSupport
16
13
  } from '@travetto/model';
17
14
  import {
18
15
  ModelQuery, ModelQueryCrudSupport, ModelQueryFacetSupport, ModelQuerySupport,
19
- PageableModelQuery, ValidStringFields, WhereClause, ModelQuerySuggestSupport
16
+ PageableModelQuery, ValidStringFields, WhereClause, ModelQuerySuggestSupport,
17
+ QueryVerifier
20
18
  } from '@travetto/model-query';
21
19
 
22
- import { ShutdownManager, type Class, type DeepPartial, AppError, TypedObject } from '@travetto/runtime';
20
+ import {
21
+ ShutdownManager, type Class, type DeepPartial, AppError, TypedObject,
22
+ castTo, asFull, BlobMeta, ByteRange, BinaryInput, BinaryUtil
23
+ } from '@travetto/runtime';
23
24
  import { Injectable } from '@travetto/di';
24
25
  import { FieldConfig, SchemaRegistry, SchemaValidator } from '@travetto/schema';
25
26
 
@@ -31,21 +32,20 @@ import { ModelQuerySuggestUtil } from '@travetto/model-query/src/internal/servic
31
32
  import { PointImpl } from '@travetto/model-query/src/internal/model/point';
32
33
  import { ModelQueryExpiryUtil } from '@travetto/model-query/src/internal/service/expiry';
33
34
  import { ModelExpiryUtil } from '@travetto/model/src/internal/service/expiry';
34
- import { enforceRange, StreamModel, STREAMS } from '@travetto/model/src/internal/service/stream';
35
35
  import { AllViewⲐ } from '@travetto/schema/src/internal/types';
36
36
  import { ModelBulkUtil } from '@travetto/model/src/internal/service/bulk';
37
+ import { MODEL_BLOB, ModelBlobNamespace, ModelBlobUtil } from '@travetto/model/src/internal/service/blob';
37
38
 
38
39
  import { MongoUtil, WithId } from './internal/util';
39
40
  import { MongoModelConfig } from './config';
40
41
 
41
42
  const IdxFieldsⲐ = Symbol.for('@travetto/model-mongo:idx');
42
43
 
43
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
44
- const asFielded = <T extends ModelType>(cfg: IndexConfig<T>): { [IdxFieldsⲐ]: Sort } => (cfg as unknown as { [IdxFieldsⲐ]: Sort });
44
+ const asFielded = (cfg: IndexConfig<ModelType>): { [IdxFieldsⲐ]: Sort } => castTo(cfg);
45
45
 
46
46
  type IdxCfg = CreateIndexesOptions;
47
47
 
48
- type StreamRaw = GridFSFile & { metadata: StreamMeta };
48
+ type BlobRaw = GridFSFile & { metadata?: BlobMeta };
49
49
 
50
50
  /**
51
51
  * Mongo-based model source
@@ -53,7 +53,7 @@ type StreamRaw = GridFSFile & { metadata: StreamMeta };
53
53
  @Injectable()
54
54
  export class MongoModelService implements
55
55
  ModelCrudSupport, ModelStorageSupport,
56
- ModelBulkSupport, ModelStreamSupport,
56
+ ModelBulkSupport, ModelBlobSupport,
57
57
  ModelIndexedSupport, ModelQuerySupport,
58
58
  ModelQueryCrudSupport, ModelQueryFacetSupport,
59
59
  ModelQuerySuggestSupport, ModelExpirySupport {
@@ -65,12 +65,11 @@ export class MongoModelService implements
65
65
 
66
66
  constructor(public readonly config: MongoModelConfig) { }
67
67
 
68
- async #describeStreamRaw(location: string): Promise<StreamRaw> {
69
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
70
- const files: StreamRaw[] = (await this.#bucket.find({ filename: location }, { limit: 1 }).toArray()) as StreamRaw[];
68
+ async #describeBlobRaw(location: string): Promise<BlobRaw> {
69
+ const files: BlobRaw[] = await this.#bucket.find({ filename: location }, { limit: 1 }).toArray();
71
70
 
72
71
  if (!files?.length) {
73
- throw new NotFoundError(STREAMS, location);
72
+ throw new NotFoundError(ModelBlobNamespace, location);
74
73
  }
75
74
 
76
75
  return files[0];
@@ -80,7 +79,7 @@ export class MongoModelService implements
80
79
  this.client = await MongoClient.connect(this.config.url, this.config.options);
81
80
  this.#db = this.client.db(this.config.namespace);
82
81
  this.#bucket = new GridFSBucket(this.#db, {
83
- bucketName: STREAMS,
82
+ bucketName: ModelBlobNamespace,
84
83
  writeConcern: { w: 1 }
85
84
  });
86
85
  await ModelStorageUtil.registerModelChangeListener(this);
@@ -88,8 +87,8 @@ export class MongoModelService implements
88
87
  ModelExpiryUtil.registerCull(this);
89
88
  }
90
89
 
91
- getWhere<T extends ModelType>(cls: Class<T>, where: WhereClause<T>, checkExpiry = true): Record<string, unknown> {
92
- return MongoUtil.prepareQuery(cls, { where }, checkExpiry).filter;
90
+ getWhereFilter<T extends ModelType>(cls: Class<T>, where: WhereClause<T>, checkExpiry = true): Record<string, unknown> {
91
+ return MongoUtil.extractWhereFilter(cls, where, checkExpiry);
93
92
  }
94
93
 
95
94
  // Storage
@@ -117,14 +116,13 @@ export class MongoModelService implements
117
116
  return out;
118
117
  }
119
118
 
120
- getIndicies<T extends ModelType>(cls: Class<T>): ([IndexSpecification] | [IndexSpecification, IdxCfg])[] {
119
+ getIndices<T extends ModelType>(cls: Class<T>): ([IndexSpecification] | [IndexSpecification, IdxCfg])[] {
121
120
  const indices = ModelRegistry.get(cls).indices ?? [];
122
121
  return [
123
122
  ...indices.map((idx): [IndexSpecification, IdxCfg] => {
124
123
  const combined = asFielded(idx)[IdxFieldsⲐ] ??= Object.assign({}, ...idx.fields.map(x => MongoUtil.toIndex(x)));
125
124
  return [
126
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
127
- combined as IndexSpecification,
125
+ castTo(combined),
128
126
  (idx.type === 'unique' ? { unique: true } : {})
129
127
  ];
130
128
  }),
@@ -134,7 +132,7 @@ export class MongoModelService implements
134
132
 
135
133
  async establishIndices<T extends ModelType>(cls: Class<T>): Promise<void> {
136
134
  const col = await this.getStore(cls);
137
- const creating = this.getIndicies(cls);
135
+ const creating = this.getIndices(cls);
138
136
  if (creating.length) {
139
137
  console.debug('Creating indexes', { indices: creating });
140
138
  for (const el of creating) {
@@ -152,10 +150,8 @@ export class MongoModelService implements
152
150
  }
153
151
 
154
152
  async truncateModel<T extends ModelType>(cls: Class<T>): Promise<void> {
155
- if (cls === StreamModel) {
156
- try {
157
- await this.#bucket.drop();
158
- } catch { }
153
+ if (cls === MODEL_BLOB) {
154
+ await this.#bucket.drop().catch(() => { });
159
155
  } else {
160
156
  const col = await this.getStore(cls);
161
157
  await col.deleteMany({});
@@ -172,7 +168,7 @@ export class MongoModelService implements
172
168
  // Crud
173
169
  async get<T extends ModelType>(cls: Class<T>, id: string): Promise<T> {
174
170
  const store = await this.getStore(cls);
175
- const result = await store.findOne(this.getWhere<ModelType>(cls, { id }), {});
171
+ const result = await store.findOne(this.getWhereFilter<ModelType>(cls, { id }), {});
176
172
  if (result) {
177
173
  const res = await ModelCrudUtil.load(cls, result);
178
174
  if (res) {
@@ -183,24 +179,22 @@ export class MongoModelService implements
183
179
  }
184
180
 
185
181
  async create<T extends ModelType>(cls: Class<T>, item: OptionalId<T>): Promise<T> {
186
- const cleaned = await ModelCrudUtil.preStore(cls, item, this);
187
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
188
- (cleaned as WithId<T>)._id = MongoUtil.uuid(cleaned.id);
182
+ const cleaned: WithId<T, Binary> = castTo(await ModelCrudUtil.preStore(cls, item, this));
183
+ cleaned._id = MongoUtil.uuid(cleaned.id);
189
184
 
190
185
  const store = await this.getStore(cls);
191
- const result = await store.insertOne(cleaned);
186
+ const result = await store.insertOne(castTo(cleaned));
192
187
  if (!result.insertedId) {
193
188
  throw new ExistsError(cls, cleaned.id);
194
189
  }
195
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
196
- delete (cleaned as { _id?: unknown })._id;
190
+ delete cleaned._id;
197
191
  return cleaned;
198
192
  }
199
193
 
200
194
  async update<T extends ModelType>(cls: Class<T>, item: T): Promise<T> {
201
195
  item = await ModelCrudUtil.preStore(cls, item, this);
202
196
  const store = await this.getStore(cls);
203
- const res = await store.replaceOne(this.getWhere<ModelType>(cls, { id: item.id }), item);
197
+ const res = await store.replaceOne(this.getWhereFilter<ModelType>(cls, { id: item.id }), item);
204
198
  if (res.matchedCount === 0) {
205
199
  throw new NotFoundError(cls, item.id);
206
200
  }
@@ -212,7 +206,7 @@ export class MongoModelService implements
212
206
  const store = await this.getStore(cls);
213
207
  try {
214
208
  await store.updateOne(
215
- this.getWhere<ModelType>(cls, { id: cleaned.id }, false),
209
+ this.getWhereFilter<ModelType>(cls, { id: cleaned.id }, false),
216
210
  { $set: cleaned },
217
211
  { upsert: true }
218
212
  );
@@ -240,15 +234,11 @@ export class MongoModelService implements
240
234
  const items = MongoUtil.extractSimple(cls, final, undefined, false);
241
235
  final = Object
242
236
  .entries(items)
243
- .reduce<Record<string, unknown>>((acc, [k, v]) => {
237
+ .reduce<Partial<Record<'$unset' | '$set', Record<string, unknown>>>>((acc, [k, v]) => {
244
238
  if (v === null || v === undefined) {
245
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
246
- const o = (acc.$unset ??= {}) as Record<string, unknown>;
247
- o[k] = v;
239
+ (acc.$unset ??= {})[k] = v;
248
240
  } else {
249
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
250
- const o = (acc.$set ??= {}) as Record<string, unknown>;
251
- o[k] = v;
241
+ (acc.$set ??= {})[k] = v;
252
242
  }
253
243
  return acc;
254
244
  }, {});
@@ -256,7 +246,7 @@ export class MongoModelService implements
256
246
  const id = item.id;
257
247
 
258
248
  const res = await store.findOneAndUpdate(
259
- this.getWhere<ModelType>(cls, { id }),
249
+ this.getWhereFilter<ModelType>(cls, { id }),
260
250
  final,
261
251
  { returnDocument: 'after', includeResultMetadata: true }
262
252
  );
@@ -270,7 +260,7 @@ export class MongoModelService implements
270
260
 
271
261
  async delete<T extends ModelType>(cls: Class<T>, id: string): Promise<void> {
272
262
  const store = await this.getStore(cls);
273
- const result = await store.deleteOne(this.getWhere<ModelType>(cls, { id }, false));
263
+ const result = await store.deleteOne(this.getWhereFilter<ModelType>(cls, { id }, false));
274
264
  if (result.deletedCount === 0) {
275
265
  throw new NotFoundError(cls, id);
276
266
  }
@@ -278,7 +268,7 @@ export class MongoModelService implements
278
268
 
279
269
  async * list<T extends ModelType>(cls: Class<T>): AsyncIterable<T> {
280
270
  const store = await this.getStore(cls);
281
- const cursor = store.find(this.getWhere(cls, {}), { timeout: true }).batchSize(100);
271
+ const cursor = store.find(this.getWhereFilter(cls, {}), { timeout: true }).batchSize(100);
282
272
  for await (const el of cursor) {
283
273
  try {
284
274
  yield MongoUtil.postLoadId(await ModelCrudUtil.load(cls, el));
@@ -290,37 +280,39 @@ export class MongoModelService implements
290
280
  }
291
281
  }
292
282
 
293
- // Stream
294
- async upsertStream(location: string, input: Readable, meta: StreamMeta): Promise<void> {
283
+ // Blob
284
+ async upsertBlob(location: string, input: BinaryInput, meta?: BlobMeta, overwrite = true): Promise<void> {
285
+ const existing = await this.describeBlob(location).then(() => true, () => false);
286
+ if (!overwrite && existing) {
287
+ return;
288
+ }
289
+ const [stream, blobMeta] = await ModelBlobUtil.getInput(input, meta);
295
290
  const writeStream = this.#bucket.openUploadStream(location, {
296
- contentType: meta.contentType,
297
- metadata: meta
291
+ contentType: blobMeta.contentType,
292
+ metadata: blobMeta,
298
293
  });
294
+ await pipeline(stream, writeStream);
299
295
 
300
- await pipeline(input, writeStream);
301
- }
302
-
303
- async getStream(location: string, range?: StreamRange): Promise<Readable> {
304
- const meta = await this.describeStream(location);
305
-
306
- if (range) {
307
- range = enforceRange(range, meta.size);
308
- range.end! += 1; // range is exclusive
296
+ if (existing) {
297
+ const [read] = await this.#bucket.find({ filename: location, _id: { $ne: writeStream.id } }).toArray();
298
+ await this.#bucket.delete(read._id);
309
299
  }
300
+ }
310
301
 
311
- const res = await this.#bucket.openDownloadStreamByName(location, range);
312
- if (!res) {
313
- throw new NotFoundError(STREAMS, location);
314
- }
315
- return res;
302
+ async getBlob(location: string, range?: ByteRange): Promise<Blob> {
303
+ const meta = await this.describeBlob(location);
304
+ const final = range ? ModelBlobUtil.enforceRange(range, meta.size!) : undefined;
305
+ const mongoRange = final ? { start: final.start, end: final.end + 1 } : undefined;
306
+ const res = (): Readable => this.#bucket.openDownloadStreamByName(location, mongoRange);
307
+ return BinaryUtil.readableBlob(res, { ...meta, range: final });
316
308
  }
317
309
 
318
- async describeStream(location: string): Promise<StreamMeta> {
319
- return (await this.#describeStreamRaw(location)).metadata;
310
+ async describeBlob(location: string): Promise<BlobMeta> {
311
+ return (await this.#describeBlobRaw(location)).metadata ?? {};
320
312
  }
321
313
 
322
- async deleteStream(location: string): Promise<void> {
323
- const fileId = (await this.#describeStreamRaw(location))._id;
314
+ async deleteBlob(location: string): Promise<void> {
315
+ const fileId = (await this.#describeBlobRaw(location))._id;
324
316
  await this.#bucket.delete(fileId);
325
317
  }
326
318
 
@@ -350,8 +342,7 @@ export class MongoModelService implements
350
342
 
351
343
  for (const op of operations) {
352
344
  if (op.insert) {
353
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
354
- bulk.insert(MongoUtil.preInsertId(op.insert as T));
345
+ bulk.insert(MongoUtil.preInsertId(asFull(op.insert)));
355
346
  } else if (op.upsert) {
356
347
  bulk.find({ _id: MongoUtil.uuid(op.upsert.id!) }).upsert().updateOne({ $set: op.upsert });
357
348
  } else if (op.update) {
@@ -365,13 +356,11 @@ export class MongoModelService implements
365
356
 
366
357
  for (const op of operations) {
367
358
  if (op.insert) {
368
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
369
- MongoUtil.postLoadId(op.insert as T);
359
+ MongoUtil.postLoadId(asFull(op.insert));
370
360
  }
371
361
  }
372
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
373
- for (const [index, _id] of TypedObject.entries(res.upsertedIds) as [number, ObjectId][]) {
374
- out.insertedIds.set(+index, MongoUtil.idToString(_id));
362
+ for (const [index, _id] of TypedObject.entries(res.upsertedIds)) {
363
+ out.insertedIds.set(+index, MongoUtil.idToString(castTo(_id)));
375
364
  }
376
365
 
377
366
  if (out.counts) {
@@ -385,8 +374,7 @@ export class MongoModelService implements
385
374
  out.errors = res.getWriteErrors();
386
375
  for (const err of out.errors) {
387
376
  const op = operations[err.index];
388
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
389
- const k = Object.keys(op)[0] as keyof BulkResponse['counts'];
377
+ const k = TypedObject.keys(op)[0];
390
378
  out.counts[k] -= 1;
391
379
  }
392
380
  out.counts.error = out.errors.length;
@@ -405,10 +393,9 @@ export class MongoModelService implements
405
393
  const { key } = ModelIndexedUtil.computeIndexKey(cls, idx, body);
406
394
  const store = await this.getStore(cls);
407
395
  const result = await store.findOne(
408
- this.getWhere(
396
+ this.getWhereFilter(
409
397
  cls,
410
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
411
- ModelIndexedUtil.projectIndex(cls, idx, body) as WhereClause<T>
398
+ castTo(ModelIndexedUtil.projectIndex(cls, idx, body))
412
399
  )
413
400
  );
414
401
  if (!result) {
@@ -421,10 +408,9 @@ export class MongoModelService implements
421
408
  const { key } = ModelIndexedUtil.computeIndexKey(cls, idx, body);
422
409
  const store = await this.getStore(cls);
423
410
  const result = await store.deleteOne(
424
- this.getWhere(
411
+ this.getWhereFilter(
425
412
  cls,
426
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
427
- ModelIndexedUtil.projectIndex(cls, idx, body) as WhereClause<T>
413
+ castTo(ModelIndexedUtil.projectIndex(cls, idx, body))
428
414
  )
429
415
  );
430
416
  if (result.deletedCount) {
@@ -445,25 +431,24 @@ export class MongoModelService implements
445
431
  throw new AppError('Cannot list on unique indices', 'data');
446
432
  }
447
433
 
448
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
449
- const where = this.getWhere(
434
+ const where = this.getWhereFilter(
450
435
  cls,
451
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
452
- ModelIndexedUtil.projectIndex(cls, idx, body, { emptySortValue: { $exists: true } }) as WhereClause<T>
453
- ) as Filter<Document>;
436
+ castTo(ModelIndexedUtil.projectIndex(cls, idx, body, { emptySortValue: { $exists: true } }))
437
+ );
454
438
 
455
439
  const cursor = store.find(where, { timeout: true }).batchSize(100).sort(asFielded(idxCfg)[IdxFieldsⲐ]);
456
440
 
457
441
  for await (const el of cursor) {
458
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
459
- yield (await MongoUtil.postLoadId(await ModelCrudUtil.load(cls, el))) as T;
442
+ yield (await MongoUtil.postLoadId(await ModelCrudUtil.load(cls, el)));
460
443
  }
461
444
  }
462
445
 
463
446
  // Query
464
447
  async query<T extends ModelType>(cls: Class<T>, query: PageableModelQuery<T>): Promise<T[]> {
448
+ await QueryVerifier.verify(cls, query);
449
+
465
450
  const col = await this.getStore(cls);
466
- const { filter } = MongoUtil.prepareQuery(cls, query);
451
+ const filter = MongoUtil.extractWhereFilter(cls, query.where);
467
452
  let cursor = col.find<T>(filter, {});
468
453
  if (query.select) {
469
454
  const selectKey = Object.keys(query.select)[0];
@@ -493,8 +478,10 @@ export class MongoModelService implements
493
478
  }
494
479
 
495
480
  async queryCount<T extends ModelType>(cls: Class<T>, query: ModelQuery<T>): Promise<number> {
481
+ await QueryVerifier.verify(cls, query);
482
+
496
483
  const col = await this.getStore(cls);
497
- const { filter } = MongoUtil.prepareQuery(cls, query);
484
+ const filter = MongoUtil.extractWhereFilter(cls, query.where);
498
485
  return col.countDocuments(filter);
499
486
  }
500
487
 
@@ -505,11 +492,14 @@ export class MongoModelService implements
505
492
 
506
493
  // Query Crud
507
494
  async updateOneWithQuery<T extends ModelType>(cls: Class<T>, data: T, query: ModelQuery<T>): Promise<T> {
495
+ await QueryVerifier.verify(cls, query);
496
+
508
497
  const col = await this.getStore(cls);
509
498
  const item = await ModelCrudUtil.preStore(cls, data, this);
510
- query = ModelQueryUtil.getQueryWithId(cls, data, query);
499
+ const where = ModelQueryUtil.getWhereClause(cls, query.where);
500
+ where.id = item.id;
511
501
 
512
- const { filter } = MongoUtil.prepareQuery(cls, query);
502
+ const filter = MongoUtil.extractWhereFilter(cls, where);
513
503
  const res = await col.replaceOne(filter, item);
514
504
  if (res.matchedCount === 0) {
515
505
  throw new NotFoundError(cls, item.id);
@@ -518,57 +508,62 @@ export class MongoModelService implements
518
508
  }
519
509
 
520
510
  async deleteByQuery<T extends ModelType>(cls: Class<T>, query: ModelQuery<T>): Promise<number> {
511
+ await QueryVerifier.verify(cls, query);
512
+
521
513
  const col = await this.getStore(cls);
522
- const { filter } = MongoUtil.prepareQuery(cls, query, false);
514
+ const filter = MongoUtil.extractWhereFilter(cls, query.where, false);
523
515
  const res = await col.deleteMany(filter);
524
516
  return res.deletedCount ?? 0;
525
517
  }
526
518
 
527
519
  async updateByQuery<T extends ModelType>(cls: Class<T>, query: ModelQuery<T>, data: Partial<T>): Promise<number> {
528
- const col = await this.getStore(cls);
520
+ await QueryVerifier.verify(cls, query);
529
521
 
522
+ const col = await this.getStore(cls);
530
523
  const items = MongoUtil.extractSimple(cls, data);
531
- const final = Object.entries(items).reduce<Record<string, unknown>>((acc, [k, v]) => {
532
- if (v === null || v === undefined) {
533
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
534
- const o = (acc.$unset = acc.$unset ?? {}) as Record<string, unknown>;
535
- o[k] = v;
536
- } else {
537
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
538
- const o = (acc.$set = acc.$set ?? {}) as Record<string, unknown>;
539
- o[k] = v;
540
- }
541
- return acc;
542
- }, {});
524
+ const final = Object.entries(items).reduce<Partial<Record<'$unset' | '$set', Record<string, unknown>>>>(
525
+ (acc, [k, v]) => {
526
+ if (v === null || v === undefined) {
527
+ (acc.$unset ??= {})[k] = v;
528
+ } else {
529
+ (acc.$set ??= {})[k] = v;
530
+ }
531
+ return acc;
532
+ }, {});
543
533
 
544
- const { filter } = MongoUtil.prepareQuery(cls, query);
534
+ const filter = MongoUtil.extractWhereFilter(cls, query.where);
545
535
  const res = await col.updateMany(filter, final);
546
536
  return res.matchedCount;
547
537
  }
548
538
 
549
539
  // Facet
550
540
  async facet<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, query?: ModelQuery<T>): Promise<{ key: string, count: number }[]> {
541
+ await QueryVerifier.verify(cls, query);
542
+
551
543
  const col = await this.getStore(cls);
552
- const aggs: object[] = [{
553
- $group: {
554
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
555
- _id: `$${field as string}`,
556
- count: {
557
- $sum: 1
558
- }
559
- }
560
- }];
544
+ if (query) {
545
+ await QueryVerifier.verify(cls, query);
546
+ }
561
547
 
562
548
  let q: Record<string, unknown> = { [field]: { $exists: true } };
563
549
 
564
550
  if (query?.where) {
565
- q = { $and: [q, MongoUtil.prepareQuery(cls, query).filter] };
551
+ q = { $and: [q, MongoUtil.extractWhereFilter(cls, query.where)] };
566
552
  }
567
553
 
568
- aggs.unshift({ $match: q });
554
+ const aggregations: object[] = [
555
+ { $match: q },
556
+ {
557
+ $group: {
558
+ _id: `$${field}`,
559
+ count: {
560
+ $sum: 1
561
+ }
562
+ }
563
+ }
564
+ ];
569
565
 
570
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
571
- const result = (await col.aggregate(aggs).toArray()) as { _id: ObjectId, count: number }[];
566
+ const result = await col.aggregate<{ _id: ObjectId, count: number }>(aggregations).toArray();
572
567
 
573
568
  return result.map(val => ({
574
569
  key: MongoUtil.idToString(val._id),
@@ -578,12 +573,14 @@ export class MongoModelService implements
578
573
 
579
574
  // Suggest
580
575
  async suggestValues<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Promise<string[]> {
576
+ await QueryVerifier.verify(cls, query);
581
577
  const q = ModelQuerySuggestUtil.getSuggestFieldQuery<T>(cls, field, prefix, query);
582
578
  const results = await this.query<T>(cls, q);
583
579
  return ModelQuerySuggestUtil.combineSuggestResults<T, string>(cls, field, prefix, results, (a) => a, query && query.limit);
584
580
  }
585
581
 
586
582
  async suggest<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Promise<T[]> {
583
+ await QueryVerifier.verify(cls, query);
587
584
  const q = ModelQuerySuggestUtil.getSuggestQuery<T>(cls, field, prefix, query);
588
585
  const results = await this.query<T>(cls, q);
589
586
  return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, results, (_, b) => b, query && query.limit);