@toa.io/storages.mongodb 1.0.0-alpha.29 → 1.0.0-alpha.292
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 +44 -0
- package/package.json +10 -8
- package/readme.md +16 -0
- package/src/client.js +127 -38
- package/src/factory.js +6 -12
- package/src/index.js +1 -7
- package/src/migrations.js +343 -0
- package/src/outbox.js +173 -0
- package/src/record.js +82 -20
- package/src/storage.js +361 -131
- package/src/translate/criteria.js +25 -16
- package/src/translate/options.js +17 -11
- package/src/translate/rename.js +1 -5
- package/src/translate.js +14 -15
- package/test/migrations.test.js +388 -0
- package/test/query/criteria.fixtures.js +2 -7
- package/test/query/criteria.test.js +5 -4
- package/test/record.test.js +98 -25
- package/test/storage.test.js +129 -0
- package/test/translate.test.js +59 -7
- package/types/connection.d.ts +18 -12
- package/types/pointer.d.ts +4 -6
- package/types/record.d.ts +5 -7
- package/src/collection.js +0 -69
- package/src/deployment.js +0 -22
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
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.292](https://github.com/toa-io/toa/compare/v1.0.0-alpha.291...v1.0.0-alpha.292) (2026-09-07)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **definitions:** what a package declares is read from one package ([7052341](https://github.com/toa-io/toa/commit/70523411b5d9e2b204c999aa02365173f3bb518d))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# [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)
|
|
14
|
+
|
|
15
|
+
**Note:** Version bump only for package @toa.io/storages.mongodb
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# [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)
|
|
22
|
+
|
|
23
|
+
* 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)
|
|
24
|
+
|
|
25
|
+
### BREAKING CHANGES
|
|
26
|
+
|
|
27
|
+
* a component that read `process.env.TOA_*` reads `context` instead;
|
|
28
|
+
`echo(input)` no longer substitutes from the environment; a bash operation sees no
|
|
29
|
+
`TOA_*`; images no longer set `USER node` — see migrations/289.md.
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# [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)
|
|
33
|
+
|
|
34
|
+
**Note:** Version bump only for package @toa.io/storages.mongodb
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# [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)
|
|
41
|
+
|
|
42
|
+
### Features
|
|
43
|
+
|
|
44
|
+
* **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.
|
|
3
|
+
"version": "1.0.0-alpha.292",
|
|
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,14 @@
|
|
|
19
20
|
"test": "echo \"Error: run tests from root\" && exit 1"
|
|
20
21
|
},
|
|
21
22
|
"dependencies": {
|
|
22
|
-
"@toa.io/
|
|
23
|
-
"@toa.io/
|
|
24
|
-
"@toa.io/
|
|
25
|
-
"@toa.io/generic": "1.0.0-alpha.
|
|
26
|
-
"@toa.io/pointer": "1.0.0-alpha.
|
|
27
|
-
"mongodb": "6.
|
|
23
|
+
"@toa.io/conveyor": "1.0.0-alpha.292",
|
|
24
|
+
"@toa.io/core": "1.0.0-alpha.292",
|
|
25
|
+
"@toa.io/definitions": "1.0.0-alpha.292",
|
|
26
|
+
"@toa.io/generic": "1.0.0-alpha.292",
|
|
27
|
+
"@toa.io/pointer": "1.0.0-alpha.292",
|
|
28
|
+
"mongodb": "7.6.0",
|
|
29
|
+
"openspan": "1.0.0-alpha.286",
|
|
28
30
|
"saslprep": "1.0.3"
|
|
29
31
|
},
|
|
30
|
-
"gitHead": "
|
|
32
|
+
"gitHead": "0f6aa3ad9dbec86bb632a7975ea61d1dde1d8fc5"
|
|
31
33
|
}
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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 '@toa.io/definitions/storages.mongodb'
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
124
|
-
|
|
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
|
-
|
|
136
|
-
return process.env.TOA_CONTEXT
|
|
137
|
-
}
|
|
197
|
+
resolveDB() {
|
|
198
|
+
const context = environment.get('TOA_CONTEXT')
|
|
138
199
|
|
|
139
|
-
if (
|
|
140
|
-
|
|
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
|
|
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/factory.js
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
import { Client } from './client.js'
|
|
2
|
+
import { Storage } from './storage.js'
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
4
|
+
export class Factory {
|
|
5
|
+
storage(locator, entity, options = {}) {
|
|
6
|
+
const client = new Client(locator, options.outbox === true)
|
|
6
7
|
|
|
7
|
-
|
|
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
|