@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.
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
package/src/app.ts ADDED
@@ -0,0 +1,562 @@
1
+ import { normalizeFunctionResponse } from './functions'
2
+ import { FlowerbaseHttpError, requestJson, requestStream } from './http'
3
+ import { SessionManager } from './session'
4
+ import { AppConfig, CredentialsLike, FunctionCallPayload, ProfileData, SessionData } from './types'
5
+ import { Credentials } from './credentials'
6
+ import { User } from './user'
7
+
8
+ const API_PREFIX = '/api/client/v2.0'
9
+
10
+ type LoginResponse = {
11
+ access_token: string
12
+ refresh_token: string
13
+ user_id: string
14
+ }
15
+
16
+ type SessionResponse = {
17
+ access_token: string
18
+ }
19
+
20
+ export class App {
21
+ private static readonly appCache: Record<string, App> = {}
22
+ static readonly Credentials = Credentials
23
+
24
+ readonly id: string
25
+ readonly baseUrl: string
26
+ readonly timeout: number
27
+ private readonly sessionManager: SessionManager
28
+ private readonly usersById = new Map<string, User>()
29
+ private readonly sessionsByUserId = new Map<string, SessionData>()
30
+ private usersOrder: string[] = []
31
+ private readonly profilesByUserId = new Map<string, ProfileData>()
32
+ private readonly sessionBootstrapPromise: Promise<void>
33
+ private readonly listeners = new Set<() => void>()
34
+
35
+ emailPasswordAuth: {
36
+ registerUser: (input: { email: string; password: string }) => Promise<unknown>
37
+ confirmUser: (input: { token: string; tokenId: string }) => Promise<unknown>
38
+ resendConfirmationEmail: (input: { email: string }) => Promise<unknown>
39
+ retryCustomConfirmation: (input: { email: string }) => Promise<unknown>
40
+ sendResetPasswordEmail: (input: { email: string } | string) => Promise<unknown>
41
+ callResetPasswordFunction: (
42
+ input: { email: string; password: string } | string,
43
+ passwordOrArg?: string,
44
+ ...args: unknown[]
45
+ ) => Promise<unknown>
46
+ resetPassword: (input: { token: string; tokenId: string; password: string }) => Promise<unknown>
47
+ }
48
+
49
+ constructor(idOrConfig: string | AppConfig) {
50
+ const config = typeof idOrConfig === 'string' ? { id: idOrConfig } : idOrConfig
51
+ this.id = config.id
52
+ this.baseUrl = (config.baseUrl ?? '').replace(/\/$/, '')
53
+ this.timeout = config.timeout ?? 10000
54
+ this.sessionManager = new SessionManager(this.id)
55
+ const persistedSessionsByUser = this.sessionManager.getSessionsByUser()
56
+ for (const [userId, session] of Object.entries(persistedSessionsByUser)) {
57
+ this.sessionsByUserId.set(userId, session)
58
+ }
59
+ this.usersOrder = this.sessionManager.getUsersOrder()
60
+ for (const userId of this.sessionsByUserId.keys()) {
61
+ if (!this.usersOrder.includes(userId)) {
62
+ this.usersOrder.push(userId)
63
+ }
64
+ }
65
+ for (const userId of this.usersOrder) {
66
+ this.getOrCreateUser(userId)
67
+ }
68
+
69
+ const currentSession = this.sessionManager.get()
70
+ if (currentSession?.userId) {
71
+ this.sessionsByUserId.set(currentSession.userId, currentSession)
72
+ this.getOrCreateUser(currentSession.userId)
73
+ this.touchUser(currentSession.userId)
74
+ this.persistSessionsByUser()
75
+ } else {
76
+ this.setCurrentSessionFromOrder()
77
+ }
78
+ this.sessionBootstrapPromise = this.bootstrapSessionOnLoad()
79
+
80
+ this.emailPasswordAuth = {
81
+ registerUser: ({ email, password }) =>
82
+ this.postProvider('/local-userpass/register', { email, password }),
83
+ confirmUser: ({ token, tokenId }) =>
84
+ this.postProvider('/local-userpass/confirm', { token, tokenId }),
85
+ resendConfirmationEmail: ({ email }) =>
86
+ this.postProvider('/local-userpass/confirm/send', { email }),
87
+ retryCustomConfirmation: ({ email }) =>
88
+ this.postProvider('/local-userpass/confirm/call', { email }),
89
+ sendResetPasswordEmail: (input) =>
90
+ this.postProvider('/local-userpass/reset/send', {
91
+ email: typeof input === 'string' ? input : input.email
92
+ }),
93
+ callResetPasswordFunction: (input, passwordOrArg, ...args) => {
94
+ if (typeof input === 'string') {
95
+ return this.postProvider('/local-userpass/reset/call', {
96
+ email: input,
97
+ password: passwordOrArg,
98
+ arguments: args
99
+ })
100
+ }
101
+
102
+ return this.postProvider('/local-userpass/reset/call', {
103
+ email: input.email,
104
+ password: input.password,
105
+ arguments: [passwordOrArg, ...args].filter((value) => value !== undefined)
106
+ })
107
+ },
108
+ resetPassword: ({ token, tokenId, password }) =>
109
+ this.postProvider('/local-userpass/reset', { token, tokenId, password })
110
+ }
111
+ }
112
+
113
+ static getApp(appIdOrConfig: string | AppConfig) {
114
+ const appId = typeof appIdOrConfig === 'string' ? appIdOrConfig : appIdOrConfig.id
115
+ if (appId in App.appCache) {
116
+ return App.appCache[appId]
117
+ }
118
+ const app = new App(appIdOrConfig)
119
+ App.appCache[appId] = app
120
+ return app
121
+ }
122
+
123
+ get currentUser() {
124
+ for (const userId of this.usersOrder) {
125
+ const user = this.usersById.get(userId)
126
+ if (user?.state === 'active') {
127
+ return user
128
+ }
129
+ }
130
+ return null
131
+ }
132
+
133
+ get allUsers(): Readonly<Record<string, User>> {
134
+ const activeUsers: string[] = []
135
+ const loggedOutUsers: string[] = []
136
+ for (const userId of this.usersOrder) {
137
+ const user = this.usersById.get(userId)
138
+ if (!user) continue
139
+ if (user.state === 'active') {
140
+ activeUsers.push(userId)
141
+ } else if (user.state === 'logged-out') {
142
+ loggedOutUsers.push(userId)
143
+ }
144
+ }
145
+
146
+ const users = Object.fromEntries(
147
+ [...activeUsers, ...loggedOutUsers].map((userId) => [userId, this.usersById.get(userId)!])
148
+ )
149
+ return users
150
+ }
151
+
152
+ private persistSessionsByUser() {
153
+ this.sessionManager.setSessionsByUser(Object.fromEntries(this.sessionsByUserId.entries()))
154
+ }
155
+
156
+ private persistUsersOrder() {
157
+ this.sessionManager.setUsersOrder(this.usersOrder)
158
+ }
159
+
160
+ private touchUser(userId: string) {
161
+ this.usersOrder = [userId, ...this.usersOrder.filter((id) => id !== userId)]
162
+ this.persistUsersOrder()
163
+ }
164
+
165
+ private removeUserFromOrder(userId: string) {
166
+ this.usersOrder = this.usersOrder.filter((id) => id !== userId)
167
+ this.persistUsersOrder()
168
+ }
169
+
170
+ private setSessionForUser(session: SessionData) {
171
+ this.sessionsByUserId.set(session.userId, session)
172
+ this.sessionManager.set(session)
173
+ this.persistSessionsByUser()
174
+ }
175
+
176
+ private clearSessionForUser(userId: string) {
177
+ this.sessionsByUserId.delete(userId)
178
+ this.persistSessionsByUser()
179
+ }
180
+
181
+ private setCurrentSessionFromOrder() {
182
+ for (const userId of this.usersOrder) {
183
+ const session = this.sessionsByUserId.get(userId)
184
+ if (session) {
185
+ this.sessionManager.set(session)
186
+ return
187
+ }
188
+ }
189
+ this.sessionManager.clear()
190
+ }
191
+
192
+ private notifyListeners(userId?: string) {
193
+ for (const callback of Array.from(this.listeners)) {
194
+ try {
195
+ callback()
196
+ } catch {
197
+ // Listener failures should not break auth/session lifecycle.
198
+ }
199
+ }
200
+
201
+ if (userId) {
202
+ this.usersById.get(userId)?.notifyListeners()
203
+ }
204
+ }
205
+
206
+ private providerUrl(path: string) {
207
+ return `${this.baseUrl}${API_PREFIX}/app/${this.id}/auth/providers${path}`
208
+ }
209
+
210
+ private authUrl(path: string) {
211
+ return `${this.baseUrl}${API_PREFIX}/auth${path}`
212
+ }
213
+
214
+ private functionsUrl(path = '/call') {
215
+ return `${this.baseUrl}${API_PREFIX}/app/${this.id}/functions${path}`
216
+ }
217
+
218
+ private async createSession(refreshToken: string): Promise<SessionResponse> {
219
+ return requestJson<SessionResponse>({
220
+ url: this.authUrl('/session'),
221
+ method: 'POST',
222
+ bearerToken: refreshToken,
223
+ timeout: this.timeout
224
+ })
225
+ }
226
+
227
+ private async bootstrapSessionOnLoad(): Promise<void> {
228
+ const session = this.sessionManager.get()
229
+ if (!session || typeof localStorage === 'undefined') {
230
+ return
231
+ }
232
+
233
+ try {
234
+ const result = await this.createSession(session.refreshToken)
235
+ this.setSessionForUser({
236
+ ...session,
237
+ accessToken: result.access_token
238
+ })
239
+ } catch {
240
+ this.clearSessionForUser(session.userId)
241
+ this.setCurrentSessionFromOrder()
242
+ }
243
+ }
244
+
245
+ private async ensureSessionBootstrapped() {
246
+ await this.sessionBootstrapPromise
247
+ }
248
+
249
+ private async setLoggedInUser(
250
+ data: LoginResponse,
251
+ providerType: CredentialsLike['provider'],
252
+ profileEmail?: string
253
+ ) {
254
+ const sessionResult = await this.createSession(data.refresh_token)
255
+ const session: SessionData = {
256
+ accessToken: sessionResult.access_token,
257
+ refreshToken: data.refresh_token,
258
+ userId: data.user_id
259
+ }
260
+ this.setSessionForUser(session)
261
+ const user = this.getOrCreateUser(data.user_id)
262
+ user.setProviderType(providerType)
263
+ this.touchUser(data.user_id)
264
+ if (profileEmail) {
265
+ user.profile = { email: profileEmail }
266
+ }
267
+ this.notifyListeners(data.user_id)
268
+ return user
269
+ }
270
+
271
+ private getOrCreateUser(userId: string) {
272
+ const existing = this.usersById.get(userId)
273
+ if (existing) {
274
+ return existing
275
+ }
276
+ const user = new User(this, userId)
277
+ this.usersById.set(userId, user)
278
+ return user
279
+ }
280
+
281
+ async logIn(credentials: CredentialsLike) {
282
+ if (credentials.provider === 'local-userpass') {
283
+ const result = await this.postProvider<LoginResponse>('/local-userpass/login', {
284
+ username: credentials.email,
285
+ password: credentials.password
286
+ })
287
+ return this.setLoggedInUser(result, 'local-userpass', credentials.email)
288
+ }
289
+
290
+ if (credentials.provider === 'anon-user') {
291
+ const result = await this.postProvider<LoginResponse>('/anon-user/login', {})
292
+ return this.setLoggedInUser(result, 'anon-user')
293
+ }
294
+
295
+ if (credentials.provider === 'custom-function') {
296
+ const result = await this.postProvider<LoginResponse>('/custom-function/login', credentials.payload)
297
+ return this.setLoggedInUser(result, 'custom-function')
298
+ }
299
+
300
+ if (credentials.provider === 'custom-token') {
301
+ const result = await this.postProvider<LoginResponse>('/custom-token/login', { token: credentials.token })
302
+ return this.setLoggedInUser(result, 'custom-token')
303
+ }
304
+
305
+ const unsupportedProvider: never = credentials
306
+ throw new Error(`Unsupported credentials provider: ${JSON.stringify(unsupportedProvider)}`)
307
+ }
308
+
309
+ switchUser(nextUser: User) {
310
+ const knownUser = this.usersById.get(nextUser.id)
311
+ if (!knownUser) {
312
+ throw new Error('The user was never logged into this app')
313
+ }
314
+ this.touchUser(nextUser.id)
315
+ const session = this.sessionsByUserId.get(nextUser.id)
316
+ if (session) {
317
+ this.sessionManager.set(session)
318
+ }
319
+ this.notifyListeners(nextUser.id)
320
+ }
321
+
322
+ async removeUser(user: User) {
323
+ const knownUser = this.usersById.get(user.id)
324
+ if (!knownUser) {
325
+ throw new Error('The user was never logged into this app')
326
+ }
327
+ if (this.sessionsByUserId.has(user.id)) {
328
+ await this.logoutUser(user.id)
329
+ }
330
+ this.usersById.delete(user.id)
331
+ this.removeUserFromOrder(user.id)
332
+ this.profilesByUserId.delete(user.id)
333
+ this.clearSessionForUser(user.id)
334
+ this.setCurrentSessionFromOrder()
335
+ this.notifyListeners(user.id)
336
+ }
337
+
338
+ async deleteUser(user: User) {
339
+ await this.requestWithAccessToken((accessToken) =>
340
+ requestJson({
341
+ url: this.authUrl('/delete'),
342
+ method: 'DELETE',
343
+ bearerToken: accessToken,
344
+ timeout: this.timeout
345
+ }),
346
+ user.id
347
+ )
348
+ await this.removeUser(user)
349
+ }
350
+
351
+ getSessionOrThrow(userId?: string) {
352
+ const targetUserId = userId ?? this.currentUser?.id
353
+ const session = targetUserId ? this.sessionsByUserId.get(targetUserId) : this.sessionManager.get()
354
+ if (!session) {
355
+ throw new Error('User is not authenticated')
356
+ }
357
+ return session
358
+ }
359
+
360
+ getSession(userId?: string) {
361
+ if (userId) {
362
+ return this.sessionsByUserId.get(userId) ?? null
363
+ }
364
+ return this.sessionManager.get()
365
+ }
366
+
367
+ hasUser(userId: string) {
368
+ return this.usersById.has(userId)
369
+ }
370
+
371
+ getProfileSnapshot(userId: string) {
372
+ return this.profilesByUserId.get(userId)
373
+ }
374
+
375
+ async postProvider<T = unknown>(path: string, body: unknown): Promise<T> {
376
+ return requestJson<T>({
377
+ url: this.providerUrl(path),
378
+ method: 'POST',
379
+ body,
380
+ timeout: this.timeout
381
+ })
382
+ }
383
+
384
+ private async requestWithAccessToken<T>(operation: (accessToken: string) => Promise<T>, userId?: string) {
385
+ const firstSession = this.getSessionOrThrow(userId)
386
+ try {
387
+ return await operation(firstSession.accessToken)
388
+ } catch (error) {
389
+ if (!(error instanceof FlowerbaseHttpError) || error.status !== 401) {
390
+ throw error
391
+ }
392
+ await this.refreshAccessToken(userId)
393
+ const refreshedSession = this.getSessionOrThrow(userId)
394
+ return operation(refreshedSession.accessToken)
395
+ }
396
+ }
397
+
398
+ async callFunction(name: string, args: unknown[], userId?: string) {
399
+ await this.ensureSessionBootstrapped()
400
+ const payload: FunctionCallPayload = {
401
+ name,
402
+ arguments: args
403
+ }
404
+
405
+ const result = await this.requestWithAccessToken((accessToken) =>
406
+ requestJson<unknown>({
407
+ url: this.functionsUrl('/call'),
408
+ method: 'POST',
409
+ body: payload,
410
+ bearerToken: accessToken,
411
+ timeout: this.timeout
412
+ }),
413
+ userId
414
+ )
415
+
416
+ return normalizeFunctionResponse(result)
417
+ }
418
+
419
+ async callFunctionStreaming(name: string, args: unknown[], userId?: string): Promise<AsyncIterable<Uint8Array>> {
420
+ await this.ensureSessionBootstrapped()
421
+ const payload: FunctionCallPayload = {
422
+ name,
423
+ arguments: args
424
+ }
425
+ const resolveSession = () => this.getSessionOrThrow(userId)
426
+ const refreshSession = () => this.refreshAccessToken(userId)
427
+ const timeout = this.timeout
428
+ const url = this.functionsUrl('/call')
429
+
430
+ return {
431
+ async *[Symbol.asyncIterator]() {
432
+ let didRefresh = false
433
+ while (true) {
434
+ const session = resolveSession()
435
+ let stream: AsyncIterable<Uint8Array>
436
+
437
+ try {
438
+ stream = await requestStream({
439
+ url,
440
+ method: 'POST',
441
+ body: payload,
442
+ bearerToken: session.accessToken,
443
+ timeout
444
+ })
445
+ } catch (error) {
446
+ if (!didRefresh && error instanceof FlowerbaseHttpError && error.status === 401) {
447
+ await refreshSession()
448
+ didRefresh = true
449
+ continue
450
+ }
451
+ throw error
452
+ }
453
+
454
+ try {
455
+ for await (const chunk of stream) {
456
+ yield chunk
457
+ }
458
+ return
459
+ } catch (error) {
460
+ if (!didRefresh && error instanceof FlowerbaseHttpError && error.status === 401) {
461
+ await refreshSession()
462
+ didRefresh = true
463
+ continue
464
+ }
465
+ throw error
466
+ }
467
+ }
468
+ }
469
+ }
470
+ }
471
+
472
+ async callService(name: string, args: unknown[], service = 'mongodb-atlas', userId?: string) {
473
+ await this.ensureSessionBootstrapped()
474
+ const payload: FunctionCallPayload = {
475
+ name,
476
+ service,
477
+ arguments: args
478
+ }
479
+
480
+ return this.requestWithAccessToken((accessToken) =>
481
+ requestJson<unknown>({
482
+ url: this.functionsUrl('/call'),
483
+ method: 'POST',
484
+ body: payload,
485
+ bearerToken: accessToken,
486
+ timeout: this.timeout
487
+ }),
488
+ userId
489
+ )
490
+ }
491
+
492
+ async getProfile(userId?: string): Promise<ProfileData> {
493
+ await this.ensureSessionBootstrapped()
494
+ const profile = await this.requestWithAccessToken((accessToken) =>
495
+ requestJson<ProfileData>({
496
+ url: this.authUrl('/profile'),
497
+ method: 'GET',
498
+ bearerToken: accessToken,
499
+ timeout: this.timeout
500
+ }),
501
+ userId
502
+ )
503
+ const session = this.getSessionOrThrow(userId)
504
+ this.profilesByUserId.set(session.userId, profile)
505
+ return profile
506
+ }
507
+
508
+ async refreshAccessToken(userId?: string) {
509
+ await this.ensureSessionBootstrapped()
510
+ const session = this.getSessionOrThrow(userId)
511
+
512
+ try {
513
+ const result = await this.createSession(session.refreshToken)
514
+
515
+ this.setSessionForUser({
516
+ ...session,
517
+ accessToken: result.access_token
518
+ })
519
+ this.touchUser(session.userId)
520
+ this.notifyListeners(session.userId)
521
+
522
+ return result.access_token
523
+ } catch (error) {
524
+ this.clearSessionForUser(session.userId)
525
+ this.setCurrentSessionFromOrder()
526
+ this.notifyListeners(session.userId)
527
+ throw error
528
+ }
529
+ }
530
+
531
+ async logoutUser(userId?: string) {
532
+ const session = this.getSession(userId ?? this.currentUser?.id)
533
+ try {
534
+ if (session) {
535
+ await requestJson({
536
+ url: this.authUrl('/session'),
537
+ method: 'DELETE',
538
+ bearerToken: session.refreshToken,
539
+ timeout: this.timeout
540
+ })
541
+ }
542
+ } finally {
543
+ if (session) {
544
+ this.clearSessionForUser(session.userId)
545
+ this.notifyListeners(session.userId)
546
+ }
547
+ this.setCurrentSessionFromOrder()
548
+ }
549
+ }
550
+
551
+ addListener(callback: () => void) {
552
+ this.listeners.add(callback)
553
+ }
554
+
555
+ removeListener(callback: () => void) {
556
+ this.listeners.delete(callback)
557
+ }
558
+
559
+ removeAllListeners() {
560
+ this.listeners.clear()
561
+ }
562
+ }
package/src/bson.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { EJSON, ObjectId, BSON as RawBSON } from 'bson'
2
+
3
+ const ObjectID = ObjectId
4
+ const BSON = Object.assign({}, RawBSON, { ObjectId, ObjectID })
5
+
6
+ export { BSON, EJSON, ObjectId, ObjectID }
@@ -0,0 +1,31 @@
1
+ import { CredentialsLike } from './types'
2
+
3
+ export class Credentials {
4
+ static emailPassword(email: string, password: string): CredentialsLike {
5
+ return {
6
+ provider: 'local-userpass',
7
+ email,
8
+ password
9
+ }
10
+ }
11
+
12
+ static anonymous(): CredentialsLike {
13
+ return {
14
+ provider: 'anon-user'
15
+ }
16
+ }
17
+
18
+ static function(payload: Record<string, unknown>): CredentialsLike {
19
+ return {
20
+ provider: 'custom-function',
21
+ payload
22
+ }
23
+ }
24
+
25
+ static jwt(token: string): CredentialsLike {
26
+ return {
27
+ provider: 'custom-token',
28
+ token
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,56 @@
1
+ import { EJSON } from './bson'
2
+
3
+ const RESERVED_PROXY_KEYS = new Set([
4
+ 'toJSON',
5
+ 'then',
6
+ 'catch',
7
+ 'finally',
8
+ 'constructor',
9
+ '__proto__',
10
+ 'prototype'
11
+ ])
12
+
13
+ const deserialize = <T>(value: T): T => {
14
+ if (!value || typeof value !== 'object') return value
15
+ return EJSON.deserialize(value as Record<string, unknown>) as T
16
+ }
17
+
18
+ export const normalizeFunctionResponse = (value: unknown) => {
19
+ if (typeof value === 'string') {
20
+ try {
21
+ const parsed = JSON.parse(value)
22
+ return deserialize(parsed)
23
+ } catch {
24
+ return value
25
+ }
26
+ }
27
+
28
+ return deserialize(value)
29
+ }
30
+
31
+ export const createFunctionsProxy = (
32
+ callFunction: (name: string, args: unknown[]) => Promise<unknown>,
33
+ callFunctionStreaming: (name: string, args: unknown[]) => Promise<AsyncIterable<Uint8Array>>
34
+ ): Record<string, (...args: unknown[]) => Promise<unknown>> & {
35
+ callFunction: (name: string, ...args: unknown[]) => Promise<unknown>
36
+ callFunctionStreaming: (name: string, ...args: unknown[]) => Promise<AsyncIterable<Uint8Array>>
37
+ } =>
38
+ new Proxy(
39
+ {},
40
+ {
41
+ get: (_, key) => {
42
+ if (typeof key !== 'string') return undefined
43
+ if (RESERVED_PROXY_KEYS.has(key)) return undefined
44
+ if (key === 'callFunction') {
45
+ return (name: string, ...args: unknown[]) => callFunction(name, args)
46
+ }
47
+ if (key === 'callFunctionStreaming') {
48
+ return (name: string, ...args: unknown[]) => callFunctionStreaming(name, args)
49
+ }
50
+ return (...args: unknown[]) => callFunction(key, args)
51
+ }
52
+ }
53
+ ) as Record<string, (...args: unknown[]) => Promise<unknown>> & {
54
+ callFunction: (name: string, ...args: unknown[]) => Promise<unknown>
55
+ callFunctionStreaming: (name: string, ...args: unknown[]) => Promise<AsyncIterable<Uint8Array>>
56
+ }