resora 0.1.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/dist/index.mjs ADDED
@@ -0,0 +1,368 @@
1
+ //#region src/ApiResource.ts
2
+ /**
3
+ * ApiResource function to return the Resource instance
4
+ *
5
+ * @param instance Resource instance
6
+ * @returns Resource instance
7
+ */
8
+ function ApiResource(instance) {
9
+ return instance;
10
+ }
11
+
12
+ //#endregion
13
+ //#region src/ServerResponse.ts
14
+ var ServerResponse = class {
15
+ _status = 200;
16
+ headers = {};
17
+ constructor(response, body) {
18
+ this.response = response;
19
+ this.body = body;
20
+ }
21
+ /**
22
+ * Set the HTTP status code for the response
23
+ *
24
+ * @param status
25
+ * @returns The current ServerResponse instance
26
+ */
27
+ setStatusCode(status) {
28
+ this._status = status;
29
+ if ("status" in this.response && typeof this.response.status === "function") this.response.status(status);
30
+ else if ("status" in this.response) this.response.status = status;
31
+ return this;
32
+ }
33
+ /**
34
+ * Get the current HTTP status code for the response
35
+ *
36
+ * @returns
37
+ */
38
+ status() {
39
+ return this._status;
40
+ }
41
+ /**
42
+ * Get the current HTTP status text for the response
43
+ *
44
+ * @returns
45
+ */
46
+ statusText() {
47
+ if ("statusMessage" in this.response) return this.response.statusMessage;
48
+ else if ("statusText" in this.response) return this.response.statusText;
49
+ }
50
+ /**
51
+ * Set a cookie in the response header
52
+ *
53
+ * @param name The name of the cookie
54
+ * @param value The value of the cookie
55
+ * @param options Optional cookie attributes (e.g., path, domain, maxAge)
56
+ * @returns The current ServerResponse instance
57
+ */
58
+ setCookie(name, value, options) {
59
+ this.#addHeader("Set-Cookie", `${name}=${value}; ${Object.entries(options || {}).map(([key, val]) => `${key}=${val}`).join("; ")}`);
60
+ return this;
61
+ }
62
+ /**
63
+ * Convert the resource to a JSON response body
64
+ *
65
+ * @param headers Optional headers to add to the response
66
+ * @returns The current ServerResponse instance
67
+ */
68
+ setHeaders(headers) {
69
+ for (const [key, value] of Object.entries(headers)) this.#addHeader(key, value);
70
+ return this;
71
+ }
72
+ /**
73
+ * Add a single header to the response
74
+ *
75
+ * @param key The name of the header
76
+ * @param value The value of the header
77
+ * @returns The current ServerResponse instance
78
+ */
79
+ header(key, value) {
80
+ this.#addHeader(key, value);
81
+ return this;
82
+ }
83
+ /**
84
+ * Add a single header to the response
85
+ *
86
+ * @param key The name of the header
87
+ * @param value The value of the header
88
+ */
89
+ #addHeader(key, value) {
90
+ this.headers[key] = value;
91
+ if ("headers" in this.response) this.response.headers.set(key, value);
92
+ else if ("setHeader" in this.response) this.response.setHeader(key, value);
93
+ }
94
+ /**
95
+ * Promise-like then method to allow chaining with async/await or .then() syntax
96
+ *
97
+ * @param onfulfilled Callback to handle the fulfilled state of the promise, receiving the response body
98
+ * @param onrejected Callback to handle the rejected state of the promise, receiving the error reason
99
+ * @returns A promise that resolves to the result of the onfulfilled or onrejected callback
100
+ */
101
+ then(onfulfilled, onrejected) {
102
+ const resolved = Promise.resolve(this.body).then(onfulfilled, onrejected);
103
+ if ("send" in this.response) this.response.send(this.body);
104
+ return resolved;
105
+ }
106
+ /**
107
+ * Promise-like catch method to handle rejected state of the promise
108
+ *
109
+ * @param onrejected
110
+ * @returns
111
+ */
112
+ catch(onrejected) {
113
+ return this.then(void 0, onrejected);
114
+ }
115
+ /**
116
+ * Promise-like finally method to handle cleanup after promise is settled
117
+ *
118
+ * @param onfinally
119
+ * @returns
120
+ */
121
+ finally(onfinally) {
122
+ return this.then(onfinally, onfinally);
123
+ }
124
+ };
125
+
126
+ //#endregion
127
+ //#region src/ResourceCollection.ts
128
+ /**
129
+ * ResourceCollection class to handle API resource transformation and response building for collections
130
+ */
131
+ var ResourceCollection = class {
132
+ body = { data: [] };
133
+ resource;
134
+ collects;
135
+ called = {};
136
+ constructor(rsc, res) {
137
+ this.res = res;
138
+ this.resource = rsc;
139
+ }
140
+ /**
141
+ * Get the original resource data
142
+ */
143
+ data() {
144
+ return this.toArray();
145
+ }
146
+ /**
147
+ * Convert resource to JSON response format
148
+ *
149
+ * @returns
150
+ */
151
+ json() {
152
+ if (!this.called.json) {
153
+ this.called.json = true;
154
+ let data = this.data();
155
+ if (this.collects) data = data.map((item) => new this.collects(item).data());
156
+ this.body = { data };
157
+ if (!Array.isArray(this.resource)) {
158
+ if (this.resource.pagination && this.resource.cursor) this.body.meta = {
159
+ pagination: this.resource.pagination,
160
+ cursor: this.resource.cursor
161
+ };
162
+ else if (this.resource.pagination) this.body.meta = { pagination: this.resource.pagination };
163
+ else if (this.resource.cursor) this.body.meta = { cursor: this.resource.cursor };
164
+ }
165
+ }
166
+ return this;
167
+ }
168
+ /**
169
+ * Flatten resource to return original data
170
+ *
171
+ * @returns
172
+ */
173
+ toArray() {
174
+ this.called.toArray = true;
175
+ this.json();
176
+ return Array.isArray(this.resource) ? [...this.resource] : [...this.resource.data];
177
+ }
178
+ /**
179
+ * Add additional properties to the response body
180
+ *
181
+ * @param extra Additional properties to merge into the response body
182
+ * @returns
183
+ */
184
+ additional(extra) {
185
+ this.called.additional = true;
186
+ this.json();
187
+ delete extra.cursor;
188
+ delete extra.pagination;
189
+ if (extra.data && Array.isArray(this.body.data)) this.body.data = [...this.body.data, ...extra.data];
190
+ this.body = {
191
+ ...this.body,
192
+ ...extra
193
+ };
194
+ return this;
195
+ }
196
+ response(res) {
197
+ this.called.toResponse = true;
198
+ return new ServerResponse(res ?? this.res, this.body);
199
+ }
200
+ setCollects(collects) {
201
+ this.collects = collects;
202
+ return this;
203
+ }
204
+ /**
205
+ * Promise-like then method to allow chaining with async/await or .then() syntax
206
+ *
207
+ * @param onfulfilled Callback to handle the fulfilled state of the promise, receiving the response body
208
+ * @param onrejected Callback to handle the rejected state of the promise, receiving the error reason
209
+ * @returns A promise that resolves to the result of the onfulfilled or onrejected callback
210
+ */
211
+ then(onfulfilled, onrejected) {
212
+ this.called.then = true;
213
+ this.json();
214
+ const resolved = Promise.resolve(this.body).then(onfulfilled, onrejected);
215
+ if (this.res) this.res.send(this.body);
216
+ return resolved;
217
+ }
218
+ /**
219
+ * Promise-like catch method to handle rejected state of the promise
220
+ *
221
+ * @param onrejected
222
+ * @returns
223
+ */
224
+ catch(onrejected) {
225
+ return this.then(void 0, onrejected);
226
+ }
227
+ /**
228
+ * Promise-like finally method to handle cleanup after promise is settled
229
+ *
230
+ * @param onfinally
231
+ * @returns
232
+ */
233
+ finally(onfinally) {
234
+ return this.then(onfinally, onfinally);
235
+ }
236
+ };
237
+
238
+ //#endregion
239
+ //#region src/Resource.ts
240
+ /**
241
+ * Resource class to handle API resource transformation and response building
242
+ */
243
+ var Resource = class {
244
+ body = { data: {} };
245
+ resource;
246
+ called = {};
247
+ constructor(rsc, res) {
248
+ this.res = res;
249
+ this.resource = rsc;
250
+ /**
251
+ * Copy properties from rsc to this instance for easy
252
+ * access, but only if data is not an array
253
+ */
254
+ if (!Array.isArray(this.resource.data ?? this.resource)) {
255
+ for (const key of Object.keys(this.resource.data ?? this.resource)) if (!(key in this)) Object.defineProperty(this, key, {
256
+ enumerable: true,
257
+ configurable: true,
258
+ get: () => {
259
+ return this.resource.data?.[key] ?? this.resource[key];
260
+ },
261
+ set: (value) => {
262
+ if (this.resource.data && this.resource.data[key]) this.resource.data[key] = value;
263
+ else this.resource[key] = value;
264
+ }
265
+ });
266
+ }
267
+ }
268
+ /**
269
+ * Create a ResourceCollection from an array of resource data or a Collectible instance
270
+ *
271
+ * @param data
272
+ * @returns
273
+ */
274
+ static collection(data) {
275
+ return new ResourceCollection(data).setCollects(this);
276
+ }
277
+ /**
278
+ * Get the original resource data
279
+ */
280
+ data() {
281
+ return this.toArray();
282
+ }
283
+ /**
284
+ * Convert resource to JSON response format
285
+ *
286
+ * @returns
287
+ */
288
+ json() {
289
+ if (!this.called.json) {
290
+ this.called.json = true;
291
+ const resource = this.data();
292
+ let data = Array.isArray(resource) ? [...resource] : { ...resource };
293
+ if (typeof data.data !== "undefined") data = data.data;
294
+ this.body = { data };
295
+ }
296
+ return this;
297
+ }
298
+ /**
299
+ * Flatten resource to array format (for collections) or return original data for single resources
300
+ *
301
+ * @returns
302
+ */
303
+ toArray() {
304
+ this.called.toArray = true;
305
+ this.json();
306
+ let data = Array.isArray(this.resource) ? [...this.resource] : { ...this.resource };
307
+ if (!Array.isArray(data) && typeof data.data !== "undefined") data = data.data;
308
+ return data;
309
+ }
310
+ /**
311
+ * Add additional properties to the response body
312
+ *
313
+ * @param extra Additional properties to merge into the response body
314
+ * @returns
315
+ */
316
+ additional(extra) {
317
+ this.called.additional = true;
318
+ this.json();
319
+ if (extra.data) this.body.data = Array.isArray(this.body.data) ? [...this.body.data, ...extra.data] : {
320
+ ...this.body.data,
321
+ ...extra.data
322
+ };
323
+ this.body = {
324
+ ...this.body,
325
+ ...extra
326
+ };
327
+ return this;
328
+ }
329
+ response(res) {
330
+ this.called.toResponse = true;
331
+ return new ServerResponse(res ?? this.res, this.body);
332
+ }
333
+ /**
334
+ * Promise-like then method to allow chaining with async/await or .then() syntax
335
+ *
336
+ * @param onfulfilled Callback to handle the fulfilled state of the promise, receiving the response body
337
+ * @param onrejected Callback to handle the rejected state of the promise, receiving the error reason
338
+ * @returns A promise that resolves to the result of the onfulfilled or onrejected callback
339
+ */
340
+ then(onfulfilled, onrejected) {
341
+ this.called.then = true;
342
+ this.json();
343
+ const resolved = Promise.resolve(this.body).then(onfulfilled, onrejected);
344
+ if (this.res) this.res.send(this.body);
345
+ return resolved;
346
+ }
347
+ /**
348
+ * Promise-like catch method to handle rejected state of the promise
349
+ *
350
+ * @param onrejected
351
+ * @returns
352
+ */
353
+ catch(onrejected) {
354
+ return this.then(void 0, onrejected);
355
+ }
356
+ /**
357
+ * Promise-like finally method to handle cleanup after promise is settled
358
+ *
359
+ * @param onfinally
360
+ * @returns
361
+ */
362
+ finally(onfinally) {
363
+ return this.then(onfinally, onfinally);
364
+ }
365
+ };
366
+
367
+ //#endregion
368
+ export { ApiResource, Resource };
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "resora",
3
+ "version": "0.1.0",
4
+ "description": "A structured API response layer for Node.js and TypeScript with automatic JSON responses, collection support, and pagination handling.",
5
+ "keywords": [
6
+ "api",
7
+ "resource",
8
+ "transformer",
9
+ "laravel",
10
+ "typescript",
11
+ "express",
12
+ "h3",
13
+ "pagination",
14
+ "json",
15
+ "response"
16
+ ],
17
+ "homepage": "https://github.com/toneflix/resora",
18
+ "bugs": {
19
+ "url": "https://github.com/toneflix/resora/issues"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/toneflix/resora.git"
24
+ },
25
+ "license": "MIT",
26
+ "author": "3m1n1nce <3m1n1nce@toneflix.net>",
27
+ "type": "module",
28
+ "main": "./dist/index.cjs",
29
+ "module": "./dist/index.mjs",
30
+ "types": "./dist/index.d.cts",
31
+ "exports": {
32
+ ".": {
33
+ "import": "./dist/index.mjs",
34
+ "require": "./dist/index.cjs"
35
+ },
36
+ "./package.json": "./package.json"
37
+ },
38
+ "files": [
39
+ "dist",
40
+ "README.md",
41
+ "LICENSE"
42
+ ],
43
+ "devDependencies": {
44
+ "@eslint/js": "^10.0.1",
45
+ "@eslint/markdown": "^7.5.1",
46
+ "@types/express": "^4.17.21",
47
+ "@types/node": "^20.10.6",
48
+ "@types/supertest": "^6.0.3",
49
+ "@vitest/coverage-v8": "4.0.18",
50
+ "barrelize": "^1.7.3",
51
+ "eslint": "^10.0.0",
52
+ "express": "^5.1.0",
53
+ "h3": "2.0.1-rc.14",
54
+ "supertest": "^7.1.1",
55
+ "tsdown": "^0.20.3",
56
+ "tsx": "^4.21.0",
57
+ "typescript": "^5.3.3",
58
+ "typescript-eslint": "^8.56.0",
59
+ "vite-tsconfig-paths": "^6.1.1",
60
+ "vitepress": "2.0.0-alpha.16",
61
+ "vitest": "^4.0.18",
62
+ "vue": "^3.5.28"
63
+ },
64
+ "engines": {
65
+ "node": ">=20.0.0"
66
+ },
67
+ "scripts": {
68
+ "lint": "eslint",
69
+ "test": "pnpm vitest",
70
+ "test:coverage": "pnpm vitest --coverage --watch=false",
71
+ "build": "pnpm tsdown",
72
+ "barrel": "barrelize",
73
+ "docs:dev": "vitepress dev docs",
74
+ "docs:build": "vitepress build docs",
75
+ "docs:preview": "vitepress preview docs"
76
+ }
77
+ }