@travetto/model-mongo 2.1.5 → 2.2.2
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 +9 -9
- package/package.json +4 -4
- package/src/config.ts +6 -6
- package/src/internal/util.ts +36 -15
- package/src/service.ts +68 -57
package/README.md
CHANGED
|
@@ -10,10 +10,10 @@ npm install @travetto/model-mongo
|
|
|
10
10
|
|
|
11
11
|
This module provides an [mongodb](https://mongodb.com)-based implementation for the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations."). This source allows the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") module to read, write and query against [mongodb](https://mongodb.com).. Given the dynamic nature of [mongodb](https://mongodb.com), during development when models are modified, nothing needs to be done to adapt to the latest schema.
|
|
12
12
|
|
|
13
|
-
Supported
|
|
13
|
+
Supported features:
|
|
14
14
|
|
|
15
15
|
* [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/service/crud.ts#L11)
|
|
16
|
-
* [Streaming](https://github.com/travetto/travetto/tree/main/module/model/src/service/stream.ts#
|
|
16
|
+
* [Streaming](https://github.com/travetto/travetto/tree/main/module/model/src/service/stream.ts#L3)
|
|
17
17
|
* [Expiry](https://github.com/travetto/travetto/tree/main/module/model/src/service/expiry.ts#L11)
|
|
18
18
|
* [Bulk](https://github.com/travetto/travetto/tree/main/module/model/src/service/bulk.ts#L23)
|
|
19
19
|
* [Indexed](https://github.com/travetto/travetto/tree/main/module/model/src/service/indexed.ts#L12)
|
|
@@ -95,22 +95,22 @@ export class MongoModelConfig {
|
|
|
95
95
|
};
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
|
-
* Should we
|
|
98
|
+
* Should we auto create the db
|
|
99
99
|
*/
|
|
100
100
|
autoCreate?: boolean;
|
|
101
101
|
|
|
102
102
|
/**
|
|
103
|
-
* Frequency of culling for
|
|
103
|
+
* Frequency of culling for cullable content
|
|
104
104
|
*/
|
|
105
105
|
cullRate?: number | TimeSpan;
|
|
106
106
|
|
|
107
107
|
/**
|
|
108
108
|
* Load a resource
|
|
109
109
|
*/
|
|
110
|
-
async fetch(val: string) {
|
|
110
|
+
async fetch(val: string): Promise<string> {
|
|
111
111
|
return ResourceManager.read(val)
|
|
112
112
|
.then(res => typeof res === 'string' ? res : res.toString('utf8'))
|
|
113
|
-
.catch(
|
|
113
|
+
.catch(() => fs.readFile(val)
|
|
114
114
|
.then(res => typeof res === 'string' ? res : res.toString('utf8'))
|
|
115
115
|
)
|
|
116
116
|
.catch(() => val);
|
|
@@ -119,7 +119,7 @@ export class MongoModelConfig {
|
|
|
119
119
|
/**
|
|
120
120
|
* Load all the ssl certs as needed
|
|
121
121
|
*/
|
|
122
|
-
async postConstruct() {
|
|
122
|
+
async postConstruct(): Promise<void> {
|
|
123
123
|
const opts = this.options;
|
|
124
124
|
if (opts.ssl) {
|
|
125
125
|
if (opts.sslCert) {
|
|
@@ -140,7 +140,7 @@ export class MongoModelConfig {
|
|
|
140
140
|
/**
|
|
141
141
|
* Build connection URLs
|
|
142
142
|
*/
|
|
143
|
-
get url() {
|
|
143
|
+
get url(): string {
|
|
144
144
|
const hosts = this.hosts
|
|
145
145
|
.map(h => (this.srvRecord || h.includes(':')) ? h : `${h}:${this.port}`)
|
|
146
146
|
.join(',');
|
|
@@ -159,4 +159,4 @@ export class MongoModelConfig {
|
|
|
159
159
|
standard [Configuration](https://github.com/travetto/travetto/tree/main/module/config#readme "Environment-aware config management using yaml files")resolution paths.
|
|
160
160
|
|
|
161
161
|
|
|
162
|
-
The SSL file options in `clientOptions` will automatically be resolved to files when given a path. This path can be a [ResourceManager](https://github.com/travetto/travetto/tree/main/module/base/src/resource.ts#
|
|
162
|
+
The SSL file options in `clientOptions` will automatically be resolved to files when given a path. This path can be a [ResourceManager](https://github.com/travetto/travetto/tree/main/module/base/src/resource.ts#L16) path or just a standard file path.
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-mongo",
|
|
3
3
|
"displayName": "MongoDB Model Support",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.2.2",
|
|
5
5
|
"description": "Mongo backing for the travetto model module.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"mongo",
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"directory": "module/model-mongo"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@travetto/config": "^2.
|
|
30
|
-
"@travetto/model": "^2.
|
|
31
|
-
"@travetto/model-query": "2.
|
|
29
|
+
"@travetto/config": "^2.2.2",
|
|
30
|
+
"@travetto/model": "^2.2.2",
|
|
31
|
+
"@travetto/model-query": "2.2.2",
|
|
32
32
|
"mongodb": "^4.8.0"
|
|
33
33
|
},
|
|
34
34
|
"publishConfig": {
|
package/src/config.ts
CHANGED
|
@@ -47,22 +47,22 @@ export class MongoModelConfig {
|
|
|
47
47
|
};
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
* Should we
|
|
50
|
+
* Should we auto create the db
|
|
51
51
|
*/
|
|
52
52
|
autoCreate?: boolean;
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
|
-
* Frequency of culling for
|
|
55
|
+
* Frequency of culling for cullable content
|
|
56
56
|
*/
|
|
57
57
|
cullRate?: number | TimeSpan;
|
|
58
58
|
|
|
59
59
|
/**
|
|
60
60
|
* Load a resource
|
|
61
61
|
*/
|
|
62
|
-
async fetch(val: string) {
|
|
62
|
+
async fetch(val: string): Promise<string> {
|
|
63
63
|
return ResourceManager.read(val)
|
|
64
64
|
.then(res => typeof res === 'string' ? res : res.toString('utf8'))
|
|
65
|
-
.catch(
|
|
65
|
+
.catch(() => fs.readFile(val)
|
|
66
66
|
.then(res => typeof res === 'string' ? res : res.toString('utf8'))
|
|
67
67
|
)
|
|
68
68
|
.catch(() => val);
|
|
@@ -71,7 +71,7 @@ export class MongoModelConfig {
|
|
|
71
71
|
/**
|
|
72
72
|
* Load all the ssl certs as needed
|
|
73
73
|
*/
|
|
74
|
-
async postConstruct() {
|
|
74
|
+
async postConstruct(): Promise<void> {
|
|
75
75
|
const opts = this.options;
|
|
76
76
|
if (opts.ssl) {
|
|
77
77
|
if (opts.sslCert) {
|
|
@@ -92,7 +92,7 @@ export class MongoModelConfig {
|
|
|
92
92
|
/**
|
|
93
93
|
* Build connection URLs
|
|
94
94
|
*/
|
|
95
|
-
get url() {
|
|
95
|
+
get url(): string {
|
|
96
96
|
const hosts = this.hosts
|
|
97
97
|
.map(h => (this.srvRecord || h.includes(':')) ? h : `${h}:${this.port}`)
|
|
98
98
|
.join(',');
|
package/src/internal/util.ts
CHANGED
|
@@ -17,29 +17,35 @@ const RADIANS_TO: Record<DistanceUnit, number> = {
|
|
|
17
17
|
rad: 1
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
export type WithId<T> = T & { _id
|
|
20
|
+
export type WithId<T> = T & { _id?: mongo.Binary };
|
|
21
|
+
const isWithId = <T extends ModelType>(o: T): o is WithId<T> => o && '_id' in o;
|
|
22
|
+
|
|
21
23
|
|
|
22
24
|
/**
|
|
23
25
|
* Basic mongo utils for conforming to the model module
|
|
24
26
|
*/
|
|
25
27
|
export class MongoUtil {
|
|
26
28
|
|
|
27
|
-
static toIndex<T extends ModelType>(f: IndexField<T>) {
|
|
29
|
+
static toIndex<T extends ModelType>(f: IndexField<T>): Record<string, number> {
|
|
28
30
|
const keys = [];
|
|
29
31
|
while (typeof f !== 'number' && typeof f !== 'boolean' && Object.keys(f)) {
|
|
30
32
|
const key = Object.keys(f)[0];
|
|
33
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
31
34
|
f = f[key as keyof typeof f] as IndexField<T>;
|
|
32
35
|
keys.push(key);
|
|
33
36
|
}
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
34
38
|
const rf = f as unknown as (number | boolean);
|
|
35
|
-
return {
|
|
39
|
+
return {
|
|
40
|
+
[keys.join('.')]: typeof rf === 'boolean' ? (rf ? 1 : 0) : rf
|
|
41
|
+
};
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
static uuid(val: string) {
|
|
44
|
+
static uuid(val: string): mongo.Binary {
|
|
39
45
|
return new mongo.Binary(Buffer.from(val.replace(/-/g, ''), 'hex'), mongo.Binary.SUBTYPE_UUID);
|
|
40
46
|
}
|
|
41
47
|
|
|
42
|
-
static idToString(id: string | mongo.ObjectId | mongo.Binary) {
|
|
48
|
+
static idToString(id: string | mongo.ObjectId | mongo.Binary): string {
|
|
43
49
|
if (typeof id === 'string') {
|
|
44
50
|
return id;
|
|
45
51
|
} else if (id instanceof mongo.ObjectId) {
|
|
@@ -49,32 +55,41 @@ export class MongoUtil {
|
|
|
49
55
|
}
|
|
50
56
|
}
|
|
51
57
|
|
|
52
|
-
static async postLoadId<T extends ModelType>(item: T) {
|
|
53
|
-
if (item
|
|
54
|
-
item.id = this.idToString(
|
|
55
|
-
delete
|
|
58
|
+
static async postLoadId<T extends ModelType>(item: T): Promise<T> {
|
|
59
|
+
if (isWithId(item)) {
|
|
60
|
+
item.id = this.idToString(item._id!);
|
|
61
|
+
delete item._id;
|
|
56
62
|
}
|
|
57
63
|
return item;
|
|
58
64
|
}
|
|
59
65
|
|
|
60
|
-
static preInsertId<T extends ModelType>(item: T) {
|
|
66
|
+
static preInsertId<T extends ModelType>(item: T): T {
|
|
61
67
|
if (item && item.id) {
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
69
|
+
const itemWithId = item as WithId<T>;
|
|
70
|
+
itemWithId._id = this.uuid(item.id);
|
|
71
|
+
// @ts-expect-error
|
|
72
|
+
delete item.id;
|
|
64
73
|
}
|
|
65
74
|
return item;
|
|
66
75
|
}
|
|
67
76
|
|
|
68
|
-
static prepareQuery<T extends ModelType, U extends Query<T> | ModelQuery<T>>(cls: Class<T>, query: U, checkExpiry = true) {
|
|
77
|
+
static prepareQuery<T extends ModelType, U extends Query<T> | ModelQuery<T>>(cls: Class<T>, query: U, checkExpiry = true): {
|
|
78
|
+
query: U & { where: WhereClause<T> };
|
|
79
|
+
filter: Record<string, unknown>;
|
|
80
|
+
} {
|
|
69
81
|
const q = ModelQueryUtil.getQueryAndVerify(cls, query, checkExpiry);
|
|
70
82
|
return {
|
|
71
83
|
query: q,
|
|
72
84
|
filter: q.where ? this.extractWhereClause(q.where) : {}
|
|
73
|
-
}
|
|
85
|
+
};
|
|
74
86
|
}
|
|
75
87
|
|
|
88
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
76
89
|
static has$And = (o: unknown): o is ({ $and: WhereClause<unknown>[] }) => !!o && '$and' in (o as object);
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
77
91
|
static has$Or = (o: unknown): o is ({ $or: WhereClause<unknown>[] }) => !!o && '$or' in (o as object);
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
78
93
|
static has$Not = (o: unknown): o is ({ $not: WhereClause<unknown> }) => !!o && '$not' in (o as object);
|
|
79
94
|
|
|
80
95
|
/**
|
|
@@ -99,7 +114,7 @@ export class MongoUtil {
|
|
|
99
114
|
static replaceId(v: string[]): mongo.Binary[];
|
|
100
115
|
static replaceId(v: string): mongo.Binary;
|
|
101
116
|
static replaceId(v: unknown): undefined;
|
|
102
|
-
static replaceId(v: string | string[] | Record<string, unknown> | unknown) {
|
|
117
|
+
static replaceId(v: string | string[] | Record<string, unknown> | unknown): unknown {
|
|
103
118
|
if (typeof v === 'string') {
|
|
104
119
|
return this.uuid(v);
|
|
105
120
|
} else if (Array.isArray(v)) {
|
|
@@ -123,10 +138,12 @@ export class MongoUtil {
|
|
|
123
138
|
*/
|
|
124
139
|
static extractSimple<T>(o: T, path: string = ''): Record<string, unknown> {
|
|
125
140
|
const out: Record<string, unknown> = {};
|
|
141
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
126
142
|
const sub = o as Record<string, unknown>;
|
|
127
143
|
const keys = Object.keys(sub);
|
|
128
144
|
for (const key of keys) {
|
|
129
145
|
const subpath = `${path}${key}`;
|
|
146
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
130
147
|
const v = sub[key] as Record<string, unknown>;
|
|
131
148
|
|
|
132
149
|
if (subpath === 'id') { // Handle ids directly
|
|
@@ -142,13 +159,17 @@ export class MongoUtil {
|
|
|
142
159
|
v[sk] = ModelQueryUtil.resolveComparator(sv);
|
|
143
160
|
}
|
|
144
161
|
} else if (firstKey === '$regex') {
|
|
162
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
145
163
|
v.$regex = Util.toRegex(v.$regex as string | RegExp);
|
|
146
164
|
} else if (firstKey && '$near' in v) {
|
|
165
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
147
166
|
const dist = v.$maxDistance as number;
|
|
167
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
148
168
|
const distance = dist / RADIANS_TO[(v.$unit as DistanceUnit ?? 'km')];
|
|
149
169
|
v.$maxDistance = distance;
|
|
150
170
|
delete v.$unit;
|
|
151
171
|
} else if (firstKey && '$geoWithin' in v) {
|
|
172
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
152
173
|
const coords = v.$geoWithin as [number, number][];
|
|
153
174
|
const first = coords[0];
|
|
154
175
|
const last = coords[coords.length - 1];
|
package/src/service.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/consistent-type-assertions */
|
|
1
2
|
import * as mongo from 'mongodb';
|
|
3
|
+
import { Readable } from 'stream';
|
|
2
4
|
|
|
3
5
|
import {
|
|
4
6
|
ModelRegistry, ModelType, OptionalId,
|
|
5
7
|
ModelCrudSupport, ModelStorageSupport, ModelStreamSupport,
|
|
6
8
|
ModelExpirySupport, ModelBulkSupport, ModelIndexedSupport,
|
|
7
|
-
StreamMeta,
|
|
9
|
+
StreamMeta, BulkOp, BulkResponse,
|
|
8
10
|
NotFoundError, ExistsError, IndexConfig
|
|
9
11
|
} from '@travetto/model';
|
|
10
12
|
import {
|
|
@@ -33,10 +35,13 @@ import { MongoModelConfig } from './config';
|
|
|
33
35
|
|
|
34
36
|
const IdxFieldsⲐ = Symbol.for('@trv:model-mongo/idx');
|
|
35
37
|
|
|
36
|
-
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
39
|
+
const asFielded = <T extends ModelType>(cfg: IndexConfig<T>): { [IdxFieldsⲐ]: mongo.Sort } => (cfg as unknown as { [IdxFieldsⲐ]: mongo.Sort });
|
|
37
40
|
|
|
38
41
|
type IdxCfg = mongo.CreateIndexesOptions;
|
|
39
42
|
|
|
43
|
+
type StreamRaw = mongo.GridFSFile & { metadata: StreamMeta };
|
|
44
|
+
|
|
40
45
|
/**
|
|
41
46
|
* Mongo-based model source
|
|
42
47
|
*/
|
|
@@ -54,8 +59,9 @@ export class MongoModelService implements
|
|
|
54
59
|
|
|
55
60
|
constructor(public readonly config: MongoModelConfig) { }
|
|
56
61
|
|
|
57
|
-
async #describeStreamRaw(location: string) {
|
|
58
|
-
|
|
62
|
+
async #describeStreamRaw(location: string): Promise<StreamRaw> {
|
|
63
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
64
|
+
const files: StreamRaw[] = (await this.#bucket.find({ filename: location }, { limit: 1 }).toArray()) as StreamRaw[];
|
|
59
65
|
|
|
60
66
|
if (!files?.length) {
|
|
61
67
|
throw new NotFoundError(STREAMS, location);
|
|
@@ -64,7 +70,7 @@ export class MongoModelService implements
|
|
|
64
70
|
return files[0];
|
|
65
71
|
}
|
|
66
72
|
|
|
67
|
-
async postConstruct() {
|
|
73
|
+
async postConstruct(): Promise<void> {
|
|
68
74
|
this.client = await mongo.MongoClient.connect(this.config.url, this.config.options);
|
|
69
75
|
this.#db = this.client.db(this.config.namespace);
|
|
70
76
|
this.#bucket = new mongo.GridFSBucket(this.#db, {
|
|
@@ -76,21 +82,21 @@ export class MongoModelService implements
|
|
|
76
82
|
ModelExpiryUtil.registerCull(this);
|
|
77
83
|
}
|
|
78
84
|
|
|
79
|
-
getWhere<T extends ModelType>(cls: Class<T>, where: WhereClause<T>, checkExpiry = true) {
|
|
85
|
+
getWhere<T extends ModelType>(cls: Class<T>, where: WhereClause<T>, checkExpiry = true): Record<string, unknown> {
|
|
80
86
|
return MongoUtil.prepareQuery(cls, { where }, checkExpiry).filter;
|
|
81
87
|
}
|
|
82
88
|
|
|
83
89
|
/**
|
|
84
90
|
* Build a mongo identifier
|
|
85
91
|
*/
|
|
86
|
-
uuid() {
|
|
92
|
+
uuid(): string {
|
|
87
93
|
return Util.uuid();
|
|
88
94
|
}
|
|
89
95
|
|
|
90
96
|
// Storage
|
|
91
|
-
async createStorage() { }
|
|
97
|
+
async createStorage(): Promise<void> { }
|
|
92
98
|
|
|
93
|
-
async deleteStorage() {
|
|
99
|
+
async deleteStorage(): Promise<void> {
|
|
94
100
|
await this.#db.dropDatabase();
|
|
95
101
|
}
|
|
96
102
|
|
|
@@ -112,21 +118,22 @@ export class MongoModelService implements
|
|
|
112
118
|
return out;
|
|
113
119
|
}
|
|
114
120
|
|
|
115
|
-
getIndicies<T extends ModelType>(cls: Class<T>) {
|
|
121
|
+
getIndicies<T extends ModelType>(cls: Class<T>): ([mongo.IndexSpecification] | [mongo.IndexSpecification, IdxCfg])[] {
|
|
116
122
|
const indices = ModelRegistry.get(cls).indices ?? [];
|
|
117
123
|
return [
|
|
118
|
-
...indices.map(idx => {
|
|
119
|
-
const combined = asFielded(idx)[IdxFieldsⲐ] ??= Object.assign({}, ...idx.fields.map(x => MongoUtil.toIndex(x
|
|
124
|
+
...indices.map((idx): [mongo.IndexSpecification, IdxCfg] => {
|
|
125
|
+
const combined = asFielded(idx)[IdxFieldsⲐ] ??= Object.assign({}, ...idx.fields.map(x => MongoUtil.toIndex(x)));
|
|
120
126
|
return [
|
|
127
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
121
128
|
combined as mongo.IndexSpecification,
|
|
122
|
-
(idx.type === 'unique' ? { unique: true } : {})
|
|
123
|
-
]
|
|
129
|
+
(idx.type === 'unique' ? { unique: true } : {})
|
|
130
|
+
];
|
|
124
131
|
}),
|
|
125
|
-
...this.getGeoIndices(cls).map(x => [x]
|
|
132
|
+
...this.getGeoIndices(cls).map((x): [mongo.IndexSpecification] => [x])
|
|
126
133
|
];
|
|
127
134
|
}
|
|
128
135
|
|
|
129
|
-
async establishIndices<T extends ModelType>(cls: Class<T>) {
|
|
136
|
+
async establishIndices<T extends ModelType>(cls: Class<T>): Promise<void> {
|
|
130
137
|
const col = await this.getStore(cls);
|
|
131
138
|
const creating = this.getIndicies(cls);
|
|
132
139
|
if (creating.length) {
|
|
@@ -137,15 +144,15 @@ export class MongoModelService implements
|
|
|
137
144
|
}
|
|
138
145
|
}
|
|
139
146
|
|
|
140
|
-
async createModel(cls: Class) {
|
|
147
|
+
async createModel(cls: Class): Promise<void> {
|
|
141
148
|
await this.establishIndices(cls);
|
|
142
149
|
}
|
|
143
150
|
|
|
144
|
-
async changeModel(cls: Class) {
|
|
151
|
+
async changeModel(cls: Class): Promise<void> {
|
|
145
152
|
await this.establishIndices(cls);
|
|
146
153
|
}
|
|
147
154
|
|
|
148
|
-
async truncateModel<T extends ModelType>(cls: Class<T>) {
|
|
155
|
+
async truncateModel<T extends ModelType>(cls: Class<T>): Promise<void> {
|
|
149
156
|
if (cls === StreamModel) {
|
|
150
157
|
try {
|
|
151
158
|
await this.#bucket.drop();
|
|
@@ -164,7 +171,7 @@ export class MongoModelService implements
|
|
|
164
171
|
}
|
|
165
172
|
|
|
166
173
|
// Crud
|
|
167
|
-
async get<T extends ModelType>(cls: Class<T>, id: string) {
|
|
174
|
+
async get<T extends ModelType>(cls: Class<T>, id: string): Promise<T> {
|
|
168
175
|
const store = await this.getStore(cls);
|
|
169
176
|
const result = await store.findOne(this.getWhere<ModelType>(cls, { id }), {});
|
|
170
177
|
if (result) {
|
|
@@ -176,8 +183,9 @@ export class MongoModelService implements
|
|
|
176
183
|
throw new NotFoundError(cls, id);
|
|
177
184
|
}
|
|
178
185
|
|
|
179
|
-
async create<T extends ModelType>(cls: Class<T>, item: OptionalId<T>) {
|
|
186
|
+
async create<T extends ModelType>(cls: Class<T>, item: OptionalId<T>): Promise<T> {
|
|
180
187
|
const cleaned = await ModelCrudUtil.preStore(cls, item, this);
|
|
188
|
+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
|
181
189
|
(cleaned as WithId<T>)._id = MongoUtil.uuid(cleaned.id);
|
|
182
190
|
|
|
183
191
|
const store = await this.getStore(cls);
|
|
@@ -189,7 +197,7 @@ export class MongoModelService implements
|
|
|
189
197
|
return cleaned;
|
|
190
198
|
}
|
|
191
199
|
|
|
192
|
-
async update<T extends ModelType>(cls: Class<T>, item: T) {
|
|
200
|
+
async update<T extends ModelType>(cls: Class<T>, item: T): Promise<T> {
|
|
193
201
|
item = await ModelCrudUtil.preStore(cls, item, this);
|
|
194
202
|
const store = await this.getStore(cls);
|
|
195
203
|
const res = await store.replaceOne(this.getWhere<ModelType>(cls, { id: item.id }), item);
|
|
@@ -199,7 +207,7 @@ export class MongoModelService implements
|
|
|
199
207
|
return item;
|
|
200
208
|
}
|
|
201
209
|
|
|
202
|
-
async upsert<T extends ModelType>(cls: Class<T>, item: OptionalId<T>) {
|
|
210
|
+
async upsert<T extends ModelType>(cls: Class<T>, item: OptionalId<T>): Promise<T> {
|
|
203
211
|
const cleaned = await ModelCrudUtil.preStore(cls, item, this);
|
|
204
212
|
const store = await this.getStore(cls);
|
|
205
213
|
try {
|
|
@@ -209,7 +217,7 @@ export class MongoModelService implements
|
|
|
209
217
|
{ upsert: true }
|
|
210
218
|
);
|
|
211
219
|
} catch (err) {
|
|
212
|
-
if (err.message.includes('duplicate key error')) {
|
|
220
|
+
if (err instanceof Error && err.message.includes('duplicate key error')) {
|
|
213
221
|
throw new ExistsError(cls, cleaned.id);
|
|
214
222
|
} else {
|
|
215
223
|
throw err;
|
|
@@ -218,7 +226,7 @@ export class MongoModelService implements
|
|
|
218
226
|
return cleaned;
|
|
219
227
|
}
|
|
220
228
|
|
|
221
|
-
async updatePartial<T extends ModelType>(cls: Class<T>, item: Partial<T> & { id: string }, view?: string) {
|
|
229
|
+
async updatePartial<T extends ModelType>(cls: Class<T>, item: Partial<T> & { id: string }, view?: string): Promise<T> {
|
|
222
230
|
const store = await this.getStore(cls);
|
|
223
231
|
|
|
224
232
|
if (view) {
|
|
@@ -232,16 +240,18 @@ export class MongoModelService implements
|
|
|
232
240
|
let final: Record<string, unknown> = item;
|
|
233
241
|
|
|
234
242
|
const items = MongoUtil.extractSimple(final);
|
|
235
|
-
final = Object
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
243
|
+
final = Object
|
|
244
|
+
.entries(items)
|
|
245
|
+
.reduce<Record<string, unknown>>((acc, [k, v]) => {
|
|
246
|
+
if (v === null || v === undefined) {
|
|
247
|
+
const o = (acc.$unset ??= {}) as Record<string, unknown>;
|
|
248
|
+
o[k] = v;
|
|
249
|
+
} else {
|
|
250
|
+
const o = (acc.$set ??= {}) as Record<string, unknown>;
|
|
251
|
+
o[k] = v;
|
|
252
|
+
}
|
|
253
|
+
return acc;
|
|
254
|
+
}, {});
|
|
245
255
|
|
|
246
256
|
const id = item.id;
|
|
247
257
|
const res = await store.findOneAndUpdate(
|
|
@@ -257,7 +267,7 @@ export class MongoModelService implements
|
|
|
257
267
|
return this.get(cls, id);
|
|
258
268
|
}
|
|
259
269
|
|
|
260
|
-
async delete<T extends ModelType>(cls: Class<T>, id: string) {
|
|
270
|
+
async delete<T extends ModelType>(cls: Class<T>, id: string): Promise<void> {
|
|
261
271
|
const store = await this.getStore(cls);
|
|
262
272
|
const result = await store.deleteOne(this.getWhere<ModelType>(cls, { id }, false));
|
|
263
273
|
if (result.deletedCount === 0) {
|
|
@@ -265,22 +275,22 @@ export class MongoModelService implements
|
|
|
265
275
|
}
|
|
266
276
|
}
|
|
267
277
|
|
|
268
|
-
async * list<T extends ModelType>(cls: Class<T>) {
|
|
278
|
+
async * list<T extends ModelType>(cls: Class<T>): AsyncIterable<T> {
|
|
269
279
|
const store = await this.getStore(cls);
|
|
270
280
|
const cursor = store.find(this.getWhere(cls, {}), { timeout: true }).batchSize(100);
|
|
271
281
|
for await (const el of cursor) {
|
|
272
282
|
try {
|
|
273
283
|
yield MongoUtil.postLoadId(await ModelCrudUtil.load(cls, el));
|
|
274
|
-
} catch (
|
|
275
|
-
if (!(
|
|
276
|
-
throw
|
|
284
|
+
} catch (err) {
|
|
285
|
+
if (!(err instanceof NotFoundError)) {
|
|
286
|
+
throw err;
|
|
277
287
|
}
|
|
278
288
|
}
|
|
279
289
|
}
|
|
280
290
|
}
|
|
281
291
|
|
|
282
292
|
// Stream
|
|
283
|
-
async upsertStream(location: string, input:
|
|
293
|
+
async upsertStream(location: string, input: Readable, meta: StreamMeta): Promise<void> {
|
|
284
294
|
const writeStream = this.#bucket.openUploadStream(location, {
|
|
285
295
|
contentType: meta.contentType,
|
|
286
296
|
metadata: meta
|
|
@@ -293,7 +303,7 @@ export class MongoModelService implements
|
|
|
293
303
|
});
|
|
294
304
|
}
|
|
295
305
|
|
|
296
|
-
async getStream(location: string) {
|
|
306
|
+
async getStream(location: string): Promise<Readable> {
|
|
297
307
|
await this.describeStream(location);
|
|
298
308
|
|
|
299
309
|
const res = await this.#bucket.openDownloadStreamByName(location);
|
|
@@ -303,18 +313,18 @@ export class MongoModelService implements
|
|
|
303
313
|
return res;
|
|
304
314
|
}
|
|
305
315
|
|
|
306
|
-
async describeStream(location: string) {
|
|
316
|
+
async describeStream(location: string): Promise<StreamMeta> {
|
|
307
317
|
return (await this.#describeStreamRaw(location)).metadata;
|
|
308
318
|
}
|
|
309
319
|
|
|
310
|
-
async deleteStream(location: string) {
|
|
320
|
+
async deleteStream(location: string): Promise<void> {
|
|
311
321
|
const fileId = (await this.#describeStreamRaw(location))._id;
|
|
312
322
|
await this.#bucket.delete(fileId);
|
|
313
323
|
}
|
|
314
324
|
|
|
315
325
|
// Bulk
|
|
316
|
-
async processBulk<T extends ModelType>(cls: Class<T>, operations: BulkOp<T>[]) {
|
|
317
|
-
const out: BulkResponse = {
|
|
326
|
+
async processBulk<T extends ModelType>(cls: Class<T>, operations: BulkOp<T>[]): Promise<BulkResponse<{ index: number }>> {
|
|
327
|
+
const out: BulkResponse<{ index: number }> = {
|
|
318
328
|
errors: [],
|
|
319
329
|
counts: {
|
|
320
330
|
delete: 0,
|
|
@@ -368,7 +378,7 @@ export class MongoModelService implements
|
|
|
368
378
|
|
|
369
379
|
if (res.hasWriteErrors()) {
|
|
370
380
|
out.errors = res.getWriteErrors();
|
|
371
|
-
for (const err of out.errors
|
|
381
|
+
for (const err of out.errors) {
|
|
372
382
|
const op = operations[err.index];
|
|
373
383
|
const k = Object.keys(op)[0] as keyof BulkResponse['counts'];
|
|
374
384
|
out.counts[k] -= 1;
|
|
@@ -380,12 +390,12 @@ export class MongoModelService implements
|
|
|
380
390
|
}
|
|
381
391
|
|
|
382
392
|
// Expiry
|
|
383
|
-
deleteExpired<T extends ModelType>(cls: Class<T>) {
|
|
393
|
+
deleteExpired<T extends ModelType>(cls: Class<T>): Promise<number> {
|
|
384
394
|
return ModelQueryExpiryUtil.deleteExpired(this, cls);
|
|
385
395
|
}
|
|
386
396
|
|
|
387
397
|
// Indexed
|
|
388
|
-
async getByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>) {
|
|
398
|
+
async getByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<T> {
|
|
389
399
|
const { key } = ModelIndexedUtil.computeIndexKey(cls, idx, body);
|
|
390
400
|
const store = await this.getStore(cls);
|
|
391
401
|
const result = await store.findOne(
|
|
@@ -400,7 +410,7 @@ export class MongoModelService implements
|
|
|
400
410
|
return await ModelCrudUtil.load(cls, result);
|
|
401
411
|
}
|
|
402
412
|
|
|
403
|
-
async deleteByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>) {
|
|
413
|
+
async deleteByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<void> {
|
|
404
414
|
const { key } = ModelIndexedUtil.computeIndexKey(cls, idx, body);
|
|
405
415
|
const store = await this.getStore(cls);
|
|
406
416
|
const result = await store.deleteOne(
|
|
@@ -419,7 +429,7 @@ export class MongoModelService implements
|
|
|
419
429
|
return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
|
|
420
430
|
}
|
|
421
431
|
|
|
422
|
-
async * listByIndex<T extends ModelType>(cls: Class<T>, idx: string, body?: DeepPartial<T>) {
|
|
432
|
+
async * listByIndex<T extends ModelType>(cls: Class<T>, idx: string, body?: DeepPartial<T>): AsyncIterable<T> {
|
|
423
433
|
const store = await this.getStore(cls);
|
|
424
434
|
const idxCfg = ModelRegistry.getIndex(cls, idx);
|
|
425
435
|
|
|
@@ -445,7 +455,8 @@ export class MongoModelService implements
|
|
|
445
455
|
const { filter } = MongoUtil.prepareQuery(cls, query);
|
|
446
456
|
let cursor = col.find<T>(filter, {});
|
|
447
457
|
if (query.select) {
|
|
448
|
-
const
|
|
458
|
+
const selectKey = Object.keys(query.select)[0];
|
|
459
|
+
const select = typeof selectKey === 'string' && selectKey.startsWith('$') ? query.select : MongoUtil.extractSimple(query.select);
|
|
449
460
|
// Remove id if not explicitly defined, and selecting fields directly
|
|
450
461
|
if (!select['_id']) {
|
|
451
462
|
const values = new Set([...Object.values(select)]);
|
|
@@ -489,11 +500,11 @@ export class MongoModelService implements
|
|
|
489
500
|
return res.deletedCount ?? 0;
|
|
490
501
|
}
|
|
491
502
|
|
|
492
|
-
async updateByQuery<T extends ModelType>(cls: Class<T>, query: ModelQuery<T>, data: Partial<T>) {
|
|
503
|
+
async updateByQuery<T extends ModelType>(cls: Class<T>, query: ModelQuery<T>, data: Partial<T>): Promise<number> {
|
|
493
504
|
const col = await this.getStore(cls);
|
|
494
505
|
|
|
495
506
|
const items = MongoUtil.extractSimple(data);
|
|
496
|
-
const final = Object.entries(items).reduce((acc, [k, v]) => {
|
|
507
|
+
const final = Object.entries(items).reduce<Record<string, unknown>>((acc, [k, v]) => {
|
|
497
508
|
if (v === null || v === undefined) {
|
|
498
509
|
const o = (acc.$unset = acc.$unset ?? {}) as Record<string, unknown>;
|
|
499
510
|
o[k] = v;
|
|
@@ -502,7 +513,7 @@ export class MongoModelService implements
|
|
|
502
513
|
o[k] = v;
|
|
503
514
|
}
|
|
504
515
|
return acc;
|
|
505
|
-
}, {}
|
|
516
|
+
}, {});
|
|
506
517
|
|
|
507
518
|
const { filter } = MongoUtil.prepareQuery(cls, query);
|
|
508
519
|
const res = await col.updateMany(filter, final);
|
|
@@ -514,7 +525,7 @@ export class MongoModelService implements
|
|
|
514
525
|
const col = await this.getStore(cls);
|
|
515
526
|
const pipeline: object[] = [{
|
|
516
527
|
$group: {
|
|
517
|
-
_id: `$${field}`,
|
|
528
|
+
_id: `$${field as string}`,
|
|
518
529
|
count: {
|
|
519
530
|
$sum: 1
|
|
520
531
|
}
|
|
@@ -541,7 +552,7 @@ export class MongoModelService implements
|
|
|
541
552
|
async suggestValues<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Promise<string[]> {
|
|
542
553
|
const q = ModelQuerySuggestUtil.getSuggestFieldQuery(cls, field, prefix, query);
|
|
543
554
|
const results = await this.query(cls, q);
|
|
544
|
-
return ModelQuerySuggestUtil.combineSuggestResults(cls, field
|
|
555
|
+
return ModelQuerySuggestUtil.combineSuggestResults(cls, field, prefix, results, (a) => a, query && query.limit);
|
|
545
556
|
}
|
|
546
557
|
|
|
547
558
|
async suggest<T extends ModelType>(cls: Class<T>, field: ValidStringFields<T>, prefix?: string, query?: PageableModelQuery<T>): Promise<T[]> {
|