resora 1.0.1 → 1.2.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 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
  };
@@ -457,6 +459,17 @@ const isArkormLikeCollection = (value) => {
457
459
  return typeof value.all === "function";
458
460
  };
459
461
  /**
462
+ * Type guard to check if a value is a Resora collection-like serializer.
463
+ *
464
+ * @param value
465
+ * @returns
466
+ */
467
+ const isResoraCollectionLike = (value) => {
468
+ if (!value || typeof value !== "object") return false;
469
+ const candidate = value;
470
+ return typeof candidate.toObject === "function" && typeof candidate.getBody === "function" && typeof candidate.json === "function" && typeof candidate.setCollects === "function";
471
+ };
472
+ /**
460
473
  * Normalize a value for serialization by recursively converting Arkorm-like models and
461
474
  * collections to plain objects, while preserving the structure of arrays and plain objects.
462
475
  *
@@ -465,6 +478,7 @@ const isArkormLikeCollection = (value) => {
465
478
  */
466
479
  const normalizeSerializableData = (value) => {
467
480
  if (Array.isArray(value)) return value.map((item) => normalizeSerializableData(item));
481
+ if (isResoraCollectionLike(value)) return normalizeSerializableData(value.toObject());
468
482
  if (isArkormLikeModel(value)) return normalizeSerializableData(value.toObject());
469
483
  if (isArkormLikeCollection(value)) {
470
484
  const collectionData = value.all();
@@ -1340,8 +1354,10 @@ var ServerResponse = class {
1340
1354
  */
1341
1355
  #addHeader(key, value) {
1342
1356
  this.headers[key] = value;
1343
- 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);
1344
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);
1345
1361
  }
1346
1362
  /**
1347
1363
  * Dispatch the current body and apply any deferred transport state.
@@ -1363,9 +1379,10 @@ var ServerResponse = class {
1363
1379
  this.headers = { ...beforeSend.headers };
1364
1380
  if ("send" in this.response && typeof this.response.send === "function") {
1365
1381
  if ("statusCode" in this.response) this.response.statusCode = this._status;
1366
- const sentResponse = this.response.send(this.body);
1367
- if (sentResponse && "status" in sentResponse && typeof sentResponse.status === "function") sentResponse.status(this._status);
1368
- 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);
1369
1386
  runPluginHook("afterSend", {
1370
1387
  response: this,
1371
1388
  rawResponse: this.response,
@@ -1375,7 +1392,14 @@ var ServerResponse = class {
1375
1392
  });
1376
1393
  return this.body;
1377
1394
  }
1378
- 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) {
1400
+ this.response.body = this.body;
1401
+ this.response.__resoraStatus = this._status;
1402
+ }
1379
1403
  runPluginHook("afterSend", {
1380
1404
  response: this,
1381
1405
  rawResponse: this.response,
@@ -2062,11 +2086,19 @@ var ResourceCollection = class ResourceCollection extends BaseSerializer {
2062
2086
  this.res = extractResponseFromCtx(ctx);
2063
2087
  }
2064
2088
  }
2089
+ getSourceData() {
2090
+ return Array.isArray(this.resource) ? this.resource : isArkormLikeCollection(this.resource) ? this.resource.all() : this.resource.data;
2091
+ }
2092
+ resolveObjectData() {
2093
+ let data = this.getSourceData();
2094
+ if (this.collects) data = data.map((item) => new this.collects(item).data());
2095
+ return normalizeSerializableData(data);
2096
+ }
2065
2097
  /**
2066
2098
  * Get the original resource data
2067
2099
  */
2068
2100
  data() {
2069
- return this.toObject();
2101
+ return this.getSourceData();
2070
2102
  }
2071
2103
  /**
2072
2104
  * Get the current serialized output body.
@@ -2146,7 +2178,7 @@ var ResourceCollection = class ResourceCollection extends BaseSerializer {
2146
2178
  if (!this.called.json) {
2147
2179
  this.called.json = true;
2148
2180
  let data = this.data();
2149
- if (this.collects) data = data.map((item) => new this.collects(item).data());
2181
+ if (this.collects && this.data === ResourceCollection.prototype.data) data = data.map((item) => new this.collects(item).data());
2150
2182
  data = normalizeSerializableData(data);
2151
2183
  data = sanitizeConditionalAttributes(data);
2152
2184
  const paginationExtras = !Array.isArray(this.resource) ? buildPaginationExtras(this.resource) : {};
@@ -2187,8 +2219,7 @@ var ResourceCollection = class ResourceCollection extends BaseSerializer {
2187
2219
  */
