@sfutureapps/db-sdk 0.3.16 → 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 +1 -1
- package/src/client.js +188 -50
- package/src/index.js +1 -3
- package/src/query-builder.js +27 -13
package/package.json
CHANGED
package/src/client.js
CHANGED
|
@@ -1,53 +1,191 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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.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(
|
|
6
|
+
return new QueryBuilder(table);
|
|
9
7
|
},
|
|
10
8
|
};
|
|
11
9
|
}
|
package/src/query-builder.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import post from "./client";
|
|
2
|
+
|
|
1
3
|
export class QueryBuilder {
|
|
2
|
-
constructor(
|
|
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
|
|
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
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
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
|
|
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
|
|
181
|
+
return post({
|
|
182
|
+
endpoint: "/db/v1/delete",
|
|
183
|
+
data: { table: this.tableName, filters: this._filters }
|
|
184
|
+
});
|
|
171
185
|
}
|
|
172
186
|
}
|