alsabase 1.0.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/README.md +272 -0
- package/dist/Client.d.ts +60 -0
- package/dist/Client.js +220 -0
- package/dist/ClientResponseError.d.ts +25 -0
- package/dist/ClientResponseError.js +46 -0
- package/dist/index.cjs +1428 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +1405 -0
- package/dist/services/BaseService.d.ts +7 -0
- package/dist/services/BaseService.js +9 -0
- package/dist/services/CollectionService.d.ts +42 -0
- package/dist/services/CollectionService.js +94 -0
- package/dist/services/FileService.d.ts +71 -0
- package/dist/services/FileService.js +119 -0
- package/dist/services/HooksService.d.ts +76 -0
- package/dist/services/HooksService.js +135 -0
- package/dist/services/LogService.d.ts +32 -0
- package/dist/services/LogService.js +63 -0
- package/dist/services/RealtimeService.d.ts +28 -0
- package/dist/services/RealtimeService.js +186 -0
- package/dist/services/RecordService.d.ts +105 -0
- package/dist/services/RecordService.js +287 -0
- package/dist/services/SuperuserService.d.ts +35 -0
- package/dist/services/SuperuserService.js +80 -0
- package/dist/stores/AsyncAuthStore.d.ts +13 -0
- package/dist/stores/AsyncAuthStore.js +32 -0
- package/dist/stores/BaseAuthStore.d.ts +18 -0
- package/dist/stores/BaseAuthStore.js +81 -0
- package/dist/stores/LocalAuthStore.d.ts +8 -0
- package/dist/stores/LocalAuthStore.js +43 -0
- package/dist/types.d.ts +241 -0
- package/dist/types.js +1 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# AlsaBase JavaScript / TypeScript Client SDK
|
|
2
|
+
|
|
3
|
+
Official client SDK for **AlsaBase** - lightweight, batteries-included SQLite backend engine with instant REST APIs, realtime subscriptions, authentication, and superuser collection management.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install alsabase
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or via yarn/pnpm:
|
|
14
|
+
```bash
|
|
15
|
+
yarn add alsabase
|
|
16
|
+
# or
|
|
17
|
+
pnpm add alsabase
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Browser via CDN (ESM):
|
|
21
|
+
```html
|
|
22
|
+
<script type="module">
|
|
23
|
+
import AlsaBase from "https://cdn.jsdelivr.net/npm/alsabase/dist/index.js";
|
|
24
|
+
|
|
25
|
+
const ab = new AlsaBase("http://127.0.0.1:8090");
|
|
26
|
+
</script>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Quickstart
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import AlsaBase from "alsabase";
|
|
35
|
+
|
|
36
|
+
const ab = new AlsaBase("http://127.0.0.1:8090");
|
|
37
|
+
|
|
38
|
+
// 1. Authenticate as a regular user
|
|
39
|
+
const authData = await ab.collection("users").authWithPassword("john@example.com", "12345678");
|
|
40
|
+
|
|
41
|
+
// 2. Query paginated records
|
|
42
|
+
const result = await ab.collection("posts").getList(1, 20, {
|
|
43
|
+
filter: ab.filter("status = {:status}", { status: "published" }),
|
|
44
|
+
sort: "-created_at",
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
console.log(result.items);
|
|
48
|
+
|
|
49
|
+
// 3. Create a new record
|
|
50
|
+
const newPost = await ab.collection("posts").create({
|
|
51
|
+
title: "Hello World",
|
|
52
|
+
content: "Welcome to AlsaBase!",
|
|
53
|
+
status: "published",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// 4. Realtime subscription
|
|
57
|
+
const unsubscribe = await ab.collection("posts").subscribe("*", (e) => {
|
|
58
|
+
console.log("Post event:", e.action, e.record);
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Core Features
|
|
65
|
+
|
|
66
|
+
### 1. Authentication
|
|
67
|
+
|
|
68
|
+
#### User Authentication (Password & OTP)
|
|
69
|
+
```typescript
|
|
70
|
+
// Login with username or email
|
|
71
|
+
await ab.collection("users").authWithPassword("test@example.com", "password123");
|
|
72
|
+
|
|
73
|
+
// One-Time Password (OTP)
|
|
74
|
+
await ab.collection("users").requestOTP("test@example.com");
|
|
75
|
+
await ab.collection("users").authWithOTP("123456", "test@example.com");
|
|
76
|
+
|
|
77
|
+
// Check current auth status
|
|
78
|
+
console.log(ab.authStore.isValid);
|
|
79
|
+
console.log(ab.authStore.token);
|
|
80
|
+
console.log(ab.authStore.model);
|
|
81
|
+
|
|
82
|
+
// Logout
|
|
83
|
+
ab.authStore.clear();
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
#### Superuser / Admin Authentication
|
|
87
|
+
```typescript
|
|
88
|
+
// Login as Superuser / Admin
|
|
89
|
+
await ab.superusers.authWithPassword("admin@example.com", "superpassword");
|
|
90
|
+
|
|
91
|
+
// Admin alias is also available:
|
|
92
|
+
await ab.admins.authWithPassword("admin@example.com", "superpassword");
|
|
93
|
+
|
|
94
|
+
// Check if initial superuser setup is required
|
|
95
|
+
const { hasSuperuser } = await ab.superusers.hasInitialSuperuser();
|
|
96
|
+
|
|
97
|
+
if (!hasSuperuser) {
|
|
98
|
+
await ab.superusers.setupInitialSuperuser("admin@example.com", "superpassword");
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
### 2. Record Operations (CRUD)
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
// List records with pagination
|
|
108
|
+
const list = await ab.collection("articles").getList(1, 50, {
|
|
109
|
+
filter: 'status = "published"',
|
|
110
|
+
sort: "-created_at",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// Fetch all records across pages
|
|
114
|
+
const allArticles = await ab.collection("articles").getFullList();
|
|
115
|
+
|
|
116
|
+
// Fetch single record by ID
|
|
117
|
+
const article = await ab.collection("articles").getOne("RECORD_ID");
|
|
118
|
+
|
|
119
|
+
// Fetch first item matching filter
|
|
120
|
+
const first = await ab.collection("articles").getFirstListItem('slug = "intro"');
|
|
121
|
+
|
|
122
|
+
// Create record
|
|
123
|
+
const record = await ab.collection("articles").create({
|
|
124
|
+
title: "New Post",
|
|
125
|
+
slug: "new-post",
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Update record
|
|
129
|
+
await ab.collection("articles").update("RECORD_ID", {
|
|
130
|
+
title: "Updated Title",
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Delete record
|
|
134
|
+
await ab.collection("articles").delete("RECORD_ID");
|
|
135
|
+
|
|
136
|
+
// Truncate (delete all records - Superuser only)
|
|
137
|
+
await ab.collection("articles").truncate();
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
### 3. Collection Management (Superuser / Admin)
|
|
143
|
+
|
|
144
|
+
Create, inspect, and update collection schemas dynamically from code:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
// 1. Inspect table schema via ab.collection()
|
|
148
|
+
const tableSchema = await ab.collection("products").getSchema();
|
|
149
|
+
console.log("Table columns:", tableSchema.fields);
|
|
150
|
+
|
|
151
|
+
// 2. Inspect table schema via ab.collections service
|
|
152
|
+
const col = await ab.collections.getSchema("products"); // or ab.collections.getOne("products")
|
|
153
|
+
|
|
154
|
+
// 3. Inspect table schema via top-level helper
|
|
155
|
+
const schema = await ab.getSchema("products");
|
|
156
|
+
|
|
157
|
+
// List all collections
|
|
158
|
+
const collections = await ab.collections.getFullList();
|
|
159
|
+
|
|
160
|
+
// Create a new collection
|
|
161
|
+
const newCollection = await ab.collections.create({
|
|
162
|
+
name: "products",
|
|
163
|
+
type: "base",
|
|
164
|
+
fields: [
|
|
165
|
+
{ name: "title", type: "text", required: true },
|
|
166
|
+
{ name: "price", type: "number", required: true },
|
|
167
|
+
{ name: "in_stock", type: "bool" },
|
|
168
|
+
{ name: "tags", type: "json" },
|
|
169
|
+
],
|
|
170
|
+
rules: {
|
|
171
|
+
listRule: "", // Public
|
|
172
|
+
viewRule: "", // Public
|
|
173
|
+
createRule: "@request.auth.id != ''", // Authenticated users
|
|
174
|
+
updateRule: "@request.auth.id != ''",
|
|
175
|
+
deleteRule: null, // Admin only
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Update collection schema or rules
|
|
180
|
+
await ab.collections.update("products", {
|
|
181
|
+
fields: [
|
|
182
|
+
...newCollection.fields,
|
|
183
|
+
{ name: "sku", type: "text", unique: true },
|
|
184
|
+
],
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Delete collection
|
|
188
|
+
await ab.collections.delete("products");
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
### 4. Server Logs & Telemetry (Superuser / Admin)
|
|
194
|
+
|
|
195
|
+
Query server error logs, execution metrics, and latency timelines:
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
// Query recent server logs
|
|
199
|
+
const logs = await ab.logs.getList(1, 50, {
|
|
200
|
+
level: "ERROR",
|
|
201
|
+
search: "/api/collections",
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// Get log summary stats
|
|
205
|
+
const stats = await ab.logs.getStats();
|
|
206
|
+
console.log("Total errors:", stats.error, "Average latency:", stats.avgDurationMs, "ms");
|
|
207
|
+
|
|
208
|
+
// Get 7-day hourly request timeline
|
|
209
|
+
const timeline = await ab.logs.getTimeline();
|
|
210
|
+
|
|
211
|
+
// Clear logs
|
|
212
|
+
await ab.logs.clear();
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
### 5. Realtime Subscriptions
|
|
218
|
+
|
|
219
|
+
Subscribe to realtime create, update, and delete events via WebSocket / SSE:
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
// Subscribe to all changes in a collection
|
|
223
|
+
const unsub = await ab.collection("messages").subscribe("*", (e) => {
|
|
224
|
+
console.log("Action:", e.action); // 'create' | 'update' | 'delete'
|
|
225
|
+
console.log("Record:", e.record);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
// Subscribe to a specific record
|
|
229
|
+
const unsubRecord = await ab.collection("messages").subscribe("RECORD_ID", (e) => {
|
|
230
|
+
console.log("Message updated:", e.record);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Unsubscribe
|
|
234
|
+
unsub();
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
### 6. File Asset Helpers
|
|
240
|
+
|
|
241
|
+
Generate URLs for uploaded media files and static site assets:
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
// Record file URL helper
|
|
245
|
+
const fileUrl = ab.files.getUrl(record, record.avatar);
|
|
246
|
+
|
|
247
|
+
// Public website assets helper (served from _public)
|
|
248
|
+
const publicUrl = ab.files.getPublicUrl("images/hero.webp");
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
### 7. Serverless Hooks & Crons (Superuser / Admin)
|
|
254
|
+
|
|
255
|
+
Inspect and trigger serverless backend hooks:
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
// Get overview of loaded hook files, registered routes, and cron jobs
|
|
259
|
+
const overview = await ab.hooks.getOverview();
|
|
260
|
+
|
|
261
|
+
// Manually trigger a scheduled cron job
|
|
262
|
+
await ab.hooks.triggerCron("daily_cleanup");
|
|
263
|
+
|
|
264
|
+
// Reload hooks
|
|
265
|
+
await ab.hooks.reload();
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## License
|
|
271
|
+
|
|
272
|
+
MIT License.
|
package/dist/Client.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { BaseAuthStore } from "./stores/BaseAuthStore";
|
|
2
|
+
import { RecordService } from "./services/RecordService";
|
|
3
|
+
import { CollectionService } from "./services/CollectionService";
|
|
4
|
+
import { SuperuserService } from "./services/SuperuserService";
|
|
5
|
+
import { LogService } from "./services/LogService";
|
|
6
|
+
import { RealtimeService } from "./services/RealtimeService";
|
|
7
|
+
import { FileService } from "./services/FileService";
|
|
8
|
+
import { HooksService } from "./services/HooksService";
|
|
9
|
+
import type { SendOptions, RecordModel, CollectionModel, CommonOptions } from "./types";
|
|
10
|
+
export declare class AlsaBase {
|
|
11
|
+
baseUrl: string;
|
|
12
|
+
authStore: BaseAuthStore;
|
|
13
|
+
readonly superusers: SuperuserService;
|
|
14
|
+
readonly collections: CollectionService;
|
|
15
|
+
readonly logs: LogService;
|
|
16
|
+
readonly realtime: RealtimeService;
|
|
17
|
+
readonly files: FileService;
|
|
18
|
+
readonly hooks: HooksService;
|
|
19
|
+
private recordServices;
|
|
20
|
+
private cancelControllers;
|
|
21
|
+
constructor(baseUrl?: string, authStore?: BaseAuthStore);
|
|
22
|
+
/**
|
|
23
|
+
* Alias for superusers service (admins)
|
|
24
|
+
*/
|
|
25
|
+
get admins(): SuperuserService;
|
|
26
|
+
/**
|
|
27
|
+
* Returns a RecordService instance for the specified collection
|
|
28
|
+
*/
|
|
29
|
+
collection<T = RecordModel>(idOrName: string): RecordService<T>;
|
|
30
|
+
/**
|
|
31
|
+
* Returns the schema and column definitions for a table/collection (Requires Superuser authentication)
|
|
32
|
+
*/
|
|
33
|
+
getSchema(idOrName: string, options?: CommonOptions): Promise<CollectionModel>;
|
|
34
|
+
/**
|
|
35
|
+
* Returns the schema and column definitions for a table/collection (Requires Superuser authentication)
|
|
36
|
+
* Alias for getSchema()
|
|
37
|
+
*/
|
|
38
|
+
getTableSchema(idOrName: string, options?: CommonOptions): Promise<CollectionModel>;
|
|
39
|
+
/**
|
|
40
|
+
* Helper to format filter expression string with parameterized values
|
|
41
|
+
*/
|
|
42
|
+
filter(expr: string, params?: Record<string, any>): string;
|
|
43
|
+
/**
|
|
44
|
+
* Cancels a pending request with matching requestKey
|
|
45
|
+
*/
|
|
46
|
+
cancelRequest(requestKey: string): this;
|
|
47
|
+
/**
|
|
48
|
+
* Cancels all pending requests
|
|
49
|
+
*/
|
|
50
|
+
cancelAllRequests(): this;
|
|
51
|
+
/**
|
|
52
|
+
* Builds an absolute URL with query parameters
|
|
53
|
+
*/
|
|
54
|
+
buildUrl(path: string, query?: Record<string, any>): string;
|
|
55
|
+
/**
|
|
56
|
+
* Dispatches an HTTP request to the AlsaBase server
|
|
57
|
+
*/
|
|
58
|
+
send<T = any>(path: string, options?: SendOptions): Promise<T>;
|
|
59
|
+
}
|
|
60
|
+
export type Client = AlsaBase;
|
package/dist/Client.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { BaseAuthStore } from "./stores/BaseAuthStore";
|
|
2
|
+
import { LocalAuthStore } from "./stores/LocalAuthStore";
|
|
3
|
+
import { ClientResponseError } from "./ClientResponseError";
|
|
4
|
+
import { RecordService } from "./services/RecordService";
|
|
5
|
+
import { CollectionService } from "./services/CollectionService";
|
|
6
|
+
import { SuperuserService } from "./services/SuperuserService";
|
|
7
|
+
import { LogService } from "./services/LogService";
|
|
8
|
+
import { RealtimeService } from "./services/RealtimeService";
|
|
9
|
+
import { FileService } from "./services/FileService";
|
|
10
|
+
import { HooksService } from "./services/HooksService";
|
|
11
|
+
export class AlsaBase {
|
|
12
|
+
baseUrl;
|
|
13
|
+
authStore;
|
|
14
|
+
superusers;
|
|
15
|
+
collections;
|
|
16
|
+
logs;
|
|
17
|
+
realtime;
|
|
18
|
+
files;
|
|
19
|
+
hooks;
|
|
20
|
+
recordServices = new Map();
|
|
21
|
+
cancelControllers = new Map();
|
|
22
|
+
constructor(baseUrl = "/", authStore) {
|
|
23
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
24
|
+
this.authStore =
|
|
25
|
+
authStore ||
|
|
26
|
+
(typeof window !== "undefined" ? new LocalAuthStore() : new BaseAuthStore());
|
|
27
|
+
this.superusers = new SuperuserService(this);
|
|
28
|
+
this.collections = new CollectionService(this);
|
|
29
|
+
this.logs = new LogService(this);
|
|
30
|
+
this.realtime = new RealtimeService(this);
|
|
31
|
+
this.files = new FileService(this);
|
|
32
|
+
this.hooks = new HooksService(this);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Alias for superusers service (admins)
|
|
36
|
+
*/
|
|
37
|
+
get admins() {
|
|
38
|
+
return this.superusers;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Returns a RecordService instance for the specified collection
|
|
42
|
+
*/
|
|
43
|
+
collection(idOrName) {
|
|
44
|
+
if (!this.recordServices.has(idOrName)) {
|
|
45
|
+
this.recordServices.set(idOrName, new RecordService(this, idOrName));
|
|
46
|
+
}
|
|
47
|
+
return this.recordServices.get(idOrName);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Returns the schema and column definitions for a table/collection (Requires Superuser authentication)
|
|
51
|
+
*/
|
|
52
|
+
async getSchema(idOrName, options) {
|
|
53
|
+
return this.collections.getOne(idOrName, options);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Returns the schema and column definitions for a table/collection (Requires Superuser authentication)
|
|
57
|
+
* Alias for getSchema()
|
|
58
|
+
*/
|
|
59
|
+
async getTableSchema(idOrName, options) {
|
|
60
|
+
return this.collections.getOne(idOrName, options);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Helper to format filter expression string with parameterized values
|
|
64
|
+
*/
|
|
65
|
+
filter(expr, params = {}) {
|
|
66
|
+
if (!params || Object.keys(params).length === 0) {
|
|
67
|
+
return expr;
|
|
68
|
+
}
|
|
69
|
+
let result = expr;
|
|
70
|
+
for (const [key, val] of Object.entries(params)) {
|
|
71
|
+
let formattedVal;
|
|
72
|
+
if (val === null || val === undefined) {
|
|
73
|
+
formattedVal = "null";
|
|
74
|
+
}
|
|
75
|
+
else if (typeof val === "number" || typeof val === "boolean") {
|
|
76
|
+
formattedVal = String(val);
|
|
77
|
+
}
|
|
78
|
+
else if (val instanceof Date) {
|
|
79
|
+
formattedVal = `"${val.toISOString()}"`;
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
formattedVal = `"${String(val).replace(/"/g, '\\"')}"`;
|
|
83
|
+
}
|
|
84
|
+
const pattern = new RegExp(`{:?\\b${key}\\b}`, "g");
|
|
85
|
+
result = result.replace(pattern, formattedVal);
|
|
86
|
+
}
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Cancels a pending request with matching requestKey
|
|
91
|
+
*/
|
|
92
|
+
cancelRequest(requestKey) {
|
|
93
|
+
const controller = this.cancelControllers.get(requestKey);
|
|
94
|
+
if (controller) {
|
|
95
|
+
controller.abort();
|
|
96
|
+
this.cancelControllers.delete(requestKey);
|
|
97
|
+
}
|
|
98
|
+
return this;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Cancels all pending requests
|
|
102
|
+
*/
|
|
103
|
+
cancelAllRequests() {
|
|
104
|
+
for (const controller of this.cancelControllers.values()) {
|
|
105
|
+
controller.abort();
|
|
106
|
+
}
|
|
107
|
+
this.cancelControllers.clear();
|
|
108
|
+
return this;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Builds an absolute URL with query parameters
|
|
112
|
+
*/
|
|
113
|
+
buildUrl(path, query) {
|
|
114
|
+
const cleanPath = path.startsWith("/") ? path : `/${path}`;
|
|
115
|
+
let url = `${this.baseUrl}${cleanPath}`;
|
|
116
|
+
if (query && Object.keys(query).length > 0) {
|
|
117
|
+
const searchParams = new URLSearchParams();
|
|
118
|
+
for (const [k, v] of Object.entries(query)) {
|
|
119
|
+
if (v !== undefined && v !== null && v !== "") {
|
|
120
|
+
searchParams.append(k, typeof v === "object" ? JSON.stringify(v) : String(v));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const qs = searchParams.toString();
|
|
124
|
+
if (qs) {
|
|
125
|
+
url += (url.includes("?") ? "&" : "?") + qs;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return url;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Dispatches an HTTP request to the AlsaBase server
|
|
132
|
+
*/
|
|
133
|
+
async send(path, options = {}) {
|
|
134
|
+
const url = this.buildUrl(path, options.query || options.params);
|
|
135
|
+
// Auto-cancellation handling
|
|
136
|
+
let requestKey = options.requestKey;
|
|
137
|
+
if (requestKey === undefined && options.autoCancel !== false && (options.method === "GET" || !options.method)) {
|
|
138
|
+
requestKey = `${options.method || "GET"} ${url}`;
|
|
139
|
+
}
|
|
140
|
+
let controller;
|
|
141
|
+
if (requestKey) {
|
|
142
|
+
this.cancelRequest(requestKey);
|
|
143
|
+
controller = new AbortController();
|
|
144
|
+
this.cancelControllers.set(requestKey, controller);
|
|
145
|
+
}
|
|
146
|
+
const headers = {
|
|
147
|
+
...(options.headers || {}),
|
|
148
|
+
};
|
|
149
|
+
// Attach Authorization header if authenticated and not already provided
|
|
150
|
+
if (!headers["Authorization"] && !headers["authorization"] && this.authStore.token) {
|
|
151
|
+
headers["Authorization"] = `Bearer ${this.authStore.token}`;
|
|
152
|
+
}
|
|
153
|
+
let body = options.body;
|
|
154
|
+
// Auto stringify plain objects if not FormData/Blob/Buffer
|
|
155
|
+
if (body !== undefined &&
|
|
156
|
+
body !== null &&
|
|
157
|
+
typeof body === "object" &&
|
|
158
|
+
typeof body.append !== "function" &&
|
|
159
|
+
!(typeof Blob !== "undefined" && body instanceof Blob) &&
|
|
160
|
+
!(typeof ArrayBuffer !== "undefined" && body instanceof ArrayBuffer)) {
|
|
161
|
+
if (!headers["Content-Type"] && !headers["content-type"]) {
|
|
162
|
+
headers["Content-Type"] = "application/json";
|
|
163
|
+
}
|
|
164
|
+
body = JSON.stringify(body);
|
|
165
|
+
}
|
|
166
|
+
const fetchOptions = {
|
|
167
|
+
...options,
|
|
168
|
+
headers,
|
|
169
|
+
body,
|
|
170
|
+
signal: controller ? controller.signal : options.signal,
|
|
171
|
+
};
|
|
172
|
+
try {
|
|
173
|
+
const response = await fetch(url, fetchOptions);
|
|
174
|
+
if (requestKey) {
|
|
175
|
+
this.cancelControllers.delete(requestKey);
|
|
176
|
+
}
|
|
177
|
+
// Parse response JSON or text
|
|
178
|
+
let data = null;
|
|
179
|
+
const contentType = response.headers.get("content-type") || "";
|
|
180
|
+
if (contentType.includes("application/json")) {
|
|
181
|
+
data = await response.json().catch(() => null);
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
data = await response.text().catch(() => null);
|
|
185
|
+
}
|
|
186
|
+
if (!response.ok) {
|
|
187
|
+
throw new ClientResponseError({
|
|
188
|
+
url,
|
|
189
|
+
status: response.status,
|
|
190
|
+
data,
|
|
191
|
+
response,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
return data;
|
|
195
|
+
}
|
|
196
|
+
catch (err) {
|
|
197
|
+
if (requestKey) {
|
|
198
|
+
this.cancelControllers.delete(requestKey);
|
|
199
|
+
}
|
|
200
|
+
if (err.name === "AbortError") {
|
|
201
|
+
throw new ClientResponseError({
|
|
202
|
+
url,
|
|
203
|
+
status: 0,
|
|
204
|
+
data: { message: "The request was autocancelled or aborted." },
|
|
205
|
+
isAbort: true,
|
|
206
|
+
originalError: err,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
if (err instanceof ClientResponseError) {
|
|
210
|
+
throw err;
|
|
211
|
+
}
|
|
212
|
+
throw new ClientResponseError({
|
|
213
|
+
url,
|
|
214
|
+
status: 0,
|
|
215
|
+
data: { message: err.message },
|
|
216
|
+
originalError: err,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export declare class ClientResponseError extends Error {
|
|
2
|
+
url: string;
|
|
3
|
+
status: number;
|
|
4
|
+
response: {
|
|
5
|
+
[key: string]: any;
|
|
6
|
+
};
|
|
7
|
+
data: {
|
|
8
|
+
[key: string]: any;
|
|
9
|
+
};
|
|
10
|
+
isAbort: boolean;
|
|
11
|
+
originalError: any;
|
|
12
|
+
constructor(errData?: any);
|
|
13
|
+
toJSON(): {
|
|
14
|
+
url: string;
|
|
15
|
+
status: number;
|
|
16
|
+
data: {
|
|
17
|
+
[key: string]: any;
|
|
18
|
+
};
|
|
19
|
+
response: {
|
|
20
|
+
[key: string]: any;
|
|
21
|
+
};
|
|
22
|
+
isAbort: boolean;
|
|
23
|
+
message: string;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export class ClientResponseError extends Error {
|
|
2
|
+
url = "";
|
|
3
|
+
status = 0;
|
|
4
|
+
response = {};
|
|
5
|
+
data = {};
|
|
6
|
+
isAbort = false;
|
|
7
|
+
originalError = null;
|
|
8
|
+
constructor(errData) {
|
|
9
|
+
super("ClientResponseError");
|
|
10
|
+
if (errData !== null && typeof errData === "object") {
|
|
11
|
+
this.url = errData.url || "";
|
|
12
|
+
this.status = errData.status || 0;
|
|
13
|
+
this.data = errData.data || {};
|
|
14
|
+
this.response = errData.response || this.data;
|
|
15
|
+
this.isAbort = !!errData.isAbort;
|
|
16
|
+
this.originalError = errData.originalError || null;
|
|
17
|
+
if (errData.message) {
|
|
18
|
+
this.message = errData.message;
|
|
19
|
+
}
|
|
20
|
+
else if (this.data?.message) {
|
|
21
|
+
this.message = this.data.message;
|
|
22
|
+
}
|
|
23
|
+
else if (this.data?.error) {
|
|
24
|
+
this.message = this.data.error;
|
|
25
|
+
}
|
|
26
|
+
else if (this.status) {
|
|
27
|
+
this.message = `Response error. Status code: ${this.status}`;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (typeof DOMException !== "undefined" && errData instanceof DOMException && errData.name === "AbortError") {
|
|
31
|
+
this.isAbort = true;
|
|
32
|
+
this.message = "The request was autocancelled or aborted.";
|
|
33
|
+
}
|
|
34
|
+
Object.setPrototypeOf(this, ClientResponseError.prototype);
|
|
35
|
+
}
|
|
36
|
+
toJSON() {
|
|
37
|
+
return {
|
|
38
|
+
url: this.url,
|
|
39
|
+
status: this.status,
|
|
40
|
+
data: this.data,
|
|
41
|
+
response: this.response,
|
|
42
|
+
isAbort: this.isAbort,
|
|
43
|
+
message: this.message,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|