@owlmeans/mongo-resource 0.1.18-rc.2 → 0.1.18-rc.21

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.
Files changed (47) hide show
  1. package/README.md +232 -71
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/mongo-resource/SKILL.md +80 -30
  4. package/build/consts.d.ts +8 -4
  5. package/build/consts.d.ts.map +1 -1
  6. package/build/consts.js +8 -4
  7. package/build/consts.js.map +1 -1
  8. package/build/declarations.d.ts.map +1 -1
  9. package/build/declarations.js +5 -6
  10. package/build/declarations.js.map +1 -1
  11. package/build/index.d.ts +1 -0
  12. package/build/index.d.ts.map +1 -1
  13. package/build/index.js +1 -0
  14. package/build/index.js.map +1 -1
  15. package/build/resource.d.ts +2 -2
  16. package/build/resource.d.ts.map +1 -1
  17. package/build/resource.js +91 -118
  18. package/build/resource.js.map +1 -1
  19. package/build/types.d.ts +16 -5
  20. package/build/types.d.ts.map +1 -1
  21. package/build/utils/criteria.d.ts +24 -0
  22. package/build/utils/criteria.d.ts.map +1 -0
  23. package/build/utils/criteria.js +221 -0
  24. package/build/utils/criteria.js.map +1 -0
  25. package/build/utils/index.d.ts +1 -0
  26. package/build/utils/index.d.ts.map +1 -1
  27. package/build/utils/index.js +1 -0
  28. package/build/utils/index.js.map +1 -1
  29. package/build/utils/migrations.d.ts.map +1 -1
  30. package/build/utils/migrations.js +16 -1
  31. package/build/utils/migrations.js.map +1 -1
  32. package/build/utils/refs.d.ts +5 -3
  33. package/build/utils/refs.d.ts.map +1 -1
  34. package/build/utils/refs.js +4 -1
  35. package/build/utils/refs.js.map +1 -1
  36. package/package.json +6 -6
  37. package/src/consts.ts +8 -4
  38. package/src/declarations.ts +5 -6
  39. package/src/index.ts +1 -0
  40. package/src/resource.ts +115 -144
  41. package/src/types.ts +17 -5
  42. package/src/utils/criteria.ts +248 -0
  43. package/src/utils/index.ts +1 -0
  44. package/src/utils/migrations.ts +17 -2
  45. package/src/utils/refs.ts +8 -6
  46. package/tests/criteria.spec.ts +114 -0
  47. package/build/.gitkeep +0 -0
@@ -5,7 +5,8 @@ import type { Collection, Db } from 'mongodb'
5
5
  import {
6
6
  DEF_MIGRATION_POLL, DEF_MIGRATION_WAIT, DEF_MIGRATIONS_COLLECTION, MONGO_DUPLICATE_KEY
7
7
  } from '../consts.js'
8
- import type { MongoResource, MongoTx } from '../types.js'
8
+ import type { MongoDbService, MongoResource, MongoTx } from '../types.js'
9
+ import { DEFAULT_DB_ALIAS } from '../consts.js'
9
10
  import { mongoCollectionName } from './name.js'
10
11
 
11
12
  interface LedgerRecord {
@@ -40,7 +41,21 @@ export const makeMongoTx = (
40
41
  return collection.collectionName
41
42
  }
42
43
 
43
- return mongoCollectionName(config, context.resource<MongoResource<ResourceRecord>>(alias))
44
+ // Name the target's collection from the target's OWN db config. Resources registered under
45
+ // different db aliases can share one database while carrying different `resourcePrefix`es, and
46
+ // naming one of them with this migration's config silently addresses a collection that does not
47
+ // exist — reads come back empty and writes create a decoy beside the real thing.
48
+ const target = context.resource<MongoResource<ResourceRecord>>(alias)
49
+ let targetConfig = config
50
+ try {
51
+ const mongo = context.service<MongoDbService>(target.serviceAlias ?? target.dbAlias ?? DEFAULT_DB_ALIAS)
52
+ targetConfig = mongo.config(target.dbAlias ?? DEFAULT_DB_ALIAS) ?? config
53
+ } catch {
54
+ // A resource whose service is not reachable from here keeps the caller's config — the
55
+ // historical behaviour, and correct whenever both share one alias.
56
+ }
57
+
58
+ return mongoCollectionName(targetConfig, target)
44
59
  }
45
60
 
