@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
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,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
|
+
}
|
package/src/functions.ts
ADDED
|
@@ -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
|
+
}
|