@flowerforce/flowerbase-client 0.1.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.
Files changed (63) hide show
  1. package/CHANGELOG.md +0 -0
  2. package/LICENSE +3 -0
  3. package/README.md +209 -0
  4. package/dist/app.d.ts +85 -0
  5. package/dist/app.d.ts.map +1 -0
  6. package/dist/app.js +461 -0
  7. package/dist/bson.d.ts +8 -0
  8. package/dist/bson.d.ts.map +1 -0
  9. package/dist/bson.js +10 -0
  10. package/dist/credentials.d.ts +8 -0
  11. package/dist/credentials.d.ts.map +1 -0
  12. package/dist/credentials.js +30 -0
  13. package/dist/functions.d.ts +6 -0
  14. package/dist/functions.d.ts.map +1 -0
  15. package/dist/functions.js +47 -0
  16. package/dist/http.d.ts +35 -0
  17. package/dist/http.d.ts.map +1 -0
  18. package/dist/http.js +170 -0
  19. package/dist/index.d.ts +8 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +16 -0
  22. package/dist/mongo.d.ts +4 -0
  23. package/dist/mongo.d.ts.map +1 -0
  24. package/dist/mongo.js +106 -0
  25. package/dist/session.d.ts +18 -0
  26. package/dist/session.d.ts.map +1 -0
  27. package/dist/session.js +105 -0
  28. package/dist/session.native.d.ts +14 -0
  29. package/dist/session.native.d.ts.map +1 -0
  30. package/dist/session.native.js +76 -0
  31. package/dist/types.d.ts +97 -0
  32. package/dist/types.d.ts.map +1 -0
  33. package/dist/types.js +2 -0
  34. package/dist/user.d.ts +37 -0
  35. package/dist/user.d.ts.map +1 -0
  36. package/dist/user.js +125 -0
  37. package/dist/watch.d.ts +3 -0
  38. package/dist/watch.d.ts.map +1 -0
  39. package/dist/watch.js +139 -0
  40. package/jest.config.ts +13 -0
  41. package/package.json +41 -0
  42. package/project.json +11 -0
  43. package/rollup.config.js +17 -0
  44. package/src/__tests__/auth.test.ts +213 -0
  45. package/src/__tests__/compat.test.ts +22 -0
  46. package/src/__tests__/functions.test.ts +312 -0
  47. package/src/__tests__/mongo.test.ts +83 -0
  48. package/src/__tests__/session.test.ts +597 -0
  49. package/src/__tests__/watch.test.ts +336 -0
  50. package/src/app.ts +562 -0
  51. package/src/bson.ts +6 -0
  52. package/src/credentials.ts +31 -0
  53. package/src/functions.ts +56 -0
  54. package/src/http.ts +221 -0
  55. package/src/index.ts +15 -0
  56. package/src/mongo.ts +112 -0
  57. package/src/session.native.ts +89 -0
  58. package/src/session.ts +114 -0
  59. package/src/types.ts +114 -0
  60. package/src/user.ts +150 -0
  61. package/src/watch.ts +156 -0
  62. package/tsconfig.json +34 -0
  63. package/tsconfig.spec.json +13 -0