46
61
  return {
package/src/utils/refs.ts CHANGED
@@ -1,5 +1,4 @@
1
1
  import { MisshapedRecord } from '@owlmeans/resource'
2
- import type { ListCriteria } from '@owlmeans/resource'
3
2
  import { ObjectId } from 'mongodb'
4
3
  import type { Collection, Document } from 'mongodb'
5
4
 
@@ -97,7 +96,7 @@ const marshalCriteriaValue = (value: unknown): unknown => {
97
96
  }
98
97
 
99
98
  /**
100
- * Convert list criteria the way records are converted: values addressed at `_id` or at a
99
+ * Convert a mongo filter the way records are converted: values addressed at `_id` or at a
101
100
  * declared reference become `ObjectId`s, and the `id` alias records actually carry is
102
101
  * mapped onto `_id` — documents never store `id`, so before this mapping such criteria
103
102
  * silently matched nothing.
@@ -105,17 +104,20 @@ const marshalCriteriaValue = (value: unknown): unknown => {
105
104
  * Tolerant by design: a value that is not 24 hex passes through unconverted. Criteria are
106
105
  * matched against the collection, and against an `ObjectId` typed field a stray string
107
106
  * matches nothing — which is exactly what it matched before the field was converted.
107
+ *
108
+ * Operators arrive already in mongo's own vocabulary — {@link criteriaToFilter} translates
109
+ * the portable one first, so this pass only has to convert values.
108
110
  */
109
111
  export const marshalCriteria = (
110
- criteria: ListCriteria | undefined, refs: Map<string, MongoReference>
111
- ): ListCriteria | undefined => {
112
+ criteria: Document | undefined, refs: Map<string, MongoReference>
113
+ ): Document | undefined => {
112
114
  if (criteria == null) {
113
115
  return criteria
114
116
  }
115
117
 
116
118
  return Object.fromEntries(Object.entries(criteria).map(([key, value]) => {
117
119
  if (LOGICAL_OPERATORS.includes(key) && Array.isArray(value)) {
118
- return [key, value.map(sub => marshalCriteria(sub as ListCriteria, refs))]
120
+ return [key, value.map(sub => marshalCriteria(sub as Document, refs))]
119
121
  }
120
122
  if (key === 'id' || key === '_id') {
121
123
  return ['_id', marshalCriteriaValue(value)]
@@ -125,7 +127,7 @@ export const marshalCriteria = (
125
127
  }
126
128
 
127
129
  return [key, value]
128
- })) as ListCriteria
130
+ })) as Document
129
131
  }
130
132
 
