@toa.io/storages.mongodb 1.0.0-alpha.29 → 1.0.0-alpha.291

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/CHANGELOG.md ADDED
@@ -0,0 +1,37 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
+
6
+ # [1.0.0-alpha.291](https://github.com/toa-io/toa/compare/v1.0.0-alpha.290...v1.0.0-alpha.291) (2026-09-07)
7
+
8
+ **Note:** Version bump only for package @toa.io/storages.mongodb
9
+
10
+
11
+
12
+
13
+
14
+ # [1.0.0-alpha.289](https://github.com/toa-io/toa/compare/v1.0.0-alpha.288...v1.0.0-alpha.289) (2026-09-07)
15
+
16
+ * A deploy moves only what changed, and a component sees none of the runtime's environment (#1073) ([f38e3db](https://github.com/toa-io/toa/commit/f38e3db533f24db866c57bfa1ef294eff1eaf499)), closes [#1073](https://github.com/toa-io/toa/issues/1073) [#1064](https://github.com/toa-io/toa/issues/1064) [#1066](https://github.com/toa-io/toa/issues/1066) [#1067](https://github.com/toa-io/toa/issues/1067) [#1068](https://github.com/toa-io/toa/issues/1068) [#1069](https://github.com/toa-io/toa/issues/1069) [#1071](https://github.com/toa-io/toa/issues/1071) [#1070](https://github.com/toa-io/toa/issues/1070) [#1072](https://github.com/toa-io/toa/issues/1072)
17
+
18
+ ### BREAKING CHANGES
19
+
20
+ * a component that read `process.env.TOA_*` reads `context` instead;
21
+ `echo(input)` no longer substitutes from the environment; a bash operation sees no
22
+ `TOA_*`; images no longer set `USER node` — see migrations/289.md.
23
+
24
+
25
+ # [1.0.0-alpha.288](https://github.com/toa-io/toa/compare/v1.0.0-alpha.287...v1.0.0-alpha.288) (2026-09-06)
26
+
27
+ **Note:** Version bump only for package @toa.io/storages.mongodb
28
+
29
+
30
+
31
+
32
+
33
+ # [1.0.0-alpha.287](https://github.com/toa-io/toa/compare/v1.0.0-alpha.286...v1.0.0-alpha.287) (2026-09-06)
34
+
35
+ ### Features
36
+
37
+ * **mongodb:** a migration says what it is doing ([a917a81](https://github.com/toa-io/toa/commit/a917a81fdc94eb23fd73182c0edf891e6b2df843))
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@toa.io/storages.mongodb",
3
- "version": "1.0.0-alpha.29",
3
+ "version": "1.0.0-alpha.291",
4
+ "type": "module",
4
5
  "description": "Toa MongoDB Storage Connector",
5
6
  "author": "temich <tema.gurtovoy@gmail.com>",
6
7
  "homepage": "https://github.com/toa-io/toa#readme",
@@ -19,13 +20,13 @@
19
20
  "test": "echo \"Error: run tests from root\" && exit 1"
20
21
  },
21
22
  "dependencies": {
22
- "@toa.io/console": "1.0.0-alpha.29",
23
- "@toa.io/conveyor": "1.0.0-alpha.29",
24
- "@toa.io/core": "1.0.0-alpha.29",
25
- "@toa.io/generic": "1.0.0-alpha.29",
26
- "@toa.io/pointer": "1.0.0-alpha.29",
27
- "mongodb": "6.3.0",
23
+ "@toa.io/conveyor": "1.0.0-alpha.291",
24
+ "@toa.io/core": "1.0.0-alpha.291",
25
+ "@toa.io/generic": "1.0.0-alpha.291",
26
+ "@toa.io/pointer": "1.0.0-alpha.291",
27
+ "mongodb": "7.6.0",
28
+ "openspan": "1.0.0-alpha.286",
28
29
  "saslprep": "1.0.3"
29
30
  },
30
- "gitHead": "186d0e17c12a9016b8076edf925963cbb32b9219"
31
+ "gitHead": "bcd8d8d07bd3e64fc826af8719472404662cc37f"
31
32
  }