@@ -0,0 +1,336 @@
1
+ import { App } from '../app'
2
+ import { EJSON, ObjectId } from '../bson'
3
+ import { Credentials } from '../credentials'
4
+
5
+ const streamFromLines = (lines: string[]) => {
6
+ const encoded = lines.map((line) => `${line}\n`).join('')
7
+ const bytes = new TextEncoder().encode(encoded)
8
+
9
+ return new ReadableStream<Uint8Array>({
10
+ start(controller) {
11
+ controller.enqueue(bytes)
12
+ controller.close()
13
+ }
14
+ })
15
+ }
16
+
17
+ const decodeBaasRequest = (url: string) => {
18
+ const parsedUrl = new URL(url)
19
+ const encoded = parsedUrl.searchParams.get('baas_request')
20
+ if (!encoded) throw new Error('baas_request missing')
21
+ const base64 = decodeURIComponent(encoded)
22
+ const json = Buffer.from(base64, 'base64').toString('utf8')
23
+ return EJSON.deserialize(JSON.parse(json)) as {
24
+ arguments: Array<{
25
+ filter?: Record<string, unknown>
26
+ ids?: unknown[]
27
+ }>
28
+ }
29
+ }
30
+
31
+ describe('flowerbase-client watch', () => {
32
+ const originalFetch = global.fetch
33
+
34
+ afterEach(() => {
35
+ jest.useRealTimers()
36
+ global.fetch = originalFetch
37
+ })
38
+
39
+ it('receives SSE events through watch iterator', async () => {
40
+ global.fetch = jest
41
+ .fn()
42
+ .mockResolvedValueOnce({
43
+ ok: true,
44
+ text: async () => JSON.stringify({
45
+ access_token: 'access',
46
+ refresh_token: 'refresh',
47
+ user_id: 'user-1'
48
+ })
49
+ })
50
+ .mockResolvedValueOnce({
51
+ ok: true,
52
+ text: async () => JSON.stringify({ access_token: 'access' })
53
+ })
54
+ .mockResolvedValueOnce({
55
+ ok: true,
56
+ body: streamFromLines(['data: {"operationType":"insert","fullDocument":{"title":"A"}}', ''])
57
+ }) as unknown as typeof fetch
58
+
59
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
60
+ await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
61
+
62
+ const iterator = app.currentUser!
63
+ .mongoClient('mongodb-atlas')
64
+ .db('testdb')
65
+ .collection('todos')
66
+ .watch()
67
+
68
+ const first = await iterator.next()
69
+ expect(first.done).toBe(false)
70
+ expect(first.value).toEqual({ operationType: 'insert', fullDocument: { title: 'A' } })
71
+
72
+ iterator.close()
73
+
74
+ const [url, request] = (global.fetch as jest.Mock).mock.calls[2]
75
+ expect(url).toContain('/functions/call?baas_request=')
76
+ expect(request.headers.Authorization).toBe('Bearer access')
77
+ })
78
+
79
+ it('closes iterator on return', async () => {
80
+ global.fetch = jest
81
+ .fn()
82
+ .mockResolvedValueOnce({
83
+ ok: true,
84
+ text: async () => JSON.stringify({
85
+ access_token: 'access',
86
+ refresh_token: 'refresh',
87
+ user_id: 'user-1'
88
+ })
89
+ })
90
+ .mockResolvedValueOnce({
91
+ ok: true,
92
+ text: async () => JSON.stringify({ access_token: 'access' })
93
+ })
94
+ .mockResolvedValue({
95
+ ok: true,
96
+ body: streamFromLines([])
97
+ }) as unknown as typeof fetch
98
+
99
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
100
+ await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
101
+
102
+ const iterator = app.currentUser!
103
+ .mongoClient('mongodb-atlas')
104
+ .db('testdb')
105
+ .collection('todos')
106
+ .watch()
107
+
108
+ iterator.close()
109
+ const result = await iterator.next()
110
+ expect(result.done).toBe(true)
111
+ })
112
+
113
+ it('reconnects with backoff after network errors', async () => {
114
+ jest.useFakeTimers()
115
+ global.fetch = jest
116
+ .fn()
117
+ .mockResolvedValueOnce({
118
+ ok: true,
119
+ text: async () => JSON.stringify({
120
+ access_token: 'access',
121
+ refresh_token: 'refresh',
122
+ user_id: 'user-1'
123
+ })
124
+ })
125
+ .mockResolvedValueOnce({
126
+ ok: true,
127
+ text: async () => JSON.stringify({ access_token: 'access' })
128
+ })
129
+ .mockRejectedValueOnce(new Error('network'))
130
+ .mockResolvedValueOnce({
131
+ ok: true,
132
+ body: streamFromLines(['data: {"operationType":"update"}', ''])
133
+ }) as unknown as typeof fetch
134
+
135
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
136
+ await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
137
+
138
+ const iterator = app.currentUser!
139
+ .mongoClient('mongodb-atlas')
140
+ .db('testdb')
141
+ .collection('todos')
142
+ .watch()
143
+
144
+ await jest.advanceTimersByTimeAsync(250)
145
+ const result = await iterator.next()
146
+
147
+ expect(result.done).toBe(false)
148
+ expect(result.value).toEqual({ operationType: 'update' })
149
+ expect((global.fetch as jest.Mock).mock.calls.length).toBeGreaterThanOrEqual(4)
150
+
151
+ iterator.close()
152
+ })
153
+
154
+ it('maps watch ids/filter options into watch arguments', async () => {
155
+ global.fetch = jest
156
+ .fn()
157
+ .mockResolvedValueOnce({
158
+ ok: true,
159
+ text: async () => JSON.stringify({
160
+ access_token: 'access',
161
+ refresh_token: 'refresh',
162
+ user_id: 'user-1'
163
+ })
164
+ })
165
+ .mockResolvedValueOnce({
166
+ ok: true,
167
+ text: async () => JSON.stringify({ access_token: 'access' })
168
+ })
169
+ .mockResolvedValue({
170
+ ok: true,
171
+ body: streamFromLines([])
172
+ }) as unknown as typeof fetch
173
+
174
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
175
+ await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
176
+
177
+ const collection = app.currentUser!.mongoClient('mongodb-atlas').db('testdb').collection('todos')
178
+
179
+ const byIds = collection.watch({ ids: ['id-1', 'id-2'] })
180
+ byIds.close()
181
+ const firstRequest = decodeBaasRequest((global.fetch as jest.Mock).mock.calls[2][0] as string)
182
+ expect(firstRequest.arguments[0].ids).toEqual(['id-1', 'id-2'])
183
+ expect(firstRequest.arguments[0].filter).toBeUndefined()
184
+
185
+ const byFilter = collection.watch({ filter: { operationType: 'insert' } })
186
+ byFilter.close()
187
+ const secondRequest = decodeBaasRequest((global.fetch as jest.Mock).mock.calls[3][0] as string)
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
+ )
285
+ })
286
+
287
+ it('rejects watch options with both ids and filter', async () => {
288
+ global.fetch = jest
289
+ .fn()
290
+ .mockResolvedValueOnce({
291
+ ok: true,
292
+ text: async () => JSON.stringify({
293
+ access_token: 'access',
294
+ refresh_token: 'refresh',
295
+ user_id: 'user-1'
296
+ })
297
+ })
298
+ .mockResolvedValueOnce({
299
+ ok: true,
300
+ text: async () => JSON.stringify({ access_token: 'access' })
301
+ }) as unknown as typeof fetch
302
+
303
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
304
+ await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
305
+
306
+ const collection = app.currentUser!.mongoClient('mongodb-atlas').db('testdb').collection('todos')
307
+ expect(() => collection.watch({ ids: ['id-1'], filter: { operationType: 'insert' } })).toThrow(
308
+ 'watch options cannot include both "ids" and "filter"'
309
+ )
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
+ })
336
+ })