@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/user.ts
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import type { App } from './app'
|
|
2
|
+
import { createFunctionsProxy } from './functions'
|
|
3
|
+
import { createMongoClient } from './mongo'
|
|
4
|
+
import { MongoClientLike, UserLike } from './types'
|
|
5
|
+
|
|
6
|
+
export class User implements UserLike {
|
|
7
|
+
readonly id: string
|
|
8
|
+
customData: Record<string, unknown> = {}
|
|
9
|
+
profile?: { email?: string;[key: string]: unknown }
|
|
10
|
+
private readonly app: App
|
|
11
|
+
private _providerType: string | null = null
|
|
12
|
+
private readonly listeners = new Set<() => void>()
|
|
13
|
+
|
|
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
|
+
}
|
|
18
|
+
|
|
19
|
+
constructor(app: App, id: string) {
|
|
20
|
+
this.app = app
|
|
21
|
+
this.id = id
|
|
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
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async logOut() {
|
|
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)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async refreshAccessToken() {
|
|
110
|
+
const accessToken = await this.app.refreshAccessToken(this.id)
|
|
111
|
+
this.customData = this.resolveCustomDataFromToken()
|
|
112
|
+
return accessToken
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async refreshCustomData(): Promise<Record<string, unknown>> {
|
|
116
|
+
const profile = await this.app.getProfile(this.id)
|
|
117
|
+
this.profile = profile.data
|
|
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
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
mongoClient(serviceName: string): MongoClientLike {
|
|
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
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
package/src/watch.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { EJSON } from './bson'
|
|
2
|
+
import { WatchAsyncIterator, WatchConfig } from './types'
|
|
3
|
+
|
|
4
|
+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
5
|
+
|
|
6
|
+
const createWatchRequest = ({
|
|
7
|
+
database,
|
|
8
|
+
collection,
|
|
9
|
+
filter,
|
|
10
|
+
ids
|
|
11
|
+
}: WatchConfig) => ({
|
|
12
|
+
name: 'watch',
|
|
13
|
+
service: 'mongodb-atlas',
|
|
14
|
+
arguments: [
|
|
15
|
+
{
|
|
16
|
+
database,
|
|
17
|
+
collection,
|
|
18
|
+
...(filter ? { filter } : {}),
|
|
19
|
+
...(ids ? { ids } : {})
|
|
20
|
+
}
|
|
21
|
+
]
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const toBase64 = (input: string) => {
|
|
25
|
+
if (typeof btoa === 'function') {
|
|
26
|
+
return btoa(input)
|
|
27
|
+
}
|
|
28
|
+
throw new Error('Base64 encoder not available in current runtime')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const parseSsePayload = (line: string) => {
|
|
32
|
+
if (!line.startsWith('data:')) return null
|
|
33
|
+
const raw = line.slice(5).trim()
|
|
34
|
+
if (!raw) return null
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
return EJSON.deserialize(JSON.parse(raw))
|
|
38
|
+
} catch {
|
|
39
|
+
return raw
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const createWatchIterator = (config: WatchConfig): WatchAsyncIterator<unknown> => {
|
|
44
|
+
let closed = false
|
|
45
|
+
let activeController: AbortController | null = null
|
|
46
|
+
const queue: unknown[] = []
|
|
47
|
+
const waiters: Array<(value: IteratorResult<unknown>) => void> = []
|
|
48
|
+
|
|
49
|
+
const enqueue = (value: unknown) => {
|
|
50
|
+
const waiter = waiters.shift()
|
|
51
|
+
if (waiter) {
|
|
52
|
+
waiter({ done: false, value })
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
queue.push(value)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const close = () => {
|
|
59
|
+
if (closed) return
|
|
60
|
+
closed = true
|
|
61
|
+
activeController?.abort()
|
|
62
|
+
while (waiters.length > 0) {
|
|
63
|
+
const resolve = waiters.shift()
|
|
64
|
+
resolve?.({ done: true, value: undefined })
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const run = async () => {
|
|
69
|
+
let attempts = 0
|
|
70
|
+
while (!closed) {
|
|
71
|
+
const controller = new AbortController()
|
|
72
|
+
activeController = controller
|
|
73
|
+
const request = createWatchRequest(config)
|
|
74
|
+
const encoded = toBase64(JSON.stringify(EJSON.serialize(request, { relaxed: false })))
|
|
75
|
+
const url = `${config.baseUrl}/api/client/v2.0/app/${config.appId}/functions/call?baas_request=${encodeURIComponent(encoded)}`
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const response = await fetch(url, {
|
|
79
|
+
method: 'GET',
|
|
80
|
+
headers: {
|
|
81
|
+
Authorization: `Bearer ${config.accessToken}`,
|
|
82
|
+
Accept: 'text/event-stream'
|
|
83
|
+
},
|
|
84
|
+
signal: controller.signal
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
if (!response.ok || !response.body) {
|
|
88
|
+
throw new Error(`Watch request failed (${response.status})`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
attempts = 0
|
|
92
|
+
const reader = response.body.getReader()
|
|
93
|
+
const decoder = new TextDecoder()
|
|
94
|
+
let buffer = ''
|
|
95
|
+
|
|
96
|
+
while (!closed) {
|
|
97
|
+
const { done, value } = await reader.read()
|
|
98
|
+
if (done) break
|
|
99
|
+
|
|
100
|
+
buffer += decoder.decode(value, { stream: true })
|
|
101
|
+
const lines = buffer.split('\n')
|
|
102
|
+
buffer = lines.pop() ?? ''
|
|
103
|
+
|
|
104
|
+
for (const line of lines) {
|
|
105
|
+
const parsed = parseSsePayload(line)
|
|
106
|
+
if (parsed !== null) {
|
|
107
|
+
enqueue(parsed)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
} catch {
|
|
112
|
+
if (closed) {
|
|
113
|
+
break
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (closed) {
|
|
118
|
+
break
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
attempts += 1
|
|
122
|
+
const backoff = Math.min(5000, 250 * 2 ** (attempts - 1))
|
|
123
|
+
await sleep(backoff)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
void run()
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
[Symbol.asyncIterator]() {
|
|
131
|
+
return this
|
|
132
|
+
},
|
|
133
|
+
next() {
|
|
134
|
+
if (queue.length > 0) {
|
|
135
|
+
return Promise.resolve({ done: false, value: queue.shift() })
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (closed) {
|
|
139
|
+
return Promise.resolve({ done: true, value: undefined })
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return new Promise((resolve) => {
|
|
143
|
+
waiters.push(resolve)
|
|
144
|
+
})
|
|
145
|
+
},
|
|
146
|
+
return() {
|
|
147
|
+
close()
|
|
148
|
+
return Promise.resolve({ done: true, value: undefined })
|
|
149
|
+
},
|
|
150
|
+
throw(error?: unknown) {
|
|
151
|
+
close()
|
|
152
|
+
return Promise.reject(error)
|
|
153
|
+
},
|
|
154
|
+
close
|
|
155
|
+
}
|
|
156
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"outDir": "./dist",
|
|
4
|
+
"rootDir": "./src",
|
|
5
|
+
"module": "commonjs",
|
|
6
|
+
"target": "ES2020",
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"declarationMap": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"moduleResolution": "node",
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"baseUrl": ".",
|
|
14
|
+
"paths": {
|
|
15
|
+
"*": [
|
|
16
|
+
"../../node_modules/*"
|
|
17
|
+
]
|
|
18
|
+
},
|
|
19
|
+
"lib": [
|
|
20
|
+
"ES2021",
|
|
21
|
+
"DOM"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"include": [
|
|
25
|
+
"src/**/*"
|
|
26
|
+
],
|
|
27
|
+
"exclude": [
|
|
28
|
+
"node_modules",
|
|
29
|
+
"**/*.test.ts",
|
|
30
|
+
"**/*.spec.ts",
|
|
31
|
+
"jest.config.ts",
|
|
32
|
+
"dist"
|
|
33
|
+
]
|
|
34
|
+
}
|