@owlmeans/mongo-resource 0.1.18-rc.8 → 0.1.18
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 +232 -71
- package/agent-meta/manifest.json +3 -3
- package/agent-meta/skills/mongo-resource/SKILL.md +89 -44
- package/build/consts.d.ts +8 -4
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js +8 -4
- package/build/consts.js.map +1 -1
- package/build/declarations/types.d.ts +12 -0
- package/build/declarations/types.d.ts.map +1 -0
- package/build/declarations/types.js +2 -0
- package/build/declarations/types.js.map +1 -0
- package/build/declarations.d.ts +4 -9
- package/build/declarations.d.ts.map +1 -1
- package/build/declarations.js +28 -21
- package/build/declarations.js.map +1 -1
- package/build/index.d.ts +4 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -0
- package/build/index.js.map +1 -1
- package/build/resource.d.ts +2 -2
- package/build/resource.d.ts.map +1 -1
- package/build/resource.js +100 -130
- package/build/resource.js.map +1 -1
- package/build/types.d.ts +11 -5
- package/build/types.d.ts.map +1 -1
- package/build/types.local.d.ts +4 -0
- package/build/types.local.d.ts.map +1 -0
- package/build/types.local.js +2 -0
- package/build/types.local.js.map +1 -0
- package/build/utils/consts.local.d.ts +25 -0
- package/build/utils/consts.local.d.ts.map +1 -0
- package/build/utils/consts.local.js +31 -0
- package/build/utils/consts.local.js.map +1 -0
- package/build/utils/criteria/types.d.ts +27 -0
- package/build/utils/criteria/types.d.ts.map +1 -0
- package/build/utils/criteria/types.js +2 -0
- package/build/utils/criteria/types.js.map +1 -0
- package/build/utils/criteria.d.ts +9 -0
- package/build/utils/criteria.d.ts.map +1 -0
- package/build/utils/criteria.js +204 -0
- package/build/utils/criteria.js.map +1 -0
- package/build/utils/index.d.ts +1 -0
- package/build/utils/index.d.ts.map +1 -1
- package/build/utils/index.js +1 -0
- package/build/utils/index.js.map +1 -1
- package/build/utils/indexes/types.d.ts +19 -0
- package/build/utils/indexes/types.d.ts.map +1 -0
- package/build/utils/indexes/types.js +2 -0
- package/build/utils/indexes/types.js.map +1 -0
- package/build/utils/indexes.d.ts +3 -4
- package/build/utils/indexes.d.ts.map +1 -1
- package/build/utils/indexes.js +44 -28
- package/build/utils/indexes.js.map +1 -1
- package/build/utils/life-cycle/types.d.ts +36 -0
- package/build/utils/life-cycle/types.d.ts.map +1 -0
- package/build/utils/life-cycle/types.js +2 -0
- package/build/utils/life-cycle/types.js.map +1 -0
- package/build/utils/life-cycle.d.ts +3 -30
- package/build/utils/life-cycle.d.ts.map +1 -1
- package/build/utils/life-cycle.js +96 -112
- package/build/utils/life-cycle.js.map +1 -1
- package/build/utils/migrations.d.ts.map +1 -1
- package/build/utils/migrations.js +1 -2
- package/build/utils/migrations.js.map +1 -1
- package/build/utils/name.d.ts.map +1 -1
- package/build/utils/name.js +1 -5
- package/build/utils/name.js.map +1 -1
- package/build/utils/refs/types.d.ts +78 -0
- package/build/utils/refs/types.d.ts.map +1 -0
- package/build/utils/refs/types.js +2 -0
- package/build/utils/refs/types.js.map +1 -0
- package/build/utils/refs.d.ts +13 -67
- package/build/utils/refs.d.ts.map +1 -1
- package/build/utils/refs.js +115 -158
- package/build/utils/refs.js.map +1 -1
- package/build/utils/schema/types.d.ts +16 -0
- package/build/utils/schema/types.d.ts.map +1 -0
- package/build/utils/schema/types.js +2 -0
- package/build/utils/schema/types.js.map +1 -0
- package/build/utils/schema.d.ts +3 -11
- package/build/utils/schema.d.ts.map +1 -1
- package/build/utils/schema.js +86 -88
- package/build/utils/schema.js.map +1 -1
- package/build/utils/types.local.d.ts +13 -0
- package/build/utils/types.local.d.ts.map +1 -0
- package/build/utils/types.local.js +2 -0
- package/build/utils/types.local.js.map +1 -0
- package/package.json +10 -9
- package/src/consts.ts +8 -4
- package/src/declarations/types.ts +12 -0
- package/src/declarations.ts +32 -28
- package/src/index.ts +4 -0
- package/src/resource.ts +121 -163
- package/src/types.local.ts +5 -0
- package/src/types.ts +13 -7
- package/src/utils/consts.local.ts +35 -0
- package/src/utils/criteria/types.ts +28 -0
- package/src/utils/criteria.ts +235 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/indexes/types.ts +20 -0
- package/src/utils/indexes.ts +48 -28
- package/src/utils/life-cycle/types.ts +39 -0
- package/src/utils/life-cycle.ts +120 -134
- package/src/utils/migrations.ts +3 -17
- package/src/utils/name.ts +1 -6
- package/src/utils/refs/types.ts +79 -0
- package/src/utils/refs.ts +147 -189
- package/src/utils/schema/types.ts +17 -0
- package/src/utils/schema.ts +92 -91
- package/src/utils/types.local.ts +13 -0
- package/tests/context.ts +3 -3
- package/tests/criteria.spec.ts +116 -0
- package/tests/crud.spec.ts +2 -2
- package/tests/indexes.spec.ts +42 -0
- package/tests/refs.spec.ts +5 -3
- package/build/.gitkeep +0 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { UnsupportedArgumentError } from '@owlmeans/resource'
|
|
3
|
+
import { ObjectId } from 'mongodb'
|
|
4
|
+
|
|
5
|
+
import { mongoCriteriaHelper } from '../src/utils/criteria.js'
|
|
6
|
+
import type { MongoReference } from '../src/types.js'
|
|
7
|
+
|
|
8
|
+
const { criteriaToFilter, sortToMongo } = mongoCriteriaHelper
|
|
9
|
+
|
|
10
|
+
const HEX = '6712abcdef0123456789abcd'
|
|
11
|
+
const OTHER = '6712abcdef0123456789abce'
|
|
12
|
+
|
|
13
|
+
const refs = (...fields: string[]): Map<string, MongoReference> =>
|
|
14
|
+
new Map(fields.map(field => [field, { field }]))
|
|
15
|
+
|
|
16
|
+
describe('criteria translation', () => {
|
|
17
|
+
test('reads a bare value as equality, a bare array as membership and null as absence', () => {
|
|
18
|
+
expect(criteriaToFilter({ title: 'a' }, refs())).toEqual({ title: 'a' })
|
|
19
|
+
expect(criteriaToFilter({ title: ['a', 'b'] }, refs())).toEqual({ title: { $in: ['a', 'b'] } })
|
|
20
|
+
expect(criteriaToFilter({ title: null }, refs())).toEqual({ title: { $eq: null } })
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('skips an undefined value rather than demanding one', () => {
|
|
24
|
+
/** An untouched filter must never empty a list. */
|
|
25
|
+
expect(criteriaToFilter({ title: undefined }, refs())).toEqual({})
|
|
26
|
+
expect(criteriaToFilter(undefined, refs())).toEqual({})
|
|
27
|
+
expect(criteriaToFilter({ title: 'a', slug: undefined }, refs())).toEqual({ title: 'a' })
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
test('reads sibling fields as a conjunction', () => {
|
|
31
|
+
expect(criteriaToFilter({ title: 'a', slug: 'b' }, refs())).toEqual({ title: 'a', slug: 'b' })
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('passes mongo\'s own operators through and merges them per field', () => {
|
|
35
|
+
expect(criteriaToFilter({ age: { $gte: 1, $lt: 5 } }, refs())).toEqual({ age: { $gte: 1, $lt: 5 } })
|
|
36
|
+
expect(criteriaToFilter({ title: { $nin: ['a'] } }, refs())).toEqual({ title: { $nin: ['a'] } })
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
test('answers presence as a comparison against null, the way the other stores do', () => {
|
|
40
|
+
expect(criteriaToFilter({ slug: { $null: true } }, refs())).toEqual({ slug: { $eq: null } })
|
|
41
|
+
expect(criteriaToFilter({ slug: { $null: false } }, refs())).toEqual({ slug: { $ne: null } })
|
|
42
|
+
expect(criteriaToFilter({ slug: { $exists: true } }, refs())).toEqual({ slug: { $ne: null } })
|
|
43
|
+
expect(criteriaToFilter({ slug: { $exists: false } }, refs())).toEqual({ slug: { $eq: null } })
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('turns the text operators into anchored regular expressions', () => {
|
|
47
|
+
expect(criteriaToFilter({ title: { $like: 'ab_%' } }, refs()))
|
|
48
|
+
.toEqual({ title: { $regex: '^ab..*$' } })
|
|
49
|
+
expect(criteriaToFilter({ title: { $ilike: 'a%' } }, refs()))
|
|
50
|
+
.toEqual({ title: { $regex: '^a.*$', $options: 'i' } })
|
|
51
|
+
expect(criteriaToFilter({ title: { $startsWith: 'a.b' } }, refs()))
|
|
52
|
+
.toEqual({ title: { $regex: '^a\\.b' } })
|
|
53
|
+
expect(criteriaToFilter({ title: { $endsWith: 'a.b' } }, refs()))
|
|
54
|
+
.toEqual({ title: { $regex: 'a\\.b$' } })
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
test('splits two operators that would claim the same mongo key', () => {
|
|
58
|
+
expect(criteriaToFilter({ title: { $startsWith: 'a', $endsWith: 'z' } }, refs()))
|
|
59
|
+
.toEqual({ title: { $regex: '^a' }, $and: [{ title: { $regex: 'z$' } }] })
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('turns a range into its bounds and refuses a malformed one', () => {
|
|
63
|
+
expect(criteriaToFilter({ age: { $between: [1, 5] } }, refs()))
|
|
64
|
+
.toEqual({ age: { $gte: 1, $lte: 5 } })
|
|
65
|
+
expect(() => criteriaToFilter({ age: { $between: [1] } }, refs()))
|
|
66
|
+
.toThrow(UnsupportedArgumentError)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('maps the array operators onto their mongo counterparts', () => {
|
|
70
|
+
expect(criteriaToFilter({ tags: { $contains: ['a', 'b'] } }, refs()))
|
|
71
|
+
.toEqual({ tags: { $all: ['a', 'b'] } })
|
|
72
|
+
expect(criteriaToFilter({ tags: { $overlaps: ['a', 'b'] } }, refs()))
|
|
73
|
+
.toEqual({ tags: { $in: ['a', 'b'] } })
|
|
74
|
+
expect(criteriaToFilter({ tags: { $contained: ['a', 'b'] } }, refs()))
|
|
75
|
+
.toEqual({ tags: { $not: { $elemMatch: { $nin: ['a', 'b'] } }, $ne: null } })
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test('carries the logical branches over, negating through $nor', () => {
|
|
79
|
+
expect(criteriaToFilter({ $or: [{ title: 'a' }, { title: 'b' }] }, refs()))
|
|
80
|
+
.toEqual({ $or: [{ title: 'a' }, { title: 'b' }] })
|
|
81
|
+
expect(criteriaToFilter({ $and: [{ title: 'a', slug: 'b' }] }, refs()))
|
|
82
|
+
.toEqual({ $and: [{ title: 'a', slug: 'b' }] })
|
|
83
|
+
/** Mongo has no top level `$not`. */
|
|
84
|
+
expect(criteriaToFilter({ $not: { title: 'a' } }, refs()))
|
|
85
|
+
.toEqual({ $nor: [{ title: 'a' }] })
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
test('refuses an operator this store cannot answer', () => {
|
|
89
|
+
expect(() => criteriaToFilter({ title: { $nope: 1 } }, refs())).toThrow(UnsupportedArgumentError)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('converts ids and declared references on the way through', () => {
|
|
93
|
+
expect(criteriaToFilter({ id: HEX }, refs())).toEqual({ _id: new ObjectId(HEX) })
|
|
94
|
+
expect(criteriaToFilter({ ownerId: [HEX, OTHER] }, refs('ownerId')))
|
|
95
|
+
.toEqual({ ownerId: { $in: [new ObjectId(HEX), new ObjectId(OTHER)] } })
|
|
96
|
+
expect(criteriaToFilter({ ownerIds: { $contains: [HEX] } }, refs('ownerIds')))
|
|
97
|
+
.toEqual({ ownerIds: { $all: [new ObjectId(HEX)] } })
|
|
98
|
+
/** Not a mongo id — probes tolerantly, matching nothing rather than throwing. */
|
|
99
|
+
expect(criteriaToFilter({ ownerId: 'ext:key' }, refs('ownerId'))).toEqual({ ownerId: 'ext:key' })
|
|
100
|
+
/** A 24-hex value on a field that was never declared a reference stays a string. */
|
|
101
|
+
expect(criteriaToFilter({ title: HEX }, refs('ownerId'))).toEqual({ title: HEX })
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe('sort translation', () => {
|
|
106
|
+
test('reads a bare field as ascending and maps id onto _id', () => {
|
|
107
|
+
expect(sortToMongo(['createdAt', { field: 'title', order: 'desc' }, 'id']))
|
|
108
|
+
.toEqual({ createdAt: 1, title: -1, _id: 1 })
|
|
109
|
+
expect(sortToMongo([{ field: 'title' }])).toEqual({ title: 1 })
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('answers nothing when nothing is asked for', () => {
|
|
113
|
+
expect(sortToMongo()).toBeUndefined()
|
|
114
|
+
expect(sortToMongo([])).toBeUndefined()
|
|
115
|
+
})
|
|
116
|
+
})
|
package/tests/crud.spec.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { afterAll, describe, expect, test } from 'bun:test'
|
|
2
|
-
import { runCleanups } from '@owlmeans/test-integration'
|
|
3
2
|
import { getTestEnv } from './context.js'
|
|
3
|
+
import { cleanupHelper } from '@owlmeans/test-integration'
|
|
4
4
|
|
|
5
5
|
const env = getTestEnv()
|
|
6
6
|
const it = env.gate.skip ? test.skip : test
|
|
7
7
|
|
|
8
8
|
afterAll(async () => {
|
|
9
|
-
await runCleanups()
|
|
9
|
+
await cleanupHelper.runCleanups()
|
|
10
10
|
})
|
|
11
11
|
|
|
12
12
|
describe('@owlmeans/mongo-resource — connection round-trip', () => {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { mongoIndexUtils } from '../src/utils/indexes.js'
|
|
3
|
+
|
|
4
|
+
const { indexChanged } = mongoIndexUtils
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* No gate: this is the pure decision behind `updateIndexes`, and the case that matters needs no
|
|
8
|
+
* database — an index the server reports back is compared against the declaration it came from.
|
|
9
|
+
*/
|
|
10
|
+
describe('@owlmeans/mongo-resource — index comparison', () => {
|
|
11
|
+
const declared = { key: { currentJobId: 1 }, sparse: true, unique: true }
|
|
12
|
+
|
|
13
|
+
test('the same index reported in the server\'s own option order is unchanged', () => {
|
|
14
|
+
// What the server answers for exactly this declaration — options in its order, plus `v`.
|
|
15
|
+
expect(indexChanged({ v: 2, key: { currentJobId: 1 }, unique: true, sparse: true }, declared)).toBe(false)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
test('fields the server reports rather than the declaration asks for are not a change', () => {
|
|
19
|
+
const reported = {
|
|
20
|
+
v: 2, key: { currentJobId: 1 }, unique: true, sparse: true, background: false,
|
|
21
|
+
collation: { locale: 'en', strength: 2 },
|
|
22
|
+
}
|
|
23
|
+
expect(indexChanged(reported, declared)).toBe(false)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
test('a changed option, a dropped option and a changed key are changes', () => {
|
|
27
|
+
expect(indexChanged({ v: 2, key: { currentJobId: 1 }, unique: false, sparse: true }, declared)).toBe(true)
|
|
28
|
+
expect(indexChanged({ v: 2, key: { currentJobId: 1 }, unique: true }, declared)).toBe(true)
|
|
29
|
+
expect(indexChanged({ v: 2, key: { currentJobId: -1 }, unique: true, sparse: true }, declared)).toBe(true)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('a compound index compares its key in order', () => {
|
|
33
|
+
const compound = { key: { entityId: 1, createdAt: -1 } }
|
|
34
|
+
expect(indexChanged({ v: 2, key: { entityId: 1, createdAt: -1 } }, compound)).toBe(false)
|
|
35
|
+
expect(indexChanged({ v: 2, key: { createdAt: -1, entityId: 1 } }, compound)).toBe(true)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('a text index is never recreated', () => {
|
|
39
|
+
const text = { v: 2, key: { _fts: 'text', _ftsx: 1 }, weights: { title: 1 }, textIndexVersion: 3 }
|
|
40
|
+
expect(indexChanged(text, { key: { title: 'text' } })).toBe(false)
|
|
41
|
+
})
|
|
42
|
+
})
|
package/tests/refs.spec.ts
CHANGED
|
@@ -2,11 +2,13 @@ import { describe, expect, test } from 'bun:test'
|
|
|
2
2
|
import { MisshapedRecord } from '@owlmeans/resource'
|
|
3
3
|
import { ObjectId } from 'mongodb'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { mongoRefHelper } from '../src/utils/refs.js'
|
|
6
|
+
import type { MongoReference } from '../src/types.js'
|
|
7
|
+
|
|
8
|
+
const {
|
|
6
9
|
demarshalReference, demarshalRefs, identityCriteria, isObjectIdHex, marshalCriteria,
|
|
7
10
|
marshalReference, refMigrationName
|
|
8
|
-
}
|
|
9
|
-
import type { MongoReference } from '../src/types.js'
|
|
11
|
+
} = mongoRefHelper
|
|
10
12
|
|
|
11
13
|
const HEX = '6712abcdef0123456789abcd'
|
|
12
14
|
const OTHER = '6712abcdef0123456789abce'
|
package/build/.gitkeep
DELETED
|
File without changes
|