@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.
Files changed (116) hide show
  1. package/README.md +232 -71
  2. package/agent-meta/manifest.json +3 -3
  3. package/agent-meta/skills/mongo-resource/SKILL.md +89 -44
  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/types.d.ts +12 -0
  9. package/build/declarations/types.d.ts.map +1 -0
  10. package/build/declarations/types.js +2 -0
  11. package/build/declarations/types.js.map +1 -0
  12. package/build/declarations.d.ts +4 -9
  13. package/build/declarations.d.ts.map +1 -1
  14. package/build/declarations.js +28 -21
  15. package/build/declarations.js.map +1 -1
  16. package/build/index.d.ts +4 -0
  17. package/build/index.d.ts.map +1 -1
  18. package/build/index.js +1 -0
  19. package/build/index.js.map +1 -1
  20. package/build/resource.d.ts +2 -2
  21. package/build/resource.d.ts.map +1 -1
  22. package/build/resource.js +100 -130
  23. package/build/resource.js.map +1 -1
  24. package/build/types.d.ts +11 -5
  25. package/build/types.d.ts.map +1 -1
  26. package/build/types.local.d.ts +4 -0
  27. package/build/types.local.d.ts.map +1 -0
  28. package/build/types.local.js +2 -0
  29. package/build/types.local.js.map +1 -0
  30. package/build/utils/consts.local.d.ts +25 -0
  31. package/build/utils/consts.local.d.ts.map +1 -0
  32. package/build/utils/consts.local.js +31 -0
  33. package/build/utils/consts.local.js.map +1 -0
  34. package/build/utils/criteria/types.d.ts +27 -0
  35. package/build/utils/criteria/types.d.ts.map +1 -0
  36. package/build/utils/criteria/types.js +2 -0
  37. package/build/utils/criteria/types.js.map +1 -0
  38. package/build/utils/criteria.d.ts +9 -0
  39. package/build/utils/criteria.d.ts.map +1 -0
  40. package/build/utils/criteria.js +204 -0
  41. package/build/utils/criteria.js.map +1 -0
  42. package/build/utils/index.d.ts +1 -0
  43. package/build/utils/index.d.ts.map +1 -1
  44. package/build/utils/index.js +1 -0
  45. package/build/utils/index.js.map +1 -1
  46. package/build/utils/indexes/types.d.ts +19 -0
  47. package/build/utils/indexes/types.d.ts.map +1 -0
  48. package/build/utils/indexes/types.js +2 -0
  49. package/build/utils/indexes/types.js.map +1 -0
  50. package/build/utils/indexes.d.ts +3 -4
  51. package/build/utils/indexes.d.ts.map +1 -1
  52. package/build/utils/indexes.js +44 -28
  53. package/build/utils/indexes.js.map +1 -1
  54. package/build/utils/life-cycle/types.d.ts +36 -0
  55. package/build/utils/life-cycle/types.d.ts.map +1 -0
  56. package/build/utils/life-cycle/types.js +2 -0
  57. package/build/utils/life-cycle/types.js.map +1 -0
  58. package/build/utils/life-cycle.d.ts +3 -30
  59. package/build/utils/life-cycle.d.ts.map +1 -1
  60. package/build/utils/life-cycle.js +96 -112
  61. package/build/utils/life-cycle.js.map +1 -1
  62. package/build/utils/migrations.d.ts.map +1 -1
  63. package/build/utils/migrations.js +1 -2
  64. package/build/utils/migrations.js.map +1 -1
  65. package/build/utils/name.d.ts.map +1 -1
  66. package/build/utils/name.js +1 -5
  67. package/build/utils/name.js.map +1 -1
  68. package/build/utils/refs/types.d.ts +78 -0
  69. package/build/utils/refs/types.d.ts.map +1 -0
  70. package/build/utils/refs/types.js +2 -0
  71. package/build/utils/refs/types.js.map +1 -0
  72. package/build/utils/refs.d.ts +13 -67
  73. package/build/utils/refs.d.ts.map +1 -1
  74. package/build/utils/refs.js +115 -158
  75. package/build/utils/refs.js.map +1 -1
  76. package/build/utils/schema/types.d.ts +16 -0
  77. package/build/utils/schema/types.d.ts.map +1 -0
  78. package/build/utils/schema/types.js +2 -0
  79. package/build/utils/schema/types.js.map +1 -0
  80. package/build/utils/schema.d.ts +3 -11
  81. package/build/utils/schema.d.ts.map +1 -1
  82. package/build/utils/schema.js +86 -88
  83. package/build/utils/schema.js.map +1 -1
  84. package/build/utils/types.local.d.ts +13 -0
  85. package/build/utils/types.local.d.ts.map +1 -0
  86. package/build/utils/types.local.js +2 -0
  87. package/build/utils/types.local.js.map +1 -0
  88. package/package.json +10 -9
  89. package/src/consts.ts +8 -4
  90. package/src/declarations/types.ts +12 -0
  91. package/src/declarations.ts +32 -28
  92. package/src/index.ts +4 -0
  93. package/src/resource.ts +121 -163
  94. package/src/types.local.ts +5 -0
  95. package/src/types.ts +13 -7
  96. package/src/utils/consts.local.ts +35 -0
  97. package/src/utils/criteria/types.ts +28 -0
  98. package/src/utils/criteria.ts +235 -0
  99. package/src/utils/index.ts +1 -0
  100. package/src/utils/indexes/types.ts +20 -0
  101. package/src/utils/indexes.ts +48 -28
  102. package/src/utils/life-cycle/types.ts +39 -0
  103. package/src/utils/life-cycle.ts +120 -134
  104. package/src/utils/migrations.ts +3 -17
  105. package/src/utils/name.ts +1 -6
  106. package/src/utils/refs/types.ts +79 -0
  107. package/src/utils/refs.ts +147 -189
  108. package/src/utils/schema/types.ts +17 -0
  109. package/src/utils/schema.ts +92 -91
  110. package/src/utils/types.local.ts +13 -0
  111. package/tests/context.ts +3 -3
  112. package/tests/criteria.spec.ts +116 -0
  113. package/tests/crud.spec.ts +2 -2
  114. package/tests/indexes.spec.ts +42 -0
  115. package/tests/refs.spec.ts +5 -3
  116. package/build/.gitkeep +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/mongo-resource",
