@travetto/model-mongo 8.0.0-alpha.1 → 8.0.0-alpha.11

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#L15)
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model-mongo",
3
- "version": "8.0.0-alpha.1",
3
+ "version": "8.0.0-alpha.11",
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.1",
30
- "@travetto/model": "^8.0.0-alpha.1",
31
- "@travetto/model-query": "^8.0.0-alpha.1",
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.11",
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.1"
36
+ "@travetto/cli": "^8.0.0-alpha.15"
36
37
  },
37
38
  "peerDependenciesMeta": {
38
39
  "@travetto/cli": {
@@ -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
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));
@@ -131,7 +159,10 @@ export class MongoModelService implements
131
159
 
132
160
  async upsertModel(cls: Class): Promise<void> {
133
161
  const col = await this.getStore(cls);
134
- 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
+ ];
135
166
  const existingIndices = (await col.indexes().catch(() => [])).filter(idx => idx.name !== '_id_');
136
167
 
137
168
  const pendingMap = Object.fromEntries(indices.map(pair => [pair[1].name!, pair]));
@@ -405,54 +436,116 @@ export class MongoModelService implements
405
436
  }
406
437
 
407
438
  // Indexed
408
- async getByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<T> {
409
- const { key } = ModelIndexedUtil.computeIndexKey(cls, idx, body);
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> {
410
444
  const store = await this.getStore(cls);
445
+
446
+ const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
447
+
411
448
  const result = await store.findOne(
412
- this.getWhereFilter(
413
- cls,
414
- castTo(ModelIndexedUtil.projectIndex(cls, idx, body))
415
- )
449
+ this.getWhereFilter(cls, castTo(computed.project({ sort: true, includeId: true })))
416
450
  );
417
451
  if (!result) {
418
- throw new NotFoundError(`${cls.name}: ${idx}`, key);
452
+ throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
419
453
  }
420
454
  return await this.postLoad(cls, result);
455
+
421
456
  }
422
457
 
423
- async deleteByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<void> {
424
- 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> {
425
463
  const store = await this.getStore(cls);
464
+ const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
465
+
426
466
  const result = await store.deleteOne(
427
- this.getWhereFilter(
428
- cls,
429
- castTo(ModelIndexedUtil.projectIndex(cls, idx, body))
430
- )
467
+ this.getWhereFilter(cls, castTo(computed.project({ sort: true, includeId: true })))
431
468
  );
432
- if (result.deletedCount) {
433
- return;
469
+ if (!result.deletedCount) {
470
+ throw new NotFoundError(`${cls.name}: ${idx}`, computed.getKey({ sort: true }));
434
471
  }
435
- throw new NotFoundError(`${cls.name}: ${idx}`, key);
436
472
  }
437
473
 
438
- 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> {
439
479
  return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
440
480
  }
441
481
 
442
- async * listByIndex<T extends ModelType>(cls: Class<T>, idx: string, body?: DeepPartial<T>): AsyncIterable<T> {
443
- const store = await this.getStore(cls);
444
- const idxConfig = ModelRegistryIndex.getIndex(cls, idx, ['sorted', 'unsorted']);
445
-
446
- const where = this.getWhereFilter(
447
- cls,
448
- castTo(ModelIndexedUtil.projectIndex(cls, idx, body, { emptySortValue: { $exists: true } }))
449
- );
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 pageByIndex<
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 };
450
521
 
451
- const sort = castTo<{ [ListIndexSymbol]: PlainIdx }>(idxConfig)[ListIndexSymbol] ??= MongoUtil.getPlainIndex(idxConfig);
452
- const cursor = store.find(where, { timeout: true }).batchSize(100).sort(castTo(sort));
522
+ }
523
+ }
453
524
 
454
- for await (const item of cursor) {
455
- yield await this.postLoad(cls, item);
525
+ async * listByIndex<
526
+ T extends ModelType,
527
+ K extends KeyedIndexSelection<T>,
528
+ S extends SortedIndexSelection<T>
529
+ >(
530
+ cls: Class<T>,
531
+ idx: SortedIndex<T, K, S>,
532
+ body: KeyedIndexBody<T, K>,
533
+ ): AsyncIterable<T> {
534
+ let offset = 0;
535
+ while (offset >= 0) {
536
+ const cursor = (await this.#buildIndexQuery(cls, idx, body))
537
+ .limit(100)
538
+ .skip(offset);
539
+
540
+ const items = await cursor.toArray();
541
+ if (items.length === 0) {
542
+ offset = -1;
543
+ } else {
544
+ offset += items.length;
545
+ for (const item of items) {
546
+ yield await this.postLoad(cls, item);
547
+ }
548
+ }
456
549
  }
457
550
  }
458
551