resora 1.1.0 → 1.2.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,7 @@ Resora introduces a dedicated **response transformation layer** that removes the
33
33
  - Explicit data-to-response transformation
34
34
  - Automatic JSON response dispatch
35
35
  - First-class collection support
36
+ - Nested resource and collection composition
36
37
  - Built-in pagination metadata handling
37
38
  - Built-in cursor metadata handling
38
39
  - Conditional attribute helpers (`when`, `whenNotNull`, `mergeWhen`)
@@ -134,7 +135,57 @@ Response:
134
135
  }
135
136
  ```
136
137
 
137
- ---
138
+ ### Nested Resources and Collections
139
+
140
+ ```ts
141
+ import { Resource, ResourceCollection } from 'resora';
142
+
143
+ class FamilyMemberResource extends Resource {
144
+ data() {
145
+ return {
146
+ id: this.id,
147
+ fullName: `${this.firstName} ${this.lastName}`,
148
+ };
149
+ }
150
+ }
151
+
152
+ class FamilyMemberCollection extends ResourceCollection {
153
+ collects = FamilyMemberResource;
154
+
155
+ data() {
156
+ return this.toObject();
157
+ }
158
+ }
159
+
160
+ class FamilyOverviewResource extends Resource {
161
+ data() {
162
+ return {
163
+ id: this.id,
164
+ familyName: this.familyName,
165
+ members: new FamilyMemberCollection(this.members ?? []),
166
+ };
167
+ }
168
+ }
169
+ ```
170
+
171
+ Response:
172
+
173
+ ```json
174
+ {
175
+ "data": {
176
+ "id": 1,
177
+ "familyName": "Smiths",
178
+ "members": [
179
+ {
180
+ "id": 1,
181
+ "fullName": "Jane Doe"
182
+ }
183
+ ]
184
+ }
185
+ }
186
+ ```
187
+
188
+ You can also call `.toObject()` explicitly when you want the transformed collection array immediately inside a parent resource.
138
189
 
139
190
  ## Architectural Positioning
140
191
 
@@ -160,8 +211,6 @@ This separation ensures:
160
211
  - Strong typing as a first-class feature
161
212
  - Framework independence
162
213
 
163
- ---
164
-
165
214
  ## Framework Compatibility
166
215
 
167
216
  Resora is not tied to a specific HTTP framework.
@@ -191,6 +240,10 @@ Plugins can:
191
240
  - inject framework integrations without core changes
192
241
  - register reusable transformation utilities
193
242
 
243
+ Available plugins:
244
+
245
+ - [`@resora/plugin-clear-router`](https://arkstack-hq.github.io/resora/plugins/clear-router.md)
246
+
194
247
  ## Conditional Rendering Example
195
248
 
196
249
  ```ts
