@flowerforce/flowerbase-client 0.1.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/http.ts
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
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
|
|
61
|
+
|
|
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)
|
|
90
|
+
this.name = 'FlowerbaseHttpError'
|
|
91
|
+
this.status = params.statusCode
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
type RequestParams = {
|
|
96
|
+
url: string
|
|
97
|
+
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
|
98
|
+
body?: unknown
|
|
99
|
+
bearerToken?: string
|
|
100
|
+
timeout?: number
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const parseBody = async (response: Response) => {
|
|
104
|
+
const text = await response.text()
|
|
105
|
+
if (!text) return null
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
return JSON.parse(text)
|
|
109
|
+
} catch {
|
|
110
|
+
return text
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const timeoutSignal = (timeout = 10000) => {
|
|
115
|
+
const controller = new AbortController()
|
|
116
|
+
const timer = setTimeout(() => controller.abort(), timeout)
|
|
117
|
+
return {
|
|
118
|
+
signal: controller.signal,
|
|
119
|
+
clear: () => clearTimeout(timer)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export const requestJson = async <T = unknown>({
|
|
124
|
+
url,
|
|
125
|
+
method = 'GET',
|
|
126
|
+
body,
|
|
127
|
+
bearerToken,
|
|
128
|
+
timeout
|
|
129
|
+
}: RequestParams): Promise<T> => {
|
|
130
|
+
const { signal, clear } = timeoutSignal(timeout)
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const response = await fetch(url, {
|
|
134
|
+
method,
|
|
135
|
+
headers: {
|
|
136
|
+
...(body !== undefined ? { 'Content-Type': 'application/json' } : {}),
|
|
137
|
+
...(bearerToken ? { Authorization: `Bearer ${bearerToken}` } : {})
|
|
138
|
+
},
|
|
139
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
140
|
+
signal
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
const payload = await parseBody(response)
|
|
144
|
+
|
|
145
|
+
if (!response.ok) {
|
|
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]() {
|
|
207
|
+
try {
|
|
208
|
+
while (true) {
|
|
209
|
+
const { done, value } = await reader.read()
|
|
210
|
+
if (done) break
|
|
211
|
+
if (value) yield value
|
|
212
|
+
}
|
|
213
|
+
} finally {
|
|
214
|
+
reader.releaseLock()
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} finally {
|
|
219
|
+
clear()
|
|
220
|
+
}
|
|
221
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
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 { MongoDBRealmError } from './http'
|
|
7
|
+
export { BSON, EJSON, ObjectId, ObjectID }
|
|
8
|
+
export type {
|
|
9
|
+
AppConfig,
|
|
10
|
+
CredentialsLike,
|
|
11
|
+
UserLike,
|
|
12
|
+
MongoClientLike,
|
|
13
|
+
CollectionLike,
|
|
14
|
+
WatchAsyncIterator
|
|
15
|
+
} from './types'
|
package/src/mongo.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
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, serviceName: string, userId: string): 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
|
+
], serviceName, userId)
|
|
35
|
+
return mapResult(result)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const normalizeWatchInput = (input?: unknown) => {
|
|
39
|
+
if (typeof input === 'undefined') {
|
|
40
|
+
return { filter: undefined, ids: undefined }
|
|
41
|
+
}
|
|
42
|
+
if (Array.isArray(input)) {
|
|
43
|
+
throw new Error('watch accepts only an options object with "filter" or "ids"')
|
|
44
|
+
}
|
|
45
|
+
if (!input || typeof input !== 'object') {
|
|
46
|
+
throw new Error('watch options must be an object')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const typed = input as { ids?: unknown[]; filter?: Record<string, unknown>; [key: string]: unknown }
|
|
50
|
+
const keys = Object.keys(typed)
|
|
51
|
+
const hasOnlyAllowedKeys = keys.every((key) => key === 'ids' || key === 'filter')
|
|
52
|
+
if (!hasOnlyAllowedKeys) {
|
|
53
|
+
throw new Error('watch options support only "filter" or "ids"')
|
|
54
|
+
}
|
|
55
|
+
if (typed.ids || typed.filter) {
|
|
56
|
+
if (typed.ids && typed.filter) {
|
|
57
|
+
throw new Error('watch options cannot include both "ids" and "filter"')
|
|
58
|
+
}
|
|
59
|
+
const { ids, filter } = typed
|
|
60
|
+
if (filter && typeof filter === 'object' && '$match' in filter) {
|
|
61
|
+
throw new Error('watch filter must be a query object, not a $match stage')
|
|
62
|
+
}
|
|
63
|
+
if (ids) {
|
|
64
|
+
if (!Array.isArray(ids)) {
|
|
65
|
+
throw new Error('watch ids must be an array')
|
|
66
|
+
}
|
|
67
|
+
return { filter: undefined, ids }
|
|
68
|
+
}
|
|
69
|
+
if (filter) {
|
|
70
|
+
return { filter, ids: undefined }
|
|
71
|
+
}
|
|
72
|
+
return { filter: undefined, ids: undefined }
|
|
73
|
+
}
|
|
74
|
+
throw new Error('watch options must include "filter" or "ids"')
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
find: (query = {}, options = {}) => callService('find', [{ query, options }]),
|
|
79
|
+
findOne: (query = {}, options = {}) => callService('findOne', [{ query, options }]),
|
|
80
|
+
findOneAndUpdate: (filter, update, options = {}) =>
|
|
81
|
+
callService('findOneAndUpdate', [{ filter, update, options }]),
|
|
82
|
+
findOneAndReplace: (filter, replacement, options = {}) =>
|
|
83
|
+
callService('findOneAndReplace', [{ filter, replacement, options }]),
|
|
84
|
+
findOneAndDelete: (filter, options = {}) => callService('findOneAndDelete', [{ filter, options }]),
|
|
85
|
+
aggregate: (pipeline) => callService('aggregate', [{ pipeline }]),
|
|
86
|
+
count: (query = {}, options = {}) => callService('count', [{ query, options }]),
|
|
87
|
+
insertOne: (document, options = {}) => callService('insertOne', [{ document, options }]),
|
|
88
|
+
insertMany: (documents, options = {}) => callService('insertMany', [{ documents, options }]),
|
|
89
|
+
updateOne: (filter, update, options = {}) =>
|
|
90
|
+
callService('updateOne', [{ filter, update, options }]),
|
|
91
|
+
updateMany: (filter, update, options = {}) =>
|
|
92
|
+
callService('updateMany', [{ filter, update, options }]),
|
|
93
|
+
deleteOne: (filter, options = {}) => callService('deleteOne', [{ query: filter, options }]),
|
|
94
|
+
deleteMany: (filter, options = {}) => callService('deleteMany', [{ query: filter, options }]),
|
|
95
|
+
watch: (options) => {
|
|
96
|
+
const { filter, ids } = normalizeWatchInput(options)
|
|
97
|
+
const session = app.getSessionOrThrow(userId)
|
|
98
|
+
return createWatchIterator({
|
|
99
|
+
appId: app.id,
|
|
100
|
+
baseUrl: app.baseUrl,
|
|
101
|
+
accessToken: session.accessToken,
|
|
102
|
+
database,
|
|
103
|
+
collection,
|
|
104
|
+
filter,
|
|
105
|
+
ids,
|
|
106
|
+
timeout: app.timeout
|
|
107
|
+
})
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
})
|
|
112
|
+
})
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { SessionData } from './types'
|
|
2
|
+
import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
3
|
+
|
|
4
|
+
type StorageLike = {
|
|
5
|
+
getItem: (key: string) => Promise<string | null>
|
|
6
|
+
setItem: (key: string, value: string) => Promise<void>
|
|
7
|
+
removeItem: (key: string) => Promise<void>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const memoryStore = new Map<string, string>()
|
|
11
|
+
|
|
12
|
+
const getAsyncStorage = (): StorageLike => AsyncStorage
|
|
13
|
+
|
|
14
|
+
const parseSession = (raw: string | null): SessionData | null => {
|
|
15
|
+
if (!raw) return null
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(raw) as SessionData
|
|
18
|
+
} catch {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class SessionManager {
|
|
24
|
+
private readonly key: string
|
|
25
|
+
private session: SessionData | null = null
|
|
26
|
+
private readonly asyncStorage = getAsyncStorage()
|
|
27
|
+
private hydrationPromise: Promise<void> | null = null
|
|
28
|
+
|
|
29
|
+
constructor(appId: string) {
|
|
30
|
+
this.key = `flowerbase:${appId}:session`
|
|
31
|
+
this.session = this.load()
|
|
32
|
+
void this.hydrateFromAsyncStorage()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private hydrateFromAsyncStorage() {
|
|
36
|
+
if (!this.asyncStorage) {
|
|
37
|
+
return Promise.resolve()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (this.hydrationPromise) {
|
|
41
|
+
return this.hydrationPromise
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.hydrationPromise = this.asyncStorage
|
|
45
|
+
.getItem(this.key)
|
|
46
|
+
.then((raw) => {
|
|
47
|
+
const parsed = parseSession(raw)
|
|
48
|
+
if (!parsed) return
|
|
49
|
+
this.session = parsed
|
|
50
|
+
memoryStore.set(this.key, JSON.stringify(parsed))
|
|
51
|
+
})
|
|
52
|
+
.catch(() => {
|
|
53
|
+
// Ignore storage read failures and keep memory fallback.
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
return this.hydrationPromise
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
load(): SessionData | null {
|
|
60
|
+
return parseSession(memoryStore.get(this.key) ?? null)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get() {
|
|
64
|
+
return this.session
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
set(session: SessionData) {
|
|
68
|
+
this.session = session
|
|
69
|
+
const raw = JSON.stringify(session)
|
|
70
|
+
memoryStore.set(this.key, raw)
|
|
71
|
+
|
|
72
|
+
if (this.asyncStorage) {
|
|
73
|
+
void this.asyncStorage.setItem(this.key, raw).catch(() => {
|
|
74
|
+
// Ignore write failures and keep memory fallback.
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
clear() {
|
|
80
|
+
this.session = null
|
|
81
|
+
memoryStore.delete(this.key)
|
|
82
|
+
|
|
83
|
+
if (this.asyncStorage) {
|
|
84
|
+
void this.asyncStorage.removeItem(this.key).catch(() => {
|
|
85
|
+
// Ignore delete failures and keep memory fallback.
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
package/src/session.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
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 usersKey: string
|
|
28
|
+
private readonly sessionsKey: string
|
|
29
|
+
private readonly storage = getStorage()
|
|
30
|
+
private session: SessionData | null = null
|
|
31
|
+
|
|
32
|
+
constructor(appId: string) {
|
|
33
|
+
this.key = `flowerbase:${appId}:session`
|
|
34
|
+
this.usersKey = `flowerbase:${appId}:users`
|
|
35
|
+
this.sessionsKey = `flowerbase:${appId}:sessions`
|
|
36
|
+
this.session = this.load()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
load(): SessionData | null {
|
|
40
|
+
const raw = this.storage.getItem(this.key)
|
|
41
|
+
if (!raw) return null
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(raw) as SessionData
|
|
45
|
+
} catch {
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
get() {
|
|
51
|
+
return this.session
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
set(session: SessionData) {
|
|
55
|
+
this.session = session
|
|
56
|
+
this.storage.setItem(this.key, JSON.stringify(session))
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
clear() {
|
|
60
|
+
this.session = null
|
|
61
|
+
this.storage.removeItem(this.key)
|
|
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
|
+
}
|
|
114
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export type AppConfig = {
|
|
2
|
+
id: string
|
|
3
|
+
baseUrl?: string
|
|
4
|
+
timeout?: number
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type CredentialsLike =
|
|
8
|
+
| { provider: 'local-userpass'; email: string; password: string }
|
|
9
|
+
| { provider: 'anon-user' }
|
|
10
|
+
| { provider: 'custom-function'; payload: Record<string, unknown> }
|
|
11
|
+
| { provider: 'custom-token'; token: string }
|
|
12
|
+
|
|
13
|
+
export type SessionData = {
|
|
14
|
+
accessToken: string
|
|
15
|
+
refreshToken: string
|
|
16
|
+
userId: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type ProfileData = {
|
|
20
|
+
_id?: string
|
|
21
|
+
identities?: unknown[]
|
|
22
|
+
type?: string
|
|
23
|
+
custom_data?: Record<string, unknown>
|
|
24
|
+
data?: Record<string, unknown>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type FunctionCallPayload = {
|
|
28
|
+
name: string
|
|
29
|
+
arguments: unknown[]
|
|
30
|
+
service?: string
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type WatchConfig = {
|
|
34
|
+
appId: string
|
|
35
|
+
baseUrl: string
|
|
36
|
+
accessToken: string
|
|
37
|
+
database: string
|
|
38
|
+
collection: string
|
|
39
|
+
filter?: Record<string, unknown>
|
|
40
|
+
ids?: unknown[]
|
|
41
|
+
timeout?: number
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type WatchAsyncIterator<TChange = unknown> = AsyncIterableIterator<TChange> & {
|
|
45
|
+
close: () => void
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface CollectionLike {
|
|
49
|
+
find: (query?: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>
|
|
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>
|
|
64
|
+
insertOne: (document: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>
|
|
65
|
+
insertMany: (documents: Record<string, unknown>[], options?: Record<string, unknown>) => Promise<unknown>
|
|
66
|
+
updateOne: (
|
|
67
|
+
filter: Record<string, unknown>,
|
|
68
|
+
update: Record<string, unknown>,
|
|
69
|
+
options?: Record<string, unknown>
|
|
70
|
+
) => Promise<unknown>
|
|
71
|
+
updateMany: (
|
|
72
|
+
filter: Record<string, unknown>,
|
|
73
|
+
update: Record<string, unknown>,
|
|
74
|
+
options?: Record<string, unknown>
|
|
75
|
+
) => Promise<unknown>
|
|
76
|
+
deleteOne: (filter: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>
|
|
77
|
+
deleteMany: (filter: Record<string, unknown>, options?: Record<string, unknown>) => Promise<unknown>
|
|
78
|
+
watch: (options?: unknown) => WatchAsyncIterator<unknown>
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface MongoDbLike {
|
|
82
|
+
collection: (name: string) => CollectionLike
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface MongoClientLike {
|
|
86
|
+
db: (name: string) => MongoDbLike
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface UserLike {
|
|
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>
|
|
98
|
+
profile?: {
|
|
99
|
+
email?: string
|
|
100
|
+
[key: string]: unknown
|
|
101
|
+
}
|
|
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
|
+
}
|
|
106
|
+
logOut: () => Promise<void>
|
|
107
|
+
callFunction: (name: string, ...args: unknown[]) => Promise<unknown>
|
|
108
|
+
refreshAccessToken: () => Promise<string>
|
|
109
|
+
refreshCustomData: () => Promise<Record<string, unknown>>
|
|
110
|
+
mongoClient: (serviceName: string) => MongoClientLike
|
|
111
|
+
addListener: (callback: () => void) => void
|
|
112
|
+
removeListener: (callback: () => void) => void
|
|
113
|
+
removeAllListeners: () => void
|
|
114
|
+
}
|