@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,312 @@
1
+ import { App } from '../app'
2
+ import { Credentials } from '../credentials'
3
+ import { FlowerbaseHttpError } from '../http'
4
+
5
+ describe('flowerbase-client functions', () => {
6
+ const originalFetch = global.fetch
7
+ const streamFromChunks = (chunks: string[]) =>
8
+ new ReadableStream<Uint8Array>({
9
+ start(controller) {
10
+ for (const chunk of chunks) {
11
+ controller.enqueue(new TextEncoder().encode(chunk))
12
+ }
13
+ controller.close()
14
+ }
15
+ })
16
+
17
+ afterEach(() => {
18
+ global.fetch = originalFetch
19
+ })
20
+
21
+ it('calls dynamic function proxies', async () => {
22
+ global.fetch = jest
23
+ .fn()
24
+ .mockResolvedValueOnce({
25
+ ok: true,
26
+ text: async () => JSON.stringify({
27
+ access_token: 'access',
28
+ refresh_token: 'refresh',
29
+ user_id: 'user-1'
30
+ })
31
+ })
32
+ .mockResolvedValueOnce({
33
+ ok: true,
34
+ text: async () => JSON.stringify({ access_token: 'access' })
35
+ })
36
+ .mockResolvedValueOnce({
37
+ ok: true,
38
+ text: async () => JSON.stringify({ result: 42 })
39
+ }) as unknown as typeof fetch
40
+
41
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
42
+ await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
43
+
44
+ const result = await app.currentUser!.functions.sum(40, 2)
45
+ expect(result).toEqual({ result: 42 })
46
+
47
+ expect(global.fetch).toHaveBeenNthCalledWith(
48
+ 3,
49
+ 'http://localhost:3000/api/client/v2.0/app/my-app/functions/call',
50
+ expect.objectContaining({
51
+ method: 'POST',
52
+ headers: expect.objectContaining({ Authorization: 'Bearer access' })
53
+ })
54
+ )
55
+ })
56
+
57
+ it('throws function execution errors', async () => {
58
+ global.fetch = jest
59
+ .fn()
60
+ .mockResolvedValueOnce({
61
+ ok: true,
62
+ text: async () => JSON.stringify({
63
+ access_token: 'access',
64
+ refresh_token: 'refresh',
65
+ user_id: 'user-1'
66
+ })
67
+ })
68
+ .mockResolvedValueOnce({
69
+ ok: true,
70
+ text: async () => JSON.stringify({ access_token: 'access' })
71
+ })
72
+ .mockResolvedValueOnce({
73
+ ok: false,
74
+ status: 400,
75
+ text: async () => JSON.stringify({
76
+ error: '{"message":"boom","name":"Error"}',
77
+ error_code: 'FunctionExecutionError'
78
+ })
79
+ }) as unknown as typeof fetch
80
+
81
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
82
+ await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
83
+
84
+ await expect(app.currentUser!.functions.explode()).rejects.toThrow('boom')
85
+ })
86
+
87
+ it('supports functions.callFunction compatibility helper', async () => {
88
+ global.fetch = jest
89
+ .fn()
90
+ .mockResolvedValueOnce({
91
+ ok: true,
92
+ text: async () => JSON.stringify({
93
+ access_token: 'access',
94
+ refresh_token: 'refresh',
95
+ user_id: 'user-1'
96
+ })
97
+ })
98
+ .mockResolvedValueOnce({
99
+ ok: true,
100
+ text: async () => JSON.stringify({ access_token: 'access' })
101
+ })
102
+ .mockResolvedValueOnce({
103
+ ok: true,
104
+ text: async () => JSON.stringify({ result: 7 })
105
+ }) as unknown as typeof fetch
106
+
107
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
108
+ await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
109
+
110
+ const result = await app.currentUser!.functions.callFunction('sum', 3, 4)
111
+ expect(result).toEqual({ result: 7 })
112
+ })
113
+
114
+ it('does not treat toJSON as a remote function name', async () => {
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
+ }) as unknown as typeof fetch
129
+
130
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
131
+ await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
132
+
133
+ expect(JSON.stringify(app.currentUser!.functions)).toBe('{}')
134
+ expect(global.fetch).toHaveBeenCalledTimes(2)
135
+ })
136
+
137
+ it('supports functions.callFunctionStreaming', async () => {
138
+ global.fetch = jest
139
+ .fn()
140
+ .mockResolvedValueOnce({
141
+ ok: true,
142
+ text: async () => JSON.stringify({
143
+ access_token: 'access',
144
+ refresh_token: 'refresh',
145
+ user_id: 'user-1'
146
+ })
147
+ })
148
+ .mockResolvedValueOnce({
149
+ ok: true,
150
+ text: async () => JSON.stringify({ access_token: 'access' })
151
+ })
152
+ .mockResolvedValueOnce({
153
+ ok: true,
154
+ body: streamFromChunks(['a', 'b'])
155
+ }) as unknown as typeof fetch
156
+
157
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
158
+ await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
159
+
160
+ const stream = await app.currentUser!.functions.callFunctionStreaming('streamData')
161
+ const received: string[] = []
162
+ for await (const chunk of stream) {
163
+ received.push(new TextDecoder().decode(chunk))
164
+ }
165
+
166
+ expect(received).toEqual(['a', 'b'])
167
+ })
168
+
169
+ it('executes user-bound functions with that user session', async () => {
170
+ global.fetch = jest
171
+ .fn()
172
+ .mockResolvedValueOnce({
173
+ ok: true,
174
+ text: async () => JSON.stringify({ access_token: 'login-a1', refresh_token: 'refresh-1', user_id: 'user-1' })
175
+ })
176
+ .mockResolvedValueOnce({
177
+ ok: true,
178
+ text: async () => JSON.stringify({ access_token: 'access-1' })
179
+ })
180
+ .mockResolvedValueOnce({
181
+ ok: true,
182
+ text: async () => JSON.stringify({ access_token: 'login-a2', refresh_token: 'refresh-2', user_id: 'user-2' })
183
+ })
184
+ .mockResolvedValueOnce({
185
+ ok: true,
186
+ text: async () => JSON.stringify({ access_token: 'access-2' })
187
+ })
188
+ .mockResolvedValue({
189
+ ok: true,
190
+ text: async () => JSON.stringify({ ok: true })
191
+ }) as unknown as typeof fetch
192
+
193
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
194
+ const user1 = await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
195
+ await app.logIn(Credentials.anonymous())
196
+
197
+ await user1.functions.sum(1, 2)
198
+ const request = (global.fetch as jest.Mock).mock.calls[4][1]
199
+ expect(request.headers.Authorization).toBe('Bearer access-1')
200
+ })
201
+
202
+ it('retries streaming call when initial request returns 401', async () => {
203
+ const streamFromChunks = (chunks: string[]) =>
204
+ new ReadableStream<Uint8Array>({
205
+ start(controller) {
206
+ for (const chunk of chunks) {
207
+ controller.enqueue(new TextEncoder().encode(chunk))
208
+ }
209
+ controller.close()
210
+ }
211
+ })
212
+
213
+ global.fetch = jest
214
+ .fn()
215
+ .mockResolvedValueOnce({
216
+ ok: true,
217
+ text: async () => JSON.stringify({ access_token: 'login-a1', refresh_token: 'refresh-1', user_id: 'user-1' })
218
+ })
219
+ .mockResolvedValueOnce({
220
+ ok: true,
221
+ text: async () => JSON.stringify({ access_token: 'access-1' })
222
+ })
223
+ .mockResolvedValueOnce({
224
+ ok: false,
225
+ status: 401,
226
+ statusText: 'Unauthorized',
227
+ text: async () => JSON.stringify({ error: 'Unauthorized', error_code: 'InvalidSession' })
228
+ })
229
+ .mockResolvedValueOnce({
230
+ ok: true,
231
+ text: async () => JSON.stringify({ access_token: 'access-2' })
232
+ })
233
+ .mockResolvedValueOnce({
234
+ ok: true,
235
+ body: streamFromChunks(['x', 'y'])
236
+ }) as unknown as typeof fetch
237
+
238
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
239
+ await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
240
+
241
+ const stream = await app.currentUser!.functions.callFunctionStreaming('streamData')
242
+ const received: string[] = []
243
+ for await (const chunk of stream) {
244
+ received.push(new TextDecoder().decode(chunk))
245
+ }
246
+
247
+ expect(received).toEqual(['x', 'y'])
248
+ expect((global.fetch as jest.Mock).mock.calls[3][0]).toBe('http://localhost:3000/api/client/v2.0/auth/session')
249
+ })
250
+
251
+ it('retries streaming call when stream fails with 401 during iteration', async () => {
252
+ const streamWithAuthError = () =>
253
+ new ReadableStream<Uint8Array>({
254
+ start(controller) {
255
+ controller.enqueue(new TextEncoder().encode('a'))
256
+ controller.error(
257
+ new FlowerbaseHttpError({
258
+ method: 'POST',
259
+ url: 'http://localhost:3000/api/client/v2.0/app/my-app/functions/call',
260
+ statusCode: 401,
261
+ statusText: 'Unauthorized',
262
+ error: 'Expired token'
263
+ })
264
+ )
265
+ }
266
+ })
267
+
268
+ const streamFromChunks = (chunks: string[]) =>
269
+ new ReadableStream<Uint8Array>({
270
+ start(controller) {
271
+ for (const chunk of chunks) {
272
+ controller.enqueue(new TextEncoder().encode(chunk))
273
+ }
274
+ controller.close()
275
+ }
276
+ })
277
+
278
+ global.fetch = jest
279
+ .fn()
280
+ .mockResolvedValueOnce({
281
+ ok: true,
282
+ text: async () => JSON.stringify({ access_token: 'login-a1', refresh_token: 'refresh-1', user_id: 'user-1' })
283
+ })
284
+ .mockResolvedValueOnce({
285
+ ok: true,
286
+ text: async () => JSON.stringify({ access_token: 'access-1' })
287
+ })
288
+ .mockResolvedValueOnce({
289
+ ok: true,
290
+ body: streamWithAuthError()
291
+ })
292
+ .mockResolvedValueOnce({
293
+ ok: true,
294
+ text: async () => JSON.stringify({ access_token: 'access-2' })
295
+ })
296
+ .mockResolvedValueOnce({
297
+ ok: true,
298
+ body: streamFromChunks(['b'])
299
+ }) as unknown as typeof fetch
300
+
301
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
302
+ await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
303
+
304
+ const stream = await app.currentUser!.functions.callFunctionStreaming('streamData')
305
+ const received: string[] = []
306
+ for await (const chunk of stream) {
307
+ received.push(new TextDecoder().decode(chunk))
308
+ }
309
+
310
+ expect(received).toContain('b')
311
+ })
312
+ })
@@ -0,0 +1,83 @@
1
+ import { ObjectId } from 'bson'
2
+ import { App } from '../app'
3
+ import { Credentials } from '../credentials'
4
+
5
+ describe('flowerbase-client mongo service wrapper', () => {
6
+ const originalFetch = global.fetch
7
+
8
+ afterEach(() => {
9
+ global.fetch = originalFetch
10
+ })
11
+
12
+ it('maps CRUD calls to mongodb-atlas service payload', async () => {
13
+ global.fetch = jest
14
+ .fn()
15
+ .mockResolvedValueOnce({
16
+ ok: true,
17
+ text: async () => JSON.stringify({
18
+ access_token: 'access',
19
+ refresh_token: 'refresh',
20
+ user_id: 'user-1'
21
+ })
22
+ })
23
+ .mockResolvedValue({
24
+ ok: true,
25
+ text: async () => JSON.stringify({ ok: true })
26
+ }) as unknown as typeof fetch
27
+
28
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
29
+ await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
30
+
31
+ const collection = app.currentUser!.mongoClient('mongodb-atlas').db('testdb').collection('todos')
32
+
33
+ await collection.find({ done: false })
34
+ await collection.findOne({ done: false })
35
+ await collection.insertOne({ title: 'new task' })
36
+ await collection.updateOne({ _id: new ObjectId('507f1f77bcf86cd799439011') }, { $set: { done: true } })
37
+ await collection.updateMany({ done: false }, { $set: { done: true } })
38
+ await collection.deleteOne({ done: true })
39
+
40
+ expect((global.fetch as jest.Mock).mock.calls).toHaveLength(8)
41
+ const [url, request] = (global.fetch as jest.Mock).mock.calls[3]
42
+ expect(url).toBe('http://localhost:3000/api/client/v2.0/app/my-app/functions/call')
43
+ expect(request.method).toBe('POST')
44
+ const parsed = JSON.parse(request.body)
45
+ expect(parsed.service).toBe('mongodb-atlas')
46
+ expect(parsed.name).toBe('findOne')
47
+ })
48
+
49
+ it('supports extended CRUD operations and custom service name', async () => {
50
+ global.fetch = jest
51
+ .fn()
52
+ .mockResolvedValueOnce({
53
+ ok: true,
54
+ text: async () => JSON.stringify({
55
+ access_token: 'access',
56
+ refresh_token: 'refresh',
57
+ user_id: 'user-1'
58
+ })
59
+ })
60
+ .mockResolvedValue({
61
+ ok: true,
62
+ text: async () => JSON.stringify({ ok: true })
63
+ }) as unknown as typeof fetch
64
+
65
+ const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
66
+ await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
67
+
68
+ const collection = app.currentUser!.mongoClient('my-service').db('testdb').collection('todos')
69
+
70
+ await collection.findOneAndUpdate({ done: false }, { $set: { done: true } })
71
+ await collection.findOneAndReplace({ done: true }, { done: true, title: 'done' })
72
+ await collection.findOneAndDelete({ done: true })
73
+ await collection.aggregate([{ $match: { done: true } }])
74
+ await collection.count({ done: true })
75
+ await collection.insertMany([{ title: 'A' }, { title: 'B' }])
76
+ await collection.deleteMany({ done: true })
77
+
78
+ const calls = (global.fetch as jest.Mock).mock.calls
79
+ const lastBody = JSON.parse(calls[calls.length - 1][1].body)
80
+ expect(lastBody.service).toBe('my-service')
81
+ expect(lastBody.name).toBe('deleteMany')
82
+ })
83
+ })