@owlmeans/mongo-resource 0.1.18-rc.32 → 0.1.18-rc.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/mongo-resource/SKILL.md +9 -4
- package/build/utils/indexes.d.ts +11 -1
- package/build/utils/indexes.d.ts.map +1 -1
- package/build/utils/indexes.js +54 -30
- package/build/utils/indexes.js.map +1 -1
- package/package.json +5 -5
- package/src/utils/indexes.ts +57 -32
- package/tests/indexes.spec.ts +40 -0
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.
|
|
15
|
+
bun add @owlmeans/mongo-resource@^0.1.18-rc.34
|
|
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.
|
|
297
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.35
|
|
298
298
|
```
|
|
299
299
|
|
|
300
300
|
The embedded files are version-matched to this package release. Do not edit them
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/mongo-resource",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-09-
|
|
4
|
+
"version": "0.1.18-rc.34",
|
|
5
|
+
"generatedAt": "2026-09-22T21:58:46.774Z",
|
|
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.
|
|
11
|
+
**Install:** `"@owlmeans/mongo-resource": "^0.1.18-rc.34"` 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
|
|
137
|
-
pattern or options differ from the declaration is dropped and created again, so a renamed
|
|
138
|
-
does converge — after `Pre`, which is why the bullet above exists.
|
|
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**
|
package/build/utils/indexes.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/build/utils/indexes.js
CHANGED
|
@@ -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
|
|
3
|
-
|
|
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,
|
|
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.
|
|
3
|
+
"version": "0.1.18-rc.34",
|
|
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.
|
|
30
|
-
"@owlmeans/resource": "^0.1.18-rc.
|
|
31
|
-
"@owlmeans/server-context": "^0.1.18-rc.
|
|
29
|
+
"@owlmeans/context": "^0.1.18-rc.31",
|
|
30
|
+
"@owlmeans/resource": "^0.1.18-rc.33",
|
|
31
|
+
"@owlmeans/server-context": "^0.1.18-rc.34"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@owlmeans/dep-config": "workspace:*",
|
|
35
|
-
"@owlmeans/test-integration": "^0.1.18-rc.
|
|
35
|
+
"@owlmeans/test-integration": "^0.1.18-rc.28",
|
|
36
36
|
"@types/bun": "^1.4.0",
|
|
37
37
|
"@types/node": "^26.1.0",
|
|
38
38
|
"mongodb": "^7.5.0",
|
package/src/utils/indexes.ts
CHANGED
|
@@ -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
|
|
7
|
-
|
|
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
|
+
})
|