package/readme.md ADDED
@@ -0,0 +1,16 @@
1
+ # MongoDB Storage
2
+
3
+ ## Tracing
4
+
5
+ Commands are recorded as `client` spans within the trace of the current invocation,
6
+ using the [driver command monitoring events](https://www.mongodb.com/docs/drivers/node/current/monitoring/command-monitoring/).
7
+
8
+ Spans are named `{command} {collection}` and carry `db.*` attributes following the
9
+ [OpenTelemetry semantic conventions](https://opentelemetry.io/docs/specs/semconv/database/mongodb/):
10
+ `db.system`, `db.namespace`, `db.collection.name`, `db.operation.name`.
11
+
12
+ Commands executed outside of a sampled trace context (e.g. index management on startup)
13
+ and internal driver commands (`hello`, `ping`, authentication) are not recorded.
14
+
15
+ Monitoring is client-side only and does not affect the MongoDB server. Span recording
16
+ adds no waiting to the query path: exporting is buffered and happens in the background.
package/src/client.js CHANGED
@@ -1,28 +1,58 @@
1
- 'use strict'
2
-
3
1
  /**
4
2
  * @typedef {import('mongodb').MongoClient} MongoClient
5
3
  * @typedef {{ count: number, client: MongoClient }} Instance
6
4
  * @typedef {import('@toa.io/core').Locator} Locator
7
5
  */
8
6
 
9
- const { Connector } = require('@toa.io/core')
10
- const { resolve } = require('@toa.io/pointer')
11
- const { ID } = require('./deployment')
12
- const { MongoClient } = require('mongodb')
7
+ import { console } from 'openspan'
8
+ import { environment } from '@toa.io/generic'
9
+ import { Connector } from '@toa.io/core'
10
+ import { resolve } from '@toa.io/pointer'
11
+ import { ID } from './deployment.js'
12
+ import { MongoClient } from 'mongodb'
13
13
 
14
14
  /**
15
15
  * @type {Record<string, Promise<Instance>>}
16
16
  */
17
17
  const INSTANCES = {}
18
18
 
19
- class Client extends Connector {
19
+ export class Client extends Connector {
20
+ name
21
+
20
22
  /**
21
23
  * @public
22
24
  * @type {import('mongodb').Collection}
23
25
  */
24
26
  collection
25
27
 
28
+ /**
29
+ * The outbox rows of this component, absent unless something consumes its events. Created
30
+ * eagerly beside the entity collection, because a transaction cannot create a collection and
31
+ * an index build cannot run inside one.
32
+ *
33
+ * @public
34
+ * @type {import('mongodb').Collection | undefined}
35
+ */
36
+ outbox
37
+
38
+ /**
39
+ * Whether this deployment can run transactions at all. A standalone mongod cannot, and an
40
+ * outbox without atomicity is worse than none, so the storage falls back to inline emission.
41
+ *
42
+ * @public
43
+ * @type {boolean}
44
+ */
45
+ transactional = false
46
+
47
+ /**
48
+ * The database this component's collections live in, which is where the migration state
49
+ * is kept as well.
50
+ *
51
+ * @public
52
+ * @type {import('mongodb').Db}
53
+ */
54
+ db
55
+
26
56
  /**
27
57
  * @private
28
58
  * @type {Locator}
@@ -41,13 +71,22 @@ class Client extends Connector {
41
71
  */
42
72
  key
43
73
 
74
+ /**
75
+ * @private
76
+ * @type {boolean}
77
+ */
78
+ publishes
79
+
44
80
  /**
45
81
  * @param {Locator} locator
82
+ * @param {boolean} [publishes] whether this component publishes anything
46
83
  */
47
- constructor (locator) {
84
+ constructor(locator, publishes = false) {
48
85
  super()
49
86
 
50
87
  this.locator = locator
88
+ this.name = locator.lowercase
89
+ this.publishes = publishes
51
90
  }
52
91
 
53
92
  /**
@@ -55,29 +94,51 @@ class Client extends Connector {
55
94
  * @override
56
95
  * @return {Promise<void>}
57
96
  */
58
- async open () {
97
+ async open() {
59
98
  const urls = await this.resolveURLs()
60
99
  const dbname = this.resolveDB()
61
- const collname = this.locator.lowercase
62
100
 
63
101
  this.key = getKey(dbname, urls)
64
102
 
65
- INSTANCES[this.key] ??= this.createInstance(urls)
103
+ try {
104
+ INSTANCES[this.key] ??= this.createInstance(urls)
105
+ } catch (error) {
106
+ console.error('Failed to connect to MongoDB', { urls, error })
107
+ }
66
108
 
67
109
  this.instance = await INSTANCES[this.key]
68
110
  this.instance.count++
69
111
 
70
112
  const db = this.instance.client.db(dbname)
71
113
 
72
- try {
73
- this.collection = await db.createCollection(collname)
74
- } catch (e) {
75
- if (e.code !== ALREADY_EXISTS) {
76
- throw e
77
- }
114
+ this.db = db
115
+ this.collection = await collection(db, this.name)
116
+ this.transactional = await transactional(db)
78
117
 
79
- this.collection = db.collection(collname)
80
- }
118
+ if (!this.publishes) return
119
+
120
+ if (this.transactional) this.outbox = await collection(db, this.name + OUTBOX)
121
+ else
122
+ console.warn(
123
+ 'MongoDB is not a replica set; events are emitted inline, without an outbox',
124
+ { collection: this.name }
125
+ )
126
+ }
127
+
128
+ /**
129
+ * Runs `fn` in a transaction and answers what it returned. The driver may call `fn` more
130
+ * than once, so it must not hold state of its own — an outbox row is built by the caller
131
+ * and reused, and a rolled back attempt leaves nothing behind.
132
+ *
133
+ * @public
134
+ * @template T
135
+ * @param {(session: import('mongodb').ClientSession) => Promise<T>} fn
136
+ * @return {Promise<T>}
137
+ */
138
+ async transaction(fn) {
139
+ return this.instance.client.withSession(async (session) =>
140
+ session.withTransaction(async () => fn(session))
141
+ )
81
142
  }
82
143
 
83
144
  /**
@@ -85,7 +146,7 @@ class Client extends Connector {
85
146
  * @override
86
147
  * @return {Promise<void>}
87
148
  */
88
- async close () {
149
+ async close() {
89
150
  const instance = await INSTANCES[this.key]
90
151
 
91
152
  instance.count--
@@ -101,11 +162,11 @@ class Client extends Connector {
101
162
  * @param {string[]} urls
102
163
  * @return {Promise<Instance>}
103
164
  */
104
- async createInstance (urls) {
165
+ async createInstance(urls) {
105
166
  const client = new MongoClient(urls.join(','), OPTIONS)
106
167
  const hosts = urls.map((str) => new URL(str).host)
107
168
 
108
- console.info('Connecting to MongoDB:', hosts.join(', '))
169
+ console.info('Connecting to MongoDB', { address: hosts.join(', ') })
109
170
 
110
171
  await client.connect()
111
172
 
@@ -119,9 +180,11 @@ class Client extends Connector {
119
180
  * @private
120
181
  * @return {Promise<string[]>}
121
182
  */
122
- async resolveURLs () {
123
- if (process.env.TOA_DEV === '1') {
124
- return ['mongodb://developer:secret@localhost']
183
+ async resolveURLs() {
184
+ // Toa's own development stack is not on the conventional ports: the applications built on
185
+ // Toa are, and they share the machine. See CONTRIBUTING.md.
186
+ if (environment.get('TOA_DEV') === '1') {
187
+ return ['mongodb://developer:secret@localhost:31020']
125
188
  } else {
126
189
  return await resolve(ID, this.locator.id)
127
190
  }
@@ -131,29 +194,55 @@ class Client extends Connector {
131
194
  * @private
132
195
  * @return {string}
133
196
  */
134
- resolveDB () {
135
- if (process.env.TOA_CONTEXT !== undefined) {
136
- return process.env.TOA_CONTEXT
137
- }
197
+ resolveDB() {
198
+ const context = environment.get('TOA_CONTEXT')
138
199
 
139
- if (process.env.TOA_DEV === '1') {
140
- return 'toa-dev'
141
- }
200
+ if (context !== undefined) return context
201
+
202
+ if (environment.get('TOA_DEV') === '1') return 'toa-dev'
142
203
 
143
204
  throw new Error('Environment variable TOA_CONTEXT is not defined')
144
205
  }
145
206
  }
146
207
 
147
- function getKey (db, urls) {
208
+ function getKey(db, urls) {
148
209
  return db + ':' + urls.sort().join(' ')
149
210
  }
150
211
 
212
+ /**
213
+ * Concurrent pods race to create the same collection, and losing that race is not an error.
214
+ */
215
+ async function collection(db, name) {
216
+ try {
217
+ return await db.createCollection(name)
218
+ } catch (e) {
219
+ if (e.code !== ALREADY_EXISTS) throw e
220
+
221
+ return db.collection(name)
222
+ }
223
+ }
224
+
225
+ async function transactional(db) {
226
+ try {
227
+ const hello = await db.admin().command({ hello: 1 })
228
+
229
+ return hello.setName !== undefined || hello.msg === 'isdbgrid'
230
+ } catch (e) {
231
+ console.warn('MongoDB transaction support could not be determined', { error: e })
232
+
233
+ return false
234
+ }
235
+ }
236
+
237
+ /**
238
+ * `monitorCommands` is deliberately absent. It makes the driver materialize every reply
239
+ * eagerly to populate the monitoring event (`CommandSucceededEvent`), which defeats the
240
+ * lazy per-document deserialization a cursor exists for — a 100-document batch is then
241
+ * deserialized twice. `Storage` times its own calls instead.
242
+ */
151
243
  const OPTIONS = {
152
- ignoreUndefined: true,
153
- connectTimeoutMS: 0,
154
- serverSelectionTimeoutMS: 0
244
+ ignoreUndefined: true
155
245
  }
156
246
 
157
247
  const ALREADY_EXISTS = 48
158
-
159
- exports.Client = Client
248
+ const OUTBOX = '_outbox'
package/src/deployment.js CHANGED
@@ -1,22 +1,17 @@
1
- 'use strict'
1
+ import { createVariables } from '@toa.io/pointer'
2
2
 
3
- const { createVariables } = require('@toa.io/pointer')
4
-
5
- const deployment = (instances, annotation) => {
3
+ export const deployment = (instances, annotation) => {
6
4
  const requests = instances.map((instance) => createRequest(instance))
7
5
  const variables = createVariables(ID, annotation, requests)
8
6
 
9
7
  return { variables }
10
8
  }
11
9
 
12
- function createRequest (instance) {
10
+ function createRequest(instance) {
13
11
  return {
14
12
  group: instance.locator.label,
15
13
  selectors: [instance.locator.id]
16
14
  }
17
15
  }
18
16
 
19
- const ID = 'mongodb'
20
-
21
- exports.ID = ID
22
- exports.deployment = deployment
17
+ export const ID = 'mongodb'
package/src/factory.js CHANGED
@@ -1,16 +1,10 @@
1
- 'use strict'
1
+ import { Client } from './client.js'
2
+ import { Storage } from './storage.js'
2
3
 
3
- const { Client } = require('./client')
4
- const { Collection } = require('./collection')
5
- const { Storage } = require('./storage')
4
+ export class Factory {
5
+ storage(locator, entity, options = {}) {
6
+ const client = new Client(locator, options.outbox === true)
6
7
 
7
- class Factory {
8
- storage (locator, entity) {
9
- const client = new Client(locator)
10
- const connection = new Collection(client)
11
-
12
- return new Storage(connection, entity)
8
+ return new Storage(client, entity)
13
9
  }
14
10
  }
15
-
16
- exports.Factory = Factory
package/src/index.js CHANGED
@@ -1,7 +1,2 @@
1
- 'use strict'
2
-
3
- const { deployment } = require('./deployment')
4
- const { Factory } = require('./factory')
5
-
6
- exports.deployment = deployment
7
- exports.Factory = Factory
1
+ export { deployment } from './deployment.js'
2
+ export { Factory } from './factory.js'