@x12i/helpers 1.0.0 → 1.0.1

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
@@ -33,6 +33,45 @@ You can also import the mapper directly:
33
33
  const { mapObject } = require("@x12i/helpers/objects-mapper");
34
34
  ```
35
35
 
36
+ ### HTTP tools (request ⇄ curl, base URLs)
37
+
38
+ ```js
39
+ const {
40
+ requestToCurl,
41
+ curlToRequest,
42
+ buildUrl,
43
+ createBaseUrlClient,
44
+ } = require("@x12i/helpers");
45
+
46
+ const curl = requestToCurl({
47
+ method: "POST",
48
+ url: "https://api.example.com/v1/users",
49
+ headers: { authorization: "Bearer TOKEN" },
50
+ body: { name: "Jane" },
51
+ });
52
+
53
+ // curl -X POST -H 'authorization: Bearer TOKEN' -H 'content-type: application/json' --data-raw '{"name":"Jane"}' 'https://api.example.com/v1/users'
54
+ console.log(curl);
55
+
56
+ const req = curlToRequest(curl);
57
+ // { method: 'POST', url: 'https://api.example.com/v1/users', headers: { authorization: 'Bearer TOKEN', 'content-type': 'application/json' }, body: '{"name":"Jane"}' }
58
+ console.log(req);
59
+
60
+ console.log(buildUrl("https://api.example.com/", "/v1/users", { limit: 10, tags: ["a", "b"] }));
61
+ // https://api.example.com/v1/users?limit=10&tags=a&tags=b
62
+
63
+ const api = createBaseUrlClient("https://api.example.com/", {
64
+ headers: { authorization: "Bearer TOKEN" },
65
+ });
66
+ console.log(api.curl("/v1/health"));
67
+ ```
68
+
69
+ Direct import:
70
+
71
+ ```js
72
+ const { requestToCurl } = require("@x12i/helpers/http-tools");
73
+ ```
74
+
36
75
  ## API
37
76
 
38
77
  - `mapObject(source, mapping, opts)`
@@ -40,4 +79,8 @@ const { mapObject } = require("@x12i/helpers/objects-mapper");
40
79
  - `mapArray(sourceArray, mapping, opts)`
41
80
  - `deepGet(obj, path)`
42
81
  - `deepSet(obj, path, value)`
82
+ - `requestToCurl(req)`
83
+ - `curlToRequest(curlCommand)`
84
+ - `buildUrl(baseUrl, pathOrUrl, query)`
85
+ - `createBaseUrlClient(baseUrl, defaults)`
43
86
 
package/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  module.exports = {
2
2
  ...require("./src/helpers/objectsMapper.js"),
3
+ ...require("./src/helpers/httpTools.js"),
3
4
  };
4
5
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@x12i/helpers",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Small helper utilities for x12i projects.",
5
5
  "license": "MIT",
6
6
  "author": "x12i",
@@ -20,7 +20,8 @@
20
20
  "main": "./index.js",
21
21
  "exports": {
22
22
  ".": "./index.js",
23
- "./objects-mapper": "./src/helpers/objectsMapper.js"
23
+ "./objects-mapper": "./src/helpers/objectsMapper.js",
24
+ "./http-tools": "./src/helpers/httpTools.js"
24
25
  },
25
26
  "files": [
26
27
  "index.js",
@@ -32,4 +33,3 @@
32
33
  "test": "node --test"
33
34
  }
34
35
  }
35
-
@@ -0,0 +1,215 @@
1
+ function shellEscapeSingleQuotes(s) {
2
+ // Wrap in single quotes, escape existing single quotes for POSIX shells.
3
+ // abc'def -> 'abc'"'"'def'
4
+ return `'${String(s).replace(/'/g, `'\"'\"'`)}'`;
5
+ }
6
+
7
+ function normalizeHeaders(headers) {
8
+ if (!headers) return {};
9
+ if (Array.isArray(headers)) {
10
+ // [ [k,v], ... ]
11
+ return Object.fromEntries(headers);
12
+ }
13
+ if (headers instanceof Map) {
14
+ return Object.fromEntries(headers.entries());
15
+ }
16
+ return { ...headers };
17
+ }
18
+
19
+ /**
20
+ * Build a fully-qualified URL from base + path + query.
21
+ *
22
+ * - If `pathOrUrl` is already absolute (http:// or https://), it is used as-is.
23
+ * - Query values can be strings/numbers/booleans/arrays; null/undefined are skipped.
24
+ */
25
+ function buildUrl(baseUrl, pathOrUrl = "", query) {
26
+ const raw = String(pathOrUrl || "");
27
+ const isAbsolute = /^https?:\/\//i.test(raw);
28
+
29
+ const base = isAbsolute ? new URL(raw) : new URL(raw.replace(/^\//, ""), String(baseUrl || ""));
30
+
31
+ if (query && typeof query === "object") {
32
+ for (const [k, v] of Object.entries(query)) {
33
+ if (v == null) continue;
34
+ if (Array.isArray(v)) {
35
+ for (const item of v) {
36
+ if (item == null) continue;
37
+ base.searchParams.append(k, String(item));
38
+ }
39
+ } else {
40
+ base.searchParams.set(k, String(v));
41
+ }
42
+ }
43
+ }
44
+
45
+ return base.toString();
46
+ }
47
+
48
+ /**
49
+ * Convert a request description into a curl command.
50
+ *
51
+ * @param {object} req
52
+ * @param {string} req.url
53
+ * @param {string} [req.method]
54
+ * @param {object} [req.headers]
55
+ * @param {any} [req.body] - string/object/buffer-ish; objects are JSON.stringified
56
+ */
57
+ function requestToCurl(req) {
58
+ if (!req || typeof req !== "object") throw new TypeError("requestToCurl: req must be an object");
59
+ if (!req.url) throw new TypeError("requestToCurl: req.url is required");
60
+
61
+ const method = String(req.method || "GET").toUpperCase();
62
+ const headers = normalizeHeaders(req.headers);
63
+
64
+ const parts = ["curl"];
65
+
66
+ if (method !== "GET") {
67
+ parts.push("-X", method);
68
+ }
69
+
70
+ for (const [k, v] of Object.entries(headers)) {
71
+ if (v == null) continue;
72
+ parts.push("-H", shellEscapeSingleQuotes(`${k}: ${v}`));
73
+ }
74
+
75
+ if (req.body !== undefined) {
76
+ let body = req.body;
77
+ if (body && typeof body === "object" && !Buffer.isBuffer(body)) {
78
+ body = JSON.stringify(body);
79
+ // Add content-type if caller didn't set it.
80
+ const hasCt = Object.keys(headers).some((h) => h.toLowerCase() === "content-type");
81
+ if (!hasCt) parts.push("-H", shellEscapeSingleQuotes("content-type: application/json"));
82
+ }
83
+ parts.push("--data-raw", shellEscapeSingleQuotes(body));
84
+ }
85
+
86
+ parts.push(shellEscapeSingleQuotes(req.url));
87
+ return parts.join(" ");
88
+ }
89
+
90
+ function unquote(s) {
91
+ const str = String(s);
92
+ if ((str.startsWith("'") && str.endsWith("'")) || (str.startsWith('"') && str.endsWith('"'))) {
93
+ return str.slice(1, -1);
94
+ }
95
+ return str;
96
+ }
97
+
98
+ function tokenizeCurl(cmd) {
99
+ const s = String(cmd).trim();
100
+ const tokens = [];
101
+ let i = 0;
102
+ while (i < s.length) {
103
+ while (s[i] === " " || s[i] === "\t" || s[i] === "\n") i++;
104
+ if (i >= s.length) break;
105
+
106
+ const q = s[i];
107
+ if (q === "'" || q === '"') {
108
+ let j = i + 1;
109
+ let out = "";
110
+ while (j < s.length) {
111
+ const ch = s[j];
112
+ if (ch === q) break;
113
+ if (q === '"' && ch === "\\" && j + 1 < s.length) {
114
+ out += s[j + 1];
115
+ j += 2;
116
+ continue;
117
+ }
118
+ out += ch;
119
+ j++;
120
+ }
121
+ tokens.push(out);
122
+ i = j + 1;
123
+ } else {
124
+ let j = i;
125
+ while (j < s.length && !/\s/.test(s[j])) j++;
126
+ tokens.push(s.slice(i, j));
127
+ i = j;
128
+ }
129
+ }
130
+ return tokens;
131
+ }
132
+
133
+ /**
134
+ * Parse a curl command into a request-like object.
135
+ *
136
+ * Supports: -X/--request, -H/--header, -d/--data/--data-raw, and a URL token.
137
+ */
138
+ function curlToRequest(curlCommand) {
139
+ const tokens = tokenizeCurl(curlCommand);
140
+ if (!tokens.length || tokens[0] !== "curl") throw new TypeError("curlToRequest: command must start with curl");
141
+
142
+ let method = "GET";
143
+ let url;
144
+ const headers = {};
145
+ let body;
146
+
147
+ for (let i = 1; i < tokens.length; i++) {
148
+ const t = tokens[i];
149
+ if (t === "-X" || t === "--request") {
150
+ method = String(tokens[++i] || "").toUpperCase();
151
+ continue;
152
+ }
153
+ if (t === "-H" || t === "--header") {
154
+ const hv = tokens[++i];
155
+ const raw = unquote(hv);
156
+ const idx = raw.indexOf(":");
157
+ if (idx !== -1) {
158
+ const k = raw.slice(0, idx).trim();
159
+ const v = raw.slice(idx + 1).trim();
160
+ headers[k] = v;
161
+ }
162
+ continue;
163
+ }
164
+ if (t === "-d" || t === "--data" || t === "--data-raw" || t === "--data-binary") {
165
+ body = unquote(tokens[++i]);
166
+ if (method === "GET") method = "POST";
167
+ continue;
168
+ }
169
+ if (!t.startsWith("-") && !url) {
170
+ url = unquote(t);
171
+ continue;
172
+ }
173
+ }
174
+
175
+ if (!url) throw new TypeError("curlToRequest: URL not found");
176
+
177
+ return { method, url, headers, body };
178
+ }
179
+
180
+ /**
181
+ * Create a small request builder around a base URL.
182
+ */
183
+ function createBaseUrlClient(baseUrl, defaults = {}) {
184
+ const defaultHeaders = normalizeHeaders(defaults.headers);
185
+
186
+ return {
187
+ buildUrl: (pathOrUrl, query) => buildUrl(baseUrl, pathOrUrl, query),
188
+ request: (pathOrUrl, options = {}) => {
189
+ const url = buildUrl(baseUrl, pathOrUrl, options.query);
190
+ const headers = { ...defaultHeaders, ...normalizeHeaders(options.headers) };
191
+ return {
192
+ method: (options.method || "GET").toUpperCase(),
193
+ url,
194
+ headers,
195
+ body: options.body,
196
+ };
197
+ },
198
+ curl: (pathOrUrl, options = {}) => requestToCurl(
199
+ {
200
+ ...defaults,
201
+ ...options,
202
+ url: buildUrl(baseUrl, pathOrUrl, options.query),
203
+ headers: { ...defaultHeaders, ...normalizeHeaders(options.headers) },
204
+ }
205
+ ),
206
+ };
207
+ }
208
+
209
+ module.exports = {
210
+ buildUrl,
211
+ requestToCurl,
212
+ curlToRequest,
213
+ createBaseUrlClient,
214
+ };
215
+