3
- "version": "0.1.18-rc.8",
3
+ "version": "0.1.18",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -26,16 +26,17 @@
26
26
  "mongodb": "*"
27
27
  },
28
28
  "dependencies": {
29
- "@owlmeans/context": "^0.1.18-rc.6",
30
- "@owlmeans/resource": "^0.1.18-rc.7",
31
- "@owlmeans/server-context": "^0.1.18-rc.8"
29
+ "@owlmeans/context": "^0.1.18",
30
+ "@owlmeans/log": "^0.1.18",
31
+ "@owlmeans/resource": "^0.1.18",
32
+ "@owlmeans/server-context": "^0.1.18"
32
33
  },
33
34
  "devDependencies": {
34
- "@owlmeans/dep-config": "workspace:*",
35
- "@owlmeans/test-integration": "^0.1.18-rc.6",
36
- "@types/bun": "^1.4.0",
37
- "@types/node": "^26.1.0",
38
- "mongodb": "^7.5.0",
35
+ "@owlmeans/dep-config": "^0.1.18",
36
+ "@owlmeans/test-integration": "^0.1.18",
37
+ "@types/bun": "^1.4.2",
38
+ "@types/node": "^26.6.4",
39
+ "mongodb": "^7.7.0",
39
40
  "nodemon": "^3.1.14",
40
41
  "typescript": "^7.0.2"
41
42
  },
package/src/consts.ts CHANGED
@@ -1,14 +1,18 @@
1
1
 
2
2
  export const DEFAULT_DB_ALIAS = 'mongo'
3
3
 
4
- export const DEFAULT_PAGE_SIZE = 10
4
+ /**
5
+ * Page size a `list` call gets when it asks for none. Mongo cannot afford an unbounded read
6
+ * of a collection by omission, so paging is the default here — `list(where, { size: 0 })`
7
+ * asks for the whole result set, explicitly and greppably.
8
+ */
9
+ export const DEFAULT_PAGE_SIZE = 100
5
10
 
6
11
  /**
7
12
  * Collection that records which code-registered migrations have already been applied.
8
13
  *
9
- * One ledger per database, which is the right boundary: `dbName()` already varies the
10
- * database per Entity/User layer, so a tenant's migrations are tracked with the tenant's
11
- * data and dropping the database drops the ledger with it.
14
+ * One ledger per database, which is the right boundary: it sits beside the data its
15
+ * migrations changed, so dropping the database drops the ledger with it.
12
16
  */
13
17
  export const DEF_MIGRATIONS_COLLECTION = '_owlmeans_migrations'
14
18
 
