@adobe/aio-lib-db 0.1.0-alpha.1
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/COPYING.txt +7 -0
- package/COPYRIGHT +5 -0
- package/LICENSE +201 -0
- package/README.md +557 -0
- package/index.js +12 -0
- package/lib/AbstractDbCursor.js +350 -0
- package/lib/AggregateCursor.js +187 -0
- package/lib/DbBase.js +149 -0
- package/lib/DbClient.js +114 -0
- package/lib/DbCollection.js +352 -0
- package/lib/DbError.js +21 -0
- package/lib/FindCursor.js +107 -0
- package/lib/api/client.js +96 -0
- package/lib/api/collection.js +425 -0
- package/lib/api/db.js +90 -0
- package/lib/constants.js +44 -0
- package/lib/init.js +35 -0
- package/package.json +56 -0
- package/utils/apiRequest.js +103 -0
- package/utils/axiosUtils.js +57 -0
- package/utils/ejsonHandler.js +61 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
|
|
7
|
+
Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
const { Long } = require("bson")
|
|
13
|
+
const { Readable, Transform } = require("node:stream")
|
|
14
|
+
const DbError = require("./DbError")
|
|
15
|
+
const { CURSOR_INIT_ERR_MESSAGE } = require("./constants")
|
|
16
|
+
const { getAxiosClient } = require("../utils/axiosUtils")
|
|
17
|
+
const { getMoreApi } = require("./api/collection")
|
|
18
|
+
const { closeApi } = require("./api/client")
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Abstract base class for database cursors.
|
|
22
|
+
* @abstract
|
|
23
|
+
*/
|
|
24
|
+
class AbstractDbCursor extends Readable {
|
|
25
|
+
/** @type boolean */
|
|
26
|
+
#readInProgress
|
|
27
|
+
/** @type function */
|
|
28
|
+
#transform
|
|
29
|
+
/** @type boolean */
|
|
30
|
+
#hasEmittedClose
|
|
31
|
+
|
|
32
|
+
// Protected properties
|
|
33
|
+
/** @type DbBase */
|
|
34
|
+
_db
|
|
35
|
+
/** @type DbClient */
|
|
36
|
+
_dbClient
|
|
37
|
+
/** @type string */
|
|
38
|
+
_collection
|
|
39
|
+
/** @type Object */
|
|
40
|
+
_options
|
|
41
|
+
/** @type Object[] */
|
|
42
|
+
_buffer
|
|
43
|
+
/** @type boolean */
|
|
44
|
+
_sessionClosed
|
|
45
|
+
/** @type {(Long|number)} */
|
|
46
|
+
_id
|
|
47
|
+
/** @type boolean */
|
|
48
|
+
_initialized
|
|
49
|
+
/** @type AxiosInstance */
|
|
50
|
+
_axiosClient
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* @param {DbBase} db
|
|
54
|
+
* @param {DbClient} dbClient
|
|
55
|
+
* @param {string} collection
|
|
56
|
+
* @param {Object} options
|
|
57
|
+
*/
|
|
58
|
+
constructor(db, dbClient, collection, options) {
|
|
59
|
+
super({ objectMode: true, highWaterMark: 1 })
|
|
60
|
+
this._initialized = false
|
|
61
|
+
this._db = db
|
|
62
|
+
this._dbClient = dbClient
|
|
63
|
+
this._collection = collection
|
|
64
|
+
this._options = options
|
|
65
|
+
this.#readInProgress = false
|
|
66
|
+
this.#transform = null
|
|
67
|
+
this._sessionClosed = false
|
|
68
|
+
this.#hasEmittedClose = false
|
|
69
|
+
this._axiosClient = getAxiosClient(true)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Checks if there are more results that have not been exhausted yet
|
|
74
|
+
*
|
|
75
|
+
* @returns {Promise<boolean>}
|
|
76
|
+
* @throws {DbError}
|
|
77
|
+
*/
|
|
78
|
+
async hasNext() {
|
|
79
|
+
if (!this._initialized) {
|
|
80
|
+
await this.#initialize()
|
|
81
|
+
}
|
|
82
|
+
else if (this._buffer.length === 0 && !this.#isDead) {
|
|
83
|
+
try {
|
|
84
|
+
await this._getMore()
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
try {
|
|
88
|
+
await this.close()
|
|
89
|
+
}
|
|
90
|
+
catch (swallow) {
|
|
91
|
+
// Throw the original error, not any that happen while killing the cursor in response
|
|
92
|
+
}
|
|
93
|
+
throw err
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Automatically close the session if the end of the results has been reached
|
|
98
|
+
if (Long.ZERO.eq(this._id)) {
|
|
99
|
+
await this.close()
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return this._buffer.length > 0
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Gets and exhausts the next result, or returns null if no results are remaining
|
|
107
|
+
*
|
|
108
|
+
* @returns {Promise<Object|null>}
|
|
109
|
+
* @throws {DbError}
|
|
110
|
+
*/
|
|
111
|
+
async next() {
|
|
112
|
+
let res = await this.hasNext() ? this._buffer.shift() : null
|
|
113
|
+
if (res !== null && this.#transform) {
|
|
114
|
+
res = await this.#transform(res)
|
|
115
|
+
}
|
|
116
|
+
return res
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Gets the rest of the results in a single array and exhausts the cursor
|
|
121
|
+
*
|
|
122
|
+
* @returns {Promise<Object[]>}
|
|
123
|
+
* @throws {DbError}
|
|
124
|
+
*/
|
|
125
|
+
async toArray() {
|
|
126
|
+
let results = []
|
|
127
|
+
while (await this.hasNext()) {
|
|
128
|
+
results = results.concat(this._buffer)
|
|
129
|
+
this._buffer = []
|
|
130
|
+
}
|
|
131
|
+
if (this.#transform) {
|
|
132
|
+
results = await Promise.all(results.map(doc => this.#transform(doc)))
|
|
133
|
+
}
|
|
134
|
+
return results
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async _getMore() {
|
|
138
|
+
// getMore does not accept all options that the initial request does
|
|
139
|
+
const getMoreOptions = {}
|
|
140
|
+
if (this._options.batchSize) {
|
|
141
|
+
getMoreOptions.batchSize = this._options.batchSize
|
|
142
|
+
}
|
|
143
|
+
if (this._options.comment) {
|
|
144
|
+
getMoreOptions.comment = this._options.comment
|
|
145
|
+
}
|
|
146
|
+
const data = await getMoreApi(this._db, this._axiosClient, this._collection, this._id, getMoreOptions)
|
|
147
|
+
this._buffer = data.cursor['nextBatch']
|
|
148
|
+
if (Long.ZERO.eq(data.cursor['id'])) {
|
|
149
|
+
this._id = Long.ZERO
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Closes the API session. If the cursor was not dead, also clear the buffer to release resources.
|
|
155
|
+
*/
|
|
156
|
+
async close() {
|
|
157
|
+
if (!this._sessionClosed) {
|
|
158
|
+
this._sessionClosed = true
|
|
159
|
+
this._dbClient.unregisterCursor(this)
|
|
160
|
+
// If the session has been closed before the end of results was reached either through an error or explicit close,
|
|
161
|
+
// stop iteration and clear the buffer
|
|
162
|
+
if (this._initialized && this._id && Long.ZERO.ne(this._id)) {
|
|
163
|
+
this._id = Long.ZERO
|
|
164
|
+
this._buffer = []
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
await closeApi(this._db, this._axiosClient)
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
// Note: the 'close' event signifies that the cursor session is no longer active, regardless of any remaining
|
|
171
|
+
// documents in the buffer. This is in contrast to 'cursor.closed', which waits for the buffer to be consumed.
|
|
172
|
+
// This matches the behavior of the MongoDB Node.js driver.
|
|
173
|
+
if (!this.#hasEmittedClose) {
|
|
174
|
+
this.emit('close')
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
emit(event, ...args) {
|
|
181
|
+
try {
|
|
182
|
+
super.emit(event, ...args)
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
if (event?.toLowerCase() === 'close') {
|
|
186
|
+
this.#hasEmittedClose = true
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The cursor ID
|
|
193
|
+
* Will be 0 if the results fit in one batch.
|
|
194
|
+
*
|
|
195
|
+
* @const
|
|
196
|
+
* @readonly
|
|
197
|
+
* @returns {Long|number}
|
|
198
|
+
*/
|
|
199
|
+
get id() {
|
|
200
|
+
return this._id
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* A cursor is closed if all buffered documents have been consumed and no more will be retrieved.
|
|
205
|
+
*
|
|
206
|
+
* @returns {boolean}
|
|
207
|
+
*/
|
|
208
|
+
get closed() {
|
|
209
|
+
return this._initialized && this.#isDead && this._buffer?.length === 0
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Checks if the cursor will fetch more results from the API
|
|
214
|
+
*
|
|
215
|
+
* @return {boolean}
|
|
216
|
+
*/
|
|
217
|
+
get #isDead() {
|
|
218
|
+
return Long.ZERO.eq(this._id) || this._sessionClosed
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Set the batch size for the cursor.
|
|
223
|
+
*
|
|
224
|
+
* @param {number} batchSize
|
|
225
|
+
* @returns {this}
|
|
226
|
+
*/
|
|
227
|
+
batchSize(batchSize) {
|
|
228
|
+
this._throwIfInitialized()
|
|
229
|
+
this._options.batchSize = batchSize
|
|
230
|
+
return this
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Set a transformation function to apply to each document in the cursor.
|
|
235
|
+
*
|
|
236
|
+
* @param {function} fun
|
|
237
|
+
* @returns {this}
|
|
238
|
+
*/
|
|
239
|
+
map(fun) {
|
|
240
|
+
this._throwIfInitialized()
|
|
241
|
+
if (this.#transform) {
|
|
242
|
+
const oldTransform = this.#transform
|
|
243
|
+
this.#transform = async (doc) => {
|
|
244
|
+
return fun(await oldTransform(doc))
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
this.#transform = fun
|
|
249
|
+
}
|
|
250
|
+
return this
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Throw an error if iteration has already started.
|
|
255
|
+
*
|
|
256
|
+
* @throws {DbError}
|
|
257
|
+
* @protected
|
|
258
|
+
* @internal
|
|
259
|
+
*/
|
|
260
|
+
_throwIfInitialized() {
|
|
261
|
+
if (this._initialized) {
|
|
262
|
+
throw new DbError(CURSOR_INIT_ERR_MESSAGE)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Initialize the cursor by making the first request to the API.
|
|
268
|
+
* This is called automatically when the first call to hasNext() is made.
|
|
269
|
+
* After initialization, no further modifications to the request can be made.
|
|
270
|
+
*
|
|
271
|
+
* @returns {Promise<void>}
|
|
272
|
+
*/
|
|
273
|
+
async #initialize() {
|
|
274
|
+
// Register the cursor as active with the DbClient then make the first request
|
|
275
|
+
this._dbClient.registerCursor(this)
|
|
276
|
+
const firstBatch = await this._firstRequest()
|
|
277
|
+
this._id = firstBatch.cursor.id
|
|
278
|
+
this._buffer = firstBatch.cursor['firstBatch']
|
|
279
|
+
this._initialized = true
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Method to be implemented by subclasses to make the first API request.
|
|
284
|
+
* Should be a single call to apiRequest.apiPost()
|
|
285
|
+
*
|
|
286
|
+
* @returns {Promise<Object>}
|
|
287
|
+
* @protected
|
|
288
|
+
* @internal
|
|
289
|
+
* @abstract
|
|
290
|
+
*/
|
|
291
|
+
_firstRequest() {
|
|
292
|
+
throw new SyntaxError('The _firstRequest() method must be implemented by the subclass.')
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async* [Symbol.asyncIterator]() {
|
|
296
|
+
while (await this.hasNext()) {
|
|
297
|
+
yield await this.next()
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Get a readable stream version of the cursor with a transform if desired
|
|
303
|
+
*
|
|
304
|
+
* @param {{(doc: Object): Object}=} transform A function that takes a document and applies some transformation
|
|
305
|
+
* @returns {Stream.Readable}
|
|
306
|
+
*/
|
|
307
|
+
stream(transform = undefined) {
|
|
308
|
+
let readable = Readable.from(this)
|
|
309
|
+
if (transform) {
|
|
310
|
+
const transformedStream = readable.pipe(
|
|
311
|
+
new Transform({
|
|
312
|
+
objectMode: true,
|
|
313
|
+
highWaterMark: 1,
|
|
314
|
+
transform(chunk, _, callback) {
|
|
315
|
+
try {
|
|
316
|
+
const transformed = transform(chunk)
|
|
317
|
+
callback(undefined, transformed)
|
|
318
|
+
}
|
|
319
|
+
catch (err) {
|
|
320
|
+
callback(err)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
})
|
|
324
|
+
)
|
|
325
|
+
readable.on('error', err => transformedStream.emit('error', err))
|
|
326
|
+
readable = transformedStream
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return readable
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async _read(size) {
|
|
333
|
+
if (!this.#readInProgress) {
|
|
334
|
+
this.#readInProgress = true
|
|
335
|
+
try {
|
|
336
|
+
if (!(await this.hasNext())) {
|
|
337
|
+
this.push(null)
|
|
338
|
+
return
|
|
339
|
+
}
|
|
340
|
+
this.push(await this.next())
|
|
341
|
+
this.#readInProgress = false
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
this.destroy(err)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
module.exports = AbstractDbCursor
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
|
|
7
|
+
Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
const AbstractDbCursor = require("./AbstractDbCursor")
|
|
13
|
+
const { aggregateApi } = require("./api/collection")
|
|
14
|
+
|
|
15
|
+
class AggregateCursor extends AbstractDbCursor {
|
|
16
|
+
#pipeline
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {DbBase} db
|
|
20
|
+
* @param {DbClient} client
|
|
21
|
+
* @param {string} collectionName
|
|
22
|
+
* @param {Object[]} pipeline
|
|
23
|
+
* @param {Object} options
|
|
24
|
+
*/
|
|
25
|
+
constructor(db, client, collectionName, pipeline, options) {
|
|
26
|
+
super(db, client, collectionName, options)
|
|
27
|
+
this.#pipeline = pipeline
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get the first batch of results from the database
|
|
32
|
+
*
|
|
33
|
+
* @returns {Promise<Object>}
|
|
34
|
+
* @throws {DbError}
|
|
35
|
+
* @protected
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
async _firstRequest() {
|
|
39
|
+
return await aggregateApi(this._db, this._axiosClient, this._collection, this.#pipeline, this._options)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Explains the query
|
|
44
|
+
*
|
|
45
|
+
* @returns {Object}
|
|
46
|
+
*/
|
|
47
|
+
async explain() {
|
|
48
|
+
this._throwIfInitialized()
|
|
49
|
+
this._options.explain = true
|
|
50
|
+
// Explain results don't get iterated on, so we don't need the buffer or session
|
|
51
|
+
const explainResults = await aggregateApi(
|
|
52
|
+
this._db,
|
|
53
|
+
this._db.axiosClient,
|
|
54
|
+
this._collection,
|
|
55
|
+
this.#pipeline,
|
|
56
|
+
this._options
|
|
57
|
+
)
|
|
58
|
+
this._id = 0
|
|
59
|
+
this._sessionClosed = true
|
|
60
|
+
this._buffer = []
|
|
61
|
+
this._initialized = true
|
|
62
|
+
return explainResults
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Add a stage to the aggregation pipeline.
|
|
67
|
+
*
|
|
68
|
+
* @param {Object} stage
|
|
69
|
+
* @returns {this}
|
|
70
|
+
*/
|
|
71
|
+
addStage(stage) {
|
|
72
|
+
this._throwIfInitialized()
|
|
73
|
+
this.#pipeline.push(stage)
|
|
74
|
+
return this
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Add a $group stage to the aggregation pipeline.
|
|
79
|
+
*
|
|
80
|
+
* @param {Object} groupSpec
|
|
81
|
+
* @returns {this}
|
|
82
|
+
*/
|
|
83
|
+
group(groupSpec) {
|
|
84
|
+
return this.addStage({ $group: groupSpec })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Add a $limit stage to the aggregation pipeline.
|
|
89
|
+
* @param {number} limit
|
|
90
|
+
* @returns {this}
|
|
91
|
+
*/
|
|
92
|
+
limit(limit) {
|
|
93
|
+
return this.addStage({ $limit: limit })
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Add a $match stage to the aggregation pipeline.
|
|
98
|
+
*
|
|
99
|
+
* @param {Object} matchSpec
|
|
100
|
+
* @returns {this}
|
|
101
|
+
*/
|
|
102
|
+
match(matchSpec) {
|
|
103
|
+
return this.addStage({ $match: matchSpec })
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Add an $out stage to the aggregation pipeline.
|
|
108
|
+
*
|
|
109
|
+
* @param {(Object|string)} outSpec
|
|
110
|
+
* @returns {this}
|
|
111
|
+
*/
|
|
112
|
+
out(outSpec) {
|
|
113
|
+
return this.addStage({ $out: outSpec })
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Add a $project stage to the aggregation pipeline.
|
|
118
|
+
*
|
|
119
|
+
* @param {Object} projectSpec
|
|
120
|
+
* @returns {this}
|
|
121
|
+
*/
|
|
122
|
+
project(projectSpec) {
|
|
123
|
+
return this.addStage({ $project: projectSpec })
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Add a $lookup stage to the aggregation pipeline
|
|
128
|
+
*
|
|
129
|
+
* @param {Object}lookupSpec
|
|
130
|
+
* @returns {this}
|
|
131
|
+
*/
|
|
132
|
+
lookup(lookupSpec) {
|
|
133
|
+
return this.addStage({ $lookup: lookupSpec })
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Add a $redact stage to the aggregation pipeline
|
|
138
|
+
*
|
|
139
|
+
* @param {Object} redactSpec
|
|
140
|
+
* @returns {this}
|
|
141
|
+
*/
|
|
142
|
+
redact(redactSpec) {
|
|
143
|
+
return this.addStage({ $redact: redactSpec })
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Add a $skip stage to the aggregation pipeline.
|
|
148
|
+
*
|
|
149
|
+
* @param {number} skip
|
|
150
|
+
* @returns {this}
|
|
151
|
+
*/
|
|
152
|
+
skip(skip) {
|
|
153
|
+
return this.addStage({ $skip: skip })
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Add a $sort stage to the aggregation pipeline
|
|
158
|
+
*
|
|
159
|
+
* @param {(Object|string|number)} sortSpec
|
|
160
|
+
* @returns {this}
|
|
161
|
+
*/
|
|
162
|
+
sort(sortSpec) {
|
|
163
|
+
return this.addStage({ $sort: sortSpec })
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Add a $count stage to the aggregation pipeline
|
|
168
|
+
*
|
|
169
|
+
* @param {(Object|string)} unwindSpec
|
|
170
|
+
* @returns {this}
|
|
171
|
+
*/
|
|
172
|
+
unwind(unwindSpec) {
|
|
173
|
+
return this.addStage({ $unwind: unwindSpec })
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Add a $geoNear stage to the aggregation pipeline
|
|
178
|
+
*
|
|
179
|
+
* @param {Object} geoNearSpec
|
|
180
|
+
* @returns {this}
|
|
181
|
+
*/
|
|
182
|
+
geoNear(geoNearSpec) {
|
|
183
|
+
return this.addStage({ $geoNear: geoNearSpec })
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = AggregateCursor
|
package/lib/DbBase.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
|
|
7
|
+
Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
require('dotenv/config')
|
|
13
|
+
const { pingApi, provisionStatusApi, provisionRequestApi, deleteDatabaseApi } = require('./api/db')
|
|
14
|
+
const DbClient = require('./DbClient')
|
|
15
|
+
const DbError = require("./DbError")
|
|
16
|
+
const { getAxiosClient } = require("../utils/axiosUtils")
|
|
17
|
+
const {
|
|
18
|
+
ALLOWED_REGIONS, STAGE_ENV, STAGE_ENDPOINT, PROD_ENDPOINT_RUNTIME, PROD_ENDPOINT_EXTERNAL
|
|
19
|
+
} = require("./constants")
|
|
20
|
+
const { getCliEnv } = require("@adobe/aio-lib-env")
|
|
21
|
+
|
|
22
|
+
class DbBase {
|
|
23
|
+
/**
|
|
24
|
+
* @param {string} region
|
|
25
|
+
* @param {string} runtimeNamespace
|
|
26
|
+
* @param {string} runtimeAuth
|
|
27
|
+
* @hideconstructor
|
|
28
|
+
*/
|
|
29
|
+
constructor(region, runtimeNamespace, runtimeAuth) {
|
|
30
|
+
this.runtimeAuth = runtimeAuth
|
|
31
|
+
if (!this.runtimeAuth) {
|
|
32
|
+
throw new DbError('Runtime auth is required')
|
|
33
|
+
}
|
|
34
|
+
if (!/.+:.+/.test(this.runtimeAuth)) {
|
|
35
|
+
throw new DbError("Invalid format for runtime auth, must be '<user>:<pass>'")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.runtimeNamespace = runtimeNamespace
|
|
39
|
+
if (!this.runtimeNamespace) {
|
|
40
|
+
throw new DbError('Runtime namespace is required')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
this.region = region.toLowerCase()
|
|
44
|
+
const env = process.env.AIO_DB_ENVIRONMENT || getCliEnv()
|
|
45
|
+
const validRegions = ALLOWED_REGIONS[env]
|
|
46
|
+
if (!validRegions.includes(this.region)) {
|
|
47
|
+
throw new DbError(`Invalid region '${region}' for the ${env} environment, must be one of: ${validRegions.join(', ')}`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let serviceUrl
|
|
51
|
+
// Allow overriding service URL via environment variable for testing
|
|
52
|
+
if (process.env.AIO_DB_ENDPOINT) {
|
|
53
|
+
serviceUrl = process.env.AIO_DB_ENDPOINT
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
if (env === STAGE_ENV) {
|
|
57
|
+
// Stage environment does not have a separate runtime endpoint
|
|
58
|
+
serviceUrl = STAGE_ENDPOINT
|
|
59
|
+
}
|
|
60
|
+
else if (process.env.__OW_ACTIVATION_ID) {
|
|
61
|
+
// If __OW_ACTIVATION_ID is set, the sdk is being used from inside a runtime action
|
|
62
|
+
serviceUrl = PROD_ENDPOINT_RUNTIME
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
serviceUrl = PROD_ENDPOINT_EXTERNAL
|
|
66
|
+
}
|
|
67
|
+
serviceUrl = serviceUrl.replaceAll(/<region>/gi, this.region)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.serviceUrl = serviceUrl
|
|
71
|
+
this.axiosClient = getAxiosClient()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Instantiates and returns a new DbBase object
|
|
76
|
+
*
|
|
77
|
+
* @static
|
|
78
|
+
* @constructs
|
|
79
|
+
* @param {Object=} config
|
|
80
|
+
* @param {string=} config.namespace required here or as __OW_API_NAMESPACE in .env
|
|
81
|
+
* @param {string=} config.apikey required here or as __OW_API_KEY in .env
|
|
82
|
+
* @param {string=} config.region optional, default is 'amer'. Allowed prod values are 'amer', 'emea', 'apac'
|
|
83
|
+
* @returns {Promise<DbBase>} a new DbBase instance
|
|
84
|
+
* @memberof DbBase
|
|
85
|
+
*/
|
|
86
|
+
static async init(config = {}) {
|
|
87
|
+
const namespace = config.namespace || process.env.__OW_NAMESPACE
|
|
88
|
+
const apikey = config.apikey || process.env.__OW_API_KEY
|
|
89
|
+
const region = config.region || ALLOWED_REGIONS[getCliEnv()].at(0)
|
|
90
|
+
return new DbBase(region, namespace, apikey)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Send a request to provision a database scoped to the configured AIO runtime namespace
|
|
95
|
+
*
|
|
96
|
+
* @returns {Promise<Object>}
|
|
97
|
+
* @throws {DbError}
|
|
98
|
+
* @memberof DbBase
|
|
99
|
+
*/
|
|
100
|
+
async provisionRequest() {
|
|
101
|
+
return provisionRequestApi(this)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check the provisioning status for the configured AIO runtime namespace
|
|
106
|
+
*
|
|
107
|
+
* @returns {Promise<Object>}
|
|
108
|
+
* @throws {DbError}
|
|
109
|
+
* @memberof DbBase
|
|
110
|
+
*/
|
|
111
|
+
async provisionStatus() {
|
|
112
|
+
return provisionStatusApi(this)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Initialize connection to App Builder Database Service
|
|
117
|
+
*
|
|
118
|
+
* @returns {Promise<DbClient>} a new DbClient instance
|
|
119
|
+
* @throws {DbError}
|
|
120
|
+
* @memberof DbBase
|
|
121
|
+
*/
|
|
122
|
+
async connect() {
|
|
123
|
+
return DbClient.init(this)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* General connectivity check with App Builder Database Service
|
|
128
|
+
*
|
|
129
|
+
* @returns {Promise<Object>} ping results
|
|
130
|
+
* @throws {DbError}
|
|
131
|
+
* @memberof DbBase
|
|
132
|
+
*/
|
|
133
|
+
async ping() {
|
|
134
|
+
return await pingApi(this)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Delete the tenant database for the configured AIO runtime namespace
|
|
139
|
+
*
|
|
140
|
+
* @returns {Promise<Object>}
|
|
141
|
+
* @throws {DbError}
|
|
142
|
+
* @memberof DbBase
|
|
143
|
+
*/
|
|
144
|
+
async deleteDatabase() {
|
|
145
|
+
return deleteDatabaseApi(this)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = DbBase
|