@flowerforce/flowerbase-client 0.1.1-beta.2 → 0.1.1-beta.3
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 +7 -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 +23 -4
- package/dist/user.d.ts.map +1 -1
- package/dist/user.js +96 -7
- 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 +213 -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 +16 -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 +115 -11
package/dist/user.js
CHANGED
|
@@ -5,26 +5,115 @@ const functions_1 = require("./functions");
|
|
|
5
5
|
const mongo_1 = require("./mongo");
|
|
6
6
|
class User {
|
|
7
7
|
constructor(app, id) {
|
|
8
|
+
this._providerType = null;
|
|
9
|
+
this.listeners = new Set();
|
|
8
10
|
this.app = app;
|
|
9
11
|
this.id = id;
|
|
10
|
-
this.functions = (0, functions_1.createFunctionsProxy)((name, args) => this.app.callFunction(name, args));
|
|
12
|
+
this.functions = (0, functions_1.createFunctionsProxy)((name, args) => this.app.callFunction(name, args, this.id), (name, args) => this.app.callFunctionStreaming(name, args, this.id));
|
|
13
|
+
}
|
|
14
|
+
get state() {
|
|
15
|
+
if (!this.app.hasUser(this.id)) {
|
|
16
|
+
return 'removed';
|
|
17
|
+
}
|
|
18
|
+
return this.isLoggedIn ? 'active' : 'logged-out';
|
|
19
|
+
}
|
|
20
|
+
get isLoggedIn() {
|
|
21
|
+
return this.accessToken !== null && this.refreshToken !== null;
|
|
22
|
+
}
|
|
23
|
+
get providerType() {
|
|
24
|
+
return this._providerType;
|
|
25
|
+
}
|
|
26
|
+
get identities() {
|
|
27
|
+
return this.app.getProfileSnapshot(this.id)?.identities ?? [];
|
|
28
|
+
}
|
|
29
|
+
get customData() {
|
|
30
|
+
const payload = this.decodeAccessTokenPayload();
|
|
31
|
+
if (!payload)
|
|
32
|
+
return {};
|
|
33
|
+
const fromUserData = 'user_data' in payload && payload.user_data && typeof payload.user_data === 'object'
|
|
34
|
+
? payload.user_data
|
|
35
|
+
: 'userData' in payload && payload.userData && typeof payload.userData === 'object'
|
|
36
|
+
? payload.userData
|
|
37
|
+
: 'custom_data' in payload && payload.custom_data && typeof payload.custom_data === 'object'
|
|
38
|
+
? payload.custom_data
|
|
39
|
+
: {};
|
|
40
|
+
return fromUserData;
|
|
41
|
+
}
|
|
42
|
+
get accessToken() {
|
|
43
|
+
const session = this.app.getSession(this.id);
|
|
44
|
+
if (!session)
|
|
45
|
+
return null;
|
|
46
|
+
return session.accessToken;
|
|
47
|
+
}
|
|
48
|
+
get refreshToken() {
|
|
49
|
+
const session = this.app.getSession(this.id);
|
|
50
|
+
if (!session)
|
|
51
|
+
return null;
|
|
52
|
+
return session.refreshToken;
|
|
53
|
+
}
|
|
54
|
+
setProviderType(providerType) {
|
|
55
|
+
this._providerType = providerType;
|
|
56
|
+
}
|
|
57
|
+
decodeAccessTokenPayload() {
|
|
58
|
+
if (!this.accessToken)
|
|
59
|
+
return null;
|
|
60
|
+
const parts = this.accessToken.split('.');
|
|
61
|
+
if (parts.length < 2)
|
|
62
|
+
return null;
|
|
63
|
+
const base64Url = parts[1];
|
|
64
|
+
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/').padEnd(Math.ceil(base64Url.length / 4) * 4, '=');
|
|
65
|
+
const decodeBase64 = (input) => {
|
|
66
|
+
if (typeof atob === 'function')
|
|
67
|
+
return atob(input);
|
|
68
|
+
const runtimeBuffer = globalThis.Buffer;
|
|
69
|
+
if (runtimeBuffer)
|
|
70
|
+
return runtimeBuffer.from(input, 'base64').toString('utf8');
|
|
71
|
+
return '';
|
|
72
|
+
};
|
|
73
|
+
try {
|
|
74
|
+
const decoded = decodeBase64(base64);
|
|
75
|
+
return JSON.parse(decoded);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
11
80
|
}
|
|
12
81
|
async logOut() {
|
|
13
|
-
await this.app.logoutUser();
|
|
82
|
+
await this.app.logoutUser(this.id);
|
|
83
|
+
}
|
|
84
|
+
async callFunction(name, ...args) {
|
|
85
|
+
return this.app.callFunction(name, args, this.id);
|
|
14
86
|
}
|
|
15
87
|
async refreshAccessToken() {
|
|
16
|
-
return this.app.refreshAccessToken();
|
|
88
|
+
return this.app.refreshAccessToken(this.id);
|
|
17
89
|
}
|
|
18
90
|
async refreshCustomData() {
|
|
19
|
-
const profile = await this.app.getProfile();
|
|
91
|
+
const profile = await this.app.getProfile(this.id);
|
|
20
92
|
this.profile = profile.data;
|
|
93
|
+
this.notifyListeners();
|
|
21
94
|
return profile.custom_data || {};
|
|
22
95
|
}
|
|
23
96
|
mongoClient(serviceName) {
|
|
24
|
-
|
|
25
|
-
|
|
97
|
+
return (0, mongo_1.createMongoClient)(this.app, serviceName, this.id);
|
|
98
|
+
}
|
|
99
|
+
addListener(callback) {
|
|
100
|
+
this.listeners.add(callback);
|
|
101
|
+
}
|
|
102
|
+
removeListener(callback) {
|
|
103
|
+
this.listeners.delete(callback);
|
|
104
|
+
}
|
|
105
|
+
removeAllListeners() {
|
|
106
|
+
this.listeners.clear();
|
|
107
|
+
}
|
|
108
|
+
notifyListeners() {
|
|
109
|
+
for (const callback of Array.from(this.listeners)) {
|
|
110
|
+
try {
|
|
111
|
+
callback();
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Listener failures should not break user lifecycle operations.
|
|
115
|
+
}
|
|
26
116
|
}
|
|
27
|
-
return (0, mongo_1.createMongoClient)(this.app);
|
|
28
117
|
}
|
|
29
118
|
}
|
|
30
119
|
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.3",
|
|
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,207 @@ 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('supports functions.callFunctionStreaming', 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
|
+
})
|
|
129
|
+
.mockResolvedValueOnce({
|
|
130
|
+
ok: true,
|
|
131
|
+
body: streamFromChunks(['a', 'b'])
|
|
132
|
+
}) as unknown as typeof fetch
|
|
133
|
+
|
|
134
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
135
|
+
await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
136
|
+
|
|
137
|
+
const stream = await app.currentUser!.functions.callFunctionStreaming('streamData')
|
|
138
|
+
const received: string[] = []
|
|
139
|
+
for await (const chunk of stream) {
|
|
140
|
+
received.push(new TextDecoder().decode(chunk))
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
expect(received).toEqual(['a', 'b'])
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('executes user-bound functions with that user session', async () => {
|
|
147
|
+
global.fetch = jest
|
|
148
|
+
.fn()
|
|
149
|
+
.mockResolvedValueOnce({
|
|
150
|
+
ok: true,
|
|
151
|
+
text: async () => JSON.stringify({ access_token: 'login-a1', refresh_token: 'refresh-1', user_id: 'user-1' })
|
|
152
|
+
})
|
|
153
|
+
.mockResolvedValueOnce({
|
|
154
|
+
ok: true,
|
|
155
|
+
text: async () => JSON.stringify({ access_token: 'access-1' })
|
|
156
|
+
})
|
|
157
|
+
.mockResolvedValueOnce({
|
|
158
|
+
ok: true,
|
|
159
|
+
text: async () => JSON.stringify({ access_token: 'login-a2', refresh_token: 'refresh-2', user_id: 'user-2' })
|
|
160
|
+
})
|
|
161
|
+
.mockResolvedValueOnce({
|
|
162
|
+
ok: true,
|
|
163
|
+
text: async () => JSON.stringify({ access_token: 'access-2' })
|
|
164
|
+
})
|
|
165
|
+
.mockResolvedValue({
|
|
166
|
+
ok: true,
|
|
167
|
+
text: async () => JSON.stringify({ ok: true })
|
|
168
|
+
}) as unknown as typeof fetch
|
|
169
|
+
|
|
170
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
171
|
+
const user1 = await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
172
|
+
await app.logIn(Credentials.anonymous())
|
|
173
|
+
|
|
174
|
+
await user1.functions.sum(1, 2)
|
|
175
|
+
const request = (global.fetch as jest.Mock).mock.calls[4][1]
|
|
176
|
+
expect(request.headers.Authorization).toBe('Bearer access-1')
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('retries streaming call when initial request returns 401', async () => {
|
|
180
|
+
const streamFromChunks = (chunks: string[]) =>
|
|
181
|
+
new ReadableStream<Uint8Array>({
|
|
182
|
+
start(controller) {
|
|
183
|
+
for (const chunk of chunks) {
|
|
184
|
+
controller.enqueue(new TextEncoder().encode(chunk))
|
|
185
|
+
}
|
|
186
|
+
controller.close()
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
global.fetch = jest
|
|
191
|
+
.fn()
|
|
192
|
+
.mockResolvedValueOnce({
|
|
193
|
+
ok: true,
|
|
194
|
+
text: async () => JSON.stringify({ access_token: 'login-a1', refresh_token: 'refresh-1', user_id: 'user-1' })
|
|
195
|
+
})
|
|
196
|
+
.mockResolvedValueOnce({
|
|
197
|
+
ok: true,
|
|
198
|
+
text: async () => JSON.stringify({ access_token: 'access-1' })
|
|
199
|
+
})
|
|
200
|
+
.mockResolvedValueOnce({
|
|
201
|
+
ok: false,
|
|
202
|
+
status: 401,
|
|
203
|
+
statusText: 'Unauthorized',
|
|
204
|
+
text: async () => JSON.stringify({ error: 'Unauthorized', error_code: 'InvalidSession' })
|
|
205
|
+
})
|
|
206
|
+
.mockResolvedValueOnce({
|
|
207
|
+
ok: true,
|
|
208
|
+
text: async () => JSON.stringify({ access_token: 'access-2' })
|
|
209
|
+
})
|
|
210
|
+
.mockResolvedValueOnce({
|
|
211
|
+
ok: true,
|
|
212
|
+
body: streamFromChunks(['x', 'y'])
|
|
213
|
+
}) as unknown as typeof fetch
|
|
214
|
+
|
|
215
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
216
|
+
await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
217
|
+
|
|
218
|
+
const stream = await app.currentUser!.functions.callFunctionStreaming('streamData')
|
|
219
|
+
const received: string[] = []
|
|
220
|
+
for await (const chunk of stream) {
|
|
221
|
+
received.push(new TextDecoder().decode(chunk))
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
expect(received).toEqual(['x', 'y'])
|
|
225
|
+
expect((global.fetch as jest.Mock).mock.calls[3][0]).toBe('http://localhost:3000/api/client/v2.0/auth/session')
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('retries streaming call when stream fails with 401 during iteration', async () => {
|
|
229
|
+
const streamWithAuthError = () =>
|
|
230
|
+
new ReadableStream<Uint8Array>({
|
|
231
|
+
start(controller) {
|
|
232
|
+
controller.enqueue(new TextEncoder().encode('a'))
|
|
233
|
+
controller.error(
|
|
234
|
+
new FlowerbaseHttpError({
|
|
235
|
+
method: 'POST',
|
|
236
|
+
url: 'http://localhost:3000/api/client/v2.0/app/my-app/functions/call',
|
|
237
|
+
statusCode: 401,
|
|
238
|
+
statusText: 'Unauthorized',
|
|
239
|
+
error: 'Expired token'
|
|
240
|
+
})
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
const streamFromChunks = (chunks: string[]) =>
|
|
246
|
+
new ReadableStream<Uint8Array>({
|
|
247
|
+
start(controller) {
|
|
248
|
+
for (const chunk of chunks) {
|
|
249
|
+
controller.enqueue(new TextEncoder().encode(chunk))
|
|
250
|
+
}
|
|
251
|
+
controller.close()
|
|
252
|
+
}
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
global.fetch = jest
|
|
256
|
+
.fn()
|
|
257
|
+
.mockResolvedValueOnce({
|
|
258
|
+
ok: true,
|
|
259
|
+
text: async () => JSON.stringify({ access_token: 'login-a1', refresh_token: 'refresh-1', user_id: 'user-1' })
|
|
260
|
+
})
|
|
261
|
+
.mockResolvedValueOnce({
|
|
262
|
+
ok: true,
|
|
263
|
+
text: async () => JSON.stringify({ access_token: 'access-1' })
|
|
264
|
+
})
|
|
265
|
+
.mockResolvedValueOnce({
|
|
266
|
+
ok: true,
|
|
267
|
+
body: streamWithAuthError()
|
|
268
|
+
})
|
|
269
|
+
.mockResolvedValueOnce({
|
|
270
|
+
ok: true,
|
|
271
|
+
text: async () => JSON.stringify({ access_token: 'access-2' })
|
|
272
|
+
})
|
|
273
|
+
.mockResolvedValueOnce({
|
|
274
|
+
ok: true,
|
|
275
|
+
body: streamFromChunks(['b'])
|
|
276
|
+
}) as unknown as typeof fetch
|
|
277
|
+
|
|
278
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
279
|
+
await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
280
|
+
|
|
281
|
+
const stream = await app.currentUser!.functions.callFunctionStreaming('streamData')
|
|
282
|
+
const received: string[] = []
|
|
283
|
+
for await (const chunk of stream) {
|
|
284
|
+
received.push(new TextDecoder().decode(chunk))
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
expect(received).toContain('b')
|
|
288
|
+
})
|
|
76
289
|
})
|
|
@@ -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
|
})
|