@giaeulate/baas-sdk 1.1.1 → 1.3.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/CHANGELOG.md ADDED
@@ -0,0 +1,26 @@
1
+ # Changelog
2
+
3
+ ## 1.3.0 — 2026-06-04
4
+
5
+ Backward-compatible additions + two dead-route fixes. Validated end-to-end (33 live checks) against a running golang-baas server.
6
+
7
+ ### Added
8
+
9
+ - **`client.rpc(fn, params, { get })`** — invoke PL/pgSQL functions (PostgREST `.rpc()` parity), POST or GET.
10
+ - **QueryBuilder**: `upsert(data, onConflict)`, `count()` (via `Prefer: count=exact` → `Content-Range`), `contains`/`containedBy`/`overlaps` (cs/cd/ov array ops), `textSearch` (fts/plfts/phfts/wfts), and `and`/`notAnd`/`notOr` logic groups.
11
+ - **Auth**: `signInAnonymous()`, `requestOtp(email, createUser?)`, `verifyOtp(type, email, token)`, `upgradeAnonymous(email, password)`.
12
+ - **Storage**: `download(fileId)` → `Blob` (streams via the backend download route; sends the Bearer so private-bucket files work).
13
+ - HTTP errors now surface `status`, and 429 responses include `{ rateLimited, retryAfter }`.
14
+
15
+ ### Fixed
16
+
17
+ - `functions.update()` no longer calls a non-existent `PUT /api/functions/{id}`; it upserts by name (`POST /api/functions`, the backend's `ON CONFLICT (name)`).
18
+ - `graphql.getSchema()` no longer hits a non-existent `GET /api/graphql/schema`; it runs introspection through `POST /api/graphql`.
19
+
20
+ ### Changed
21
+
22
+ - Removed the unused `axios` dependency — the SDK now has **zero runtime dependencies** (native `fetch`).
23
+
24
+ ## 1.2.0
25
+
26
+ - Previous release.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Gianluca
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # @giaeulate/baas-sdk
2
+
3
+ TypeScript SDK for [golang-baas](https://github.com/Giaeulate/golang-baas) — a Supabase-style Backend-as-a-Service. Auth, data (PostgREST-style), storage, realtime, edge functions, RPC, and the admin APIs. **Zero runtime dependencies** (native `fetch` + `WebSocket`).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ bun add @giaeulate/baas-sdk
9
+ # or: pnpm add @giaeulate/baas-sdk
10
+ ```
11
+
12
+ ## Quick start
13
+
14
+ ```ts
15
+ import { BaasClient } from '@giaeulate/baas-sdk';
16
+
17
+ const baas = new BaasClient('https://your-baas.example.com', 'YOUR_ANON_KEY');
18
+
19
+ // Auth
20
+ await baas.auth.login('user@example.com', 'password');
21
+ await baas.auth.signInAnonymous();
22
+
23
+ // Data (PostgREST-style)
24
+ const { data } = await baas.from('orders')
25
+ .select('id,total,status')
26
+ .eq('status', 'pending')
27
+ .gt('total', 100)
28
+ .order('created_at', { ascending: false })
29
+ .get();
30
+
31
+ // Logic groups, array & full-text operators
32
+ await baas.from('products').and('price.gt.10,price.lt.50').get();
33
+ await baas.from('products').contains('tags', ['featured']).get();
34
+
35
+ // Count (Content-Range)
36
+ const { count } = await baas.from('orders').eq('status', 'pending').count();
37
+
38
+ // Upsert
39
+ await baas.from('settings').upsert({ id: 1, theme: 'dark' }, 'id');
40
+
41
+ // RPC (PL/pgSQL)
42
+ const result = await baas.rpc('accept_delivery_offer', { offer_id: '...' });
43
+
44
+ // Storage
45
+ const file = await baas.storage.upload(myFile, 'bucket-id');
46
+ const blob = await baas.storage.download(file.id);
47
+
48
+ // Realtime
49
+ await baas.realtime.subscribe('orders', 'insert', (evt) => console.log(evt));
50
+ ```
51
+
52
+ ## License
53
+
54
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,416 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/config.ts
4
+ import { readFileSync, existsSync } from "fs";
5
+ import { resolve } from "path";
6
+ function getArg(argv, name) {
7
+ const idx = argv.indexOf(`--${name}`);
8
+ return idx !== -1 && idx + 1 < argv.length ? argv[idx + 1] : null;
9
+ }
10
+ function hasFlag(argv, name) {
11
+ return argv.includes(`--${name}`);
12
+ }
13
+ function loadEnvFile(path) {
14
+ if (!existsSync(path)) return;
15
+ const content = readFileSync(path, "utf-8");
16
+ for (const line of content.split("\n")) {
17
+ const trimmed = line.trim();
18
+ if (!trimmed || trimmed.startsWith("#")) continue;
19
+ const eq = trimmed.indexOf("=");
20
+ if (eq === -1) continue;
21
+ const key = trimmed.slice(0, eq).trim();
22
+ const val = trimmed.slice(eq + 1).trim();
23
+ if ((key === "VITE_BAAS_URL" || key === "BAAS_URL") && !process.env.BAAS_URL) {
24
+ process.env.BAAS_URL = val;
25
+ }
26
+ if ((key === "VITE_BAAS_API_KEY" || key === "BAAS_API_KEY") && !process.env.BAAS_API_KEY) {
27
+ process.env.BAAS_API_KEY = val;
28
+ }
29
+ }
30
+ }
31
+ function resolveConfig(argv) {
32
+ const migrationsDir = resolve(
33
+ getArg(argv, "dir") || process.env.BAAS_MIGRATIONS_DIR || process.cwd()
34
+ );
35
+ const candidates = [
36
+ resolve(process.cwd(), ".env"),
37
+ resolve(migrationsDir, "../../react-delivery-platform-admin-web/.env"),
38
+ resolve(process.cwd(), "../../react-delivery-platform-admin-web/.env"),
39
+ resolve(process.cwd(), "../react-delivery-platform-admin-web/.env"),
40
+ resolve(migrationsDir, ".env"),
41
+ resolve(migrationsDir, "../.env")
42
+ ];
43
+ for (const c of candidates) loadEnvFile(c);
44
+ const baasUrl = getArg(argv, "url") || process.env.BAAS_URL || "";
45
+ const token = getArg(argv, "token") || process.env.BAAS_API_KEY || "";
46
+ return { baasUrl, token, migrationsDir };
47
+ }
48
+
49
+ // src/cli/client.ts
50
+ function msg(e) {
51
+ return e instanceof Error ? e.message : String(e);
52
+ }
53
+ var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
54
+ var isPoisoned = (m) => /401|invalid or expired token/i.test(m);
55
+ function createSqlClient(baasUrl, token) {
56
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
57
+ const endpoint = `${baasUrl.replace(/\/$/, "")}/api/v1/sql`;
58
+ async function runSQL(sql, params) {
59
+ const body = { query: sql };
60
+ if (params && params.length > 0) body.params = params;
61
+ let res;
62
+ try {
63
+ res = await fetch(endpoint, {
64
+ method: "POST",
65
+ headers: {
66
+ "Content-Type": "application/json",
67
+ Authorization: `Bearer ${token}`
68
+ },
69
+ body: JSON.stringify(body)
70
+ });
71
+ } catch (err) {
72
+ const cause = err?.cause;
73
+ const detail = cause ? ` (cause: ${cause.code || cause.message})` : "";
74
+ throw new Error(`Network failure: ${msg(err)}${detail}`);
75
+ }
76
+ const text = await res.text();
77
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${text}`);
78
+ return text;
79
+ }
80
+ async function runSQLResilient(sql, params) {
81
+ const maxAttempts = 8;
82
+ let lastErr;
83
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
84
+ try {
85
+ return await runSQL(sql, params);
86
+ } catch (err) {
87
+ lastErr = err;
88
+ if (isPoisoned(msg(err)) && attempt < maxAttempts) {
89
+ process.stdout.write(` (401 pool poisoned, retry ${attempt}/${maxAttempts - 1})
90
+ `);
91
+ await sleep(400 * attempt);
92
+ continue;
93
+ }
94
+ throw err;
95
+ }
96
+ }
97
+ throw lastErr;
98
+ }
99
+ async function preflight() {
100
+ const maxAttempts = 6;
101
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
102
+ try {
103
+ await runSQL("SELECT 1 AS ok");
104
+ return;
105
+ } catch (err) {
106
+ const poisoned = isPoisoned(msg(err));
107
+ if (poisoned && attempt < maxAttempts) {
108
+ await sleep(500 * attempt);
109
+ continue;
110
+ }
111
+ if (poisoned) {
112
+ throw new Error(
113
+ `${msg(err)}
114
+ -> Persistent 401. Do NOT regenerate the key (it is valid).
115
+ Cause: BaaS pool holding a connection in an aborted transaction.
116
+ Deterministic fix: restart the BaaS container to clear the pool.`
117
+ );
118
+ }
119
+ throw err;
120
+ }
121
+ }
122
+ }
123
+ return { runSQL, runSQLResilient, preflight };
124
+ }
125
+
126
+ // src/cli/migrations.ts
127
+ import { createHash } from "crypto";
128
+ import { readFileSync as readFileSync2, readdirSync, existsSync as existsSync2, writeFileSync } from "fs";
129
+ import { resolve as resolve2 } from "path";
130
+ var MIGRATION_RE = /^(\d{3,})_(.+)\.sql$/;
131
+ var SEED_FILE = "005_seed.sql";
132
+ var TRACKING_TABLE = "public.schema_migrations";
133
+ var BENIGN_RE = /already exists|duplicate entry|duplicate key|SQLSTATE 42710|SQLSTATE 42P07|SQLSTATE 42701|SQLSTATE 42P06|SQLSTATE 42723|SQLSTATE 23505|HTTP 409/i;
134
+ function discover(dir, includeSeed = false) {
135
+ if (!existsSync2(dir)) throw new Error(`Migrations dir not found: ${dir}`);
136
+ return readdirSync(dir).filter((f) => MIGRATION_RE.test(f)).filter((f) => includeSeed || f !== SEED_FILE).sort().map((f) => {
137
+ const m = MIGRATION_RE.exec(f);
138
+ return { file: f, version: m[1], name: m[2], path: resolve2(dir, f) };
139
+ });
140
+ }
141
+ function checksum(content) {
142
+ return createHash("sha256").update(content, "utf8").digest("hex");
143
+ }
144
+ function fileKey(version, name) {
145
+ return `${version}_${name}.sql`;
146
+ }
147
+ function slugify(name) {
148
+ return name.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
149
+ }
150
+ function nextVersion(files) {
151
+ let max = 0;
152
+ for (const f of files) {
153
+ const n = parseInt(f.version, 10);
154
+ if (n > max) max = n;
155
+ }
156
+ return String(max + 1).padStart(3, "0");
157
+ }
158
+ async function ensureTrackingTable(client) {
159
+ await client.runSQLResilient(
160
+ `CREATE TABLE IF NOT EXISTS ${TRACKING_TABLE} (
161
+ version text NOT NULL,
162
+ name text NOT NULL,
163
+ checksum text NOT NULL,
164
+ applied_at timestamptz NOT NULL DEFAULT now()
165
+ );
166
+ DO $$
167
+ BEGIN
168
+ IF EXISTS (
169
+ SELECT 1 FROM pg_constraint
170
+ WHERE conname = 'schema_migrations_pkey'
171
+ AND conrelid = '${TRACKING_TABLE}'::regclass
172
+ ) THEN
173
+ ALTER TABLE ${TRACKING_TABLE} DROP CONSTRAINT schema_migrations_pkey;
174
+ END IF;
175
+ END $$;
176
+ CREATE UNIQUE INDEX IF NOT EXISTS schema_migrations_version_name_key
177
+ ON ${TRACKING_TABLE} (version, name);`
178
+ );
179
+ }
180
+ async function getApplied(client) {
181
+ const body = await client.runSQLResilient(
182
+ `SELECT version, name, checksum FROM ${TRACKING_TABLE} ORDER BY version`
183
+ );
184
+ const parsed = JSON.parse(body);
185
+ const rows = Array.isArray(parsed.data) ? parsed.data : [];
186
+ const map = /* @__PURE__ */ new Map();
187
+ for (const r of rows) {
188
+ const version = String(r.version);
189
+ const name = String(r.name ?? "");
190
+ map.set(fileKey(version, name), { version, name, checksum: String(r.checksum) });
191
+ }
192
+ return map;
193
+ }
194
+ async function recordApplied(client, m, sum) {
195
+ await client.runSQL(
196
+ `INSERT INTO ${TRACKING_TABLE} (version, name, checksum)
197
+ VALUES ($1, $2, $3)
198
+ ON CONFLICT (version, name) DO NOTHING`,
199
+ [m.version, m.name, sum]
200
+ );
201
+ }
202
+ function dbNew(dir, rawName) {
203
+ if (!rawName) throw new Error("Usage: baas db new <name>");
204
+ const all = discover(dir, true);
205
+ const version = nextVersion(all);
206
+ const slug = slugify(rawName) || "migration";
207
+ const file = `${version}_${slug}.sql`;
208
+ const path = resolve2(dir, file);
209
+ if (existsSync2(path)) throw new Error(`Already exists: ${file}`);
210
+ const header = `-- ${file}
211
+ -- Created ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}
212
+ -- Apply with: baas db push
213
+
214
+ `;
215
+ writeFileSync(path, header, "utf8");
216
+ return file;
217
+ }
218
+ async function dbStatus(client, dir, includeSeed) {
219
+ await ensureTrackingTable(client);
220
+ const applied = await getApplied(client);
221
+ const files = discover(dir, includeSeed);
222
+ const seen = /* @__PURE__ */ new Set();
223
+ const out = [];
224
+ for (const f of files) {
225
+ seen.add(f.file);
226
+ const a = applied.get(f.file);
227
+ if (!a) {
228
+ out.push({ version: f.version, file: f.file, state: "pending" });
229
+ } else if (a.checksum !== checksum(readFileSync2(f.path, "utf8"))) {
230
+ out.push({ version: f.version, file: f.file, state: "drift" });
231
+ } else {
232
+ out.push({ version: f.version, file: f.file, state: "applied" });
233
+ }
234
+ }
235
+ for (const [key, a] of applied) {
236
+ if (!seen.has(key)) {
237
+ out.push({ version: a.version, file: `${key} (missing)`, state: "orphan" });
238
+ }
239
+ }
240
+ out.sort((x, y) => x.version.localeCompare(y.version));
241
+ return out;
242
+ }
243
+ async function dbPush(client, dir, opts) {
244
+ await ensureTrackingTable(client);
245
+ const applied = await getApplied(client);
246
+ const files = discover(dir, !!opts.includeSeed);
247
+ const pending = files.filter((f) => !applied.has(f.file));
248
+ if (opts.dryRun) {
249
+ return { applied: [], skipped: [], pending: pending.map((f) => f.file) };
250
+ }
251
+ const result = { applied: [], skipped: [], pending: [] };
252
+ for (const f of pending) {
253
+ const content = readFileSync2(f.path, "utf8");
254
+ process.stdout.write(` Applying ${f.file} (${content.length} chars)...
255
+ `);
256
+ try {
257
+ await client.runSQLResilient(content);
258
+ await recordApplied(client, f, checksum(content));
259
+ result.applied.push(f.file);
260
+ process.stdout.write(` OK ${f.file}
261
+ `);
262
+ } catch (err) {
263
+ const message = err instanceof Error ? err.message : String(err);
264
+ if (!opts.strict && BENIGN_RE.test(message)) {
265
+ await recordApplied(client, f, checksum(content));
266
+ result.skipped.push(f.file);
267
+ process.stdout.write(` SKIP ${f.file} (already applied) -> recorded
268
+ `);
269
+ continue;
270
+ }
271
+ throw new Error(`FAIL ${f.file}: ${message}`);
272
+ }
273
+ }
274
+ return result;
275
+ }
276
+ async function dbBaseline(client, dir, opts) {
277
+ await ensureTrackingTable(client);
278
+ const applied = await getApplied(client);
279
+ const files = discover(dir, !!opts.includeSeed);
280
+ const throughNum = opts.through ? parseInt(opts.through, 10) : null;
281
+ const marked = [];
282
+ for (const f of files) {
283
+ if (throughNum !== null && parseInt(f.version, 10) > throughNum) continue;
284
+ if (applied.has(f.file)) continue;
285
+ await recordApplied(client, f, checksum(readFileSync2(f.path, "utf8")));
286
+ marked.push(f.file);
287
+ }
288
+ return marked;
289
+ }
290
+
291
+ // src/cli/index.ts
292
+ var HELP = `baas \u2014 migration CLI for golang-baas
293
+
294
+ Usage:
295
+ baas db new <name> Scaffold the next NNN_<name>.sql migration
296
+ baas db status Show applied / pending / drift / orphan per file
297
+ baas db list Alias of status
298
+ baas db push Apply pending migrations and record them
299
+ baas db baseline Mark existing files as applied without running them
300
+
301
+ Options:
302
+ --dir <path> Migrations directory (default: cwd or BAAS_MIGRATIONS_DIR)
303
+ --url <url> BaaS URL (default: BAAS_URL / .env)
304
+ --token <key> API key (default: BAAS_API_KEY / .env)
305
+ --dry-run push: list pending without applying
306
+ --strict push: halt on any error (no benign "already exists" skip)
307
+ --through <NNN> baseline: only mark files up to and including version NNN
308
+ --seed include 005_seed.sql in discovery
309
+
310
+ Credentials resolve from flags, then env (BAAS_URL/BAAS_API_KEY), then a .env
311
+ file (supports VITE_BAAS_URL/VITE_BAAS_API_KEY).`;
312
+ function printStatus(rows) {
313
+ const counts = { applied: 0, pending: 0, drift: 0, orphan: 0 };
314
+ for (const r of rows) {
315
+ counts[r.state]++;
316
+ console.log(` ${r.state.toUpperCase().padEnd(7)} ${r.file}`);
317
+ }
318
+ if (rows.length === 0) console.log(" (no migration files found)");
319
+ console.log(
320
+ `
321
+ ${counts.applied} applied, ${counts.pending} pending, ${counts.drift} drift, ${counts.orphan} orphan.`
322
+ );
323
+ if (counts.drift > 0) {
324
+ console.log("\nWARNING: drift = a tracked file changed since it was applied.");
325
+ }
326
+ }
327
+ async function main() {
328
+ const argv = process.argv.slice(2);
329
+ const group = argv[0];
330
+ const sub = argv[1];
331
+ if (!group || group === "help" || group === "--help" || group === "-h") {
332
+ console.log(HELP);
333
+ return;
334
+ }
335
+ if (group !== "db") {
336
+ console.error(`Unknown command: ${group}
337
+ `);
338
+ console.log(HELP);
339
+ process.exit(1);
340
+ }
341
+ const cfg = resolveConfig(argv);
342
+ const includeSeed = hasFlag(argv, "seed");
343
+ if (sub === "new") {
344
+ const positional = argv[2] && !argv[2].startsWith("--") ? argv[2] : "";
345
+ const name = positional || getArg(argv, "name") || "";
346
+ const file = dbNew(cfg.migrationsDir, name);
347
+ console.log(`Created ${file} in ${cfg.migrationsDir}`);
348
+ return;
349
+ }
350
+ if (!cfg.baasUrl || !cfg.token) {
351
+ console.error(
352
+ "ERROR: missing BaaS URL or token.\n Provide --url/--token, set BAAS_URL/BAAS_API_KEY, or have a .env nearby."
353
+ );
354
+ process.exit(1);
355
+ }
356
+ console.log(`BaaS: ${cfg.baasUrl}`);
357
+ console.log(`Dir: ${cfg.migrationsDir}`);
358
+ console.log(`Token: ${cfg.token.slice(0, 16)}...
359
+ `);
360
+ const client = createSqlClient(cfg.baasUrl, cfg.token);
361
+ switch (sub) {
362
+ case "status":
363
+ case "list": {
364
+ await client.preflight();
365
+ printStatus(await dbStatus(client, cfg.migrationsDir, includeSeed));
366
+ break;
367
+ }
368
+ case "push": {
369
+ const dryRun = hasFlag(argv, "dry-run");
370
+ await client.preflight();
371
+ const res = await dbPush(client, cfg.migrationsDir, {
372
+ dryRun,
373
+ includeSeed,
374
+ strict: hasFlag(argv, "strict")
375
+ });
376
+ if (dryRun) {
377
+ if (res.pending.length === 0) {
378
+ console.log("No pending migrations.");
379
+ } else {
380
+ console.log(`Pending (${res.pending.length}):`);
381
+ res.pending.forEach((f) => console.log(` ${f}`));
382
+ }
383
+ } else if (res.applied.length === 0 && res.skipped.length === 0) {
384
+ console.log("No pending migrations.");
385
+ } else {
386
+ console.log(`
387
+ Applied ${res.applied.length}, skipped ${res.skipped.length}.`);
388
+ }
389
+ break;
390
+ }
391
+ case "baseline": {
392
+ await client.preflight();
393
+ const marked = await dbBaseline(client, cfg.migrationsDir, {
394
+ through: getArg(argv, "through") || void 0,
395
+ includeSeed
396
+ });
397
+ if (marked.length === 0) {
398
+ console.log("Nothing to baseline (all discovered files already tracked).");
399
+ } else {
400
+ console.log(`Baselined ${marked.length} file(s) as applied (not executed):`);
401
+ marked.forEach((f) => console.log(` ${f}`));
402
+ }
403
+ break;
404
+ }
405
+ default:
406
+ console.error(`Unknown db subcommand: ${sub ?? "(none)"}
407
+ `);
408
+ console.log(HELP);
409
+ process.exit(1);
410
+ }
411
+ }
412
+ main().catch((err) => {
413
+ console.error(`
414
+ ${err instanceof Error ? err.message : String(err)}`);
415
+ process.exit(1);
416
+ });