monsqlize 2.0.3 → 2.0.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  # CHANGELOG
2
2
 
3
- > Summary index — current release details are packaged in [changelogs/v2.0.3.md](./changelogs/v2.0.3.md); historical details live in the repository changelog archive.
4
- > **Last updated**: 2026-06-11
3
+ > Summary index — current release details are packaged in [changelogs/v2.0.5.md](./changelogs/v2.0.5.md); historical details live in the repository changelog archive.
4
+ > **Last updated**: 2026-06-13
5
5
 
6
6
  ---
7
7
 
@@ -9,6 +9,8 @@
9
9
 
10
10
  | Version | Date | Summary | Details |
11
11
  |---------|------|---------|---------|
12
+ | [v2.0.5](./changelogs/v2.0.5.md) | 2026-06-13 | Patch: Model schema adapter delegates DSL type authority to `schema-dsl@2.0.10`, removing the duplicated monSQLize allowlist while preserving legacy aliases and business literals | [View](./changelogs/v2.0.5.md) |
13
+ | [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
14
  | [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
15
  | [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
16
  | [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 +448,9 @@ const result = await msq.collection('orders').insertOne(dataFromMongoose);
446
448
  changelogs/
447
449
  ├── README.md # 变更文档说明
448
450
  ├── TEMPLATE.md # 变更文档模板
449
- ├── v2.0.3.md # 当前发布详细变更
451
+ ├── v2.0.5.md # 当前发布详细变更
452
+ ├── v2.0.4.md # v2.0.4 详细变更
453
+ ├── v2.0.3.md # v2.0.3 详细变更
450
454
  ├── v2.0.2.md # v2.0.2 详细变更
451
455
  ├── v2.0.1.md # v2.0.1 详细变更
452
456
  ├── v2.0.0.md # v2 TypeScript 重写发布详细变更
@@ -500,7 +504,9 @@ changelogs/
500
504
 
501
505
  ## 相关文档
502
506
 
503
- - [changelogs/v2.0.3.md](./changelogs/v2.0.3.md) - 当前发布详细变更文档
507
+ - [changelogs/v2.0.5.md](./changelogs/v2.0.5.md) - 当前发布详细变更文档
508
+ - [changelogs/v2.0.4.md](./changelogs/v2.0.4.md) - v2.0.4 详细变更文档
509
+ - [changelogs/v2.0.3.md](./changelogs/v2.0.3.md) - v2.0.3 详细变更文档
504
510
  - [changelogs/v2.0.2.md](./changelogs/v2.0.2.md) - v2.0.2 详细变更文档
505
511
  - [changelogs/v2.0.1.md](./changelogs/v2.0.1.md) - v2.0.1 详细变更文档
506
512
  - [README.md](./README.md) - 项目说明
@@ -508,5 +514,5 @@ changelogs/
508
514
 
509
515
  ---
510
516
 
511
- **最后更新**: 2026-06-11
517
+ **最后更新**: 2026-06-13
512
518
 
package/README.md CHANGED
@@ -7,6 +7,8 @@ TypeScript-native MongoDB ODM and enhancement layer with v1-compatible APIs, mul
7
7
  [![MongoDB](https://img.shields.io/badge/MongoDB-6.x%20%2F%207.x-green.svg)](https://www.mongodb.com/)
8
8
  [![Node.js](https://img.shields.io/badge/Node.js-18%2B-brightgreen)](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 optimistic locking.
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.3`.
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.8`; deprecated `2.3.x` mistake releases are intentionally excluded.
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.
@@ -0,0 +1,33 @@
1
+ # v2.0.5 — 2026-06-13
2
+
3
+ ## Overview
4
+
5
+ v2.0.5 is a compatibility patch for Model schema validation. It removes the duplicated schema-dsl base-type allowlist from monSQLize and delegates DSL type decisions to schema-dsl, keeping schema-dsl as the single validation DSL authority.
6
+
7
+ ---
8
+
9
+ ## Runtime Fixes
10
+
11
+ - Removed the monSQLize-side schema-dsl base-type allowlist from the Model schema adapter.
12
+ - Delegated schema compilation and unknown-type handling to schema-dsl diagnostics instead of maintaining a second type table.
13
+ - Preserved compatibility for schema-dsl-owned aliases such as `int`, `mixed`, `buffer` and `objectid`.
14
+ - Preserved existing literal business DSL strings such as `datetime` and `admin_login!` through schema-dsl fallback semantics.
15
+
16
+ ## Dependencies
17
+
18
+ - Targets `schema-dsl@2.0.10`, which contains the shared legacy DSL aliases and structured diagnostics used by this patch.
19
+ - Keeps the root `cache-hub@2.2.4` dependency unchanged.
20
+
21
+ ## Compatibility
22
+
23
+ - SemVer: patch release.
24
+ - Breaking changes: none.
25
+ - Existing v2 Model schema declarations continue to validate through the same public Model API.
26
+ - Consumer projects do not need to change source code for the checked schema alias and business-literal paths.
27
+
28
+ ## Validation
29
+
30
+ - `npm run type-check`
31
+ - targeted Model schema validation tests
32
+ - targeted pure-function and branch coverage tests
33
+ - local schema-dsl@2.0.10 compatibility smoke
@@ -320,49 +320,8 @@ try {
320
320
  _schemaValidateFn = mod.validate;
321
321
  } catch {
322
322
  }
323
- var KNOWN_SCHEMA_BASE_TYPES = /* @__PURE__ */ new Set([
324
- "string",
325
- "number",
326
- "boolean",
327
- "integer",
328
- "float",
329
- "int",
330
- "double",
331
- "decimal",
332
- "date",
333
- "objectid",
334
- "uuid",
335
- "email",
336
- "url",
337
- "buffer",
338
- "binary",
339
- "object",
340
- "array",
341
- "any",
342
- "mixed",
343
- "null"
344
- ]);
345
- function _extractBaseType(typeStr) {
346
- const m = typeStr.match(/^[a-zA-Z_]+/);
347
- return m ? m[0].toLowerCase() : "";
348
- }
349
323
  function _makeValidatingDslFn(realDsl) {
350
324
  const validating = function validatingDsl(fields) {
351
- if (fields && typeof fields === "object") {
352
- for (const [field, spec] of Object.entries(fields)) {
353
- if (typeof spec === "string") {
354
- if (spec.includes("|")) {
355
- continue;
356
- }
357
- const base = _extractBaseType(spec);
358
- if (base && !KNOWN_SCHEMA_BASE_TYPES.has(base)) {
359
- throw new TypeError(
360
- `[schema] Invalid type "${base}" in field "${field}". Known types: ${[...KNOWN_SCHEMA_BASE_TYPES].join(", ")}.`
361
- );
362
- }
363
- }
364
- }
365
- }
366
325
  return realDsl(fields);
367
326
  };
368
327
  return validating;
@@ -997,6 +956,75 @@ function stableIndexStringify(value) {
997
956
  }
998
957
  return JSON.stringify(value) ?? "undefined";
999
958
  }
959
+ function getIndexOptionName(options) {
960
+ return typeof options.name === "string" && options.name.length > 0 ? options.name : void 0;
961
+ }
962
+ function summarizeIndexError(error) {
963
+ if (error instanceof Error) {
964
+ const record = error;
965
+ return {
966
+ name: error.name,
967
+ message: error.message,
968
+ code: record.code ?? record.codeName
969
+ };
970
+ }
971
+ return {
972
+ message: String(error)
973
+ };
974
+ }
975
+ function isRecord(value) {
976
+ return !!value && typeof value === "object" && !Array.isArray(value);
977
+ }
978
+ function getExistingIndexKey(index) {
979
+ return index.key;
980
+ }
981
+ function declaredOptionEntries(options) {
982
+ return Object.entries(options).filter(([name, value]) => {
983
+ if (value === void 0) return false;
984
+ if (name === "background") return false;
985
+ return true;
986
+ });
987
+ }
988
+ function indexOptionsMatch(existing, declared) {
989
+ if (stableIndexStringify(getExistingIndexKey(existing)) !== stableIndexStringify(declared.key)) {
990
+ return false;
991
+ }
992
+ for (const [name, value] of declaredOptionEntries(declared.options)) {
993
+ const existingValue = existing[name];
994
+ if (value === false && existingValue === void 0) {
995
+ continue;
996
+ }
997
+ if (stableIndexStringify(existingValue) !== stableIndexStringify(value)) {
998
+ return false;
999
+ }
1000
+ }
1001
+ return true;
1002
+ }
1003
+ function findExistingIndexByName(existingIndexes, name) {
1004
+ if (!name) return void 0;
1005
+ return existingIndexes.find((index) => index.name === name);
1006
+ }
1007
+ function findExistingIndexByKey(existingIndexes, key) {
1008
+ const fingerprint = stableIndexStringify(key);
1009
+ return existingIndexes.find((index) => stableIndexStringify(getExistingIndexKey(index)) === fingerprint);
1010
+ }
1011
+ function createIndexEnsureError(message, result, cause) {
1012
+ return createError(ErrorCodes.MONGODB_ERROR, message, [result], cause);
1013
+ }
1014
+ function resolveModelAutoIndexOptions(definition, runtimeAutoIndex) {
1015
+ const modelAutoIndex = toCompatDefinition(definition).options?.autoIndex;
1016
+ const value = modelAutoIndex ?? runtimeAutoIndex;
1017
+ if (value === false) {
1018
+ return { enabled: false, emitEvents: true };
1019
+ }
1020
+ if (value && typeof value === "object") {
1021
+ return {
1022
+ enabled: value.enabled !== false,
1023
+ emitEvents: value.emitEvents !== false
1024
+ };
1025
+ }
1026
+ return { enabled: true, emitEvents: true };
1027
+ }
1000
1028
  function getIndexTaskRegistry(runtime) {
1001
1029
  if (!runtime) {
1002
1030
  return fallbackModelIndexTasks;
@@ -1024,6 +1052,20 @@ function resolveIndexTaskScope(collection, options) {
1024
1052
  };
1025
1053
  }
1026
1054
  }
1055
+ function toIndexNamespace(scope) {
1056
+ return {
1057
+ db: scope.dbName,
1058
+ collection: scope.collectionName,
1059
+ poolName: scope.poolName
1060
+ };
1061
+ }
1062
+ function emitIndexFailure(runtime, payload, emitEvents) {
1063
+ if (!emitEvents) {
1064
+ return;
1065
+ }
1066
+ const emitter = runtime;
1067
+ emitter?.emit?.("model-index-error", payload);
1068
+ }
1027
1069
  function warnIndexFailure(runtime, taskKey, error) {
1028
1070
  const logger = runtime;
1029
1071
  logger?.logger?.warn?.("[MonSQLize] model index creation failed", {
@@ -1031,9 +1073,10 @@ function warnIndexFailure(runtime, taskKey, error) {
1031
1073
  error: error instanceof Error ? error.message : String(error)
1032
1074
  });
1033
1075
  }
1034
- function scheduleIndexTask(collection, key, indexOptions, options) {
1076
+ function scheduleIndexTask(collection, declaredIndex, emitEvents, options) {
1035
1077
  const scope = resolveIndexTaskScope(collection, options);
1036
- const indexFingerprint = stableIndexStringify({ key, options: indexOptions });
1078
+ const { key, options: indexOptions } = declaredIndex;
1079
+ const indexFingerprint = declaredIndex.fingerprint;
1037
1080
  const taskKey = `${scope.poolName}:${scope.dbName}:${scope.collectionName}:${indexFingerprint}`;
1038
1081
  const registry = getIndexTaskRegistry(options?.runtime);
1039
1082
  const existing = registry.get(taskKey);
@@ -1051,6 +1094,14 @@ function scheduleIndexTask(collection, key, indexOptions, options) {
1051
1094
  task.status = "failed";
1052
1095
  task.error = error;
1053
1096
  warnIndexFailure(options?.runtime, taskKey, error);
1097
+ emitIndexFailure(options?.runtime, {
1098
+ namespace: scope,
1099
+ taskKey,
1100
+ source: declaredIndex.source,
1101
+ key,
1102
+ options: indexOptions,
1103
+ error: summarizeIndexError(error)
1104
+ }, emitEvents);
1054
1105
  resolve();
1055
1106
  });
1056
1107
  });
@@ -1167,6 +1218,134 @@ function resolveModelHooksFactory(definition) {
1167
1218
  const hooks = toCompatDefinition(definition).hooks;
1168
1219
  return typeof hooks === "function" ? hooks : null;
1169
1220
  }
1221
+ function collectModelIndexDefinitions(definition, softDeleteConfig) {
1222
+ const declared = [];
1223
+ if (softDeleteConfig?.enabled && softDeleteConfig.type === "timestamp" && softDeleteConfig.ttl) {
1224
+ const key = { [softDeleteConfig.field]: 1 };
1225
+ const options = { expireAfterSeconds: softDeleteConfig.ttl };
1226
+ declared.push({
1227
+ source: "softDelete",
1228
+ key,
1229
+ options,
1230
+ name: getIndexOptionName(options),
1231
+ fingerprint: stableIndexStringify({ key, options })
1232
+ });
1233
+ }
1234
+ const indexes = toCompatDefinition(definition).indexes;
1235
+ if (!Array.isArray(indexes) || indexes.length === 0) {
1236
+ return declared;
1237
+ }
1238
+ for (const indexSpec of indexes) {
1239
+ if (!isRecord(indexSpec) || !indexSpec.key) {
1240
+ continue;
1241
+ }
1242
+ const { key, ...indexOptions } = indexSpec;
1243
+ declared.push({
1244
+ source: "definition",
1245
+ key,
1246
+ options: indexOptions,
1247
+ name: getIndexOptionName(indexOptions),
1248
+ fingerprint: stableIndexStringify({ key, options: indexOptions })
1249
+ });
1250
+ }
1251
+ return declared;
1252
+ }
1253
+ async function ensureModelIndexesForCollection(collection, definition, softDeleteConfig, options = {}) {
1254
+ const namespace = toIndexNamespace(resolveIndexTaskScope(collection, options));
1255
+ const declared = collectModelIndexDefinitions(definition, softDeleteConfig);
1256
+ const existingIndexes = await collection.listIndexes();
1257
+ const existing = [];
1258
+ const missing = [];
1259
+ const conflicts = [];
1260
+ for (const declaredIndex of declared) {
1261
+ const existingByName = findExistingIndexByName(existingIndexes, declaredIndex.name);
1262
+ if (existingByName) {
1263
+ if (indexOptionsMatch(existingByName, declaredIndex)) {
1264
+ existing.push({ declared: declaredIndex, existing: existingByName });
1265
+ } else {
1266
+ conflicts.push({
1267
+ declared: declaredIndex,
1268
+ existing: existingByName,
1269
+ reason: "name-conflict"
1270
+ });
1271
+ }
1272
+ continue;
1273
+ }
1274
+ const existingByKey = findExistingIndexByKey(existingIndexes, declaredIndex.key);
1275
+ if (existingByKey) {
1276
+ if (indexOptionsMatch(existingByKey, declaredIndex)) {
1277
+ existing.push({ declared: declaredIndex, existing: existingByKey });
1278
+ } else {
1279
+ conflicts.push({
1280
+ declared: declaredIndex,
1281
+ existing: existingByKey,
1282
+ reason: "options-conflict"
1283
+ });
1284
+ }
1285
+ continue;
1286
+ }
1287
+ missing.push(declaredIndex);
1288
+ }
1289
+ const result = {
1290
+ dryRun: options.dryRun === true,
1291
+ namespace,
1292
+ declared,
1293
+ existing,
1294
+ missing,
1295
+ created: [],
1296
+ conflicts,
1297
+ failed: [],
1298
+ skipped: options.dryRun === true ? missing.map((declaredIndex) => ({ declared: declaredIndex, reason: "dry-run" })) : conflicts.map((conflict) => ({ declared: conflict.declared, reason: conflict.reason }))
1299
+ };
1300
+ if (conflicts.length > 0 && options.throwOnError) {
1301
+ throw createIndexEnsureError("Model index conflicts detected.", result);
1302
+ }
1303
+ if (options.dryRun === true) {
1304
+ return result;
1305
+ }
1306
+ for (const declaredIndex of missing) {
1307
+ try {
1308
+ const createdName = await collection.createIndex(declaredIndex.key, declaredIndex.options);
1309
+ result.created.push({
1310
+ declared: declaredIndex,
1311
+ name: typeof createdName === "string" ? createdName : void 0,
1312
+ result: createdName
1313
+ });
1314
+ } catch (error) {
1315
+ result.failed.push({
1316
+ declared: declaredIndex,
1317
+ error: summarizeIndexError(error)
1318
+ });
1319
+ if (options.throwOnError) {
1320
+ throw createIndexEnsureError(
1321
+ "Model index creation failed.",
1322
+ result,
1323
+ error instanceof Error ? error : void 0
1324
+ );
1325
+ }
1326
+ }
1327
+ }
1328
+ return result;
1329
+ }
1330
+ function summarizeModelIndexEnsureResults(results) {
1331
+ return results.reduce((totals, result) => ({
1332
+ declared: totals.declared + result.declared.length,
1333
+ existing: totals.existing + result.existing.length,
1334
+ missing: totals.missing + result.missing.length,
1335
+ created: totals.created + result.created.length,
1336
+ conflicts: totals.conflicts + result.conflicts.length,
1337
+ failed: totals.failed + result.failed.length,
1338
+ skipped: totals.skipped + result.skipped.length
1339
+ }), {
1340
+ declared: 0,
1341
+ existing: 0,
1342
+ missing: 0,
1343
+ created: 0,
1344
+ conflicts: 0,
1345
+ failed: 0,
1346
+ skipped: 0
1347
+ });
1348
+ }
1170
1349
  function initializeModelV1Methods(target, definition) {
1171
1350
  const methods = toCompatDefinition(definition).methods;
1172
1351
  if (typeof methods !== "function") {
@@ -1191,25 +1370,12 @@ function initializeModelV1Methods(target, definition) {
1191
1370
  }
1192
1371
  }
1193
1372
  function scheduleModelIndexes(collection, definition, softDeleteConfig, options) {
1194
- if (softDeleteConfig?.enabled && softDeleteConfig.type === "timestamp" && softDeleteConfig.ttl) {
1195
- const softDeleteIndex = softDeleteConfig;
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) {
1373
+ const autoIndex = resolveModelAutoIndexOptions(definition, options?.autoIndex);
1374
+ if (!autoIndex.enabled) {
1205
1375
  return;
1206
1376
  }
1207
- for (const indexSpec of indexes) {
1208
- if (!indexSpec?.key) {
1209
- continue;
1210
- }
1211
- const { key, ...indexOptions } = indexSpec;
1212
- scheduleIndexTask(collection, key, indexOptions, options);
1377
+ for (const declaredIndex of collectModelIndexDefinitions(definition, softDeleteConfig)) {
1378
+ scheduleIndexTask(collection, declaredIndex, autoIndex.emitEvents, options);
1213
1379
  }
1214
1380
  }
1215
1381
 
@@ -1777,7 +1943,8 @@ var ModelInstance = class {
1777
1943
  runtime: this.runtime,
1778
1944
  dbName: options.dbName,
1779
1945
  poolName: options.poolName,
1780
- collectionName: options.collectionName
1946
+ collectionName: options.collectionName,
1947
+ autoIndex: this.runtime.options?.autoIndex
1781
1948
  });
1782
1949
  this._v1InstanceMethods = initializeModelV1Methods(this, options.definition);
1783
1950
  }
@@ -1981,6 +2148,15 @@ var ModelInstance = class {
1981
2148
  listIndexes() {
1982
2149
  return this.collection.listIndexes();
1983
2150
  }
2151
+ ensureIndexes(options = {}) {
2152
+ return ensureModelIndexesForCollection(this.collection, this.definition, this._softDeleteConfig, {
2153
+ ...options,
2154
+ runtime: this.runtime,
2155
+ dbName: this.dbName,
2156
+ poolName: this.poolName,
2157
+ collectionName: this.collectionName
2158
+ });
2159
+ }
1984
2160
  dropIndex(name) {
1985
2161
  return this.collection.dropIndex(name);
1986
2162
  }
@@ -8079,6 +8255,26 @@ function createRuntimeModelInstance(host, name, scope) {
8079
8255
  });
8080
8256
  return instance;
8081
8257
  }
8258
+ async function ensureRuntimeModelIndexes(host, options = {}) {
8259
+ const modelNames = options.models ?? Model.list();
8260
+ const models = [];
8261
+ for (const name of modelNames) {
8262
+ const model = host.scopedModel(name, {
8263
+ database: options.database,
8264
+ pool: options.pool
8265
+ });
8266
+ const result = await model.ensureIndexes({
8267
+ dryRun: options.dryRun,
8268
+ throwOnError: options.throwOnError
8269
+ });
8270
+ models.push({ name, result });
8271
+ }
8272
+ return {
8273
+ dryRun: options.dryRun === true,
8274
+ models,
8275
+ totals: summarizeModelIndexEnsureResults(models.map((item) => item.result))
8276
+ };
8277
+ }
8082
8278
 
8083
8279
  // src/entry/runtime-core-hosts.ts
8084
8280
  function resolveAdapterCache(state) {
@@ -10787,7 +10983,8 @@ var MonSQLizeRuntime = class {
10787
10983
  namespace: d.namespace,
10788
10984
  log: d.log,
10789
10985
  countQueue: this.options.countQueue,
10790
- models: this.options.models
10986
+ models: this.options.models,
10987
+ autoIndex: this.options.autoIndex
10791
10988
  };
10792
10989
  }
10793
10990
  async close() {
@@ -10984,6 +11181,10 @@ var MonSQLizeRuntime = class {
10984
11181
  cache.set(name, instance);
10985
11182
  return instance;
10986
11183
  }
11184
+ async ensureModelIndexes(options = {}) {
11185
+ this.ensureConnected();
11186
+ return ensureRuntimeModelIndexes(this, options);
11187
+ }
10987
11188
  // Capability delegation ----------------------------------------------------
10988
11189
  async startSession(options = {}) {
10989
11190
  this.ensureConnected();
@@ -303,49 +303,8 @@ try {
303
303
  _schemaValidateFn = mod.validate;
304
304
  } catch {
305
305
  }
306
- var KNOWN_SCHEMA_BASE_TYPES = /* @__PURE__ */ new Set([
307
- "string",
308
- "number",
309
- "boolean",
310
- "integer",
311
- "float",
312
- "int",
313
- "double",
314
- "decimal",
315
- "date",
316
- "objectid",
317
- "uuid",
318
- "email",
319
- "url",
320
- "buffer",
321
- "binary",
322
- "object",
323
- "array",
324
- "any",
325
- "mixed",
326
- "null"
327
- ]);
328
- function _extractBaseType(typeStr) {
329
- const m = typeStr.match(/^[a-zA-Z_]+/);
330
- return m ? m[0].toLowerCase() : "";
331
- }
332
306
  function _makeValidatingDslFn(realDsl) {
333
307
  const validating = function validatingDsl(fields) {
334
- if (fields && typeof fields === "object") {
335
- for (const [field, spec] of Object.entries(fields)) {
336
- if (typeof spec === "string") {
337
- if (spec.includes("|")) {
338
- continue;
339
- }
340
- const base = _extractBaseType(spec);
341
- if (base && !KNOWN_SCHEMA_BASE_TYPES.has(base)) {
342
- throw new TypeError(
343
- `[schema] Invalid type "${base}" in field "${field}". Known types: ${[...KNOWN_SCHEMA_BASE_TYPES].join(", ")}.`
344
- );
345
- }
346
- }
347
- }
348
- }
349
308
  return realDsl(fields);
350
309
  };
351
310
  return validating;
@@ -980,6 +939,75 @@ function stableIndexStringify(value) {
980
939
  }
981
940
  return JSON.stringify(value) ?? "undefined";
982
941
  }
942
+ function getIndexOptionName(options) {
943
+ return typeof options.name === "string" && options.name.length > 0 ? options.name : void 0;
944
+ }
945
+ function summarizeIndexError(error) {
946
+ if (error instanceof Error) {
947
+ const record = error;
948
+ return {
949
+ name: error.name,
950
+ message: error.message,
951
+ code: record.code ?? record.codeName
952
+ };
953
+ }
954
+ return {
955
+ message: String(error)
956
+ };
957
+ }
958
+ function isRecord(value) {
959
+ return !!value && typeof value === "object" && !Array.isArray(value);
960
+ }
961
+ function getExistingIndexKey(index) {
962
+ return index.key;
963
+ }
964
+ function declaredOptionEntries(options) {
965
+ return Object.entries(options).filter(([name, value]) => {
966
+ if (value === void 0) return false;
967
+ if (name === "background") return false;
968
+ return true;
969
+ });
970
+ }
971
+ function indexOptionsMatch(existing, declared) {
972
+ if (stableIndexStringify(getExistingIndexKey(existing)) !== stableIndexStringify(declared.key)) {
973
+ return false;
974
+ }
975
+ for (const [name, value] of declaredOptionEntries(declared.options)) {
976
+ const existingValue = existing[name];
977
+ if (value === false && existingValue === void 0) {
978
+ continue;
979
+ }
980
+ if (stableIndexStringify(existingValue) !== stableIndexStringify(value)) {
981
+ return false;
982
+ }
983
+ }
984
+ return true;
985
+ }
986
+ function findExistingIndexByName(existingIndexes, name) {
987
+ if (!name) return void 0;
988
+ return existingIndexes.find((index) => index.name === name);
989
+ }
990
+ function findExistingIndexByKey(existingIndexes, key) {
991
+ const fingerprint = stableIndexStringify(key);
992
+ return existingIndexes.find((index) => stableIndexStringify(getExistingIndexKey(index)) === fingerprint);
993
+ }
994
+ function createIndexEnsureError(message, result, cause) {
995
+ return createError(ErrorCodes.MONGODB_ERROR, message, [result], cause);
996
+ }
997
+ function resolveModelAutoIndexOptions(definition, runtimeAutoIndex) {
998
+ const modelAutoIndex = toCompatDefinition(definition).options?.autoIndex;
999
+ const value = modelAutoIndex ?? runtimeAutoIndex;
1000
+ if (value === false) {
1001
+ return { enabled: false, emitEvents: true };
1002
+ }
1003
+ if (value && typeof value === "object") {
1004
+ return {
1005
+ enabled: value.enabled !== false,
1006
+ emitEvents: value.emitEvents !== false
1007
+ };
1008
+ }
1009
+ return { enabled: true, emitEvents: true };
1010
+ }
983
1011
  function getIndexTaskRegistry(runtime) {
984
1012
  if (!runtime) {
985
1013
  return fallbackModelIndexTasks;
@@ -1007,6 +1035,20 @@ function resolveIndexTaskScope(collection, options) {
1007
1035
  };
1008
1036
  }
1009
1037
  }
1038
+ function toIndexNamespace(scope) {
1039
+ return {
1040
+ db: scope.dbName,
1041
+ collection: scope.collectionName,
1042
+ poolName: scope.poolName
1043
+ };
1044
+ }
1045
+ function emitIndexFailure(runtime, payload, emitEvents) {
1046
+ if (!emitEvents) {
1047
+ return;
1048
+ }
1049
+ const emitter = runtime;
1050
+ emitter?.emit?.("model-index-error", payload);
1051
+ }
1010
1052
  function warnIndexFailure(runtime, taskKey, error) {
1011
1053
  const logger = runtime;
1012
1054
  logger?.logger?.warn?.("[MonSQLize] model index creation failed", {
@@ -1014,9 +1056,10 @@ function warnIndexFailure(runtime, taskKey, error) {
1014
1056
  error: error instanceof Error ? error.message : String(error)
1015
1057
  });
1016
1058
  }
1017
- function scheduleIndexTask(collection, key, indexOptions, options) {
1059
+ function scheduleIndexTask(collection, declaredIndex, emitEvents, options) {
1018
1060
  const scope = resolveIndexTaskScope(collection, options);
1019
- const indexFingerprint = stableIndexStringify({ key, options: indexOptions });
1061
+ const { key, options: indexOptions } = declaredIndex;
1062
+ const indexFingerprint = declaredIndex.fingerprint;
1020
1063
  const taskKey = `${scope.poolName}:${scope.dbName}:${scope.collectionName}:${indexFingerprint}`;
1021
1064
  const registry = getIndexTaskRegistry(options?.runtime);
1022
1065
  const existing = registry.get(taskKey);
@@ -1034,6 +1077,14 @@ function scheduleIndexTask(collection, key, indexOptions, options) {
1034
1077
  task.status = "failed";
1035
1078
  task.error = error;
1036
1079
  warnIndexFailure(options?.runtime, taskKey, error);
1080
+ emitIndexFailure(options?.runtime, {
1081
+ namespace: scope,
1082
+ taskKey,
1083
+ source: declaredIndex.source,
1084
+ key,
1085
+ options: indexOptions,
1086
+ error: summarizeIndexError(error)
1087
+ }, emitEvents);
1037
1088
  resolve();
1038
1089
  });
1039
1090
  });
@@ -1150,6 +1201,134 @@ function resolveModelHooksFactory(definition) {
1150
1201
  const hooks = toCompatDefinition(definition).hooks;
1151
1202
  return typeof hooks === "function" ? hooks : null;
1152
1203
  }
1204
+ function collectModelIndexDefinitions(definition, softDeleteConfig) {
1205
+ const declared = [];
1206
+ if (softDeleteConfig?.enabled && softDeleteConfig.type === "timestamp" && softDeleteConfig.ttl) {
1207
+ const key = { [softDeleteConfig.field]: 1 };
1208
+ const options = { expireAfterSeconds: softDeleteConfig.ttl };
1209
+ declared.push({
1210
+ source: "softDelete",
1211
+ key,
1212
+ options,
1213
+ name: getIndexOptionName(options),
1214
+ fingerprint: stableIndexStringify({ key, options })
1215
+ });
1216
+ }
1217
+ const indexes = toCompatDefinition(definition).indexes;
1218
+ if (!Array.isArray(indexes) || indexes.length === 0) {
1219
+ return declared;
1220
+ }
1221
+ for (const indexSpec of indexes) {
1222
+ if (!isRecord(indexSpec) || !indexSpec.key) {
1223
+ continue;
1224
+ }
1225
+ const { key, ...indexOptions } = indexSpec;
1226
+ declared.push({
1227
+ source: "definition",
1228
+ key,
1229
+ options: indexOptions,
1230
+ name: getIndexOptionName(indexOptions),
1231
+ fingerprint: stableIndexStringify({ key, options: indexOptions })
1232
+ });
1233
+ }
1234
+ return declared;
1235
+ }
1236
+ async function ensureModelIndexesForCollection(collection, definition, softDeleteConfig, options = {}) {
1237
+ const namespace = toIndexNamespace(resolveIndexTaskScope(collection, options));
1238
+ const declared = collectModelIndexDefinitions(definition, softDeleteConfig);
1239
+ const existingIndexes = await collection.listIndexes();
1240
+ const existing = [];
1241
+ const missing = [];
1242
+ const conflicts = [];
1243
+ for (const declaredIndex of declared) {
1244
+ const existingByName = findExistingIndexByName(existingIndexes, declaredIndex.name);
1245
+ if (existingByName) {
1246
+ if (indexOptionsMatch(existingByName, declaredIndex)) {
1247
+ existing.push({ declared: declaredIndex, existing: existingByName });
1248
+ } else {
1249
+ conflicts.push({
1250
+ declared: declaredIndex,
1251
+ existing: existingByName,
1252
+ reason: "name-conflict"
1253
+ });
1254
+ }
1255
+ continue;
1256
+ }
1257
+ const existingByKey = findExistingIndexByKey(existingIndexes, declaredIndex.key);
1258
+ if (existingByKey) {
1259
+ if (indexOptionsMatch(existingByKey, declaredIndex)) {
1260
+ existing.push({ declared: declaredIndex, existing: existingByKey });
1261
+ } else {
1262
+ conflicts.push({
1263
+ declared: declaredIndex,
1264
+ existing: existingByKey,
1265
+ reason: "options-conflict"
1266
+ });
1267
+ }
1268
+ continue;
1269
+ }
1270
+ missing.push(declaredIndex);
1271
+ }
1272
+ const result = {
1273
+ dryRun: options.dryRun === true,
1274
+ namespace,
1275
+ declared,
1276
+ existing,
1277
+ missing,
1278
+ created: [],
1279
+ conflicts,
1280
+ failed: [],
1281
+ skipped: options.dryRun === true ? missing.map((declaredIndex) => ({ declared: declaredIndex, reason: "dry-run" })) : conflicts.map((conflict) => ({ declared: conflict.declared, reason: conflict.reason }))
1282
+ };
1283
+ if (conflicts.length > 0 && options.throwOnError) {
1284
+ throw createIndexEnsureError("Model index conflicts detected.", result);
1285
+ }
1286
+ if (options.dryRun === true) {
1287
+ return result;
1288
+ }
1289
+ for (const declaredIndex of missing) {
1290
+ try {
1291
+ const createdName = await collection.createIndex(declaredIndex.key, declaredIndex.options);
1292
+ result.created.push({
1293
+ declared: declaredIndex,
1294
+ name: typeof createdName === "string" ? createdName : void 0,
1295
+ result: createdName
1296
+ });
1297
+ } catch (error) {
1298
+ result.failed.push({
1299
+ declared: declaredIndex,
1300
+ error: summarizeIndexError(error)
1301
+ });
1302
+ if (options.throwOnError) {
1303
+ throw createIndexEnsureError(
1304
+ "Model index creation failed.",
1305
+ result,
1306
+ error instanceof Error ? error : void 0
1307
+ );
1308
+ }
1309
+ }
1310
+ }
1311
+ return result;
1312
+ }
1313
+ function summarizeModelIndexEnsureResults(results) {
1314
+ return results.reduce((totals, result) => ({
1315
+ declared: totals.declared + result.declared.length,
1316
+ existing: totals.existing + result.existing.length,
1317
+ missing: totals.missing + result.missing.length,
1318
+ created: totals.created + result.created.length,
1319
+ conflicts: totals.conflicts + result.conflicts.length,
1320
+ failed: totals.failed + result.failed.length,
1321
+ skipped: totals.skipped + result.skipped.length
1322
+ }), {
1323
+ declared: 0,
1324
+ existing: 0,
1325
+ missing: 0,
1326
+ created: 0,
1327
+ conflicts: 0,
1328
+ failed: 0,
1329
+ skipped: 0
1330
+ });
1331
+ }
1153
1332
  function initializeModelV1Methods(target, definition) {
1154
1333
  const methods = toCompatDefinition(definition).methods;
1155
1334
  if (typeof methods !== "function") {
@@ -1174,25 +1353,12 @@ function initializeModelV1Methods(target, definition) {
1174
1353
  }
1175
1354
  }
1176
1355
  function scheduleModelIndexes(collection, definition, softDeleteConfig, options) {
1177
- if (softDeleteConfig?.enabled && softDeleteConfig.type === "timestamp" && softDeleteConfig.ttl) {
1178
- const softDeleteIndex = softDeleteConfig;
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) {
1356
+ const autoIndex = resolveModelAutoIndexOptions(definition, options?.autoIndex);
1357
+ if (!autoIndex.enabled) {
1188
1358
  return;
1189
1359
  }
1190
- for (const indexSpec of indexes) {
1191
- if (!indexSpec?.key) {
1192
- continue;
1193
- }
1194
- const { key, ...indexOptions } = indexSpec;
1195
- scheduleIndexTask(collection, key, indexOptions, options);
1360
+ for (const declaredIndex of collectModelIndexDefinitions(definition, softDeleteConfig)) {
1361
+ scheduleIndexTask(collection, declaredIndex, autoIndex.emitEvents, options);
1196
1362
  }
1197
1363
  }
1198
1364
 
@@ -1760,7 +1926,8 @@ var ModelInstance = class {
1760
1926
  runtime: this.runtime,
1761
1927
  dbName: options.dbName,
1762
1928
  poolName: options.poolName,
1763
- collectionName: options.collectionName
1929
+ collectionName: options.collectionName,
1930
+ autoIndex: this.runtime.options?.autoIndex
1764
1931
  });
1765
1932
  this._v1InstanceMethods = initializeModelV1Methods(this, options.definition);
1766
1933
  }
@@ -1964,6 +2131,15 @@ var ModelInstance = class {
1964
2131
  listIndexes() {
1965
2132
  return this.collection.listIndexes();
1966
2133
  }
2134
+ ensureIndexes(options = {}) {
2135
+ return ensureModelIndexesForCollection(this.collection, this.definition, this._softDeleteConfig, {
2136
+ ...options,
2137
+ runtime: this.runtime,
2138
+ dbName: this.dbName,
2139
+ poolName: this.poolName,
2140
+ collectionName: this.collectionName
2141
+ });
2142
+ }
1967
2143
  dropIndex(name) {
1968
2144
  return this.collection.dropIndex(name);
1969
2145
  }
@@ -8062,6 +8238,26 @@ function createRuntimeModelInstance(host, name, scope) {
8062
8238
  });
8063
8239
  return instance;
8064
8240
  }
8241
+ async function ensureRuntimeModelIndexes(host, options = {}) {
8242
+ const modelNames = options.models ?? Model.list();
8243
+ const models = [];
8244
+ for (const name of modelNames) {
8245
+ const model = host.scopedModel(name, {
8246
+ database: options.database,
8247
+ pool: options.pool
8248
+ });
8249
+ const result = await model.ensureIndexes({
8250
+ dryRun: options.dryRun,
8251
+ throwOnError: options.throwOnError
8252
+ });
8253
+ models.push({ name, result });
8254
+ }
8255
+ return {
8256
+ dryRun: options.dryRun === true,
8257
+ models,
8258
+ totals: summarizeModelIndexEnsureResults(models.map((item) => item.result))
8259
+ };
8260
+ }
8065
8261
 
8066
8262
  // src/entry/runtime-core-hosts.ts
8067
8263
  function resolveAdapterCache(state) {
@@ -10773,7 +10969,8 @@ var MonSQLizeRuntime = class {
10773
10969
  namespace: d.namespace,
10774
10970
  log: d.log,
10775
10971
  countQueue: this.options.countQueue,
10776
- models: this.options.models
10972
+ models: this.options.models,
10973
+ autoIndex: this.options.autoIndex
10777
10974
  };
10778
10975
  }
10779
10976
  async close() {
@@ -10970,6 +11167,10 @@ var MonSQLizeRuntime = class {
10970
11167
  cache.set(name, instance);
10971
11168
  return instance;
10972
11169
  }
11170
+ async ensureModelIndexes(options = {}) {
11171
+ this.ensureConnected();
11172
+ return ensureRuntimeModelIndexes(this, options);
11173
+ }
10973
11174
  // Capability delegation ----------------------------------------------------
10974
11175
  async startSession(options = {}) {
10975
11176
  this.ensureConnected();
@@ -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>;
@@ -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",
3
+ "version": "2.0.5",
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,8 @@
18
18
  "dist/**/*.cjs",
19
19
  "dist/**/*.mjs",
20
20
  "dist/**/*.d.ts",
21
+ "changelogs/v2.0.5.md",
22
+ "changelogs/v2.0.4.md",
21
23
  "changelogs/v2.0.3.md",
22
24
  "changelogs/v2.0.2.md",
23
25
  "changelogs/v2.0.1.md",
@@ -115,7 +117,7 @@
115
117
  "cache-hub": "2.2.4",
116
118
  "ioredis": "5.8.2",
117
119
  "mongodb": "6.21.0",
118
- "schema-dsl": "2.0.8",
120
+ "schema-dsl": "2.0.10",
119
121
  "ssh2": "1.17.0"
120
122
  }
121
123
  }