@owlmeans/mongo-resource 0.1.18-rc.31 → 0.1.18-rc.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -12,7 +12,7 @@ Client-side records go in [`@owlmeans/state`](../state).
12
12
  ## Installation
13
13
 
14
14
  ```bash
15
- bun add @owlmeans/mongo-resource@^0.1.18-rc.31
15
+ bun add @owlmeans/mongo-resource@^0.1.18-rc.33
16
16
  ```
17
17
 
18
18
  `mongodb` and `ajv` are peer dependencies. The connection service comes from
@@ -294,7 +294,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
294
294
  your project's skill store (`.agents/skills/`):
295
295
 
296
296
  ```sh
297
- npx @owlmeans/agent-skills@^0.1.18-rc.32
297
+ npx @owlmeans/agent-skills@^0.1.18-rc.34
298
298
  ```
299
299
 
300
300
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/mongo-resource",
4
- "version": "0.1.18-rc.31",
5
- "generatedAt": "2026-09-22T12:36:21.811Z",
4
+ "version": "0.1.18-rc.33",
5
+ "generatedAt": "2026-09-22T20:45:24.047Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -8,7 +8,7 @@ user-invocable: false
8
8
  # @owlmeans/mongo-resource
9
9
 
10
10
  **Layer:** Infra
11
- **Install:** `"@owlmeans/mongo-resource": "^0.1.18-rc.31"` in `dependencies` (peers `mongodb`, `ajv`)
11
+ **Install:** `"@owlmeans/mongo-resource": "^0.1.18-rc.33"` in `dependencies` (peers `mongodb`, `ajv`)
12
12
 
13
13
  The Mongo counterpart of [[postgres-resource]]. A collection has no structure of its own, so
14
14
  here the resource layer owns the *validator* (`$jsonSchema` from the AJV schema), the indexes,
@@ -133,9 +133,14 @@ with `marshalReference(field, value)` and convert read-back documents' reference
133
133
  after earlier collections were already rewritten, leaving the database half-migrated and the boot
134
134
  aborting on every restart. Drop the stale index by name in the body first (guard on the key spec
135
135
  so the drop is idempotent) and let reconciliation recreate it from the declaration.
136
- - **Index reconciliation matches by name and recreates on any difference.** A live index whose key
137
- pattern or options differ from the declaration is dropped and created again, so a renamed field
138
- does converge — after `Pre`, which is why the bullet above exists. Text indexes are the one
136
+ - **Index reconciliation matches by name and recreates on a REAL difference.** A live index whose
137
+ key pattern or options differ from the declaration is dropped and created again, so a renamed
138
+ field does converge — after `Pre`, which is why the bullet above exists. The comparison treats
139
+ the key as ordered (a compound index is its key order) and the options as a set, and ignores
140
+ what the server reports rather than the declaration asks for (`v`, `collation`, index versions):
141
+ comparing the two documents as JSON instead made every boot drop and recreate every index, which
142
+ stayed invisible until two processes booted together and the loser died on `IndexNotFound`. A
143
+ drop that finds the index already gone is therefore tolerated. Text indexes are the one
139
144
  exception: a live index carrying `weights` is left as it is, and changing one means dropping it
140
145
  yourself.
141
146
  - On a collection this boot just created, every registered migration is **baselined**
@@ -1,5 +1,15 @@
1
1
  import type { ResourceRecord } from '@owlmeans/resource';
2
2
  import type { MongoResource } from '../types.js';
3
- import type { Collection } from 'mongodb';
3
+ import type { Collection, Document } from 'mongodb';
4
+ /**
5
+ * Whether an index the database already holds differs from the one declared.
6
+ *
7
+ * Mongo returns an index's options in ITS order, not the declaration's, so comparing the two
8
+ * documents as JSON reported every index as changed — and every boot then dropped and recreated
9
+ * every index. That is invisible until two processes boot at once, when the second one's drop
10
+ * fails with `IndexNotFound` and takes the service down with it. Only the KEY is order-sensitive
11
+ * (a compound index is its key order); options are a set.
12
+ */
13
+ export declare const indexChanged: (existing: Document, proposed: Document) => boolean;
4
14
  export declare const updateIndexes: (collection: Collection, resource: MongoResource<ResourceRecord>) => Promise<void>;
5
15
  //# sourceMappingURL=indexes.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"indexes.d.ts","sourceRoot":"","sources":["../../src/utils/indexes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAEzC,eAAO,MAAM,aAAa,eAAsB,UAAU,YAAY,aAAa,CAAC,cAAc,CAAC,kBAiClG,CAAA"}
