@flowerforce/flowerbase-client 0.1.1-beta.2 → 0.1.1-beta.4
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/dist/app.d.ts +55 -10
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +322 -47
- package/dist/credentials.d.ts +1 -0
- package/dist/credentials.d.ts.map +1 -1
- package/dist/credentials.js +6 -0
- package/dist/functions.d.ts +4 -1
- package/dist/functions.d.ts.map +1 -1
- package/dist/functions.js +18 -1
- package/dist/http.d.ts +24 -4
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +121 -25
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/mongo.d.ts +1 -1
- package/dist/mongo.d.ts.map +1 -1
- package/dist/mongo.js +45 -4
- package/dist/session.d.ts +6 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +52 -0
- package/dist/session.native.d.ts.map +1 -1
- package/dist/session.native.js +5 -10
- package/dist/types.d.ts +28 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/user.d.ts +24 -4
- package/dist/user.d.ts.map +1 -1
- package/dist/user.js +103 -8
- package/package.json +12 -1
- package/src/__tests__/auth.test.ts +49 -0
- package/src/__tests__/compat.test.ts +10 -0
- package/src/__tests__/functions.test.ts +236 -0
- package/src/__tests__/mongo.test.ts +35 -0
- package/src/__tests__/session.test.ts +494 -0
- package/src/__tests__/watch.test.ts +74 -0
- package/src/app.ts +390 -63
- package/src/credentials.ts +7 -0
- package/src/functions.ts +27 -3
- package/src/http.ts +156 -27
- package/src/index.ts +1 -0
- package/src/mongo.ts +48 -4
- package/src/session.native.ts +2 -11
- package/src/session.ts +55 -0
- package/src/types.ts +34 -4
- package/src/user.ts +123 -12
package/src/app.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { normalizeFunctionResponse } from './functions'
|
|
2
|
-
import { requestJson } from './http'
|
|
2
|
+
import { FlowerbaseHttpError, requestJson, requestStream } from './http'
|
|
3
3
|
import { SessionManager } from './session'
|
|
4
4
|
import { AppConfig, CredentialsLike, FunctionCallPayload, ProfileData, SessionData } from './types'
|
|
5
|
+
import { Credentials } from './credentials'
|
|
5
6
|
import { User } from './user'
|
|
6
7
|
|
|
7
8
|
const API_PREFIX = '/api/client/v2.0'
|
|
@@ -17,44 +18,191 @@ type SessionResponse = {
|
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export class App {
|
|
21
|
+
private static readonly appCache: Record<string, App> = {}
|
|
22
|
+
static readonly Credentials = Credentials
|
|
23
|
+
|
|
20
24
|
readonly id: string
|
|
21
25
|
readonly baseUrl: string
|
|
22
26
|
readonly timeout: number
|
|
23
27
|
private readonly sessionManager: SessionManager
|
|
24
|
-
|
|
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>()
|
|
25
32
|
private readonly sessionBootstrapPromise: Promise<void>
|
|
33
|
+
private readonly listeners = new Set<() => void>()
|
|
26
34
|
|
|
27
35
|
emailPasswordAuth: {
|
|
28
36
|
registerUser: (input: { email: string; password: string }) => Promise<unknown>
|
|
29
|
-
|
|
30
|
-
|
|
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>
|
|
31
46
|
resetPassword: (input: { token: string; tokenId: string; password: string }) => Promise<unknown>
|
|
32
47
|
}
|
|
33
48
|
|
|
34
|
-
constructor(
|
|
49
|
+
constructor(idOrConfig: string | AppConfig) {
|
|
50
|
+
const config = typeof idOrConfig === 'string' ? { id: idOrConfig } : idOrConfig
|
|
35
51
|
this.id = config.id
|
|
36
|
-
this.baseUrl = config.baseUrl.replace(/\/$/, '')
|
|
52
|
+
this.baseUrl = (config.baseUrl ?? '').replace(/\/$/, '')
|
|
37
53
|
this.timeout = config.timeout ?? 10000
|
|
38
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
|
+
}
|
|
39
68
|
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
42
|
-
this.
|
|
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()
|
|
43
77
|
}
|
|
44
78
|
this.sessionBootstrapPromise = this.bootstrapSessionOnLoad()
|
|
45
79
|
|
|
46
80
|
this.emailPasswordAuth = {
|
|
47
81
|
registerUser: ({ email, password }) =>
|
|
48
82
|
this.postProvider('/local-userpass/register', { email, password }),
|
|
49
|
-
|
|
50
|
-
this.postProvider('/local-userpass/
|
|
51
|
-
|
|
52
|
-
this.postProvider('/local-userpass/
|
|
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
|
+
},
|
|
53
108
|
resetPassword: ({ token, tokenId, password }) =>
|
|
54
109
|
this.postProvider('/local-userpass/reset', { token, tokenId, password })
|
|
55
110
|
}
|
|
56
111
|
}
|
|
57
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
|
+
|
|
58
206
|
private providerUrl(path: string) {
|
|
59
207
|
return `${this.baseUrl}${API_PREFIX}/app/${this.id}/auth/providers${path}`
|
|
60
208
|
}
|
|
@@ -84,13 +232,13 @@ export class App {
|
|
|
84
232
|
|
|
85
233
|
try {
|
|
86
234
|
const result = await this.createSession(session.refreshToken)
|
|
87
|
-
this.
|
|
235
|
+
this.setSessionForUser({
|
|
88
236
|
...session,
|
|
89
237
|
accessToken: result.access_token
|
|
90
238
|
})
|
|
91
239
|
} catch {
|
|
92
|
-
this.
|
|
93
|
-
this.
|
|
240
|
+
this.clearSessionForUser(session.userId)
|
|
241
|
+
this.setCurrentSessionFromOrder()
|
|
94
242
|
}
|
|
95
243
|
}
|
|
96
244
|
|
|
@@ -98,19 +246,36 @@ export class App {
|
|
|
98
246
|
await this.sessionBootstrapPromise
|
|
99
247
|
}
|
|
100
248
|
|
|
101
|
-
private async setLoggedInUser(
|
|
249
|
+
private async setLoggedInUser(
|
|
250
|
+
data: LoginResponse,
|
|
251
|
+
providerType: CredentialsLike['provider'],
|
|
252
|
+
profileEmail?: string
|
|
253
|
+
) {
|
|
102
254
|
const sessionResult = await this.createSession(data.refresh_token)
|
|
103
255
|
const session: SessionData = {
|
|
104
256
|
accessToken: sessionResult.access_token,
|
|
105
257
|
refreshToken: data.refresh_token,
|
|
106
258
|
userId: data.user_id
|
|
107
259
|
}
|
|
108
|
-
this.
|
|
109
|
-
|
|
260
|
+
this.setSessionForUser(session)
|
|
261
|
+
const user = this.getOrCreateUser(data.user_id)
|
|
262
|
+
user.setProviderType(providerType)
|
|
263
|
+
this.touchUser(data.user_id)
|
|
110
264
|
if (profileEmail) {
|
|
111
|
-
|
|
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
|
|
112
275
|
}
|
|
113
|
-
|
|
276
|
+
const user = new User(this, userId)
|
|
277
|
+
this.usersById.set(userId, user)
|
|
278
|
+
return user
|
|
114
279
|
}
|
|
115
280
|
|
|
116
281
|
async logIn(credentials: CredentialsLike) {
|
|
@@ -119,26 +284,94 @@ export class App {
|
|
|
119
284
|
username: credentials.email,
|
|
120
285
|
password: credentials.password
|
|
121
286
|
})
|
|
122
|
-
return this.setLoggedInUser(result, credentials.email)
|
|
287
|
+
return this.setLoggedInUser(result, 'local-userpass', credentials.email)
|
|
123
288
|
}
|
|
124
289
|
|
|
125
290
|
if (credentials.provider === 'anon-user') {
|
|
126
291
|
const result = await this.postProvider<LoginResponse>('/anon-user/login', {})
|
|
127
|
-
return this.setLoggedInUser(result)
|
|
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')
|
|
128
303
|
}
|
|
129
304
|
|
|
130
|
-
const
|
|
131
|
-
|
|
305
|
+
const unsupportedProvider: never = credentials
|
|
306
|
+
throw new Error(`Unsupported credentials provider: ${JSON.stringify(unsupportedProvider)}`)
|
|
132
307
|
}
|
|
133
308
|
|
|
134
|
-
|
|
135
|
-
const
|
|
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()
|
|
136
354
|
if (!session) {
|
|
137
355
|
throw new Error('User is not authenticated')
|
|
138
356
|
}
|
|
139
357
|
return session
|
|
140
358
|
}
|
|
141
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
|
+
|
|
142
375
|
async postProvider<T = unknown>(path: string, body: unknown): Promise<T> {
|
|
143
376
|
return requestJson<T>({
|
|
144
377
|
url: this.providerUrl(path),
|
|
@@ -148,76 +381,155 @@ export class App {
|
|
|
148
381
|
})
|
|
149
382
|
}
|
|
150
383
|
|
|
151
|
-
async
|
|
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) {
|
|
152
399
|
await this.ensureSessionBootstrapped()
|
|
153
|
-
const session = this.getSessionOrThrow()
|
|
154
400
|
const payload: FunctionCallPayload = {
|
|
155
401
|
name,
|
|
156
402
|
arguments: args
|
|
157
403
|
}
|
|
158
404
|
|
|
159
|
-
const result = await
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
+
)
|
|
166
415
|
|
|
167
416
|
return normalizeFunctionResponse(result)
|
|
168
417
|
}
|
|
169
418
|
|
|
170
|
-
async
|
|
419
|
+
async callFunctionStreaming(name: string, args: unknown[], userId?: string): Promise<AsyncIterable<Uint8Array>> {
|
|
171
420
|
await this.ensureSessionBootstrapped()
|
|
172
|
-
const session = this.getSessionOrThrow()
|
|
173
421
|
const payload: FunctionCallPayload = {
|
|
174
422
|
name,
|
|
175
|
-
service: 'mongodb-atlas',
|
|
176
423
|
arguments: args
|
|
177
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
|
+
}
|
|
178
471
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
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
|
+
)
|
|
186
490
|
}
|
|
187
491
|
|
|
188
|
-
async getProfile(): Promise<ProfileData> {
|
|
492
|
+
async getProfile(userId?: string): Promise<ProfileData> {
|
|
189
493
|
await this.ensureSessionBootstrapped()
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
197
506
|
}
|
|
198
507
|
|
|
199
|
-
async refreshAccessToken() {
|
|
508
|
+
async refreshAccessToken(userId?: string) {
|
|
200
509
|
await this.ensureSessionBootstrapped()
|
|
201
|
-
const session = this.getSessionOrThrow()
|
|
510
|
+
const session = this.getSessionOrThrow(userId)
|
|
202
511
|
|
|
203
512
|
try {
|
|
204
513
|
const result = await this.createSession(session.refreshToken)
|
|
205
514
|
|
|
206
|
-
this.
|
|
515
|
+
this.setSessionForUser({
|
|
207
516
|
...session,
|
|
208
517
|
accessToken: result.access_token
|
|
209
518
|
})
|
|
519
|
+
this.touchUser(session.userId)
|
|
520
|
+
this.notifyListeners(session.userId)
|
|
210
521
|
|
|
211
522
|
return result.access_token
|
|
212
523
|
} catch (error) {
|
|
213
|
-
this.
|
|
214
|
-
this.
|
|
524
|
+
this.clearSessionForUser(session.userId)
|
|
525
|
+
this.setCurrentSessionFromOrder()
|
|
526
|
+
this.notifyListeners(session.userId)
|
|
215
527
|
throw error
|
|
216
528
|
}
|
|
217
529
|
}
|
|
218
530
|
|
|
219
|
-
async logoutUser() {
|
|
220
|
-
const session = this.
|
|
531
|
+
async logoutUser(userId?: string) {
|
|
532
|
+
const session = this.getSession(userId ?? this.currentUser?.id)
|
|
221
533
|
try {
|
|
222
534
|
if (session) {
|
|
223
535
|
await requestJson({
|
|
@@ -228,8 +540,23 @@ export class App {
|
|
|
228
540
|
})
|
|
229
541
|
}
|
|
230
542
|
} finally {
|
|
231
|
-
|
|
232
|
-
|
|
543
|
+
if (session) {
|
|
544
|
+
this.clearSessionForUser(session.userId)
|
|
545
|
+
this.notifyListeners(session.userId)
|
|
546
|
+
}
|
|
547
|
+
this.setCurrentSessionFromOrder()
|
|
233
548
|
}
|
|
234
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
|
+
}
|
|
235
562
|
}
|
package/src/credentials.ts
CHANGED
package/src/functions.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { EJSON } from './bson'
|
|
2
2
|
|
|
3
|
+
const RESERVED_PROXY_KEYS = new Set([
|
|
4
|
+
'toJSON',
|
|
5
|
+
'then',
|
|
6
|
+
'catch',
|
|
7
|
+
'finally',
|
|
8
|
+
'constructor',
|
|
9
|
+
'__proto__',
|
|
10
|
+
'prototype'
|
|
11
|
+
])
|
|
12
|
+
|
|
3
13
|
const deserialize = <T>(value: T): T => {
|
|
4
14
|
if (!value || typeof value !== 'object') return value
|
|
5
15
|
return EJSON.deserialize(value as Record<string, unknown>) as T
|
|
@@ -19,14 +29,28 @@ export const normalizeFunctionResponse = (value: unknown) => {
|
|
|
19
29
|
}
|
|
20
30
|
|
|
21
31
|
export const createFunctionsProxy = (
|
|
22
|
-
callFunction: (name: string, args: unknown[]) => Promise<unknown
|
|
23
|
-
|
|
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
|
+
} =>
|
|
24
38
|
new Proxy(
|
|
25
39
|
{},
|
|
26
40
|
{
|
|
27
41
|
get: (_, key) => {
|
|
28
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
|
+
}
|
|
29
50
|
return (...args: unknown[]) => callFunction(key, args)
|
|
30
51
|
}
|
|
31
52
|
}
|
|
32
|
-
) as Record<string, (...args: unknown[]) => Promise<unknown>>
|
|
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
|
+
}
|