2188
2220
  toObject() {
2189
2221
  this.called.toObject = true;
2190
- this.json();
2191
- return normalizeSerializableData(Array.isArray(this.resource) ? this.resource : isArkormLikeCollection(this.resource) ? this.resource.all() : this.resource.data);
2222
+ return this.resolveObjectData();
2192
2223
  }
2193
2224
  /**
2194
2225
  * Convert resource to object format and return original data.
@@ -2687,6 +2718,7 @@ exports.hasPaginationLink = hasPaginationLink;
2687
2718
  exports.isArkormLikeCollection = isArkormLikeCollection;
2688
2719
  exports.isArkormLikeModel = isArkormLikeModel;
2689
2720
  exports.isPlainObject = isPlainObject;
2721
+ exports.isResoraCollectionLike = isResoraCollectionLike;
2690
2722
  exports.loadRuntimeConfig = loadRuntimeConfig;
2691
2723
  exports.mergeMetadata = mergeMetadata;
2692
2724
  exports.normalizeSerializableData = normalizeSerializableData;
package/dist/index.d.cts CHANGED
@@ -736,6 +736,8 @@ declare class ResourceCollection<R extends ResourceData[] | Collectible | Collec
736
736
  isPaginatedCollectible(value: unknown): value is Collectible;
737
737
  constructor(rsc: R);
738
738
  constructor(rsc: R, ctx: Response | H3Event | Record<string, any>);
739
+ private getSourceData;
740
+ private resolveObjectData;
739
741
  /**
740
742
  * Get the original resource data
741
743
  */