@@ -0,0 +1,12 @@
1
+ import type { MongoDeclaration } from '../types.js'
2
+
3
+ /**
4
+ * The per-alias declaration store every mongo resource maker reads and extends — migrations and
5
+ * declared references, keyed by resource alias.
6
+ */
7
+ export interface MongoDeclarationHelper {
8
+ /** The declaration of an alias, created empty on first use. */
9
+ getDeclaration: (alias: string) => MongoDeclaration
10
+ /** Testing seam — drops every declaration so a spec can redeclare a resource from scratch. */
11
+ resetDeclarations: (alias?: string) => void
12
+ }
@@ -1,42 +1,46 @@
1
1
  import { createMigrationRegistry } from '@owlmeans/resource'
2
- import type { MigrationRegistry } from '@owlmeans/resource'
3
2
 
4
- import type { MongoReference, MongoTx } from './types.js'
5
-
6
- export interface MongoDeclaration {
7
- migrations: MigrationRegistry<MongoTx>
8
- /** Declared ObjectId references, keyed by field. Registered via `resource.reference()`. */
9
- references: Map<string, MongoReference>
10
- }
3
+ import type { MongoDeclarationHelper } from './declarations/types.js'
4
+ import type { MongoTx, MongoDeclaration } from './types.js'
11
5
 
12
6
  /**
13
7
  * Per-alias migration store, held at module scope rather than on the resource object.
14
8
  *
15
- * `reinitializeContext` rebuilds every resource from the maker, which drops anything a
16
- * caller attached by chaining afterwards. For indexes that is survivable — they already
17
- * exist in the database and `updateIndexes` only ever adds. Migrations are not: a layer
18
- * switch points the resource at a *different* database, and a registry emptied by the
19
- * rebuild would mean the entity database silently never gets the transformation. Keying
20
- * by alias makes the declarations outlive any number of context switches.
9
+ * A maker may run more than once for the same alias — a custom maker wrapping the built-in
10
+ * one, a maker called again by an app or a spec. Keying the declarations by alias makes
11
+ * that a no-op: every run reads and extends the same registry, so nothing a caller declared
12
+ * by chaining onto an earlier resource object is lost. Losing a migration is silent — the
13
+ * data transformation simply never runs — which is why the store cannot live on the object.
14
+ *
15
+ * Process-wide for the same reason: it stays outside the factory, so every helper built from it
16
+ * reads the one store.
21
17
  */
22
18
  const declarations: Map<string, MongoDeclaration> = new Map()
23
19
 