1
+ {"version":3,"file":"indexes.d.ts","sourceRoot":"","sources":["../../src/utils/indexes.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAChD,OAAO,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAanD;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,aAAc,QAAQ,YAAY,QAAQ,KAAG,OAMrE,CAAA;AAED,eAAO,MAAM,aAAa,eAAsB,UAAU,YAAY,aAAa,CAAC,cAAc,CAAC,kBA8BlG,CAAA"}
@@ -1,34 +1,58 @@
1
+ /** What the SERVER reports about an index rather than what a declaration asks for — never a
2
+ * reason to recreate one. `collation` is here because a collection's default collation is
3
+ * reported on every index in it, including the ones declared without any. */
4
+ const REPORTED_FIELDS = new Set([
5
+ 'v', 'ns', 'name', 'collation', 'textIndexVersion', '2dsphereIndexVersion', 'background'
6
+ ]);
7
+ const options = (spec) => Object.entries(spec)
8
+ .filter(([field]) => field !== 'key' && !REPORTED_FIELDS.has(field))
9
+ .sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
10
+ /**
11
+ * Whether an index the database already holds differs from the one declared.
12
+ *
13
+ * Mongo returns an index's options in ITS order, not the declaration's, so comparing the two
14
+ * documents as JSON reported every index as changed — and every boot then dropped and recreated
15
+ * every index. That is invisible until two processes boot at once, when the second one's drop
16
+ * fails with `IndexNotFound` and takes the service down with it. Only the KEY is order-sensitive
17
+ * (a compound index is its key order); options are a set.
18
+ */
19
+ export const indexChanged = (existing, proposed) => {
20
+ // We do not recreate text indexes.
21
+ if ('weights' in existing)
22
+ return false;
23
+ if (JSON.stringify(existing.key) !== JSON.stringify(proposed.key))
24
+ return true;
25
+ return JSON.stringify(options(existing)) !== JSON.stringify(options(proposed));
26
+ };
1
27
  export const updateIndexes = async (collection, resource) => {
2
- if (resource.indexes != null) {
3
- await Promise.all(resource.indexes.map(async (index) => {
4
- const _index = (await collection.indexes()).find(_index => _index.name === index.name);
5
- if (_index != null) {
6
- const existing = { ..._index };
7
- if ('name' in existing) {
8
- delete existing.name;
9
- }
10
- if ('v' in existing) {
11
- delete existing.v;
12
- }
13
- const propsed = { key: index.index, ...index.options };
14
- // We do not recreate text indexes
15
- if (JSON.stringify(existing) !== JSON.stringify(propsed) && !('weights' in existing)) {
16
- console.info(JSON.stringify(existing));
17
- console.info(JSON.stringify(propsed));
18
- console.debug(`Recreate index ${resource.alias} : ${index.name}`);
19
- await collection.dropIndex(index.name);
20
- await collection.createIndex(index.index, {
21
- name: index.name, ...((index.options != null) ? index.options : {})
22
- });
23
- }
24
- }
25
- else {
26
- console.debug(`Create index ${resource.alias} : ${index.name}`);
27
- await collection.createIndex(index.index, {
28
- name: index.name, ...((index.options != null) ? index.options : {})
29
- });
30
- }
31
- }));
28
+ if (resource.indexes == null) {
29
+ return;
32
30
  }
31
+ const present = await collection.indexes();
32
+ await Promise.all(resource.indexes.map(async (index) => {
33
+ const create = async () => await collection.createIndex(index.index, {
34
+ name: index.name, ...((index.options != null) ? index.options : {})
35
+ });
36
+ const existing = present.find(_index => _index.name === index.name);
37
+ if (existing == null) {
38
+ console.debug(`Create index ${resource.alias} : ${index.name}`);
39
+ await create();
40
+ return;
41
+ }
42
+ if (!indexChanged(existing, { key: index.index, ...index.options })) {
43
+ return;
44
+ }
45
+ console.debug(`Recreate index ${resource.alias} : ${index.name}`);
46
+ try {
47
+ await collection.dropIndex(index.name);
48
+ }
49
+ catch (e) {
50
+ // IndexNotFound: another process booting alongside this one dropped it first — it is
51
+ // recreating the same declaration, so there is nothing here to fail over.
52
+ if (e.code !== 27)
53
+ throw e;
54
+ }
55
+ await create();
56
+ }));
33
57
  };
34
58
  //# sourceMappingURL=indexes.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"indexes.js","sourceRoot":"","sources":["../../src/utils/indexes.ts"],"names":[],"mappings":"AAIA,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,EAAE,UAAsB,EAAE,QAAuC,EAAE,EAAE;IACrG,IAAI,QAAQ,CAAC,OAAO,IAAI,IAAI,EAAE,CAAC;QAC7B,MAAM,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,GAAG,CACpC,KAAK,EAAC,KAAK,EAAC,EAAE;YACZ,MAAM,MAAM,GAAG,CAAC,MAAM,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,CAAC,CAAA;YACtF,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC;gBACnB,MAAM,QAAQ,GAAG,EAAE,GAAG,MAAM,EAAE,CAAA;gBAC9B,IAAI,MAAM,IAAI,QAAQ,EAAE,CAAC;oBACvB,OAAO,QAAQ,CAAC,IAAI,CAAA;gBACtB,CAAC;gBACD,IAAI,GAAG,IAAI,QAAQ,EAAE,CAAC;oBACpB,OAAO,QAAQ,CAAC,CAAC,CAAA;gBACnB,CAAC;gBACD,MAAM,OAAO,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,KAAK,EAAE,GAAG,KAAK,CAAC,OAAO,EAAE,CAAA;gBACtD,kCAAkC;gBAClC,IAAI,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,SAAS,IAAI,QAAQ,CAAC,EAAE,CAAC;oBACrF,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAA;oBACtC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAA;oBACrC,OAAO,CAAC,KAAK,CAAC,kBAAkB,QAAQ,CAAC,KAAK,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC,CAAA;oBACjE,MAAM,UAAU,CAAC,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;oBACtC,MAAM,UAAU,CAAC,WAAW,CAAC,KAAK,CAAC,KAAK,EAAE;wBACxC,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;qBACpE,CAAC,CAAA;gBACJ,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,KAAK,CAAC,gBAAgB,QAAQ,CAAC,KAAK,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC,CAAA;gBAC/D,MAAM,UAAU,CAAC,WAAW,CAAC,KAAK,CAAC,KAAK,EAAE;oBACxC,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;iBACpE,CAAC,CAAA;YACJ,CAAC;QACH,CAAC,CACF,CAAC,CAAA;IACJ,CAAC;AACH,CAAC,CAAA"}
1
+ {"version":3,"file":"indexes.js","sourceRoot":"","sources":["../../src/utils/indexes.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/mongo-resource",
3
- "version": "0.1.18-rc.31",
3
+ "version": "0.1.18-rc.33",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -26,13 +26,13 @@
26
26
  "mongodb": "*"
27
27
  },
28
28
  "dependencies": {
29
- "@owlmeans/context": "^0.1.18-rc.28",
30
- "@owlmeans/resource": "^0.1.18-rc.30",
31
- "@owlmeans/server-context": "^0.1.18-rc.31"
29
+ "@owlmeans/context": "^0.1.18-rc.30",
30
+ "@owlmeans/resource": "^0.1.18-rc.32",
31
+ "@owlmeans/server-context": "^0.1.18-rc.33"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@owlmeans/dep-config": "workspace:*",
35
- "@owlmeans/test-integration": "^0.1.18-rc.25",
35
+ "@owlmeans/test-integration": "^0.1.18-rc.27",
36
36
  "@types/bun": "^1.4.0",
37
37
  "@types/node": "^26.1.0",
38
38
  "mongodb": "^7.5.0",
@@ -1,38 +1,63 @@
1
1
  import type { ResourceRecord } from '@owlmeans/resource'
2
2
  import type { MongoResource } from '../types.js'
3
- import type { Collection } from 'mongodb'
3
+ import type { Collection, Document } from 'mongodb'
4
+
5
+ /** What the SERVER reports about an index rather than what a declaration asks for — never a
6
+ * reason to recreate one. `collation` is here because a collection's default collation is
7
+ * reported on every index in it, including the ones declared without any. */
8
+ const REPORTED_FIELDS = new Set([
9
+ 'v', 'ns', 'name', 'collation', 'textIndexVersion', '2dsphereIndexVersion', 'background'
10
+ ])
11
+
12
+ const options = (spec: Document): [string, unknown][] => Object.entries(spec)
13
+ .filter(([field]) => field !== 'key' && !REPORTED_FIELDS.has(field))
14
+ .sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)
15
+
16
+ /**
17
+ * Whether an index the database already holds differs from the one declared.
18
+ *
19
+ * Mongo returns an index's options in ITS order, not the declaration's, so comparing the two
20
+ * documents as JSON reported every index as changed — and every boot then dropped and recreated
21
+ * every index. That is invisible until two processes boot at once, when the second one's drop
22
+ * fails with `IndexNotFound` and takes the service down with it. Only the KEY is order-sensitive
23
+ * (a compound index is its key order); options are a set.
24
+ */
25
+ export const indexChanged = (existing: Document, proposed: Document): boolean => {
26
+ // We do not recreate text indexes.
27
+ if ('weights' in existing) return false
28
+ if (JSON.stringify(existing.key) !== JSON.stringify(proposed.key)) return true
29
+
30
+ return JSON.stringify(options(existing)) !== JSON.stringify(options(proposed))
31
+ }
4
32
 
5
33
  export const updateIndexes = async (collection: Collection, resource: MongoResource<ResourceRecord>) => {
6
- if (resource.indexes != null) {
7
- await Promise.all(resource.indexes.map(
8
- async index => {
9
- const _index = (await collection.indexes()).find(_index => _index.name === index.name)
10
- if (_index != null) {
11
- const existing = { ..._index }
12
- if ('name' in existing) {
13
- delete existing.name
14
- }
15
- if ('v' in existing) {
16
- delete existing.v
17
- }
18
- const propsed = { key: index.index, ...index.options }
19
- // We do not recreate text indexes
20
- if (JSON.stringify(existing) !== JSON.stringify(propsed) && !('weights' in existing)) {
21
- console.info(JSON.stringify(existing))
22
- console.info(JSON.stringify(propsed))
23
- console.debug(`Recreate index ${resource.alias} : ${index.name}`)
24
- await collection.dropIndex(index.name)
25
- await collection.createIndex(index.index, {
26
- name: index.name, ...((index.options != null) ? index.options : {})
27
- })
28
- }
29
- } else {
30
- console.debug(`Create index ${resource.alias} : ${index.name}`)
31
- await collection.createIndex(index.index, {
32
- name: index.name, ...((index.options != null) ? index.options : {})
33
- })
34
- }
35
- }
36
- ))
34
+ if (resource.indexes == null) {
35
+ return
37
36
  }
37
+ const present = await collection.indexes()
38
+ await Promise.all(resource.indexes.map(
39
+ async index => {
40
+ const create = async () => await collection.createIndex(index.index, {
41
+ name: index.name, ...((index.options != null) ? index.options : {})
42
+ })
43
+ const existing = present.find(_index => _index.name === index.name)
44
+ if (existing == null) {
45
+ console.debug(`Create index ${resource.alias} : ${index.name}`)
46
+ await create()
47
+ return
48
+ }
49
+ if (!indexChanged(existing, { key: index.index, ...index.options })) {
50
+ return
51
+ }
52
+ console.debug(`Recreate index ${resource.alias} : ${index.name}`)
53
+ try {
54
+ await collection.dropIndex(index.name)
55
+ } catch (e) {
56
+ // IndexNotFound: another process booting alongside this one dropped it first — it is
57
+ // recreating the same declaration, so there is nothing here to fail over.
58
+ if ((e as { code?: number }).code !== 27) throw e
59
+ }
60
+ await create()
61
+ }
62
+ ))
38
63
  }
@@ -0,0 +1,40 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { indexChanged } from '../src/utils/indexes.js'
3
+
4
+ /**
5
+ * No gate: this is the pure decision behind `updateIndexes`, and the case that matters needs no
6
+ * database — an index the server reports back is compared against the declaration it came from.
7
+ */
8
+ describe('@owlmeans/mongo-resource — index comparison', () => {
9
+ const declared = { key: { currentJobId: 1 }, sparse: true, unique: true }
10
+
11
+ test('the same index reported in the server\'s own option order is unchanged', () => {
12
+ // What the server answers for exactly this declaration — options in its order, plus `v`.
13
+ expect(indexChanged({ v: 2, key: { currentJobId: 1 }, unique: true, sparse: true }, declared)).toBe(false)
14
+ })
15
+
16
+ test('fields the server reports rather than the declaration asks for are not a change', () => {
17
+ const reported = {
18
+ v: 2, key: { currentJobId: 1 }, unique: true, sparse: true, background: false,
19
+ collation: { locale: 'en', strength: 2 },
20
+ }
21
+ expect(indexChanged(reported, declared)).toBe(false)
22
+ })
23
+
24
+ test('a changed option, a dropped option and a changed key are changes', () => {
25
+ expect(indexChanged({ v: 2, key: { currentJobId: 1 }, unique: false, sparse: true }, declared)).toBe(true)
26
+ expect(indexChanged({ v: 2, key: { currentJobId: 1 }, unique: true }, declared)).toBe(true)
27
+ expect(indexChanged({ v: 2, key: { currentJobId: -1 }, unique: true, sparse: true }, declared)).toBe(true)
28
+ })
29
+
30
+ test('a compound index compares its key in order', () => {
31
+ const compound = { key: { entityId: 1, createdAt: -1 } }
32
+ expect(indexChanged({ v: 2, key: { entityId: 1, createdAt: -1 } }, compound)).toBe(false)
33
+ expect(indexChanged({ v: 2, key: { createdAt: -1, entityId: 1 } }, compound)).toBe(true)
34
+ })
35
+
36
+ test('a text index is never recreated', () => {
37
+ const text = { v: 2, key: { _fts: 'text', _ftsx: 1 }, weights: { title: 1 }, textIndexVersion: 3 }
38
+ expect(indexChanged(text, { key: { title: 'text' } })).toBe(false)
39
+ })
40
+ })