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 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.
@@ -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
+ }