@@ -208,8 +261,6 @@ class UserResource extends Resource {
208
261
 
209
262
  Falsy/null attributes are omitted from the final serialized payload.
210
263
 
211
- ---
212
-
213
264
  ## When to Use Resora
214
265
 
215
266
  Resora is a good fit if you:
@@ -221,8 +272,6 @@ Resora is a good fit if you:
221
272
 
222
273
  It is intentionally not opinionated about routing, validation, or persistence.
223
274
 
224
- ---
225
-
226
275
  ## Documentation
227
276
 
228
277
  - Getting Started: https://arkstack-hq.github.io/resora/guide/getting-started
@@ -230,8 +279,6 @@ It is intentionally not opinionated about routing, validation, or persistence.
230
279
  - Conditional Rendering: https://arkstack-hq.github.io/resora/guide/conditional-attributes
231
280
  - Pagination & Cursor Recipes: https://arkstack-hq.github.io/resora/guide/pagination-cursor-recipes
232
281
 
233
- ---
234
-
235
282
  ## License
236
283
 
237
284
  MIT
package/dist/index.cjs CHANGED
@@ -358,6 +358,8 @@ const extractUrlFromRequest = (req) => {
358
358
  const extractResponseFromCtx = (ctx) => {
359
359
  if (!ctx || typeof ctx !== "object") return;
360
360
  const obj = ctx;
361
+ if (typeof obj.header === "function" && typeof obj.status === "function") return ctx;
362
+ if (typeof obj.set === "function" && "status" in obj && "body" in obj) return ctx;
361
363
  if (obj.res && typeof obj.res === "object") return obj.res;
362
364
  return ctx;
363
365
  };
@@ -1352,8 +1354,10 @@ var ServerResponse = class {
1352
1354
  */
1353
1355
  #addHeader(key, value) {
1354
1356
  this.headers[key] = value;
1355
- if ("headers" in this.response) this.response.headers.set(key, value);
1357
+ if ("headers" in this.response && this.response.headers && typeof this.response.headers.set === "function") this.response.headers.set(key, value);
1356
1358
  else if ("setHeader" in this.response) this.response.setHeader(key, value);
1359
+ else if ("set" in this.response && typeof this.response.set === "function") this.response.set(key, value);
1360
+ else if ("header" in this.response && typeof this.response.header === "function") this.response.header(key, value);
1357
1361
  }
1358
1362
  /**
1359
1363
  * Dispatch the current body and apply any deferred transport state.
@@ -1375,9 +1379,10 @@ var ServerResponse = class {
1375
1379
  this.headers = { ...beforeSend.headers };
1376
1380
  if ("send" in this.response && typeof this.response.send === "function") {
1377
1381
  if ("statusCode" in this.response) this.response.statusCode = this._status;
1378
- const sentResponse = this.response.send(this.body);
1379
- if (sentResponse && "status" in sentResponse && typeof sentResponse.status === "function") sentResponse.status(this._status);
1380
- else if ("status" in this.response && typeof this.response.status === "function") this.response.status(this._status);
1382
+ if ("status" in this.response && typeof this.response.status === "function") this.response.status(this._status);
1383
+ else if ("code" in this.response && typeof this.response.code === "function") this.response.code(this._status);
1384
+ this.response.__resoraStatus = this._status;
1385
+ this.response.send(this.body);
1381
1386
  runPluginHook("afterSend", {
1382
1387
  response: this,
1383
1388
  rawResponse: this.response,
@@ -1387,7 +1392,14 @@ var ServerResponse = class {
1387
1392
  });
1388
1393
  return this.body;
1389
1394
  }
1390
- if ("status" in this.response && typeof this.response.status !== "function") this.response.status = this._status;
1395
+ if ("status" in this.response && typeof this.response.status === "function") {
1396
+ this.response.status(this._status);
1397
+ this.response.__resoraStatus = this._status;
1398
+ } else if ("status" in this.response && typeof this.response.status !== "function") this.response.status = this._status;
1399
+ if ("body" in this.response && typeof this.response.body !== "function") {
1400
+ this.response.body = this.body;
1401
+ this.response.__resoraStatus = this._status;
1402
+ }
1391
1403
  runPluginHook("afterSend", {
1392
1404
  response: this,
1393
1405
  rawResponse: this.response,
package/dist/index.mjs CHANGED
@@ -329,6 +329,8 @@ const extractUrlFromRequest = (req) => {
329
329
  const extractResponseFromCtx = (ctx) => {
330
330
  if (!ctx || typeof ctx !== "object") return;
331
331
  const obj = ctx;
332
+ if (typeof obj.header === "function" && typeof obj.status === "function") return ctx;
333
+ if (typeof obj.set === "function" && "status" in obj && "body" in obj) return ctx;
332
334
  if (obj.res && typeof obj.res === "object") return obj.res;
333
335
  return ctx;
334
336
  };
@@ -1323,8 +1325,10 @@ var ServerResponse = class {
1323
1325
  */
1324
1326
  #addHeader(key, value) {
1325
1327
  this.headers[key] = value;
1326
- if ("headers" in this.response) this.response.headers.set(key, value);
1328
+ if ("headers" in this.response && this.response.headers && typeof this.response.headers.set === "function") this.response.headers.set(key, value);
1327
1329
  else if ("setHeader" in this.response) this.response.setHeader(key, value);
1330
+ else if ("set" in this.response && typeof this.response.set === "function") this.response.set(key, value);
1331
+ else if ("header" in this.response && typeof this.response.header === "function") this.response.header(key, value);
1328
1332
  }
1329
1333
  /**
1330
1334
  * Dispatch the current body and apply any deferred transport state.
@@ -1346,9 +1350,10 @@ var ServerResponse = class {
1346
1350
  this.headers = { ...beforeSend.headers };
1347
1351
  if ("send" in this.response && typeof this.response.send === "function") {
1348
1352
  if ("statusCode" in this.response) this.response.statusCode = this._status;
1349
- const sentResponse = this.response.send(this.body);
1350
- if (sentResponse && "status" in sentResponse && typeof sentResponse.status === "function") sentResponse.status(this._status);
1351
- else if ("status" in this.response && typeof this.response.status === "function") this.response.status(this._status);
1353
+ if ("status" in this.response && typeof this.response.status === "function") this.response.status(this._status);
1354
+ else if ("code" in this.response && typeof this.response.code === "function") this.response.code(this._status);
1355
+ this.response.__resoraStatus = this._status;
1356
+ this.response.send(this.body);
1352
1357
  runPluginHook("afterSend", {
1353
1358
  response: this,
1354
1359
  rawResponse: this.response,
@@ -1358,7 +1363,14 @@ var ServerResponse = class {
1358
1363
  });
1359
1364
  return this.body;
1360
1365
  }
1361
- if ("status" in this.response && typeof this.response.status !== "function") this.response.status = this._status;
1366
+ if ("status" in this.response && typeof this.response.status === "function") {
1367
+ this.response.status(this._status);
1368
+ this.response.__resoraStatus = this._status;
1369
+ } else if ("status" in this.response && typeof this.response.status !== "function") this.response.status = this._status;
1370
+ if ("body" in this.response && typeof this.response.body !== "function") {
1371
+ this.response.body = this.body;
1372
+ this.response.__resoraStatus = this._status;
1373
+ }
1362
1374
  runPluginHook("afterSend", {
1363
1375
  response: this,
1364
1376
  rawResponse: this.response,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "resora",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "description": "A structured API response layer for Node.js and TypeScript with automatic JSON responses, collection support, and pagination handling.",
5
5
  "keywords": [
6
6
  "api",
@@ -50,20 +50,20 @@
50
50
  "@eslint/markdown": "^7.5.1",
51
51
  "@types/express": "^4.17.21",
52
52
  "@types/node": "^20.10.6",
53
- "@types/supertest": "^6.0.3",
54
53
  "@vitest/coverage-v8": "4.0.18",
55
- "arkormx": "^0.2.6",
54
+ "arkormx": "^2.0.8",
56
55
  "barrelize": "^1.7.3",
57
56
  "eslint": "^10.0.0",
58
57
  "express": "^5.1.0",
59
58
  "h3": "2.0.1-rc.14",
60
- "supertest": "^7.1.1",
59
+ "parasito": "^0.1.6",
61
60
  "tsdown": "^0.20.3",
62
61
  "tsx": "^4.21.0",
63
62
  "typescript": "^5.3.3",
64
63
  "typescript-eslint": "^8.56.0",
64
+ "unrun": "^0.3.0",
65
65
  "vite-tsconfig-paths": "^6.1.1",
66
- "vitepress": "2.0.0-alpha.16",
66
+ "vitepress": "^2.0.0-alpha.17",
67
67
  "vitest": "^4.0.18",
68
68
  "vue": "^3.5.28"
69
69
  },
@@ -85,6 +85,6 @@
85
85
  "docs:build": "vitepress build docs",
86
86
  "docs:preview": "vitepress preview docs",
87
87
  "version:packages": "pnpm -r --filter \"@resora/*\" version:patch && git add . && git commit -m \"chore: bump package versions\"",
88
- "publish:packages": "pnpm version:packages && pnpm -r --filter \"@arkstack/*\" publish --access public"
88
+ "publish:packages": "pnpm version:packages && pnpm -r --filter \"@resora/*\" publish --access public"
89
89
  }
90
90
  }