@sfutureapps/db-sdk 0.3.15 → 0.3.17

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/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@sfutureapps/db-sdk",
3
- "version": "0.3.15",
3
+ "version": "0.3.17",
4
4
  "description": "SfutureApps JS SDK for ThinkPHP DB Gateway (MySQL)",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
+ "types": "./src/index.d.ts",
7
8
  "main": "./src/index.js",
8
9
  "exports": {
9
10
  ".": "./src/index.js"
package/src/client.js CHANGED
@@ -1,53 +1,191 @@
1
- import axios from "axios";
2
-
3
- export class HttpClient {
4
- constructor(baseUrl, opts = {}) {
5
- this.baseUrl = String(baseUrl).replace(/\/+$/, "");
6
- this.apiKey = opts.apiKey;
7
- this.accessToken = opts.accessToken;
8
- this.extraHeaders = opts.headers || {};
9
-
10
- this.ax = axios.create({
11
- baseURL: this.baseUrl,
12
- timeout: opts.timeoutMs || 15000,
13
- headers: {
14
- "Content-Type": "application/json",
15
- ...this.extraHeaders,
16
- },
17
- });
18
- }
19
-
20
- async post(path, body, config = {}) {
21
- const headers = {
22
- ...this.extraHeaders,
23
- ...(config.headers || {}),
24
- };
25
-
26
- if (this.apiKey) headers["x-api-key"] = this.apiKey;
27
-
28
- const token = this.accessToken?.();
29
- if (token) headers["Authorization"] = `Bearer ${token}`;
30
-
31
- try {
32
- const res = await this.ax.post(path, body, { ...config, headers });
33
- const payload = res.data || {};
34
- return {
35
- data: payload.data ?? null,
36
- count: payload.count ?? null,
37
- meta: payload.meta ?? null,
38
- error: payload.error ?? null,
39
- };
40
- } catch (e) {
41
- const status = e?.response?.status ?? 0;
42
- const payload = e?.response?.data ?? null;
43
- return {
44
- data: null,
45
- error: {
46
- status,
47
- message: payload?.error?.message ?? e?.message ?? "Network error",
48
- details: payload ?? e,
49
- },
50
- };
1
+ // form-urlencoded request helpers (Vite / React / Next / Node safe)
2
+
3
+ // --- Env helpers -------------------------------------------------------------
4
+
5
+ function getEnv(key, fallback = "") {
6
+ // Node / Next server
7
+ if (typeof process !== "undefined" && process.env) {
8
+ if (process.env[ key ] !== undefined) return process.env[ key ];
9
+ if (process.env[ `NEXT_PUBLIC_${key}` ] !== undefined) return process.env[ `NEXT_PUBLIC_${key}` ];
10
+ if (process.env[ `VITE_${key}` ] !== undefined) return process.env[ `VITE_${key}` ];
11
+ if (process.env[ `REACT_APP_${key}` ] !== undefined) return process.env[ `REACT_APP_${key}` ];
12
+ }
13
+ // Vite client
14
+ if (typeof import.meta !== "undefined" && import.meta.env) {
15
+ if (import.meta.env[ key ] !== undefined) return import.meta.env[ key ];
16
+ if (import.meta.env[ `VITE_${key}` ] !== undefined) return import.meta.env[ `VITE_${key}` ];
17
+ if (import.meta.env[ `NEXT_PUBLIC_${key}` ] !== undefined) return import.meta.env[ `NEXT_PUBLIC_${key}` ];
18
+ if (import.meta.env[ `REACT_APP_${key}` ] !== undefined) return import.meta.env[ `REACT_APP_${key}` ];
19
+ }
20
+ // Browser globals (optional)
21
+ if (typeof window !== "undefined") {
22
+ if (window[ key ] !== undefined) return window[ key ];
23
+ if (window.ENV?.[ key ] !== undefined) return window.ENV[ key ];
24
+ }
25
+ return fallback;
26
+ }
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);
34
+
35
+ // 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 (or BASE_URL) is required. For Vite use VITE_API_URL, for Next use NEXT_PUBLIC_API_URL, for CRA use REACT_APP_API_URL."
42
+ );
43
+ }
44
+
45
+ // --- Utilities ---------------------------------------------------------------
46
+
47
+ export function getToken() {
48
+ try {
49
+ if (typeof window !== "undefined" && window.localStorage) {
50
+ const t = window.localStorage.getItem(TOKEN_NAME);
51
+ if (t) return t;
51
52
  }
53
+ } catch (_) { }
54
+ // Fallback (SSR): allow env var named as TOKEN_NAME to carry the value
55
+ const fromEnv = getEnv(TOKEN_NAME, "");
56
+ return fromEnv || "";
57
+ }
58
+
59
+ export function toForm(data) {
60
+ if (!data || typeof data !== "object") return undefined;
61
+ const form = new URLSearchParams();
62
+ for (const [ k, v ] of Object.entries(data)) {
63
+ if (v === undefined || v === null) continue;
64
+ if (Array.isArray(v)) {
65
+ for (const item of v) form.append(k, String(item));
66
+ } else if (typeof v === "object") {
67
+ form.append(k, JSON.stringify(v));
68
+ } else {
69
+ form.append(k, String(v));
70
+ }
71
+ }
72
+ return form.toString();
73
+ }
74
+
75
+ /**
76
+ * Build a full URL by joining API_URL and endpoint, then append params.
77
+ * - Keeps API_URL path segments (e.g., '/api') intact.
78
+ * - Accepts endpoint with or without leading slash.
79
+ */
80
+ export function buildUrl(endpoint = "", params) {
81
+ const base = API_URL.replace(/\/+$/, ""); // trim trailing slashes
82
+ const ep = String(endpoint || "").replace(/^\/+/, ""); // trim leading slashes
83
+ const full = ep ? `${base}/${ep}` : base;
84
+ const url = new URL(full);
85
+
86
+ if (params && typeof params === "object") {
87
+ for (const [ k, v ] of Object.entries(params)) {
88
+ if (v === undefined || v === null) continue;
89
+ if (Array.isArray(v)) {
90
+ for (const item of v) url.searchParams.append(k, String(item));
91
+ } else if (typeof v === "object") {
92
+ url.searchParams.append(k, JSON.stringify(v));
93
+ } else {
94
+ url.searchParams.append(k, String(v));
95
+ }
96
+ }
97
+ }
98
+ return url;
99
+ }
100
+
101
+ export async function httpFetch(input, init = {}) {
102
+ const { timeoutMs = 15000, ...rest } = init;
103
+ const ac = new AbortController();
104
+ const id = setTimeout(() => ac.abort(), timeoutMs);
105
+ try {
106
+ const res = await fetch(input, { ...rest, signal: ac.signal });
107
+ const ct = res.headers.get("content-type") || "";
108
+ const payload = ct.includes("application/json") ? await res.json() : await res.text();
109
+ if (!res.ok) {
110
+ const err = new Error(`HTTP ${res.status} ${res.statusText}`);
111
+ err.body = payload;
112
+ throw err;
113
+ }
114
+ return payload;
115
+ } finally {
116
+ clearTimeout(id);
52
117
  }
53
118
  }
119
+
120
+ // --- HTTP helpers ------------------------------------------------------------
121
+
122
+ export async function get({ endpoint, params } = {}) {
123
+ if (!endpoint || typeof endpoint !== "string") {
124
+ throw new Error("Invalid URL");
125
+ }
126
+
127
+ const url = buildUrl(endpoint, params);
128
+
129
+ // console.log("GET", url.toString());
130
+
131
+
132
+ if (REQUIRE_AUTH && ADD_TOKEN_TO_GET) {
133
+ const token = getToken();
134
+ if (!token) throw new Error(`Auth required but token '${TOKEN_NAME}' not found`);
135
+ url.searchParams.set("token", token);
136
+ }
137
+
138
+ const headers = {
139
+ Accept: "application/json",
140
+ ...(API_KEY ? { "X-API-Key": API_KEY } : {}),
141
+ };
142
+
143
+ return httpFetch(url.toString(), { method: "POST", headers });
144
+ }
145
+
146
+ export default async function post({ endpoint, data, params } = {}) {
147
+ if (!endpoint || typeof endpoint !== "string") {
148
+ throw new Error("Invalid URL");
149
+ }
150
+
151
+ // Compatibility with earlier behavior
152
+ if (!data && !params) return get({ endpoint });
153
+ if (!data && params) return get({ endpoint, params });
154
+
155
+ if (data !== null && typeof data !== "object") {
156
+ throw new Error("Invalid data");
157
+ }
158
+
159
+ const url = buildUrl(endpoint, params);
160
+
161
+ // console.log("POST", url.toString());
162
+
163
+ const headers = {
164
+ "Content-Type": "application/x-www-form-urlencoded",
165
+ Accept: "application/json",
166
+ ...(API_KEY ? { "X-API-Key": API_KEY } : {}),
167
+ };
168
+
169
+ const bodyObj = { ...(data || {}) };
170
+
171
+ if (REQUIRE_AUTH) {
172
+ const token = getToken();
173
+ if (!token) throw new Error(`Auth required but token '${TOKEN_NAME}' not found`);
174
+ bodyObj.token = token;
175
+ }
176
+
177
+ return httpFetch(url.toString(), {
178
+ method: "POST",
179
+ headers,
180
+ body: toForm(bodyObj),
181
+ });
182
+ }
183
+
184
+ /*
185
+ Usage notes:
186
+ - Set VITE_API_URL=https://m3menu.nealika.net/api (or NEXT_PUBLIC_API_URL / REACT_APP_API_URL).
187
+ - Call with endpoint WITHOUT a leading slash, e.g.:
188
+ await get({ endpoint: "hotel/list", params: { page: 1 } });
189
+ await post({ endpoint: "auth/login", data: { email, password } });
190
+ - If you pass a full absolute URL as `endpoint`, it will override the base (standard URL behavior).
191
+ */
package/src/index.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ // src/index.d.ts
2
+ export function createClient(
3
+ baseUrl: string,
4
+ opts?: {
5
+ apiKey?: string;
6
+ accessToken?: () => string | null;
7
+ headers?: Record<string, string>;
8
+ }
9
+ ): {
10
+ from<T = any>(table: string): any;
11
+ };
package/src/index.js CHANGED
@@ -1,11 +1,9 @@
1
- import { HttpClient } from "./client.js";
2
1
  import { QueryBuilder } from "./query-builder.js";
3
2
 
4
3
  export function createClient(baseUrl, opts = {}) {
5
- const http = new HttpClient(baseUrl, opts);
6
4
  return {
7
5
  from(table) {
8
- return new QueryBuilder(http, table);
6
+ return new QueryBuilder(table);
9
7
  },
10
8
  };
11
9
  }
@@ -1,9 +1,10 @@
1
+ import post from "./client";
2
+
1
3
  export class QueryBuilder {
2
- constructor(client, table) {
3
- this.client = client;
4
+ constructor(table) {
4
5
  this.tableName = table;
5
6
 
6
- this._select = ["*"];
7
+ this._select = [ "*" ];
7
8
  this._filters = [];
8
9
  this._order = [];
9
10
  this._limit = null;
@@ -22,7 +23,7 @@ export class QueryBuilder {
22
23
  // ---- select ----
23
24
  select(columns = "*") {
24
25
  const cols = String(columns).trim();
25
- this._select = cols === "*" || cols === "" ? ["*"] : cols.split(",").map(s => s.trim()).filter(Boolean);
26
+ this._select = cols === "*" || cols === "" ? [ "*" ] : cols.split(",").map(s => s.trim()).filter(Boolean);
26
27
  return this;
27
28
  }
28
29
 
@@ -124,12 +125,12 @@ export class QueryBuilder {
124
125
  if (this._page !== null) payload.page = this._page;
125
126
  if (this._offset !== null) payload.offset = this._offset;
126
127
 
127
- const res = await this.client.post("/db/v1/query", payload);
128
+ const res = await post({ endpoint: "/db/v1/query", data: payload });
128
129
 
129
130
  if (res.error) return res;
130
131
 
131
132
  if (this._single) {
132
- const first = Array.isArray(res.data) ? (res.data[0] ?? null) : null;
133
+ const first = Array.isArray(res.data) ? (res.data[ 0 ] ?? null) : null;
133
134
  return { data: first, error: null, count: res.count ?? null, meta: res.meta ?? null };
134
135
  }
135
136
 
@@ -138,11 +139,15 @@ export class QueryBuilder {
138
139
 
139
140
  // ---- aggregates ----
140
141
  _agg(fn, field = "*") {
141
- return this.client.post("/db/v1/query", {
142
- table: this.tableName,
143
- aggregate: { fn, field },
144
- filters: this._filters,
142
+ return post({
143
+ endpoint: "/db/v1/query",
144
+ data: {
145
+ table: this.tableName,
146
+ aggregate: { fn, field },
147
+ filters: this._filters,
148
+ }
145
149
  });
150
+
146
151
  }
147
152
 
148
153
  count(field = "*") { return this._agg("count", field); }
@@ -153,20 +158,29 @@ export class QueryBuilder {
153
158
 
154
159
  // ---- write ----
155
160
  insert(values) {
156
- return this.client.post("/db/v1/insert", { table: this.tableName, values });
161
+ return post({
162
+ endpoint: "/db/v1/insert",
163
+ data: { table: this.tableName, values }
164
+ });
157
165
  }
158
166
 
159
167
  update(values) {
160
168
  if (this._filters.length === 0) {
161
169
  return Promise.resolve({ data: null, error: { message: "Update requires filters (e.g. .eq('id', 1))" } });
162
170
  }
163
- return this.client.post("/db/v1/update", { table: this.tableName, values, filters: this._filters });
171
+ return post({
172
+ endpoint: "/db/v1/update",
173
+ data: { table: this.tableName, values, filters: this._filters }
174
+ });
164
175
  }
165
176
 
166
177
  delete() {
167
178
  if (this._filters.length === 0) {
168
179
  return Promise.resolve({ data: null, error: { message: "Delete requires filters (e.g. .eq('id', 1))" } });
169
180
  }
170
- return this.client.post("/db/v1/delete", { table: this.tableName, filters: this._filters });
181
+ return post({
182
+ endpoint: "/db/v1/delete",
183
+ data: { table: this.tableName, filters: this._filters }
184
+ });
171
185
  }
172
186
  }