@urbackend/sdk 0.1.0 → 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/.prettierrc +7 -0
- package/README.md +85 -82
- package/eslint.config.js +15 -0
- package/package.json +3 -11
- package/src/client.ts +143 -0
- package/src/errors.ts +90 -0
- package/src/index.ts +18 -0
- package/src/modules/auth.ts +209 -0
- package/src/modules/database.ts +124 -0
- package/src/modules/mail.ts +16 -0
- package/src/modules/schema.ts +24 -0
- package/src/modules/storage.ts +41 -0
- package/src/types/index.ts +154 -0
- package/tests/auth.test.ts +169 -0
- package/tests/database.test.ts +134 -0
- package/tests/mail.test.ts +41 -0
- package/tests/schema.test.ts +32 -0
- package/tests/storage.test.ts +62 -0
- package/tests/tsconfig.json +9 -0
- package/tsconfig.json +18 -0
- package/tsup.config.ts +16 -0
- package/vitest.config.ts +12 -0
- package/dist/index.cjs +0 -315
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.mts +0 -158
- package/dist/index.d.ts +0 -158
- package/dist/index.mjs +0 -281
- package/dist/index.mjs.map +0 -1
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { UrBackendClient } from '../client';
|
|
2
|
+
import { DocumentData, InsertPayload, UpdatePayload, PatchPayload, QueryParams } from '../types';
|
|
3
|
+
import { NotFoundError } from '../errors';
|
|
4
|
+
|
|
5
|
+
export class DatabaseModule {
|
|
6
|
+
constructor(private client: UrBackendClient) {}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Fetch all documents from a collection with optional query parameters
|
|
10
|
+
*/
|
|
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
|
+
|
|
15
|
+
try {
|
|
16
|
+
return await this.client.request<T[]>('GET', path);
|
|
17
|
+
} catch (e) {
|
|
18
|
+
if (e instanceof NotFoundError) {
|
|
19
|
+
return [] as T[];
|
|
20
|
+
}
|
|
21
|
+
throw e;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Fetch a single document by its ID
|
|
27
|
+
*/
|
|
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}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Insert a new document into a collection
|
|
39
|
+
*/
|
|
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
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Update an existing document by its ID (Full replacement)
|
|
53
|
+
*/
|
|
54
|
+
public async update<T extends DocumentData>(
|
|
55
|
+
collection: string,
|
|
56
|
+
id: string,
|
|
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
|
|
74
|
+
): Promise<T> {
|
|
75
|
+
return this.client.request<T>('PATCH', `/api/data/${collection}/${id}`, {
|
|
76
|
+
body: data,
|
|
77
|
+
token
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Delete a document by its ID
|
|
83
|
+
*/
|
|
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}` : '';
|
|
123
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
import { UrBackendClient } from '../client';
|
|
3
|
+
import { UploadResponse } from '../types';
|
|
4
|
+
|
|
5
|
+
export class StorageModule {
|
|
6
|
+
constructor(private client: UrBackendClient) {}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Upload a file to storage
|
|
10
|
+
*/
|
|
11
|
+
public async upload(file: unknown, filename?: string): Promise<UploadResponse> {
|
|
12
|
+
const formData = new FormData();
|
|
13
|
+
|
|
14
|
+
if (
|
|
15
|
+
typeof window === 'undefined' &&
|
|
16
|
+
typeof Buffer !== 'undefined' &&
|
|
17
|
+
Buffer.isBuffer(file)
|
|
18
|
+
) {
|
|
19
|
+
// In Node.js environment, convert Buffer to Blob for standard FormData
|
|
20
|
+
const blob = new Blob([file as unknown as BlobPart]);
|
|
21
|
+
formData.append('file', blob, filename || 'file');
|
|
22
|
+
} else {
|
|
23
|
+
// Browser File/Blob or Node.js Blob/File
|
|
24
|
+
formData.append('file', file as unknown as Blob, filename);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return this.client.request<UploadResponse>('POST', '/api/storage/upload', {
|
|
28
|
+
body: formData,
|
|
29
|
+
isMultipart: true,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Delete a file from storage by its path
|
|
35
|
+
*/
|
|
36
|
+
public async deleteFile(path: string): Promise<{ deleted: boolean }> {
|
|
37
|
+
return this.client.request<{ deleted: boolean }>('DELETE', '/api/storage/file', {
|
|
38
|
+
body: { path },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
export interface UrBackendConfig {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
baseUrl?: string;
|
|
4
|
+
headers?: Record<string, string>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface RequestOptions {
|
|
8
|
+
body?: unknown;
|
|
9
|
+
token?: string;
|
|
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;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SignUpPayload {
|
|
26
|
+
email: string;
|
|
27
|
+
password: string;
|
|
28
|
+
username?: string;
|
|
29
|
+
name?: string;
|
|
30
|
+
[key: string]: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface LoginPayload {
|
|
34
|
+
email: string;
|
|
35
|
+
password: string;
|
|
36
|
+
}
|
|
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
|
+
|
|
77
|
+
export interface AuthUser {
|
|
78
|
+
_id: string;
|
|
79
|
+
email: string;
|
|
80
|
+
username?: string;
|
|
81
|
+
name?: string;
|
|
82
|
+
[key: string]: unknown;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface AuthResponse {
|
|
86
|
+
accessToken?: string;
|
|
87
|
+
/** @deprecated use accessToken instead */
|
|
88
|
+
token?: string;
|
|
89
|
+
expiresIn?: string;
|
|
90
|
+
userId?: string;
|
|
91
|
+
user?: AuthUser;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface DocumentData {
|
|
95
|
+
_id: string;
|
|
96
|
+
[key: string]: unknown;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface InsertPayload {
|
|
100
|
+
[key: string]: unknown;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface UpdatePayload {
|
|
104
|
+
[key: string]: unknown;
|
|
105
|
+
}
|
|
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
|
+
|
|
143
|
+
export interface UploadResponse {
|
|
144
|
+
url: string;
|
|
145
|
+
path: string;
|
|
146
|
+
provider: 'internal' | 'external';
|
|
147
|
+
message?: string;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface ApiResponse<T> {
|
|
151
|
+
data: T;
|
|
152
|
+
success: boolean;
|
|
153
|
+
message?: string;
|
|
154
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { expect, test, vi, beforeEach } from 'vitest';
|
|
2
|
+
import urBackend from '../src/index';
|
|
3
|
+
|
|
4
|
+
const mockApiKey = 'pk_live_test';
|
|
5
|
+
let client: ReturnType<typeof urBackend>;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
vi.resetAllMocks();
|
|
9
|
+
client = urBackend({ apiKey: mockApiKey });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('signUp returns user object on success', async () => {
|
|
13
|
+
const mockUser = { _id: '123', email: 'test@example.com' };
|
|
14
|
+
vi.stubGlobal(
|
|
15
|
+
'fetch',
|
|
16
|
+
vi.fn().mockResolvedValue({
|
|
17
|
+
ok: true,
|
|
18
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
19
|
+
json: () => Promise.resolve({ success: true, data: mockUser }),
|
|
20
|
+
}),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const user = await client.auth.signUp({ email: 'test@example.com', password: 'password' });
|
|
24
|
+
expect(user).toEqual(mockUser);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('login stores accessToken', async () => {
|
|
28
|
+
const mockAccessToken = 'mock-access-token';
|
|
29
|
+
vi.stubGlobal(
|
|
30
|
+
'fetch',
|
|
31
|
+
vi.fn().mockResolvedValue({
|
|
32
|
+
ok: true,
|
|
33
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
34
|
+
json: () =>
|
|
35
|
+
Promise.resolve({
|
|
36
|
+
success: true,
|
|
37
|
+
data: { accessToken: mockAccessToken, user: { _id: '123', email: 'test@example.com' } },
|
|
38
|
+
}),
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const response = await client.auth.login({ email: 'test@example.com', password: 'password' });
|
|
43
|
+
expect(response.accessToken).toBe(mockAccessToken);
|
|
44
|
+
expect(client.auth.getToken()).toBe(mockAccessToken);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('me() uses stored token from login', async () => {
|
|
48
|
+
const mockToken = 'mock-token';
|
|
49
|
+
const mockUser = { _id: '123', email: 'test@example.com' };
|
|
50
|
+
|
|
51
|
+
// First mock login
|
|
52
|
+
vi.stubGlobal(
|
|
53
|
+
'fetch',
|
|
54
|
+
vi.fn().mockResolvedValue({
|
|
55
|
+
ok: true,
|
|
56
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
57
|
+
json: () =>
|
|
58
|
+
Promise.resolve({
|
|
59
|
+
success: true,
|
|
60
|
+
data: { accessToken: mockToken, user: mockUser },
|
|
61
|
+
}),
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
await client.auth.login({ email: 'test@example.com', password: 'password' });
|
|
65
|
+
|
|
66
|
+
// Then mock me call
|
|
67
|
+
const meFetchMock = vi.fn().mockResolvedValue({
|
|
68
|
+
ok: true,
|
|
69
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
70
|
+
json: () => Promise.resolve({ success: true, data: mockUser }),
|
|
71
|
+
});
|
|
72
|
+
vi.stubGlobal('fetch', meFetchMock);
|
|
73
|
+
|
|
74
|
+
await client.auth.me();
|
|
75
|
+
|
|
76
|
+
expect(meFetchMock).toHaveBeenCalledWith(
|
|
77
|
+
expect.stringContaining('/api/userAuth/me'),
|
|
78
|
+
expect.objectContaining({
|
|
79
|
+
headers: expect.objectContaining({
|
|
80
|
+
Authorization: `Bearer ${mockToken}`,
|
|
81
|
+
}),
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
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);
|
|
94
|
+
|
|
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
|
+
}),
|
|
104
|
+
}),
|
|
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();
|
|
118
|
+
|
|
119
|
+
expect(refreshFetchMock).toHaveBeenCalledWith(
|
|
120
|
+
expect.stringContaining('/api/userAuth/refresh-token'),
|
|
121
|
+
expect.objectContaining({
|
|
122
|
+
method: 'POST',
|
|
123
|
+
credentials: 'include',
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
|
|
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');
|
|
137
|
+
|
|
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
|
+
}),
|
|
147
|
+
);
|
|
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
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
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('getAll returns array of typed documents', async () => {
|
|
12
|
+
const mockData = [
|
|
13
|
+
{ _id: '1', name: 'Product 1' },
|
|
14
|
+
{ _id: '2', name: 'Product 2' },
|
|
15
|
+
];
|
|
16
|
+
vi.stubGlobal(
|
|
17
|
+
'fetch',
|
|
18
|
+
vi.fn().mockResolvedValue({
|
|
19
|
+
ok: true,
|
|
20
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
21
|
+
json: () => Promise.resolve({ success: true, data: mockData }),
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const items = await client.db.getAll<{ _id: string; name: string }>('products');
|
|
26
|
+
expect(items).toEqual(mockData);
|
|
27
|
+
});
|
|
28
|
+
|
|
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
|
+
});
|
|
44
|
+
|
|
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');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('insert returns created document and handles optional token', async () => {
|
|
56
|
+
const payload = { name: 'New Item' };
|
|
57
|
+
const mockCreated = { _id: 'new-id', ...payload };
|
|
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);
|
|
64
|
+
|
|
65
|
+
const result = await client.db.insert<{ _id: string; name: string }>('products', payload, 'user-token');
|
|
66
|
+
|
|
67
|
+
expect(result._id).toBe('new-id');
|
|
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
|
+
}),
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
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);
|
|
88
|
+
|
|
89
|
+
await client.db.patch('products', '1', payload);
|
|
90
|
+
|
|
91
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
92
|
+
expect.stringContaining('/api/data/products/1'),
|
|
93
|
+
expect.objectContaining({
|
|
94
|
+
method: 'PATCH',
|
|
95
|
+
body: JSON.stringify(payload),
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
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');
|
|
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
|
+
}),
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
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
|
+
});
|