@sfutureapps/db-sdk 0.3.32 → 3.0.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,17 +1,59 @@
1
1
  # @sfutureapps/db-sdk
2
2
 
3
- JS SDK for ThinkPHP DB Gateway (MySQL)
3
+ JS SDK for a ThinkPHP DB Gateway (MySQL).
4
+
5
+ It provides a chainable query builder for:
6
+ - selecting rows with filters, ordering, pagination
7
+ - joins, group/having, distinct
8
+ - aggregate helpers (`count`, `sum`, `avg`, `min`, `max`)
9
+ - inserts, updates, deletes (updates/deletes require filters)
10
+
11
+ Under the hood it calls the gateway endpoints:
12
+ - `POST v3/api/query`
13
+ - `POST v3/api/insert`
14
+ - `POST v3/api/update`
15
+ - `POST v3/api/delete`
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm i @sfutureapps/db-sdk
21
+ # or
22
+ pnpm add @sfutureapps/db-sdk
23
+ # or
24
+ bun add @sfutureapps/db-sdk
25
+ ```
26
+
27
+ ## Create a client
4
28
 
5
- ## Create client
6
29
  ```ts
7
30
  import { createClient } from "@sfutureapps/db-sdk";
8
31
 
9
- const db = createClient("https://yourdomain.com", {
32
+ const db = createClient("https://yourdomain.com/api", {
33
+ // optional
34
+ apiKey: "YOUR_API_KEY",
35
+ // optional (recommended for SSR or custom storage)
10
36
  accessToken: () => localStorage.getItem("token"),
37
+ // optional extra headers
38
+ headers: { "X-App-Version": "1.0.0" },
11
39
  });
12
40
  ```
13
41
 
14
- ## Select + pagination
42
+ ### Environment-variable configuration (optional)
43
+
44
+ If you prefer env vars (or you already have apps using them), the SDK also reads:
45
+ - `API_URL` (also supports `VITE_API_URL`, `NEXT_PUBLIC_API_URL`, `REACT_APP_API_URL`)
46
+ - `API_KEY`
47
+ - `TOKEN_NAME` (default: `token`)
48
+ - `IS_AUTH` (`true/1/yes` enables token requirement)
49
+ - `INCLUDE_TOKEN_IN_GET` (default: `true`)
50
+
51
+ If you pass `baseUrl` to `createClient(...)`, it takes precedence over `API_URL`.
52
+
53
+ ## How to use
54
+
55
+ ### Select + pagination
56
+
15
57
  ```ts
16
58
  const res = await db
17
59
  .from("booking_paginate_view")
@@ -22,25 +64,129 @@ const res = await db
22
64
  .withCount()
23
65
  .execute();
24
66
 
25
- console.log(res.data);
26
- console.log(res.meta); // { page, limit, offset, total, last_page }
67
+ if (res.error) throw new Error(res.error.message);
68
+
69
+ console.log(res.data); // rows
70
+ console.log(res.paginate); // { page, limit, offset, total, last_page, ... }
71
+ ```
72
+
73
+ ### Single row
74
+
75
+ ```ts
76
+ const res = await db.from("bookings").select("*").eq("id", 123).single().execute();
77
+ if (res.error) throw new Error(res.error.message);
78
+ console.log(res.data); // object | null
27
79
  ```
28
80
 
29
- ## Insert / Update / Delete
81
+ ### Insert / Update / Delete
82
+
30
83
  ```ts
31
84
  await db.from("bookings").insert({ property_id: 1, user_id: 2, total_amount: 50 });
32
85
 
86
+ // Update/Delete require at least one filter
33
87
  await db.from("bookings").eq("id", 123).update({ status: "paid" });
34
-
35
88
  await db.from("bookings").eq("id", 123).delete();
36
89
  ```
37
90
 
38
- ## Aggregate
91
+ ### Aggregates
92
+
39
93
  ```ts
40
94
  const totalPaid = await db.from("bookings").eq("status", "paid").sum("total_amount");
95
+ if (totalPaid.error) throw new Error(totalPaid.error.message);
41
96
  console.log(totalPaid.data);
97
+
98
+ // Notes:
99
+ // - .withCount() = pagination total rows (res.paginate.total)
100
+ // - .count() = aggregate COUNT(*) result in res.data
42
101
  ```
43
102
 
44
- ## Count notes
45
- - `.withCount()` = pagination total rows (meta)
46
- - `.count()` = aggregate COUNT(*)
103
+ ## Full example (TypeScript)
104
+
105
+ This is a complete end-to-end example covering read + paginate, joins, aggregates, and writes.
106
+
107
+ ```ts
108
+ import { createClient } from "@sfutureapps/db-sdk";
109
+
110
+ type Booking = {
111
+ id: number;
112
+ property_id: number;
113
+ user_id: number;
114
+ status: "draft" | "paid" | "cancelled" | string;
115
+ total_amount: number;
116
+ createtime: string;
117
+ };
118
+
119
+ async function main() {
120
+ const db = createClient("https://yourdomain.com/api", {
121
+ apiKey: "YOUR_API_KEY",
122
+ accessToken: () => {
123
+ // Browser: read from localStorage (or cookie)
124
+ // SSR/Node: return process.env.TOKEN
125
+ return typeof window !== "undefined" ? localStorage.getItem("token") : null;
126
+ },
127
+ });
128
+
129
+ // 1) List bookings (page 1, 20 per page) + total count
130
+ const list = await db
131
+ .from<Booking>("bookings")
132
+ .select(["id", "property_id", "user_id", "status", "total_amount", "createtime"])
133
+ .eq("status", "paid")
134
+ .order("createtime", { ascending: false })
135
+ .page(1, 20)
136
+ .withCount()
137
+ .execute();
138
+
139
+ if (list.error) throw new Error(list.error.message);
140
+ console.log("rows:", list.data.length);
141
+ console.log("paginate:", list.paginate);
142
+
143
+ // 2) Fetch a single booking
144
+ const one = await db.from<Booking>("bookings").select("*").eq("id", 123).single().execute();
145
+ if (one.error) throw new Error(one.error.message);
146
+ console.log("booking:", one.data);
147
+
148
+ // 3) Join example (join clause depends on your gateway/SQL conventions)
149
+ // NOTE: 'on' is passed through to the gateway.
150
+ const joined = await db
151
+ .from<any>("bookings b")
152
+ .select(["b.id", "b.total_amount", "p.name as property_name"])
153
+ .join("properties p", "p.id = b.property_id", "LEFT")
154
+ .gte("b.total_amount", 50)
155
+ .limit(10)
156
+ .execute();
157
+
158
+ if (joined.error) throw new Error(joined.error.message);
159
+ console.log("joined:", joined.data);
160
+
161
+ // 4) Aggregates
162
+ const sum = await db.from<Booking>("bookings").eq("status", "paid").sum("total_amount");
163
+ if (sum.error) throw new Error(sum.error.message);
164
+ console.log("total paid:", sum.data);
165
+
166
+ // 5) Insert
167
+ const inserted = await db.from<Booking>("bookings").insert({
168
+ property_id: 1,
169
+ user_id: 2,
170
+ status: "draft",
171
+ total_amount: 50,
172
+ });
173
+
174
+ if (inserted.error) throw new Error(inserted.error.message);
175
+ console.log("inserted:", inserted.data);
176
+
177
+ // 6) Update (requires filters)
178
+ const updated = await db.from<Booking>("bookings").eq("id", 123).update({ status: "paid" });
179
+ if (updated.error) throw new Error(updated.error.message);
180
+ console.log("updated:", updated.data);
181
+
182
+ // 7) Delete (requires filters)
183
+ const deleted = await db.from<Booking>("bookings").eq("id", 123).delete();
184
+ if (deleted.error) throw new Error(deleted.error.message);
185
+ console.log("deleted:", deleted.data);
186
+ }
187
+
188
+ main().catch((err) => {
189
+ console.error(err);
190
+ process.exitCode = 1;
191
+ });
192
+ ```
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@sfutureapps/db-sdk",
3
- "version": "0.3.32",
3
+ "version": "3.0.35",
4
4
  "description": "SfutureApps JS SDK for ThinkPHP DB Gateway (MySQL)",
5
- "license": "MIT",
5
+ "license": "sFutureApps EULA v7.0",
6
6
  "type": "module",
7
7
  "types": "./src/index.d.ts",
8
8
  "main": "./src/index.js",
@@ -27,8 +27,5 @@
27
27
  "typescript",
28
28
  "sFutureApps",
29
29
  "sfutureapps.com"
30
- ],
31
- "dependencies": {
32
- "axios": "^1.13.2"
33
- }
30
+ ]
34
31
  }
package/src/client.js CHANGED
@@ -25,34 +25,64 @@ function getEnv(key, fallback = "") {
25
25
  return fallback;
26
26
  }
27
27
 
28
- // Only accept explicit API_URL (which also maps to NEXT_PUBLIC_API_URL / VITE_API_URL / REACT_APP_API_URL via getEnv)
29
- const API_URL = (getEnv("API_URL") || "").replace(/\/+$/, "");
30
- const API_KEY = getEnv("API_KEY") || "";
31
- const TOKEN_NAME = getEnv("TOKEN_NAME", "token") || "token";
32
- const IS_AUTH = String(getEnv("IS_AUTH") || "").toLowerCase();
33
- const REQUIRE_AUTH = [ "true", "1", "yes" ].includes(IS_AUTH);
28
+ // --- Runtime configuration ---------------------------------------------------
29
+ // Defaults come from env so existing apps keep working.
30
+ const defaultBaseUrl = (getEnv("API_URL") || "").replace(/\/+$/, "");
31
+ const defaultApiKey = getEnv("API_KEY") || "";
32
+ const defaultTokenName = getEnv("TOKEN_NAME", "token") || "token";
33
+ const defaultIsAuth = String(getEnv("IS_AUTH") || "").toLowerCase();
34
+ const defaultRequireAuth = [ "true", "1", "yes" ].includes(defaultIsAuth);
34
35
 
35
36
  // If you never want token on GET, set INCLUDE_TOKEN_IN_GET=false explicitly.
36
- const INCLUDE_TOKEN_IN_GET = String(getEnv("INCLUDE_TOKEN_IN_GET", "true")).toLowerCase();
37
- const ADD_TOKEN_TO_GET = [ "true", "1", "yes" ].includes(INCLUDE_TOKEN_IN_GET);
38
-
39
- if (!API_URL) {
40
- throw new Error(
41
- "API_URL is required. For Vite use VITE_API_URL, for Next use NEXT_PUBLIC_API_URL, for CRA use REACT_APP_API_URL."
42
- );
37
+ const defaultIncludeTokenInGet = String(getEnv("INCLUDE_TOKEN_IN_GET", "true")).toLowerCase();
38
+ const defaultAddTokenToGet = [ "true", "1", "yes" ].includes(defaultIncludeTokenInGet);
39
+
40
+ const clientConfig = {
41
+ baseUrl: defaultBaseUrl,
42
+ apiKey: defaultApiKey,
43
+ tokenName: defaultTokenName,
44
+ requireAuth: defaultRequireAuth,
45
+ addTokenToGet: defaultAddTokenToGet,
46
+ accessToken: null,
47
+ headers: {},
48
+ };
49
+
50
+ export function configureClient(opts = {}) {
51
+ if (!opts || typeof opts !== "object") return;
52
+
53
+ if (typeof opts.baseUrl === "string") {
54
+ clientConfig.baseUrl = opts.baseUrl.replace(/\/+$/, "");
55
+ }
56
+ if (typeof opts.apiKey === "string") {
57
+ clientConfig.apiKey = opts.apiKey;
58
+ }
59
+ if (typeof opts.accessToken === "function") {
60
+ clientConfig.accessToken = opts.accessToken;
61
+ }
62
+ if (opts.headers && typeof opts.headers === "object") {
63
+ clientConfig.headers = { ...opts.headers };
64
+ }
43
65
  }
44
66
 
45
67
  // --- Utilities ---------------------------------------------------------------
46
68
 
47
69
  export function getToken() {
70
+ // Prefer explicit token provider (supports SSR + custom storage)
71
+ try {
72
+ if (typeof clientConfig.accessToken === "function") {
73
+ const t = clientConfig.accessToken();
74
+ if (t) return String(t);
75
+ }
76
+ } catch (_) { }
77
+
48
78
  try {
49
79
  if (typeof window !== "undefined" && window.localStorage) {
50
- const t = window.localStorage.getItem(TOKEN_NAME);
80
+ const t = window.localStorage.getItem(clientConfig.tokenName);
51
81
  if (t) return t;
52
82
  }
53
83
  } catch (_) { }
54
84
  // Fallback (SSR): allow env var named as TOKEN_NAME to carry the value
55
- const fromEnv = getEnv(TOKEN_NAME, "");
85
+ const fromEnv = getEnv(clientConfig.tokenName, "");
56
86
  return fromEnv || "";
57
87
  }
58
88
 
@@ -78,7 +108,12 @@ export function toForm(data) {
78
108
  * - Accepts endpoint with or without leading slash.
79
109
  */
80
110
  export function buildUrl(endpoint = "", params) {
81
- const base = API_URL.replace(/\/+$/, ""); // trim trailing slashes
111
+ const base = String(clientConfig.baseUrl || "").replace(/\/+$/, ""); // trim trailing slashes
112
+ if (!base) {
113
+ throw new Error(
114
+ "Base URL is required. Pass it to createClient(baseUrl) or set API_URL (VITE_API_URL / NEXT_PUBLIC_API_URL / REACT_APP_API_URL)."
115
+ );
116
+ }
82
117
  const ep = String(endpoint || "").replace(/^\/+/, ""); // trim leading slashes
83
118
  const full = ep ? `${base}/${ep}` : base;
84
119
  const url = new URL(full);
@@ -129,15 +164,16 @@ export async function get({ endpoint, params } = {}) {
129
164
  // console.log("GET", url.toString());
130
165
 
131
166
 
132
- if (REQUIRE_AUTH && ADD_TOKEN_TO_GET) {
167
+ if (clientConfig.requireAuth && clientConfig.addTokenToGet) {
133
168
  const token = getToken();
134
- if (!token) throw new Error(`Auth required but token '${TOKEN_NAME}' not found`);
169
+ if (!token) throw new Error(`Auth required but token '${clientConfig.tokenName}' not found`);
135
170
  url.searchParams.set("token", token);
136
171
  }
137
172
 
138
173
  const headers = {
139
174
  Accept: "application/json",
140
- ...(API_KEY ? { "X-API-Key": API_KEY } : {}),
175
+ ...(clientConfig.apiKey ? { "X-API-Key": clientConfig.apiKey } : {}),
176
+ ...(clientConfig.headers || {}),
141
177
  };
142
178
 
143
179
  return httpFetch(url.toString(), { method: "POST", headers });
@@ -162,14 +198,15 @@ export default async function post({ endpoint, data, params } = {}) {
162
198
 
163
199
  const headers = {
164
200
  "Content-Type": "application/x-www-form-urlencoded",
165
- ...(API_KEY ? { "X-API-Key": API_KEY } : {}),
201
+ ...(clientConfig.apiKey ? { "X-API-Key": clientConfig.apiKey } : {}),
202
+ ...(clientConfig.headers || {}),
166
203
  };
167
204
 
168
205
  const bodyObj = { ...(data || {}) };
169
206
 
170
- if (REQUIRE_AUTH) {
207
+ if (clientConfig.requireAuth) {
171
208
  const token = getToken();
172
- if (!token) throw new Error(`Auth required but token '${TOKEN_NAME}' not found`);
209
+ if (!token) throw new Error(`Auth required but token '${clientConfig.tokenName}' not found`);
173
210
  bodyObj.token = token;
174
211
  }
175
212
 
package/src/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { QueryBuilder } from "./query-builder.js";
2
+ import { configureClient } from "./client.js";
2
3
 
3
4
  export function createClient(baseUrl, opts = {}) {
5
+ configureClient({ baseUrl, ...opts });
4
6
  return {
5
7
  from(table) {
6
8
  return new QueryBuilder(table);