@flowerforce/flowerbase-client 0.1.1-beta.2
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 +198 -0
- package/dist/app.d.ts +40 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +186 -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 +7 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +24 -0
- package/dist/functions.d.ts +3 -0
- package/dist/functions.d.ts.map +1 -0
- package/dist/functions.js +30 -0
- package/dist/http.d.ts +15 -0
- package/dist/http.d.ts.map +1 -0
- package/dist/http.js +74 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/mongo.d.ts +4 -0
- package/dist/mongo.d.ts.map +1 -0
- package/dist/mongo.js +61 -0
- package/dist/session.d.ts +12 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +53 -0
- package/dist/session.native.d.ts +14 -0
- package/dist/session.native.d.ts.map +1 -0
- package/dist/session.native.js +81 -0
- package/dist/types.d.ts +73 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/user.d.ts +17 -0
- package/dist/user.d.ts.map +1 -0
- package/dist/user.js +30 -0
- package/dist/watch.d.ts +3 -0
- package/dist/watch.d.ts.map +1 -0
- package/dist/watch.js +138 -0
- package/jest.config.ts +13 -0
- package/package.json +30 -0
- package/project.json +11 -0
- package/rollup.config.js +17 -0
- package/src/__tests__/auth.test.ts +164 -0
- package/src/__tests__/compat.test.ts +12 -0
- package/src/__tests__/functions.test.ts +76 -0
- package/src/__tests__/mongo.test.ts +48 -0
- package/src/__tests__/session.test.ts +103 -0
- package/src/__tests__/watch.test.ts +138 -0
- package/src/app.ts +235 -0
- package/src/bson.ts +6 -0
- package/src/credentials.ts +24 -0
- package/src/functions.ts +32 -0
- package/src/http.ts +92 -0
- package/src/index.ts +14 -0
- package/src/mongo.ts +63 -0
- package/src/session.native.ts +98 -0
- package/src/session.ts +59 -0
- package/src/types.ts +84 -0
- package/src/user.ts +39 -0
- package/src/watch.ts +150 -0
- package/tsconfig.json +34 -0
- package/tsconfig.spec.json +13 -0
package/src/app.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { normalizeFunctionResponse } from './functions'
|
|
2
|
+
import { requestJson } from './http'
|
|
3
|
+
import { SessionManager } from './session'
|
|
4
|
+
import { AppConfig, CredentialsLike, FunctionCallPayload, ProfileData, SessionData } from './types'
|
|
5
|
+
import { User } from './user'
|
|
6
|
+
|
|
7
|
+
const API_PREFIX = '/api/client/v2.0'
|
|
8
|
+
|
|
9
|
+
type LoginResponse = {
|
|
10
|
+
access_token: string
|
|
11
|
+
refresh_token: string
|
|
12
|
+
user_id: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type SessionResponse = {
|
|
16
|
+
access_token: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class App {
|
|
20
|
+
readonly id: string
|
|
21
|
+
readonly baseUrl: string
|
|
22
|
+
readonly timeout: number
|
|
23
|
+
private readonly sessionManager: SessionManager
|
|
24
|
+
currentUser: User | null = null
|
|
25
|
+
private readonly sessionBootstrapPromise: Promise<void>
|
|
26
|
+
|
|
27
|
+
emailPasswordAuth: {
|
|
28
|
+
registerUser: (input: { email: string; password: string }) => Promise<unknown>
|
|
29
|
+
sendResetPasswordEmail: (email: string) => Promise<unknown>
|
|
30
|
+
callResetPasswordFunction: (email: string, password: string, ...args: unknown[]) => Promise<unknown>
|
|
31
|
+
resetPassword: (input: { token: string; tokenId: string; password: string }) => Promise<unknown>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
constructor(config: AppConfig) {
|
|
35
|
+
this.id = config.id
|
|
36
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, '')
|
|
37
|
+
this.timeout = config.timeout ?? 10000
|
|
38
|
+
this.sessionManager = new SessionManager(this.id)
|
|
39
|
+
|
|
40
|
+
const session = this.sessionManager.get()
|
|
41
|
+
if (session?.userId) {
|
|
42
|
+
this.currentUser = new User(this, session.userId)
|
|
43
|
+
}
|
|
44
|
+
this.sessionBootstrapPromise = this.bootstrapSessionOnLoad()
|
|
45
|
+
|
|
46
|
+
this.emailPasswordAuth = {
|
|
47
|
+
registerUser: ({ email, password }) =>
|
|
48
|
+
this.postProvider('/local-userpass/register', { email, password }),
|
|
49
|
+
sendResetPasswordEmail: (email) =>
|
|
50
|
+
this.postProvider('/local-userpass/reset/send', { email }),
|
|
51
|
+
callResetPasswordFunction: (email, password, ...args) =>
|
|
52
|
+
this.postProvider('/local-userpass/reset/call', { email, password, arguments: args }),
|
|
53
|
+
resetPassword: ({ token, tokenId, password }) =>
|
|
54
|
+
this.postProvider('/local-userpass/reset', { token, tokenId, password })
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private providerUrl(path: string) {
|
|
59
|
+
return `${this.baseUrl}${API_PREFIX}/app/${this.id}/auth/providers${path}`
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private authUrl(path: string) {
|
|
63
|
+
return `${this.baseUrl}${API_PREFIX}/auth${path}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private functionsUrl(path = '/call') {
|
|
67
|
+
return `${this.baseUrl}${API_PREFIX}/app/${this.id}/functions${path}`
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private async createSession(refreshToken: string): Promise<SessionResponse> {
|
|
71
|
+
return requestJson<SessionResponse>({
|
|
72
|
+
url: this.authUrl('/session'),
|
|
73
|
+
method: 'POST',
|
|
74
|
+
bearerToken: refreshToken,
|
|
75
|
+
timeout: this.timeout
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private async bootstrapSessionOnLoad(): Promise<void> {
|
|
80
|
+
const session = this.sessionManager.get()
|
|
81
|
+
if (!session || typeof localStorage === 'undefined') {
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const result = await this.createSession(session.refreshToken)
|
|
87
|
+
this.sessionManager.set({
|
|
88
|
+
...session,
|
|
89
|
+
accessToken: result.access_token
|
|
90
|
+
})
|
|
91
|
+
} catch {
|
|
92
|
+
this.sessionManager.clear()
|
|
93
|
+
this.currentUser = null
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private async ensureSessionBootstrapped() {
|
|
98
|
+
await this.sessionBootstrapPromise
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private async setLoggedInUser(data: LoginResponse, profileEmail?: string) {
|
|
102
|
+
const sessionResult = await this.createSession(data.refresh_token)
|
|
103
|
+
const session: SessionData = {
|
|
104
|
+
accessToken: sessionResult.access_token,
|
|
105
|
+
refreshToken: data.refresh_token,
|
|
106
|
+
userId: data.user_id
|
|
107
|
+
}
|
|
108
|
+
this.sessionManager.set(session)
|
|
109
|
+
this.currentUser = new User(this, data.user_id)
|
|
110
|
+
if (profileEmail) {
|
|
111
|
+
this.currentUser.profile = { email: profileEmail }
|
|
112
|
+
}
|
|
113
|
+
return this.currentUser
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async logIn(credentials: CredentialsLike) {
|
|
117
|
+
if (credentials.provider === 'local-userpass') {
|
|
118
|
+
const result = await this.postProvider<LoginResponse>('/local-userpass/login', {
|
|
119
|
+
username: credentials.email,
|
|
120
|
+
password: credentials.password
|
|
121
|
+
})
|
|
122
|
+
return this.setLoggedInUser(result, credentials.email)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (credentials.provider === 'anon-user') {
|
|
126
|
+
const result = await this.postProvider<LoginResponse>('/anon-user/login', {})
|
|
127
|
+
return this.setLoggedInUser(result)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const result = await this.postProvider<LoginResponse>('/custom-function/login', credentials.payload)
|
|
131
|
+
return this.setLoggedInUser(result)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
getSessionOrThrow() {
|
|
135
|
+
const session = this.sessionManager.get()
|
|
136
|
+
if (!session) {
|
|
137
|
+
throw new Error('User is not authenticated')
|
|
138
|
+
}
|
|
139
|
+
return session
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async postProvider<T = unknown>(path: string, body: unknown): Promise<T> {
|
|
143
|
+
return requestJson<T>({
|
|
144
|
+
url: this.providerUrl(path),
|
|
145
|
+
method: 'POST',
|
|
146
|
+
body,
|
|
147
|
+
timeout: this.timeout
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async callFunction(name: string, args: unknown[]) {
|
|
152
|
+
await this.ensureSessionBootstrapped()
|
|
153
|
+
const session = this.getSessionOrThrow()
|
|
154
|
+
const payload: FunctionCallPayload = {
|
|
155
|
+
name,
|
|
156
|
+
arguments: args
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const result = await requestJson<unknown>({
|
|
160
|
+
url: this.functionsUrl('/call'),
|
|
161
|
+
method: 'POST',
|
|
162
|
+
body: payload,
|
|
163
|
+
bearerToken: session.accessToken,
|
|
164
|
+
timeout: this.timeout
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
return normalizeFunctionResponse(result)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async callService(name: string, args: unknown[]) {
|
|
171
|
+
await this.ensureSessionBootstrapped()
|
|
172
|
+
const session = this.getSessionOrThrow()
|
|
173
|
+
const payload: FunctionCallPayload = {
|
|
174
|
+
name,
|
|
175
|
+
service: 'mongodb-atlas',
|
|
176
|
+
arguments: args
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return requestJson<unknown>({
|
|
180
|
+
url: this.functionsUrl('/call'),
|
|
181
|
+
method: 'POST',
|
|
182
|
+
body: payload,
|
|
183
|
+
bearerToken: session.accessToken,
|
|
184
|
+
timeout: this.timeout
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async getProfile(): Promise<ProfileData> {
|
|
189
|
+
await this.ensureSessionBootstrapped()
|
|
190
|
+
const session = this.getSessionOrThrow()
|
|
191
|
+
return requestJson<ProfileData>({
|
|
192
|
+
url: this.authUrl('/profile'),
|
|
193
|
+
method: 'GET',
|
|
194
|
+
bearerToken: session.accessToken,
|
|
195
|
+
timeout: this.timeout
|
|
196
|
+
})
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async refreshAccessToken() {
|
|
200
|
+
await this.ensureSessionBootstrapped()
|
|
201
|
+
const session = this.getSessionOrThrow()
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const result = await this.createSession(session.refreshToken)
|
|
205
|
+
|
|
206
|
+
this.sessionManager.set({
|
|
207
|
+
...session,
|
|
208
|
+
accessToken: result.access_token
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
return result.access_token
|
|
212
|
+
} catch (error) {
|
|
213
|
+
this.sessionManager.clear()
|
|
214
|
+
this.currentUser = null
|
|
215
|
+
throw error
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async logoutUser() {
|
|
220
|
+
const session = this.sessionManager.get()
|
|
221
|
+
try {
|
|
222
|
+
if (session) {
|
|
223
|
+
await requestJson({
|
|
224
|
+
url: this.authUrl('/session'),
|
|
225
|
+
method: 'DELETE',
|
|
226
|
+
bearerToken: session.refreshToken,
|
|
227
|
+
timeout: this.timeout
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
} finally {
|
|
231
|
+
this.sessionManager.clear()
|
|
232
|
+
this.currentUser = null
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
package/src/bson.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
}
|
package/src/functions.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { EJSON } from './bson'
|
|
2
|
+
|
|
3
|
+
const deserialize = <T>(value: T): T => {
|
|
4
|
+
if (!value || typeof value !== 'object') return value
|
|
5
|
+
return EJSON.deserialize(value as Record<string, unknown>) as T
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const normalizeFunctionResponse = (value: unknown) => {
|
|
9
|
+
if (typeof value === 'string') {
|
|
10
|
+
try {
|
|
11
|
+
const parsed = JSON.parse(value)
|
|
12
|
+
return deserialize(parsed)
|
|
13
|
+
} catch {
|
|
14
|
+
return value
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return deserialize(value)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const createFunctionsProxy = (
|
|
22
|
+
callFunction: (name: string, args: unknown[]) => Promise<unknown>
|
|
23
|
+
): Record<string, (...args: unknown[]) => Promise<unknown>> =>
|
|
24
|
+
new Proxy(
|
|
25
|
+
{},
|
|
26
|
+
{
|
|
27
|
+
get: (_, key) => {
|
|
28
|
+
if (typeof key !== 'string') return undefined
|
|
29
|
+
return (...args: unknown[]) => callFunction(key, args)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
) as Record<string, (...args: unknown[]) => Promise<unknown>>
|
package/src/http.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export class FlowerbaseHttpError extends Error {
|
|
2
|
+
status: number
|
|
3
|
+
payload?: unknown
|
|
4
|
+
|
|
5
|
+
constructor(message: string, status: number, payload?: unknown) {
|
|
6
|
+
super(message)
|
|
7
|
+
this.name = 'FlowerbaseHttpError'
|
|
8
|
+
this.status = status
|
|
9
|
+
this.payload = payload
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type RequestParams = {
|
|
14
|
+
url: string
|
|
15
|
+
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
|
16
|
+
body?: unknown
|
|
17
|
+
bearerToken?: string
|
|
18
|
+
timeout?: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const parseBody = async (response: Response) => {
|
|
22
|
+
const text = await response.text()
|
|
23
|
+
if (!text) return null
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(text)
|
|
27
|
+
} catch {
|
|
28
|
+
return text
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const timeoutSignal = (timeout = 10000) => {
|
|
33
|
+
const controller = new AbortController()
|
|
34
|
+
const timer = setTimeout(() => controller.abort(), timeout)
|
|
35
|
+
return {
|
|
36
|
+
signal: controller.signal,
|
|
37
|
+
clear: () => clearTimeout(timer)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const requestJson = async <T = unknown>({
|
|
42
|
+
url,
|
|
43
|
+
method = 'GET',
|
|
44
|
+
body,
|
|
45
|
+
bearerToken,
|
|
46
|
+
timeout
|
|
47
|
+
}: RequestParams): Promise<T> => {
|
|
48
|
+
const { signal, clear } = timeoutSignal(timeout)
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const response = await fetch(url, {
|
|
52
|
+
method,
|
|
53
|
+
headers: {
|
|
54
|
+
...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
|
|
55
|
+
...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {})
|
|
56
|
+
},
|
|
57
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
58
|
+
signal
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const payload = await parseBody(response)
|
|
62
|
+
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
let parsedErrorMessage: string | null = null
|
|
65
|
+
if (payload && typeof payload === 'object' && 'error' in payload && typeof payload.error === 'string') {
|
|
66
|
+
try {
|
|
67
|
+
const parsed = JSON.parse(payload.error)
|
|
68
|
+
if (parsed && typeof parsed === 'object' && 'message' in parsed && typeof parsed.message === 'string') {
|
|
69
|
+
parsedErrorMessage = parsed.message
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
parsedErrorMessage = null
|
|
73
|
+
}
|
|
74
|
+
}
|
|
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
|
+
}
|
|
87
|
+
|
|
88
|
+
return payload as T
|
|
89
|
+
} finally {
|
|
90
|
+
clear()
|
|
91
|
+
}
|
|
92
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { BSON, EJSON, ObjectId, ObjectID } from './bson'
|
|
2
|
+
|
|
3
|
+
export { App } from './app'
|
|
4
|
+
export { User } from './user'
|
|
5
|
+
export { Credentials } from './credentials'
|
|
6
|
+
export { BSON, EJSON, ObjectId, ObjectID }
|
|
7
|
+
export type {
|
|
8
|
+
AppConfig,
|
|
9
|
+
CredentialsLike,
|
|
10
|
+
UserLike,
|
|
11
|
+
MongoClientLike,
|
|
12
|
+
CollectionLike,
|
|
13
|
+
WatchAsyncIterator
|
|
14
|
+
} from './types'
|
package/src/mongo.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { App } from './app'
|
|
2
|
+
import { EJSON } from './bson'
|
|
3
|
+
import { CollectionLike, MongoClientLike } from './types'
|
|
4
|
+
import { createWatchIterator } from './watch'
|
|
5
|
+
|
|
6
|
+
const serialize = (value: unknown) => EJSON.serialize(value, { relaxed: false })
|
|
7
|
+
const deserialize = <T>(value: T): T => {
|
|
8
|
+
if (!value || typeof value !== 'object') return value
|
|
9
|
+
return EJSON.deserialize(value as Record<string, unknown>) as T
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const mapResult = (value: unknown) => {
|
|
13
|
+
if (typeof value === 'string') {
|
|
14
|
+
try {
|
|
15
|
+
return deserialize(JSON.parse(value))
|
|
16
|
+
} catch {
|
|
17
|
+
return value
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return deserialize(value)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const createMongoClient = (app: App): MongoClientLike => ({
|
|
24
|
+
db: (database: string) => ({
|
|
25
|
+
collection: (collection: string): CollectionLike => {
|
|
26
|
+
const callService = async (name: string, args: unknown[]) => {
|
|
27
|
+
const result = await app.callService(name, [
|
|
28
|
+
{
|
|
29
|
+
database,
|
|
30
|
+
collection,
|
|
31
|
+
...serialize(args[0] ?? {}),
|
|
32
|
+
...(args[1] !== undefined ? { options: serialize(args[1]) } : {})
|
|
33
|
+
}
|
|
34
|
+
])
|
|
35
|
+
return mapResult(result)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
find: (query = {}, options = {}) => callService('find', [{ query, options }]),
|
|
40
|
+
findOne: (query = {}, options = {}) => callService('findOne', [{ query, options }]),
|
|
41
|
+
insertOne: (document, options = {}) => callService('insertOne', [{ document, options }]),
|
|
42
|
+
updateOne: (filter, update, options = {}) =>
|
|
43
|
+
callService('updateOne', [{ filter, update, options }]),
|
|
44
|
+
updateMany: (filter, update, options = {}) =>
|
|
45
|
+
callService('updateMany', [{ filter, update, options }]),
|
|
46
|
+
deleteOne: (filter, options = {}) => callService('deleteOne', [{ query: filter, options }]),
|
|
47
|
+
watch: (pipeline = [], options = {}) => {
|
|
48
|
+
const session = app.getSessionOrThrow()
|
|
49
|
+
return createWatchIterator({
|
|
50
|
+
appId: app.id,
|
|
51
|
+
baseUrl: app.baseUrl,
|
|
52
|
+
accessToken: session.accessToken,
|
|
53
|
+
database,
|
|
54
|
+
collection,
|
|
55
|
+
pipeline,
|
|
56
|
+
options,
|
|
57
|
+
timeout: app.timeout
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
})
|
|
63
|
+
})
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { SessionData } from './types'
|
|
2
|
+
|
|
3
|
+
type StorageLike = {
|
|
4
|
+
getItem: (key: string) => Promise<string | null>
|
|
5
|
+
setItem: (key: string, value: string) => Promise<void>
|
|
6
|
+
removeItem: (key: string) => Promise<void>
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const memoryStore = new Map<string, string>()
|
|
10
|
+
|
|
11
|
+
const getAsyncStorage = (): StorageLike | null => {
|
|
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
|
+
}
|
|
22
|
+
|
|
23
|
+
const parseSession = (raw: string | null): SessionData | null => {
|
|
24
|
+
if (!raw) return null
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(raw) as SessionData
|
|
27
|
+
} catch {
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export class SessionManager {
|
|
33
|
+
private readonly key: string
|
|
34
|
+
private session: SessionData | null = null
|
|
35
|
+
private readonly asyncStorage = getAsyncStorage()
|
|
36
|
+
private hydrationPromise: Promise<void> | null = null
|
|
37
|
+
|
|
38
|
+
constructor(appId: string) {
|
|
39
|
+
this.key = `flowerbase:${appId}:session`
|
|
40
|
+
this.session = this.load()
|
|
41
|
+
void this.hydrateFromAsyncStorage()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private hydrateFromAsyncStorage() {
|
|
45
|
+
if (!this.asyncStorage) {
|
|
46
|
+
return Promise.resolve()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (this.hydrationPromise) {
|
|
50
|
+
return this.hydrationPromise
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
this.hydrationPromise = this.asyncStorage
|
|
54
|
+
.getItem(this.key)
|
|
55
|
+
.then((raw) => {
|
|
56
|
+
const parsed = parseSession(raw)
|
|
57
|
+
if (!parsed) return
|
|
58
|
+
this.session = parsed
|
|
59
|
+
memoryStore.set(this.key, JSON.stringify(parsed))
|
|
60
|
+
})
|
|
61
|
+
.catch(() => {
|
|
62
|
+
// Ignore storage read failures and keep memory fallback.
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
return this.hydrationPromise
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
load(): SessionData | null {
|
|
69
|
+
return parseSession(memoryStore.get(this.key) ?? null)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get() {
|
|
73
|
+
return this.session
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
set(session: SessionData) {
|
|
77
|
+
this.session = session
|
|
78
|
+
const raw = JSON.stringify(session)
|
|
79
|
+
memoryStore.set(this.key, raw)
|
|
80
|
+
|
|
81
|
+
if (this.asyncStorage) {
|
|
82
|
+
void this.asyncStorage.setItem(this.key, raw).catch(() => {
|
|
83
|
+
// Ignore write failures and keep memory fallback.
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
clear() {
|
|
89
|
+
this.session = null
|
|
90
|
+
memoryStore.delete(this.key)
|
|
91
|
+
|
|
92
|
+
if (this.asyncStorage) {
|
|
93
|
+
void this.asyncStorage.removeItem(this.key).catch(() => {
|
|
94
|
+
// Ignore delete failures and keep memory fallback.
|
|
95
|
+
})
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
package/src/session.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { SessionData } from './types'
|
|
2
|
+
|
|
3
|
+
const memoryStore = new Map<string, string>()
|
|
4
|
+
|
|
5
|
+
const getStorage = () => {
|
|
6
|
+
if (typeof localStorage !== 'undefined') {
|
|
7
|
+
return {
|
|
8
|
+
getItem: (key: string) => localStorage.getItem(key),
|
|
9
|
+
setItem: (key: string, value: string) => localStorage.setItem(key, value),
|
|
10
|
+
removeItem: (key: string) => localStorage.removeItem(key)
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
getItem: (key: string) => memoryStore.get(key) ?? null,
|
|
16
|
+
setItem: (key: string, value: string) => {
|
|
17
|
+
memoryStore.set(key, value)
|
|
18
|
+
},
|
|
19
|
+
removeItem: (key: string) => {
|
|
20
|
+
memoryStore.delete(key)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class SessionManager {
|
|
26
|
+
private readonly key: string
|
|
27
|
+
private readonly storage = getStorage()
|
|
28
|
+
private session: SessionData | null = null
|
|
29
|
+
|
|
30
|
+
constructor(appId: string) {
|
|
31
|
+
this.key = `flowerbase:${appId}:session`
|
|
32
|
+
this.session = this.load()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
load(): SessionData | null {
|
|
36
|
+
const raw = this.storage.getItem(this.key)
|
|
37
|
+
if (!raw) return null
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(raw) as SessionData
|
|
41
|
+
} catch {
|
|
42
|
+
return null
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
get() {
|
|
47
|
+
return this.session
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
set(session: SessionData) {
|
|
51
|
+
this.session = session
|
|
52
|
+
this.storage.setItem(this.key, JSON.stringify(session))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
clear() {
|
|
56
|
+
this.session = null
|
|
57
|
+
this.storage.removeItem(this.key)
|
|
58
|
+
}
|
|
59
|
+
}
|