@travetto/model-mongo 8.0.0-alpha.0 → 8.0.0-alpha.10

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
@@ -19,8 +19,8 @@ Supported features:
19
19
  * [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#L11)
20
20
  * [Expiry](https://github.com/travetto/travetto/tree/main/module/model/src/types/expiry.ts#L10)
21
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
22
  * [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#L23)
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
26
  * [Query](https://github.com/travetto/travetto/tree/main/module/model-query/src/types/query.ts#L10)
@@ -43,7 +43,7 @@ export class Init {
43
43
  }
44
44
  ```
45
45
 
46
- where the [MongoModelConfig](https://github.com/travetto/travetto/tree/main/module/model-mongo/src/config.ts#L23) is defined by:
46
+ where the [MongoModelConfig](https://github.com/travetto/travetto/tree/main/module/model-mongo/src/config.ts#L24) is defined by:
47
47
 
48
48
  **Code: Structure of MongoModelConfig**
49
49
  ```typescript
@@ -104,7 +104,8 @@ export class MongoModelConfig {
104
104
  /**
105
105
  * Load all the ssl certs as needed
106
106
  */
107
- async postConstruct(): Promise<void> {
107
+ @PostConstruct()
108
+ async finalizeConfig(): Promise<void> {
108
109
  if (this.connectionString) {
109
110
  const details = new URL(this.connectionString);
110
111
  this.hosts ??= details.hostname.split(',').filter(host => !!host);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model-mongo",
3
- "version": "8.0.0-alpha.0",
3
+ "version": "8.0.0-alpha.10",
4
4
  "type": "module",
5
5
  "description": "Mongo backing for the travetto model module.",
6
6
  "keywords": [
@@ -26,13 +26,14 @@
26
26
  "directory": "module/model-mongo"
27
27
  },
28
28
  "dependencies": {
29
- "@travetto/config": "^8.0.0-alpha.0",
30
- "@travetto/model": "^8.0.0-alpha.0",
31
- "@travetto/model-query": "^8.0.0-alpha.0",
32
- "mongodb": "^7.1.0"
29
+ "@travetto/config": "^8.0.0-alpha.10",
30
+ "@travetto/model": "^8.0.0-alpha.10",
31
+ "@travetto/model-indexed": "^8.0.0-alpha.10",
32
+ "@travetto/model-query": "^8.0.0-alpha.10",
33
+ "mongodb": "^7.1.1"
33
34
  },
34
35
  "peerDependencies": {
35
- "@travetto/cli": "^8.0.0-alpha.0"
36
+ "@travetto/cli": "^8.0.0-alpha.15"
36
37
  },
37
38
  "peerDependenciesMeta": {
38
39
  "@travetto/cli": {
package/src/config.ts CHANGED
@@ -3,6 +3,7 @@ import type mongo from 'mongodb';
3
3
  import { type TimeSpan, Runtime, RuntimeResources, BinaryUtil, CodecUtil, type BinaryType, type BinaryArray } from '@travetto/runtime';
4
4
  import { Config } from '@travetto/config';
5
5
  import { Field } from '@travetto/schema';
6
+ import { PostConstruct } from '@travetto/di';
6
7
 
7
8
  const readCert = async (input: BinaryType | string): Promise<BinaryArray> => {
8
9
  if (BinaryUtil.isBinaryType(input)) {
@@ -76,7 +77,8 @@ export class MongoModelConfig {
76
77
  /**
77
78
  * Load all the ssl certs as needed
78
79
  */
79
- async postConstruct(): Promise<void> {
80
+ @PostConstruct()
81
+ async finalizeConfig(): Promise<void> {
80
82
  if (this.connectionString) {
81
83
  const details = new URL(this.connectionString);
82
84
  this.hosts ??= details.hostname.split(',').filter(host => !!host);
@@ -3,15 +3,14 @@ import {
3
3
  type IndexDescriptionInfo
4
4
  } from 'mongodb';
5
5
 
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';
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
9
  import { DataUtil, SchemaRegistryIndex, type Point } from '@travetto/schema';
10
+ import { isModelIndexedIndex } from '@travetto/model-indexed';
10
11
 
11
12
  const PointConcrete = toConcrete<Point>();
12
13
 
13
- type IdxConfig = CreateIndexesOptions;
14
-
15
14
  /**
16
15
  * Converting units to various radians
17
16
  */
@@ -25,7 +24,19 @@ const RADIANS_TO: Record<DistanceUnit, number> = {
25
24
 
26
25
  export type WithId<T, I = unknown> = T & { _id?: I };
27
26
  export type BasicIdx = Record<string, IndexDirection>;
28
- export type PlainIdx = Record<string, -1 | 0 | 1>;
27
+
28
+ function flattenKeys(obj: Record<string, unknown>, prefix = ''): Record<string, 1 | -1> {
29
+ const out: Record<string, 1 | -1> = {};
30
+ for (const [key, value] of Object.entries(obj)) {
31
+ const path = prefix ? `${prefix}.${key}` : key;
32
+ if (typeof value === 'object' && value !== null) {
33
+ Object.assign(out, flattenKeys(castTo(value), path));
34
+ } else {
35
+ out[path] = typeof value === 'boolean' ? (value ? 1 : -1) : castTo<-1 | 1>(value);
36
+ }
37
+ }
38
+ return out;
39
+ }
29
40
 
30
41
  /**
31
42
  * Basic mongo utils for conforming to the model module
@@ -36,19 +47,6 @@ export class MongoUtil {
36
47
  return `${cls.Ⲑid}__${name}`.replace(/[^a-zA-Z0-9_]+/g, '_');
37
48
  }
38
49
 
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
50
  static uuid(value: string): Binary {
53
51
  try {
54
52
  return new Binary(
@@ -166,8 +164,8 @@ export class MongoUtil {
166
164
  return out;
167
165
  }
168
166
 
169
- static getExtraIndices<T extends ModelType>(cls: Class<T>): [BasicIdx, IdxConfig][] {
170
- const out: [BasicIdx, IdxConfig][] = [];
167
+ static getExtraIndices<T extends ModelType>(cls: Class<T>): [BasicIdx, CreateIndexesOptions][] {
168
+ const out: [BasicIdx, CreateIndexesOptions][] = [];
171
169
  const textFields: string[] = [];
172
170
  SchemaRegistryIndex.visitFields(cls, (field, path) => {
173
171
  if (field.type === PointConcrete) {
@@ -185,19 +183,26 @@ export class MongoUtil {
185
183
  return out;
186
184
  }
187
185
 
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
- }
186
+ static getIndex(cls: Class, idx: IndexConfig): [BasicIdx, CreateIndexesOptions] {
187
+ const name = this.namespaceIndex(cls, idx.name);
188
+ if (isModelQueryIndex(idx)) {
189
+ const out = idx.fields.reduce(
190
+ (acc, field) => ({ ...acc, ...flattenKeys(castTo(field)) }),
191
+ castTo<Record<string, -1 | 0 | 1>>({}));
195
192
 
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]);
193
+ return [out, { name, unique: idx.type === 'query:unique', }];
194
+ } else if (isModelIndexedIndex(idx)) {
195
+ const filter = Object.fromEntries([
196
+ ...idx.keyTemplate.map(({ path }) => [path.join('.'), 1]),
197
+ ...idx.sortTemplate.map(({ path, value }) => [path.join('.'), value === -1 ? -1 : 1])
198
+ ]);
199
+ switch (idx.type) {
200
+ case 'indexed:keyed': return [filter, { name, unique: idx.unique }];
201
+ case 'indexed:sorted': return [filter, { name }];
202
+ }
203
+ } else {
204
+ throw new IndexNotSupported(cls, idx);
205
+ }
201
206
  }
202
207
 
203
208
  static prepareCursor<T extends ModelType>(cls: Class<T>, cursor: FindCursor<T | MongoWithId<T>>, query: PageableModelQuery<T>): FindCursor<T> {
package/src/service.ts CHANGED
@@ -3,13 +3,13 @@ import {
3
3
  type Db, GridFSBucket, MongoClient, type GridFSFile, type Collection,
4
4
  type ObjectId, type RootFilterOperators, type Filter,
5
5
  type WithId as MongoWithId,
6
+ type FindCursor,
6
7
  } from 'mongodb';
7
8
 
8
9
  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
10
+ ModelRegistryIndex, type ModelType, type OptionalId, type ModelCrudSupport, type ModelStorageSupport, type ModelExpirySupport,
11
+ type ModelBulkSupport, type BulkOperation, type BulkResponse, NotFoundError, ExistsError, type ModelBlobSupport,
12
+ ModelCrudUtil, ModelStorageUtil, ModelExpiryUtil, ModelBulkUtil,
13
13
  } from '@travetto/model';
14
14
  import {
15
15
  type ModelQuery, type ModelQueryCrudSupport, type ModelQueryFacetSupport, type ModelQuerySupport,
@@ -17,18 +17,22 @@ import {
17
17
  QueryVerifier, ModelQueryUtil, ModelQuerySuggestUtil, ModelQueryCrudUtil,
18
18
  type ModelQueryFacet,
19
19
  } from '@travetto/model-query';
20
+ import {
21
+ type ModelIndexedSupport, type KeyedIndexSelection, type KeyedIndexBody, type ListPageOptions, ModelIndexedUtil,
22
+ type SingleItemIndex, type SortedIndexSelection, type ListPageResult, type SortedIndex, type FullKeyedIndexBody,
23
+ type FullKeyedIndexWithPartialBody, ModelIndexedComputedIndex,
24
+ } from '@travetto/model-indexed';
20
25
 
21
26
  import {
22
- ShutdownManager, type Class, type DeepPartial, TypedObject,
27
+ ShutdownManager, type Class, TypedObject,
23
28
  castTo, asFull, type BinaryMetadata, type ByteRange, type BinaryType, BinaryUtil, BinaryMetadataUtil,
29
+ JSONUtil,
24
30
  } from '@travetto/runtime';
25
- import { Injectable } from '@travetto/di';
31
+ import { Injectable, PostConstruct } from '@travetto/di';
26
32
 
27
- import { MongoUtil, type PlainIdx, type WithId } from './internal/util.ts';
33
+ import { MongoUtil, type WithId } from './internal/util.ts';
28
34
  import type { MongoModelConfig } from './config.ts';
29
35
 
30
- const ListIndexSymbol = Symbol();
31
-
32
36
  type BlobRaw = GridFSFile & { metadata?: BinaryMetadata };
33
37
 
34
38
  type MongoTextSearch = RootFilterOperators<unknown>['$text'];
@@ -54,6 +58,30 @@ export class MongoModelService implements
54
58
 
55
59
  constructor(config: MongoModelConfig) { this.config = config; }
56
60
 
61
+ async #buildIndexQuery<
62
+ T extends ModelType,
63
+ K extends KeyedIndexSelection<T>,
64
+ S extends SortedIndexSelection<T>
65
+ >(
66
+ cls: Class<T>,
67
+ idx: SortedIndex<T, K, S>,
68
+ body: KeyedIndexBody<T, K>
69
+ ): Promise<FindCursor> {
70
+ const store = await this.getStore(cls);
71
+ const computed = ModelIndexedComputedIndex.get(idx, body).validate();
72
+
73
+ const where = this.getWhereFilter(cls, castTo(computed.project()));
74
+
75
+ let q = store.find(where, { timeout: true })
76
+ .batchSize(100);
77
+
78
+ // TODO: We could cache this
79
+ if ('sort' in idx) {
80
+ q = q.sort(idx.sortTemplate.map(({ path, value }) => [path.join('.'), value] as const));
81
+ }
82
+ return q;
83
+ }
84
+
57
85
  restoreId(item: { id?: string, _id?: unknown }): void {
58
86
  if (item._id) {
59
87
  item.id ??= MongoUtil.idToString(castTo(item._id));
@@ -98,7 +126,8 @@ export class MongoModelService implements
98
126
  return files[0];
99
127
  }
100
128
 
101
- async postConstruct(): Promise<void> {
129
+ @PostConstruct()
130
+ async initializeClient(): Promise<void> {
102
131
  this.client = await MongoClient.connect(this.config.url, {
103
132
  ...this.config.connectionOptions,
104
133
  useBigInt64: true,
@@ -130,7 +159,10 @@ export class MongoModelService implements
130
159
 
131
160
  async upsertModel(cls: Class): Promise<void> {
132
161
  const col = await this.getStore(cls);
133
- const indices = MongoUtil.getIndices(cls, ModelRegistryIndex.getConfig(cls).indices);
162
+ const indices = [
163
+ ...ModelRegistryIndex.getIndices(cls).map(idx => MongoUtil.getIndex(cls, idx)),
164
+ ...MongoUtil.getExtraIndices(cls)
165
+ ];
134
166
  const existingIndices = (await col.indexes().catch(() => [])).filter(idx => idx.name !== '_id_');
135
167
 
136
168
  const pendingMap = Object.fromEntries(indices.map(pair => [pair[1].name!, pair]));
@@ -404,54 +436,89 @@ export class MongoModelService implements
404
436
  }
405
437
 
406
438
  // Indexed
407
- async getByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<T> {
408
- const { key } = ModelIndexedUtil.computeIndexKey(cls, idx, body);
439
+ async getByIndex<
440
+ T extends ModelType,
441
+ K extends KeyedIndexSelection<T>,
442
+ S extends SortedIndexSelection<T>
443
+ >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<T> {
409
444
  const store = await this.getStore(cls);
445
+
446
+ const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
447
+
410
448
  const result = await store.findOne(
411
- this.getWhereFilter(
412
- cls,
413
- castTo(ModelIndexedUtil.projectIndex(cls, idx, body))
414
- )
449
+ this.getWhereFilter(cls, castTo(computed.project({ sort: true })))
415
450
  );
416
451
  if (!result) {
417
- throw new NotFoundError(`${cls.name}: ${idx}`, key);
452
+ throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
418
453
  }
419
454
  return await this.postLoad(cls, result);
455
+
420
456
  }
421
457
 
422
- async deleteByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<void> {
423
- const { key } = ModelIndexedUtil.computeIndexKey(cls, idx, body);
458
+ async deleteByIndex<
459
+ T extends ModelType,
460
+ K extends KeyedIndexSelection<T>,
461
+ S extends SortedIndexSelection<T>
462
+ >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<void> {
424
463
  const store = await this.getStore(cls);
464
+ const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
465
+
425
466
  const result = await store.deleteOne(
426
- this.getWhereFilter(
427
- cls,
428
- castTo(ModelIndexedUtil.projectIndex(cls, idx, body))
429
- )
467
+ this.getWhereFilter(cls, castTo(computed.project({ sort: true })))
430
468
  );
431
- if (result.deletedCount) {
432
- return;
469
+ if (!result.deletedCount) {
470
+ throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
433
471
  }
434
- throw new NotFoundError(`${cls.name}: ${idx}`, key);
435
472
  }
436
473
 
437
- async upsertByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: OptionalId<T>): Promise<T> {
474
+ upsertByIndex<
475
+ T extends ModelType,
476
+ K extends KeyedIndexSelection<T>,
477
+ S extends SortedIndexSelection<T>
478
+ >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: OptionalId<T>): Promise<T> {
438
479
  return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
439
480
  }
440
481
 
441
- async * listByIndex<T extends ModelType>(cls: Class<T>, idx: string, body?: DeepPartial<T>): AsyncIterable<T> {
442
- const store = await this.getStore(cls);
443
- const idxConfig = ModelRegistryIndex.getIndex(cls, idx, ['sorted', 'unsorted']);
444
-
445
- const where = this.getWhereFilter(
446
- cls,
447
- castTo(ModelIndexedUtil.projectIndex(cls, idx, body, { emptySortValue: { $exists: true } }))
448
- );
449
-
450
- const sort = castTo<{ [ListIndexSymbol]: PlainIdx }>(idxConfig)[ListIndexSymbol] ??= MongoUtil.getPlainIndex(idxConfig);
451
- const cursor = store.find(where, { timeout: true }).batchSize(100).sort(castTo(sort));
482
+ updateByIndex<
483
+ T extends ModelType,
484
+ K extends KeyedIndexSelection<T>,
485
+ S extends SortedIndexSelection<T>
486
+ >(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: T): Promise<T> {
487
+ return ModelIndexedUtil.naiveUpdate(this, cls, idx, body);
488
+ }
489
+
490
+ async updatePartialByIndex<
491
+ T extends ModelType,
492
+ K extends KeyedIndexSelection<T>,
493
+ S extends SortedIndexSelection<T>
494
+ >(cls: Class<T>, idx: SingleItemIndex<T, K>, body: FullKeyedIndexWithPartialBody<T, K, S>): Promise<T> {
495
+ const item = await ModelCrudUtil.naivePartialUpdate(cls, () => this.getByIndex(cls, idx, castTo(body)), castTo(body));
496
+ return this.update(cls, item);
497
+ }
498
+
499
+ async listByIndex<
500
+ T extends ModelType,
501
+ K extends KeyedIndexSelection<T>,
502
+ S extends SortedIndexSelection<T>
503
+ >(
504
+ cls: Class<T>,
505
+ idx: SortedIndex<T, K, S>,
506
+ body: KeyedIndexBody<T, K>,
507
+ options?: ListPageOptions,
508
+ ): Promise<ListPageResult<T>> {
509
+ {
510
+ const offset = options?.offset ? JSONUtil.fromBase64<number>(options.offset) : 0;
511
+ const limit = options?.limit ?? 100;
512
+ const cursor = (await this.#buildIndexQuery(cls, idx, body))
513
+ .limit(limit)
514
+ .skip(offset);
515
+
516
+ const items: T[] = [];
517
+ for await (const item of cursor) {
518
+ items.push(await this.postLoad(cls, item));
519
+ }
520
+ return { items, nextOffset: items.length ? JSONUtil.toBase64(offset + items.length) : undefined };
452
521
 
453
- for await (const item of cursor) {
454
- yield await this.postLoad(cls, item);
455
522
  }
456
523
  }
457
524