@@ -1216,6 +1218,12 @@ type ArkormLikeModel = {
1216
1218
  type ArkormLikeCollection = {
1217
1219
  all: () => unknown;
1218
1220
  };
1221
+ type ResoraCollectionLike = {
1222
+ toObject: () => unknown;
1223
+ getBody: () => unknown;
1224
+ json: () => unknown;
1225
+ setCollects: (...args: unknown[]) => unknown;
1226
+ };
1219
1227
  /**
1220
1228
  * Type guard to check if a value is an Arkorm-like model, which is defined as an object
1221
1229
  * that has a toObject method and optionally getRawAttributes, getAttribute, and
@@ -1232,6 +1240,13 @@ declare const isArkormLikeModel: (value: unknown) => value is ArkormLikeModel;
1232
1240
  * @returns
1233
1241
  */
1234
1242
  declare const isArkormLikeCollection: (value: unknown) => value is ArkormLikeCollection;
1243
+ /**
1244
+ * Type guard to check if a value is a Resora collection-like serializer.
1245
+ *
1246
+ * @param value
1247
+ * @returns
1248
+ */
1249
+ declare const isResoraCollectionLike: (value: unknown) => value is ResoraCollectionLike;
1235
1250
  /**
1236
1251
  * Normalize a value for serialization by recursively converting Arkorm-like models and
1237
1252
  * collections to plain objects, while preserving the structure of arrays and plain objects.
@@ -1573,4 +1588,4 @@ declare const extractResponseFromCtx: (ctx: unknown) => any | undefined;
1573
1588
  */
1574
1589
  declare const setCtx: (ctx: unknown) => void;
1575
1590
  //#endregion
1576
- export { ApiResource, CONDITIONAL_ATTRIBUTE_MISSING, CaseStyle, CliApp, Collectible, CollectionBody, CollectionLike, Config, Cursor, GenericBody, GenericResource, InitCommand, MakeResource, MetaData, NonCollectible, PaginatedMetaData, Pagination, PaginatorLike, ResoraConfig, ResoraPlugin, ResoraPluginApi, ResoraPluginUtility, Resource, ResourceBody, ResourceCollection, ResourceData, ResourceDef, ResourceLevelConfig, ResponseData, ResponseDataCollection, ResponseFactory, ResponseFactoryContext, ResponseKind, ResponsePluginEvent, ResponseStructureConfig, SendPluginEvent, SerializePluginEvent, ServerResponse, appendRootProperties, applyRuntimeConfig, buildPaginationExtras, buildResponseEnvelope, createArkormCurrentPageResolver, defineConfig, definePlugin, extractRequestUrl, extractResponseFromCtx, getCaseTransformer, getCtx, getDefaultConfig, getGlobalBaseUrl, getGlobalCase, getGlobalCursorMeta, getGlobalPageName, getGlobalPaginatedExtras, getGlobalPaginatedLinks, getGlobalPaginatedMeta, getGlobalResponseFactory, getGlobalResponseRootKey, getGlobalResponseStructure, getGlobalResponseWrap, getPaginationExtraKeys, getRegisteredPlugins, getRequestUrl, getUtility, hasPaginationLink, isArkormLikeCollection, isArkormLikeModel, isPlainObject, loadRuntimeConfig, mergeMetadata, normalizeSerializableData, registerPlugin, registerUtility, resetPluginsForTests, resetRuntimeConfigForTests, resolveCurrentPage, resolveMergeWhen, resolveWhen, resolveWhenNotNull, resolveWithHookMetadata, runPluginHook, runWithCtx, sanitizeConditionalAttributes, setCtx, setGlobalBaseUrl, setGlobalCase, setGlobalCursorMeta, setGlobalPageName, setGlobalPaginatedExtras, setGlobalPaginatedLinks, setGlobalPaginatedMeta, setGlobalResponseFactory, setGlobalResponseRootKey, setGlobalResponseStructure, setGlobalResponseWrap, setRequestUrl, splitWords, toCamelCase, toKebabCase, toPascalCase, toSnakeCase, transformKeys };
1591
+ export { ApiResource, CONDITIONAL_ATTRIBUTE_MISSING, CaseStyle, CliApp, Collectible, CollectionBody, CollectionLike, Config, Cursor, GenericBody, GenericResource, InitCommand, MakeResource, MetaData, NonCollectible, PaginatedMetaData, Pagination, PaginatorLike, ResoraConfig, ResoraPlugin, ResoraPluginApi, ResoraPluginUtility, Resource, ResourceBody, ResourceCollection, ResourceData, ResourceDef, ResourceLevelConfig, ResponseData, ResponseDataCollection, ResponseFactory, ResponseFactoryContext, ResponseKind, ResponsePluginEvent, ResponseStructureConfig, SendPluginEvent, SerializePluginEvent, ServerResponse, appendRootProperties, applyRuntimeConfig, buildPaginationExtras, buildResponseEnvelope, createArkormCurrentPageResolver, defineConfig, definePlugin, extractRequestUrl, extractResponseFromCtx, getCaseTransformer, getCtx, getDefaultConfig, getGlobalBaseUrl, getGlobalCase, getGlobalCursorMeta, getGlobalPageName, getGlobalPaginatedExtras, getGlobalPaginatedLinks, getGlobalPaginatedMeta, getGlobalResponseFactory, getGlobalResponseRootKey, getGlobalResponseStructure, getGlobalResponseWrap, getPaginationExtraKeys, getRegisteredPlugins, getRequestUrl, getUtility, hasPaginationLink, isArkormLikeCollection, isArkormLikeModel, isPlainObject, isResoraCollectionLike, loadRuntimeConfig, mergeMetadata, normalizeSerializableData, registerPlugin, registerUtility, resetPluginsForTests, resetRuntimeConfigForTests, resolveCurrentPage, resolveMergeWhen, resolveWhen, resolveWhenNotNull, resolveWithHookMetadata, runPluginHook, runWithCtx, sanitizeConditionalAttributes, setCtx, setGlobalBaseUrl, setGlobalCase, setGlobalCursorMeta, setGlobalPageName, setGlobalPaginatedExtras, setGlobalPaginatedLinks, setGlobalPaginatedMeta, setGlobalResponseFactory, setGlobalResponseRootKey, setGlobalResponseStructure, setGlobalResponseWrap, setRequestUrl, splitWords, toCamelCase, toKebabCase, toPascalCase, toSnakeCase, transformKeys };
package/dist/index.d.mts CHANGED
@@ -736,6 +736,8 @@ declare class ResourceCollection<R extends ResourceData[] | Collectible | Collec
736
736
  isPaginatedCollectible(value: unknown): value is Collectible;
737
737
  constructor(rsc: R);
738
738
  constructor(rsc: R, ctx: Response | H3Event | Record<string, any>);
739
+ private getSourceData;
740
+ private resolveObjectData;
739
741
  /**
740
742
  * Get the original resource data
741
743
  */
@@ -1216,6 +1218,12 @@ type ArkormLikeModel = {
1216
1218
  type ArkormLikeCollection = {
1217
1219
  all: () => unknown;
1218
1220
  };
1221
+ type ResoraCollectionLike = {
1222
+ toObject: () => unknown;
1223
+ getBody: () => unknown;
1224
+ json: () => unknown;
1225
+ setCollects: (...args: unknown[]) => unknown;
1226
+ };
1219
1227
  /**
1220
1228
  * Type guard to check if a value is an Arkorm-like model, which is defined as an object
1221
1229
  * that has a toObject method and optionally getRawAttributes, getAttribute, and
@@ -1232,6 +1240,13 @@ declare const isArkormLikeModel: (value: unknown) => value is ArkormLikeModel;
1232
1240
  * @returns
1233
1241
  */
1234
1242
  declare const isArkormLikeCollection: (value: unknown) => value is ArkormLikeCollection;
1243
+ /**
1244
+ * Type guard to check if a value is a Resora collection-like serializer.
1245
+ *
1246
+ * @param value
1247
+ * @returns
1248
+ */
1249
+ declare const isResoraCollectionLike: (value: unknown) => value is ResoraCollectionLike;
1235
1250
  /**
1236
1251
  * Normalize a value for serialization by recursively converting Arkorm-like models and
1237
1252
  * collections to plain objects, while preserving the structure of arrays and plain objects.
@@ -1573,4 +1588,4 @@ declare const extractResponseFromCtx: (ctx: unknown) => any | undefined;
1573
1588
  */
1574
1589
  declare const setCtx: (ctx: unknown) => void;
1575
1590
  //#endregion
1576
- export { ApiResource, CONDITIONAL_ATTRIBUTE_MISSING, CaseStyle, CliApp, Collectible, CollectionBody, CollectionLike, Config, Cursor, GenericBody, GenericResource, InitCommand, MakeResource, MetaData, NonCollectible, PaginatedMetaData, Pagination, PaginatorLike, ResoraConfig, ResoraPlugin, ResoraPluginApi, ResoraPluginUtility, Resource, ResourceBody, ResourceCollection, ResourceData, ResourceDef, ResourceLevelConfig, ResponseData, ResponseDataCollection, ResponseFactory, ResponseFactoryContext, ResponseKind, ResponsePluginEvent, ResponseStructureConfig, SendPluginEvent, SerializePluginEvent, ServerResponse, appendRootProperties, applyRuntimeConfig, buildPaginationExtras, buildResponseEnvelope, createArkormCurrentPageResolver, defineConfig, definePlugin, extractRequestUrl, extractResponseFromCtx, getCaseTransformer, getCtx, getDefaultConfig, getGlobalBaseUrl, getGlobalCase, getGlobalCursorMeta, getGlobalPageName, getGlobalPaginatedExtras, getGlobalPaginatedLinks, getGlobalPaginatedMeta, getGlobalResponseFactory, getGlobalResponseRootKey, getGlobalResponseStructure, getGlobalResponseWrap, getPaginationExtraKeys, getRegisteredPlugins, getRequestUrl, getUtility, hasPaginationLink, isArkormLikeCollection, isArkormLikeModel, isPlainObject, loadRuntimeConfig, mergeMetadata, normalizeSerializableData, registerPlugin, registerUtility, resetPluginsForTests, resetRuntimeConfigForTests, resolveCurrentPage, resolveMergeWhen, resolveWhen, resolveWhenNotNull, resolveWithHookMetadata, runPluginHook, runWithCtx, sanitizeConditionalAttributes, setCtx, setGlobalBaseUrl, setGlobalCase, setGlobalCursorMeta, setGlobalPageName, setGlobalPaginatedExtras, setGlobalPaginatedLinks, setGlobalPaginatedMeta, setGlobalResponseFactory, setGlobalResponseRootKey, setGlobalResponseStructure, setGlobalResponseWrap, setRequestUrl, splitWords, toCamelCase, toKebabCase, toPascalCase, toSnakeCase, transformKeys };
1591
+ export { ApiResource, CONDITIONAL_ATTRIBUTE_MISSING, CaseStyle, CliApp, Collectible, CollectionBody, CollectionLike, Config, Cursor, GenericBody, GenericResource, InitCommand, MakeResource, MetaData, NonCollectible, PaginatedMetaData, Pagination, PaginatorLike, ResoraConfig, ResoraPlugin, ResoraPluginApi, ResoraPluginUtility, Resource, ResourceBody, ResourceCollection, ResourceData, ResourceDef, ResourceLevelConfig, ResponseData, ResponseDataCollection, ResponseFactory, ResponseFactoryContext, ResponseKind, ResponsePluginEvent, ResponseStructureConfig, SendPluginEvent, SerializePluginEvent, ServerResponse, appendRootProperties, applyRuntimeConfig, buildPaginationExtras, buildResponseEnvelope, createArkormCurrentPageResolver, defineConfig, definePlugin, extractRequestUrl, extractResponseFromCtx, getCaseTransformer, getCtx, getDefaultConfig, getGlobalBaseUrl, getGlobalCase, getGlobalCursorMeta, getGlobalPageName, getGlobalPaginatedExtras, getGlobalPaginatedLinks, getGlobalPaginatedMeta, getGlobalResponseFactory, getGlobalResponseRootKey, getGlobalResponseStructure, getGlobalResponseWrap, getPaginationExtraKeys, getRegisteredPlugins, getRequestUrl, getUtility, hasPaginationLink, isArkormLikeCollection, isArkormLikeModel, isPlainObject, isResoraCollectionLike, loadRuntimeConfig, mergeMetadata, normalizeSerializableData, registerPlugin, registerUtility, resetPluginsForTests, resetRuntimeConfigForTests, resolveCurrentPage, resolveMergeWhen, resolveWhen, resolveWhenNotNull, resolveWithHookMetadata, runPluginHook, runWithCtx, sanitizeConditionalAttributes, setCtx, setGlobalBaseUrl, setGlobalCase, setGlobalCursorMeta, setGlobalPageName, setGlobalPaginatedExtras, setGlobalPaginatedLinks, setGlobalPaginatedMeta, setGlobalResponseFactory, setGlobalResponseRootKey, setGlobalResponseStructure, setGlobalResponseWrap, setRequestUrl, splitWords, toCamelCase, toKebabCase, toPascalCase, toSnakeCase, transformKeys };
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
  };
@@ -428,6 +430,17 @@ const isArkormLikeCollection = (value) => {
428
430
  return typeof value.all === "function";
429
431
  };
430
432
  /**
433
+ * Type guard to check if a value is a Resora collection-like serializer.
434
+ *
435
+ * @param value
436
+ * @returns
437
+ */
438
+ const isResoraCollectionLike = (value) => {
439
+ if (!value || typeof value !== "object") return false;
440
+ const candidate = value;
441
+ return typeof candidate.toObject === "function" && typeof candidate.getBody === "function" && typeof candidate.json === "function" && typeof candidate.setCollects === "function";
442
+ };
443
+ /**
431
444
  * Normalize a value for serialization by recursively converting Arkorm-like models and
432
445
  * collections to plain objects, while preserving the structure of arrays and plain objects.
433
446
  *
@@ -436,6 +449,7 @@ const isArkormLikeCollection = (value) => {
436
449
  */
437
450
  const normalizeSerializableData = (value) => {
438
451
  if (Array.isArray(value)) return value.map((item) => normalizeSerializableData(item));
452
+ if (isResoraCollectionLike(value)) return normalizeSerializableData(value.toObject());
439
453
  if (isArkormLikeModel(value)) return normalizeSerializableData(value.toObject());
440
454
  if (isArkormLikeCollection(value)) {
441
455
  const collectionData = value.all();
@@ -1311,8 +1325,10 @@ var ServerResponse = class {
1311
1325
  */
1312
1326
  #addHeader(key, value) {
1313
1327
  this.headers[key] = value;
1314
- 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);
1315
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);
1316
1332
  }
1317
1333
  /**
1318
1334
  * Dispatch the current body and apply any deferred transport state.
@@ -1334,9 +1350,10 @@ var ServerResponse = class {
1334
1350
  this.headers = { ...beforeSend.headers };
1335
1351
  if ("send" in this.response && typeof this.response.send === "function") {
1336
1352
  if ("statusCode" in this.response) this.response.statusCode = this._status;
1337
- const sentResponse = this.response.send(this.body);
1338
- if (sentResponse && "status" in sentResponse && typeof sentResponse.status === "function") sentResponse.status(this._status);
1339
- 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);
1340
1357
  runPluginHook("afterSend", {
1341
1358
  response: this,
1342
1359
  rawResponse: this.response,
@@ -1346,7 +1363,14 @@ var ServerResponse = class {
1346
1363
  });
1347
1364
  return this.body;
1348
1365
  }
1349
- 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) {
1371
+ this.response.body = this.body;
1372
+ this.response.__resoraStatus = this._status;
1373
+ }
1350
1374
  runPluginHook("afterSend", {
1351
1375
  response: this,
1352
1376
  rawResponse: this.response,
@@ -2033,11 +2057,19 @@ var ResourceCollection = class ResourceCollection extends BaseSerializer {
2033
2057
  this.res = extractResponseFromCtx(ctx);
2034
2058
  }
2035
2059
  }
2060
+ getSourceData() {
2061
+ return Array.isArray(this.resource) ? this.resource : isArkormLikeCollection(this.resource) ? this.resource.all() : this.resource.data;
2062
+ }
2063
+ resolveObjectData() {
2064
+ let data = this.getSourceData();
2065
+ if (this.collects) data = data.map((item) => new this.collects(item).data());
2066
+ return normalizeSerializableData(data);
2067
+ }
2036
2068
  /**
2037
2069
  * Get the original resource data
2038
2070
  */
2039
2071
  data() {
2040
- return this.toObject();
2072
+ return this.getSourceData();
2041
2073
  }
2042
2074
  /**
2043
2075
  * Get the current serialized output body.
@@ -2117,7 +2149,7 @@ var ResourceCollection = class ResourceCollection extends BaseSerializer {
2117
2149
  if (!this.called.json) {
2118
2150
  this.called.json = true;
2119
2151
  let data = this.data();
2120
- if (this.collects) data = data.map((item) => new this.collects(item).data());
2152
+ if (this.collects && this.data === ResourceCollection.prototype.data) data = data.map((item) => new this.collects(item).data());
2121
2153
  data = normalizeSerializableData(data);
2122
2154
  data = sanitizeConditionalAttributes(data);
2123
2155
  const paginationExtras = !Array.isArray(this.resource) ? buildPaginationExtras(this.resource) : {};
@@ -2158,8 +2190,7 @@ var ResourceCollection = class ResourceCollection extends BaseSerializer {
2158
2190
  */
2159
2191
  toObject() {
2160
2192
  this.called.toObject = true;
2161
- this.json();
2162
- return normalizeSerializableData(Array.isArray(this.resource) ? this.resource : isArkormLikeCollection(this.resource) ? this.resource.all() : this.resource.data);
2193
+ return this.resolveObjectData();
2163
2194
  }
2164
2195
  /**
2165
2196
  * Convert resource to object format and return original data.
@@ -2618,4 +2649,4 @@ var Resource = class Resource extends BaseSerializer {
2618
2649
  };
2619
2650
 
2620
2651
  //#endregion
2621
- export { ApiResource, CONDITIONAL_ATTRIBUTE_MISSING, CliApp, GenericResource, InitCommand, MakeResource, Resource, ResourceCollection, ServerResponse, appendRootProperties, applyRuntimeConfig, buildPaginationExtras, buildResponseEnvelope, createArkormCurrentPageResolver, defineConfig, definePlugin, extractRequestUrl, extractResponseFromCtx, getCaseTransformer, getCtx, getDefaultConfig, getGlobalBaseUrl, getGlobalCase, getGlobalCursorMeta, getGlobalPageName, getGlobalPaginatedExtras, getGlobalPaginatedLinks, getGlobalPaginatedMeta, getGlobalResponseFactory, getGlobalResponseRootKey, getGlobalResponseStructure, getGlobalResponseWrap, getPaginationExtraKeys, getRegisteredPlugins, getRequestUrl, getUtility, hasPaginationLink, isArkormLikeCollection, isArkormLikeModel, isPlainObject, loadRuntimeConfig, mergeMetadata, normalizeSerializableData, registerPlugin, registerUtility, resetPluginsForTests, resetRuntimeConfigForTests, resolveCurrentPage, resolveMergeWhen, resolveWhen, resolveWhenNotNull, resolveWithHookMetadata, runPluginHook, runWithCtx, sanitizeConditionalAttributes, setCtx, setGlobalBaseUrl, setGlobalCase, setGlobalCursorMeta, setGlobalPageName, setGlobalPaginatedExtras, setGlobalPaginatedLinks, setGlobalPaginatedMeta, setGlobalResponseFactory, setGlobalResponseRootKey, setGlobalResponseStructure, setGlobalResponseWrap, setRequestUrl, splitWords, toCamelCase, toKebabCase, toPascalCase, toSnakeCase, transformKeys };
2652
+ export { ApiResource, CONDITIONAL_ATTRIBUTE_MISSING, CliApp, GenericResource, InitCommand, MakeResource, Resource, ResourceCollection, ServerResponse, appendRootProperties, applyRuntimeConfig, buildPaginationExtras, buildResponseEnvelope, createArkormCurrentPageResolver, defineConfig, definePlugin, extractRequestUrl, extractResponseFromCtx, getCaseTransformer, getCtx, getDefaultConfig, getGlobalBaseUrl, getGlobalCase, getGlobalCursorMeta, getGlobalPageName, getGlobalPaginatedExtras, getGlobalPaginatedLinks, getGlobalPaginatedMeta, getGlobalResponseFactory, getGlobalResponseRootKey, getGlobalResponseStructure, getGlobalResponseWrap, getPaginationExtraKeys, getRegisteredPlugins, getRequestUrl, getUtility, hasPaginationLink, isArkormLikeCollection, isArkormLikeModel, isPlainObject, isResoraCollectionLike, loadRuntimeConfig, mergeMetadata, normalizeSerializableData, registerPlugin, registerUtility, resetPluginsForTests, resetRuntimeConfigForTests, resolveCurrentPage, resolveMergeWhen, resolveWhen, resolveWhenNotNull, resolveWithHookMetadata, runPluginHook, runWithCtx, sanitizeConditionalAttributes, setCtx, setGlobalBaseUrl, setGlobalCase, setGlobalCursorMeta, setGlobalPageName, setGlobalPaginatedExtras, setGlobalPaginatedLinks, setGlobalPaginatedMeta, setGlobalResponseFactory, setGlobalResponseRootKey, setGlobalResponseStructure, setGlobalResponseWrap, setRequestUrl, splitWords, toCamelCase, toKebabCase, toPascalCase, toSnakeCase, transformKeys };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "resora",
3
- "version": "1.0.1",
3
+ "version": "1.2.0",
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,14 +50,13 @@
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
54
  "arkormx": "^0.2.6",
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",
@@ -85,6 +84,6 @@
85
84
  "docs:build": "vitepress build docs",
86
85
  "docs:preview": "vitepress preview docs",
87
86
  "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"
87
+ "publish:packages": "pnpm version:packages && pnpm -r --filter \"@resora/*\" publish --access public"
89
88
  }
90
89
  }