@giaeulate/baas-sdk 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.
@@ -0,0 +1,133 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>BaaS SDK Demo</title>
7
+ <style>
8
+ body { font-family: 'Inter', sans-serif; background: #0f172a; color: white; padding: 2rem; }
9
+ pre { background: #1e293b; padding: 1rem; border-radius: 8px; overflow-x: auto; border: 1px solid #334155; }
10
+ .card { background: #1e293b; padding: 1.5rem; border-radius: 12px; margin-bottom: 1rem; border: 1px solid #334155; }
11
+ button { background: #6366f1; color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; cursor: pointer; }
12
+ input { background: #0f172a; border: 1px solid #334155; color: white; padding: 0.5rem; border-radius: 6px; margin-bottom: 0.5rem; width: 100%; box-sizing: border-box; }
13
+ .grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
14
+ .status { font-size: 0.8rem; color: #94a3b8; margin-top: 0.5rem; }
15
+ </style>
16
+ </head>
17
+ <body>
18
+ <h1>BaaS SDK Demo</h1>
19
+
20
+ <div class="grid">
21
+ <div class="card">
22
+ <h2>Authentication</h2>
23
+ <input type="text" id="email" placeholder="Email" value="test@example.com">
24
+ <input type="password" id="password" placeholder="Password" value="password123">
25
+ <button onclick="login()">Login</button>
26
+ <button onclick="register()">Register</button>
27
+ <div id="auth-status" class="status">Not authenticated</div>
28
+ </div>
29
+
30
+ <div class="card">
31
+ <h2>Query Data</h2>
32
+ <input type="text" id="table" placeholder="Table Name" value="products">
33
+ <button onclick="fetchData()">Fetch with Select & Filter</button>
34
+ </div>
35
+ </div>
36
+
37
+ <div class="card">
38
+ <h2>Output</h2>
39
+ <pre id="output">Results will appear here...</pre>
40
+ </div>
41
+
42
+ <!-- We import the SDK as a module (transpiled to JS or just using the browser-compatible JS version) -->
43
+ <script type="module">
44
+ // For the demo, we'll use a direct implementation of the SDK logic in JS
45
+ // to avoid build steps in this environment.
46
+
47
+ class BaasClient {
48
+ constructor(url, projectID) {
49
+ this.url = url.endsWith('/') ? url.slice(0, -1) : url;
50
+ this.projectID = projectID;
51
+ this.token = localStorage.getItem('baas_token');
52
+ }
53
+
54
+ from(table) {
55
+ return new QueryBuilder(table, this.url, this.projectID, this.token);
56
+ }
57
+
58
+ async login(email, password) {
59
+ const res = await fetch(`${this.url}/api/login`, {
60
+ method: 'POST',
61
+ headers: { 'Content-Type': 'application/json', 'X-Project-ID': this.projectID },
62
+ body: JSON.stringify({ email, password }),
63
+ });
64
+ const data = await res.json();
65
+ if (data.token) {
66
+ this.token = data.token;
67
+ localStorage.setItem('baas_token', data.token);
68
+ }
69
+ return data;
70
+ }
71
+
72
+ async register(email, password) {
73
+ const res = await fetch(`${this.url}/api/register`, {
74
+ method: 'POST',
75
+ headers: { 'Content-Type': 'application/json', 'X-Project-ID': this.projectID },
76
+ body: JSON.stringify({ email, password }),
77
+ });
78
+ return await res.json();
79
+ }
80
+ }
81
+
82
+ class QueryBuilder {
83
+ constructor(table, url, projectID, token) {
84
+ this.table = table; this.url = url; this.projectID = projectID; this.token = token;
85
+ this.queryParams = new URLSearchParams();
86
+ }
87
+
88
+ select(columns = '*') { this.queryParams.set('select', columns); return this; }
89
+ eq(column, value) { this.queryParams.set(column, `eq.${value}`); return this; }
90
+ order(column, { ascending = true } = {}) { this.queryParams.set('order', `${column}.${ascending ? 'asc' : 'desc'}`); return this; }
91
+ limit(count) { this.queryParams.set('limit', count.toString()); return this; }
92
+
93
+ async get() {
94
+ const res = await fetch(`${this.url}/api/v1/data/${this.table}?${this.queryParams.toString()}`, {
95
+ headers: {
96
+ 'Content-Type': 'application/json',
97
+ 'X-Project-ID': this.projectID,
98
+ 'Authorization': this.token ? `Bearer ${this.token}` : undefined
99
+ }
100
+ });
101
+ return await res.json();
102
+ }
103
+ }
104
+
105
+ const client = new BaasClient('http://localhost:8080', 'proj-123');
106
+ window.client = client;
107
+
108
+ window.login = async () => {
109
+ const email = document.getElementById('email').value;
110
+ const password = document.getElementById('password').value;
111
+ const res = await client.login(email, password);
112
+ document.getElementById('output').textContent = JSON.stringify(res, null, 2);
113
+ document.getElementById('auth-status').textContent = res.token ? 'Authenticated ✅' : 'Login failed ❌';
114
+ };
115
+
116
+ window.register = async () => {
117
+ const email = document.getElementById('email').value;
118
+ const password = document.getElementById('password').value;
119
+ const res = await client.register(email, password);
120
+ document.getElementById('output').textContent = JSON.stringify(res, null, 2);
121
+ };
122
+
123
+ window.fetchData = async () => {
124
+ const table = document.getElementById('table').value;
125
+ const res = await client.from(table)
126
+ .select('*')
127
+ .limit(5)
128
+ .get();
129
+ document.getElementById('output').textContent = JSON.stringify(res, null, 2);
130
+ };
131
+ </script>
132
+ </body>
133
+ </html>
@@ -0,0 +1,278 @@
1
+ /**
2
+ * BaaS Client SDK
3
+ * A fluent JavaScript/TypeScript client for the Go BaaS.
4
+ */
5
+
6
+ export type QueryFilter = {
7
+ column: string;
8
+ operator: string;
9
+ value: any;
10
+ };
11
+
12
+ export class BaasClient {
13
+ public url: string;
14
+ public projectID: string = '';
15
+ public token: string | null = null;
16
+ public onAuthError?: (error: { status: number; message: string }) => void;
17
+
18
+ constructor(url: string, projectID?: string) {
19
+ this.url = url.endsWith('/') ? url.slice(0, -1) : url;
20
+ if (projectID) this.projectID = projectID;
21
+
22
+ // Attempt to load session from localStorage for persistence
23
+ if (typeof localStorage !== 'undefined') {
24
+ const storedToken = localStorage.getItem('baas_token');
25
+ const storedProject = localStorage.getItem('baas_project_id');
26
+ if (storedToken) this.token = storedToken;
27
+ if (!this.projectID && storedProject) this.projectID = storedProject;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Generates headers for API requests
33
+ */
34
+ public getHeaders(contentType: string = 'application/json') {
35
+ const headers: Record<string, string> = {
36
+ 'X-Project-ID': this.projectID || 'default',
37
+ };
38
+
39
+ if (contentType) {
40
+ headers['Content-Type'] = contentType;
41
+ }
42
+
43
+ if (this.token) {
44
+ headers['Authorization'] = `Bearer ${this.token.trim()}`;
45
+ }
46
+ return headers;
47
+ }
48
+
49
+ /**
50
+ * Data Operations
51
+ */
52
+ from(table: string) {
53
+ return new QueryBuilder(table, this.url, this);
54
+ }
55
+
56
+ /**
57
+ * Authentication
58
+ */
59
+ async login(email: string, password: string) {
60
+ const res = await fetch(`${this.url}/api/login`, {
61
+ method: 'POST',
62
+ headers: { 'Content-Type': 'application/json', 'X-Project-ID': this.projectID || 'default' },
63
+ body: JSON.stringify({ email, password }),
64
+ });
65
+ const data = await res.json();
66
+ if (!res.ok) throw new Error(data.error || 'Login failed');
67
+
68
+ if (data.token) {
69
+ this.token = data.token;
70
+ if (typeof localStorage !== 'undefined') {
71
+ localStorage.setItem('baas_token', data.token);
72
+ localStorage.setItem('baas_project_id', this.projectID);
73
+ }
74
+ }
75
+ return data;
76
+ }
77
+
78
+ async verifyMFA(mfaToken: string, code: string) {
79
+ const res = await fetch(`${this.url}/api/mfa/finalize`, {
80
+ method: 'POST',
81
+ headers: { 'Content-Type': 'application/json', 'X-Project-ID': this.projectID || 'default' },
82
+ body: JSON.stringify({ mfa_token: mfaToken, code }),
83
+ });
84
+ const data = await res.json();
85
+ if (!res.ok) throw new Error(data.error || 'MFA validation failed');
86
+
87
+ if (data.token) {
88
+ this.token = data.token;
89
+ if (typeof localStorage !== 'undefined') {
90
+ localStorage.setItem('baas_token', data.token);
91
+ }
92
+ }
93
+ return data;
94
+ }
95
+
96
+ async register(email: string, password: string) {
97
+ const res = await fetch(`${this.url}/api/register`, {
98
+ method: 'POST',
99
+ headers: { 'Content-Type': 'application/json', 'X-Project-ID': this.projectID || 'default' },
100
+ body: JSON.stringify({ email, password }),
101
+ });
102
+ const data = await res.json();
103
+ if (!res.ok) throw new Error(data.error || 'Registration failed');
104
+ return data;
105
+ }
106
+
107
+ logout() {
108
+ this.token = null;
109
+ if (typeof localStorage !== 'undefined') {
110
+ localStorage.removeItem('baas_token');
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Edge Functions
116
+ */
117
+ async invokeFunction(id: string, data: any = {}) {
118
+ const res = await fetch(`${this.url}/api/functions/${id}/execute`, {
119
+ method: 'POST',
120
+ headers: this.getHeaders(),
121
+ body: JSON.stringify(data),
122
+ });
123
+ const result = await res.json();
124
+ if (!res.ok && res.status === 401 && this.onAuthError) {
125
+ this.onAuthError({ status: 401, message: 'Expired session' });
126
+ }
127
+ return result;
128
+ }
129
+
130
+ /**
131
+ * Storage
132
+ */
133
+ async upload(bucketID: string, file: File) {
134
+ const formData = new FormData();
135
+ formData.append('file', file);
136
+ formData.append('bucket_id', bucketID);
137
+ formData.append('name', file.name);
138
+
139
+ const res = await fetch(`${this.url}/api/storage/upload`, {
140
+ method: 'POST',
141
+ headers: this.getHeaders(''), // Let browser set multipart boundary
142
+ body: formData
143
+ });
144
+ const data = await res.json();
145
+ if (!res.ok && res.status === 401 && this.onAuthError) {
146
+ this.onAuthError({ status: 401, message: 'Expired session' });
147
+ }
148
+ return data;
149
+ }
150
+
151
+ /**
152
+ * Realtime
153
+ */
154
+ subscribe(table: string, callback: (payload: any) => void) {
155
+ const protocol = this.url.startsWith('https') ? 'wss' : 'ws';
156
+ const socket = new WebSocket(`${protocol}://${this.url.replace(/^https?:\/\//, '')}/api/ws?token=${this.token}`);
157
+
158
+ socket.onmessage = (event) => {
159
+ try {
160
+ const payload = JSON.parse(event.data);
161
+ if (payload.table === table || table === '*') {
162
+ callback(payload);
163
+ }
164
+ } catch(e) {}
165
+ };
166
+
167
+ return {
168
+ unsubscribe: () => socket.close()
169
+ };
170
+ }
171
+ }
172
+
173
+ class QueryBuilder {
174
+ private table: string;
175
+ private url: string;
176
+ private client: BaasClient;
177
+ private queryParams: URLSearchParams = new URLSearchParams();
178
+
179
+ constructor(table: string, url: string, client: BaasClient) {
180
+ this.table = table;
181
+ this.url = url;
182
+ this.client = client;
183
+ }
184
+
185
+ select(columns: string = '*') {
186
+ this.queryParams.set('select', columns);
187
+ return this;
188
+ }
189
+
190
+ eq(column: string, value: any) {
191
+ this.queryParams.set(column, `eq.${value}`);
192
+ return this;
193
+ }
194
+
195
+ gt(column: string, value: any) {
196
+ this.queryParams.set(column, `gt.${value}`);
197
+ return this;
198
+ }
199
+
200
+ lt(column: string, value: any) {
201
+ this.queryParams.set(column, `lt.${value}`);
202
+ return this;
203
+ }
204
+
205
+ like(column: string, value: any) {
206
+ this.queryParams.set(column, `like.${value}`);
207
+ return this;
208
+ }
209
+
210
+ order(column: string, { ascending = true } = {}) {
211
+ this.queryParams.set('order', `${column}.${ascending ? 'asc' : 'desc'}`);
212
+ return this;
213
+ }
214
+
215
+ limit(count: number) {
216
+ this.queryParams.set('limit', count.toString());
217
+ return this;
218
+ }
219
+
220
+ offset(count: number) {
221
+ this.queryParams.set('offset', count.toString());
222
+ return this;
223
+ }
224
+
225
+ /**
226
+ * Execution
227
+ */
228
+ async get() {
229
+ const res = await fetch(`${this.url}/api/v1/data/${this.table}?${this.queryParams.toString()}`, {
230
+ headers: this.client.getHeaders(),
231
+ });
232
+ return await this.handleResponse(res);
233
+ }
234
+
235
+ async insert(data: any) {
236
+ const res = await fetch(`${this.url}/api/v1/data/${this.table}`, {
237
+ method: 'POST',
238
+ headers: this.client.getHeaders(),
239
+ body: JSON.stringify(data),
240
+ });
241
+ return await this.handleResponse(res);
242
+ }
243
+
244
+ async update(id: string | number, data: any) {
245
+ const res = await fetch(`${this.url}/api/v1/data/${this.table}/${id}`, {
246
+ method: 'PATCH',
247
+ headers: this.client.getHeaders(),
248
+ body: JSON.stringify(data),
249
+ });
250
+ return await this.handleResponse(res);
251
+ }
252
+
253
+ async delete(id: string | number) {
254
+ const res = await fetch(`${this.url}/api/v1/data/${this.table}/${id}`, {
255
+ method: 'DELETE',
256
+ headers: this.client.getHeaders(),
257
+ });
258
+ if (res.status === 204) return { data: { success: true }, error: null };
259
+ return await this.handleResponse(res);
260
+ }
261
+
262
+ private async handleResponse(res: Response) {
263
+ const text = await res.text();
264
+ let data = null;
265
+ try {
266
+ data = text ? JSON.parse(text) : null;
267
+ } catch(e) {}
268
+
269
+ if (!res.ok) {
270
+ if (res.status === 401 && this.client.onAuthError) {
271
+ this.client.onAuthError({ status: 401, message: 'Expired session' });
272
+ }
273
+ return { data: null, error: data?.error || 'Request failed', status: res.status };
274
+ }
275
+ return { data, error: null, status: res.status };
276
+ }
277
+ }
278
+
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@giaeulate/baas-sdk",
3
+ "version": "1.0.0",
4
+ "description": "Standalone SDK for BaaS Golang",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "scripts": {
17
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
18
+ "dev": "tsup src/index.ts --format cjs,esm --watch --dts",
19
+ "lint": "eslint src",
20
+ "test": "vitest run",
21
+ "test:watch": "vitest",
22
+ "coverage": "vitest run --coverage"
23
+ },
24
+ "dependencies": {
25
+ "axios": "^1.13.2"
26
+ },
27
+ "devDependencies": {
28
+ "@vitest/coverage-v8": "^4.0.17",
29
+ "jsdom": "^27.4.0",
30
+ "tsup": "^8.0.0",
31
+ "typescript": "~5.9.3",
32
+ "vitest": "^4.0.17"
33
+ }
34
+ }
@@ -0,0 +1,184 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { RealtimeService } from './src/realtime';
3
+
4
+ // Mock WebSocket
5
+ class MockWebSocket {
6
+ onopen: (() => void) | null = null;
7
+ onclose: (() => void) | null = null;
8
+ onmessage: ((event: { data: string }) => void) | null = null;
9
+ onerror: ((error: any) => void) | null = null;
10
+ readyState = 1; // OPEN immediately for simplicity
11
+ url: string;
12
+
13
+ constructor(url: string) {
14
+ this.url = url;
15
+ // Trigger onopen in next tick
16
+ Promise.resolve().then(() => {
17
+ if (this.onopen) this.onopen();
18
+ });
19
+ }
20
+
21
+ close() {
22
+ this.readyState = 3; // CLOSED
23
+ if (this.onclose) this.onclose();
24
+ }
25
+
26
+ send(data: string) {}
27
+ }
28
+
29
+ describe('RealtimeService', () => {
30
+ let service: RealtimeService;
31
+ const baseUrl = 'http://localhost:8080';
32
+ const projectId = 'test-project';
33
+ const token = 'test-token';
34
+
35
+ beforeEach(() => {
36
+ service = new RealtimeService(baseUrl, projectId, () => token, MockWebSocket);
37
+ });
38
+
39
+ afterEach(() => {
40
+ service.disconnect();
41
+ vi.restoreAllMocks();
42
+ });
43
+
44
+ it('should connect to the correct WebSocket URL', async () => {
45
+ const subPromise = service.subscribe('users', '*', () => {});
46
+ const sub = await subPromise;
47
+
48
+ const socket = service._socket;
49
+ expect(socket).not.toBeNull();
50
+ expect(socket?.url).toContain('ws://localhost:8080/ws');
51
+ expect(socket?.url).toContain(`project_id=${projectId}`);
52
+ expect(socket?.url).toContain(`token=${token}`);
53
+
54
+ sub.unsubscribe();
55
+ });
56
+
57
+ it('should dispatch events to subscribers', async () => {
58
+ const callback = vi.fn();
59
+ const sub = await service.subscribe('users', '*', callback);
60
+
61
+ const payload = {
62
+ table: 'users',
63
+ action: 'insert',
64
+ record: { id: 1, name: 'John' },
65
+ timestamp: new Date().toISOString()
66
+ };
67
+
68
+ const socket = service._socket;
69
+ if (socket) {
70
+ // @ts-ignore
71
+ socket.onmessage({ data: JSON.stringify(payload) });
72
+ }
73
+
74
+ expect(callback).toHaveBeenCalledWith(expect.objectContaining({
75
+ table: 'users',
76
+ record: { id: 1, name: 'John' }
77
+ }));
78
+
79
+ sub.unsubscribe();
80
+ });
81
+
82
+ it('should filter events by action', async () => {
83
+ const insertCallback = vi.fn();
84
+ const updateCallback = vi.fn();
85
+
86
+ const [s1, s2] = await Promise.all([
87
+ service.subscribe('users', 'INSERT', insertCallback),
88
+ service.subscribe('users', 'UPDATE', updateCallback)
89
+ ]);
90
+
91
+ const payload = {
92
+ table: 'users',
93
+ action: 'update',
94
+ record: { id: 1, name: 'John Updated' },
95
+ timestamp: new Date().toISOString()
96
+ };
97
+
98
+ const socket = service._socket;
99
+ if (socket) {
100
+ // @ts-ignore
101
+ socket.onmessage({ data: JSON.stringify(payload) });
102
+ }
103
+
104
+ expect(insertCallback).not.toHaveBeenCalled();
105
+ expect(updateCallback).toHaveBeenCalled();
106
+
107
+ s1.unsubscribe();
108
+ s2.unsubscribe();
109
+ });
110
+
111
+ it('should handle wildcard actions', async () => {
112
+ const callback = vi.fn();
113
+ const sub = await service.subscribe('users', '*', callback);
114
+
115
+ const payload = {
116
+ table: 'users',
117
+ action: 'delete',
118
+ record: { id: 1 },
119
+ timestamp: new Date().toISOString()
120
+ };
121
+
122
+ const socket = service._socket;
123
+ if (socket) {
124
+ // @ts-ignore
125
+ socket.onmessage({ data: JSON.stringify(payload) });
126
+ }
127
+
128
+ expect(callback).toHaveBeenCalled();
129
+ sub.unsubscribe();
130
+ });
131
+
132
+ it('should unsubscribe correctly', async () => {
133
+ const callback = vi.fn();
134
+ const sub = await service.subscribe('users', '*', callback);
135
+
136
+ sub.unsubscribe();
137
+
138
+ const payload = {
139
+ table: 'users',
140
+ action: 'insert',
141
+ record: { id: 1 },
142
+ timestamp: new Date().toISOString()
143
+ };
144
+
145
+ const socket = service._socket;
146
+ if (socket) {
147
+ // @ts-ignore
148
+ socket.onmessage({ data: JSON.stringify(payload) });
149
+ }
150
+
151
+ expect(callback).not.toHaveBeenCalled();
152
+ });
153
+
154
+ it('should handle wildcard table subscriptions', async () => {
155
+ const callback = vi.fn();
156
+ await service.subscribe('*', '*', callback);
157
+
158
+ const payloadA = {
159
+ table: 'table_a',
160
+ action: 'insert',
161
+ record: { id: 1 },
162
+ timestamp: new Date().toISOString()
163
+ };
164
+
165
+ const payloadB = {
166
+ table: 'table_b',
167
+ action: 'update',
168
+ record: { id: 2 },
169
+ timestamp: new Date().toISOString()
170
+ };
171
+
172
+ const socket = service._socket;
173
+ if (socket) {
174
+ // @ts-ignore
175
+ socket.onmessage({ data: JSON.stringify(payloadA) });
176
+ // @ts-ignore
177
+ socket.onmessage({ data: JSON.stringify(payloadB) });
178
+ }
179
+
180
+ expect(callback).toHaveBeenCalledWith(expect.objectContaining({ table: 'table_a' }));
181
+ expect(callback).toHaveBeenCalledWith(expect.objectContaining({ table: 'table_b' }));
182
+ expect(callback).toHaveBeenCalledTimes(2);
183
+ });
184
+ });
package/scripts/dev.sh ADDED
@@ -0,0 +1,30 @@
1
+ #!/bin/bash
2
+ set -e
3
+
4
+ # Colors for output
5
+ RED='\033[0;31m'
6
+ GREEN='\033[0;32m'
7
+ YELLOW='\033[1;33m'
8
+ BLUE='\033[0;34m'
9
+ NC='\033[0m' # No Color
10
+
11
+ echo -e "${BLUE}========================================${NC}"
12
+ echo -e "${BLUE} BaaS SDK - Development Mode ${NC}"
13
+ echo -e "${BLUE}========================================${NC}"
14
+
15
+ # Change to SDK directory
16
+ cd "$(dirname "$0")/.."
17
+
18
+ # Check if node_modules exists
19
+ if [ ! -d "node_modules" ]; then
20
+ echo -e "${YELLOW}Installing dependencies...${NC}"
21
+ npm install
22
+ fi
23
+
24
+ echo ""
25
+ echo -e "${GREEN}Starting SDK in watch mode...${NC}"
26
+ echo -e "${YELLOW}Changes will be automatically rebuilt${NC}"
27
+ echo -e "${YELLOW}Press Ctrl+C to stop${NC}"
28
+ echo ""
29
+
30
+ npm run dev