@flowerforce/flowerbase-client 0.1.1-beta.8 → 0.3.0
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 +19 -0
- package/README.md +12 -1
- package/dist/mongo.d.ts.map +1 -1
- package/dist/mongo.js +29 -25
- package/dist/types.d.ts +2 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/watch.d.ts.map +1 -1
- package/dist/watch.js +6 -5
- package/package.json +1 -1
- package/src/__tests__/watch.test.ts +131 -7
- package/src/mongo.ts +29 -24
- package/src/types.ts +2 -2
- package/src/watch.ts +11 -5
package/CHANGELOG.md
CHANGED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
## 0.3.0 (2026-03-20)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### 🚀 Features
|
|
5
|
+
|
|
6
|
+
- add MongoDB Client-Side Field Level Encryption support ([#35](https://github.com/flowerforce/flowerbase/pull/35))
|
|
7
|
+
|
|
8
|
+
- **monitoring:** Add collapsable editor ([#34](https://github.com/flowerforce/flowerbase/pull/34))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### 🩹 Fixes
|
|
12
|
+
|
|
13
|
+
- watch operationType delete ([afcbec2](https://github.com/flowerforce/flowerbase/commit/afcbec2))
|
|
14
|
+
|
|
15
|
+
- propagate runAsSystem ([#41](https://github.com/flowerforce/flowerbase/pull/41))
|
|
16
|
+
|
|
17
|
+
- add setHeader ([#44](https://github.com/flowerforce/flowerbase/pull/44))
|
|
18
|
+
|
|
19
|
+
- 401 login ([fd13ff0](https://github.com/flowerforce/flowerbase/commit/fd13ff0))
|
package/README.md
CHANGED
|
@@ -125,7 +125,7 @@ Metodi disponibili su `collection`:
|
|
|
125
125
|
- `updateOne(filter, update, options?)`
|
|
126
126
|
- `updateMany(filter, update, options?)`
|
|
127
127
|
- `deleteOne(filter, options?)`
|
|
128
|
-
- `watch(
|
|
128
|
+
- `watch(options?)`
|
|
129
129
|
|
|
130
130
|
## Watch / Change streams
|
|
131
131
|
|
|
@@ -143,6 +143,17 @@ try {
|
|
|
143
143
|
}
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
+
Esempio con filtro Realm-like:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
const stream = collection.watch({
|
|
150
|
+
filter: {
|
|
151
|
+
operationType: 'update',
|
|
152
|
+
'fullDocument.type': 'perennial'
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
```
|
|
156
|
+
|
|
146
157
|
## BSON / EJSON
|
|
147
158
|
|
|
148
159
|
Il client esporta anche:
|
package/dist/mongo.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mongo.d.ts","sourceRoot":"","sources":["../src/mongo.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,OAAO,CAAA;AAEhC,OAAO,EAAkB,eAAe,EAAE,MAAM,SAAS,CAAA;AAoBzD,eAAO,MAAM,iBAAiB,QAAS,GAAG,eAAe,MAAM,UAAU,MAAM,KAAG,
|
|
1
|
+
{"version":3,"file":"mongo.d.ts","sourceRoot":"","sources":["../src/mongo.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,OAAO,CAAA;AAEhC,OAAO,EAAkB,eAAe,EAAE,MAAM,SAAS,CAAA;AAoBzD,eAAO,MAAM,iBAAiB,QAAS,GAAG,eAAe,MAAM,UAAU,MAAM,KAAG,eAyFhF,CAAA"}
|
package/dist/mongo.js
CHANGED
|
@@ -35,37 +35,41 @@ const createMongoClient = (app, serviceName, userId) => ({
|
|
|
35
35
|
return mapResult(result);
|
|
36
36
|
};
|
|
37
37
|
const normalizeWatchInput = (input) => {
|
|
38
|
+
if (typeof input === 'undefined') {
|
|
39
|
+
return { filter: undefined, ids: undefined };
|
|
40
|
+
}
|
|
38
41
|
if (Array.isArray(input)) {
|
|
39
|
-
|
|
42
|
+
throw new Error('watch accepts only an options object with "filter" or "ids"');
|
|
43
|
+
}
|
|
44
|
+
if (!input || typeof input !== 'object') {
|
|
45
|
+
throw new Error('watch options must be an object');
|
|
46
|
+
}
|
|
47
|
+
const typed = input;
|
|
48
|
+
const keys = Object.keys(typed);
|
|
49
|
+
const hasOnlyAllowedKeys = keys.every((key) => key === 'ids' || key === 'filter');
|
|
50
|
+
if (!hasOnlyAllowedKeys) {
|
|
51
|
+
throw new Error('watch options support only "filter" or "ids"');
|
|
40
52
|
}
|
|
41
|
-
if (
|
|
42
|
-
const typed = input;
|
|
53
|
+
if (typed.ids || typed.filter) {
|
|
43
54
|
if (typed.ids && typed.filter) {
|
|
44
55
|
throw new Error('watch options cannot include both "ids" and "filter"');
|
|
45
56
|
}
|
|
46
|
-
const { ids, filter
|
|
57
|
+
const { ids, filter } = typed;
|
|
58
|
+
if (filter && typeof filter === 'object' && '$match' in filter) {
|
|
59
|
+
throw new Error('watch filter must be a query object, not a $match stage');
|
|
60
|
+
}
|
|
47
61
|
if (ids) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
};
|
|
62
|
+
if (!Array.isArray(ids)) {
|
|
63
|
+
throw new Error('watch ids must be an array');
|
|
64
|
+
}
|
|
65
|
+
return { filter: undefined, ids };
|
|
52
66
|
}
|
|
53
67
|
if (filter) {
|
|
54
|
-
return {
|
|
55
|
-
pipeline: [{ $match: filter }],
|
|
56
|
-
options
|
|
57
|
-
};
|
|
68
|
+
return { filter, ids: undefined };
|
|
58
69
|
}
|
|
59
|
-
return {
|
|
60
|
-
}
|
|
61
|
-
if (input && typeof input === 'object' && ('pipeline' in input || 'options' in input)) {
|
|
62
|
-
const typed = input;
|
|
63
|
-
return {
|
|
64
|
-
pipeline: typed.pipeline ?? [],
|
|
65
|
-
options: typed.options ?? {}
|
|
66
|
-
};
|
|
70
|
+
return { filter: undefined, ids: undefined };
|
|
67
71
|
}
|
|
68
|
-
|
|
72
|
+
throw new Error('watch options must include "filter" or "ids"');
|
|
69
73
|
};
|
|
70
74
|
return {
|
|
71
75
|
find: (query = {}, options = {}) => callService('find', [{ query, options }]),
|
|
@@ -81,8 +85,8 @@ const createMongoClient = (app, serviceName, userId) => ({
|
|
|
81
85
|
updateMany: (filter, update, options = {}) => callService('updateMany', [{ filter, update, options }]),
|
|
82
86
|
deleteOne: (filter, options = {}) => callService('deleteOne', [{ query: filter, options }]),
|
|
83
87
|
deleteMany: (filter, options = {}) => callService('deleteMany', [{ query: filter, options }]),
|
|
84
|
-
watch: (
|
|
85
|
-
const {
|
|
88
|
+
watch: (options) => {
|
|
89
|
+
const { filter, ids } = normalizeWatchInput(options);
|
|
86
90
|
const session = app.getSessionOrThrow(userId);
|
|
87
91
|
return (0, watch_1.createWatchIterator)({
|
|
88
92
|
appId: app.id,
|
|
@@ -90,8 +94,8 @@ const createMongoClient = (app, serviceName, userId) => ({
|
|
|
90
94
|
accessToken: session.accessToken,
|
|
91
95
|
database,
|
|
92
96
|
collection,
|
|
93
|
-
|
|
94
|
-
|
|
97
|
+
filter,
|
|
98
|
+
ids,
|
|
95
99
|
timeout: app.timeout
|
|
96
100
|
});
|
|
97
101
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -39,8 +39,8 @@ export type WatchConfig = {
|
|
|
39
39
|
accessToken: string;
|
|
40
40
|
database: string;
|
|
41
41
|
collection: string;
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
filter?: Record<string, unknown>;
|
|
43
|
+
ids?: unknown[];
|
|
44
44
|
timeout?: number;
|
|
45
45
|
};
|
|
46
46
|
export type WatchAsyncIterator<TChange = unknown> = AsyncIterableIterator<TChange> & {
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,SAAS,GAAG;IACtB,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,eAAe,GACvB;IAAE,QAAQ,EAAE,gBAAgB,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GAC/D;IAAE,QAAQ,EAAE,WAAW,CAAA;CAAE,GACzB;IAAE,QAAQ,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,GACjE;IAAE,QAAQ,EAAE,cAAc,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAE/C,MAAM,MAAM,WAAW,GAAG;IACxB,WAAW,EAAE,MAAM,CAAA;IACnB,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;CACf,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ,UAAU,CAAC,EAAE,OAAO,EAAE,CAAA;IACtB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IACrC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAC/B,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,MAAM,CAAA;IACZ,SAAS,EAAE,OAAO,EAAE,CAAA;IACpB,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/watch.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../src/watch.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../src/watch.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAyCzD,eAAO,MAAM,mBAAmB,WAAY,WAAW,KAAG,mBAAmB,OAAO,CAiHnF,CAAA"}
|
package/dist/watch.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createWatchIterator = void 0;
|
|
4
|
+
const bson_1 = require("./bson");
|
|
4
5
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
5
|
-
const createWatchRequest = ({ database, collection,
|
|
6
|
+
const createWatchRequest = ({ database, collection, filter, ids }) => ({
|
|
6
7
|
name: 'watch',
|
|
7
8
|
service: 'mongodb-atlas',
|
|
8
9
|
arguments: [
|
|
9
10
|
{
|
|
10
11
|
database,
|
|
11
12
|
collection,
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
...(filter ? { filter } : {}),
|
|
14
|
+
...(ids ? { ids } : {})
|
|
14
15
|
}
|
|
15
16
|
]
|
|
16
17
|
});
|
|
@@ -27,7 +28,7 @@ const parseSsePayload = (line) => {
|
|
|
27
28
|
if (!raw)
|
|
28
29
|
return null;
|
|
29
30
|
try {
|
|
30
|
-
return JSON.parse(raw);
|
|
31
|
+
return bson_1.EJSON.deserialize(JSON.parse(raw));
|
|
31
32
|
}
|
|
32
33
|
catch {
|
|
33
34
|
return raw;
|
|
@@ -62,7 +63,7 @@ const createWatchIterator = (config) => {
|
|
|
62
63
|
const controller = new AbortController();
|
|
63
64
|
activeController = controller;
|
|
64
65
|
const request = createWatchRequest(config);
|
|
65
|
-
const encoded = toBase64(JSON.stringify(request));
|
|
66
|
+
const encoded = toBase64(JSON.stringify(bson_1.EJSON.serialize(request, { relaxed: false })));
|
|
66
67
|
const url = `${config.baseUrl}/api/client/v2.0/app/${config.appId}/functions/call?baas_request=${encodeURIComponent(encoded)}`;
|
|
67
68
|
try {
|
|
68
69
|
const response = await fetch(url, {
|
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { App } from '../app'
|
|
2
|
+
import { EJSON, ObjectId } from '../bson'
|
|
2
3
|
import { Credentials } from '../credentials'
|
|
3
4
|
|
|
4
5
|
const streamFromLines = (lines: string[]) => {
|
|
@@ -19,8 +20,11 @@ const decodeBaasRequest = (url: string) => {
|
|
|
19
20
|
if (!encoded) throw new Error('baas_request missing')
|
|
20
21
|
const base64 = decodeURIComponent(encoded)
|
|
21
22
|
const json = Buffer.from(base64, 'base64').toString('utf8')
|
|
22
|
-
return JSON.parse(json) as {
|
|
23
|
-
arguments: Array<{
|
|
23
|
+
return EJSON.deserialize(JSON.parse(json)) as {
|
|
24
|
+
arguments: Array<{
|
|
25
|
+
filter?: Record<string, unknown>
|
|
26
|
+
ids?: unknown[]
|
|
27
|
+
}>
|
|
24
28
|
}
|
|
25
29
|
}
|
|
26
30
|
|
|
@@ -147,7 +151,7 @@ describe('flowerbase-client watch', () => {
|
|
|
147
151
|
iterator.close()
|
|
148
152
|
})
|
|
149
153
|
|
|
150
|
-
it('maps watch ids/filter options into
|
|
154
|
+
it('maps watch ids/filter options into watch arguments', async () => {
|
|
151
155
|
global.fetch = jest
|
|
152
156
|
.fn()
|
|
153
157
|
.mockResolvedValueOnce({
|
|
@@ -175,14 +179,109 @@ describe('flowerbase-client watch', () => {
|
|
|
175
179
|
const byIds = collection.watch({ ids: ['id-1', 'id-2'] })
|
|
176
180
|
byIds.close()
|
|
177
181
|
const firstRequest = decodeBaasRequest((global.fetch as jest.Mock).mock.calls[2][0] as string)
|
|
178
|
-
expect(firstRequest.arguments[0].
|
|
179
|
-
|
|
180
|
-
])
|
|
182
|
+
expect(firstRequest.arguments[0].ids).toEqual(['id-1', 'id-2'])
|
|
183
|
+
expect(firstRequest.arguments[0].filter).toBeUndefined()
|
|
181
184
|
|
|
182
185
|
const byFilter = collection.watch({ filter: { operationType: 'insert' } })
|
|
183
186
|
byFilter.close()
|
|
184
187
|
const secondRequest = decodeBaasRequest((global.fetch as jest.Mock).mock.calls[3][0] as string)
|
|
185
|
-
expect(secondRequest.arguments[0].
|
|
188
|
+
expect(secondRequest.arguments[0].filter).toEqual({ operationType: 'insert' })
|
|
189
|
+
expect(secondRequest.arguments[0].ids).toBeUndefined()
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('preserves ObjectId values in watch filter payload', async () => {
|
|
193
|
+
global.fetch = jest
|
|
194
|
+
.fn()
|
|
195
|
+
.mockResolvedValueOnce({
|
|
196
|
+
ok: true,
|
|
197
|
+
text: async () => JSON.stringify({
|
|
198
|
+
access_token: 'access',
|
|
199
|
+
refresh_token: 'refresh',
|
|
200
|
+
user_id: 'user-1'
|
|
201
|
+
})
|
|
202
|
+
})
|
|
203
|
+
.mockResolvedValueOnce({
|
|
204
|
+
ok: true,
|
|
205
|
+
text: async () => JSON.stringify({ access_token: 'access' })
|
|
206
|
+
})
|
|
207
|
+
.mockResolvedValue({
|
|
208
|
+
ok: true,
|
|
209
|
+
body: streamFromLines([])
|
|
210
|
+
}) as unknown as typeof fetch
|
|
211
|
+
|
|
212
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
213
|
+
await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
214
|
+
|
|
215
|
+
const collection = app.currentUser!.mongoClient('mongodb-atlas').db('testdb').collection('todos')
|
|
216
|
+
const requestId = new ObjectId('69a282a75cd849c244e001ca')
|
|
217
|
+
|
|
218
|
+
const iterator = collection.watch({
|
|
219
|
+
filter: {
|
|
220
|
+
'fullDocument.requestId': requestId,
|
|
221
|
+
operationType: 'update'
|
|
222
|
+
}
|
|
223
|
+
})
|
|
224
|
+
iterator.close()
|
|
225
|
+
|
|
226
|
+
const request = decodeBaasRequest((global.fetch as jest.Mock).mock.calls[2][0] as string)
|
|
227
|
+
const decodedRequestId = request.arguments[0].filter?.['fullDocument.requestId']
|
|
228
|
+
expect(decodedRequestId).toBeInstanceOf(ObjectId)
|
|
229
|
+
expect((decodedRequestId as ObjectId).toHexString()).toBe(requestId.toHexString())
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('rejects pipeline-based watch signature', async () => {
|
|
233
|
+
global.fetch = jest
|
|
234
|
+
.fn()
|
|
235
|
+
.mockResolvedValueOnce({
|
|
236
|
+
ok: true,
|
|
237
|
+
text: async () => JSON.stringify({
|
|
238
|
+
access_token: 'access',
|
|
239
|
+
refresh_token: 'refresh',
|
|
240
|
+
user_id: 'user-1'
|
|
241
|
+
})
|
|
242
|
+
})
|
|
243
|
+
.mockResolvedValueOnce({
|
|
244
|
+
ok: true,
|
|
245
|
+
text: async () => JSON.stringify({ access_token: 'access' })
|
|
246
|
+
})
|
|
247
|
+
.mockResolvedValue({
|
|
248
|
+
ok: true,
|
|
249
|
+
body: streamFromLines([])
|
|
250
|
+
}) as unknown as typeof fetch
|
|
251
|
+
|
|
252
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
253
|
+
await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
254
|
+
|
|
255
|
+
const collection = app.currentUser!.mongoClient('mongodb-atlas').db('testdb').collection('todos')
|
|
256
|
+
|
|
257
|
+
expect(() =>
|
|
258
|
+
collection.watch([{ $match: { operationType: 'update', 'fullDocument.type': 'perennial' } }])
|
|
259
|
+
).toThrow('watch accepts only an options object with "filter" or "ids"')
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
it('rejects unsupported watch option keys', async () => {
|
|
263
|
+
global.fetch = jest
|
|
264
|
+
.fn()
|
|
265
|
+
.mockResolvedValueOnce({
|
|
266
|
+
ok: true,
|
|
267
|
+
text: async () => JSON.stringify({
|
|
268
|
+
access_token: 'access',
|
|
269
|
+
refresh_token: 'refresh',
|
|
270
|
+
user_id: 'user-1'
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
.mockResolvedValueOnce({
|
|
274
|
+
ok: true,
|
|
275
|
+
text: async () => JSON.stringify({ access_token: 'access' })
|
|
276
|
+
}) as unknown as typeof fetch
|
|
277
|
+
|
|
278
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
279
|
+
await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
280
|
+
|
|
281
|
+
const collection = app.currentUser!.mongoClient('mongodb-atlas').db('testdb').collection('todos')
|
|
282
|
+
expect(() => collection.watch({ fullDocument: 'updateLookup' })).toThrow(
|
|
283
|
+
'watch options support only "filter" or "ids"'
|
|
284
|
+
)
|
|
186
285
|
})
|
|
187
286
|
|
|
188
287
|
it('rejects watch options with both ids and filter', async () => {
|
|
@@ -209,4 +308,29 @@ describe('flowerbase-client watch', () => {
|
|
|
209
308
|
'watch options cannot include both "ids" and "filter"'
|
|
210
309
|
)
|
|
211
310
|
})
|
|
311
|
+
|
|
312
|
+
it('rejects $match inside watch filter object', async () => {
|
|
313
|
+
global.fetch = jest
|
|
314
|
+
.fn()
|
|
315
|
+
.mockResolvedValueOnce({
|
|
316
|
+
ok: true,
|
|
317
|
+
text: async () => JSON.stringify({
|
|
318
|
+
access_token: 'access',
|
|
319
|
+
refresh_token: 'refresh',
|
|
320
|
+
user_id: 'user-1'
|
|
321
|
+
})
|
|
322
|
+
})
|
|
323
|
+
.mockResolvedValueOnce({
|
|
324
|
+
ok: true,
|
|
325
|
+
text: async () => JSON.stringify({ access_token: 'access' })
|
|
326
|
+
}) as unknown as typeof fetch
|
|
327
|
+
|
|
328
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
329
|
+
await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
330
|
+
|
|
331
|
+
const collection = app.currentUser!.mongoClient('mongodb-atlas').db('testdb').collection('todos')
|
|
332
|
+
expect(() => collection.watch({ filter: { $match: { operationType: 'insert' } } })).toThrow(
|
|
333
|
+
'watch filter must be a query object, not a $match stage'
|
|
334
|
+
)
|
|
335
|
+
})
|
|
212
336
|
})
|
package/src/mongo.ts
CHANGED
|
@@ -36,37 +36,42 @@ export const createMongoClient = (app: App, serviceName: string, userId: string)
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const normalizeWatchInput = (input?: unknown) => {
|
|
39
|
+
if (typeof input === 'undefined') {
|
|
40
|
+
return { filter: undefined, ids: undefined }
|
|
41
|
+
}
|
|
39
42
|
if (Array.isArray(input)) {
|
|
40
|
-
|
|
43
|
+
throw new Error('watch accepts only an options object with "filter" or "ids"')
|
|
44
|
+
}
|
|
45
|
+
if (!input || typeof input !== 'object') {
|
|
46
|
+
throw new Error('watch options must be an object')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const typed = input as { ids?: unknown[]; filter?: Record<string, unknown>; [key: string]: unknown }
|
|
50
|
+
const keys = Object.keys(typed)
|
|
51
|
+
const hasOnlyAllowedKeys = keys.every((key) => key === 'ids' || key === 'filter')
|
|
52
|
+
if (!hasOnlyAllowedKeys) {
|
|
53
|
+
throw new Error('watch options support only "filter" or "ids"')
|
|
41
54
|
}
|
|
42
|
-
if (
|
|
43
|
-
const typed = input as { ids?: unknown[]; filter?: Record<string, unknown>; [key: string]: unknown }
|
|
55
|
+
if (typed.ids || typed.filter) {
|
|
44
56
|
if (typed.ids && typed.filter) {
|
|
45
57
|
throw new Error('watch options cannot include both "ids" and "filter"')
|
|
46
58
|
}
|
|
47
|
-
const { ids, filter
|
|
59
|
+
const { ids, filter } = typed
|
|
60
|
+
if (filter && typeof filter === 'object' && '$match' in filter) {
|
|
61
|
+
throw new Error('watch filter must be a query object, not a $match stage')
|
|
62
|
+
}
|
|
48
63
|
if (ids) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
options
|
|
64
|
+
if (!Array.isArray(ids)) {
|
|
65
|
+
throw new Error('watch ids must be an array')
|
|
52
66
|
}
|
|
67
|
+
return { filter: undefined, ids }
|
|
53
68
|
}
|
|
54
69
|
if (filter) {
|
|
55
|
-
return {
|
|
56
|
-
pipeline: [{ $match: filter }],
|
|
57
|
-
options
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return { pipeline: [], options }
|
|
61
|
-
}
|
|
62
|
-
if (input && typeof input === 'object' && ('pipeline' in input || 'options' in input)) {
|
|
63
|
-
const typed = input as { pipeline?: unknown[]; options?: Record<string, unknown> }
|
|
64
|
-
return {
|
|
65
|
-
pipeline: typed.pipeline ?? [],
|
|
66
|
-
options: typed.options ?? {}
|
|
70
|
+
return { filter, ids: undefined }
|
|
67
71
|
}
|
|
72
|
+
return { filter: undefined, ids: undefined }
|
|
68
73
|
}
|
|
69
|
-
|
|
74
|
+
throw new Error('watch options must include "filter" or "ids"')
|
|
70
75
|
}
|
|
71
76
|
|
|
72
77
|
return {
|
|
@@ -87,8 +92,8 @@ export const createMongoClient = (app: App, serviceName: string, userId: string)
|
|
|
87
92
|
callService('updateMany', [{ filter, update, options }]),
|
|
88
93
|
deleteOne: (filter, options = {}) => callService('deleteOne', [{ query: filter, options }]),
|
|
89
94
|
deleteMany: (filter, options = {}) => callService('deleteMany', [{ query: filter, options }]),
|
|
90
|
-
watch: (
|
|
91
|
-
const {
|
|
95
|
+
watch: (options) => {
|
|
96
|
+
const { filter, ids } = normalizeWatchInput(options)
|
|
92
97
|
const session = app.getSessionOrThrow(userId)
|
|
93
98
|
return createWatchIterator({
|
|
94
99
|
appId: app.id,
|
|
@@ -96,8 +101,8 @@ export const createMongoClient = (app: App, serviceName: string, userId: string)
|
|
|
96
101
|
accessToken: session.accessToken,
|
|
97
102
|
database,
|
|
98
103
|
collection,
|
|
99
|
-
|
|
100
|
-
|
|
104
|
+
filter,
|
|
105
|
+
ids,
|
|
101
106
|
timeout: app.timeout
|
|
102
107
|
})
|
|
103
108
|
}
|
package/src/types.ts
CHANGED
package/src/watch.ts
CHANGED
|
@@ -1,16 +1,22 @@
|
|
|
1
|
+
import { EJSON } from './bson'
|
|
1
2
|
import { WatchAsyncIterator, WatchConfig } from './types'
|
|
2
3
|
|
|
3
4
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
4
5
|
|
|
5
|
-
const createWatchRequest = ({
|
|
6
|
+
const createWatchRequest = ({
|
|
7
|
+
database,
|
|
8
|
+
collection,
|
|
9
|
+
filter,
|
|
10
|
+
ids
|
|
11
|
+
}: WatchConfig) => ({
|
|
6
12
|
name: 'watch',
|
|
7
13
|
service: 'mongodb-atlas',
|
|
8
14
|
arguments: [
|
|
9
15
|
{
|
|
10
16
|
database,
|
|
11
17
|
collection,
|
|
12
|
-
|
|
13
|
-
|
|
18
|
+
...(filter ? { filter } : {}),
|
|
19
|
+
...(ids ? { ids } : {})
|
|
14
20
|
}
|
|
15
21
|
]
|
|
16
22
|
})
|
|
@@ -28,7 +34,7 @@ const parseSsePayload = (line: string) => {
|
|
|
28
34
|
if (!raw) return null
|
|
29
35
|
|
|
30
36
|
try {
|
|
31
|
-
return JSON.parse(raw)
|
|
37
|
+
return EJSON.deserialize(JSON.parse(raw))
|
|
32
38
|
} catch {
|
|
33
39
|
return raw
|
|
34
40
|
}
|
|
@@ -65,7 +71,7 @@ export const createWatchIterator = (config: WatchConfig): WatchAsyncIterator<unk
|
|
|
65
71
|
const controller = new AbortController()
|
|
66
72
|
activeController = controller
|
|
67
73
|
const request = createWatchRequest(config)
|
|
68
|
-
const encoded = toBase64(JSON.stringify(request))
|
|
74
|
+
const encoded = toBase64(JSON.stringify(EJSON.serialize(request, { relaxed: false })))
|
|
69
75
|
const url = `${config.baseUrl}/api/client/v2.0/app/${config.appId}/functions/call?baas_request=${encodeURIComponent(encoded)}`
|
|
70
76
|
|
|
71
77
|
try {
|