@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/http.ts
CHANGED
|
@@ -1,12 +1,94 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
type ParsedPayloadError = {
|
|
2
|
+
message?: string
|
|
3
|
+
error?: string
|
|
4
|
+
errorCode?: string
|
|
5
|
+
link?: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const parsePayloadError = (payload: unknown): ParsedPayloadError => {
|
|
9
|
+
if (!payload || typeof payload !== 'object') return {}
|
|
10
|
+
|
|
11
|
+
const message = 'message' in payload && typeof payload.message === 'string' ? payload.message : undefined
|
|
12
|
+
const error = 'error' in payload && typeof payload.error === 'string' ? payload.error : undefined
|
|
13
|
+
const errorCode =
|
|
14
|
+
'error_code' in payload && typeof payload.error_code === 'string'
|
|
15
|
+
? payload.error_code
|
|
16
|
+
: 'errorCode' in payload && typeof payload.errorCode === 'string'
|
|
17
|
+
? payload.errorCode
|
|
18
|
+
: undefined
|
|
19
|
+
const link = 'link' in payload && typeof payload.link === 'string' ? payload.link : undefined
|
|
20
|
+
|
|
21
|
+
if (error) {
|
|
22
|
+
try {
|
|
23
|
+
const parsed = JSON.parse(error)
|
|
24
|
+
if (parsed && typeof parsed === 'object') {
|
|
25
|
+
const nestedMessage = 'message' in parsed && typeof parsed.message === 'string' ? parsed.message : undefined
|
|
26
|
+
const nestedErrorCode =
|
|
27
|
+
'error_code' in parsed && typeof parsed.error_code === 'string'
|
|
28
|
+
? parsed.error_code
|
|
29
|
+
: 'errorCode' in parsed && typeof parsed.errorCode === 'string'
|
|
30
|
+
? parsed.errorCode
|
|
31
|
+
: undefined
|
|
32
|
+
return {
|
|
33
|
+
message: nestedMessage ?? message ?? error,
|
|
34
|
+
error,
|
|
35
|
+
errorCode: nestedErrorCode ?? errorCode,
|
|
36
|
+
link
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// Keep original error text if it isn't JSON.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
message: message ?? error,
|
|
46
|
+
error,
|
|
47
|
+
errorCode,
|
|
48
|
+
link
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class MongoDBRealmError extends Error {
|
|
53
|
+
readonly method: string
|
|
54
|
+
readonly url: string
|
|
55
|
+
readonly statusCode: number
|
|
56
|
+
readonly statusText: string
|
|
57
|
+
readonly error: string | undefined
|
|
58
|
+
readonly errorCode: string | undefined
|
|
59
|
+
readonly link: string | undefined
|
|
60
|
+
readonly payload?: unknown
|
|
4
61
|
|
|
5
|
-
constructor(
|
|
6
|
-
|
|
62
|
+
constructor(params: {
|
|
63
|
+
method: string
|
|
64
|
+
url: string
|
|
65
|
+
statusCode: number
|
|
66
|
+
statusText: string
|
|
67
|
+
error?: string
|
|
68
|
+
errorCode?: string
|
|
69
|
+
link?: string
|
|
70
|
+
payload?: unknown
|
|
71
|
+
}) {
|
|
72
|
+
super(params.error || `${params.statusCode} ${params.statusText}`.trim())
|
|
73
|
+
this.name = 'MongoDBRealmError'
|
|
74
|
+
this.method = params.method
|
|
75
|
+
this.url = params.url
|
|
76
|
+
this.statusCode = params.statusCode
|
|
77
|
+
this.statusText = params.statusText
|
|
78
|
+
this.error = params.error
|
|
79
|
+
this.errorCode = params.errorCode
|
|
80
|
+
this.link = params.link
|
|
81
|
+
this.payload = params.payload
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export class FlowerbaseHttpError extends MongoDBRealmError {
|
|
86
|
+
readonly status: number
|
|
87
|
+
|
|
88
|
+
constructor(params: ConstructorParameters<typeof MongoDBRealmError>[0]) {
|
|
89
|
+
super(params)
|
|
7
90
|
this.name = 'FlowerbaseHttpError'
|
|
8
|
-
this.status =
|
|
9
|
-
this.payload = payload
|
|
91
|
+
this.status = params.statusCode
|
|
10
92
|
}
|
|
11
93
|
}
|
|
12
94
|
|
|
@@ -61,31 +143,78 @@ export const requestJson = async <T = unknown>({
|
|
|
61
143
|
const payload = await parseBody(response)
|
|
62
144
|
|
|
63
145
|
if (!response.ok) {
|
|
64
|
-
|
|
65
|
-
|
|
146
|
+
const parsedError = parsePayloadError(payload)
|
|
147
|
+
throw new FlowerbaseHttpError({
|
|
148
|
+
method,
|
|
149
|
+
url,
|
|
150
|
+
statusCode: response.status,
|
|
151
|
+
statusText: response.statusText,
|
|
152
|
+
error: parsedError.message ?? `HTTP ${response.status}`,
|
|
153
|
+
errorCode: parsedError.errorCode,
|
|
154
|
+
link: parsedError.link,
|
|
155
|
+
payload
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return payload as T
|
|
160
|
+
} finally {
|
|
161
|
+
clear()
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export const requestStream = async ({
|
|
166
|
+
url,
|
|
167
|
+
method = 'GET',
|
|
168
|
+
body,
|
|
169
|
+
bearerToken,
|
|
170
|
+
timeout
|
|
171
|
+
}: RequestParams): Promise<AsyncIterable<Uint8Array>> => {
|
|
172
|
+
const { signal, clear } = timeoutSignal(timeout)
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
const response = await fetch(url, {
|
|
176
|
+
method,
|
|
177
|
+
headers: {
|
|
178
|
+
...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
|
|
179
|
+
...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {})
|
|
180
|
+
},
|
|
181
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
182
|
+
signal
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
if (!response.ok) {
|
|
186
|
+
const payload = await parseBody(response)
|
|
187
|
+
const parsedError = parsePayloadError(payload)
|
|
188
|
+
throw new FlowerbaseHttpError({
|
|
189
|
+
method,
|
|
190
|
+
url,
|
|
191
|
+
statusCode: response.status,
|
|
192
|
+
statusText: response.statusText,
|
|
193
|
+
error: parsedError.message ?? `HTTP ${response.status}`,
|
|
194
|
+
errorCode: parsedError.errorCode,
|
|
195
|
+
link: parsedError.link,
|
|
196
|
+
payload
|
|
197
|
+
})
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (!response.body) {
|
|
201
|
+
throw new Error('Response stream body is missing')
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const reader = response.body.getReader()
|
|
205
|
+
return {
|
|
206
|
+
async *[Symbol.asyncIterator]() {
|
|
66
207
|
try {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
208
|
+
while (true) {
|
|
209
|
+
const { done, value } = await reader.read()
|
|
210
|
+
if (done) break
|
|
211
|
+
if (value) yield value
|
|
70
212
|
}
|
|
71
|
-
}
|
|
72
|
-
|
|
213
|
+
} finally {
|
|
214
|
+
reader.releaseLock()
|
|
73
215
|
}
|
|
74
216
|
}
|
|
75
|
-
|
|
76
|
-
const message =
|
|
77
|
-
parsedErrorMessage ||
|
|
78
|
-
(payload && typeof payload === 'object' && 'message' in payload && typeof payload.message === 'string'
|
|
79
|
-
? payload.message
|
|
80
|
-
: null) ||
|
|
81
|
-
(payload && typeof payload === 'object' && 'error' in payload && typeof payload.error === 'string'
|
|
82
|
-
? payload.error
|
|
83
|
-
: null) ||
|
|
84
|
-
`HTTP ${response.status}`
|
|
85
|
-
throw new FlowerbaseHttpError(message, response.status, payload)
|
|
86
217
|
}
|
|
87
|
-
|
|
88
|
-
return payload as T
|
|
89
218
|
} finally {
|
|
90
219
|
clear()
|
|
91
220
|
}
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { BSON, EJSON, ObjectId, ObjectID } from './bson'
|
|
|
3
3
|
export { App } from './app'
|
|
4
4
|
export { User } from './user'
|
|
5
5
|
export { Credentials } from './credentials'
|
|
6
|
+
export { MongoDBRealmError } from './http'
|
|
6
7
|
export { BSON, EJSON, ObjectId, ObjectID }
|
|
7
8
|
export type {
|
|
8
9
|
AppConfig,
|
package/src/mongo.ts
CHANGED
|
@@ -20,7 +20,7 @@ const mapResult = (value: unknown) => {
|
|
|
20
20
|
return deserialize(value)
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
export const createMongoClient = (app: App): MongoClientLike => ({
|
|
23
|
+
export const createMongoClient = (app: App, serviceName: string, userId: string): MongoClientLike => ({
|
|
24
24
|
db: (database: string) => ({
|
|
25
25
|
collection: (collection: string): CollectionLike => {
|
|
26
26
|
const callService = async (name: string, args: unknown[]) => {
|
|
@@ -31,21 +31,65 @@ export const createMongoClient = (app: App): MongoClientLike => ({
|
|
|
31
31
|
...serialize(args[0] ?? {}),
|
|
32
32
|
...(args[1] !== undefined ? { options: serialize(args[1]) } : {})
|
|
33
33
|
}
|
|
34
|
-
])
|
|
34
|
+
], serviceName, userId)
|
|
35
35
|
return mapResult(result)
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
const normalizeWatchInput = (input?: unknown) => {
|
|
39
|
+
if (Array.isArray(input)) {
|
|
40
|
+
return { pipeline: input, options: {} }
|
|
41
|
+
}
|
|
42
|
+
if (input && typeof input === 'object' && ('ids' in input || 'filter' in input)) {
|
|
43
|
+
const typed = input as { ids?: unknown[]; filter?: Record<string, unknown>; [key: string]: unknown }
|
|
44
|
+
if (typed.ids && typed.filter) {
|
|
45
|
+
throw new Error('watch options cannot include both "ids" and "filter"')
|
|
46
|
+
}
|
|
47
|
+
const { ids, filter, ...options } = typed
|
|
48
|
+
if (ids) {
|
|
49
|
+
return {
|
|
50
|
+
pipeline: [{ $match: { 'documentKey._id': { $in: ids } } }],
|
|
51
|
+
options
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (filter) {
|
|
55
|
+
return {
|
|
56
|
+
pipeline: [{ $match: filter }],
|
|
57
|
+
options
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { pipeline: [], options }
|
|
61
|
+
}
|
|
62
|
+
if (input && typeof input === 'object' && ('pipeline' in input || 'options' in input)) {
|
|
63
|
+
const typed = input as { pipeline?: unknown[]; options?: Record<string, unknown> }
|
|
64
|
+
return {
|
|
65
|
+
pipeline: typed.pipeline ?? [],
|
|
66
|
+
options: typed.options ?? {}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { pipeline: [], options: (input as Record<string, unknown> | undefined) ?? {} }
|
|
70
|
+
}
|
|
71
|
+
|
|
38
72
|
return {
|
|
39
73
|
find: (query = {}, options = {}) => callService('find', [{ query, options }]),
|
|
40
74
|
findOne: (query = {}, options = {}) => callService('findOne', [{ query, options }]),
|
|
75
|
+
findOneAndUpdate: (filter, update, options = {}) =>
|
|
76
|
+
callService('findOneAndUpdate', [{ filter, update, options }]),
|
|
77
|
+
findOneAndReplace: (filter, replacement, options = {}) =>
|
|
78
|
+
callService('findOneAndReplace', [{ filter, replacement, options }]),
|
|
79
|
+
findOneAndDelete: (filter, options = {}) => callService('findOneAndDelete', [{ filter, options }]),
|
|
80
|
+
aggregate: (pipeline) => callService('aggregate', [{ pipeline }]),
|
|
81
|
+
count: (query = {}, options = {}) => callService('count', [{ query, options }]),
|
|
41
82
|
insertOne: (document, options = {}) => callService('insertOne', [{ document, options }]),
|
|
83
|
+
insertMany: (documents, options = {}) => callService('insertMany', [{ documents, options }]),
|
|
42
84
|
updateOne: (filter, update, options = {}) =>
|
|
43
85
|
callService('updateOne', [{ filter, update, options }]),
|
|
44
86
|
updateMany: (filter, update, options = {}) =>
|
|
45
87
|
callService('updateMany', [{ filter, update, options }]),
|
|
46
88
|
deleteOne: (filter, options = {}) => callService('deleteOne', [{ query: filter, options }]),
|
|
47
|
-
|
|
48
|
-
|
|
89
|
+
deleteMany: (filter, options = {}) => callService('deleteMany', [{ query: filter, options }]),
|
|
90
|
+
watch: (input) => {
|
|
91
|
+
const { pipeline, options } = normalizeWatchInput(input)
|
|
92
|
+
const session = app.getSessionOrThrow(userId)
|
|
49
93
|
return createWatchIterator({
|
|
50
94
|
appId: app.id,
|
|
51
95
|
baseUrl: app.baseUrl,
|
package/src/session.native.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SessionData } from './types'
|
|
2
|
+
import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
2
3
|
|
|
3
4
|
type StorageLike = {
|
|
4
5
|
getItem: (key: string) => Promise<string | null>
|
|
@@ -8,17 +9,7 @@ type StorageLike = {
|
|
|
8
9
|
|
|
9
10
|
const memoryStore = new Map<string, string>()
|
|
10
11
|
|
|
11
|
-
const getAsyncStorage = (): StorageLike
|
|
12
|
-
try {
|
|
13
|
-
const req = (0, eval)('require') as (name: string) => unknown
|
|
14
|
-
const asyncStorageModule = req('@react-native-async-storage/async-storage') as {
|
|
15
|
-
default?: StorageLike
|
|
16
|
-
}
|
|
17
|
-
return asyncStorageModule?.default ?? null
|
|
18
|
-
} catch {
|
|
19
|
-
return null
|
|
20
|
-
}
|
|
21
|
-
}
|
|
12
|
+
const getAsyncStorage = (): StorageLike => AsyncStorage
|
|
22
13
|
|
|
23
14
|
const parseSession = (raw: string | null): SessionData | null => {
|
|
24
15
|
if (!raw) return null
|
package/src/session.ts
CHANGED
|
@@ -24,11 +24,15 @@ const getStorage = () => {
|
|
|
24
24
|
|
|
25
25
|
export class SessionManager {
|
|
26
26
|
private readonly key: string
|
|
27
|
+
private readonly usersKey: string
|
|
28
|
+
private readonly sessionsKey: string
|
|
27
29
|
private readonly storage = getStorage()
|
|
28
30
|
private session: SessionData | null = null
|
|
29
31
|
|
|
30
32
|
constructor(appId: string) {
|
|
31
33
|
this.key = `flowerbase:${appId}:session`
|
|
34
|
+
this.usersKey = `flowerbase:${appId}:users`
|
|
35
|
+
this.sessionsKey = `flowerbase:${appId}:sessions`
|
|
32
36
|
this.session = this.load()
|
|
33
37
|
}
|
|
34
38
|
|
|
@@ -56,4 +60,55 @@ export class SessionManager {
|
|
|
56
60
|
this.session = null
|
|
57
61
|
this.storage.removeItem(this.key)
|
|
58
62
|
}
|
|
63
|
+
|
|
64
|
+
getUsersOrder() {
|
|
65
|
+
const raw = this.storage.getItem(this.usersKey)
|
|
66
|
+
if (!raw) return []
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(raw)
|
|
69
|
+
if (!Array.isArray(parsed)) return []
|
|
70
|
+
return parsed.filter((item): item is string => typeof item === 'string')
|
|
71
|
+
} catch {
|
|
72
|
+
return []
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
setUsersOrder(order: string[]) {
|
|
77
|
+
if (order.length === 0) {
|
|
78
|
+
this.storage.removeItem(this.usersKey)
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
this.storage.setItem(this.usersKey, JSON.stringify(order))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
getSessionsByUser() {
|
|
85
|
+
const raw = this.storage.getItem(this.sessionsKey)
|
|
86
|
+
if (!raw) return {} as Record<string, SessionData>
|
|
87
|
+
try {
|
|
88
|
+
const parsed = JSON.parse(raw) as Record<string, SessionData>
|
|
89
|
+
const normalized: Record<string, SessionData> = {}
|
|
90
|
+
for (const [userId, session] of Object.entries(parsed)) {
|
|
91
|
+
if (
|
|
92
|
+
session &&
|
|
93
|
+
typeof session === 'object' &&
|
|
94
|
+
typeof session.accessToken === 'string' &&
|
|
95
|
+
typeof session.refreshToken === 'string' &&
|
|
96
|
+
typeof session.userId === 'string'
|
|
97
|
+
) {
|
|
98
|
+
normalized[userId] = session
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return normalized
|
|
102
|
+
} catch {
|
|
103
|
+
return {} as Record<string, SessionData>
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
setSessionsByUser(sessionsByUser: Record<string, SessionData>) {
|
|
108
|
+
if (Object.keys(sessionsByUser).length === 0) {
|
|
109
|
+
this.storage.removeItem(this.sessionsKey)
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
this.storage.setItem(this.sessionsKey, JSON.stringify(sessionsByUser))
|
|
113
|
+
}
|
|
59
114
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type AppConfig = {
|
|
2
2
|
id: string
|
|
3
|
-
baseUrl
|
|
3
|
+
baseUrl?: string
|
|
4
4
|
timeout?: number
|
|
5
5
|
}
|
|
6
6
|
|
|
@@ -8,6 +8,7 @@ export type CredentialsLike =
|
|
|
8
8
|
| { provider: 'local-userpass'; email: string; password: string }
|
|
9
9
|
| { provider: 'anon-user' }
|
|
10
10
|
| { provider: 'custom-function'; payload: Record<string, unknown> }
|
|
11
|
+
| { provider: 'custom-token'; token: string }
|
|
11
12
|
|
|
12
13
|
export type SessionData = {
|
|
13
14
|
accessToken: string
|
|
@@ -47,7 +48,21 @@ export type WatchAsyncIterator<TChange = unknown> = AsyncIterableIterator<TChang
|
|
|
47
48
|
export interface CollectionLike {
|
|
48
49
|
find: (query?: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>
|
|
49
50
|
findOne: (query?: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>
|
|
51
|
+
findOneAndUpdate: (
|
|
52
|
+
filter: Record<string, unknown>,
|
|
53
|
+
update: Record<string, unknown>,
|
|
54
|
+
options?: Record<string, unknown>
|
|
55
|
+
) => Promise<unknown>
|
|
56
|
+
findOneAndReplace: (
|
|
57
|
+
filter: Record<string, unknown>,
|
|
58
|
+
replacement: Record<string, unknown>,
|
|
59
|
+
options?: Record<string, unknown>
|
|
60
|
+
) => Promise<unknown>
|
|
61
|
+
findOneAndDelete: (filter: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>
|
|
62
|
+
aggregate: (pipeline: Record<string, unknown>[]) => Promise<unknown>
|
|
63
|
+
count: (filter?: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>
|
|
50
64
|
insertOne: (document: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>
|
|
65
|
+
insertMany: (documents: Record<string, unknown>[], options?: Record<string, unknown>) => Promise<unknown>
|
|
51
66
|
updateOne: (
|
|
52
67
|
filter: Record<string, unknown>,
|
|
53
68
|
update: Record<string, unknown>,
|
|
@@ -59,7 +74,8 @@ export interface CollectionLike {
|
|
|
59
74
|
options?: Record<string, unknown>
|
|
60
75
|
) => Promise<unknown>
|
|
61
76
|
deleteOne: (filter: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>
|
|
62
|
-
|
|
77
|
+
deleteMany: (filter: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>
|
|
78
|
+
watch: (options?: unknown) => WatchAsyncIterator<unknown>
|
|
63
79
|
}
|
|
64
80
|
|
|
65
81
|
export interface MongoDbLike {
|
|
@@ -72,13 +88,27 @@ export interface MongoClientLike {
|
|
|
72
88
|
|
|
73
89
|
export interface UserLike {
|
|
74
90
|
id: string
|
|
91
|
+
state: 'active' | 'logged-out' | 'removed'
|
|
92
|
+
isLoggedIn: boolean
|
|
93
|
+
accessToken: string | null
|
|
94
|
+
refreshToken: string | null
|
|
95
|
+
providerType: string | null
|
|
96
|
+
identities: unknown[]
|
|
97
|
+
customData: Record<string, unknown>
|
|
75
98
|
profile?: {
|
|
76
99
|
email?: string
|
|
77
100
|
[key: string]: unknown
|
|
78
101
|
}
|
|
79
|
-
functions: Record<string, (...args: unknown[]) => Promise<unknown>>
|
|
102
|
+
functions: Record<string, (...args: unknown[]) => Promise<unknown>> & {
|
|
103
|
+
callFunction: (name: string, ...args: unknown[]) => Promise<unknown>
|
|
104
|
+
callFunctionStreaming: (name: string, ...args: unknown[]) => Promise<AsyncIterable<Uint8Array>>
|
|
105
|
+
}
|
|
80
106
|
logOut: () => Promise<void>
|
|
107
|
+
callFunction: (name: string, ...args: unknown[]) => Promise<unknown>
|
|
81
108
|
refreshAccessToken: () => Promise<string>
|
|
82
|
-
refreshCustomData: () => Promise<
|
|
109
|
+
refreshCustomData: () => Promise<Record<string, unknown>>
|
|
83
110
|
mongoClient: (serviceName: string) => MongoClientLike
|
|
111
|
+
addListener: (callback: () => void) => void
|
|
112
|
+
removeListener: (callback: () => void) => void
|
|
113
|
+
removeAllListeners: () => void
|
|
84
114
|
}
|
package/src/user.ts
CHANGED
|
@@ -1,39 +1,150 @@
|
|
|
1
1
|
import type { App } from './app'
|
|
2
2
|
import { createFunctionsProxy } from './functions'
|
|
3
3
|
import { createMongoClient } from './mongo'
|
|
4
|
-
import { MongoClientLike,
|
|
4
|
+
import { MongoClientLike, UserLike } from './types'
|
|
5
5
|
|
|
6
6
|
export class User implements UserLike {
|
|
7
|
-
id: string
|
|
7
|
+
readonly id: string
|
|
8
|
+
customData: Record<string, unknown> = {}
|
|
8
9
|
profile?: { email?: string;[key: string]: unknown }
|
|
9
10
|
private readonly app: App
|
|
11
|
+
private _providerType: string | null = null
|
|
12
|
+
private readonly listeners = new Set<() => void>()
|
|
10
13
|
|
|
11
|
-
functions: Record<string, (...args: unknown[]) => Promise<unknown>>
|
|
14
|
+
functions: Record<string, (...args: unknown[]) => Promise<unknown>> & {
|
|
15
|
+
callFunction: (name: string, ...args: unknown[]) => Promise<unknown>
|
|
16
|
+
callFunctionStreaming: (name: string, ...args: unknown[]) => Promise<AsyncIterable<Uint8Array>>
|
|
17
|
+
}
|
|
12
18
|
|
|
13
19
|
constructor(app: App, id: string) {
|
|
14
20
|
this.app = app
|
|
15
21
|
this.id = id
|
|
16
|
-
this.functions = createFunctionsProxy(
|
|
22
|
+
this.functions = createFunctionsProxy(
|
|
23
|
+
(name, args) => this.app.callFunction(name, args, this.id),
|
|
24
|
+
(name, args) => this.app.callFunctionStreaming(name, args, this.id)
|
|
25
|
+
)
|
|
26
|
+
this.customData = this.resolveCustomDataFromToken()
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get state() {
|
|
30
|
+
if (!this.app.hasUser(this.id)) {
|
|
31
|
+
return 'removed'
|
|
32
|
+
}
|
|
33
|
+
return this.isLoggedIn ? 'active' : 'logged-out'
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get isLoggedIn() {
|
|
37
|
+
return this.accessToken !== null && this.refreshToken !== null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get providerType() {
|
|
41
|
+
return this._providerType
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get identities() {
|
|
45
|
+
return this.app.getProfileSnapshot(this.id)?.identities ?? []
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private resolveCustomDataFromToken() {
|
|
49
|
+
const payload = this.decodeAccessTokenPayload()
|
|
50
|
+
if (!payload) return {}
|
|
51
|
+
return (
|
|
52
|
+
'user_data' in payload && payload.user_data && typeof payload.user_data === 'object'
|
|
53
|
+
? (payload.user_data as Record<string, unknown>)
|
|
54
|
+
: 'userData' in payload && payload.userData && typeof payload.userData === 'object'
|
|
55
|
+
? (payload.userData as Record<string, unknown>)
|
|
56
|
+
: 'custom_data' in payload && payload.custom_data && typeof payload.custom_data === 'object'
|
|
57
|
+
? (payload.custom_data as Record<string, unknown>)
|
|
58
|
+
: {}
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get accessToken() {
|
|
63
|
+
const session = this.app.getSession(this.id)
|
|
64
|
+
if (!session) return null
|
|
65
|
+
return session.accessToken
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get refreshToken() {
|
|
69
|
+
const session = this.app.getSession(this.id)
|
|
70
|
+
if (!session) return null
|
|
71
|
+
return session.refreshToken
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
setProviderType(providerType: string) {
|
|
75
|
+
this._providerType = providerType
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private decodeAccessTokenPayload() {
|
|
79
|
+
if (!this.accessToken) return null
|
|
80
|
+
const parts = this.accessToken.split('.')
|
|
81
|
+
if (parts.length < 2) return null
|
|
82
|
+
|
|
83
|
+
const base64Url = parts[1]
|
|
84
|
+
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(base64Url.length / 4) * 4, '=')
|
|
85
|
+
|
|
86
|
+
const decodeBase64 = (input: string) => {
|
|
87
|
+
if (typeof atob === 'function') return atob(input)
|
|
88
|
+
const runtimeBuffer = (globalThis as { Buffer?: { from: (data: string, encoding: string) => { toString: (enc: string) => string } } }).Buffer
|
|
89
|
+
if (runtimeBuffer) return runtimeBuffer.from(input, 'base64').toString('utf8')
|
|
90
|
+
return ''
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const decoded = decodeBase64(base64)
|
|
95
|
+
return JSON.parse(decoded) as Record<string, unknown>
|
|
96
|
+
} catch {
|
|
97
|
+
return null
|
|
98
|
+
}
|
|
17
99
|
}
|
|
18
100
|
|
|
19
101
|
async logOut() {
|
|
20
|
-
await this.app.logoutUser()
|
|
102
|
+
await this.app.logoutUser(this.id)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async callFunction(name: string, ...args: unknown[]) {
|
|
106
|
+
return this.app.callFunction(name, args, this.id)
|
|
21
107
|
}
|
|
22
108
|
|
|
23
109
|
async refreshAccessToken() {
|
|
24
|
-
|
|
110
|
+
const accessToken = await this.app.refreshAccessToken(this.id)
|
|
111
|
+
this.customData = this.resolveCustomDataFromToken()
|
|
112
|
+
return accessToken
|
|
25
113
|
}
|
|
26
114
|
|
|
27
|
-
async refreshCustomData(): Promise<
|
|
28
|
-
const profile = await this.app.getProfile()
|
|
115
|
+
async refreshCustomData(): Promise<Record<string, unknown>> {
|
|
116
|
+
const profile = await this.app.getProfile(this.id)
|
|
29
117
|
this.profile = profile.data
|
|
30
|
-
|
|
118
|
+
this.customData = (profile.custom_data && typeof profile.custom_data === 'object'
|
|
119
|
+
? profile.custom_data
|
|
120
|
+
: this.resolveCustomDataFromToken()) as Record<string, unknown>
|
|
121
|
+
this.notifyListeners()
|
|
122
|
+
return this.customData
|
|
31
123
|
}
|
|
32
124
|
|
|
33
125
|
mongoClient(serviceName: string): MongoClientLike {
|
|
34
|
-
|
|
35
|
-
|
|
126
|
+
return createMongoClient(this.app, serviceName, this.id)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
addListener(callback: () => void) {
|
|
130
|
+
this.listeners.add(callback)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
removeListener(callback: () => void) {
|
|
134
|
+
this.listeners.delete(callback)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
removeAllListeners() {
|
|
138
|
+
this.listeners.clear()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
notifyListeners() {
|
|
142
|
+
for (const callback of Array.from(this.listeners)) {
|
|
143
|
+
try {
|
|
144
|
+
callback()
|
|
145
|
+
} catch {
|
|
146
|
+
// Listener failures should not break user lifecycle operations.
|
|
147
|
+
}
|
|
36
148
|
}
|
|
37
|
-
return createMongoClient(this.app)
|
|
38
149
|
}
|
|
39
150
|
}
|