@travetto/model-mongo 7.0.0-rc.2 → 7.0.0-rc.4
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 +2 -2
- package/package.json +5 -5
- package/src/config.ts +2 -2
- package/src/internal/util.ts +41 -11
- package/src/service.ts +28 -17
package/README.md
CHANGED
|
@@ -84,9 +84,9 @@ export class MongoModelConfig {
|
|
|
84
84
|
options: mongo.MongoClientOptions = {};
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
|
-
*
|
|
87
|
+
* Allow storage modification at runtime
|
|
88
88
|
*/
|
|
89
|
-
|
|
89
|
+
modifyStorage?: boolean;
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
92
|
* Frequency of culling for cullable content
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-mongo",
|
|
3
|
-
"version": "7.0.0-rc.
|
|
3
|
+
"version": "7.0.0-rc.4",
|
|
4
4
|
"description": "Mongo backing for the travetto model module.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mongo",
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
"directory": "module/model-mongo"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@travetto/cli": "^7.0.0-rc.
|
|
29
|
-
"@travetto/config": "^7.0.0-rc.
|
|
30
|
-
"@travetto/model": "^7.0.0-rc.
|
|
31
|
-
"@travetto/model-query": "^7.0.0-rc.
|
|
28
|
+
"@travetto/cli": "^7.0.0-rc.4",
|
|
29
|
+
"@travetto/config": "^7.0.0-rc.4",
|
|
30
|
+
"@travetto/model": "^7.0.0-rc.4",
|
|
31
|
+
"@travetto/model-query": "^7.0.0-rc.4",
|
|
32
32
|
"mongodb": "^7.0.0"
|
|
33
33
|
},
|
|
34
34
|
"travetto": {
|
package/src/config.ts
CHANGED
|
@@ -43,9 +43,9 @@ export class MongoModelConfig {
|
|
|
43
43
|
options: mongo.MongoClientOptions = {};
|
|
44
44
|
|
|
45
45
|
/**
|
|
46
|
-
*
|
|
46
|
+
* Allow storage modification at runtime
|
|
47
47
|
*/
|
|
48
|
-
|
|
48
|
+
modifyStorage?: boolean;
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* Frequency of culling for cullable content
|
package/src/internal/util.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
|
-
Binary, type CreateIndexesOptions, type Filter, type FindCursor, type IndexDirection, ObjectId, type WithId as MongoWithId
|
|
2
|
+
Binary, type CreateIndexesOptions, type Filter, type FindCursor, type IndexDirection, ObjectId, type WithId as MongoWithId,
|
|
3
|
+
type IndexDescriptionInfo
|
|
3
4
|
} from 'mongodb';
|
|
4
5
|
|
|
5
6
|
import { AppError, castTo, Class, toConcrete, TypedObject } from '@travetto/runtime';
|
|
@@ -31,6 +32,10 @@ export type PlainIdx = Record<string, -1 | 0 | 1>;
|
|
|
31
32
|
*/
|
|
32
33
|
export class MongoUtil {
|
|
33
34
|
|
|
35
|
+
static namespaceIndex(cls: Class, name: string): string {
|
|
36
|
+
return `${cls.Ⲑid}__${name}`.replace(/[^a-zA-Z0-9_]+/g, '_');
|
|
37
|
+
}
|
|
38
|
+
|
|
34
39
|
static toIndex<T extends ModelType>(field: IndexField<T>): PlainIdx {
|
|
35
40
|
const keys = [];
|
|
36
41
|
while (typeof field !== 'number' && typeof field !== 'boolean' && Object.keys(field)) {
|
|
@@ -149,24 +154,21 @@ export class MongoUtil {
|
|
|
149
154
|
return out;
|
|
150
155
|
}
|
|
151
156
|
|
|
152
|
-
static getExtraIndices<T extends ModelType>(cls: Class<T>): BasicIdx[] {
|
|
153
|
-
const out: BasicIdx[] = [];
|
|
157
|
+
static getExtraIndices<T extends ModelType>(cls: Class<T>): [BasicIdx, IdxConfig][] {
|
|
158
|
+
const out: [BasicIdx, IdxConfig][] = [];
|
|
154
159
|
const textFields: string[] = [];
|
|
155
160
|
SchemaRegistryIndex.visitFields(cls, (field, path) => {
|
|
156
161
|
if (field.type === PointConcrete) {
|
|
157
162
|
const name = [...path, field].map(schema => schema.name).join('.');
|
|
158
|
-
out.push({ [name]: '2d' });
|
|
163
|
+
out.push([{ [name]: '2d' }, { name: this.namespaceIndex(cls, name) }]);
|
|
159
164
|
} else if (field.specifiers?.includes('text') && (field.specifiers?.includes('long') || field.specifiers.includes('search'))) {
|
|
160
165
|
const name = [...path, field].map(schema => schema.name).join('.');
|
|
161
166
|
textFields.push(name);
|
|
162
167
|
}
|
|
163
168
|
});
|
|
164
169
|
if (textFields.length) {
|
|
165
|
-
const text: BasicIdx =
|
|
166
|
-
|
|
167
|
-
text[field] = 'text';
|
|
168
|
-
}
|
|
169
|
-
out.push(text);
|
|
170
|
+
const text: BasicIdx = Object.fromEntries(textFields.map(field => [field, 'text']));
|
|
171
|
+
out.push([text, { name: this.namespaceIndex(cls, 'text_search') }]);
|
|
170
172
|
}
|
|
171
173
|
return out;
|
|
172
174
|
}
|
|
@@ -181,8 +183,8 @@ export class MongoUtil {
|
|
|
181
183
|
|
|
182
184
|
static getIndices<T extends ModelType>(cls: Class<T>, indices: IndexConfig<ModelType>[] = []): [BasicIdx, IdxConfig][] {
|
|
183
185
|
return [
|
|
184
|
-
...indices.map(idx => [this.getPlainIndex(idx), (idx.type === 'unique' ? { unique: true } : {})] as const),
|
|
185
|
-
...this.getExtraIndices(cls)
|
|
186
|
+
...indices.map(idx => [this.getPlainIndex(idx), { ...(idx.type === 'unique' ? { unique: true } : {}), name: this.namespaceIndex(cls, idx.name) }] as const),
|
|
187
|
+
...this.getExtraIndices(cls)
|
|
186
188
|
].map(idx => [...idx]);
|
|
187
189
|
}
|
|
188
190
|
|
|
@@ -212,4 +214,32 @@ export class MongoUtil {
|
|
|
212
214
|
|
|
213
215
|
return castTo(cursor);
|
|
214
216
|
}
|
|
217
|
+
|
|
218
|
+
static isIndexChanged(existing: IndexDescriptionInfo, [pendingKey, pendingOptions]: [BasicIdx, CreateIndexesOptions]): boolean {
|
|
219
|
+
let changed = false;
|
|
220
|
+
// Config changed
|
|
221
|
+
changed ||=
|
|
222
|
+
!!existing.unique !== !!pendingOptions.unique ||
|
|
223
|
+
!!existing.sparse !== !!pendingOptions.sparse ||
|
|
224
|
+
existing.expireAfterSeconds !== pendingOptions.expireAfterSeconds ||
|
|
225
|
+
existing.bucketSize !== pendingOptions.bucketSize;
|
|
226
|
+
|
|
227
|
+
const existingFields = existing.textIndexVersion ?
|
|
228
|
+
Object.fromEntries(Object.entries(existing.weights ?? {}).map(([key]) => [key, 'text'])) :
|
|
229
|
+
existing.key;
|
|
230
|
+
|
|
231
|
+
const pendingKeySet = new Set(Object.keys(pendingKey));
|
|
232
|
+
const existingKeySet = new Set(Object.keys(existingFields));
|
|
233
|
+
|
|
234
|
+
changed ||= pendingKeySet.size !== existingKeySet.size;
|
|
235
|
+
|
|
236
|
+
const overlap = [...pendingKeySet.intersection(existingKeySet)];
|
|
237
|
+
changed ||= overlap.length !== pendingKeySet.size;
|
|
238
|
+
|
|
239
|
+
for (let i = 0; i < overlap.length && !changed; i++) {
|
|
240
|
+
changed ||= existingFields[overlap[i]] !== pendingKey[overlap[i]];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return changed;
|
|
244
|
+
}
|
|
215
245
|
}
|
package/src/service.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { pipeline } from 'node:stream/promises';
|
|
|
3
3
|
import {
|
|
4
4
|
type Db, GridFSBucket, MongoClient, type GridFSFile, type Collection,
|
|
5
5
|
type ObjectId, type Binary, type RootFilterOperators, type Filter,
|
|
6
|
-
type WithId as MongoWithId
|
|
6
|
+
type WithId as MongoWithId,
|
|
7
7
|
} from 'mongodb';
|
|
8
8
|
|
|
9
9
|
import {
|
|
@@ -106,7 +106,7 @@ export class MongoModelService implements
|
|
|
106
106
|
bucketName: ModelBlobNamespace,
|
|
107
107
|
writeConcern: { w: 1 }
|
|
108
108
|
});
|
|
109
|
-
await ModelStorageUtil.
|
|
109
|
+
await ModelStorageUtil.storageInitialization(this);
|
|
110
110
|
ShutdownManager.onGracefulShutdown(() => this.client.close());
|
|
111
111
|
ModelExpiryUtil.registerCull(this);
|
|
112
112
|
}
|
|
@@ -126,23 +126,34 @@ export class MongoModelService implements
|
|
|
126
126
|
await this.#db.dropDatabase();
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
async
|
|
129
|
+
async upsertModel(cls: Class): Promise<void> {
|
|
130
130
|
const col = await this.getStore(cls);
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
console.debug('Creating indexes', { indices: creating });
|
|
134
|
-
for (const toCreate of creating) {
|
|
135
|
-
await col.createIndex(...toCreate);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
131
|
+
const indices = MongoUtil.getIndices(cls, ModelRegistryIndex.getConfig(cls).indices);
|
|
132
|
+
const existingIndices = (await col.indexes().catch(() => [])).filter(idx => idx.name !== '_id_');
|
|
139
133
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
}
|
|
134
|
+
const pendingMap = Object.fromEntries(indices.map(pair => [pair[1].name!, pair]));
|
|
135
|
+
const existingMap = Object.fromEntries(existingIndices.map(idx => [idx.name!, idx.key]));
|
|
143
136
|
|
|
144
|
-
|
|
145
|
-
|
|
137
|
+
for (const idx of existingIndices) {
|
|
138
|
+
if (!idx.name) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const pending = pendingMap[idx.name];
|
|
142
|
+
if (!pending) {
|
|
143
|
+
console.debug('Deleting index', { indices: idx.name });
|
|
144
|
+
await col.dropIndex(idx.name);
|
|
145
|
+
} else if (MongoUtil.isIndexChanged(idx, pending)) {
|
|
146
|
+
console.debug('Updating index', { indices: idx.name });
|
|
147
|
+
await col.dropIndex(idx.name);
|
|
148
|
+
await col.createIndex(...pending);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
for (const [name, idx] of Object.entries(pendingMap)) {
|
|
152
|
+
if (!existingMap[name]) {
|
|
153
|
+
console.debug('Creating index', { indices: name });
|
|
154
|
+
await col.createIndex(...idx);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
146
157
|
}
|
|
147
158
|
|
|
148
159
|
async truncateModel<T extends ModelType>(cls: Class<T>): Promise<void> {
|
|
@@ -158,7 +169,7 @@ export class MongoModelService implements
|
|
|
158
169
|
* Get mongo collection
|
|
159
170
|
*/
|
|
160
171
|
async getStore<T extends ModelType>(cls: Class<T>): Promise<Collection<T>> {
|
|
161
|
-
return this.#db.collection(ModelRegistryIndex.getStoreName(cls)
|
|
172
|
+
return this.#db.collection(ModelRegistryIndex.getStoreName(cls));
|
|
162
173
|
}
|
|
163
174
|
|
|
164
175
|
// Crud
|