@travetto/model-mongo 5.0.0-rc.1 → 5.0.0-rc.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 +4 -4
- package/package.json +5 -5
- package/src/config.ts +2 -2
- package/src/internal/util.ts +22 -70
- package/src/service.ts +124 -127
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)
|
|
@@ -48,7 +48,7 @@ where the [MongoModelConfig](https://github.com/travetto/travetto/tree/main/modu
|
|
|
48
48
|
```typescript
|
|
49
49
|
import type mongo from 'mongodb';
|
|
50
50
|
|
|
51
|
-
import {
|
|
51
|
+
import { TimeSpan, TimeUtil, RuntimeResources, Runtime } from '@travetto/runtime';
|
|
52
52
|
import { Config } from '@travetto/config';
|
|
53
53
|
import { Field } from '@travetto/schema';
|
|
54
54
|
|
|
@@ -151,7 +151,7 @@ export class MongoModelConfig {
|
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
if (!
|
|
154
|
+
if (!Runtime.production) {
|
|
155
155
|
opts.waitQueueTimeoutMS ??= TimeUtil.asMillis(1, 'd'); // Wait a day in dev mode
|
|
156
156
|
}
|
|
157
157
|
}
|
|
@@ -174,4 +174,4 @@ export class MongoModelConfig {
|
|
|
174
174
|
}
|
|
175
175
|
```
|
|
176
176
|
|
|
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/
|
|
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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-mongo",
|
|
3
|
-
"version": "5.0.0-rc.
|
|
3
|
+
"version": "5.0.0-rc.11",
|
|
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.
|
|
29
|
-
"@travetto/model": "^5.0.0-rc.
|
|
30
|
-
"@travetto/model-query": "^5.0.0-rc.
|
|
28
|
+
"@travetto/config": "^5.0.0-rc.11",
|
|
29
|
+
"@travetto/model": "^5.0.0-rc.11",
|
|
30
|
+
"@travetto/model-query": "^5.0.0-rc.11",
|
|
31
31
|
"mongodb": "^6.8.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@travetto/command": "^5.0.0-rc.
|
|
34
|
+
"@travetto/command": "^5.0.0-rc.10"
|
|
35
35
|
},
|
|
36
36
|
"peerDependenciesMeta": {
|
|
37
37
|
"@travetto/command": {
|
package/src/config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type mongo from 'mongodb';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { TimeSpan, TimeUtil, RuntimeResources, Runtime } from '@travetto/runtime';
|
|
4
4
|
import { Config } from '@travetto/config';
|
|
5
5
|
import { Field } from '@travetto/schema';
|
|
6
6
|
|
|
@@ -103,7 +103,7 @@ export class MongoModelConfig {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
if (!
|
|
106
|
+
if (!Runtime.production) {
|
|
107
107
|
opts.waitQueueTimeoutMS ??= TimeUtil.asMillis(1, 'd'); // Wait a day in dev mode
|
|
108
108
|
}
|
|
109
109
|
}
|
package/src/internal/util.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Binary, ObjectId } from 'mongodb';
|
|
2
2
|
|
|
3
|
-
import { Class } from '@travetto/
|
|
4
|
-
import { DistanceUnit,
|
|
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?:
|
|
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 =
|
|
33
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
75
|
-
|
|
76
|
-
|
|
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 (
|
|
80
|
+
if (ModelQueryUtil.has$And(o)) {
|
|
97
81
|
return { $and: o.$and.map(x => this.extractWhereClause<T>(cls, x)) };
|
|
98
|
-
} else if (
|
|
82
|
+
} else if (ModelQueryUtil.has$Or(o)) {
|
|
99
83
|
return { $or: o.$or.map(x => this.extractWhereClause<T>(cls, x)) };
|
|
100
|
-
} else if (
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
177
|
-
const
|
|
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
|
-
|
|
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,27 +1,28 @@
|
|
|
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,
|
|
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
|
-
|
|
12
|
-
|
|
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, ModelBlobUtil
|
|
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 {
|
|
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
|
-
import {
|
|
25
|
+
import { FieldConfig, SchemaRegistry, SchemaValidator } from '@travetto/schema';
|
|
25
26
|
|
|
26
27
|
import { ModelCrudUtil } from '@travetto/model/src/internal/service/crud';
|
|
27
28
|
import { ModelIndexedUtil } from '@travetto/model/src/internal/service/indexed';
|
|
@@ -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 } 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
|
-
|
|
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
|
|
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,
|
|
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 #
|
|
69
|
-
|
|
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(
|
|
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:
|
|
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
|
-
|
|
92
|
-
return MongoUtil.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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 ===
|
|
156
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
237
|
+
.reduce<Partial<Record<'$unset' | '$set', Record<string, unknown>>>>((acc, [k, v]) => {
|
|
244
238
|
if (v === null || v === undefined) {
|
|
245
|
-
|
|
246
|
-
const o = (acc.$unset ??= {}) as Record<string, unknown>;
|
|
247
|
-
o[k] = v;
|
|
239
|
+
(acc.$unset ??= {})[k] = v;
|
|
248
240
|
} else {
|
|
249
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
294
|
-
async
|
|
283
|
+
// Blob
|
|
284
|
+
async insertBlob(location: string, input: BinaryInput, meta?: BlobMeta, errorIfExisting = false): Promise<void> {
|
|
285
|
+
await this.describeBlob(location);
|
|
286
|
+
if (errorIfExisting) {
|
|
287
|
+
throw new ExistsError(ModelBlobNamespace, location);
|
|
288
|
+
}
|
|
289
|
+
return this.upsertBlob(location, input, meta);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async upsertBlob(location: string, input: BinaryInput, meta?: BlobMeta): Promise<void> {
|
|
293
|
+
const [stream, blobMeta] = await ModelBlobUtil.getInput(input, meta);
|
|
295
294
|
const writeStream = this.#bucket.openUploadStream(location, {
|
|
296
|
-
contentType:
|
|
297
|
-
metadata:
|
|
295
|
+
contentType: blobMeta.contentType,
|
|
296
|
+
metadata: blobMeta
|
|
298
297
|
});
|
|
299
298
|
|
|
300
|
-
await pipeline(
|
|
299
|
+
await pipeline(stream, writeStream);
|
|
301
300
|
}
|
|
302
301
|
|
|
303
|
-
async
|
|
304
|
-
const meta = await this.
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
|
|
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
|
|
319
|
-
return (await this.#
|
|
310
|
+
async describeBlob(location: string): Promise<BlobMeta> {
|
|
311
|
+
return (await this.#describeBlobRaw(location)).metadata ?? {};
|
|
320
312
|
}
|
|
321
313
|
|
|
322
|
-
async
|
|
323
|
-
const fileId = (await this.#
|
|
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
|
-
|
|
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
|
-
|
|
369
|
-
MongoUtil.postLoadId(op.insert as T);
|
|
359
|
+
MongoUtil.postLoadId(asFull(op.insert));
|
|
370
360
|
}
|
|
371
361
|
}
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
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.
|
|
396
|
+
this.getWhereFilter(
|
|
409
397
|
cls,
|
|
410
|
-
|
|
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.
|
|
411
|
+
this.getWhereFilter(
|
|
425
412
|
cls,
|
|
426
|
-
|
|
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
|
-
|
|
449
|
-
const where = this.getWhere(
|
|
434
|
+
const where = this.getWhereFilter(
|
|
450
435
|
cls,
|
|
451
|
-
|
|
452
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
499
|
+
const where = ModelQueryUtil.getWhereClause(cls, query.where);
|
|
500
|
+
where.id = item.id;
|
|
511
501
|
|
|
512
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
|
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
|
-
|
|
553
|
-
|
|
554
|
-
|
|
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.
|
|
551
|
+
q = { $and: [q, MongoUtil.extractWhereFilter(cls, query.where)] };
|
|
566
552
|
}
|
|
567
553
|
|
|
568
|
-
|
|
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
|
-
|
|
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);
|