monsqlize 2.0.3 → 2.0.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/CHANGELOG.md +8 -5
- package/README.md +33 -3
- package/changelogs/v2.0.4.md +61 -0
- package/dist/cjs/index.cjs +263 -21
- package/dist/esm/index.mjs +263 -21
- package/dist/types/model.d.ts +104 -0
- package/dist/types/monsqlize.d.ts +9 -1
- package/dist/types/runtime.d.ts +2 -1
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
-
> Summary index — current release details are packaged in [changelogs/v2.0.
|
|
4
|
-
> **Last updated**: 2026-06-
|
|
3
|
+
> Summary index — current release details are packaged in [changelogs/v2.0.4.md](./changelogs/v2.0.4.md); historical details live in the repository changelog archive.
|
|
4
|
+
> **Last updated**: 2026-06-12
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
| Version | Date | Summary | Details |
|
|
11
11
|
|---------|------|---------|---------|
|
|
12
|
+
| [v2.0.4](./changelogs/v2.0.4.md) | 2026-06-12 | Patch: production-safe Model index rollout controls, `schema-dsl@2.0.9`, capability-index wording cleanup, and documentation home refinements | [View](./changelogs/v2.0.4.md) |
|
|
12
13
|
| [v2.0.3](./changelogs/v2.0.3.md) | 2026-06-11 | Patch: v1 compatibility fixes, public stats APIs, standalone docs-site link safety, bilingual docs consistency, and release preflight alignment | [View](./changelogs/v2.0.3.md) |
|
|
13
14
|
| [v2.0.2](./changelogs/v2.0.2.md) | 2026-06-09 | Patch: direct runtime, optional and development dependencies pinned to exact versions for deterministic consumer installs | [View](./changelogs/v2.0.2.md) |
|
|
14
15
|
| [v2.0.1](./changelogs/v2.0.1.md) | 2026-06-03 | Patch: model collection/pool compatibility, automatic-index task dedupe, runtime cache/pool v1 smooth-upgrade fixes, and current docs/types alignment | [View](./changelogs/v2.0.1.md) |
|
|
@@ -446,7 +447,8 @@ const result = await msq.collection('orders').insertOne(dataFromMongoose);
|
|
|
446
447
|
changelogs/
|
|
447
448
|
├── README.md # 变更文档说明
|
|
448
449
|
├── TEMPLATE.md # 变更文档模板
|
|
449
|
-
├── v2.0.
|
|
450
|
+
├── v2.0.4.md # 当前发布详细变更
|
|
451
|
+
├── v2.0.3.md # v2.0.3 详细变更
|
|
450
452
|
├── v2.0.2.md # v2.0.2 详细变更
|
|
451
453
|
├── v2.0.1.md # v2.0.1 详细变更
|
|
452
454
|
├── v2.0.0.md # v2 TypeScript 重写发布详细变更
|
|
@@ -500,7 +502,8 @@ changelogs/
|
|
|
500
502
|
|
|
501
503
|
## 相关文档
|
|
502
504
|
|
|
503
|
-
- [changelogs/v2.0.
|
|
505
|
+
- [changelogs/v2.0.4.md](./changelogs/v2.0.4.md) - 当前发布详细变更文档
|
|
506
|
+
- [changelogs/v2.0.3.md](./changelogs/v2.0.3.md) - v2.0.3 详细变更文档
|
|
504
507
|
- [changelogs/v2.0.2.md](./changelogs/v2.0.2.md) - v2.0.2 详细变更文档
|
|
505
508
|
- [changelogs/v2.0.1.md](./changelogs/v2.0.1.md) - v2.0.1 详细变更文档
|
|
506
509
|
- [README.md](./README.md) - 项目说明
|
|
@@ -508,5 +511,5 @@ changelogs/
|
|
|
508
511
|
|
|
509
512
|
---
|
|
510
513
|
|
|
511
|
-
**最后更新**: 2026-06-
|
|
514
|
+
**最后更新**: 2026-06-12
|
|
512
515
|
|
package/README.md
CHANGED
|
@@ -7,6 +7,8 @@ TypeScript-native MongoDB ODM and enhancement layer with v1-compatible APIs, mul
|
|
|
7
7
|
[](https://www.mongodb.com/)
|
|
8
8
|
[](https://nodejs.org/)
|
|
9
9
|
|
|
10
|
+
Documentation: [English](https://vextjs.github.io/monSQLize/) · [简体中文](https://vextjs.github.io/monSQLize/zh/)
|
|
11
|
+
|
|
10
12
|
```bash
|
|
11
13
|
npm install monsqlize
|
|
12
14
|
```
|
|
@@ -37,7 +39,7 @@ monSQLize keeps the MongoDB driver mental model while adding the production feat
|
|
|
37
39
|
|
|
38
40
|
- Drop-in collection helpers that preserve MongoDB-style CRUD, aggregation, indexes, transactions, and Change Streams.
|
|
39
41
|
- Smart caching through `cache-hub`, including local memory caching, optional Redis-backed L2 caching, automatic invalidation, and function-level caching.
|
|
40
|
-
- A lightweight Model layer with `schema-dsl` validation, hooks, relations, populate, custom methods, timestamps, soft delete, and
|
|
42
|
+
- A lightweight Model layer with `schema-dsl` validation, hooks, relations, populate, custom methods, timestamps, soft delete, optimistic locking, and production-safe index preflight.
|
|
41
43
|
- Multi-connection-pool support, pool health checks, pool-scoped collections/models, and fallback strategies.
|
|
42
44
|
- Business locks and distributed locks for multi-instance deployments.
|
|
43
45
|
- Saga orchestration for multi-step business workflows.
|
|
@@ -251,6 +253,27 @@ module.exports = {
|
|
|
251
253
|
|
|
252
254
|
Relative model paths are resolved from `process.cwd()`. In production services, prefer absolute paths such as `path.join(__dirname, 'models')`.
|
|
253
255
|
|
|
256
|
+
### Production Model Index Rollout
|
|
257
|
+
|
|
258
|
+
Model-declared indexes are still created automatically by default for backward compatibility. Production services can turn off automatic indexing and run an explicit preflight before creating missing indexes:
|
|
259
|
+
|
|
260
|
+
```js
|
|
261
|
+
const db = new MonSQLize({
|
|
262
|
+
type: 'mongodb',
|
|
263
|
+
databaseName: 'mydb',
|
|
264
|
+
config: { uri: 'mongodb://localhost:27017' },
|
|
265
|
+
autoIndex: false
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const plan = await db.ensureModelIndexes({ models: ['users'], dryRun: true });
|
|
269
|
+
|
|
270
|
+
if (plan.totals.conflicts === 0) {
|
|
271
|
+
await db.ensureModelIndexes({ models: ['users'], throwOnError: true });
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
`ensureModelIndexes()` creates only missing indexes. It does not drop, rename, or rebuild conflicting indexes.
|
|
276
|
+
|
|
254
277
|
### Populate
|
|
255
278
|
|
|
256
279
|
```js
|
|
@@ -443,6 +466,7 @@ See the current support and verification documents:
|
|
|
443
466
|
|
|
444
467
|
Current TypeScript documentation and examples are the source of truth for the v2 package:
|
|
445
468
|
|
|
469
|
+
- Complete docs: [English](https://vextjs.github.io/monSQLize/) · [简体中文](https://vextjs.github.io/monSQLize/zh/)
|
|
446
470
|
- `docs/en/**` - default English documentation.
|
|
447
471
|
- `docs/zh/**` - Simplified Chinese documentation.
|
|
448
472
|
- `docs/en/recipes.md` / `docs/zh/recipes.md` - shortest copy-ready paths for common setup scenarios.
|
|
@@ -494,7 +518,7 @@ npm run test:real-env:private
|
|
|
494
518
|
|
|
495
519
|
## Release Status
|
|
496
520
|
|
|
497
|
-
The current published release is `v2.0.
|
|
521
|
+
The current published release is `v2.0.4`.
|
|
498
522
|
|
|
499
523
|
Key release-readiness points:
|
|
500
524
|
|
|
@@ -502,11 +526,17 @@ Key release-readiness points:
|
|
|
502
526
|
- Package exports are consolidated under `dist/cjs`, `dist/esm`, and `dist/types`.
|
|
503
527
|
- npm packages include the runtime bundles and declaration files only; source maps are disabled by default and can be generated locally with `MONSQLIZE_BUILD_SOURCEMAPS=1 npm run build`.
|
|
504
528
|
- v1 smooth-upgrade compatibility has been validated against the target workspace consumers.
|
|
505
|
-
- `schema-dsl` follows the npm `latest` TypeScript line `schema-dsl@2.0.
|
|
529
|
+
- `schema-dsl` follows the npm `latest` TypeScript line `schema-dsl@2.0.9`; deprecated `2.3.x` mistake releases are intentionally excluded.
|
|
506
530
|
- GitHub Actions publishes to npm from `v*` tags after running `npm run release:preflight`; the publish step skips duplicate lifecycle scripts because the gate already ran in the same job.
|
|
507
531
|
|
|
508
532
|
## Roadmap
|
|
509
533
|
|
|
534
|
+
### v2.0.4
|
|
535
|
+
|
|
536
|
+
- Production-safe Model index rollout controls with `autoIndex`, dry-run preflight, conflict reporting, and explicit `ensureIndexes()` / `ensureModelIndexes()` APIs.
|
|
537
|
+
- `schema-dsl` updated to `2.0.9`, with transitive `cache-hub` aligned to `2.2.4`.
|
|
538
|
+
- User-facing capability and verification documentation wording cleaned up, plus documentation home experience refinements.
|
|
539
|
+
|
|
510
540
|
### v2.0.3
|
|
511
541
|
|
|
512
542
|
- v1 compatibility patch for documented `findPage({ cache })` behavior.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# v2.0.4 — 2026-06-12
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
v2.0.4 is a production-safety and documentation-quality patch. It keeps the public API backward compatible while adding explicit Model index rollout controls, updating the schema validation dependency, and refining user-facing documentation language.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Runtime Fixes
|
|
10
|
+
|
|
11
|
+
- Added production-safe Model index controls:
|
|
12
|
+
- runtime-level and model-level `autoIndex` options.
|
|
13
|
+
- explicit `ModelInstance.ensureIndexes()` and `MonSQLize.ensureModelIndexes()` APIs.
|
|
14
|
+
- dry-run index preflight with `existing`, `missing`, `conflicts`, and `failed` classification.
|
|
15
|
+
- missing-index creation without dropping, renaming, or rebuilding conflicting indexes.
|
|
16
|
+
- `model-index-error` events for observable automatic-index creation failures.
|
|
17
|
+
- Kept automatic Model indexing enabled by default for backward compatibility.
|
|
18
|
+
- Moved runtime Model index aggregation into the Model runtime helper layer so the strict source-size gate remains clean.
|
|
19
|
+
|
|
20
|
+
## Dependencies
|
|
21
|
+
|
|
22
|
+
- Updated `schema-dsl` from `2.0.8` to npm `latest` `2.0.9`.
|
|
23
|
+
- Confirmed `schema-dsl@2.0.9` keeps the required `dsl()` and `validate()` runtime paths compatible with monSQLize.
|
|
24
|
+
- Confirmed the `schema-dsl` transitive `cache-hub` dependency now resolves to `2.2.4`, matching the root direct dependency.
|
|
25
|
+
|
|
26
|
+
## Documentation
|
|
27
|
+
|
|
28
|
+
- Documented the recommended production index rollout flow:
|
|
29
|
+
- set `autoIndex: false` in production services.
|
|
30
|
+
- run `ensureIndexes({ dryRun: true })` / `ensureModelIndexes({ dryRun: true })`.
|
|
31
|
+
- create only missing indexes during a low-traffic maintenance window.
|
|
32
|
+
- monitor TTL and index-build behavior.
|
|
33
|
+
- Corrected create-index documentation to distinguish local `INVALID_ARGUMENT` validation from raw MongoDB driver/server index conflicts.
|
|
34
|
+
- Reworded capability-index and verification-entry documentation to use user-facing availability and verification language instead of internal migration status terms.
|
|
35
|
+
- Refined the documentation home experience with the VextJS-style footer, aligned hero/feature layout, and updated hero data-flow visual.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Compatibility
|
|
40
|
+
|
|
41
|
+
- SemVer: patch release.
|
|
42
|
+
- Breaking changes: none.
|
|
43
|
+
- Existing v2 imports remain under `dist/cjs`, `dist/esm`, and `dist/types`.
|
|
44
|
+
- Existing default automatic Model indexing behavior remains enabled unless users opt into the new production-safe controls.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Validation
|
|
49
|
+
|
|
50
|
+
- `npm run lint`
|
|
51
|
+
- `npm run check:docs-examples`
|
|
52
|
+
- `npm run type-check`
|
|
53
|
+
- targeted Model/index unit and integration tests
|
|
54
|
+
- `npm run test:examples`
|
|
55
|
+
- `npm --prefix website run build`
|
|
56
|
+
- `npm run verify:fast`
|
|
57
|
+
- `npm run test:server-matrix`
|
|
58
|
+
- `npm run test:audit`
|
|
59
|
+
- `git diff --check`
|
|
60
|
+
|
|
61
|
+
Full release validation is handled by `npm run release:preflight`, `npm pack --dry-run`, `npm publish --dry-run`, and pack install smoke before publishing.
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -997,6 +997,75 @@ function stableIndexStringify(value) {
|
|
|
997
997
|
}
|
|
998
998
|
return JSON.stringify(value) ?? "undefined";
|
|
999
999
|
}
|
|
1000
|
+
function getIndexOptionName(options) {
|
|
1001
|
+
return typeof options.name === "string" && options.name.length > 0 ? options.name : void 0;
|
|
1002
|
+
}
|
|
1003
|
+
function summarizeIndexError(error) {
|
|
1004
|
+
if (error instanceof Error) {
|
|
1005
|
+
const record = error;
|
|
1006
|
+
return {
|
|
1007
|
+
name: error.name,
|
|
1008
|
+
message: error.message,
|
|
1009
|
+
code: record.code ?? record.codeName
|
|
1010
|
+
};
|
|
1011
|
+
}
|
|
1012
|
+
return {
|
|
1013
|
+
message: String(error)
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
function isRecord(value) {
|
|
1017
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1018
|
+
}
|
|
1019
|
+
function getExistingIndexKey(index) {
|
|
1020
|
+
return index.key;
|
|
1021
|
+
}
|
|
1022
|
+
function declaredOptionEntries(options) {
|
|
1023
|
+
return Object.entries(options).filter(([name, value]) => {
|
|
1024
|
+
if (value === void 0) return false;
|
|
1025
|
+
if (name === "background") return false;
|
|
1026
|
+
return true;
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
function indexOptionsMatch(existing, declared) {
|
|
1030
|
+
if (stableIndexStringify(getExistingIndexKey(existing)) !== stableIndexStringify(declared.key)) {
|
|
1031
|
+
return false;
|
|
1032
|
+
}
|
|
1033
|
+
for (const [name, value] of declaredOptionEntries(declared.options)) {
|
|
1034
|
+
const existingValue = existing[name];
|
|
1035
|
+
if (value === false && existingValue === void 0) {
|
|
1036
|
+
continue;
|
|
1037
|
+
}
|
|
1038
|
+
if (stableIndexStringify(existingValue) !== stableIndexStringify(value)) {
|
|
1039
|
+
return false;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return true;
|
|
1043
|
+
}
|
|
1044
|
+
function findExistingIndexByName(existingIndexes, name) {
|
|
1045
|
+
if (!name) return void 0;
|
|
1046
|
+
return existingIndexes.find((index) => index.name === name);
|
|
1047
|
+
}
|
|
1048
|
+
function findExistingIndexByKey(existingIndexes, key) {
|
|
1049
|
+
const fingerprint = stableIndexStringify(key);
|
|
1050
|
+
return existingIndexes.find((index) => stableIndexStringify(getExistingIndexKey(index)) === fingerprint);
|
|
1051
|
+
}
|
|
1052
|
+
function createIndexEnsureError(message, result, cause) {
|
|
1053
|
+
return createError(ErrorCodes.MONGODB_ERROR, message, [result], cause);
|
|
1054
|
+
}
|
|
1055
|
+
function resolveModelAutoIndexOptions(definition, runtimeAutoIndex) {
|
|
1056
|
+
const modelAutoIndex = toCompatDefinition(definition).options?.autoIndex;
|
|
1057
|
+
const value = modelAutoIndex ?? runtimeAutoIndex;
|
|
1058
|
+
if (value === false) {
|
|
1059
|
+
return { enabled: false, emitEvents: true };
|
|
1060
|
+
}
|
|
1061
|
+
if (value && typeof value === "object") {
|
|
1062
|
+
return {
|
|
1063
|
+
enabled: value.enabled !== false,
|
|
1064
|
+
emitEvents: value.emitEvents !== false
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
return { enabled: true, emitEvents: true };
|
|
1068
|
+
}
|
|
1000
1069
|
function getIndexTaskRegistry(runtime) {
|
|
1001
1070
|
if (!runtime) {
|
|
1002
1071
|
return fallbackModelIndexTasks;
|
|
@@ -1024,6 +1093,20 @@ function resolveIndexTaskScope(collection, options) {
|
|
|
1024
1093
|
};
|
|
1025
1094
|
}
|
|
1026
1095
|
}
|
|
1096
|
+
function toIndexNamespace(scope) {
|
|
1097
|
+
return {
|
|
1098
|
+
db: scope.dbName,
|
|
1099
|
+
collection: scope.collectionName,
|
|
1100
|
+
poolName: scope.poolName
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
function emitIndexFailure(runtime, payload, emitEvents) {
|
|
1104
|
+
if (!emitEvents) {
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
const emitter = runtime;
|
|
1108
|
+
emitter?.emit?.("model-index-error", payload);
|
|
1109
|
+
}
|
|
1027
1110
|
function warnIndexFailure(runtime, taskKey, error) {
|
|
1028
1111
|
const logger = runtime;
|
|
1029
1112
|
logger?.logger?.warn?.("[MonSQLize] model index creation failed", {
|
|
@@ -1031,9 +1114,10 @@ function warnIndexFailure(runtime, taskKey, error) {
|
|
|
1031
1114
|
error: error instanceof Error ? error.message : String(error)
|
|
1032
1115
|
});
|
|
1033
1116
|
}
|
|
1034
|
-
function scheduleIndexTask(collection,
|
|
1117
|
+
function scheduleIndexTask(collection, declaredIndex, emitEvents, options) {
|
|
1035
1118
|
const scope = resolveIndexTaskScope(collection, options);
|
|
1036
|
-
const
|
|
1119
|
+
const { key, options: indexOptions } = declaredIndex;
|
|
1120
|
+
const indexFingerprint = declaredIndex.fingerprint;
|
|
1037
1121
|
const taskKey = `${scope.poolName}:${scope.dbName}:${scope.collectionName}:${indexFingerprint}`;
|
|
1038
1122
|
const registry = getIndexTaskRegistry(options?.runtime);
|
|
1039
1123
|
const existing = registry.get(taskKey);
|
|
@@ -1051,6 +1135,14 @@ function scheduleIndexTask(collection, key, indexOptions, options) {
|
|
|
1051
1135
|
task.status = "failed";
|
|
1052
1136
|
task.error = error;
|
|
1053
1137
|
warnIndexFailure(options?.runtime, taskKey, error);
|
|
1138
|
+
emitIndexFailure(options?.runtime, {
|
|
1139
|
+
namespace: scope,
|
|
1140
|
+
taskKey,
|
|
1141
|
+
source: declaredIndex.source,
|
|
1142
|
+
key,
|
|
1143
|
+
options: indexOptions,
|
|
1144
|
+
error: summarizeIndexError(error)
|
|
1145
|
+
}, emitEvents);
|
|
1054
1146
|
resolve();
|
|
1055
1147
|
});
|
|
1056
1148
|
});
|
|
@@ -1167,6 +1259,134 @@ function resolveModelHooksFactory(definition) {
|
|
|
1167
1259
|
const hooks = toCompatDefinition(definition).hooks;
|
|
1168
1260
|
return typeof hooks === "function" ? hooks : null;
|
|
1169
1261
|
}
|
|
1262
|
+
function collectModelIndexDefinitions(definition, softDeleteConfig) {
|
|
1263
|
+
const declared = [];
|
|
1264
|
+
if (softDeleteConfig?.enabled && softDeleteConfig.type === "timestamp" && softDeleteConfig.ttl) {
|
|
1265
|
+
const key = { [softDeleteConfig.field]: 1 };
|
|
1266
|
+
const options = { expireAfterSeconds: softDeleteConfig.ttl };
|
|
1267
|
+
declared.push({
|
|
1268
|
+
source: "softDelete",
|
|
1269
|
+
key,
|
|
1270
|
+
options,
|
|
1271
|
+
name: getIndexOptionName(options),
|
|
1272
|
+
fingerprint: stableIndexStringify({ key, options })
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
const indexes = toCompatDefinition(definition).indexes;
|
|
1276
|
+
if (!Array.isArray(indexes) || indexes.length === 0) {
|
|
1277
|
+
return declared;
|
|
1278
|
+
}
|
|
1279
|
+
for (const indexSpec of indexes) {
|
|
1280
|
+
if (!isRecord(indexSpec) || !indexSpec.key) {
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
const { key, ...indexOptions } = indexSpec;
|
|
1284
|
+
declared.push({
|
|
1285
|
+
source: "definition",
|
|
1286
|
+
key,
|
|
1287
|
+
options: indexOptions,
|
|
1288
|
+
name: getIndexOptionName(indexOptions),
|
|
1289
|
+
fingerprint: stableIndexStringify({ key, options: indexOptions })
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
return declared;
|
|
1293
|
+
}
|
|
1294
|
+
async function ensureModelIndexesForCollection(collection, definition, softDeleteConfig, options = {}) {
|
|
1295
|
+
const namespace = toIndexNamespace(resolveIndexTaskScope(collection, options));
|
|
1296
|
+
const declared = collectModelIndexDefinitions(definition, softDeleteConfig);
|
|
1297
|
+
const existingIndexes = await collection.listIndexes();
|
|
1298
|
+
const existing = [];
|
|
1299
|
+
const missing = [];
|
|
1300
|
+
const conflicts = [];
|
|
1301
|
+
for (const declaredIndex of declared) {
|
|
1302
|
+
const existingByName = findExistingIndexByName(existingIndexes, declaredIndex.name);
|
|
1303
|
+
if (existingByName) {
|
|
1304
|
+
if (indexOptionsMatch(existingByName, declaredIndex)) {
|
|
1305
|
+
existing.push({ declared: declaredIndex, existing: existingByName });
|
|
1306
|
+
} else {
|
|
1307
|
+
conflicts.push({
|
|
1308
|
+
declared: declaredIndex,
|
|
1309
|
+
existing: existingByName,
|
|
1310
|
+
reason: "name-conflict"
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
const existingByKey = findExistingIndexByKey(existingIndexes, declaredIndex.key);
|
|
1316
|
+
if (existingByKey) {
|
|
1317
|
+
if (indexOptionsMatch(existingByKey, declaredIndex)) {
|
|
1318
|
+
existing.push({ declared: declaredIndex, existing: existingByKey });
|
|
1319
|
+
} else {
|
|
1320
|
+
conflicts.push({
|
|
1321
|
+
declared: declaredIndex,
|
|
1322
|
+
existing: existingByKey,
|
|
1323
|
+
reason: "options-conflict"
|
|
1324
|
+
});
|
|
1325
|
+
}
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
missing.push(declaredIndex);
|
|
1329
|
+
}
|
|
1330
|
+
const result = {
|
|
1331
|
+
dryRun: options.dryRun === true,
|
|
1332
|
+
namespace,
|
|
1333
|
+
declared,
|
|
1334
|
+
existing,
|
|
1335
|
+
missing,
|
|
1336
|
+
created: [],
|
|
1337
|
+
conflicts,
|
|
1338
|
+
failed: [],
|
|
1339
|
+
skipped: options.dryRun === true ? missing.map((declaredIndex) => ({ declared: declaredIndex, reason: "dry-run" })) : conflicts.map((conflict) => ({ declared: conflict.declared, reason: conflict.reason }))
|
|
1340
|
+
};
|
|
1341
|
+
if (conflicts.length > 0 && options.throwOnError) {
|
|
1342
|
+
throw createIndexEnsureError("Model index conflicts detected.", result);
|
|
1343
|
+
}
|
|
1344
|
+
if (options.dryRun === true) {
|
|
1345
|
+
return result;
|
|
1346
|
+
}
|
|
1347
|
+
for (const declaredIndex of missing) {
|
|
1348
|
+
try {
|
|
1349
|
+
const createdName = await collection.createIndex(declaredIndex.key, declaredIndex.options);
|
|
1350
|
+
result.created.push({
|
|
1351
|
+
declared: declaredIndex,
|
|
1352
|
+
name: typeof createdName === "string" ? createdName : void 0,
|
|
1353
|
+
result: createdName
|
|
1354
|
+
});
|
|
1355
|
+
} catch (error) {
|
|
1356
|
+
result.failed.push({
|
|
1357
|
+
declared: declaredIndex,
|
|
1358
|
+
error: summarizeIndexError(error)
|
|
1359
|
+
});
|
|
1360
|
+
if (options.throwOnError) {
|
|
1361
|
+
throw createIndexEnsureError(
|
|
1362
|
+
"Model index creation failed.",
|
|
1363
|
+
result,
|
|
1364
|
+
error instanceof Error ? error : void 0
|
|
1365
|
+
);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
return result;
|
|
1370
|
+
}
|
|
1371
|
+
function summarizeModelIndexEnsureResults(results) {
|
|
1372
|
+
return results.reduce((totals, result) => ({
|
|
1373
|
+
declared: totals.declared + result.declared.length,
|
|
1374
|
+
existing: totals.existing + result.existing.length,
|
|
1375
|
+
missing: totals.missing + result.missing.length,
|
|
1376
|
+
created: totals.created + result.created.length,
|
|
1377
|
+
conflicts: totals.conflicts + result.conflicts.length,
|
|
1378
|
+
failed: totals.failed + result.failed.length,
|
|
1379
|
+
skipped: totals.skipped + result.skipped.length
|
|
1380
|
+
}), {
|
|
1381
|
+
declared: 0,
|
|
1382
|
+
existing: 0,
|
|
1383
|
+
missing: 0,
|
|
1384
|
+
created: 0,
|
|
1385
|
+
conflicts: 0,
|
|
1386
|
+
failed: 0,
|
|
1387
|
+
skipped: 0
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1170
1390
|
function initializeModelV1Methods(target, definition) {
|
|
1171
1391
|
const methods = toCompatDefinition(definition).methods;
|
|
1172
1392
|
if (typeof methods !== "function") {
|
|
@@ -1191,25 +1411,12 @@ function initializeModelV1Methods(target, definition) {
|
|
|
1191
1411
|
}
|
|
1192
1412
|
}
|
|
1193
1413
|
function scheduleModelIndexes(collection, definition, softDeleteConfig, options) {
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
scheduleIndexTask(
|
|
1197
|
-
collection,
|
|
1198
|
-
{ [softDeleteIndex.field]: 1 },
|
|
1199
|
-
{ expireAfterSeconds: softDeleteIndex.ttl },
|
|
1200
|
-
options
|
|
1201
|
-
);
|
|
1202
|
-
}
|
|
1203
|
-
const indexes = toCompatDefinition(definition).indexes;
|
|
1204
|
-
if (!Array.isArray(indexes) || indexes.length === 0) {
|
|
1414
|
+
const autoIndex = resolveModelAutoIndexOptions(definition, options?.autoIndex);
|
|
1415
|
+
if (!autoIndex.enabled) {
|
|
1205
1416
|
return;
|
|
1206
1417
|
}
|
|
1207
|
-
for (const
|
|
1208
|
-
|
|
1209
|
-
continue;
|
|
1210
|
-
}
|
|
1211
|
-
const { key, ...indexOptions } = indexSpec;
|
|
1212
|
-
scheduleIndexTask(collection, key, indexOptions, options);
|
|
1418
|
+
for (const declaredIndex of collectModelIndexDefinitions(definition, softDeleteConfig)) {
|
|
1419
|
+
scheduleIndexTask(collection, declaredIndex, autoIndex.emitEvents, options);
|
|
1213
1420
|
}
|
|
1214
1421
|
}
|
|
1215
1422
|
|
|
@@ -1777,7 +1984,8 @@ var ModelInstance = class {
|
|
|
1777
1984
|
runtime: this.runtime,
|
|
1778
1985
|
dbName: options.dbName,
|
|
1779
1986
|
poolName: options.poolName,
|
|
1780
|
-
collectionName: options.collectionName
|
|
1987
|
+
collectionName: options.collectionName,
|
|
1988
|
+
autoIndex: this.runtime.options?.autoIndex
|
|
1781
1989
|
});
|
|
1782
1990
|
this._v1InstanceMethods = initializeModelV1Methods(this, options.definition);
|
|
1783
1991
|
}
|
|
@@ -1981,6 +2189,15 @@ var ModelInstance = class {
|
|
|
1981
2189
|
listIndexes() {
|
|
1982
2190
|
return this.collection.listIndexes();
|
|
1983
2191
|
}
|
|
2192
|
+
ensureIndexes(options = {}) {
|
|
2193
|
+
return ensureModelIndexesForCollection(this.collection, this.definition, this._softDeleteConfig, {
|
|
2194
|
+
...options,
|
|
2195
|
+
runtime: this.runtime,
|
|
2196
|
+
dbName: this.dbName,
|
|
2197
|
+
poolName: this.poolName,
|
|
2198
|
+
collectionName: this.collectionName
|
|
2199
|
+
});
|
|
2200
|
+
}
|
|
1984
2201
|
dropIndex(name) {
|
|
1985
2202
|
return this.collection.dropIndex(name);
|
|
1986
2203
|
}
|
|
@@ -8079,6 +8296,26 @@ function createRuntimeModelInstance(host, name, scope) {
|
|
|
8079
8296
|
});
|
|
8080
8297
|
return instance;
|
|
8081
8298
|
}
|
|
8299
|
+
async function ensureRuntimeModelIndexes(host, options = {}) {
|
|
8300
|
+
const modelNames = options.models ?? Model.list();
|
|
8301
|
+
const models = [];
|
|
8302
|
+
for (const name of modelNames) {
|
|
8303
|
+
const model = host.scopedModel(name, {
|
|
8304
|
+
database: options.database,
|
|
8305
|
+
pool: options.pool
|
|
8306
|
+
});
|
|
8307
|
+
const result = await model.ensureIndexes({
|
|
8308
|
+
dryRun: options.dryRun,
|
|
8309
|
+
throwOnError: options.throwOnError
|
|
8310
|
+
});
|
|
8311
|
+
models.push({ name, result });
|
|
8312
|
+
}
|
|
8313
|
+
return {
|
|
8314
|
+
dryRun: options.dryRun === true,
|
|
8315
|
+
models,
|
|
8316
|
+
totals: summarizeModelIndexEnsureResults(models.map((item) => item.result))
|
|
8317
|
+
};
|
|
8318
|
+
}
|
|
8082
8319
|
|
|
8083
8320
|
// src/entry/runtime-core-hosts.ts
|
|
8084
8321
|
function resolveAdapterCache(state) {
|
|
@@ -10787,7 +11024,8 @@ var MonSQLizeRuntime = class {
|
|
|
10787
11024
|
namespace: d.namespace,
|
|
10788
11025
|
log: d.log,
|
|
10789
11026
|
countQueue: this.options.countQueue,
|
|
10790
|
-
models: this.options.models
|
|
11027
|
+
models: this.options.models,
|
|
11028
|
+
autoIndex: this.options.autoIndex
|
|
10791
11029
|
};
|
|
10792
11030
|
}
|
|
10793
11031
|
async close() {
|
|
@@ -10984,6 +11222,10 @@ var MonSQLizeRuntime = class {
|
|
|
10984
11222
|
cache.set(name, instance);
|
|
10985
11223
|
return instance;
|
|
10986
11224
|
}
|
|
11225
|
+
async ensureModelIndexes(options = {}) {
|
|
11226
|
+
this.ensureConnected();
|
|
11227
|
+
return ensureRuntimeModelIndexes(this, options);
|
|
11228
|
+
}
|
|
10987
11229
|
// Capability delegation ----------------------------------------------------
|
|
10988
11230
|
async startSession(options = {}) {
|
|
10989
11231
|
this.ensureConnected();
|
package/dist/esm/index.mjs
CHANGED
|
@@ -980,6 +980,75 @@ function stableIndexStringify(value) {
|
|
|
980
980
|
}
|
|
981
981
|
return JSON.stringify(value) ?? "undefined";
|
|
982
982
|
}
|
|
983
|
+
function getIndexOptionName(options) {
|
|
984
|
+
return typeof options.name === "string" && options.name.length > 0 ? options.name : void 0;
|
|
985
|
+
}
|
|
986
|
+
function summarizeIndexError(error) {
|
|
987
|
+
if (error instanceof Error) {
|
|
988
|
+
const record = error;
|
|
989
|
+
return {
|
|
990
|
+
name: error.name,
|
|
991
|
+
message: error.message,
|
|
992
|
+
code: record.code ?? record.codeName
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
return {
|
|
996
|
+
message: String(error)
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
function isRecord(value) {
|
|
1000
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
1001
|
+
}
|
|
1002
|
+
function getExistingIndexKey(index) {
|
|
1003
|
+
return index.key;
|
|
1004
|
+
}
|
|
1005
|
+
function declaredOptionEntries(options) {
|
|
1006
|
+
return Object.entries(options).filter(([name, value]) => {
|
|
1007
|
+
if (value === void 0) return false;
|
|
1008
|
+
if (name === "background") return false;
|
|
1009
|
+
return true;
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
function indexOptionsMatch(existing, declared) {
|
|
1013
|
+
if (stableIndexStringify(getExistingIndexKey(existing)) !== stableIndexStringify(declared.key)) {
|
|
1014
|
+
return false;
|
|
1015
|
+
}
|
|
1016
|
+
for (const [name, value] of declaredOptionEntries(declared.options)) {
|
|
1017
|
+
const existingValue = existing[name];
|
|
1018
|
+
if (value === false && existingValue === void 0) {
|
|
1019
|
+
continue;
|
|
1020
|
+
}
|
|
1021
|
+
if (stableIndexStringify(existingValue) !== stableIndexStringify(value)) {
|
|
1022
|
+
return false;
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
return true;
|
|
1026
|
+
}
|
|
1027
|
+
function findExistingIndexByName(existingIndexes, name) {
|
|
1028
|
+
if (!name) return void 0;
|
|
1029
|
+
return existingIndexes.find((index) => index.name === name);
|
|
1030
|
+
}
|
|
1031
|
+
function findExistingIndexByKey(existingIndexes, key) {
|
|
1032
|
+
const fingerprint = stableIndexStringify(key);
|
|
1033
|
+
return existingIndexes.find((index) => stableIndexStringify(getExistingIndexKey(index)) === fingerprint);
|
|
1034
|
+
}
|
|
1035
|
+
function createIndexEnsureError(message, result, cause) {
|
|
1036
|
+
return createError(ErrorCodes.MONGODB_ERROR, message, [result], cause);
|
|
1037
|
+
}
|
|
1038
|
+
function resolveModelAutoIndexOptions(definition, runtimeAutoIndex) {
|
|
1039
|
+
const modelAutoIndex = toCompatDefinition(definition).options?.autoIndex;
|
|
1040
|
+
const value = modelAutoIndex ?? runtimeAutoIndex;
|
|
1041
|
+
if (value === false) {
|
|
1042
|
+
return { enabled: false, emitEvents: true };
|
|
1043
|
+
}
|
|
1044
|
+
if (value && typeof value === "object") {
|
|
1045
|
+
return {
|
|
1046
|
+
enabled: value.enabled !== false,
|
|
1047
|
+
emitEvents: value.emitEvents !== false
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
return { enabled: true, emitEvents: true };
|
|
1051
|
+
}
|
|
983
1052
|
function getIndexTaskRegistry(runtime) {
|
|
984
1053
|
if (!runtime) {
|
|
985
1054
|
return fallbackModelIndexTasks;
|
|
@@ -1007,6 +1076,20 @@ function resolveIndexTaskScope(collection, options) {
|
|
|
1007
1076
|
};
|
|
1008
1077
|
}
|
|
1009
1078
|
}
|
|
1079
|
+
function toIndexNamespace(scope) {
|
|
1080
|
+
return {
|
|
1081
|
+
db: scope.dbName,
|
|
1082
|
+
collection: scope.collectionName,
|
|
1083
|
+
poolName: scope.poolName
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
function emitIndexFailure(runtime, payload, emitEvents) {
|
|
1087
|
+
if (!emitEvents) {
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
const emitter = runtime;
|
|
1091
|
+
emitter?.emit?.("model-index-error", payload);
|
|
1092
|
+
}
|
|
1010
1093
|
function warnIndexFailure(runtime, taskKey, error) {
|
|
1011
1094
|
const logger = runtime;
|
|
1012
1095
|
logger?.logger?.warn?.("[MonSQLize] model index creation failed", {
|
|
@@ -1014,9 +1097,10 @@ function warnIndexFailure(runtime, taskKey, error) {
|
|
|
1014
1097
|
error: error instanceof Error ? error.message : String(error)
|
|
1015
1098
|
});
|
|
1016
1099
|
}
|
|
1017
|
-
function scheduleIndexTask(collection,
|
|
1100
|
+
function scheduleIndexTask(collection, declaredIndex, emitEvents, options) {
|
|
1018
1101
|
const scope = resolveIndexTaskScope(collection, options);
|
|
1019
|
-
const
|
|
1102
|
+
const { key, options: indexOptions } = declaredIndex;
|
|
1103
|
+
const indexFingerprint = declaredIndex.fingerprint;
|
|
1020
1104
|
const taskKey = `${scope.poolName}:${scope.dbName}:${scope.collectionName}:${indexFingerprint}`;
|
|
1021
1105
|
const registry = getIndexTaskRegistry(options?.runtime);
|
|
1022
1106
|
const existing = registry.get(taskKey);
|
|
@@ -1034,6 +1118,14 @@ function scheduleIndexTask(collection, key, indexOptions, options) {
|
|
|
1034
1118
|
task.status = "failed";
|
|
1035
1119
|
task.error = error;
|
|
1036
1120
|
warnIndexFailure(options?.runtime, taskKey, error);
|
|
1121
|
+
emitIndexFailure(options?.runtime, {
|
|
1122
|
+
namespace: scope,
|
|
1123
|
+
taskKey,
|
|
1124
|
+
source: declaredIndex.source,
|
|
1125
|
+
key,
|
|
1126
|
+
options: indexOptions,
|
|
1127
|
+
error: summarizeIndexError(error)
|
|
1128
|
+
}, emitEvents);
|
|
1037
1129
|
resolve();
|
|
1038
1130
|
});
|
|
1039
1131
|
});
|
|
@@ -1150,6 +1242,134 @@ function resolveModelHooksFactory(definition) {
|
|
|
1150
1242
|
const hooks = toCompatDefinition(definition).hooks;
|
|
1151
1243
|
return typeof hooks === "function" ? hooks : null;
|
|
1152
1244
|
}
|
|
1245
|
+
function collectModelIndexDefinitions(definition, softDeleteConfig) {
|
|
1246
|
+
const declared = [];
|
|
1247
|
+
if (softDeleteConfig?.enabled && softDeleteConfig.type === "timestamp" && softDeleteConfig.ttl) {
|
|
1248
|
+
const key = { [softDeleteConfig.field]: 1 };
|
|
1249
|
+
const options = { expireAfterSeconds: softDeleteConfig.ttl };
|
|
1250
|
+
declared.push({
|
|
1251
|
+
source: "softDelete",
|
|
1252
|
+
key,
|
|
1253
|
+
options,
|
|
1254
|
+
name: getIndexOptionName(options),
|
|
1255
|
+
fingerprint: stableIndexStringify({ key, options })
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
const indexes = toCompatDefinition(definition).indexes;
|
|
1259
|
+
if (!Array.isArray(indexes) || indexes.length === 0) {
|
|
1260
|
+
return declared;
|
|
1261
|
+
}
|
|
1262
|
+
for (const indexSpec of indexes) {
|
|
1263
|
+
if (!isRecord(indexSpec) || !indexSpec.key) {
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
const { key, ...indexOptions } = indexSpec;
|
|
1267
|
+
declared.push({
|
|
1268
|
+
source: "definition",
|
|
1269
|
+
key,
|
|
1270
|
+
options: indexOptions,
|
|
1271
|
+
name: getIndexOptionName(indexOptions),
|
|
1272
|
+
fingerprint: stableIndexStringify({ key, options: indexOptions })
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
return declared;
|
|
1276
|
+
}
|
|
1277
|
+
async function ensureModelIndexesForCollection(collection, definition, softDeleteConfig, options = {}) {
|
|
1278
|
+
const namespace = toIndexNamespace(resolveIndexTaskScope(collection, options));
|
|
1279
|
+
const declared = collectModelIndexDefinitions(definition, softDeleteConfig);
|
|
1280
|
+
const existingIndexes = await collection.listIndexes();
|
|
1281
|
+
const existing = [];
|
|
1282
|
+
const missing = [];
|
|
1283
|
+
const conflicts = [];
|
|
1284
|
+
for (const declaredIndex of declared) {
|
|
1285
|
+
const existingByName = findExistingIndexByName(existingIndexes, declaredIndex.name);
|
|
1286
|
+
if (existingByName) {
|
|
1287
|
+
if (indexOptionsMatch(existingByName, declaredIndex)) {
|
|
1288
|
+
existing.push({ declared: declaredIndex, existing: existingByName });
|
|
1289
|
+
} else {
|
|
1290
|
+
conflicts.push({
|
|
1291
|
+
declared: declaredIndex,
|
|
1292
|
+
existing: existingByName,
|
|
1293
|
+
reason: "name-conflict"
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
continue;
|
|
1297
|
+
}
|
|
1298
|
+
const existingByKey = findExistingIndexByKey(existingIndexes, declaredIndex.key);
|
|
1299
|
+
if (existingByKey) {
|
|
1300
|
+
if (indexOptionsMatch(existingByKey, declaredIndex)) {
|
|
1301
|
+
existing.push({ declared: declaredIndex, existing: existingByKey });
|
|
1302
|
+
} else {
|
|
1303
|
+
conflicts.push({
|
|
1304
|
+
declared: declaredIndex,
|
|
1305
|
+
existing: existingByKey,
|
|
1306
|
+
reason: "options-conflict"
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
continue;
|
|
1310
|
+
}
|
|
1311
|
+
missing.push(declaredIndex);
|
|
1312
|
+
}
|
|
1313
|
+
const result = {
|
|
1314
|
+
dryRun: options.dryRun === true,
|
|
1315
|
+
namespace,
|
|
1316
|
+
declared,
|
|
1317
|
+
existing,
|
|
1318
|
+
missing,
|
|
1319
|
+
created: [],
|
|
1320
|
+
conflicts,
|
|
1321
|
+
failed: [],
|
|
1322
|
+
skipped: options.dryRun === true ? missing.map((declaredIndex) => ({ declared: declaredIndex, reason: "dry-run" })) : conflicts.map((conflict) => ({ declared: conflict.declared, reason: conflict.reason }))
|
|
1323
|
+
};
|
|
1324
|
+
if (conflicts.length > 0 && options.throwOnError) {
|
|
1325
|
+
throw createIndexEnsureError("Model index conflicts detected.", result);
|
|
1326
|
+
}
|
|
1327
|
+
if (options.dryRun === true) {
|
|
1328
|
+
return result;
|
|
1329
|
+
}
|
|
1330
|
+
for (const declaredIndex of missing) {
|
|
1331
|
+
try {
|
|
1332
|
+
const createdName = await collection.createIndex(declaredIndex.key, declaredIndex.options);
|
|
1333
|
+
result.created.push({
|
|
1334
|
+
declared: declaredIndex,
|
|
1335
|
+
name: typeof createdName === "string" ? createdName : void 0,
|
|
1336
|
+
result: createdName
|
|
1337
|
+
});
|
|
1338
|
+
} catch (error) {
|
|
1339
|
+
result.failed.push({
|
|
1340
|
+
declared: declaredIndex,
|
|
1341
|
+
error: summarizeIndexError(error)
|
|
1342
|
+
});
|
|
1343
|
+
if (options.throwOnError) {
|
|
1344
|
+
throw createIndexEnsureError(
|
|
1345
|
+
"Model index creation failed.",
|
|
1346
|
+
result,
|
|
1347
|
+
error instanceof Error ? error : void 0
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
return result;
|
|
1353
|
+
}
|
|
1354
|
+
function summarizeModelIndexEnsureResults(results) {
|
|
1355
|
+
return results.reduce((totals, result) => ({
|
|
1356
|
+
declared: totals.declared + result.declared.length,
|
|
1357
|
+
existing: totals.existing + result.existing.length,
|
|
1358
|
+
missing: totals.missing + result.missing.length,
|
|
1359
|
+
created: totals.created + result.created.length,
|
|
1360
|
+
conflicts: totals.conflicts + result.conflicts.length,
|
|
1361
|
+
failed: totals.failed + result.failed.length,
|
|
1362
|
+
skipped: totals.skipped + result.skipped.length
|
|
1363
|
+
}), {
|
|
1364
|
+
declared: 0,
|
|
1365
|
+
existing: 0,
|
|
1366
|
+
missing: 0,
|
|
1367
|
+
created: 0,
|
|
1368
|
+
conflicts: 0,
|
|
1369
|
+
failed: 0,
|
|
1370
|
+
skipped: 0
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1153
1373
|
function initializeModelV1Methods(target, definition) {
|
|
1154
1374
|
const methods = toCompatDefinition(definition).methods;
|
|
1155
1375
|
if (typeof methods !== "function") {
|
|
@@ -1174,25 +1394,12 @@ function initializeModelV1Methods(target, definition) {
|
|
|
1174
1394
|
}
|
|
1175
1395
|
}
|
|
1176
1396
|
function scheduleModelIndexes(collection, definition, softDeleteConfig, options) {
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
scheduleIndexTask(
|
|
1180
|
-
collection,
|
|
1181
|
-
{ [softDeleteIndex.field]: 1 },
|
|
1182
|
-
{ expireAfterSeconds: softDeleteIndex.ttl },
|
|
1183
|
-
options
|
|
1184
|
-
);
|
|
1185
|
-
}
|
|
1186
|
-
const indexes = toCompatDefinition(definition).indexes;
|
|
1187
|
-
if (!Array.isArray(indexes) || indexes.length === 0) {
|
|
1397
|
+
const autoIndex = resolveModelAutoIndexOptions(definition, options?.autoIndex);
|
|
1398
|
+
if (!autoIndex.enabled) {
|
|
1188
1399
|
return;
|
|
1189
1400
|
}
|
|
1190
|
-
for (const
|
|
1191
|
-
|
|
1192
|
-
continue;
|
|
1193
|
-
}
|
|
1194
|
-
const { key, ...indexOptions } = indexSpec;
|
|
1195
|
-
scheduleIndexTask(collection, key, indexOptions, options);
|
|
1401
|
+
for (const declaredIndex of collectModelIndexDefinitions(definition, softDeleteConfig)) {
|
|
1402
|
+
scheduleIndexTask(collection, declaredIndex, autoIndex.emitEvents, options);
|
|
1196
1403
|
}
|
|
1197
1404
|
}
|
|
1198
1405
|
|
|
@@ -1760,7 +1967,8 @@ var ModelInstance = class {
|
|
|
1760
1967
|
runtime: this.runtime,
|
|
1761
1968
|
dbName: options.dbName,
|
|
1762
1969
|
poolName: options.poolName,
|
|
1763
|
-
collectionName: options.collectionName
|
|
1970
|
+
collectionName: options.collectionName,
|
|
1971
|
+
autoIndex: this.runtime.options?.autoIndex
|
|
1764
1972
|
});
|
|
1765
1973
|
this._v1InstanceMethods = initializeModelV1Methods(this, options.definition);
|
|
1766
1974
|
}
|
|
@@ -1964,6 +2172,15 @@ var ModelInstance = class {
|
|
|
1964
2172
|
listIndexes() {
|
|
1965
2173
|
return this.collection.listIndexes();
|
|
1966
2174
|
}
|
|
2175
|
+
ensureIndexes(options = {}) {
|
|
2176
|
+
return ensureModelIndexesForCollection(this.collection, this.definition, this._softDeleteConfig, {
|
|
2177
|
+
...options,
|
|
2178
|
+
runtime: this.runtime,
|
|
2179
|
+
dbName: this.dbName,
|
|
2180
|
+
poolName: this.poolName,
|
|
2181
|
+
collectionName: this.collectionName
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
1967
2184
|
dropIndex(name) {
|
|
1968
2185
|
return this.collection.dropIndex(name);
|
|
1969
2186
|
}
|
|
@@ -8062,6 +8279,26 @@ function createRuntimeModelInstance(host, name, scope) {
|
|
|
8062
8279
|
});
|
|
8063
8280
|
return instance;
|
|
8064
8281
|
}
|
|
8282
|
+
async function ensureRuntimeModelIndexes(host, options = {}) {
|
|
8283
|
+
const modelNames = options.models ?? Model.list();
|
|
8284
|
+
const models = [];
|
|
8285
|
+
for (const name of modelNames) {
|
|
8286
|
+
const model = host.scopedModel(name, {
|
|
8287
|
+
database: options.database,
|
|
8288
|
+
pool: options.pool
|
|
8289
|
+
});
|
|
8290
|
+
const result = await model.ensureIndexes({
|
|
8291
|
+
dryRun: options.dryRun,
|
|
8292
|
+
throwOnError: options.throwOnError
|
|
8293
|
+
});
|
|
8294
|
+
models.push({ name, result });
|
|
8295
|
+
}
|
|
8296
|
+
return {
|
|
8297
|
+
dryRun: options.dryRun === true,
|
|
8298
|
+
models,
|
|
8299
|
+
totals: summarizeModelIndexEnsureResults(models.map((item) => item.result))
|
|
8300
|
+
};
|
|
8301
|
+
}
|
|
8065
8302
|
|
|
8066
8303
|
// src/entry/runtime-core-hosts.ts
|
|
8067
8304
|
function resolveAdapterCache(state) {
|
|
@@ -10773,7 +11010,8 @@ var MonSQLizeRuntime = class {
|
|
|
10773
11010
|
namespace: d.namespace,
|
|
10774
11011
|
log: d.log,
|
|
10775
11012
|
countQueue: this.options.countQueue,
|
|
10776
|
-
models: this.options.models
|
|
11013
|
+
models: this.options.models,
|
|
11014
|
+
autoIndex: this.options.autoIndex
|
|
10777
11015
|
};
|
|
10778
11016
|
}
|
|
10779
11017
|
async close() {
|
|
@@ -10970,6 +11208,10 @@ var MonSQLizeRuntime = class {
|
|
|
10970
11208
|
cache.set(name, instance);
|
|
10971
11209
|
return instance;
|
|
10972
11210
|
}
|
|
11211
|
+
async ensureModelIndexes(options = {}) {
|
|
11212
|
+
this.ensureConnected();
|
|
11213
|
+
return ensureRuntimeModelIndexes(this, options);
|
|
11214
|
+
}
|
|
10973
11215
|
// Capability delegation ----------------------------------------------------
|
|
10974
11216
|
async startSession(options = {}) {
|
|
10975
11217
|
this.ensureConnected();
|
package/dist/types/model.d.ts
CHANGED
|
@@ -62,6 +62,104 @@ export interface VirtualConfig {
|
|
|
62
62
|
set?: (this: Record<string, unknown>, value: unknown) => void;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
export type ModelAutoIndexOptions = boolean | {
|
|
66
|
+
/** Enable automatic model index creation. Defaults to true for backward compatibility. */
|
|
67
|
+
enabled?: boolean;
|
|
68
|
+
/** Emit `model-index-error` when automatic index creation fails. Defaults to true. */
|
|
69
|
+
emitEvents?: boolean;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type ModelIndexSource = 'definition' | 'softDelete';
|
|
73
|
+
|
|
74
|
+
export interface ModelDeclaredIndex {
|
|
75
|
+
source: ModelIndexSource;
|
|
76
|
+
key: unknown;
|
|
77
|
+
options: Record<string, unknown>;
|
|
78
|
+
name?: string;
|
|
79
|
+
fingerprint: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ModelIndexNamespace {
|
|
83
|
+
db: string;
|
|
84
|
+
collection: string;
|
|
85
|
+
poolName: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface ModelIndexErrorSummary {
|
|
89
|
+
name?: string;
|
|
90
|
+
message: string;
|
|
91
|
+
code?: unknown;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface ModelIndexEnsureExisting {
|
|
95
|
+
declared: ModelDeclaredIndex;
|
|
96
|
+
existing: Record<string, unknown>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ModelIndexConflict {
|
|
100
|
+
declared: ModelDeclaredIndex;
|
|
101
|
+
existing?: Record<string, unknown>;
|
|
102
|
+
reason: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface ModelIndexCreated {
|
|
106
|
+
declared: ModelDeclaredIndex;
|
|
107
|
+
name?: string;
|
|
108
|
+
result?: unknown;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface ModelIndexFailure {
|
|
112
|
+
declared: ModelDeclaredIndex;
|
|
113
|
+
error: ModelIndexErrorSummary;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface ModelIndexSkipped {
|
|
117
|
+
declared: ModelDeclaredIndex;
|
|
118
|
+
reason: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface ModelEnsureIndexesOptions {
|
|
122
|
+
/** Return the index diff without creating missing indexes. */
|
|
123
|
+
dryRun?: boolean;
|
|
124
|
+
/** Throw a MonSQLize `MONGODB_ERROR` when conflicts or creation failures are found. */
|
|
125
|
+
throwOnError?: boolean;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export interface ModelEnsureAllIndexesOptions extends ModelEnsureIndexesOptions {
|
|
129
|
+
/** Limit the operation to specific registered model names. Defaults to all models. */
|
|
130
|
+
models?: string[];
|
|
131
|
+
/** Optional database scope for models without their own connection override. */
|
|
132
|
+
database?: string;
|
|
133
|
+
/** Optional pool scope for models without their own connection override. */
|
|
134
|
+
pool?: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface ModelIndexEnsureResult {
|
|
138
|
+
dryRun: boolean;
|
|
139
|
+
namespace: ModelIndexNamespace;
|
|
140
|
+
declared: ModelDeclaredIndex[];
|
|
141
|
+
existing: ModelIndexEnsureExisting[];
|
|
142
|
+
missing: ModelDeclaredIndex[];
|
|
143
|
+
created: ModelIndexCreated[];
|
|
144
|
+
conflicts: ModelIndexConflict[];
|
|
145
|
+
failed: ModelIndexFailure[];
|
|
146
|
+
skipped: ModelIndexSkipped[];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface ModelIndexEnsureSummary {
|
|
150
|
+
dryRun: boolean;
|
|
151
|
+
models: Array<{ name: string; result: ModelIndexEnsureResult }>;
|
|
152
|
+
totals: {
|
|
153
|
+
declared: number;
|
|
154
|
+
existing: number;
|
|
155
|
+
missing: number;
|
|
156
|
+
created: number;
|
|
157
|
+
conflicts: number;
|
|
158
|
+
failed: number;
|
|
159
|
+
skipped: number;
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
65
163
|
/** v1 hooks factory format */
|
|
66
164
|
export type V1HooksFactory<TDocument = Record<string, unknown>> = (
|
|
67
165
|
model: ModelInstance<TDocument>,
|
|
@@ -95,6 +193,7 @@ export type V1MethodsFactory<TDocument = Record<string, unknown>> = (
|
|
|
95
193
|
export interface ModelDefinitionOptions {
|
|
96
194
|
timestamps?: boolean | { createdAt?: string | boolean; updatedAt?: string | boolean };
|
|
97
195
|
validate?: boolean;
|
|
196
|
+
autoIndex?: ModelAutoIndexOptions;
|
|
98
197
|
softDelete?: boolean | {
|
|
99
198
|
enabled?: boolean;
|
|
100
199
|
field?: string;
|
|
@@ -443,6 +542,11 @@ export interface ModelInstance<TDocument = any> {
|
|
|
443
542
|
createIndexes(specs: Array<{ key: unknown; } & Record<string, unknown>>): Promise<string[]>;
|
|
444
543
|
/** Lists all existing index definitions on the collection. */
|
|
445
544
|
listIndexes(): Promise<Record<string, unknown>[]>;
|
|
545
|
+
/**
|
|
546
|
+
* Compares declared model indexes with the database and optionally creates missing indexes.
|
|
547
|
+
* Does not drop, rename, or rebuild conflicting indexes.
|
|
548
|
+
*/
|
|
549
|
+
ensureIndexes(options?: ModelEnsureIndexesOptions): Promise<ModelIndexEnsureResult>;
|
|
446
550
|
/**
|
|
447
551
|
* Drops the specified index by name.
|
|
448
552
|
* @param name Index name.
|
|
@@ -33,7 +33,7 @@ export interface SSHConfig {
|
|
|
33
33
|
|
|
34
34
|
import type { Collection, DbAccessor, HealthView } from './collection';
|
|
35
35
|
import type { Lock, LockOptions, LockStats } from './lock';
|
|
36
|
-
import type { ModelInstance } from './model';
|
|
36
|
+
import type { ModelAutoIndexOptions, ModelEnsureAllIndexesOptions, ModelIndexEnsureSummary, ModelInstance } from './model';
|
|
37
37
|
import type { MongoConnectConfig } from './mongodb';
|
|
38
38
|
import type { ConnectionPoolManagerOptions, PoolConfig, PoolHealthStatus, PoolStats, PoolStrategy } from './pool';
|
|
39
39
|
import type {
|
|
@@ -176,6 +176,8 @@ export interface MonSQLizeOptions {
|
|
|
176
176
|
countQueue?: boolean | { enabled?: boolean; concurrency?: number; maxQueueSize?: number; timeout?: number; };
|
|
177
177
|
/** Model definitions to auto-register on connect. Accepts a file path (string) or an object with { path, pattern?, recursive? }. @since v1.3.0 */
|
|
178
178
|
models?: string | { path: string; pattern?: string; recursive?: boolean; };
|
|
179
|
+
/** Global automatic model index creation control. Defaults to true for backward compatibility. */
|
|
180
|
+
autoIndex?: ModelAutoIndexOptions;
|
|
179
181
|
/** Auto-invalidate cache on write operations. @since v1.3.0 */
|
|
180
182
|
cacheAutoInvalidate?: boolean;
|
|
181
183
|
}
|
|
@@ -256,6 +258,11 @@ export interface MonSQLizeInstance {
|
|
|
256
258
|
*/
|
|
257
259
|
model<TDocument = any>(name: string): ModelInstance<TDocument>;
|
|
258
260
|
model(name: string): ModelInstance<any>;
|
|
261
|
+
/**
|
|
262
|
+
* Ensures declared indexes for registered models.
|
|
263
|
+
* Use `dryRun: true` for production preflight; execution only creates missing indexes.
|
|
264
|
+
*/
|
|
265
|
+
ensureModelIndexes(options?: ModelEnsureAllIndexesOptions): Promise<ModelIndexEnsureSummary>;
|
|
259
266
|
/**
|
|
260
267
|
* Start a MongoDB transaction session.
|
|
261
268
|
* @param options Optional transaction options.
|
|
@@ -442,6 +449,7 @@ export default class MonSQLize implements MonSQLizeInstance {
|
|
|
442
449
|
scopedModel(name: string, options?: { database?: string; pool?: string; }): ModelInstance<any>;
|
|
443
450
|
model<TDocument = any>(name: string): ModelInstance<TDocument>;
|
|
444
451
|
model(name: string): ModelInstance<any>;
|
|
452
|
+
ensureModelIndexes(options?: ModelEnsureAllIndexesOptions): Promise<ModelIndexEnsureSummary>;
|
|
445
453
|
startSession(options?: TransactionOptions): Promise<Transaction>;
|
|
446
454
|
withTransaction<T>(callback: (transaction: Transaction) => Promise<T>, options?: TransactionOptions): Promise<T>;
|
|
447
455
|
withLock<T>(key: string, callback: () => Promise<T>, options?: LockOptions): Promise<T>;
|
package/dist/types/runtime.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BookmarkClearResult, BookmarkListResult, BookmarkPrewarmResult, DeleteBatchResult, DeleteResult, IncrementOneResult, IndexCreateResult, InsertBatchResult, InsertManyResult, UpdateBatchResult, UpdateResult } from './collection';
|
|
2
2
|
import type { LoggerLike, ExpressionFunction, ExpressionObject } from './base';
|
|
3
|
-
import type { ModelDefinition, ModelInstance as ModelInstanceContract, RegisteredModel, RelationConfig } from './model';
|
|
3
|
+
import type { ModelDefinition, ModelEnsureIndexesOptions, ModelIndexEnsureResult, ModelInstance as ModelInstanceContract, RegisteredModel, RelationConfig } from './model';
|
|
4
4
|
import type { LockOptions, LockStats } from './lock';
|
|
5
5
|
import type { ConnectionPoolManagerOptions, FallbackStrategy, PoolConfig, PoolHealthStatus, PoolRole, PoolStats, PoolStrategy } from './pool';
|
|
6
6
|
import type { SagaDefinition, SagaOrchestratorOptions, SagaResult, SagaStats, SagaStep } from './saga';
|
|
@@ -335,6 +335,7 @@ export declare class ModelInstance<TDocument = any> implements ModelInstanceCont
|
|
|
335
335
|
createIndex(keys: unknown, options?: unknown): Promise<IndexCreateResult>;
|
|
336
336
|
createIndexes(specs: Array<{ key: unknown; } & Record<string, unknown>>): Promise<string[]>;
|
|
337
337
|
listIndexes(): Promise<Record<string, unknown>[]>;
|
|
338
|
+
ensureIndexes(options?: ModelEnsureIndexesOptions): Promise<ModelIndexEnsureResult>;
|
|
338
339
|
dropIndex(name: string): Promise<unknown>;
|
|
339
340
|
dropIndexes(): Promise<unknown>;
|
|
340
341
|
prewarmBookmarks(keyDims?: unknown, pages?: number[]): Promise<BookmarkPrewarmResult>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "monsqlize",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.4",
|
|
4
4
|
"description": "TypeScript-native MongoDB ODM with multi-level caching (cache-hub), distributed locks, Saga orchestration, unified expression system (122 operators), connection pool management, ChangeStream sync, slow-query logging, and full v1 API compatibility",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./dist/cjs/index.cjs",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"dist/**/*.cjs",
|
|
19
19
|
"dist/**/*.mjs",
|
|
20
20
|
"dist/**/*.d.ts",
|
|
21
|
+
"changelogs/v2.0.4.md",
|
|
21
22
|
"changelogs/v2.0.3.md",
|
|
22
23
|
"changelogs/v2.0.2.md",
|
|
23
24
|
"changelogs/v2.0.1.md",
|
|
@@ -115,7 +116,7 @@
|
|
|
115
116
|
"cache-hub": "2.2.4",
|
|
116
117
|
"ioredis": "5.8.2",
|
|
117
118
|
"mongodb": "6.21.0",
|
|
118
|
-
"schema-dsl": "2.0.
|
|
119
|
+
"schema-dsl": "2.0.9",
|
|
119
120
|
"ssh2": "1.17.0"
|
|
120
121
|
}
|
|
121
122
|
}
|