24
- export const getDeclaration = (alias: string): MongoDeclaration => {
25
- let declaration = declarations.get(alias)
26
- if (declaration == null) {
27
- declaration = { migrations: createMigrationRegistry<MongoTx>(), references: new Map() }
28
- declarations.set(alias, declaration)
29
- }
20
+ export const createMongoDeclarationHelper = (): MongoDeclarationHelper => {
21
+ const getDeclaration = (alias: string): MongoDeclaration => {
22
+ let declaration = declarations.get(alias)
23
+ if (declaration == null) {
24
+ declaration = { migrations: createMigrationRegistry<MongoTx>(), references: new Map() }
25
+ declarations.set(alias, declaration)
26
+ }
30
27
 
31
- return declaration
32
- }
28
+ return declaration
29
+ }
33
30
 
34
- /** Testing seam — drops every declaration so a spec can redeclare a resource from scratch. */
35
- export const resetDeclarations = (alias?: string): void => {
36
- if (alias == null) {
37
- declarations.clear()
31
+ const resetDeclarations = (alias?: string): void => {
32
+ if (alias == null) {
33
+ declarations.clear()
38
34
 
39
- return
35
+ return
36
+ }
37
+ declarations.delete(alias)
40
38
  }
41
- declarations.delete(alias)
39
+
40
+ return { getDeclaration, resetDeclarations }
42
41
  }
42
+
43
+ export const mongoDeclarationHelper = createMongoDeclarationHelper()
44
+
45
+ /** @deprecated compat:factory-refactor — use `mongoDeclarationHelper.resetDeclarations(…)` */
46
+ export const resetDeclarations = (alias?: string): void => mongoDeclarationHelper.resetDeclarations(alias)
package/src/index.ts CHANGED
@@ -2,7 +2,11 @@
2
2
  export type * from './types.js'
3
3
  export * from './consts.js'
4
4
  export * from './declarations.js'
5
+ export type * from './declarations/types.js'
5
6
  export * from './utils/migrations.js'
6
7
  export * from './utils/refs.js'
8
+ export type * from './utils/refs/types.js'
9
+ export * from './utils/criteria.js'
10
+ export type * from './utils/criteria/types.js'
7
11
  export * from './resource.js'
8
12
  export * from './helper.js'
package/src/resource.ts CHANGED
@@ -1,120 +1,144 @@
1
1
  import { appendContextual, assertContext } from '@owlmeans/context'
2
- import type { BasicContext, Contextual } from '@owlmeans/context'
3
2
  import { DEFAULT_DB_ALIAS, DEFAULT_PAGE_SIZE } from './consts.js'
4
- import { MigrationStage } from '@owlmeans/resource'
5
- import type { ListCriteria, ResourceMaker, ResourceRecord } from '@owlmeans/resource'
6
- import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
3
+ import { MigrationStage, type Criteria, type FirstOptions, type ListOptions, type ListResult, type ResourceRecord, type WriteOptions, MisshapedRecord, RecordExists, UnknownRecordError, UnsupportedArgumentError, RecordUpdateFailed } from '@owlmeans/resource'
7
4
  import type { MongoDbService, MongoReference, MongoRefOptions, MongoResource, MongoTx } from './types.js'
8
- import { initializeCollection } from './utils/life-cycle.js'
9
- import { getDeclaration } from './declarations.js'
10
- import { ObjectId } from 'mongodb'
11
- import { MisshapedRecord, RecordExists, UnknownRecordError, UnsupportedArgumentError, RecordUpdateFailed, prepareListOptions } from '@owlmeans/resource'
5
+ import { makeMongoLifeCycleUtils } from './utils/life-cycle.js'
6
+ import { mongoDeclarationHelper } from './declarations.js'
7
+ import { ObjectId, type CreateIndexesOptions, type Document, type IndexSpecification } from 'mongodb'
12
8
  import type { JSONSchemaType } from 'ajv'
13
9
  import { getSchemaSecureFeilds } from './helper.js'
14
- import {
15
- demarshalRefs, identityCriteria, makeRefMigration, marshalCriteria, marshalReference,
16
- refMigrationName
17
- } from './utils/refs.js'
10
+ import { mongoCriteriaHelper } from './utils/criteria.js'
11
+ import { makeRefMigration, mongoRefHelper } from './utils/refs.js'
12
+ import type { Config, Context } from './types.local.js'
18
13
 
19
- type Config = ServerConfig
20
- type Context<C extends Config = Config> = ServerContext<C>
21
14
 
22
15
  export const makeMongoResource = <
23
16
  R extends ResourceRecord, T extends MongoResource<R> = MongoResource<R>
24
17
  >(
25
18
  alias: string, dbAlias: string = DEFAULT_DB_ALIAS, serviceAlias: string = DEFAULT_DB_ALIAS,
26
- makeCustomResource?: ResourceMaker<R, T>, collectionName?: string
19
+ collectionName?: string
27
20
  ): T => {
28
21
  const location = `mongo-resource:${alias}`
29
22
 
30
23
  /**
31
24
  * Live view — references may be declared after the resource is built, and the
32
- * declarations are module scoped so they survive `reinitializeContext`.
25
+ * declarations live at module scope keyed by alias, so a second maker run for the
26
+ * same alias sees everything already declared for it.
33
27
  */
34
- const refs = (): Map<string, MongoReference> => getDeclaration(alias).references
28
+ const refs = (): Map<string, MongoReference> => mongoDeclarationHelper.getDeclaration(alias).references
35
29
 
36
30
  const demarshal = <Type extends ResourceRecord>(record: Type & { _id?: unknown }): Type => {
37
31
  record.id = record._id instanceof ObjectId ? record._id.toString() : record._id as string
38
32
  delete record._id
39
- return demarshalRefs(record, refs())
33
+ return mongoRefHelper.demarshalRefs(record, refs())
34
+ }
35
+
36
+ /**
37
+ * A read addressed either way. An id is matched tolerantly: a string that is not a mongo id
38
+ * finds nothing, where handing it to `ObjectId` would raise a driver error at a call site
39
+ * that only asked whether the record exists.
40
+ */
41
+ const filterOf = (idOrWhere: string | Criteria<R>): Document =>
42
+ typeof idOrWhere === 'string'
43
+ ? mongoRefHelper.identityCriteria('id', idOrWhere, refs())
44
+ : mongoCriteriaHelper.criteriaToFilter(idOrWhere, refs())
45
+
46
+ /**
47
+ * The single implementation both overloads of `load` and `get` stand on — an id and a
48
+ * criteria object differ only in how the filter is built.
49
+ */
50
+ const loadOne = async (
51
+ idOrWhere: string | Criteria<R>, opts?: FirstOptions<R>
52
+ ): Promise<R | null> => {
53
+ const sort = mongoCriteriaHelper.sortToMongo(opts?.sort)
54
+ const record = await resource.collection.findOne(
55
+ filterOf(idOrWhere), sort != null ? { sort } : {}
56
+ )
57
+
58
+ return record != null ? demarshal(record as unknown as R) : null
40
59
  }
41
60
 
42
61
  const resource: T = appendContextual<T>(alias, {
43
- get: async (id, field, opts) => {
44
- const record = await resource.load(id, field, opts)
62
+ get: (async (idOrWhere: string | Criteria<R>, opts?: FirstOptions<R>): Promise<R> => {
63
+ const record = await loadOne(idOrWhere, opts)
45
64
  if (record == null) {
46
- throw new UnknownRecordError(id)
65
+ throw new UnknownRecordError(
66
+ typeof idOrWhere === 'string' ? idOrWhere : JSON.stringify(idOrWhere)
67
+ )
47
68
  }
48
69
 
49
70
  return record
50
- },
71
+ }) as T['get'],
72
+
73
+ load: loadOne as T['load'],
51
74
 
52
- load: async (id, field, opts) => {
53
- if (typeof field === 'object') {
54
- opts = field
55
- field = field.field
75
+ list: async (where?: Criteria<R>, opts?: ListOptions<R>): Promise<ListResult<R>> => {
76
+ const filter = mongoCriteriaHelper.criteriaToFilter(where, refs())
77
+ const total = await resource.collection.countDocuments(filter)
78
+
79
+ /** `size: 0` is the explicit ask for everything; an omitted size pages by default. */
80
+ const size = opts?.size ?? DEFAULT_PAGE_SIZE
81
+ const page = opts?.page ?? 0
82
+ if (size < 1 && page !== 0) {
83
+ /** Nothing to page through — answering page 0 instead would be a silent wrong answer. */
84
+ throw new UnsupportedArgumentError('page-without-size')
56
85
  }
57
- field = field ?? '_id'
58
- if (opts?.ttl != null) {
59
- throw new UnsupportedArgumentError('ttl')
86
+
87
+ if (total === 0) {
88
+ return size > 0 ? { items: [], total, page, size } : { items: [], total }
60
89
  }
61
90
 
62
- const record = await resource.collection.findOne(identityCriteria(field, id, refs()))
63
- if (record != null) {
64
- demarshal(record)
91
+ let cursor = resource.collection.find(filter)
92
+ const sort = mongoCriteriaHelper.sortToMongo(opts?.sort)
93
+ if (sort != null) {
94
+ cursor = cursor.sort(sort)
95
+ }
96
+ if (size > 0) {
97
+ cursor = cursor.skip(page * size).limit(size)
65
98
  }
66
99
 
67
- return record
100
+ const items = (await cursor.toArray()).map(item => demarshal({ ...item } as unknown as R))
101
+
102
+ return size > 0 ? { items, total, page, size } : { items, total }
68
103
  },
69
104
 
70
- update: async (record, opts) => {
71
- let field = '_id'
72
- if (typeof opts === 'string') {
73
- field = opts
74
- } else if (typeof opts === 'object') {
75
- field = opts.field ?? field
76
- }
105
+ count: async (where?: Criteria<R>): Promise<number> =>
106
+ await resource.collection.countDocuments(mongoCriteriaHelper.criteriaToFilter(where, refs())),
77
107
 
78
- if (field !== '_id' && record[field as keyof typeof record] == null) {
79
- throw new MisshapedRecord('no-field-value')
108
+ update: async (record: Partial<R>, opts?: WriteOptions): Promise<R> => {
109
+ if (opts?.ttl != null) {
110
+ throw new UnsupportedArgumentError('ttl')
80
111
  }
81
-
82
- const id = field == '_id' ? record.id : record[field as keyof typeof record] as string
112
+ const id = record.id
83
113
  if (id == null) {
84
114
  throw new MisshapedRecord('id')
85
115
  }
86
116
 
87
- const original = await resource.get(id, field)
117
+ /** Absence is an error, not a silent no-op — `replaceOne` would just match nothing. */
118
+ const original = await resource.get(id)
88
119
 
89
- const replace = { ...record, _id: new ObjectId(original.id) }
90
- if (replace.id != null) {
91
- delete replace.id
92
- }
120
+ const replace: Document = { ...record }
121
+ /**
122
+ * Documents never store `id`, and a replacement that omits `_id` keeps the one the
123
+ * document already carries — so the whole record is replaced without touching its key.
124
+ */
125
+ delete replace.id
93
126
 
94
127
  const result = await resource.collection.replaceOne(
95
- identityCriteria(field, id, refs()),
128
+ mongoRefHelper.identityCriteria('id', original.id as string, refs()),
96
129
  _prepareValues(replace, resource.schema as JSONSchemaType<any>, refs())
97
130
  )
98
131
  if (!result.acknowledged) {
99
- throw new RecordUpdateFailed(`${field}:${id}`)
132
+ throw new RecordUpdateFailed(`id:${id}`)
100
133
  }
101
134
 
102
- return resource.get(id, opts)
135
+ return await resource.get(original.id as string)
103
136
  },
104
137
 
105
- save: async (record, opts) => {
106
- const id = record.id
107
- if (id != null || (
108
- typeof opts === 'string' && record[opts as keyof typeof record] != null
109
- ) || (
110
- typeof opts === 'object' && ("field" in opts)
111
- && record[opts.field as keyof typeof record] != null
112
- )) {
113
- return resource.update(record, opts)
114
- }
115
-
116
- return resource.create(record, typeof opts !== 'string' ? opts : undefined)
117
- },
138
+ save: async (record: Partial<R>, opts?: WriteOptions): Promise<R> =>
139
+ record.id != null
140
+ ? await resource.update(record, opts)
141
+ : await resource.create(record, opts),
118
142
 
119
143
  getDefaults: () => {
120
144
  const schema: JSONSchemaType<unknown> | undefined = resource.schema as JSONSchemaType<unknown>
@@ -130,14 +154,14 @@ export const makeMongoResource = <
130
154
  }, {})
131
155
  },
132
156
 
133
- create: async (record, opts) => {
157
+ create: async (record: Partial<R>, opts?: WriteOptions): Promise<R> => {
134
158
  if ("id" in record && record.id == null) {
135
159
  delete record.id
136
160
  }
137
161
  if (record.id != null) {
138
162
  throw new RecordExists('id-present')
139
163
  }
140
- if (opts != null && opts.ttl != null) {
164
+ if (opts?.ttl != null) {
141
165
  throw new UnsupportedArgumentError('ttl')
142
166
  }
143
167
  const result = await resource.collection.insertOne({
@@ -149,89 +173,33 @@ export const makeMongoResource = <
149
173
  throw new RecordUpdateFailed(`creation`)
150
174
  }
151
175
 
152
- const id = result.insertedId
153
-
154
- return resource.get(id.toString())
176
+ return await resource.get(result.insertedId.toString())
155
177
  },
156
178
 
157
- delete: async (id, opts) => {
158
- let record: R | null = null
159
- if (typeof id === 'object') {
160
- if (id.id == null) {
161
- throw new MisshapedRecord('id')
162
- }
163
- record = await resource.load(id.id)
164
- } else {
165
- record = await resource.load(id, opts)
166
- }
167
-
168
- if (record == null) {
169
- return null
170
- }
179
+ /** Atomic: the record is handed back by the very operation that removed it. */
180
+ delete: async (id: string): Promise<R | null> => {
181
+ const record = await resource.collection.findOneAndDelete(mongoRefHelper.identityCriteria('id', id, refs()))
171
182
 
172
- let field = '_id'
173
- if (typeof opts === 'string') {
174
- field = opts
175
- opts = undefined
176
- } else if (typeof opts === 'object') {
177
- field = opts.field ?? field
178
- }
179
-
180
- const _id = field == '_id' ? record.id : record[field as keyof typeof record] as string
181
- if (id == null) {
182
- throw new MisshapedRecord('id')
183
- }
184
-
185
- const result = await resource.collection.deleteOne(identityCriteria(field, _id as string, refs()))
186
- if (!result.acknowledged || result.deletedCount === 0) {
187
- return null
188
- }
189
-
190
- return record
183
+ return record != null ? demarshal(record as unknown as R) : null
191
184
  },
192
185
 
193
- pick: async (id, opts) => {
194
- const record = await resource.delete(id, opts)
186
+ take: async (id: string): Promise<R> => {
187
+ const record = await resource.delete(id)
195
188
  if (record == null) {
196
- throw new UnknownRecordError(typeof id == 'string' ? id : (id.id ?? 'unknown'))
189
+ throw new UnknownRecordError(id)
197
190
  }
198
191
 
199
192
  return record
200
193
  },
201
194
 
202
- list: async (criteria, opts) => {
203
- const options = prepareListOptions(DEFAULT_PAGE_SIZE, criteria, opts)
204
-
205
- criteria = marshalCriteria(options.criteria, refs()) ?? {}
206
- const pager = options.pager ?? {}
207
-
208
- const size = pager?.size ?? DEFAULT_PAGE_SIZE
209
- const total = await resource.collection.countDocuments(criteria as ListCriteria)
210
- pager.total = total
211
-
212
- const skip = (pager.page ?? 0) * size
213
- if (total === 0 && skip >= total) {
214
- return { items: [], pager }
215
- }
216
-
217
- let cursor = resource.collection.find(criteria as ListCriteria)
218
- .skip(skip).limit(size)
219
-
220
- if (pager.sort != null) {
221
- cursor = cursor.sort(
222
- typeof pager.sort === 'string'
223
- ? pager.sort
224
- : pager.sort.reduce((sort, [field, order]) =>
225
- ({ ...sort, [field as keyof typeof sort]: order ? -1 : 1 }), {}
226
- )
227
- )
195
+ purge: async (where: Criteria<R>): Promise<number> => {
196
+ const filter = mongoCriteriaHelper.criteriaToFilter(where, refs())
197
+ if (Object.keys(filter).length < 1) {
198
+ /** An empty filter here would empty the collection. */
199
+ throw new UnsupportedArgumentError('purge:no-criteria')
228
200
  }
229
201
 
230
- const items = await cursor.toArray()
231
-
232
- return {
233
- pager, items: items.map(item => demarshal({ ...item } as unknown as R))
234
- }
202
+ return (await resource.collection.deleteMany(filter)).deletedCount
235
203
  },
236
204
 
237
205
  lock: async (record, fields) => {
@@ -272,26 +240,26 @@ export const makeMongoResource = <
272
240
  return await mongo.client(dbAlias)
273
241
  },
274
242
 
275
- index: (name, index, options) => {
243
+ /**
244
+ * `index`, `migration` and `reference` hand the resource itself back so declarations
245
+ * chain — a return an object literal can't express, hence the member level casts: each
246
+ * implementation returns the closed over `resource`, which is that very object.
247
+ */
248
+ index: ((name: string, index: IndexSpecification, options?: CreateIndexesOptions) => {
276
249
  resource.indexes = resource.indexes ?? []
277
250
  resource.indexes.push({ name, index, options })
278
251
  return resource
279
- },
252
+ }) as T['index'],
280
253
 
281
- /**
282
- * `migration` and `reference` are `this`-returning in the interface, which an object
283
- * literal can't express — hence the member level casts: the implementations return
284
- * the closed over `resource`, which is that very object.
285
- */
286
254
  migration: ((name: string, apply: (tx: MongoTx) => Promise<void>, stage?: MigrationStage) => {
287
- getDeclaration(alias).migrations.register(name, apply, stage)
255
+ mongoDeclarationHelper.getDeclaration(alias).migrations.register(name, apply, stage)
288
256
  return resource
289
257
  }) as T['migration'],
290
258
 
291
- migrations: () => getDeclaration(alias).migrations,
259
+ migrations: () => mongoDeclarationHelper.getDeclaration(alias).migrations,
292
260
 
293
261
  reference: ((field: string, opts?: string | MongoRefOptions) => {
294
- const declaration = getDeclaration(alias)
262
+ const declaration = mongoDeclarationHelper.getDeclaration(alias)
295
263
  const options = typeof opts === 'string' ? { resource: opts } : opts ?? {}
296
264
  declaration.references.set(field, { field, resource: options.resource, noIndex: options.noIndex })
297
265
  /**
@@ -299,17 +267,16 @@ export const makeMongoResource = <
299
267
  * here rather than at init so it precedes migrations the app declares after its
300
268
  * `reference()` calls — the field's type contract is the foundation those build on.
301
269
  */
302
- declaration.migrations.register(refMigrationName(field), makeRefMigration(field), MigrationStage.Pre)
270
+ declaration.migrations.register(mongoRefHelper.refMigrationName(field), makeRefMigration(field), MigrationStage.Pre)
303
271
 
304
272
  return resource
305
273
  }) as T['reference'],
306
274
 
307
- references: () => [...getDeclaration(alias).references.values()]
275
+ references: () => [...mongoDeclarationHelper.getDeclaration(alias).references.values()]
308
276
  } as Partial<T>)
309
277
 
310
278
  // Explicit collection name override (decoupled from the registration alias, which may
311
- // contain characters that aren't valid in a collection name). Survives reinitializeContext
312
- // because it's threaded back into the recursive makeMongoResource call below.
279
+ // contain characters that aren't valid in a collection name).
313
280
  if (collectionName != null) {
314
281
  resource.name = collectionName
315
282
  }
@@ -322,20 +289,11 @@ export const makeMongoResource = <
322
289
  await mongo.ready()
323
290
  const db = await mongo.db(dbAlias)
324
291
  const config = mongo.config(dbAlias)
325
- resource.collection = await initializeCollection(
326
- db, config, resource as unknown as MongoResource<ResourceRecord>, context
292
+ resource.collection = await makeMongoLifeCycleUtils(db).initializeCollection(
293
+ config, resource as unknown as MongoResource<ResourceRecord>, context
327
294
  )
328
295
  }
329
296
 
330
- resource.reinitializeContext = <Type extends Contextual>(context: BasicContext<Config>) => {
331
- const resource = (makeCustomResource?.(dbAlias, serviceAlias)
332
- ?? makeMongoResource<R, T>(alias, dbAlias, serviceAlias, makeCustomResource, collectionName)) as unknown as Type
333
-
334
- resource.ctx = context
335
-
336
- return resource as Type
337
- }
338
-
339
297
  return resource
340
298
  }
341
299
 
@@ -349,7 +307,7 @@ const _prepareValues = <T extends ResourceRecord>(
349
307
  */
350
308
  if (refs != null && refs.size > 0) {
351
309
  obj = Object.fromEntries(Object.entries(obj).map(([key, value]) =>
352
- refs.has(key) ? [key, marshalReference(key, value)] : [key, value]
310
+ refs.has(key) ? [key, mongoRefHelper.marshalReference(key, value)] : [key, value]
353
311
  )) as T
354
312
  }
355
313
  // @TODO Validate keys from additional properties in the root
@@ -0,0 +1,5 @@
1
+ import type { ServerConfig, ServerContext } from '@owlmeans/server-context'
2
+
3
+ export type Config = ServerConfig
4
+
5
+ export type Context<C extends Config = Config> = ServerContext<C>
package/src/types.ts CHANGED
@@ -1,6 +1,4 @@
1
- import type {
2
- Resource, ResourceRecord, ResourceDbService, DbLocker, ResourceLocker, MigratableResource
3
- } from '@owlmeans/resource'
1
+ import type { Resource, ResourceRecord, ResourceDbService, DbLocker, LockableResource, MigratableResource, MigrationRegistry } from '@owlmeans/resource'
4
2
  import type { Collection, CreateIndexesOptions, Db, IndexSpecification, MongoClient } from 'mongodb'
5
3
  import type { AnySchema } from 'ajv'
6
4
 
@@ -44,7 +42,8 @@ export interface MongoRefOptions {
44
42
  noIndex?: boolean
45
43
  }
46
44
 
47
- export interface MongoResource<T extends ResourceRecord> extends Resource<T>, ResourceLocker<T>, MigratableResource<MongoTx> {
45
+ export interface MongoResource<T extends ResourceRecord> extends Resource<T>, LockableResource<T>,
46
+ MigratableResource<MongoTx, MongoResource<T>> {
48
47
  name?: string
49
48
  /**
50
49
  * The db-config alias this resource was registered against.
@@ -61,13 +60,14 @@ export interface MongoResource<T extends ResourceRecord> extends Resource<T>, Re
61
60
  collection: Collection
62
61
  db: () => Promise<Db>
63
62
  client: () => Promise<MongoClient>
64
- index: <Type extends MongoResource<T>>(name: string, index: IndexSpecification, options?: CreateIndexesOptions) => Type
63
+ index: (name: string, index: IndexSpecification, options?: CreateIndexesOptions) => this
65
64
  /**
66
65
  * Declare that a field stores another record's id.
67
66
  *
68
67
  * Chainable and idempotent like {@link MigratableResource.migration}, and stored the same
69
- * way — per alias at module scope — because losing the declaration to a context rebuild
70
- * would silently stop the string/ObjectId conversion for the field.
68
+ * way — per alias at module scope — so a maker that runs more than once for the same alias
69
+ * re-declares the same entry rather than losing it. A lost declaration would silently stop
70
+ * the string/ObjectId conversion for the field.
71
71
  *
72
72
  * Declare only fields whose values really are mongo ids (assigned from another record's
73
73
  * `id`). Composite keys, external provider ids, DIDs and business slugs must stay
@@ -81,3 +81,9 @@ export interface MongoResource<T extends ResourceRecord> extends Resource<T>, Re
81
81
 
82
82
  export interface MongoDbService extends ResourceDbService<Db, MongoClient>, DbLocker<ResourceRecord> {
83
83
  }
84
+
85
+ export interface MongoDeclaration {
86
+ migrations: MigrationRegistry<MongoTx>
87
+ /** Declared ObjectId references, keyed by field. Registered via `resource.reference()`. */
88
+ references: Map<string, MongoReference>
89
+ }