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