@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/dist/user.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { App } from './app';
|
|
2
|
+
import { MongoClientLike, UserLike } from './types';
|
|
3
|
+
export declare class User implements UserLike {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
customData: Record<string, unknown>;
|
|
6
|
+
profile?: {
|
|
7
|
+
email?: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
};
|
|
10
|
+
private readonly app;
|
|
11
|
+
private _providerType;
|
|
12
|
+
private readonly listeners;
|
|
13
|
+
functions: Record<string, (...args: unknown[]) => Promise<unknown>> & {
|
|
14
|
+
callFunction: (name: string, ...args: unknown[]) => Promise<unknown>;
|
|
15
|
+
callFunctionStreaming: (name: string, ...args: unknown[]) => Promise<AsyncIterable<Uint8Array>>;
|
|
16
|
+
};
|
|
17
|
+
constructor(app: App, id: string);
|
|
18
|
+
get state(): "active" | "logged-out" | "removed";
|
|
19
|
+
get isLoggedIn(): boolean;
|
|
20
|
+
get providerType(): string | null;
|
|
21
|
+
get identities(): unknown[];
|
|
22
|
+
private resolveCustomDataFromToken;
|
|
23
|
+
get accessToken(): string | null;
|
|
24
|
+
get refreshToken(): string | null;
|
|
25
|
+
setProviderType(providerType: string): void;
|
|
26
|
+
private decodeAccessTokenPayload;
|
|
27
|
+
logOut(): Promise<void>;
|
|
28
|
+
callFunction(name: string, ...args: unknown[]): Promise<any>;
|
|
29
|
+
refreshAccessToken(): Promise<string>;
|
|
30
|
+
refreshCustomData(): Promise<Record<string, unknown>>;
|
|
31
|
+
mongoClient(serviceName: string): MongoClientLike;
|
|
32
|
+
addListener(callback: () => void): void;
|
|
33
|
+
removeListener(callback: () => void): void;
|
|
34
|
+
removeAllListeners(): void;
|
|
35
|
+
notifyListeners(): void;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=user.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../src/user.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,OAAO,CAAA;AAGhC,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAA;AAEnD,qBAAa,IAAK,YAAW,QAAQ;IACnC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAK;IACxC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAA,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;KAAE,CAAA;IACnD,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAK;IACzB,OAAO,CAAC,aAAa,CAAsB;IAC3C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwB;IAElD,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC,GAAG;QACpE,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;QACpE,qBAAqB,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC,CAAA;KAChG,CAAA;gBAEW,GAAG,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM;IAUhC,IAAI,KAAK,wCAKR;IAED,IAAI,UAAU,YAEb;IAED,IAAI,YAAY,kBAEf;IAED,IAAI,UAAU,cAEb;IAED,OAAO,CAAC,0BAA0B;IAclC,IAAI,WAAW,kBAId;IAED,IAAI,YAAY,kBAIf;IAED,eAAe,CAAC,YAAY,EAAE,MAAM;IAIpC,OAAO,CAAC,wBAAwB;IAuB1B,MAAM;IAIN,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE;IAI7C,kBAAkB;IAMlB,iBAAiB,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAU3D,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,eAAe;IAIjD,WAAW,CAAC,QAAQ,EAAE,MAAM,IAAI;IAIhC,cAAc,CAAC,QAAQ,EAAE,MAAM,IAAI;IAInC,kBAAkB;IAIlB,eAAe;CAShB"}
|
package/dist/user.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.User = void 0;
|
|
4
|
+
const functions_1 = require("./functions");
|
|
5
|
+
const mongo_1 = require("./mongo");
|
|
6
|
+
class User {
|
|
7
|
+
constructor(app, id) {
|
|
8
|
+
this.customData = {};
|
|
9
|
+
this._providerType = null;
|
|
10
|
+
this.listeners = new Set();
|
|
11
|
+
this.app = app;
|
|
12
|
+
this.id = id;
|
|
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
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async logOut() {
|
|
83
|
+
await this.app.logoutUser(this.id);
|
|
84
|
+
}
|
|
85
|
+
async callFunction(name, ...args) {
|
|
86
|
+
return this.app.callFunction(name, args, this.id);
|
|
87
|
+
}
|
|
88
|
+
async refreshAccessToken() {
|
|
89
|
+
const accessToken = await this.app.refreshAccessToken(this.id);
|
|
90
|
+
this.customData = this.resolveCustomDataFromToken();
|
|
91
|
+
return accessToken;
|
|
92
|
+
}
|
|
93
|
+
async refreshCustomData() {
|
|
94
|
+
const profile = await this.app.getProfile(this.id);
|
|
95
|
+
this.profile = profile.data;
|
|
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;
|
|
101
|
+
}
|
|
102
|
+
mongoClient(serviceName) {
|
|
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
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
exports.User = User;
|
package/dist/watch.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watch.d.ts","sourceRoot":"","sources":["../src/watch.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAyCzD,eAAO,MAAM,mBAAmB,WAAY,WAAW,KAAG,mBAAmB,OAAO,CAiHnF,CAAA"}
|
package/dist/watch.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createWatchIterator = void 0;
|
|
4
|
+
const bson_1 = require("./bson");
|
|
5
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
6
|
+
const createWatchRequest = ({ database, collection, filter, ids }) => ({
|
|
7
|
+
name: 'watch',
|
|
8
|
+
service: 'mongodb-atlas',
|
|
9
|
+
arguments: [
|
|
10
|
+
{
|
|
11
|
+
database,
|
|
12
|
+
collection,
|
|
13
|
+
...(filter ? { filter } : {}),
|
|
14
|
+
...(ids ? { ids } : {})
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
});
|
|
18
|
+
const toBase64 = (input) => {
|
|
19
|
+
if (typeof btoa === 'function') {
|
|
20
|
+
return btoa(input);
|
|
21
|
+
}
|
|
22
|
+
throw new Error('Base64 encoder not available in current runtime');
|
|
23
|
+
};
|
|
24
|
+
const parseSsePayload = (line) => {
|
|
25
|
+
if (!line.startsWith('data:'))
|
|
26
|
+
return null;
|
|
27
|
+
const raw = line.slice(5).trim();
|
|
28
|
+
if (!raw)
|
|
29
|
+
return null;
|
|
30
|
+
try {
|
|
31
|
+
return bson_1.EJSON.deserialize(JSON.parse(raw));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return raw;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const createWatchIterator = (config) => {
|
|
38
|
+
let closed = false;
|
|
39
|
+
let activeController = null;
|
|
40
|
+
const queue = [];
|
|
41
|
+
const waiters = [];
|
|
42
|
+
const enqueue = (value) => {
|
|
43
|
+
const waiter = waiters.shift();
|
|
44
|
+
if (waiter) {
|
|
45
|
+
waiter({ done: false, value });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
queue.push(value);
|
|
49
|
+
};
|
|
50
|
+
const close = () => {
|
|
51
|
+
if (closed)
|
|
52
|
+
return;
|
|
53
|
+
closed = true;
|
|
54
|
+
activeController?.abort();
|
|
55
|
+
while (waiters.length > 0) {
|
|
56
|
+
const resolve = waiters.shift();
|
|
57
|
+
resolve?.({ done: true, value: undefined });
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const run = async () => {
|
|
61
|
+
let attempts = 0;
|
|
62
|
+
while (!closed) {
|
|
63
|
+
const controller = new AbortController();
|
|
64
|
+
activeController = controller;
|
|
65
|
+
const request = createWatchRequest(config);
|
|
66
|
+
const encoded = toBase64(JSON.stringify(bson_1.EJSON.serialize(request, { relaxed: false })));
|
|
67
|
+
const url = `${config.baseUrl}/api/client/v2.0/app/${config.appId}/functions/call?baas_request=${encodeURIComponent(encoded)}`;
|
|
68
|
+
try {
|
|
69
|
+
const response = await fetch(url, {
|
|
70
|
+
method: 'GET',
|
|
71
|
+
headers: {
|
|
72
|
+
Authorization: `Bearer ${config.accessToken}`,
|
|
73
|
+
Accept: 'text/event-stream'
|
|
74
|
+
},
|
|
75
|
+
signal: controller.signal
|
|
76
|
+
});
|
|
77
|
+
if (!response.ok || !response.body) {
|
|
78
|
+
throw new Error(`Watch request failed (${response.status})`);
|
|
79
|
+
}
|
|
80
|
+
attempts = 0;
|
|
81
|
+
const reader = response.body.getReader();
|
|
82
|
+
const decoder = new TextDecoder();
|
|
83
|
+
let buffer = '';
|
|
84
|
+
while (!closed) {
|
|
85
|
+
const { done, value } = await reader.read();
|
|
86
|
+
if (done)
|
|
87
|
+
break;
|
|
88
|
+
buffer += decoder.decode(value, { stream: true });
|
|
89
|
+
const lines = buffer.split('\n');
|
|
90
|
+
buffer = lines.pop() ?? '';
|
|
91
|
+
for (const line of lines) {
|
|
92
|
+
const parsed = parseSsePayload(line);
|
|
93
|
+
if (parsed !== null) {
|
|
94
|
+
enqueue(parsed);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
if (closed) {
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (closed) {
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
attempts += 1;
|
|
108
|
+
const backoff = Math.min(5000, 250 * 2 ** (attempts - 1));
|
|
109
|
+
await sleep(backoff);
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
void run();
|
|
113
|
+
return {
|
|
114
|
+
[Symbol.asyncIterator]() {
|
|
115
|
+
return this;
|
|
116
|
+
},
|
|
117
|
+
next() {
|
|
118
|
+
if (queue.length > 0) {
|
|
119
|
+
return Promise.resolve({ done: false, value: queue.shift() });
|
|
120
|
+
}
|
|
121
|
+
if (closed) {
|
|
122
|
+
return Promise.resolve({ done: true, value: undefined });
|
|
123
|
+
}
|
|
124
|
+
return new Promise((resolve) => {
|
|
125
|
+
waiters.push(resolve);
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
return() {
|
|
129
|
+
close();
|
|
130
|
+
return Promise.resolve({ done: true, value: undefined });
|
|
131
|
+
},
|
|
132
|
+
throw(error) {
|
|
133
|
+
close();
|
|
134
|
+
return Promise.reject(error);
|
|
135
|
+
},
|
|
136
|
+
close
|
|
137
|
+
};
|
|
138
|
+
};
|
|
139
|
+
exports.createWatchIterator = createWatchIterator;
|
package/jest.config.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flowerforce/flowerbase-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Client for Flowerbase",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"keywords": [],
|
|
8
|
+
"author": "",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/flowerforce/flowerbase.git"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"default": "./dist/index.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"test": "npx jest",
|
|
25
|
+
"build": "rm -rf dist/ && tsc"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
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"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/project.json
ADDED
package/rollup.config.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const { withNx } = require('@nx/rollup/with-nx')
|
|
2
|
+
|
|
3
|
+
module.exports = withNx(
|
|
4
|
+
{
|
|
5
|
+
main: './src/index.ts',
|
|
6
|
+
outputPath: './dist',
|
|
7
|
+
tsConfig: './tsconfig.lib.json',
|
|
8
|
+
compiler: 'tsc',
|
|
9
|
+
format: ['cjs', 'esm'],
|
|
10
|
+
generateExportsField: true
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
// Provide additional rollup configuration here. See: https://rollupjs.org/configuration-options
|
|
14
|
+
// e.g.
|
|
15
|
+
// output: { sourcemap: true },
|
|
16
|
+
}
|
|
17
|
+
)
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { App } from '../app'
|
|
2
|
+
import { Credentials } from '../credentials'
|
|
3
|
+
|
|
4
|
+
describe('flowerbase-client auth', () => {
|
|
5
|
+
const originalFetch = global.fetch
|
|
6
|
+
const originalLocalStorage = (globalThis as typeof globalThis & { localStorage?: unknown }).localStorage
|
|
7
|
+
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
global.fetch = originalFetch
|
|
10
|
+
if (typeof originalLocalStorage === 'undefined') {
|
|
11
|
+
Reflect.deleteProperty(globalThis, 'localStorage')
|
|
12
|
+
} else {
|
|
13
|
+
Object.defineProperty(globalThis, 'localStorage', {
|
|
14
|
+
configurable: true,
|
|
15
|
+
value: originalLocalStorage
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('logs in with email/password', async () => {
|
|
21
|
+
global.fetch = jest
|
|
22
|
+
.fn()
|
|
23
|
+
.mockResolvedValueOnce({
|
|
24
|
+
ok: true,
|
|
25
|
+
text: async () => JSON.stringify({
|
|
26
|
+
access_token: 'access',
|
|
27
|
+
refresh_token: 'refresh',
|
|
28
|
+
user_id: 'user-1'
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
.mockResolvedValueOnce({
|
|
32
|
+
ok: true,
|
|
33
|
+
text: async () => JSON.stringify({ access_token: 'access-from-session' })
|
|
34
|
+
}) as unknown as typeof fetch
|
|
35
|
+
|
|
36
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
37
|
+
const user = await app.logIn(Credentials.emailPassword('john@doe.com', 'secret123'))
|
|
38
|
+
|
|
39
|
+
expect(user.id).toBe('user-1')
|
|
40
|
+
expect(app.currentUser?.id).toBe('user-1')
|
|
41
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
42
|
+
'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/local-userpass/login',
|
|
43
|
+
expect.objectContaining({ method: 'POST' })
|
|
44
|
+
)
|
|
45
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
46
|
+
'http://localhost:3000/api/client/v2.0/auth/session',
|
|
47
|
+
expect.objectContaining({
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: expect.objectContaining({ Authorization: 'Bearer refresh' })
|
|
50
|
+
})
|
|
51
|
+
)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('logs in with anonymous provider', async () => {
|
|
55
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
56
|
+
ok: true,
|
|
57
|
+
text: async () => JSON.stringify({
|
|
58
|
+
access_token: 'access',
|
|
59
|
+
refresh_token: 'refresh',
|
|
60
|
+
user_id: 'anon-1'
|
|
61
|
+
})
|
|
62
|
+
}) as unknown as typeof fetch
|
|
63
|
+
|
|
64
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
65
|
+
const user = await app.logIn(Credentials.anonymous())
|
|
66
|
+
|
|
67
|
+
expect(user.id).toBe('anon-1')
|
|
68
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
69
|
+
'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/anon-user/login',
|
|
70
|
+
expect.objectContaining({ method: 'POST' })
|
|
71
|
+
)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('logs in with custom function provider', async () => {
|
|
75
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
76
|
+
ok: true,
|
|
77
|
+
text: async () => JSON.stringify({
|
|
78
|
+
access_token: 'access',
|
|
79
|
+
refresh_token: 'refresh',
|
|
80
|
+
user_id: 'custom-1'
|
|
81
|
+
})
|
|
82
|
+
}) as unknown as typeof fetch
|
|
83
|
+
|
|
84
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
85
|
+
const user = await app.logIn(Credentials.function({ token: 'abc' }))
|
|
86
|
+
|
|
87
|
+
expect(user.id).toBe('custom-1')
|
|
88
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
89
|
+
'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/custom-function/login',
|
|
90
|
+
expect.objectContaining({ method: 'POST' })
|
|
91
|
+
)
|
|
92
|
+
})
|
|
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
|
+
|
|
114
|
+
it('supports register and reset endpoints', async () => {
|
|
115
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
116
|
+
ok: true,
|
|
117
|
+
text: async () => JSON.stringify({ status: 'ok' })
|
|
118
|
+
}) as unknown as typeof fetch
|
|
119
|
+
|
|
120
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
121
|
+
|
|
122
|
+
await app.emailPasswordAuth.registerUser({ email: 'john@doe.com', password: 'secret123' })
|
|
123
|
+
await app.emailPasswordAuth.sendResetPasswordEmail('john@doe.com')
|
|
124
|
+
await app.emailPasswordAuth.callResetPasswordFunction('john@doe.com', 'new-secret', 'extra')
|
|
125
|
+
await app.emailPasswordAuth.resetPassword({ token: 't1', tokenId: 't2', password: 'new-secret' })
|
|
126
|
+
|
|
127
|
+
expect(global.fetch).toHaveBeenNthCalledWith(
|
|
128
|
+
1,
|
|
129
|
+
'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/local-userpass/register',
|
|
130
|
+
expect.objectContaining({ method: 'POST' })
|
|
131
|
+
)
|
|
132
|
+
expect(global.fetch).toHaveBeenNthCalledWith(
|
|
133
|
+
2,
|
|
134
|
+
'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/local-userpass/reset/send',
|
|
135
|
+
expect.objectContaining({ method: 'POST' })
|
|
136
|
+
)
|
|
137
|
+
expect(global.fetch).toHaveBeenNthCalledWith(
|
|
138
|
+
3,
|
|
139
|
+
'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/local-userpass/reset/call',
|
|
140
|
+
expect.objectContaining({ method: 'POST' })
|
|
141
|
+
)
|
|
142
|
+
expect(global.fetch).toHaveBeenNthCalledWith(
|
|
143
|
+
4,
|
|
144
|
+
'http://localhost:3000/api/client/v2.0/app/my-app/auth/providers/local-userpass/reset',
|
|
145
|
+
expect.objectContaining({ method: 'POST' })
|
|
146
|
+
)
|
|
147
|
+
})
|
|
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
|
+
|
|
178
|
+
it('calls session endpoint on app load when browser session exists', async () => {
|
|
179
|
+
const storage = new Map<string, string>()
|
|
180
|
+
storage.set(
|
|
181
|
+
'flowerbase:my-app:session',
|
|
182
|
+
JSON.stringify({ accessToken: 'old-access', refreshToken: 'refresh', userId: 'user-1' })
|
|
183
|
+
)
|
|
184
|
+
Object.defineProperty(globalThis, 'localStorage', {
|
|
185
|
+
configurable: true,
|
|
186
|
+
value: {
|
|
187
|
+
getItem: (key: string) => storage.get(key) ?? null,
|
|
188
|
+
setItem: (key: string, value: string) => {
|
|
189
|
+
storage.set(key, value)
|
|
190
|
+
},
|
|
191
|
+
removeItem: (key: string) => {
|
|
192
|
+
storage.delete(key)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
global.fetch = jest.fn().mockResolvedValue({
|
|
198
|
+
ok: true,
|
|
199
|
+
text: async () => JSON.stringify({ access_token: 'fresh-access' })
|
|
200
|
+
}) as unknown as typeof fetch
|
|
201
|
+
|
|
202
|
+
const app = new App({ id: 'my-app', baseUrl: 'http://localhost:3000' })
|
|
203
|
+
await app.getProfile().catch(() => undefined)
|
|
204
|
+
|
|
205
|
+
expect(global.fetch).toHaveBeenCalledWith(
|
|
206
|
+
'http://localhost:3000/api/client/v2.0/auth/session',
|
|
207
|
+
expect.objectContaining({
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: expect.objectContaining({ Authorization: 'Bearer refresh' })
|
|
210
|
+
})
|
|
211
|
+
)
|
|
212
|
+
})
|
|
213
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as Flowerbase from '../index'
|
|
2
|
+
|
|
3
|
+
describe('flowerbase-client compatibility surface', () => {
|
|
4
|
+
it('exposes Realm-like symbols', () => {
|
|
5
|
+
expect(typeof Flowerbase.App).toBe('function')
|
|
6
|
+
expect(typeof Flowerbase.App.getApp).toBe('function')
|
|
7
|
+
expect(Flowerbase.App.Credentials).toBe(Flowerbase.Credentials)
|
|
8
|
+
expect(typeof Flowerbase.Credentials.emailPassword).toBe('function')
|
|
9
|
+
expect(typeof Flowerbase.Credentials.anonymous).toBe('function')
|
|
10
|
+
expect(typeof Flowerbase.Credentials.function).toBe('function')
|
|
11
|
+
expect(typeof Flowerbase.Credentials.jwt).toBe('function')
|
|
12
|
+
expect(typeof Flowerbase.MongoDBRealmError).toBe('function')
|
|
13
|
+
expect(typeof Flowerbase.BSON.ObjectId).toBe('function')
|
|
14
|
+
expect(Flowerbase.ObjectID).toBe(Flowerbase.ObjectId)
|
|
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
|
+
})
|
|
22
|
+
})
|