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/README.md ADDED
@@ -0,0 +1,176 @@
1
+ # Resora
2
+
3
+ Resora is a structured API response layer for Node.js and TypeScript backends.
4
+
5
+ It provides a clean, explicit way to transform data into consistent JSON responses and automatically send them to the client. Resora supports single resources, collections, and pagination metadata while remaining framework-agnostic and strongly typed.
6
+
7
+ Resora is designed for teams that care about long-term maintainability, predictable API contracts, and clean separation of concerns.
8
+
9
+ ---
10
+
11
+ ## What Problem Does Resora Solve?
12
+
13
+ In most Node.js backends:
14
+
15
+ - Controllers shape JSON directly
16
+ - Response formats drift over time
17
+ - Pagination logic is duplicated
18
+ - Metadata handling is inconsistent
19
+
20
+ Resora introduces a dedicated **response transformation layer** that removes these concerns from controllers and centralizes response structure in one place.
21
+
22
+ ---
23
+
24
+ ## Core Capabilities
25
+
26
+ - Explicit data-to-response transformation
27
+ - Automatic JSON response dispatch
28
+ - First-class collection support
29
+ - Built-in pagination metadata handling
30
+ - Predictable and consistent response contracts
31
+ - Strong TypeScript typing
32
+ - Transport-layer friendly (Express, H3, and others)
33
+
34
+ ---
35
+
36
+ ## Basic Example
37
+
38
+ ### Single Resource
39
+
40
+ ```ts
41
+ import { Resource } from 'resora';
42
+
43
+ class UserResource extends Resource {
44
+ data() {
45
+ return this.toArray();
46
+ }
47
+ }
48
+ ```
49
+
50
+ ```ts
51
+ return new UserResource(user).additional({
52
+ status: 'success',
53
+ message: 'User retrieved',
54
+ });
55
+ ```
56
+
57
+ Response:
58
+
59
+ ```json
60
+ {
61
+ "data": {
62
+ "id": 1,
63
+ "name": "John"
64
+ },
65
+ "status": "success",
66
+ "message": "User retrieved"
67
+ }
68
+ ```
69
+
70
+ ---
71
+
72
+ ### Collection with Pagination
73
+
74
+ ```ts
75
+ import { ResourceCollection } from 'resora';
76
+
77
+ class UserCollection<R extends User[]> extends ResourceCollection<R> {
78
+ collects = UserResource;
79
+
80
+ data() {
81
+ return this.toArray();
82
+ }
83
+ }
84
+ ```
85
+
86
+ ```ts
87
+ return new UserCollection({
88
+ data: users,
89
+ pagination: {
90
+ from: 1,
91
+ to: 10,
92
+ perPage: 10,
93
+ total: 100,
94
+ },
95
+ }).additional({
96
+ status: 'success',
97
+ message: 'Users retrieved',
98
+ });
99
+ ```
100
+
101
+ Response:
102
+
103
+ ```json
104
+ {
105
+ "data": [...],
106
+ "meta": {
107
+ "pagination": {
108
+ "from": 1,
109
+ "to": 10,
110
+ "perPage": 10,
111
+ "total": 100
112
+ }
113
+ },
114
+ "status": "success",
115
+ "message": "Users retrieved"
116
+ }
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Architectural Positioning
122
+
123
+ Resora sits **between your application logic and the HTTP layer**.
124
+
125
+ - Controllers handle request flow
126
+ - Services handle business logic
127
+ - Resora handles response structure
128
+
129
+ This separation ensures:
130
+
131
+ - Stable API contracts
132
+ - Minimal controller logic
133
+ - Clear ownership of response shape
134
+
135
+ ---
136
+
137
+ ## Design Principles
138
+
139
+ - Explicit over implicit behavior
140
+ - Separation of concerns
141
+ - Minimal abstraction cost
142
+ - Strong typing as a first-class feature
143
+ - Framework independence
144
+
145
+ ---
146
+
147
+ ## Framework Compatibility
148
+
149
+ Resora is not tied to a specific HTTP framework.
150
+
151
+ It works with:
152
+
153
+ - Express
154
+ - H3
155
+ - Any application or framework that supports Connect-style middleware
156
+
157
+ Adapters can be added without changing application logic.
158
+
159
+ ---
160
+
161
+ ## When to Use Resora
162
+
163
+ Resora is a good fit if you:
164
+
165
+ - Build APIs with long-term maintenance in mind
166
+ - Care about response consistency across teams
167
+ - Want pagination and metadata handled once
168
+ - Prefer explicit structure over ad-hoc JSON responses
169
+
170
+ It is intentionally not opinionated about routing, validation, or persistence.
171
+
172
+ ---
173
+
174
+ ## License
175
+
176
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,371 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
+
3
+ //#region src/ApiResource.ts
4
+ /**
5
+ * ApiResource function to return the Resource instance
6
+ *
7
+ * @param instance Resource instance
8
+ * @returns Resource instance
9
+ */
10
+ function ApiResource(instance) {
11
+ return instance;
12
+ }
13
+
14
+ //#endregion
15
+ //#region src/ServerResponse.ts
16
+ var ServerResponse = class {
17
+ _status = 200;
18
+ headers = {};
19
+ constructor(response, body) {
20
+ this.response = response;
21
+ this.body = body;
22
+ }
23
+ /**
24
+ * Set the HTTP status code for the response
25
+ *
26
+ * @param status
27
+ * @returns The current ServerResponse instance
28
+ */
29
+ setStatusCode(status) {
30
+ this._status = status;
31
+ if ("status" in this.response && typeof this.response.status === "function") this.response.status(status);
32
+ else if ("status" in this.response) this.response.status = status;
33
+ return this;
34
+ }
35
+ /**
36
+ * Get the current HTTP status code for the response
37
+ *
38
+ * @returns
39
+ */
40
+ status() {
41
+ return this._status;
42
+ }
43
+ /**
44
+ * Get the current HTTP status text for the response
45
+ *
46
+ * @returns
47
+ */
48
+ statusText() {
49
+ if ("statusMessage" in this.response) return this.response.statusMessage;
50
+ else if ("statusText" in this.response) return this.response.statusText;
51
+ }
52
+ /**
53
+ * Set a cookie in the response header
54
+ *
55
+ * @param name The name of the cookie
56
+ * @param value The value of the cookie
57
+ * @param options Optional cookie attributes (e.g., path, domain, maxAge)
58
+ * @returns The current ServerResponse instance
59
+ */
60
+ setCookie(name, value, options) {
61
+ this.#addHeader("Set-Cookie", `${name}=${value}; ${Object.entries(options || {}).map(([key, val]) => `${key}=${val}`).join("; ")}`);
62
+ return this;
63
+ }
64
+ /**
65
+ * Convert the resource to a JSON response body
66
+ *
67
+ * @param headers Optional headers to add to the response
68
+ * @returns The current ServerResponse instance
69
+ */
70
+ setHeaders(headers) {
71
+ for (const [key, value] of Object.entries(headers)) this.#addHeader(key, value);
72
+ return this;
73
+ }
74
+ /**
75
+ * Add a single header to the response
76
+ *
77
+ * @param key The name of the header
78
+ * @param value The value of the header
79
+ * @returns The current ServerResponse instance
80
+ */
81
+ header(key, value) {
82
+ this.#addHeader(key, value);
83
+ return this;
84
+ }
85
+ /**
86
+ * Add a single header to the response
87
+ *
88
+ * @param key The name of the header
89
+ * @param value The value of the header
90
+ */
91
+ #addHeader(key, value) {
92
+ this.headers[key] = value;
93
+ if ("headers" in this.response) this.response.headers.set(key, value);
94
+ else if ("setHeader" in this.response) this.response.setHeader(key, value);
95
+ }
96
+ /**
97
+ * Promise-like then method to allow chaining with async/await or .then() syntax
98
+ *
99
+ * @param onfulfilled Callback to handle the fulfilled state of the promise, receiving the response body
100
+ * @param onrejected Callback to handle the rejected state of the promise, receiving the error reason
101
+ * @returns A promise that resolves to the result of the onfulfilled or onrejected callback
102
+ */
103
+ then(onfulfilled, onrejected) {
104
+ const resolved = Promise.resolve(this.body).then(onfulfilled, onrejected);
105
+ if ("send" in this.response) this.response.send(this.body);
106
+ return resolved;
107
+ }
108
+ /**
109
+ * Promise-like catch method to handle rejected state of the promise
110
+ *
111
+ * @param onrejected
112
+ * @returns
113
+ */
114
+ catch(onrejected) {
115
+ return this.then(void 0, onrejected);
116
+ }
117
+ /**
118
+ * Promise-like finally method to handle cleanup after promise is settled
119
+ *
120
+ * @param onfinally
121
+ * @returns
122
+ */
123
+ finally(onfinally) {
124
+ return this.then(onfinally, onfinally);
125
+ }
126
+ };
127
+
128
+ //#endregion
129
+ //#region src/ResourceCollection.ts
130
+ /**
131
+ * ResourceCollection class to handle API resource transformation and response building for collections
132
+ */
133
+ var ResourceCollection = class {
134
+ body = { data: [] };
135
+ resource;
136
+ collects;
137
+ called = {};
138
+ constructor(rsc, res) {
139
+ this.res = res;
140
+ this.resource = rsc;
141
+ }
142
+ /**
143
+ * Get the original resource data
144
+ */
145
+ data() {
146
+ return this.toArray();
147
+ }
148
+ /**
149
+ * Convert resource to JSON response format
150
+ *
151
+ * @returns
152
+ */
153
+ json() {
154
+ if (!this.called.json) {
155
+ this.called.json = true;
156
+ let data = this.data();
157
+ if (this.collects) data = data.map((item) => new this.collects(item).data());
158
+ this.body = { data };
159
+ if (!Array.isArray(this.resource)) {
160
+ if (this.resource.pagination && this.resource.cursor) this.body.meta = {
161
+ pagination: this.resource.pagination,
162
+ cursor: this.resource.cursor
163
+ };
164
+ else if (this.resource.pagination) this.body.meta = { pagination: this.resource.pagination };
165
+ else if (this.resource.cursor) this.body.meta = { cursor: this.resource.cursor };
166
+ }
167
+ }
168
+ return this;
169
+ }
170
+ /**
171
+ * Flatten resource to return original data
172
+ *
173
+ * @returns
174
+ */
175
+ toArray() {
176
+ this.called.toArray = true;
177
+ this.json();
178
+ return Array.isArray(this.resource) ? [...this.resource] : [...this.resource.data];
179
+ }
180
+ /**
181
+ * Add additional properties to the response body
182
+ *
183
+ * @param extra Additional properties to merge into the response body
184
+ * @returns
185
+ */
186
+ additional(extra) {
187
+ this.called.additional = true;
188
+ this.json();
189
+ delete extra.cursor;
190
+ delete extra.pagination;
191
+ if (extra.data && Array.isArray(this.body.data)) this.body.data = [...this.body.data, ...extra.data];
192
+ this.body = {
193
+ ...this.body,
194
+ ...extra
195
+ };
196
+ return this;
197
+ }
198
+ response(res) {
199
+ this.called.toResponse = true;
200
+ return new ServerResponse(res ?? this.res, this.body);
201
+ }
202
+ setCollects(collects) {
203
+ this.collects = collects;
204
+ return this;
205
+ }
206
+ /**
207
+ * Promise-like then method to allow chaining with async/await or .then() syntax
208
+ *
209
+ * @param onfulfilled Callback to handle the fulfilled state of the promise, receiving the response body
210
+ * @param onrejected Callback to handle the rejected state of the promise, receiving the error reason
211
+ * @returns A promise that resolves to the result of the onfulfilled or onrejected callback
212
+ */
213
+ then(onfulfilled, onrejected) {
214
+ this.called.then = true;
215
+ this.json();
216
+ const resolved = Promise.resolve(this.body).then(onfulfilled, onrejected);
217
+ if (this.res) this.res.send(this.body);
218
+ return resolved;
219
+ }
220
+ /**
221
+ * Promise-like catch method to handle rejected state of the promise
222
+ *
223
+ * @param onrejected
224
+ * @returns
225
+ */
226
+ catch(onrejected) {
227
+ return this.then(void 0, onrejected);
228
+ }
229
+ /**
230
+ * Promise-like finally method to handle cleanup after promise is settled
231
+ *
232
+ * @param onfinally
233
+ * @returns
234
+ */
235
+ finally(onfinally) {
236
+ return this.then(onfinally, onfinally);
237
+ }
238
+ };
239
+
240
+ //#endregion
241
+ //#region src/Resource.ts
242
+ /**
243
+ * Resource class to handle API resource transformation and response building
244
+ */
245
+ var Resource = class {
246
+ body = { data: {} };
247
+ resource;
248
+ called = {};
249
+ constructor(rsc, res) {
250
+ this.res = res;
251
+ this.resource = rsc;
252
+ /**
253
+ * Copy properties from rsc to this instance for easy
254
+ * access, but only if data is not an array
255
+ */
256
+ if (!Array.isArray(this.resource.data ?? this.resource)) {
257
+ for (const key of Object.keys(this.resource.data ?? this.resource)) if (!(key in this)) Object.defineProperty(this, key, {
258
+ enumerable: true,
259
+ configurable: true,
260
+ get: () => {
261
+ return this.resource.data?.[key] ?? this.resource[key];
262
+ },
263
+ set: (value) => {
264
+ if (this.resource.data && this.resource.data[key]) this.resource.data[key] = value;
265
+ else this.resource[key] = value;
266
+ }
267
+ });
268
+ }
269
+ }
270
+ /**
271
+ * Create a ResourceCollection from an array of resource data or a Collectible instance
272
+ *
273
+ * @param data
274
+ * @returns
275
+ */
276
+ static collection(data) {
277
+ return new ResourceCollection(data).setCollects(this);
278
+ }
279
+ /**
280
+ * Get the original resource data
281
+ */
282
+ data() {
283
+ return this.toArray();
284
+ }
285
+ /**
286
+ * Convert resource to JSON response format
287
+ *
288
+ * @returns
289
+ */
290
+ json() {
291
+ if (!this.called.json) {
292
+ this.called.json = true;
293
+ const resource = this.data();
294
+ let data = Array.isArray(resource) ? [...resource] : { ...resource };
295
+ if (typeof data.data !== "undefined") data = data.data;
296
+ this.body = { data };
297
+ }
298
+ return this;
299
+ }
300
+ /**
301
+ * Flatten resource to array format (for collections) or return original data for single resources
302
+ *
303
+ * @returns
304
+ */
305
+ toArray() {
306
+ this.called.toArray = true;
307
+ this.json();
308
+ let data = Array.isArray(this.resource) ? [...this.resource] : { ...this.resource };
309
+ if (!Array.isArray(data) && typeof data.data !== "undefined") data = data.data;
310
+ return data;
311
+ }
312
+ /**
313
+ * Add additional properties to the response body
314
+ *
315
+ * @param extra Additional properties to merge into the response body
316
+ * @returns
317
+ */
318
+ additional(extra) {
319
+ this.called.additional = true;
320
+ this.json();
321
+ if (extra.data) this.body.data = Array.isArray(this.body.data) ? [...this.body.data, ...extra.data] : {
322
+ ...this.body.data,
323
+ ...extra.data
324
+ };
325
+ this.body = {
326
+ ...this.body,
327
+ ...extra
328
+ };
329
+ return this;
330
+ }
331
+ response(res) {
332
+ this.called.toResponse = true;
333
+ return new ServerResponse(res ?? this.res, this.body);
334
+ }
335
+ /**
336
+ * Promise-like then method to allow chaining with async/await or .then() syntax
337
+ *
338
+ * @param onfulfilled Callback to handle the fulfilled state of the promise, receiving the response body
339
+ * @param onrejected Callback to handle the rejected state of the promise, receiving the error reason
340
+ * @returns A promise that resolves to the result of the onfulfilled or onrejected callback
341
+ */
342
+ then(onfulfilled, onrejected) {
343
+ this.called.then = true;
344
+ this.json();
345
+ const resolved = Promise.resolve(this.body).then(onfulfilled, onrejected);
346
+ if (this.res) this.res.send(this.body);
347
+ return resolved;
348
+ }
349
+ /**
350
+ * Promise-like catch method to handle rejected state of the promise
351
+ *
352
+ * @param onrejected
353
+ * @returns
354
+ */
355
+ catch(onrejected) {
356
+ return this.then(void 0, onrejected);
357
+ }
358
+ /**
359
+ * Promise-like finally method to handle cleanup after promise is settled
360
+ *
361
+ * @param onfinally
362
+ * @returns
363
+ */
364
+ finally(onfinally) {
365
+ return this.then(onfinally, onfinally);
366
+ }
367
+ };
368
+
369
+ //#endregion
370
+ exports.ApiResource = ApiResource;
371
+ exports.Resource = Resource;