@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/dist/user.js
CHANGED
|
@@ -5,26 +5,121 @@ const functions_1 = require("./functions");
|
|
|
5
5
|
const mongo_1 = require("./mongo");
|
|
6
6
|
class User {
|
|
7
7
|
constructor(app, id) {
|
|
8
|
+
this.customData = {};
|
|
9
|
+
this._providerType = null;
|
|
10
|
+
this.listeners = new Set();
|
|
8
11
|
this.app = app;
|
|
9
12
|
this.id = id;
|
|
10
|
-
this.functions = (0, functions_1.createFunctionsProxy)((name, args) => this.app.callFunction(name, args));
|
|
13
|
+
this.functions = (0, functions_1.createFunctionsProxy)((name, args) => this.app.callFunction(name, args, this.id), (name, args) => this.app.callFunctionStreaming(name, args, this.id));
|
|
14
|
+
this.customData = this.resolveCustomDataFromToken();
|
|
15
|
+
}
|
|
16
|
+
get state() {
|
|
17
|
+
if (!this.app.hasUser(this.id)) {
|
|
18
|
+
return 'removed';
|
|
19
|
+
}
|
|
20
|
+
return this.isLoggedIn ? 'active' : 'logged-out';
|
|
21
|
+
}
|
|
22
|
+
get isLoggedIn() {
|
|
23
|
+
return this.accessToken !== null && this.refreshToken !== null;
|
|
24
|
+
}
|
|
25
|
+
get providerType() {
|
|
26
|
+
return this._providerType;
|
|
27
|
+
}
|
|
28
|
+
get identities() {
|
|
29
|
+
return this.app.getProfileSnapshot(this.id)?.identities ?? [];
|
|
30
|
+
}
|
|
31
|
+
resolveCustomDataFromToken() {
|
|
32
|
+
const payload = this.decodeAccessTokenPayload();
|
|
33
|
+
if (!payload)
|
|
34
|
+
return {};
|
|
35
|
+
return ('user_data' in payload && payload.user_data && typeof payload.user_data === 'object'
|
|
36
|
+
? payload.user_data
|
|
37
|
+
: 'userData' in payload && payload.userData && typeof payload.userData === 'object'
|
|
38
|
+
? payload.userData
|
|
39
|
+
: 'custom_data' in payload && payload.custom_data && typeof payload.custom_data === 'object'
|
|
40
|
+
? payload.custom_data
|
|
41
|
+
: {});
|
|
42
|
+
}
|
|
43
|
+
get accessToken() {
|
|
44
|
+
const session = this.app.getSession(this.id);
|
|
45
|
+
if (!session)
|
|
46
|
+
return null;
|
|
47
|
+
return session.accessToken;
|
|
48
|
+
}
|
|
49
|
+
get refreshToken() {
|
|
50
|
+
const session = this.app.getSession(this.id);
|
|
51
|
+
if (!session)
|
|
52
|
+
return null;
|
|
53
|
+
return session.refreshToken;
|
|
54
|
+
}
|
|
55
|
+
setProviderType(providerType) {
|
|
56
|
+
this._providerType = providerType;
|
|
57
|
+
}
|
|
58
|
+
decodeAccessTokenPayload() {
|
|
59
|
+
if (!this.accessToken)
|
|
60
|
+
return null;
|
|
61
|
+
const parts = this.accessToken.split('.');
|
|
62
|
+
if (parts.length < 2)
|
|
63
|
+
return null;
|
|
64
|
+
const base64Url = parts[1];
|
|
65
|
+
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(base64Url.length / 4) * 4, '=');
|
|
66
|
+
const decodeBase64 = (input) => {
|
|
67
|
+
if (typeof atob === 'function')
|
|
68
|
+
return atob(input);
|
|
69
|
+
const runtimeBuffer = globalThis.Buffer;
|
|
70
|
+
if (runtimeBuffer)
|
|
71
|
+
return runtimeBuffer.from(input, 'base64').toString('utf8');
|
|
72
|
+
return '';
|
|
73
|
+
};
|
|
74
|
+
try {
|
|
75
|
+
const decoded = decodeBase64(base64);
|
|
76
|
+
return JSON.parse(decoded);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
11
81
|
}
|
|
12
82
|
async logOut() {
|
|
13
|
-
await this.app.logoutUser();
|
|
83
|
+
await this.app.logoutUser(this.id);
|
|
84
|
+
}
|
|
85
|
+
async callFunction(name, ...args) {
|
|
86
|
+
return this.app.callFunction(name, args, this.id);
|
|
14
87
|
}
|
|
15
88
|
async refreshAccessToken() {
|
|
16
|
-
|
|
89
|
+
const accessToken = await this.app.refreshAccessToken(this.id);
|
|
90
|
+
this.customData = this.resolveCustomDataFromToken();
|
|
91
|
+
return accessToken;
|
|
17
92
|
}
|
|
18
93
|
async refreshCustomData() {
|
|
19
|
-
const profile = await this.app.getProfile();
|
|
94
|
+
const profile = await this.app.getProfile(this.id);
|
|
20
95
|
this.profile = profile.data;
|
|
21
|
-
|
|
96
|
+
this.customData = (profile.custom_data && typeof profile.custom_data === 'object'
|
|
97
|
+
? profile.custom_data
|
|
98
|
+
: this.resolveCustomDataFromToken());
|
|
99
|
+
this.notifyListeners();
|
|
100
|
+
return this.customData;
|
|
22
101
|
}
|
|
23
102
|
mongoClient(serviceName) {
|
|
24
|
-
|
|
25
|
-
|
|
103
|
+
return (0, mongo_1.createMongoClient)(this.app, serviceName, this.id);
|
|
104
|
+
}
|
|
105
|
+
addListener(callback) {
|
|
106
|
+
this.listeners.add(callback);
|
|
107
|
+
}
|
|
108
|
+
removeListener(callback) {
|
|
109
|
+
this.listeners.delete(callback);
|
|
110
|
+
}
|
|
111
|
+
removeAllListeners() {
|
|
112
|
+
this.listeners.clear();
|
|
113
|
+
}
|
|
114
|
+
notifyListeners() {
|
|
115
|
+
for (const callback of Array.from(this.listeners)) {
|
|
116
|
+
try {
|
|
117
|
+
callback();
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// Listener failures should not break user lifecycle operations.
|
|
121
|
+
}
|
|
26
122
|
}
|
|
27
|
-
return (0, mongo_1.createMongoClient)(this.app);
|
|
28
123
|
}
|
|
29
124
|
}
|
|
30
125
|
exports.User = User;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flowerforce/flowerbase-client",
|
|
3
|
-
"version": "0.1.1-beta.
|
|
3
|
+
"version": "0.1.1-beta.4",
|
|
4
4
|
"description": "Client for Flowerbase",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -26,5 +26,16 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"bson": "^6.10.4"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@react-native-async-storage/async-storage": "^2.2.0"
|
|
32
|
+
},
|
|
33
|
+
"peerDependenciesMeta": {
|
|
34
|
+
"@react-native-async-storage/async-storage": {
|
|
35
|
+
"optional": true
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@react-native-async-storage/async-storage": "^2.2.0"
|
|
29
40
|
}
|
|
30
41
|
}
|
|
@@ -91,6 +91,26 @@ describe('flowerbase-client auth', () => {
|
|
|
91
91
|
)
|
|
92
92
|
})
|
|
93
93
|
|
|
94
|
+
it('logs in with custom jwt provider', async () => {
|
|
95
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
96
|
+
ok: true,
|
|
97
|
+
text: async () => JSON.stringify({
|
|
98
|
+
access_token: 'access',
|
|
99
|
+
refresh_token: 'refresh',
|
|
100
|
+
user_id: 'jwt-1'
|
|
101
|
+
})
|
|
102
|
+
}) as unknown as typeof fetch
|
|
103
|
+
|
|
104
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
105
|
+
const user = await app.logIn(Credentials.jwt('jwt-token'))
|
|
106
|
+
|
|
107
|
+
expect(user.id).toBe('jwt-1')
|
|
108
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
109
|
+
'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/custom-token/login',
|
|
110
|
+
expect.objectContaining({ method: 'POST' })
|
|
111
|
+
)
|
|
112
|
+
})
|
|
113
|
+
|
|
94
114
|
it('supports register and reset endpoints', async () => {
|
|
95
115
|
global.fetch = jest.fn().mockResolvedValue({
|
|
96
116
|
ok: true,
|
|
@@ -126,6 +146,35 @@ describe('flowerbase-client auth', () => {
|
|
|
126
146
|
)
|
|
127
147
|
})
|
|
128
148
|
|
|
149
|
+
it('supports email/password confirmation endpoints', async () => {
|
|
150
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
151
|
+
ok: true,
|
|
152
|
+
text: async () => JSON.stringify({ status: 'ok' })
|
|
153
|
+
}) as unknown as typeof fetch
|
|
154
|
+
|
|
155
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
156
|
+
|
|
157
|
+
await app.emailPasswordAuth.confirmUser({ token: 't1', tokenId: 'tid1' })
|
|
158
|
+
await app.emailPasswordAuth.resendConfirmationEmail({ email: 'john@doe.com' })
|
|
159
|
+
await app.emailPasswordAuth.retryCustomConfirmation({ email: 'john@doe.com' })
|
|
160
|
+
|
|
161
|
+
expect(global.fetch).toHaveBeenNthCalledWith(
|
|
162
|
+
1,
|
|
163
|
+
'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/local-userpass/confirm',
|
|
164
|
+
expect.objectContaining({ method: 'POST' })
|
|
165
|
+
)
|
|
166
|
+
expect(global.fetch).toHaveBeenNthCalledWith(
|
|
167
|
+
2,
|
|
168
|
+
'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/local-userpass/confirm/send',
|
|
169
|
+
expect.objectContaining({ method: 'POST' })
|
|
170
|
+
)
|
|
171
|
+
expect(global.fetch).toHaveBeenNthCalledWith(
|
|
172
|
+
3,
|
|
173
|
+
'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/local-userpass/confirm/call',
|
|
174
|
+
expect.objectContaining({ method: 'POST' })
|
|
175
|
+
)
|
|
176
|
+
})
|
|
177
|
+
|
|
129
178
|
it('calls session endpoint on app load when browser session exists', async () => {
|
|
130
179
|
const storage = new Map<string, string>()
|
|
131
180
|
storage.set(
|
|
@@ -3,10 +3,20 @@ import * as Flowerbase from '../index'
|
|
|
3
3
|
describe('flowerbase-client compatibility surface', () => {
|
|
4
4
|
it('exposes Realm-like symbols', () => {
|
|
5
5
|
expect(typeof Flowerbase.App).toBe('function')
|
|
6
|
+
expect(typeof Flowerbase.App.getApp).toBe('function')
|
|
7
|
+
expect(Flowerbase.App.Credentials).toBe(Flowerbase.Credentials)
|
|
6
8
|
expect(typeof Flowerbase.Credentials.emailPassword).toBe('function')
|
|
7
9
|
expect(typeof Flowerbase.Credentials.anonymous).toBe('function')
|
|
8
10
|
expect(typeof Flowerbase.Credentials.function).toBe('function')
|
|
11
|
+
expect(typeof Flowerbase.Credentials.jwt).toBe('function')
|
|
12
|
+
expect(typeof Flowerbase.MongoDBRealmError).toBe('function')
|
|
9
13
|
expect(typeof Flowerbase.BSON.ObjectId).toBe('function')
|
|
10
14
|
expect(Flowerbase.ObjectID).toBe(Flowerbase.ObjectId)
|
|
11
15
|
})
|
|
16
|
+
|
|
17
|
+
it('returns singleton app for same id', () => {
|
|
18
|
+
const a1 = Flowerbase.App.getApp('singleton-app')
|
|
19
|
+
const a2 = Flowerbase.App.getApp('singleton-app')
|
|
20
|
+
expect(a1).toBe(a2)
|
|
21
|
+
})
|
|
12
22
|
})
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
import { App } from '../app'
|
|
2
2
|
import { Credentials } from '../credentials'
|
|
3
|
+
import { FlowerbaseHttpError } from '../http'
|
|
3
4
|
|
|
4
5
|
describe('flowerbase-client functions', () => {
|
|
5
6
|
const originalFetch = global.fetch
|
|
7
|
+
const streamFromChunks = (chunks: string[]) =>
|
|
8
|
+
new ReadableStream<Uint8Array>({
|
|
9
|
+
start(controller) {
|
|
10
|
+
for (const chunk of chunks) {
|
|
11
|
+
controller.enqueue(new TextEncoder().encode(chunk))
|
|
12
|
+
}
|
|
13
|
+
controller.close()
|
|
14
|
+
}
|
|
15
|
+
})
|
|
6
16
|
|
|
7
17
|
afterEach(() => {
|
|
8
18
|
global.fetch = originalFetch
|
|
@@ -73,4 +83,230 @@ describe('flowerbase-client functions', () => {
|
|
|
73
83
|
|
|
74
84
|
await expect(app.currentUser!.functions.explode()).rejects.toThrow('boom')
|
|
75
85
|
})
|
|
86
|
+
|
|
87
|
+
it('supports functions.callFunction compatibility helper', async () => {
|
|
88
|
+
global.fetch = jest
|
|
89
|
+
.fn()
|
|
90
|
+
.mockResolvedValueOnce({
|
|
91
|
+
ok: true,
|
|
92
|
+
text: async () => JSON.stringify({
|
|
93
|
+
access_token: 'access',
|
|
94
|
+
refresh_token: 'refresh',
|
|
95
|
+
user_id: 'user-1'
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
.mockResolvedValueOnce({
|
|
99
|
+
ok: true,
|
|
100
|
+
text: async () => JSON.stringify({ access_token: 'access' })
|
|
101
|
+
})
|
|
102
|
+
.mockResolvedValueOnce({
|
|
103
|
+
ok: true,
|
|
104
|
+
text: async () => JSON.stringify({ result: 7 })
|
|
105
|
+
}) as unknown as typeof fetch
|
|
106
|
+
|
|
107
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
108
|
+
await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
109
|
+
|
|
110
|
+
const result = await app.currentUser!.functions.callFunction('sum', 3, 4)
|
|
111
|
+
expect(result).toEqual({ result: 7 })
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('does not treat toJSON as a remote function name', async () => {
|
|
115
|
+
global.fetch = jest
|
|
116
|
+
.fn()
|
|
117
|
+
.mockResolvedValueOnce({
|
|
118
|
+
ok: true,
|
|
119
|
+
text: async () => JSON.stringify({
|
|
120
|
+
access_token: 'access',
|
|
121
|
+
refresh_token: 'refresh',
|
|
122
|
+
user_id: 'user-1'
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
.mockResolvedValueOnce({
|
|
126
|
+
ok: true,
|
|
127
|
+
text: async () => JSON.stringify({ access_token: 'access' })
|
|
128
|
+
}) as unknown as typeof fetch
|
|
129
|
+
|
|
130
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
131
|
+
await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
132
|
+
|
|
133
|
+
expect(JSON.stringify(app.currentUser!.functions)).toBe('{}')
|
|
134
|
+
expect(global.fetch).toHaveBeenCalledTimes(2)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('supports functions.callFunctionStreaming', async () => {
|
|
138
|
+
global.fetch = jest
|
|
139
|
+
.fn()
|
|
140
|
+
.mockResolvedValueOnce({
|
|
141
|
+
ok: true,
|
|
142
|
+
text: async () => JSON.stringify({
|
|
143
|
+
access_token: 'access',
|
|
144
|
+
refresh_token: 'refresh',
|
|
145
|
+
user_id: 'user-1'
|
|
146
|
+
})
|
|
147
|
+
})
|
|
148
|
+
.mockResolvedValueOnce({
|
|
149
|
+
ok: true,
|
|
150
|
+
text: async () => JSON.stringify({ access_token: 'access' })
|
|
151
|
+
})
|
|
152
|
+
.mockResolvedValueOnce({
|
|
153
|
+
ok: true,
|
|
154
|
+
body: streamFromChunks(['a', 'b'])
|
|
155
|
+
}) as unknown as typeof fetch
|
|
156
|
+
|
|
157
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
158
|
+
await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
159
|
+
|
|
160
|
+
const stream = await app.currentUser!.functions.callFunctionStreaming('streamData')
|
|
161
|
+
const received: string[] = []
|
|
162
|
+
for await (const chunk of stream) {
|
|
163
|
+
received.push(new TextDecoder().decode(chunk))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
expect(received).toEqual(['a', 'b'])
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('executes user-bound functions with that user session', async () => {
|
|
170
|
+
global.fetch = jest
|
|
171
|
+
.fn()
|
|
172
|
+
.mockResolvedValueOnce({
|
|
173
|
+
ok: true,
|
|
174
|
+
text: async () => JSON.stringify({ access_token: 'login-a1', refresh_token: 'refresh-1', user_id: 'user-1' })
|
|
175
|
+
})
|
|
176
|
+
.mockResolvedValueOnce({
|
|
177
|
+
ok: true,
|
|
178
|
+
text: async () => JSON.stringify({ access_token: 'access-1' })
|
|
179
|
+
})
|
|
180
|
+
.mockResolvedValueOnce({
|
|
181
|
+
ok: true,
|
|
182
|
+
text: async () => JSON.stringify({ access_token: 'login-a2', refresh_token: 'refresh-2', user_id: 'user-2' })
|
|
183
|
+
})
|
|
184
|
+
.mockResolvedValueOnce({
|
|
185
|
+
ok: true,
|
|
186
|
+
text: async () => JSON.stringify({ access_token: 'access-2' })
|
|
187
|
+
})
|
|
188
|
+
.mockResolvedValue({
|
|
189
|
+
ok: true,
|
|
190
|
+
text: async () => JSON.stringify({ ok: true })
|
|
191
|
+
}) as unknown as typeof fetch
|
|
192
|
+
|
|
193
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
194
|
+
const user1 = await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
195
|
+
await app.logIn(Credentials.anonymous())
|
|
196
|
+
|
|
197
|
+
await user1.functions.sum(1, 2)
|
|
198
|
+
const request = (global.fetch as jest.Mock).mock.calls[4][1]
|
|
199
|
+
expect(request.headers.Authorization).toBe('Bearer access-1')
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('retries streaming call when initial request returns 401', async () => {
|
|
203
|
+
const streamFromChunks = (chunks: string[]) =>
|
|
204
|
+
new ReadableStream<Uint8Array>({
|
|
205
|
+
start(controller) {
|
|
206
|
+
for (const chunk of chunks) {
|
|
207
|
+
controller.enqueue(new TextEncoder().encode(chunk))
|
|
208
|
+
}
|
|
209
|
+
controller.close()
|
|
210
|
+
}
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
global.fetch = jest
|
|
214
|
+
.fn()
|
|
215
|
+
.mockResolvedValueOnce({
|
|
216
|
+
ok: true,
|
|
217
|
+
text: async () => JSON.stringify({ access_token: 'login-a1', refresh_token: 'refresh-1', user_id: 'user-1' })
|
|
218
|
+
})
|
|
219
|
+
.mockResolvedValueOnce({
|
|
220
|
+
ok: true,
|
|
221
|
+
text: async () => JSON.stringify({ access_token: 'access-1' })
|
|
222
|
+
})
|
|
223
|
+
.mockResolvedValueOnce({
|
|
224
|
+
ok: false,
|
|
225
|
+
status: 401,
|
|
226
|
+
statusText: 'Unauthorized',
|
|
227
|
+
text: async () => JSON.stringify({ error: 'Unauthorized', error_code: 'InvalidSession' })
|
|
228
|
+
})
|
|
229
|
+
.mockResolvedValueOnce({
|
|
230
|
+
ok: true,
|
|
231
|
+
text: async () => JSON.stringify({ access_token: 'access-2' })
|
|
232
|
+
})
|
|
233
|
+
.mockResolvedValueOnce({
|
|
234
|
+
ok: true,
|
|
235
|
+
body: streamFromChunks(['x', 'y'])
|
|
236
|
+
}) as unknown as typeof fetch
|
|
237
|
+
|
|
238
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
239
|
+
await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
240
|
+
|
|
241
|
+
const stream = await app.currentUser!.functions.callFunctionStreaming('streamData')
|
|
242
|
+
const received: string[] = []
|
|
243
|
+
for await (const chunk of stream) {
|
|
244
|
+
received.push(new TextDecoder().decode(chunk))
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
expect(received).toEqual(['x', 'y'])
|
|
248
|
+
expect((global.fetch as jest.Mock).mock.calls[3][0]).toBe('http://localhost:3000/api/client/v2.0/auth/session')
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('retries streaming call when stream fails with 401 during iteration', async () => {
|
|
252
|
+
const streamWithAuthError = () =>
|
|
253
|
+
new ReadableStream<Uint8Array>({
|
|
254
|
+
start(controller) {
|
|
255
|
+
controller.enqueue(new TextEncoder().encode('a'))
|
|
256
|
+
controller.error(
|
|
257
|
+
new FlowerbaseHttpError({
|
|
258
|
+
method: 'POST',
|
|
259
|
+
url: 'http://localhost:3000/api/client/v2.0/app/my-app/functions/call',
|
|
260
|
+
statusCode: 401,
|
|
261
|
+
statusText: 'Unauthorized',
|
|
262
|
+
error: 'Expired token'
|
|
263
|
+
})
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
const streamFromChunks = (chunks: string[]) =>
|
|
269
|
+
new ReadableStream<Uint8Array>({
|
|
270
|
+
start(controller) {
|
|
271
|
+
for (const chunk of chunks) {
|
|
272
|
+
controller.enqueue(new TextEncoder().encode(chunk))
|
|
273
|
+
}
|
|
274
|
+
controller.close()
|
|
275
|
+
}
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
global.fetch = jest
|
|
279
|
+
.fn()
|
|
280
|
+
.mockResolvedValueOnce({
|
|
281
|
+
ok: true,
|
|
282
|
+
text: async () => JSON.stringify({ access_token: 'login-a1', refresh_token: 'refresh-1', user_id: 'user-1' })
|
|
283
|
+
})
|
|
284
|
+
.mockResolvedValueOnce({
|
|
285
|
+
ok: true,
|
|
286
|
+
text: async () => JSON.stringify({ access_token: 'access-1' })
|
|
287
|
+
})
|
|
288
|
+
.mockResolvedValueOnce({
|
|
289
|
+
ok: true,
|
|
290
|
+
body: streamWithAuthError()
|
|
291
|
+
})
|
|
292
|
+
.mockResolvedValueOnce({
|
|
293
|
+
ok: true,
|
|
294
|
+
text: async () => JSON.stringify({ access_token: 'access-2' })
|
|
295
|
+
})
|
|
296
|
+
.mockResolvedValueOnce({
|
|
297
|
+
ok: true,
|
|
298
|
+
body: streamFromChunks(['b'])
|
|
299
|
+
}) as unknown as typeof fetch
|
|
300
|
+
|
|
301
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
302
|
+
await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
303
|
+
|
|
304
|
+
const stream = await app.currentUser!.functions.callFunctionStreaming('streamData')
|
|
305
|
+
const received: string[] = []
|
|
306
|
+
for await (const chunk of stream) {
|
|
307
|
+
received.push(new TextDecoder().decode(chunk))
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
expect(received).toContain('b')
|
|
311
|
+
})
|
|
76
312
|
})
|
|
@@ -45,4 +45,39 @@ describe('flowerbase-client mongo service wrapper', () => {
|
|
|
45
45
|
expect(parsed.service).toBe('mongodb-atlas')
|
|
46
46
|
expect(parsed.name).toBe('findOne')
|
|
47
47
|
})
|
|
48
|
+
|
|
49
|
+
it('supports extended CRUD operations and custom service name', async () => {
|
|
50
|
+
global.fetch = jest
|
|
51
|
+
.fn()
|
|
52
|
+
.mockResolvedValueOnce({
|
|
53
|
+
ok: true,
|
|
54
|
+
text: async () => JSON.stringify({
|
|
55
|
+
access_token: 'access',
|
|
56
|
+
refresh_token: 'refresh',
|
|
57
|
+
user_id: 'user-1'
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
.mockResolvedValue({
|
|
61
|
+
ok: true,
|
|
62
|
+
text: async () => JSON.stringify({ ok: true })
|
|
63
|
+
}) as unknown as typeof fetch
|
|
64
|
+
|
|
65
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
66
|
+
await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
67
|
+
|
|
68
|
+
const collection = app.currentUser!.mongoClient('my-service').db('testdb').collection('todos')
|
|
69
|
+
|
|
70
|
+
await collection.findOneAndUpdate({ done: false }, { $set: { done: true } })
|
|
71
|
+
await collection.findOneAndReplace({ done: true }, { done: true, title: 'done' })
|
|
72
|
+
await collection.findOneAndDelete({ done: true })
|
|
73
|
+
await collection.aggregate([{ $match: { done: true } }])
|
|
74
|
+
await collection.count({ done: true })
|
|
75
|
+
await collection.insertMany([{ title: 'A' }, { title: 'B' }])
|
|
76
|
+
await collection.deleteMany({ done: true })
|
|
77
|
+
|
|
78
|
+
const calls = (global.fetch as jest.Mock).mock.calls
|
|
79
|
+
const lastBody = JSON.parse(calls[calls.length - 1][1].body)
|
|
80
|
+
expect(lastBody.service).toBe('my-service')
|
|
81
|
+
expect(lastBody.name).toBe('deleteMany')
|
|
82
|
+
})
|
|
48
83
|
})
|