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 +56 -9
- package/dist/index.cjs +41 -9
- package/dist/index.d.cts +16 -1
- package/dist/index.d.mts +16 -1
- package/dist/index.mjs +41 -10
- package/package.json +3 -4
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
|
-
|
|
1367
|
-
if (
|
|
1368
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1338
|
-
if (
|
|
1339
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
"
|
|
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 \"@
|
|
87
|
+
"publish:packages": "pnpm version:packages && pnpm -r --filter \"@resora/*\" publish --access public"
|
|
89
88
|
}
|
|
90
89
|
}
|