131
133
  /**
@@ -0,0 +1,114 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { UnsupportedArgumentError } from '@owlmeans/resource'
3
+ import { ObjectId } from 'mongodb'
4
+
5
+ import { criteriaToFilter, sortToMongo } from '../src/utils/criteria.js'
6
+ import type { MongoReference } from '../src/types.js'
7
+
8
+ const HEX = '6712abcdef0123456789abcd'
9
+ const OTHER = '6712abcdef0123456789abce'
10
+
11
+ const refs = (...fields: string[]): Map<string, MongoReference> =>
12
+ new Map(fields.map(field => [field, { field }]))
13
+
14
+ describe('criteria translation', () => {
15
+ test('reads a bare value as equality, a bare array as membership and null as absence', () => {
16
+ expect(criteriaToFilter({ title: 'a' }, refs())).toEqual({ title: 'a' })
17
+ expect(criteriaToFilter({ title: ['a', 'b'] }, refs())).toEqual({ title: { $in: ['a', 'b'] } })
18
+ expect(criteriaToFilter({ title: null }, refs())).toEqual({ title: { $eq: null } })
19
+ })
20
+
21
+ test('skips an undefined value rather than demanding one', () => {
22
+ /** An untouched filter must never empty a list. */
23
+ expect(criteriaToFilter({ title: undefined }, refs())).toEqual({})
24
+ expect(criteriaToFilter(undefined, refs())).toEqual({})
25
+ expect(criteriaToFilter({ title: 'a', slug: undefined }, refs())).toEqual({ title: 'a' })
26
+ })
27
+
28
+ test('reads sibling fields as a conjunction', () => {
29
+ expect(criteriaToFilter({ title: 'a', slug: 'b' }, refs())).toEqual({ title: 'a', slug: 'b' })
30
+ })
31
+
32
+ test('passes mongo\'s own operators through and merges them per field', () => {
33
+ expect(criteriaToFilter({ age: { $gte: 1, $lt: 5 } }, refs())).toEqual({ age: { $gte: 1, $lt: 5 } })
34
+ expect(criteriaToFilter({ title: { $nin: ['a'] } }, refs())).toEqual({ title: { $nin: ['a'] } })
35
+ })
36
+
37
+ test('answers presence as a comparison against null, the way the other stores do', () => {
38
+ expect(criteriaToFilter({ slug: { $null: true } }, refs())).toEqual({ slug: { $eq: null } })
39
+ expect(criteriaToFilter({ slug: { $null: false } }, refs())).toEqual({ slug: { $ne: null } })
40
+ expect(criteriaToFilter({ slug: { $exists: true } }, refs())).toEqual({ slug: { $ne: null } })
41
+ expect(criteriaToFilter({ slug: { $exists: false } }, refs())).toEqual({ slug: { $eq: null } })
42
+ })
43
+
44
+ test('turns the text operators into anchored regular expressions', () => {
45
+ expect(criteriaToFilter({ title: { $like: 'ab_%' } }, refs()))
46
+ .toEqual({ title: { $regex: '^ab..*$' } })
47
+ expect(criteriaToFilter({ title: { $ilike: 'a%' } }, refs()))
48
+ .toEqual({ title: { $regex: '^a.*$', $options: 'i' } })
49
+ expect(criteriaToFilter({ title: { $startsWith: 'a.b' } }, refs()))
50
+ .toEqual({ title: { $regex: '^a\\.b' } })
51
+ expect(criteriaToFilter({ title: { $endsWith: 'a.b' } }, refs()))
52
+ .toEqual({ title: { $regex: 'a\\.b$' } })
53
+ })
54
+
55
+ test('splits two operators that would claim the same mongo key', () => {
56
+ expect(criteriaToFilter({ title: { $startsWith: 'a', $endsWith: 'z' } }, refs()))
57
+ .toEqual({ title: { $regex: '^a' }, $and: [{ title: { $regex: 'z$' } }] })
58
+ })
59
+
60
+ test('turns a range into its bounds and refuses a malformed one', () => {
61
+ expect(criteriaToFilter({ age: { $between: [1, 5] } }, refs()))
62
+ .toEqual({ age: { $gte: 1, $lte: 5 } })
63
+ expect(() => criteriaToFilter({ age: { $between: [1] } }, refs()))
64
+ .toThrow(UnsupportedArgumentError)
65
+ })
66
+
67
+ test('maps the array operators onto their mongo counterparts', () => {
68
+ expect(criteriaToFilter({ tags: { $contains: ['a', 'b'] } }, refs()))
69
+ .toEqual({ tags: { $all: ['a', 'b'] } })
70
+ expect(criteriaToFilter({ tags: { $overlaps: ['a', 'b'] } }, refs()))
71
+ .toEqual({ tags: { $in: ['a', 'b'] } })
72
+ expect(criteriaToFilter({ tags: { $contained: ['a', 'b'] } }, refs()))
73
+ .toEqual({ tags: { $not: { $elemMatch: { $nin: ['a', 'b'] } }, $ne: null } })
74
+ })
75
+
76
+ test('carries the logical branches over, negating through $nor', () => {
77
+ expect(criteriaToFilter({ $or: [{ title: 'a' }, { title: 'b' }] }, refs()))
78
+ .toEqual({ $or: [{ title: 'a' }, { title: 'b' }] })
79
+ expect(criteriaToFilter({ $and: [{ title: 'a', slug: 'b' }] }, refs()))
80
+ .toEqual({ $and: [{ title: 'a', slug: 'b' }] })
81
+ /** Mongo has no top level `$not`. */
82
+ expect(criteriaToFilter({ $not: { title: 'a' } }, refs()))
83
+ .toEqual({ $nor: [{ title: 'a' }] })
84
+ })
85
+
86
+ test('refuses an operator this store cannot answer', () => {
87
+ expect(() => criteriaToFilter({ title: { $nope: 1 } }, refs())).toThrow(UnsupportedArgumentError)
88
+ })
89
+
90
+ test('converts ids and declared references on the way through', () => {
91
+ expect(criteriaToFilter({ id: HEX }, refs())).toEqual({ _id: new ObjectId(HEX) })
92
+ expect(criteriaToFilter({ ownerId: [HEX, OTHER] }, refs('ownerId')))
93
+ .toEqual({ ownerId: { $in: [new ObjectId(HEX), new ObjectId(OTHER)] } })
94
+ expect(criteriaToFilter({ ownerIds: { $contains: [HEX] } }, refs('ownerIds')))
95
+ .toEqual({ ownerIds: { $all: [new ObjectId(HEX)] } })
96
+ /** Not a mongo id — probes tolerantly, matching nothing rather than throwing. */
97
+ expect(criteriaToFilter({ ownerId: 'ext:key' }, refs('ownerId'))).toEqual({ ownerId: 'ext:key' })
98
+ /** A 24-hex value on a field that was never declared a reference stays a string. */
99
+ expect(criteriaToFilter({ title: HEX }, refs('ownerId'))).toEqual({ title: HEX })
100
+ })
101
+ })
102
+
103
+ describe('sort translation', () => {
104
+ test('reads a bare field as ascending and maps id onto _id', () => {
105
+ expect(sortToMongo(['createdAt', { field: 'title', order: 'desc' }, 'id']))
106
+ .toEqual({ createdAt: 1, title: -1, _id: 1 })
107
+ expect(sortToMongo([{ field: 'title' }])).toEqual({ title: 1 })
108
+ })
109
+
110
+ test('answers nothing when nothing is asked for', () => {
111
+ expect(sortToMongo()).toBeUndefined()
112
+ expect(sortToMongo([])).toBeUndefined()
113
+ })
114
+ })
package/build/.gitkeep DELETED
File without changes