@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.
Files changed (63) hide show
  1. package/CHANGELOG.md +0 -0
  2. package/LICENSE +3 -0
  3. package/README.md +209 -0
  4. package/dist/app.d.ts +85 -0
  5. package/dist/app.d.ts.map +1 -0
  6. package/dist/app.js +461 -0
  7. package/dist/bson.d.ts +8 -0
  8. package/dist/bson.d.ts.map +1 -0
  9. package/dist/bson.js +10 -0
  10. package/dist/credentials.d.ts +8 -0
  11. package/dist/credentials.d.ts.map +1 -0
  12. package/dist/credentials.js +30 -0
  13. package/dist/functions.d.ts +6 -0
  14. package/dist/functions.d.ts.map +1 -0
  15. package/dist/functions.js +47 -0
  16. package/dist/http.d.ts +35 -0
  17. package/dist/http.d.ts.map +1 -0
  18. package/dist/http.js +170 -0
  19. package/dist/index.d.ts +8 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +16 -0
  22. package/dist/mongo.d.ts +4 -0
  23. package/dist/mongo.d.ts.map +1 -0
  24. package/dist/mongo.js +106 -0
  25. package/dist/session.d.ts +18 -0
  26. package/dist/session.d.ts.map +1 -0
  27. package/dist/session.js +105 -0
  28. package/dist/session.native.d.ts +14 -0
  29. package/dist/session.native.d.ts.map +1 -0
  30. package/dist/session.native.js +76 -0
  31. package/dist/types.d.ts +97 -0
  32. package/dist/types.d.ts.map +1 -0
  33. package/dist/types.js +2 -0
  34. package/dist/user.d.ts +37 -0
  35. package/dist/user.d.ts.map +1 -0
  36. package/dist/user.js +125 -0
  37. package/dist/watch.d.ts +3 -0
  38. package/dist/watch.d.ts.map +1 -0
  39. package/dist/watch.js +139 -0
  40. package/jest.config.ts +13 -0
  41. package/package.json +41 -0
  42. package/project.json +11 -0
  43. package/rollup.config.js +17 -0
  44. package/src/__tests__/auth.test.ts +213 -0
  45. package/src/__tests__/compat.test.ts +22 -0
  46. package/src/__tests__/functions.test.ts +312 -0
  47. package/src/__tests__/mongo.test.ts +83 -0
  48. package/src/__tests__/session.test.ts +597 -0
  49. package/src/__tests__/watch.test.ts +336 -0
  50. package/src/app.ts +562 -0
  51. package/src/bson.ts +6 -0
  52. package/src/credentials.ts +31 -0
  53. package/src/functions.ts +56 -0
  54. package/src/http.ts +221 -0
  55. package/src/index.ts +15 -0
  56. package/src/mongo.ts +112 -0
  57. package/src/session.native.ts +89 -0
  58. package/src/session.ts +114 -0
  59. package/src/types.ts +114 -0
  60. package/src/user.ts +150 -0
  61. package/src/watch.ts +156 -0
  62. package/tsconfig.json +34 -0
  63. 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
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "types": [
5
+ "node",
6
+ "jest"
7
+ ]
8
+ },
9
+ "include": [
10
+ "src/**/*.test.ts",
11
+ "src/**/*.spec.ts"
12
+ ]
13
+ }