@flowerforce/flowerbase-client 0.1.1-beta.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 +0 -0
- package/LICENSE +3 -0
- package/README.md +209 -0
- package/dist/app.d.ts +85 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +461 -0
- package/dist/bson.d.ts +8 -0
- package/dist/bson.d.ts.map +1 -0
- package/dist/bson.js +10 -0
- package/dist/credentials.d.ts +8 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +30 -0
- package/dist/functions.d.ts +6 -0
- package/dist/functions.d.ts.map +1 -0
- package/dist/functions.js +47 -0
- package/dist/http.d.ts +35 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +170 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/mongo.d.ts +4 -0
- package/dist/mongo.d.ts.map +1 -0
- package/dist/mongo.js +106 -0
- package/dist/session.d.ts +18 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +105 -0
- package/dist/session.native.d.ts +14 -0
- package/dist/session.native.d.ts.map +1 -0
- package/dist/session.native.js +76 -0
- package/dist/types.d.ts +97 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/user.d.ts +37 -0
- package/dist/user.d.ts.map +1 -0
- package/dist/user.js +125 -0
- package/dist/watch.d.ts +3 -0
- package/dist/watch.d.ts.map +1 -0
- package/dist/watch.js +139 -0
- package/jest.config.ts +13 -0
- package/package.json +41 -0
- package/project.json +11 -0
- package/rollup.config.js +17 -0
- package/src/__tests__/auth.test.ts +213 -0
- package/src/__tests__/compat.test.ts +22 -0
- package/src/__tests__/functions.test.ts +312 -0
- package/src/__tests__/mongo.test.ts +83 -0
- package/src/__tests__/session.test.ts +597 -0
- package/src/__tests__/watch.test.ts +336 -0
- package/src/app.ts +562 -0
- package/src/bson.ts +6 -0
- package/src/credentials.ts +31 -0
- package/src/functions.ts +56 -0
- package/src/http.ts +221 -0
- package/src/index.ts +15 -0
- package/src/mongo.ts +112 -0
- package/src/session.native.ts +89 -0
- package/src/session.ts +114 -0
- package/src/types.ts +114 -0
- package/src/user.ts +150 -0
- package/src/watch.ts +156 -0
- package/tsconfig.json +34 -0
- 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
|
+
})
|