@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 CHANGED
@@ -84,9 +84,9 @@ export class MongoModelConfig {
84
84
  options: mongo.MongoClientOptions = {};
85
85
 
86
86
  /**
87
- * Should we auto create the db
87
+ * Allow storage modification at runtime
88
88
  */
89
- autoCreate?: boolean;
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.2",
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.2",
29
- "@travetto/config": "^7.0.0-rc.2",
30
- "@travetto/model": "^7.0.0-rc.2",
31
- "@travetto/model-query": "^7.0.0-rc.2",
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
- * Should we auto create the db
46
+ * Allow storage modification at runtime
47
47
  */
48
- autoCreate?: boolean;
48
+ modifyStorage?: boolean;
49
49
 
50
50
  /**
51
51
  * Frequency of culling for cullable content
@@ -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
- for (const field of textFields) {
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).map((idx) => [idx, {}] as const)
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.registerModelChangeListener(this);
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 establishIndices<T extends ModelType>(cls: Class<T>): Promise<void> {
129
+ async upsertModel(cls: Class): Promise<void> {
130
130
  const col = await this.getStore(cls);
131
- const creating = MongoUtil.getIndices(cls, ModelRegistryIndex.getConfig(cls).indices);
132
- if (creating.length) {
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
- async createModel(cls: Class): Promise<void> {
141
- await this.establishIndices(cls);
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
- async changeModel(cls: Class): Promise<void> {
145
- await this.establishIndices(cls);
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).toLowerCase().replace(/[^A-Za-z0-9_]+/g, '_'));
172
+ return this.#db.collection(ModelRegistryIndex.getStoreName(cls));
162
173
  }
163
174
 
164
175
  // Crud