@urbackend/sdk 0.1.1 → 0.2.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/package.json +1 -1
- package/src/client.ts +40 -4
- package/src/index.ts +6 -1
- package/src/modules/auth.ts +167 -5
- package/src/modules/database.ts +83 -12
- package/src/modules/mail.ts +16 -0
- package/src/modules/schema.ts +24 -0
- package/src/types/index.ts +98 -2
- package/tests/auth.test.ts +88 -27
- package/tests/database.test.ts +91 -75
- package/tests/mail.test.ts +41 -0
- package/tests/schema.test.ts +32 -0
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -3,6 +3,8 @@ import { UrBackendError, parseApiError } from './errors';
|
|
|
3
3
|
import { AuthModule } from './modules/auth';
|
|
4
4
|
import { DatabaseModule } from './modules/database';
|
|
5
5
|
import { StorageModule } from './modules/storage';
|
|
6
|
+
import { SchemaModule } from './modules/schema';
|
|
7
|
+
import { MailModule } from './modules/mail';
|
|
6
8
|
|
|
7
9
|
export class UrBackendClient {
|
|
8
10
|
private apiKey: string;
|
|
@@ -10,6 +12,8 @@ export class UrBackendClient {
|
|
|
10
12
|
private _auth?: AuthModule;
|
|
11
13
|
private _db?: DatabaseModule;
|
|
12
14
|
private _storage?: StorageModule;
|
|
15
|
+
private _schema?: SchemaModule;
|
|
16
|
+
private _mail?: MailModule;
|
|
13
17
|
private headers: Record<string, string>;
|
|
14
18
|
|
|
15
19
|
constructor(config: UrBackendConfig) {
|
|
@@ -17,9 +21,9 @@ export class UrBackendClient {
|
|
|
17
21
|
this.baseUrl = config.baseUrl || 'https://api.ub.bitbros.in';
|
|
18
22
|
this.headers = config.headers || {};
|
|
19
23
|
|
|
20
|
-
if (typeof window !== 'undefined') {
|
|
24
|
+
if (typeof window !== 'undefined' && this.apiKey.startsWith('sk_live_')) {
|
|
21
25
|
console.warn(
|
|
22
|
-
'⚠️ urbackend-sdk: Avoid exposing your
|
|
26
|
+
'⚠️ urbackend-sdk: Avoid exposing your Secret Key (sk_live_...) in client-side code. This can lead to unauthorized access to your account and data. Use your Publishable Key (pk_live_...) instead.',
|
|
23
27
|
);
|
|
24
28
|
}
|
|
25
29
|
}
|
|
@@ -45,6 +49,28 @@ export class UrBackendClient {
|
|
|
45
49
|
return this._storage;
|
|
46
50
|
}
|
|
47
51
|
|
|
52
|
+
get schema(): SchemaModule {
|
|
53
|
+
if (!this._schema) {
|
|
54
|
+
this._schema = new SchemaModule(this);
|
|
55
|
+
}
|
|
56
|
+
return this._schema;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
get mail(): MailModule {
|
|
60
|
+
if (!this._mail) {
|
|
61
|
+
this._mail = new MailModule(this);
|
|
62
|
+
}
|
|
63
|
+
return this._mail;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public getBaseUrl(): string {
|
|
67
|
+
return this.baseUrl;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public getApiKey(): string {
|
|
71
|
+
return this.apiKey;
|
|
72
|
+
}
|
|
73
|
+
|
|
48
74
|
/**
|
|
49
75
|
* Internal request handler
|
|
50
76
|
*/
|
|
@@ -56,7 +82,7 @@ export class UrBackendClient {
|
|
|
56
82
|
const url = `${this.baseUrl}${path}`;
|
|
57
83
|
const headers: Record<string, string> = {
|
|
58
84
|
'x-api-key': this.apiKey,
|
|
59
|
-
'User-Agent': `urbackend-sdk-js/0.
|
|
85
|
+
'User-Agent': `urbackend-sdk-js/0.2.0`,
|
|
60
86
|
...this.headers,
|
|
61
87
|
};
|
|
62
88
|
|
|
@@ -64,6 +90,11 @@ export class UrBackendClient {
|
|
|
64
90
|
headers['Authorization'] = `Bearer ${options.token}`;
|
|
65
91
|
}
|
|
66
92
|
|
|
93
|
+
// Merge custom headers from options if provided
|
|
94
|
+
if (options.headers) {
|
|
95
|
+
Object.assign(headers, options.headers);
|
|
96
|
+
}
|
|
97
|
+
|
|
67
98
|
let requestBody: BodyInit | undefined;
|
|
68
99
|
|
|
69
100
|
if (options.isMultipart) {
|
|
@@ -79,6 +110,7 @@ export class UrBackendClient {
|
|
|
79
110
|
method,
|
|
80
111
|
headers,
|
|
81
112
|
body: requestBody,
|
|
113
|
+
credentials: options.credentials,
|
|
82
114
|
});
|
|
83
115
|
|
|
84
116
|
if (!response.ok) {
|
|
@@ -89,7 +121,11 @@ export class UrBackendClient {
|
|
|
89
121
|
if (contentType && contentType.includes('application/json')) {
|
|
90
122
|
const json = await response.json();
|
|
91
123
|
// The API returns { data, success, message }
|
|
92
|
-
return
|
|
124
|
+
// If data is present, return it. If success/message are present but no data, return the whole object (for exchange/logout etc)
|
|
125
|
+
if (json.data !== undefined) {
|
|
126
|
+
return json.data;
|
|
127
|
+
}
|
|
128
|
+
return json;
|
|
93
129
|
}
|
|
94
130
|
|
|
95
131
|
return (await response.text()) as unknown as T;
|
package/src/index.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { UrBackendClient } from './client';
|
|
2
2
|
import { UrBackendConfig } from './types';
|
|
3
|
+
import { AuthModule } from './modules/auth';
|
|
4
|
+
import { DatabaseModule } from './modules/database';
|
|
5
|
+
import { StorageModule } from './modules/storage';
|
|
6
|
+
import { SchemaModule } from './modules/schema';
|
|
7
|
+
import { MailModule } from './modules/mail';
|
|
3
8
|
|
|
4
9
|
export * from './types';
|
|
5
10
|
export * from './errors';
|
|
6
|
-
export { UrBackendClient };
|
|
11
|
+
export { UrBackendClient, AuthModule, DatabaseModule, StorageModule, SchemaModule, MailModule };
|
|
7
12
|
|
|
8
13
|
/**
|
|
9
14
|
* Factory function to create a new urBackend client
|
package/src/modules/auth.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { UrBackendClient } from '../client';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
AuthUser,
|
|
4
|
+
AuthResponse,
|
|
5
|
+
SignUpPayload,
|
|
6
|
+
LoginPayload,
|
|
7
|
+
UpdateProfilePayload,
|
|
8
|
+
ChangePasswordPayload,
|
|
9
|
+
VerifyEmailPayload,
|
|
10
|
+
ResendOtpPayload,
|
|
11
|
+
RequestPasswordResetPayload,
|
|
12
|
+
ResetPasswordPayload,
|
|
13
|
+
SocialExchangePayload,
|
|
14
|
+
SocialExchangeResponse,
|
|
15
|
+
RequestOptions,
|
|
16
|
+
} from '../types';
|
|
3
17
|
import { AuthError } from '../errors';
|
|
4
18
|
|
|
5
19
|
export class AuthModule {
|
|
@@ -21,7 +35,15 @@ export class AuthModule {
|
|
|
21
35
|
const response = await this.client.request<AuthResponse>('POST', '/api/userAuth/login', {
|
|
22
36
|
body: payload,
|
|
23
37
|
});
|
|
24
|
-
|
|
38
|
+
|
|
39
|
+
this.sessionToken = response.accessToken || response.token;
|
|
40
|
+
|
|
41
|
+
if (!response.accessToken && response.token) {
|
|
42
|
+
console.warn(
|
|
43
|
+
'urbackend-sdk: The server returned "token" which is deprecated. Please update your backend to return "accessToken".',
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
25
47
|
return response;
|
|
26
48
|
}
|
|
27
49
|
|
|
@@ -32,16 +54,156 @@ export class AuthModule {
|
|
|
32
54
|
const activeToken = token || this.sessionToken;
|
|
33
55
|
|
|
34
56
|
if (!activeToken) {
|
|
35
|
-
throw new AuthError(
|
|
57
|
+
throw new AuthError(
|
|
58
|
+
'Authentication token is required for /me endpoint',
|
|
59
|
+
401,
|
|
60
|
+
'/api/userAuth/me',
|
|
61
|
+
);
|
|
36
62
|
}
|
|
37
63
|
|
|
38
64
|
return this.client.request<AuthUser>('GET', '/api/userAuth/me', { token: activeToken });
|
|
39
65
|
}
|
|
40
66
|
|
|
41
67
|
/**
|
|
42
|
-
*
|
|
68
|
+
* Update the current authenticated user's profile
|
|
69
|
+
*/
|
|
70
|
+
public async updateProfile(payload: UpdateProfilePayload, token?: string): Promise<{ message: string }> {
|
|
71
|
+
const activeToken = token || this.sessionToken;
|
|
72
|
+
if (!activeToken) {
|
|
73
|
+
throw new AuthError('Authentication token is required to update profile', 401, '/api/userAuth/update-profile');
|
|
74
|
+
}
|
|
75
|
+
return this.client.request<{ message: string }>('PUT', '/api/userAuth/update-profile', {
|
|
76
|
+
body: payload,
|
|
77
|
+
token: activeToken,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Change the current authenticated user's password
|
|
83
|
+
*/
|
|
84
|
+
public async changePassword(payload: ChangePasswordPayload, token?: string): Promise<{ message: string }> {
|
|
85
|
+
const activeToken = token || this.sessionToken;
|
|
86
|
+
if (!activeToken) {
|
|
87
|
+
throw new AuthError('Authentication token is required to change password', 401, '/api/userAuth/change-password');
|
|
88
|
+
}
|
|
89
|
+
return this.client.request<{ message: string }>('PUT', '/api/userAuth/change-password', {
|
|
90
|
+
body: payload,
|
|
91
|
+
token: activeToken,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Verify user email with OTP
|
|
97
|
+
*/
|
|
98
|
+
public async verifyEmail(payload: VerifyEmailPayload): Promise<{ message: string }> {
|
|
99
|
+
return this.client.request<{ message: string }>('POST', '/api/userAuth/verify-email', {
|
|
100
|
+
body: payload,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Resend verification OTP
|
|
106
|
+
*/
|
|
107
|
+
public async resendVerificationOtp(payload: ResendOtpPayload): Promise<{ message: string }> {
|
|
108
|
+
return this.client.request<{ message: string }>('POST', '/api/userAuth/resend-verification-otp', {
|
|
109
|
+
body: payload,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Request password reset OTP
|
|
115
|
+
*/
|
|
116
|
+
public async requestPasswordReset(payload: RequestPasswordResetPayload): Promise<{ message: string }> {
|
|
117
|
+
return this.client.request<{ message: string }>('POST', '/api/userAuth/request-password-reset', {
|
|
118
|
+
body: payload,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Reset user password with OTP
|
|
43
124
|
*/
|
|
44
|
-
public
|
|
125
|
+
public async resetPassword(payload: ResetPasswordPayload): Promise<{ message: string }> {
|
|
126
|
+
return this.client.request<{ message: string }>('POST', '/api/userAuth/reset-password', {
|
|
127
|
+
body: payload,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get public-safe profile by username
|
|
133
|
+
*/
|
|
134
|
+
public async publicProfile(username: string): Promise<AuthUser> {
|
|
135
|
+
return this.client.request<AuthUser>('GET', `/api/userAuth/public/${username}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Refresh the access token
|
|
140
|
+
* @param refreshToken Optional refresh token for header mode. If omitted, uses cookie mode.
|
|
141
|
+
*/
|
|
142
|
+
public async refreshToken(refreshToken?: string): Promise<AuthResponse> {
|
|
143
|
+
const options: RequestOptions = {};
|
|
144
|
+
if (refreshToken) {
|
|
145
|
+
options.headers = { 'x-refresh-token': refreshToken, 'x-refresh-token-mode': 'header' };
|
|
146
|
+
} else {
|
|
147
|
+
options.credentials = 'include';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const response = await this.client.request<AuthResponse>('POST', '/api/userAuth/refresh-token', options);
|
|
151
|
+
this.sessionToken = response.accessToken || response.token;
|
|
152
|
+
return response;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Returns the start URL for social authentication.
|
|
157
|
+
* Redirect the user's browser to this URL to begin the flow.
|
|
158
|
+
*/
|
|
159
|
+
public socialStart(provider: 'github' | 'google'): string {
|
|
160
|
+
return `${this.client.getBaseUrl()}/api/userAuth/social/${provider}/start?key=${this.client.getApiKey()}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Exchange social auth rtCode for a refresh token
|
|
165
|
+
*/
|
|
166
|
+
public async socialExchange(payload: SocialExchangePayload): Promise<SocialExchangeResponse> {
|
|
167
|
+
return this.client.request<SocialExchangeResponse>('POST', '/api/userAuth/social/exchange', {
|
|
168
|
+
body: payload,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Revoke the current session and clear local state
|
|
174
|
+
*/
|
|
175
|
+
public async logout(token?: string): Promise<{ success: boolean; message: string }> {
|
|
176
|
+
const activeToken = token || this.sessionToken;
|
|
177
|
+
let result = { success: true, message: 'Logged out locally' };
|
|
178
|
+
|
|
179
|
+
if (activeToken) {
|
|
180
|
+
try {
|
|
181
|
+
result = await this.client.request<{ success: boolean; message: string }>(
|
|
182
|
+
'POST',
|
|
183
|
+
'/api/userAuth/logout',
|
|
184
|
+
{ token: activeToken, credentials: 'include' },
|
|
185
|
+
);
|
|
186
|
+
} catch (e) {
|
|
187
|
+
// Silently fail if server logout fails, we still want to clear local state
|
|
188
|
+
console.warn('urbackend-sdk: Server logout failed', e);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
45
192
|
this.sessionToken = undefined;
|
|
193
|
+
return result;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Manually set the session token (e.g. after social auth exchange)
|
|
198
|
+
*/
|
|
199
|
+
public setToken(token: string): void {
|
|
200
|
+
this.sessionToken = token;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get the current stored session token
|
|
205
|
+
*/
|
|
206
|
+
public getToken(): string | undefined {
|
|
207
|
+
return this.sessionToken;
|
|
46
208
|
}
|
|
47
209
|
}
|
package/src/modules/database.ts
CHANGED
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { UrBackendClient } from '../client';
|
|
2
|
-
import { DocumentData, InsertPayload, UpdatePayload } from '../types';
|
|
2
|
+
import { DocumentData, InsertPayload, UpdatePayload, PatchPayload, QueryParams } from '../types';
|
|
3
3
|
import { NotFoundError } from '../errors';
|
|
4
4
|
|
|
5
5
|
export class DatabaseModule {
|
|
6
6
|
constructor(private client: UrBackendClient) {}
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* Fetch all documents from a collection
|
|
9
|
+
* Fetch all documents from a collection with optional query parameters
|
|
10
10
|
*/
|
|
11
|
-
public async getAll<T extends DocumentData>(collection: string): Promise<T[]> {
|
|
11
|
+
public async getAll<T extends DocumentData>(collection: string, params: QueryParams = {}): Promise<T[]> {
|
|
12
|
+
const queryString = this.buildQueryString(params);
|
|
13
|
+
const path = `/api/data/${collection}${queryString}`;
|
|
14
|
+
|
|
12
15
|
try {
|
|
13
|
-
return await this.client.request<T[]>('GET',
|
|
16
|
+
return await this.client.request<T[]>('GET', path);
|
|
14
17
|
} catch (e) {
|
|
15
18
|
if (e instanceof NotFoundError) {
|
|
16
19
|
return [] as T[];
|
|
@@ -22,32 +25,100 @@ export class DatabaseModule {
|
|
|
22
25
|
/**
|
|
23
26
|
* Fetch a single document by its ID
|
|
24
27
|
*/
|
|
25
|
-
public async getOne<T extends DocumentData>(
|
|
26
|
-
|
|
28
|
+
public async getOne<T extends DocumentData>(
|
|
29
|
+
collection: string,
|
|
30
|
+
id: string,
|
|
31
|
+
options: { populate?: string | string[]; expand?: string | string[] } = {}
|
|
32
|
+
): Promise<T> {
|
|
33
|
+
const queryString = this.buildQueryString(options);
|
|
34
|
+
return this.client.request<T>('GET', `/api/data/${collection}/${id}${queryString}`);
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
/**
|
|
30
38
|
* Insert a new document into a collection
|
|
31
39
|
*/
|
|
32
|
-
public async insert<T extends DocumentData>(
|
|
33
|
-
|
|
40
|
+
public async insert<T extends DocumentData>(
|
|
41
|
+
collection: string,
|
|
42
|
+
data: InsertPayload,
|
|
43
|
+
token?: string
|
|
44
|
+
): Promise<T> {
|
|
45
|
+
return this.client.request<T>('POST', `/api/data/${collection}`, {
|
|
46
|
+
body: data,
|
|
47
|
+
token
|
|
48
|
+
});
|
|
34
49
|
}
|
|
35
50
|
|
|
36
51
|
/**
|
|
37
|
-
* Update an existing document by its ID
|
|
52
|
+
* Update an existing document by its ID (Full replacement)
|
|
38
53
|
*/
|
|
39
54
|
public async update<T extends DocumentData>(
|
|
40
55
|
collection: string,
|
|
41
56
|
id: string,
|
|
42
57
|
data: UpdatePayload,
|
|
58
|
+
token?: string
|
|
59
|
+
): Promise<T> {
|
|
60
|
+
return this.client.request<T>('PUT', `/api/data/${collection}/${id}`, {
|
|
61
|
+
body: data,
|
|
62
|
+
token
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Partially update an existing document by its ID
|
|
68
|
+
*/
|
|
69
|
+
public async patch<T extends DocumentData>(
|
|
70
|
+
collection: string,
|
|
71
|
+
id: string,
|
|
72
|
+
data: PatchPayload,
|
|
73
|
+
token?: string
|
|
43
74
|
): Promise<T> {
|
|
44
|
-
return this.client.request<T>('
|
|
75
|
+
return this.client.request<T>('PATCH', `/api/data/${collection}/${id}`, {
|
|
76
|
+
body: data,
|
|
77
|
+
token
|
|
78
|
+
});
|
|
45
79
|
}
|
|
46
80
|
|
|
47
81
|
/**
|
|
48
82
|
* Delete a document by its ID
|
|
49
83
|
*/
|
|
50
|
-
public async delete(collection: string, id: string): Promise<{ deleted: boolean }> {
|
|
51
|
-
|
|
84
|
+
public async delete(collection: string, id: string, token?: string): Promise<{ deleted: boolean }> {
|
|
85
|
+
const result = await this.client.request<{ message?: string; id?: string } | null>(
|
|
86
|
+
'DELETE',
|
|
87
|
+
`/api/data/${collection}/${id}`,
|
|
88
|
+
{ token },
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const deleted =
|
|
92
|
+
typeof result === 'object' &&
|
|
93
|
+
result !== null &&
|
|
94
|
+
(result.id === id || result.message === 'Document deleted');
|
|
95
|
+
|
|
96
|
+
return { deleted };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Internal helper to build query string from QueryParams
|
|
101
|
+
*/
|
|
102
|
+
private buildQueryString(params: QueryParams): string {
|
|
103
|
+
const searchParams = new URLSearchParams();
|
|
104
|
+
|
|
105
|
+
Object.entries(params).forEach(([key, value]) => {
|
|
106
|
+
if (value === undefined || value === null) return;
|
|
107
|
+
|
|
108
|
+
if (key === 'filter' && typeof value === 'object') {
|
|
109
|
+
Object.entries(value as Record<string, unknown>).forEach(([fKey, fValue]) => {
|
|
110
|
+
if (fValue !== undefined && fValue !== null) {
|
|
111
|
+
searchParams.append(fKey, String(fValue));
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
} else if ((key === 'populate' || key === 'expand') && Array.isArray(value)) {
|
|
115
|
+
searchParams.append(key, value.join(','));
|
|
116
|
+
} else {
|
|
117
|
+
searchParams.append(key, String(value));
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const str = searchParams.toString();
|
|
122
|
+
return str ? `?${str}` : '';
|
|
52
123
|
}
|
|
53
124
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { UrBackendClient } from '../client';
|
|
2
|
+
import { SendMailPayload, SendMailResponse } from '../types';
|
|
3
|
+
|
|
4
|
+
export class MailModule {
|
|
5
|
+
constructor(private client: UrBackendClient) {}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Send an email using the urBackend mail service.
|
|
9
|
+
* Note: This requires a Secret Key (sk_live_...) and should be called from a server environment.
|
|
10
|
+
*/
|
|
11
|
+
public async send(payload: SendMailPayload): Promise<SendMailResponse> {
|
|
12
|
+
return this.client.request<SendMailResponse>('POST', '/api/mail/send', {
|
|
13
|
+
body: payload,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { UrBackendClient } from '../client';
|
|
2
|
+
import { CollectionSchema } from '../types';
|
|
3
|
+
|
|
4
|
+
export class SchemaModule {
|
|
5
|
+
constructor(private client: UrBackendClient) {}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Fetch the schema definition for a collection
|
|
9
|
+
*/
|
|
10
|
+
public async getSchema(collection: string): Promise<CollectionSchema> {
|
|
11
|
+
const trimmedCollection = collection.trim();
|
|
12
|
+
if (trimmedCollection === '') {
|
|
13
|
+
throw new Error('Collection name cannot be empty or whitespace-only');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const encodedCollection = encodeURIComponent(trimmedCollection);
|
|
17
|
+
const response = await this.client.request<{
|
|
18
|
+
message: string;
|
|
19
|
+
collection: CollectionSchema;
|
|
20
|
+
}>('GET', `/api/schemas/${encodedCollection}`);
|
|
21
|
+
|
|
22
|
+
return response.collection;
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -8,12 +8,26 @@ export interface RequestOptions {
|
|
|
8
8
|
body?: unknown;
|
|
9
9
|
token?: string;
|
|
10
10
|
isMultipart?: boolean;
|
|
11
|
+
credentials?: RequestCredentials;
|
|
12
|
+
headers?: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface QueryParams {
|
|
16
|
+
page?: number;
|
|
17
|
+
limit?: number;
|
|
18
|
+
sort?: string;
|
|
19
|
+
populate?: string | string[];
|
|
20
|
+
expand?: string | string[];
|
|
21
|
+
filter?: Record<string, unknown>;
|
|
22
|
+
[key: string]: unknown;
|
|
11
23
|
}
|
|
12
24
|
|
|
13
25
|
export interface SignUpPayload {
|
|
14
26
|
email: string;
|
|
15
27
|
password: string;
|
|
28
|
+
username?: string;
|
|
16
29
|
name?: string;
|
|
30
|
+
[key: string]: unknown;
|
|
17
31
|
}
|
|
18
32
|
|
|
19
33
|
export interface LoginPayload {
|
|
@@ -21,16 +35,60 @@ export interface LoginPayload {
|
|
|
21
35
|
password: string;
|
|
22
36
|
}
|
|
23
37
|
|
|
38
|
+
export interface UpdateProfilePayload {
|
|
39
|
+
username?: string;
|
|
40
|
+
name?: string;
|
|
41
|
+
[key: string]: unknown;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ChangePasswordPayload {
|
|
45
|
+
currentPassword: string;
|
|
46
|
+
newPassword: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface VerifyEmailPayload {
|
|
50
|
+
email: string;
|
|
51
|
+
otp: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ResendOtpPayload {
|
|
55
|
+
email: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface RequestPasswordResetPayload {
|
|
59
|
+
email: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface ResetPasswordPayload {
|
|
63
|
+
email: string;
|
|
64
|
+
otp: string;
|
|
65
|
+
newPassword: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface SocialExchangePayload {
|
|
69
|
+
token: string;
|
|
70
|
+
rtCode: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface SocialExchangeResponse {
|
|
74
|
+
refreshToken: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
24
77
|
export interface AuthUser {
|
|
25
78
|
_id: string;
|
|
26
79
|
email: string;
|
|
80
|
+
username?: string;
|
|
27
81
|
name?: string;
|
|
28
82
|
[key: string]: unknown;
|
|
29
83
|
}
|
|
30
84
|
|
|
31
85
|
export interface AuthResponse {
|
|
32
|
-
|
|
33
|
-
|
|
86
|
+
accessToken?: string;
|
|
87
|
+
/** @deprecated use accessToken instead */
|
|
88
|
+
token?: string;
|
|
89
|
+
expiresIn?: string;
|
|
90
|
+
userId?: string;
|
|
91
|
+
user?: AuthUser;
|
|
34
92
|
}
|
|
35
93
|
|
|
36
94
|
export interface DocumentData {
|
|
@@ -46,9 +104,47 @@ export interface UpdatePayload {
|
|
|
46
104
|
[key: string]: unknown;
|
|
47
105
|
}
|
|
48
106
|
|
|
107
|
+
export interface PatchPayload {
|
|
108
|
+
[key: string]: unknown;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface SchemaField {
|
|
112
|
+
key: string;
|
|
113
|
+
type: string;
|
|
114
|
+
required: boolean;
|
|
115
|
+
unique?: boolean;
|
|
116
|
+
ref?: string;
|
|
117
|
+
items?: {
|
|
118
|
+
type: string;
|
|
119
|
+
fields?: SchemaField[];
|
|
120
|
+
};
|
|
121
|
+
fields?: SchemaField[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface CollectionSchema {
|
|
125
|
+
name: string;
|
|
126
|
+
model: SchemaField[];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface SendMailPayload {
|
|
130
|
+
to: string | string[];
|
|
131
|
+
subject: string;
|
|
132
|
+
text?: string;
|
|
133
|
+
html?: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface SendMailResponse {
|
|
137
|
+
id: string | null;
|
|
138
|
+
provider: 'byok' | 'default';
|
|
139
|
+
monthlyUsage: number;
|
|
140
|
+
monthlyLimit: number;
|
|
141
|
+
}
|
|
142
|
+
|
|
49
143
|
export interface UploadResponse {
|
|
50
144
|
url: string;
|
|
51
145
|
path: string;
|
|
146
|
+
provider: 'internal' | 'external';
|
|
147
|
+
message?: string;
|
|
52
148
|
}
|
|
53
149
|
|
|
54
150
|
export interface ApiResponse<T> {
|
package/tests/auth.test.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import { expect, test, vi } from 'vitest';
|
|
1
|
+
import { expect, test, vi, beforeEach } from 'vitest';
|
|
2
2
|
import urBackend from '../src/index';
|
|
3
3
|
|
|
4
|
-
const mockApiKey = '
|
|
5
|
-
|
|
4
|
+
const mockApiKey = 'pk_live_test';
|
|
5
|
+
let client: ReturnType<typeof urBackend>;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.resetAllMocks();
|
|
9
|
+
client = urBackend({ apiKey: mockApiKey });
|
|
10
|
+
});
|
|
6
11
|
|
|
7
12
|
test('signUp returns user object on success', async () => {
|
|
8
13
|
const mockUser = { _id: '123', email: 'test@example.com' };
|
|
@@ -19,8 +24,8 @@ test('signUp returns user object on success', async () => {
|
|
|
19
24
|
expect(user).toEqual(mockUser);
|
|
20
25
|
});
|
|
21
26
|
|
|
22
|
-
test('login stores
|
|
23
|
-
const
|
|
27
|
+
test('login stores accessToken', async () => {
|
|
28
|
+
const mockAccessToken = 'mock-access-token';
|
|
24
29
|
vi.stubGlobal(
|
|
25
30
|
'fetch',
|
|
26
31
|
vi.fn().mockResolvedValue({
|
|
@@ -29,13 +34,14 @@ test('login stores session token', async () => {
|
|
|
29
34
|
json: () =>
|
|
30
35
|
Promise.resolve({
|
|
31
36
|
success: true,
|
|
32
|
-
data: {
|
|
37
|
+
data: { accessToken: mockAccessToken, user: { _id: '123', email: 'test@example.com' } },
|
|
33
38
|
}),
|
|
34
39
|
}),
|
|
35
40
|
);
|
|
36
41
|
|
|
37
42
|
const response = await client.auth.login({ email: 'test@example.com', password: 'password' });
|
|
38
|
-
expect(response.
|
|
43
|
+
expect(response.accessToken).toBe(mockAccessToken);
|
|
44
|
+
expect(client.auth.getToken()).toBe(mockAccessToken);
|
|
39
45
|
});
|
|
40
46
|
|
|
41
47
|
test('me() uses stored token from login', async () => {
|
|
@@ -51,7 +57,7 @@ test('me() uses stored token from login', async () => {
|
|
|
51
57
|
json: () =>
|
|
52
58
|
Promise.resolve({
|
|
53
59
|
success: true,
|
|
54
|
-
data: {
|
|
60
|
+
data: { accessToken: mockToken, user: mockUser },
|
|
55
61
|
}),
|
|
56
62
|
}),
|
|
57
63
|
);
|
|
@@ -77,32 +83,87 @@ test('me() uses stored token from login', async () => {
|
|
|
77
83
|
);
|
|
78
84
|
});
|
|
79
85
|
|
|
80
|
-
test('
|
|
81
|
-
client.auth.
|
|
82
|
-
|
|
83
|
-
|
|
86
|
+
test('logout() calls server and clears local token', async () => {
|
|
87
|
+
client.auth.setToken('test-token');
|
|
88
|
+
const logoutFetchMock = vi.fn().mockResolvedValue({
|
|
89
|
+
ok: true,
|
|
90
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
91
|
+
json: () => Promise.resolve({ success: true, message: 'Logged out' }),
|
|
92
|
+
});
|
|
93
|
+
vi.stubGlobal('fetch', logoutFetchMock);
|
|
84
94
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
await client.auth.logout();
|
|
96
|
+
|
|
97
|
+
expect(logoutFetchMock).toHaveBeenCalledWith(
|
|
98
|
+
expect.stringContaining('/api/userAuth/logout'),
|
|
99
|
+
expect.objectContaining({
|
|
100
|
+
method: 'POST',
|
|
101
|
+
headers: expect.objectContaining({
|
|
102
|
+
Authorization: 'Bearer test-token',
|
|
103
|
+
}),
|
|
94
104
|
}),
|
|
95
105
|
);
|
|
106
|
+
expect(client.auth.getToken()).toBeUndefined();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('refreshToken() uses credentials: include by default', async () => {
|
|
110
|
+
const refreshFetchMock = vi.fn().mockResolvedValue({
|
|
111
|
+
ok: true,
|
|
112
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
113
|
+
json: () => Promise.resolve({ success: true, data: { accessToken: 'new-token' } }),
|
|
114
|
+
});
|
|
115
|
+
vi.stubGlobal('fetch', refreshFetchMock);
|
|
116
|
+
|
|
117
|
+
await client.auth.refreshToken();
|
|
96
118
|
|
|
97
|
-
|
|
98
|
-
'
|
|
119
|
+
expect(refreshFetchMock).toHaveBeenCalledWith(
|
|
120
|
+
expect.stringContaining('/api/userAuth/refresh-token'),
|
|
121
|
+
expect.objectContaining({
|
|
122
|
+
method: 'POST',
|
|
123
|
+
credentials: 'include',
|
|
124
|
+
}),
|
|
99
125
|
);
|
|
100
126
|
});
|
|
101
127
|
|
|
102
|
-
test('
|
|
103
|
-
|
|
128
|
+
test('refreshToken(token) uses header mode', async () => {
|
|
129
|
+
const refreshFetchMock = vi.fn().mockResolvedValue({
|
|
130
|
+
ok: true,
|
|
131
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
132
|
+
json: () => Promise.resolve({ success: true, data: { accessToken: 'new-token' } }),
|
|
133
|
+
});
|
|
134
|
+
vi.stubGlobal('fetch', refreshFetchMock);
|
|
135
|
+
|
|
136
|
+
await client.auth.refreshToken('manual-refresh-token');
|
|
104
137
|
|
|
105
|
-
|
|
106
|
-
'
|
|
138
|
+
expect(refreshFetchMock).toHaveBeenCalledWith(
|
|
139
|
+
expect.stringContaining('/api/userAuth/refresh-token'),
|
|
140
|
+
expect.objectContaining({
|
|
141
|
+
method: 'POST',
|
|
142
|
+
headers: expect.objectContaining({
|
|
143
|
+
'x-refresh-token': 'manual-refresh-token',
|
|
144
|
+
'x-refresh-token-mode': 'header',
|
|
145
|
+
}),
|
|
146
|
+
}),
|
|
107
147
|
);
|
|
108
148
|
});
|
|
149
|
+
|
|
150
|
+
test('socialStart returns correct URL', () => {
|
|
151
|
+
const url = client.auth.socialStart('github');
|
|
152
|
+
expect(url).toBe('https://api.ub.bitbros.in/api/userAuth/social/github/start?key=pk_live_test');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('publicProfile calls correct endpoint', async () => {
|
|
156
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
157
|
+
ok: true,
|
|
158
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
159
|
+
json: () => Promise.resolve({ success: true, data: { username: 'yash' } }),
|
|
160
|
+
});
|
|
161
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
162
|
+
|
|
163
|
+
await client.auth.publicProfile('yash');
|
|
164
|
+
|
|
165
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
166
|
+
expect.stringContaining('/api/userAuth/public/yash'),
|
|
167
|
+
expect.any(Object),
|
|
168
|
+
);
|
|
169
|
+
});
|
package/tests/database.test.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import { expect, test, vi } from 'vitest';
|
|
1
|
+
import { expect, test, vi, beforeEach } from 'vitest';
|
|
2
2
|
import urBackend from '../src/index';
|
|
3
3
|
|
|
4
|
-
const mockApiKey = '
|
|
4
|
+
const mockApiKey = 'pk_live_test';
|
|
5
5
|
const client = urBackend({ apiKey: mockApiKey });
|
|
6
6
|
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.resetAllMocks();
|
|
9
|
+
});
|
|
10
|
+
|
|
7
11
|
test('getAll returns array of typed documents', async () => {
|
|
8
12
|
const mockData = [
|
|
9
13
|
{ _id: '1', name: 'Product 1' },
|
|
@@ -20,99 +24,111 @@ test('getAll returns array of typed documents', async () => {
|
|
|
20
24
|
|
|
21
25
|
const items = await client.db.getAll<{ _id: string; name: string }>('products');
|
|
22
26
|
expect(items).toEqual(mockData);
|
|
23
|
-
expect(items[0].name).toBe('Product 1');
|
|
24
27
|
});
|
|
25
28
|
|
|
26
|
-
test('
|
|
27
|
-
vi.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
test('getAll with query params builds correct query string', async () => {
|
|
30
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
31
|
+
ok: true,
|
|
32
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
33
|
+
json: () => Promise.resolve({ success: true, data: [] }),
|
|
34
|
+
});
|
|
35
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
36
|
+
|
|
37
|
+
await client.db.getAll('products', {
|
|
38
|
+
page: 2,
|
|
39
|
+
limit: 10,
|
|
40
|
+
sort: 'price:asc',
|
|
41
|
+
populate: 'category',
|
|
42
|
+
filter: { price_gt: 100 }
|
|
43
|
+
});
|
|
36
44
|
|
|
37
|
-
|
|
45
|
+
const url = fetchMock.mock.calls[0][0] as string;
|
|
46
|
+
const searchParams = new URL(url).searchParams;
|
|
47
|
+
|
|
48
|
+
expect(searchParams.get('page')).toBe('2');
|
|
49
|
+
expect(searchParams.get('limit')).toBe('10');
|
|
50
|
+
expect(searchParams.get('sort')).toBe('price:asc');
|
|
51
|
+
expect(searchParams.get('populate')).toBe('category');
|
|
52
|
+
expect(searchParams.get('price_gt')).toBe('100');
|
|
38
53
|
});
|
|
39
54
|
|
|
40
|
-
test('insert returns created document
|
|
55
|
+
test('insert returns created document and handles optional token', async () => {
|
|
41
56
|
const payload = { name: 'New Item' };
|
|
42
57
|
const mockCreated = { _id: 'new-id', ...payload };
|
|
43
|
-
vi.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}),
|
|
50
|
-
);
|
|
58
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
59
|
+
ok: true,
|
|
60
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
61
|
+
json: () => Promise.resolve({ success: true, data: mockCreated }),
|
|
62
|
+
});
|
|
63
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
51
64
|
|
|
52
|
-
const result = await client.db.insert<{ _id: string; name: string }>('products', payload);
|
|
65
|
+
const result = await client.db.insert<{ _id: string; name: string }>('products', payload, 'user-token');
|
|
66
|
+
|
|
53
67
|
expect(result._id).toBe('new-id');
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
vi.fn().mockResolvedValue({
|
|
62
|
-
ok: true,
|
|
63
|
-
headers: new Headers({ 'content-type': 'application/json' }),
|
|
64
|
-
json: () => Promise.resolve({ success: true, data: mockUpdated }),
|
|
68
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
69
|
+
expect.stringContaining('/api/data/products'),
|
|
70
|
+
expect.objectContaining({
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: expect.objectContaining({
|
|
73
|
+
Authorization: 'Bearer user-token',
|
|
74
|
+
}),
|
|
65
75
|
}),
|
|
66
76
|
);
|
|
67
|
-
|
|
68
|
-
const result = await client.db.update<{ _id: string; name: string }>('products', 'id-1', payload);
|
|
69
|
-
expect(result.name).toBe('Updated');
|
|
70
77
|
});
|
|
71
78
|
|
|
72
|
-
test('
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
);
|
|
79
|
+
test('patch sends PATCH request', async () => {
|
|
80
|
+
const payload = { price: 50 };
|
|
81
|
+
const mockUpdated = { _id: '1', name: 'Original', price: 50 };
|
|
82
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
83
|
+
ok: true,
|
|
84
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
85
|
+
json: () => Promise.resolve({ success: true, data: mockUpdated }),
|
|
86
|
+
});
|
|
87
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
81
88
|
|
|
82
|
-
|
|
83
|
-
expect(result.deleted).toBe(true);
|
|
84
|
-
});
|
|
89
|
+
await client.db.patch('products', '1', payload);
|
|
85
90
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
status: 429,
|
|
92
|
-
url: 'https://api.urbackend.bitbros.in/api/data/products',
|
|
93
|
-
headers: new Headers({ 'Retry-After': '60' }),
|
|
94
|
-
json: () => Promise.resolve({ success: false, message: 'Too Many Requests' }),
|
|
91
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
92
|
+
expect.stringContaining('/api/data/products/1'),
|
|
93
|
+
expect.objectContaining({
|
|
94
|
+
method: 'PATCH',
|
|
95
|
+
body: JSON.stringify(payload),
|
|
95
96
|
}),
|
|
96
97
|
);
|
|
98
|
+
});
|
|
97
99
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
100
|
+
test('getOne with populate builds correct query string', async () => {
|
|
101
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
102
|
+
ok: true,
|
|
103
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
104
|
+
json: () => Promise.resolve({ success: true, data: { _id: '1' } }),
|
|
105
|
+
});
|
|
106
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
107
|
+
|
|
108
|
+
await client.db.getOne('products', '1', { populate: 'category' });
|
|
109
|
+
|
|
110
|
+
const url = fetchMock.mock.calls[0][0] as string;
|
|
111
|
+
expect(url).toContain('?populate=category');
|
|
104
112
|
});
|
|
105
113
|
|
|
106
|
-
test('
|
|
107
|
-
vi.
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
test('delete returns { deleted: true } and handles token', async () => {
|
|
115
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
116
|
+
ok: true,
|
|
117
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
118
|
+
json: () => Promise.resolve({ success: true, data: { message: 'Document deleted', id: 'id-1' } }),
|
|
119
|
+
});
|
|
120
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
121
|
+
|
|
122
|
+
const result = await client.db.delete('products', 'id-1', 'admin-token');
|
|
123
|
+
|
|
124
|
+
expect(result.deleted).toBe(true);
|
|
125
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
126
|
+
expect.stringContaining('/api/data/products/id-1'),
|
|
127
|
+
expect.objectContaining({
|
|
128
|
+
method: 'DELETE',
|
|
129
|
+
headers: expect.objectContaining({
|
|
130
|
+
Authorization: 'Bearer admin-token',
|
|
131
|
+
}),
|
|
114
132
|
}),
|
|
115
133
|
);
|
|
116
|
-
|
|
117
|
-
await expect(client.db.insert('products', {})).rejects.toThrow('Invalid data format');
|
|
118
134
|
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { expect, test, vi, beforeEach } from 'vitest';
|
|
2
|
+
import urBackend from '../src/index';
|
|
3
|
+
|
|
4
|
+
const mockApiKey = 'sk_live_test';
|
|
5
|
+
const client = urBackend({ apiKey: mockApiKey });
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.resetAllMocks();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('send() sends POST request to mail endpoint', async () => {
|
|
12
|
+
const payload = {
|
|
13
|
+
to: 'user@example.com',
|
|
14
|
+
subject: 'Hello',
|
|
15
|
+
text: 'Test mail'
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const mockResponse = {
|
|
19
|
+
success: true,
|
|
20
|
+
data: { id: 'msg_1', provider: 'default', monthlyUsage: 1, monthlyLimit: 100 },
|
|
21
|
+
message: 'Sent'
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const fetchMock = vi.fn().mockResolvedValue({
|
|
25
|
+
ok: true,
|
|
26
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
27
|
+
json: () => Promise.resolve(mockResponse),
|
|
28
|
+
});
|
|
29
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
30
|
+
|
|
31
|
+
const result = await client.mail.send(payload);
|
|
32
|
+
|
|
33
|
+
expect(result).toEqual(mockResponse.data);
|
|
34
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
35
|
+
expect.stringContaining('/api/mail/send'),
|
|
36
|
+
expect.objectContaining({
|
|
37
|
+
method: 'POST',
|
|
38
|
+
body: JSON.stringify(payload),
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { expect, test, vi, beforeEach } from 'vitest';
|
|
2
|
+
import urBackend from '../src/index';
|
|
3
|
+
|
|
4
|
+
const mockApiKey = 'pk_live_test';
|
|
5
|
+
const client = urBackend({ apiKey: mockApiKey });
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.resetAllMocks();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('getSchema returns collection schema', async () => {
|
|
12
|
+
const mockSchema = {
|
|
13
|
+
name: 'products',
|
|
14
|
+
model: [
|
|
15
|
+
{ key: 'name', type: 'String', required: true },
|
|
16
|
+
{ key: 'price', type: 'Number', required: true }
|
|
17
|
+
]
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
vi.stubGlobal(
|
|
21
|
+
'fetch',
|
|
22
|
+
vi.fn().mockResolvedValue({
|
|
23
|
+
ok: true,
|
|
24
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
25
|
+
json: () => Promise.resolve({ message: 'Schema exists', collection: mockSchema }),
|
|
26
|
+
}),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const schema = await client.schema.getSchema('products');
|
|
30
|
+
expect(schema).toEqual(mockSchema);
|
|
31
|
+
expect(schema.name).toBe('products');
|
|
32
|
+
});
|