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/dist/index.js ADDED
@@ -0,0 +1,1405 @@
1
+ // src/stores/BaseAuthStore.ts
2
+ var BaseAuthStore = class {
3
+ _token = "";
4
+ _model = null;
5
+ _listeners = /* @__PURE__ */ new Set();
6
+ get token() {
7
+ return this._token;
8
+ }
9
+ get model() {
10
+ return this._model;
11
+ }
12
+ get isValid() {
13
+ if (!this.token) return false;
14
+ const jwt = this.parseJwt(this.token);
15
+ if (!jwt || !jwt.exp) return true;
16
+ const now = Math.floor(Date.now() / 1e3);
17
+ return jwt.exp > now;
18
+ }
19
+ get isSuperuser() {
20
+ if (!this.model) return false;
21
+ return !!this.model.email && this.model.collectionName === void 0;
22
+ }
23
+ get isAdmin() {
24
+ return this.isSuperuser;
25
+ }
26
+ save(token, model) {
27
+ this._token = token || "";
28
+ this._model = model || null;
29
+ this.triggerChange();
30
+ }
31
+ clear() {
32
+ this._token = "";
33
+ this._model = null;
34
+ this.triggerChange();
35
+ }
36
+ onChange(callback) {
37
+ this._listeners.add(callback);
38
+ return () => {
39
+ this._listeners.delete(callback);
40
+ };
41
+ }
42
+ triggerChange() {
43
+ for (const listener of this._listeners) {
44
+ try {
45
+ listener(this._token, this._model);
46
+ } catch (err) {
47
+ console.error("AuthStore change listener error:", err);
48
+ }
49
+ }
50
+ }
51
+ parseJwt(token) {
52
+ if (!token) return null;
53
+ try {
54
+ const base64Url = token.split(".")[1];
55
+ if (!base64Url) return null;
56
+ const base64 = base64Url.replace(/-/g, "+").replace(/_/g, "/");
57
+ let jsonPayload;
58
+ if (typeof atob === "function") {
59
+ jsonPayload = decodeURIComponent(
60
+ atob(base64).split("").map((c) => "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2)).join("")
61
+ );
62
+ } else if (typeof globalThis.Buffer !== "undefined") {
63
+ jsonPayload = globalThis.Buffer.from(base64, "base64").toString("utf8");
64
+ } else {
65
+ return null;
66
+ }
67
+ return JSON.parse(jsonPayload);
68
+ } catch {
69
+ return null;
70
+ }
71
+ }
72
+ };
73
+
74
+ // src/stores/LocalAuthStore.ts
75
+ var DEFAULT_STORAGE_KEY = "alsabase_auth";
76
+ var LocalAuthStore = class extends BaseAuthStore {
77
+ storageKey;
78
+ constructor(storageKey = DEFAULT_STORAGE_KEY) {
79
+ super();
80
+ this.storageKey = storageKey;
81
+ this.loadInitial();
82
+ }
83
+ loadInitial() {
84
+ if (typeof window === "undefined" || !window.localStorage) return;
85
+ try {
86
+ const raw = window.localStorage.getItem(this.storageKey);
87
+ if (!raw) return;
88
+ const parsed = JSON.parse(raw);
89
+ if (parsed && typeof parsed === "object") {
90
+ this._token = parsed.token || "";
91
+ this._model = parsed.model || null;
92
+ }
93
+ } catch {
94
+ }
95
+ }
96
+ save(token, model) {
97
+ super.save(token, model);
98
+ if (typeof window !== "undefined" && window.localStorage) {
99
+ try {
100
+ window.localStorage.setItem(
101
+ this.storageKey,
102
+ JSON.stringify({ token: this._token, model: this._model })
103
+ );
104
+ } catch {
105
+ }
106
+ }
107
+ }
108
+ clear() {
109
+ super.clear();
110
+ if (typeof window !== "undefined" && window.localStorage) {
111
+ try {
112
+ window.localStorage.removeItem(this.storageKey);
113
+ } catch {
114
+ }
115
+ }
116
+ }
117
+ };
118
+
119
+ // src/ClientResponseError.ts
120
+ var ClientResponseError = class _ClientResponseError extends Error {
121
+ url = "";
122
+ status = 0;
123
+ response = {};
124
+ data = {};
125
+ isAbort = false;
126
+ originalError = null;
127
+ constructor(errData) {
128
+ super("ClientResponseError");
129
+ if (errData !== null && typeof errData === "object") {
130
+ this.url = errData.url || "";
131
+ this.status = errData.status || 0;
132
+ this.data = errData.data || {};
133
+ this.response = errData.response || this.data;
134
+ this.isAbort = !!errData.isAbort;
135
+ this.originalError = errData.originalError || null;
136
+ if (errData.message) {
137
+ this.message = errData.message;
138
+ } else if (this.data?.message) {
139
+ this.message = this.data.message;
140
+ } else if (this.data?.error) {
141
+ this.message = this.data.error;
142
+ } else if (this.status) {
143
+ this.message = `Response error. Status code: ${this.status}`;
144
+ }
145
+ }
146
+ if (typeof DOMException !== "undefined" && errData instanceof DOMException && errData.name === "AbortError") {
147
+ this.isAbort = true;
148
+ this.message = "The request was autocancelled or aborted.";
149
+ }
150
+ Object.setPrototypeOf(this, _ClientResponseError.prototype);
151
+ }
152
+ toJSON() {
153
+ return {
154
+ url: this.url,
155
+ status: this.status,
156
+ data: this.data,
157
+ response: this.response,
158
+ isAbort: this.isAbort,
159
+ message: this.message
160
+ };
161
+ }
162
+ };
163
+
164
+ // src/services/BaseService.ts
165
+ var BaseService = class {
166
+ client;
167
+ constructor(client) {
168
+ this.client = client;
169
+ }
170
+ async send(path, options) {
171
+ return this.client.send(path, options);
172
+ }
173
+ };
174
+
175
+ // src/services/RecordService.ts
176
+ var RecordService = class extends BaseService {
177
+ collectionIdOrName;
178
+ constructor(client, collectionIdOrName) {
179
+ super(client);
180
+ this.collectionIdOrName = collectionIdOrName;
181
+ }
182
+ /**
183
+ * Base API endpoint path for the collection records
184
+ */
185
+ get baseCrudPath() {
186
+ return `/api/collections/${encodeURIComponent(this.collectionIdOrName)}/records`;
187
+ }
188
+ /**
189
+ * Returns the current schema and column definitions of this table/collection (Requires Superuser authentication)
190
+ */
191
+ async getSchema(options) {
192
+ return this.client.collections.getOne(this.collectionIdOrName, options);
193
+ }
194
+ /**
195
+ * Returns the schema of this table/collection (Requires Superuser authentication)
196
+ * Alias for getSchema()
197
+ */
198
+ async schema(options) {
199
+ return this.getSchema(options);
200
+ }
201
+ /**
202
+ * Returns the schema of this table/collection (Requires Superuser authentication)
203
+ * Alias for getSchema()
204
+ */
205
+ async getTableSchema(options) {
206
+ return this.getSchema(options);
207
+ }
208
+ /**
209
+ * Returns a paginated list of records
210
+ */
211
+ async getList(page = 1, perPage = 30, options) {
212
+ const query = {
213
+ page,
214
+ limit: perPage,
215
+ ...options
216
+ };
217
+ return this.send(this.baseCrudPath, {
218
+ method: "GET",
219
+ query,
220
+ ...options
221
+ });
222
+ }
223
+ /**
224
+ * Returns a list of all records in batches
225
+ */
226
+ async getFullList(options) {
227
+ const batchSize = options?.batch || 200;
228
+ let page = 1;
229
+ let result = [];
230
+ let hasMore = true;
231
+ while (hasMore) {
232
+ const list = await this.getList(page, batchSize, {
233
+ ...options,
234
+ skipTotal: true
235
+ });
236
+ result = result.concat(list.items);
237
+ if (list.items.length < batchSize || list.totalPages && page >= list.totalPages) {
238
+ hasMore = false;
239
+ } else {
240
+ page++;
241
+ }
242
+ }
243
+ return result;
244
+ }
245
+ /**
246
+ * Returns the first record matching the specified filter expression
247
+ */
248
+ async getFirstListItem(filter, options) {
249
+ const list = await this.getList(1, 1, {
250
+ ...options,
251
+ filter
252
+ });
253
+ if (!list.items || list.items.length === 0) {
254
+ throw new Error(`Record not found for filter: ${filter}`);
255
+ }
256
+ return list.items[0];
257
+ }
258
+ /**
259
+ * Returns a single record by its ID
260
+ */
261
+ async getOne(id, options) {
262
+ return this.send(`${this.baseCrudPath}/${encodeURIComponent(id)}`, {
263
+ method: "GET",
264
+ ...options
265
+ });
266
+ }
267
+ /**
268
+ * Creates a new record in the collection
269
+ */
270
+ async create(body, options) {
271
+ return this.send(this.baseCrudPath, {
272
+ method: "POST",
273
+ body,
274
+ ...options
275
+ });
276
+ }
277
+ /**
278
+ * Updates an existing record by its ID
279
+ */
280
+ async update(id, body, options) {
281
+ return this.send(`${this.baseCrudPath}/${encodeURIComponent(id)}`, {
282
+ method: "PATCH",
283
+ body,
284
+ ...options
285
+ });
286
+ }
287
+ /**
288
+ * Deletes a record by its ID
289
+ */
290
+ async delete(id, options) {
291
+ await this.send(`${this.baseCrudPath}/${encodeURIComponent(id)}`, {
292
+ method: "DELETE",
293
+ ...options
294
+ });
295
+ return true;
296
+ }
297
+ /**
298
+ * Truncates (deletes all records) in the collection (Requires superuser access)
299
+ */
300
+ async truncate(options) {
301
+ await this.send(
302
+ `/api/collections/${encodeURIComponent(this.collectionIdOrName)}/truncate`,
303
+ {
304
+ method: "DELETE",
305
+ ...options
306
+ }
307
+ );
308
+ return true;
309
+ }
310
+ // --- Auth Methods ---
311
+ /**
312
+ * Authenticates a user with username/email and password
313
+ */
314
+ async authWithPassword(identity, password, options) {
315
+ const res = await this.send("/api/auth/users/login", {
316
+ method: "POST",
317
+ body: { identity, password },
318
+ ...options
319
+ });
320
+ const authResponse = {
321
+ token: res.token,
322
+ record: res.user || res.record,
323
+ meta: res.meta
324
+ };
325
+ this.client.authStore.save(authResponse.token, authResponse.record);
326
+ return authResponse;
327
+ }
328
+ /**
329
+ * Authenticates a user with a one-time password (OTP)
330
+ */
331
+ async authWithOTP(otp, email, options) {
332
+ const res = await this.send("/api/auth/verify-otp", {
333
+ method: "POST",
334
+ body: { otp, email },
335
+ ...options
336
+ });
337
+ const authResponse = {
338
+ token: res.token,
339
+ record: res.user || res.record,
340
+ meta: res.meta
341
+ };
342
+ this.client.authStore.save(authResponse.token, authResponse.record);
343
+ return authResponse;
344
+ }
345
+ /**
346
+ * Sends an OTP verification email to the user
347
+ */
348
+ async requestOTP(email, options) {
349
+ return this.send("/api/auth/request-otp", {
350
+ method: "POST",
351
+ body: { email },
352
+ ...options
353
+ });
354
+ }
355
+ /**
356
+ * Refreshes the currently authenticated record token and profile
357
+ */
358
+ async authRefresh(options) {
359
+ const user = await this.send("/api/auth/users/me", {
360
+ method: "GET",
361
+ ...options
362
+ });
363
+ const token = this.client.authStore.token;
364
+ const authResponse = {
365
+ token,
366
+ record: user
367
+ };
368
+ this.client.authStore.save(token, user);
369
+ return authResponse;
370
+ }
371
+ /**
372
+ * Sends a password reset email
373
+ */
374
+ async requestPasswordReset(email, options) {
375
+ await this.send("/api/auth/request-password-reset", {
376
+ method: "POST",
377
+ body: { email },
378
+ ...options
379
+ });
380
+ return true;
381
+ }
382
+ /**
383
+ * Confirms a password reset request with a token and new password
384
+ */
385
+ async confirmPasswordReset(token, password, _passwordConfirm, options) {
386
+ await this.send("/api/auth/confirm-password-reset", {
387
+ method: "POST",
388
+ body: { token, password },
389
+ ...options
390
+ });
391
+ return true;
392
+ }
393
+ /**
394
+ * Sends an email verification request
395
+ */
396
+ async requestVerification(email, options) {
397
+ await this.send("/api/auth/request-verification", {
398
+ method: "POST",
399
+ body: { email },
400
+ ...options
401
+ });
402
+ return true;
403
+ }
404
+ /**
405
+ * Confirms an email verification request
406
+ */
407
+ async confirmVerification(token, options) {
408
+ await this.send("/api/auth/confirm-verification", {
409
+ method: "POST",
410
+ body: { token },
411
+ ...options
412
+ });
413
+ return true;
414
+ }
415
+ /**
416
+ * Sends an email change verification request
417
+ */
418
+ async requestEmailChange(newEmail, options) {
419
+ await this.send("/api/auth/request-email-change", {
420
+ method: "POST",
421
+ body: { newEmail },
422
+ ...options
423
+ });
424
+ return true;
425
+ }
426
+ /**
427
+ * Confirms an email change request
428
+ */
429
+ async confirmEmailChange(token, password, options) {
430
+ await this.send("/api/auth/confirm-email-change", {
431
+ method: "POST",
432
+ body: { token, password },
433
+ ...options
434
+ });
435
+ return true;
436
+ }
437
+ // --- Realtime Subscriptions ---
438
+ /**
439
+ * Subscribes to realtime events for this collection or a specific record ID
440
+ */
441
+ async subscribe(topicOrListener, listener) {
442
+ let topic;
443
+ let cb;
444
+ if (typeof topicOrListener === "function") {
445
+ topic = this.collectionIdOrName;
446
+ cb = topicOrListener;
447
+ } else {
448
+ topic = `${this.collectionIdOrName}/${topicOrListener}`;
449
+ cb = listener;
450
+ }
451
+ return this.client.realtime.subscribe(topic, cb);
452
+ }
453
+ /**
454
+ * Unsubscribes from realtime events for this collection or a specific record ID
455
+ */
456
+ async unsubscribe(topic) {
457
+ const fullTopic = topic ? `${this.collectionIdOrName}/${topic}` : this.collectionIdOrName;
458
+ return this.client.realtime.unsubscribe(fullTopic);
459
+ }
460
+ };
461
+
462
+ // src/services/CollectionService.ts
463
+ var CollectionService = class extends BaseService {
464
+ /**
465
+ * Returns a list of all collections (Superuser required)
466
+ */
467
+ async getFullList(options) {
468
+ const res = await this.send(
469
+ "/api/collections",
470
+ {
471
+ method: "GET",
472
+ ...options
473
+ }
474
+ );
475
+ return res.items || [];
476
+ }
477
+ /**
478
+ * Returns a paginated list of collections
479
+ */
480
+ async getList(page = 1, perPage = 30, options) {
481
+ const all = await this.getFullList(options);
482
+ const start = (page - 1) * perPage;
483
+ const end = start + perPage;
484
+ const items = all.slice(start, end);
485
+ const total = all.length;
486
+ const totalPages = Math.ceil(total / perPage) || 1;
487
+ return {
488
+ page,
489
+ perPage,
490
+ totalItems: total,
491
+ totalPages,
492
+ items
493
+ };
494
+ }
495
+ /**
496
+ * Returns a single collection schema by its name or ID (Superuser required)
497
+ */
498
+ async getOne(idOrName, options) {
499
+ return this.send(
500
+ `/api/collections/${encodeURIComponent(idOrName)}/schema`,
501
+ {
502
+ method: "GET",
503
+ ...options
504
+ }
505
+ );
506
+ }
507
+ /**
508
+ * Returns the schema and column definitions of a table/collection (Superuser required)
509
+ * Alias for getOne()
510
+ */
511
+ async getSchema(idOrName, options) {
512
+ return this.getOne(idOrName, options);
513
+ }
514
+ /**
515
+ * Returns the schema of a table/collection (Superuser required)
516
+ * Alias for getOne()
517
+ */
518
+ async getTableSchema(idOrName, options) {
519
+ return this.getOne(idOrName, options);
520
+ }
521
+ /**
522
+ * Creates a new collection schema
523
+ */
524
+ async create(data, options) {
525
+ return this.send("/api/collections", {
526
+ method: "POST",
527
+ body: data,
528
+ ...options
529
+ });
530
+ }
531
+ /**
532
+ * Updates an existing collection schema
533
+ */
534
+ async update(idOrName, data, options) {
535
+ return this.send(
536
+ `/api/collections/${encodeURIComponent(idOrName)}`,
537
+ {
538
+ method: "PATCH",
539
+ body: data,
540
+ ...options
541
+ }
542
+ );
543
+ }
544
+ /**
545
+ * Deletes a collection schema
546
+ */
547
+ async delete(idOrName, options) {
548
+ await this.send(`/api/collections/${encodeURIComponent(idOrName)}`, {
549
+ method: "DELETE",
550
+ ...options
551
+ });
552
+ return true;
553
+ }
554
+ /**
555
+ * Truncates all records in a collection
556
+ */
557
+ async truncate(idOrName, options) {
558
+ await this.send(
559
+ `/api/collections/${encodeURIComponent(idOrName)}/truncate`,
560
+ {
561
+ method: "DELETE",
562
+ ...options
563
+ }
564
+ );
565
+ return true;
566
+ }
567
+ };
568
+
569
+ // src/services/SuperuserService.ts
570
+ var SuperuserService = class extends BaseService {
571
+ /**
572
+ * Authenticates a superuser with email and password
573
+ */
574
+ async authWithPassword(email, password, options) {
575
+ const res = await this.send(
576
+ "/api/auth/superusers/login",
577
+ {
578
+ method: "POST",
579
+ body: { email, password },
580
+ ...options
581
+ }
582
+ );
583
+ this.client.authStore.save(res.token, res.user);
584
+ return res;
585
+ }
586
+ /**
587
+ * Checks if an initial superuser exists in the system
588
+ */
589
+ async hasInitialSuperuser(options) {
590
+ return this.send(
591
+ "/api/auth/superusers/has-initial",
592
+ {
593
+ method: "GET",
594
+ ...options
595
+ }
596
+ );
597
+ }
598
+ /**
599
+ * Sets up the first superuser account (Only available when no superusers exist)
600
+ */
601
+ async setupInitialSuperuser(email, password, options) {
602
+ const res = await this.send(
603
+ "/api/auth/superusers/setup",
604
+ {
605
+ method: "POST",
606
+ body: { email, password },
607
+ ...options
608
+ }
609
+ );
610
+ this.client.authStore.save(res.token, res.user);
611
+ return res;
612
+ }
613
+ /**
614
+ * Returns current authenticated superuser profile
615
+ */
616
+ async getMe(options) {
617
+ return this.send("/api/auth/superusers/me", {
618
+ method: "GET",
619
+ ...options
620
+ });
621
+ }
622
+ /**
623
+ * Refreshes superuser auth state
624
+ */
625
+ async authRefresh(options) {
626
+ const user = await this.getMe(options);
627
+ const token = this.client.authStore.token;
628
+ const res = {
629
+ token,
630
+ user
631
+ };
632
+ this.client.authStore.save(token, user);
633
+ return res;
634
+ }
635
+ /**
636
+ * Requests a password reset email for superuser
637
+ */
638
+ async requestPasswordReset(email, options) {
639
+ await this.send("/api/auth/request-password-reset", {
640
+ method: "POST",
641
+ body: { email },
642
+ ...options
643
+ });
644
+ return true;
645
+ }
646
+ /**
647
+ * Confirms a password reset with token
648
+ */
649
+ async confirmPasswordReset(token, password, _passwordConfirm, options) {
650
+ await this.send("/api/auth/confirm-password-reset", {
651
+ method: "POST",
652
+ body: { token, password },
653
+ ...options
654
+ });
655
+ return true;
656
+ }
657
+ };
658
+
659
+ // src/services/LogService.ts
660
+ var LogService = class extends BaseService {
661
+ /**
662
+ * Returns a paginated list of system access and error logs (Superuser required)
663
+ */
664
+ async getList(page = 1, perPage = 50, options) {
665
+ const query = {
666
+ page,
667
+ limit: perPage,
668
+ ...options
669
+ };
670
+ const res = await this.send("/api/logs", {
671
+ method: "GET",
672
+ query,
673
+ ...options
674
+ });
675
+ return {
676
+ page: res.page,
677
+ perPage: res.limit,
678
+ totalItems: res.total,
679
+ totalPages: res.totalPages,
680
+ items: res.items
681
+ };
682
+ }
683
+ /**
684
+ * Returns timeline distribution of system requests and errors
685
+ */
686
+ async getTimeline(options) {
687
+ return this.send("/api/logs/timeline", {
688
+ method: "GET",
689
+ ...options
690
+ });
691
+ }
692
+ /**
693
+ * Returns summary stats for server logs
694
+ */
695
+ async getStats(options) {
696
+ return this.send("/api/logs/stats", {
697
+ method: "GET",
698
+ ...options
699
+ });
700
+ }
701
+ /**
702
+ * Deletes a batch of logs by IDs or all matching filter criteria
703
+ */
704
+ async deleteBatch(params, options) {
705
+ return this.send("/api/logs/delete-batch", {
706
+ method: "POST",
707
+ body: params,
708
+ ...options
709
+ });
710
+ }
711
+ /**
712
+ * Clears all system logs
713
+ */
714
+ async clear(options) {
715
+ await this.send("/api/logs", {
716
+ method: "DELETE",
717
+ ...options
718
+ });
719
+ return true;
720
+ }
721
+ };
722
+
723
+ // src/services/RealtimeService.ts
724
+ import { io } from "socket.io-client";
725
+ var RealtimeService = class extends BaseService {
726
+ subscriptions = /* @__PURE__ */ new Map();
727
+ socket = null;
728
+ isConnecting = false;
729
+ /**
730
+ * Checks if realtime is currently connected
731
+ */
732
+ get isConnected() {
733
+ return !!(this.socket && this.socket.connected);
734
+ }
735
+ /**
736
+ * Subscribes a listener callback to a specific topic or collection
737
+ */
738
+ async subscribe(topic, listener) {
739
+ const trimmedTopic = (topic || "*").trim();
740
+ if (!this.subscriptions.has(trimmedTopic)) {
741
+ this.subscriptions.set(trimmedTopic, /* @__PURE__ */ new Set());
742
+ }
743
+ this.subscriptions.get(trimmedTopic).add(listener);
744
+ this.ensureConnection();
745
+ if (this.socket && this.socket.connected) {
746
+ this.socket.emit("subscribe", trimmedTopic);
747
+ }
748
+ return () => {
749
+ this.unsubscribeFromTopic(trimmedTopic, listener);
750
+ };
751
+ }
752
+ /**
753
+ * Unsubscribes all listeners or a specific listener from a topic
754
+ */
755
+ async unsubscribe(topic) {
756
+ if (!topic) {
757
+ if (this.socket && this.socket.connected && this.subscriptions.size > 0) {
758
+ this.socket.emit("unsubscribe", Array.from(this.subscriptions.keys()));
759
+ }
760
+ this.subscriptions.clear();
761
+ this.disconnect();
762
+ return;
763
+ }
764
+ const trimmedTopic = topic.trim();
765
+ this.subscriptions.delete(trimmedTopic);
766
+ if (this.socket && this.socket.connected) {
767
+ this.socket.emit("unsubscribe", trimmedTopic);
768
+ }
769
+ if (this.subscriptions.size === 0) {
770
+ this.disconnect();
771
+ }
772
+ }
773
+ /**
774
+ * Publishes a custom event to a realtime topic
775
+ */
776
+ async publish(topic, data, event) {
777
+ if (this.socket && this.socket.connected) {
778
+ this.socket.emit("publish", { topic, data, event });
779
+ return;
780
+ }
781
+ await this.send("/api/realtime/publish", {
782
+ method: "POST",
783
+ body: {
784
+ topic,
785
+ data,
786
+ event
787
+ }
788
+ });
789
+ }
790
+ unsubscribeFromTopic(topic, listener) {
791
+ const set = this.subscriptions.get(topic);
792
+ if (set) {
793
+ set.delete(listener);
794
+ if (set.size === 0) {
795
+ this.subscriptions.delete(topic);
796
+ if (this.socket && this.socket.connected) {
797
+ this.socket.emit("unsubscribe", topic);
798
+ }
799
+ }
800
+ }
801
+ if (this.subscriptions.size === 0) {
802
+ this.disconnect();
803
+ }
804
+ }
805
+ ensureConnection() {
806
+ if (this.isConnecting || this.isConnected) return;
807
+ this.connect();
808
+ }
809
+ connect() {
810
+ if (this.socket) return;
811
+ this.isConnecting = true;
812
+ const socketUrl = this.client.baseUrl;
813
+ this.socket = io(socketUrl, {
814
+ path: "/api/socket.io",
815
+ auth: {
816
+ token: this.client.authStore.token
817
+ },
818
+ transports: ["websocket", "polling"],
819
+ reconnection: true,
820
+ reconnectionAttempts: Infinity,
821
+ reconnectionDelay: 1e3,
822
+ reconnectionDelayMax: 5e3
823
+ });
824
+ this.socket.on("connect", () => {
825
+ this.isConnecting = false;
826
+ const topics = Array.from(this.subscriptions.keys());
827
+ if (topics.length > 0) {
828
+ this.socket.emit("subscribe", topics);
829
+ }
830
+ });
831
+ this.socket.on("disconnect", () => {
832
+ this.isConnecting = false;
833
+ });
834
+ this.socket.on("connect_error", () => {
835
+ this.isConnecting = false;
836
+ });
837
+ this.socket.on("logs", (data) => {
838
+ const logData = data?.log || data;
839
+ this.dispatchMessage(logData, "logs");
840
+ });
841
+ this.socket.on("log", (data) => {
842
+ const logData = data?.log || data;
843
+ this.dispatchMessage(logData, "logs");
844
+ });
845
+ this.socket.on("record", (data) => {
846
+ const collection = data?.collection;
847
+ this.dispatchMessage(data, collection);
848
+ });
849
+ this.socket.onAny((eventName, ...args) => {
850
+ if ([
851
+ "connect",
852
+ "disconnect",
853
+ "connect_error",
854
+ "connected",
855
+ "subscriptions",
856
+ "pong"
857
+ ].includes(eventName)) {
858
+ return;
859
+ }
860
+ const data = args[0];
861
+ this.dispatchMessage(data, eventName);
862
+ });
863
+ }
864
+ dispatchMessage(data, explicitTopic) {
865
+ if (!data) return;
866
+ const collection = explicitTopic || data.collection || data.topic;
867
+ const recordId = data.record?.id;
868
+ for (const [topic, listeners] of this.subscriptions.entries()) {
869
+ let isMatch = false;
870
+ if (topic === "*" || topic === collection) {
871
+ isMatch = true;
872
+ } else if (recordId && topic === `${collection}/${recordId}`) {
873
+ isMatch = true;
874
+ } else if (topic === `${collection}/*`) {
875
+ isMatch = true;
876
+ }
877
+ if (isMatch) {
878
+ for (const listener of listeners) {
879
+ try {
880
+ listener(data);
881
+ } catch (err) {
882
+ console.error("[AlsaBase Realtime] Listener error:", err);
883
+ }
884
+ }
885
+ }
886
+ }
887
+ }
888
+ disconnect() {
889
+ this.isConnecting = false;
890
+ if (this.socket) {
891
+ try {
892
+ this.socket.disconnect();
893
+ } catch {
894
+ }
895
+ this.socket = null;
896
+ }
897
+ }
898
+ };
899
+
900
+ // src/services/FileService.ts
901
+ var FileService = class extends BaseService {
902
+ /**
903
+ * Generates a URL for accessing an uploaded record file asset
904
+ *
905
+ * @param record Record object or record ID string
906
+ * @param filename Filename of the uploaded asset
907
+ * @param queryParams Optional query parameters (e.g. thumb, download)
908
+ */
909
+ getUrl(record, filename, queryParams) {
910
+ if (!filename) return "";
911
+ let fileUrlPath;
912
+ if (typeof record === "object" && record !== null) {
913
+ const col = record.collectionName || record.collectionId || "";
914
+ const id = record.id || "";
915
+ if (col && id) {
916
+ fileUrlPath = `/api/static-files/file/${encodeURIComponent(col)}/${encodeURIComponent(id)}/${encodeURIComponent(filename)}`;
917
+ } else if (id) {
918
+ fileUrlPath = `/api/static-files/file/${encodeURIComponent(id)}/${encodeURIComponent(filename)}`;
919
+ } else {
920
+ fileUrlPath = `/api/static-files/file/${encodeURIComponent(filename)}`;
921
+ }
922
+ } else if (typeof record === "string" && record) {
923
+ fileUrlPath = `/api/static-files/file/${encodeURIComponent(record)}/${encodeURIComponent(filename)}`;
924
+ } else {
925
+ fileUrlPath = `/api/static-files/file/${encodeURIComponent(filename)}`;
926
+ }
927
+ return this.client.buildUrl(fileUrlPath, queryParams);
928
+ }
929
+ /**
930
+ * Generates a URL for a public website static asset hosted in _public
931
+ *
932
+ * @param relPath Relative path inside _public (e.g. 'images/banner.png' or 'maps/mafia/lost-heaven/')
933
+ */
934
+ getPublicUrl(relPath) {
935
+ const clean = relPath.replace(/^[\/\\]+/, "");
936
+ return `${this.client.baseUrl.replace(/\/+$/, "")}/${clean}`;
937
+ }
938
+ /**
939
+ * Lists all files in the _public directory
940
+ */
941
+ async listPublicFiles(options) {
942
+ return this.send("/api/static-files", {
943
+ method: "GET",
944
+ ...options
945
+ });
946
+ }
947
+ /**
948
+ * Gets a folder tree for the _public directory
949
+ */
950
+ async getPublicTree(dir = "", options) {
951
+ return this.send(`/api/static-files/tree?dir=${encodeURIComponent(dir)}`, {
952
+ method: "GET",
953
+ ...options
954
+ });
955
+ }
956
+ /**
957
+ * Reads a file's content from the _public directory
958
+ */
959
+ async readPublicFile(path, options) {
960
+ return this.send(`/api/static-files/file?path=${encodeURIComponent(path)}`, {
961
+ method: "GET",
962
+ ...options
963
+ });
964
+ }
965
+ /**
966
+ * Saves or updates a file in the _public directory (Superuser required)
967
+ */
968
+ async savePublicFile(name, content, isBase64 = false, options) {
969
+ return this.send("/api/static-files/file", {
970
+ method: "POST",
971
+ body: { name, content, isBase64 },
972
+ ...options
973
+ });
974
+ }
975
+ /**
976
+ * Uploads a batch of files to the _public directory (Superuser required)
977
+ */
978
+ async uploadPublicBatch(files, options) {
979
+ return this.send("/api/static-files/upload-batch", {
980
+ method: "POST",
981
+ body: { files },
982
+ ...options
983
+ });
984
+ }
985
+ /**
986
+ * Deletes a file in the _public directory (Superuser required)
987
+ */
988
+ async deletePublicFile(path, options) {
989
+ return this.send(`/api/static-files/file?path=${encodeURIComponent(path)}`, {
990
+ method: "DELETE",
991
+ ...options
992
+ });
993
+ }
994
+ /**
995
+ * Creates a folder inside the _public directory (Superuser required)
996
+ */
997
+ async createPublicFolder(path, options) {
998
+ return this.send("/api/static-files/folder", {
999
+ method: "POST",
1000
+ body: { path },
1001
+ ...options
1002
+ });
1003
+ }
1004
+ /**
1005
+ * Deletes a folder and all its contents inside the _public directory (Superuser required)
1006
+ */
1007
+ async deletePublicFolder(path, options) {
1008
+ return this.send(`/api/static-files/folder?path=${encodeURIComponent(path)}`, {
1009
+ method: "DELETE",
1010
+ ...options
1011
+ });
1012
+ }
1013
+ };
1014
+
1015
+ // src/services/HooksService.ts
1016
+ var HooksService = class extends BaseService {
1017
+ /**
1018
+ * Returns an overview of all active hooks, routes, crons, and commands (Superuser required)
1019
+ */
1020
+ async getOverview(options) {
1021
+ return this.send("/api/hooks", {
1022
+ method: "GET",
1023
+ ...options
1024
+ });
1025
+ }
1026
+ /**
1027
+ * Lists all files in the _hooks directory (Superuser required)
1028
+ */
1029
+ async listFiles(options) {
1030
+ return this.send("/api/hooks/files", {
1031
+ method: "GET",
1032
+ ...options
1033
+ });
1034
+ }
1035
+ /**
1036
+ * Gets a folder tree for the _hooks directory (Superuser required)
1037
+ */
1038
+ async getTree(dir = "", options) {
1039
+ return this.send(`/api/hooks/tree?dir=${encodeURIComponent(dir)}`, {
1040
+ method: "GET",
1041
+ ...options
1042
+ });
1043
+ }
1044
+ /**
1045
+ * Reads a file's content from the _hooks directory (Superuser required)
1046
+ */
1047
+ async readFile(path, options) {
1048
+ return this.send(`/api/hooks/files/file?path=${encodeURIComponent(path)}`, {
1049
+ method: "GET",
1050
+ ...options
1051
+ });
1052
+ }
1053
+ /**
1054
+ * Saves or updates a file in the _hooks directory (Superuser required)
1055
+ */
1056
+ async saveFile(name, content, isBase64 = false, options) {
1057
+ return this.send("/api/hooks/files", {
1058
+ method: "POST",
1059
+ body: { name, content, isBase64 },
1060
+ ...options
1061
+ });
1062
+ }
1063
+ /**
1064
+ * Uploads a batch of files to the _hooks directory (Superuser required)
1065
+ */
1066
+ async uploadBatch(files, options) {
1067
+ return this.send("/api/hooks/files/upload-batch", {
1068
+ method: "POST",
1069
+ body: { files },
1070
+ ...options
1071
+ });
1072
+ }
1073
+ /**
1074
+ * Deletes a file in the _hooks directory (Superuser required)
1075
+ */
1076
+ async deleteFile(path, options) {
1077
+ return this.send(`/api/hooks/files/file?path=${encodeURIComponent(path)}`, {
1078
+ method: "DELETE",
1079
+ ...options
1080
+ });
1081
+ }
1082
+ /**
1083
+ * Creates a folder inside the _hooks directory (Superuser required)
1084
+ */
1085
+ async createFolder(path, options) {
1086
+ return this.send("/api/hooks/files/folder", {
1087
+ method: "POST",
1088
+ body: { path },
1089
+ ...options
1090
+ });
1091
+ }
1092
+ /**
1093
+ * Deletes a folder and all its contents inside the _hooks directory (Superuser required)
1094
+ */
1095
+ async deleteFolder(path, options) {
1096
+ return this.send(`/api/hooks/files/folder?path=${encodeURIComponent(path)}`, {
1097
+ method: "DELETE",
1098
+ ...options
1099
+ });
1100
+ }
1101
+ /**
1102
+ * Manually triggers execution of a scheduled cron job (Superuser required)
1103
+ */
1104
+ async triggerCron(name, options) {
1105
+ return this.send(`/api/hooks/cron/${encodeURIComponent(name)}/trigger`, {
1106
+ method: "POST",
1107
+ ...options
1108
+ });
1109
+ }
1110
+ /**
1111
+ * Cancels a running cron job (Superuser required)
1112
+ */
1113
+ async cancelCron(executionId, name, options) {
1114
+ return this.send("/api/hooks/cron/cancel", {
1115
+ method: "POST",
1116
+ body: { executionId, name },
1117
+ ...options
1118
+ });
1119
+ }
1120
+ /**
1121
+ * Runs a custom hook CLI command (Superuser required)
1122
+ */
1123
+ async runCommand(name, args, options) {
1124
+ return this.send(`/api/hooks/commands/${encodeURIComponent(name)}/run`, {
1125
+ method: "POST",
1126
+ body: { args },
1127
+ ...options
1128
+ });
1129
+ }
1130
+ /**
1131
+ * Cancels a running CLI command (Superuser required)
1132
+ */
1133
+ async cancelCommand(executionId, name, options) {
1134
+ return this.send("/api/hooks/commands/cancel", {
1135
+ method: "POST",
1136
+ body: { executionId, name },
1137
+ ...options
1138
+ });
1139
+ }
1140
+ /**
1141
+ * Reloads all hook files and re-registers custom routes (Superuser required)
1142
+ */
1143
+ async reload(options) {
1144
+ return this.send("/api/hooks/reload", {
1145
+ method: "POST",
1146
+ ...options
1147
+ });
1148
+ }
1149
+ };
1150
+
1151
+ // src/Client.ts
1152
+ var AlsaBase = class {
1153
+ baseUrl;
1154
+ authStore;
1155
+ superusers;
1156
+ collections;
1157
+ logs;
1158
+ realtime;
1159
+ files;
1160
+ hooks;
1161
+ recordServices = /* @__PURE__ */ new Map();
1162
+ cancelControllers = /* @__PURE__ */ new Map();
1163
+ constructor(baseUrl = "/", authStore) {
1164
+ this.baseUrl = baseUrl.replace(/\/+$/, "");
1165
+ this.authStore = authStore || (typeof window !== "undefined" ? new LocalAuthStore() : new BaseAuthStore());
1166
+ this.superusers = new SuperuserService(this);
1167
+ this.collections = new CollectionService(this);
1168
+ this.logs = new LogService(this);
1169
+ this.realtime = new RealtimeService(this);
1170
+ this.files = new FileService(this);
1171
+ this.hooks = new HooksService(this);
1172
+ }
1173
+ /**
1174
+ * Alias for superusers service (admins)
1175
+ */
1176
+ get admins() {
1177
+ return this.superusers;
1178
+ }
1179
+ /**
1180
+ * Returns a RecordService instance for the specified collection
1181
+ */
1182
+ collection(idOrName) {
1183
+ if (!this.recordServices.has(idOrName)) {
1184
+ this.recordServices.set(
1185
+ idOrName,
1186
+ new RecordService(this, idOrName)
1187
+ );
1188
+ }
1189
+ return this.recordServices.get(idOrName);
1190
+ }
1191
+ /**
1192
+ * Returns the schema and column definitions for a table/collection (Requires Superuser authentication)
1193
+ */
1194
+ async getSchema(idOrName, options) {
1195
+ return this.collections.getOne(idOrName, options);
1196
+ }
1197
+ /**
1198
+ * Returns the schema and column definitions for a table/collection (Requires Superuser authentication)
1199
+ * Alias for getSchema()
1200
+ */
1201
+ async getTableSchema(idOrName, options) {
1202
+ return this.collections.getOne(idOrName, options);
1203
+ }
1204
+ /**
1205
+ * Helper to format filter expression string with parameterized values
1206
+ */
1207
+ filter(expr, params = {}) {
1208
+ if (!params || Object.keys(params).length === 0) {
1209
+ return expr;
1210
+ }
1211
+ let result = expr;
1212
+ for (const [key, val] of Object.entries(params)) {
1213
+ let formattedVal;
1214
+ if (val === null || val === void 0) {
1215
+ formattedVal = "null";
1216
+ } else if (typeof val === "number" || typeof val === "boolean") {
1217
+ formattedVal = String(val);
1218
+ } else if (val instanceof Date) {
1219
+ formattedVal = `"${val.toISOString()}"`;
1220
+ } else {
1221
+ formattedVal = `"${String(val).replace(/"/g, '\\"')}"`;
1222
+ }
1223
+ const pattern = new RegExp(`{:?\\b${key}\\b}`, "g");
1224
+ result = result.replace(pattern, formattedVal);
1225
+ }
1226
+ return result;
1227
+ }
1228
+ /**
1229
+ * Cancels a pending request with matching requestKey
1230
+ */
1231
+ cancelRequest(requestKey) {
1232
+ const controller = this.cancelControllers.get(requestKey);
1233
+ if (controller) {
1234
+ controller.abort();
1235
+ this.cancelControllers.delete(requestKey);
1236
+ }
1237
+ return this;
1238
+ }
1239
+ /**
1240
+ * Cancels all pending requests
1241
+ */
1242
+ cancelAllRequests() {
1243
+ for (const controller of this.cancelControllers.values()) {
1244
+ controller.abort();
1245
+ }
1246
+ this.cancelControllers.clear();
1247
+ return this;
1248
+ }
1249
+ /**
1250
+ * Builds an absolute URL with query parameters
1251
+ */
1252
+ buildUrl(path, query) {
1253
+ const cleanPath = path.startsWith("/") ? path : `/${path}`;
1254
+ let url = `${this.baseUrl}${cleanPath}`;
1255
+ if (query && Object.keys(query).length > 0) {
1256
+ const searchParams = new URLSearchParams();
1257
+ for (const [k, v] of Object.entries(query)) {
1258
+ if (v !== void 0 && v !== null && v !== "") {
1259
+ searchParams.append(k, typeof v === "object" ? JSON.stringify(v) : String(v));
1260
+ }
1261
+ }
1262
+ const qs = searchParams.toString();
1263
+ if (qs) {
1264
+ url += (url.includes("?") ? "&" : "?") + qs;
1265
+ }
1266
+ }
1267
+ return url;
1268
+ }
1269
+ /**
1270
+ * Dispatches an HTTP request to the AlsaBase server
1271
+ */
1272
+ async send(path, options = {}) {
1273
+ const url = this.buildUrl(path, options.query || options.params);
1274
+ let requestKey = options.requestKey;
1275
+ if (requestKey === void 0 && options.autoCancel !== false && (options.method === "GET" || !options.method)) {
1276
+ requestKey = `${options.method || "GET"} ${url}`;
1277
+ }
1278
+ let controller;
1279
+ if (requestKey) {
1280
+ this.cancelRequest(requestKey);
1281
+ controller = new AbortController();
1282
+ this.cancelControllers.set(requestKey, controller);
1283
+ }
1284
+ const headers = {
1285
+ ...options.headers || {}
1286
+ };
1287
+ if (!headers["Authorization"] && !headers["authorization"] && this.authStore.token) {
1288
+ headers["Authorization"] = `Bearer ${this.authStore.token}`;
1289
+ }
1290
+ let body = options.body;
1291
+ if (body !== void 0 && body !== null && typeof body === "object" && typeof body.append !== "function" && !(typeof Blob !== "undefined" && body instanceof Blob) && !(typeof ArrayBuffer !== "undefined" && body instanceof ArrayBuffer)) {
1292
+ if (!headers["Content-Type"] && !headers["content-type"]) {
1293
+ headers["Content-Type"] = "application/json";
1294
+ }
1295
+ body = JSON.stringify(body);
1296
+ }
1297
+ const fetchOptions = {
1298
+ ...options,
1299
+ headers,
1300
+ body,
1301
+ signal: controller ? controller.signal : options.signal
1302
+ };
1303
+ try {
1304
+ const response = await fetch(url, fetchOptions);
1305
+ if (requestKey) {
1306
+ this.cancelControllers.delete(requestKey);
1307
+ }
1308
+ let data = null;
1309
+ const contentType = response.headers.get("content-type") || "";
1310
+ if (contentType.includes("application/json")) {
1311
+ data = await response.json().catch(() => null);
1312
+ } else {
1313
+ data = await response.text().catch(() => null);
1314
+ }
1315
+ if (!response.ok) {
1316
+ throw new ClientResponseError({
1317
+ url,
1318
+ status: response.status,
1319
+ data,
1320
+ response
1321
+ });
1322
+ }
1323
+ return data;
1324
+ } catch (err) {
1325
+ if (requestKey) {
1326
+ this.cancelControllers.delete(requestKey);
1327
+ }
1328
+ if (err.name === "AbortError") {
1329
+ throw new ClientResponseError({
1330
+ url,
1331
+ status: 0,
1332
+ data: { message: "The request was autocancelled or aborted." },
1333
+ isAbort: true,
1334
+ originalError: err
1335
+ });
1336
+ }
1337
+ if (err instanceof ClientResponseError) {
1338
+ throw err;
1339
+ }
1340
+ throw new ClientResponseError({
1341
+ url,
1342
+ status: 0,
1343
+ data: { message: err.message },
1344
+ originalError: err
1345
+ });
1346
+ }
1347
+ }
1348
+ };
1349
+
1350
+ // src/stores/AsyncAuthStore.ts
1351
+ var AsyncAuthStore = class extends BaseAuthStore {
1352
+ _saveHandler;
1353
+ _clearHandler;
1354
+ constructor(options = {}) {
1355
+ super();
1356
+ this._saveHandler = options.save;
1357
+ this._clearHandler = options.clear;
1358
+ if (options.initial) {
1359
+ try {
1360
+ const parsed = JSON.parse(options.initial);
1361
+ if (parsed && typeof parsed === "object") {
1362
+ this._token = parsed.token || "";
1363
+ this._model = parsed.model || null;
1364
+ }
1365
+ } catch {
1366
+ }
1367
+ }
1368
+ }
1369
+ save(token, model) {
1370
+ super.save(token, model);
1371
+ if (this._saveHandler) {
1372
+ this._saveHandler(JSON.stringify({ token: this._token, model: this._model })).catch(
1373
+ (err) => console.error("AsyncAuthStore save failed:", err)
1374
+ );
1375
+ }
1376
+ }
1377
+ clear() {
1378
+ super.clear();
1379
+ if (this._clearHandler) {
1380
+ this._clearHandler().catch(
1381
+ (err) => console.error("AsyncAuthStore clear failed:", err)
1382
+ );
1383
+ }
1384
+ }
1385
+ };
1386
+
1387
+ // src/index.ts
1388
+ var index_default = AlsaBase;
1389
+ export {
1390
+ AlsaBase,
1391
+ AsyncAuthStore,
1392
+ BaseAuthStore,
1393
+ BaseService,
1394
+ AlsaBase as Client,
1395
+ ClientResponseError,
1396
+ CollectionService,
1397
+ FileService,
1398
+ HooksService,
1399
+ LocalAuthStore,
1400
+ LogService,
1401
+ RealtimeService,
1402
+ RecordService,
1403
+ SuperuserService,
1404
+ index_default as default
1405
+ };