resora 1.3.2 → 1.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -436,17 +436,17 @@ const mergeMetadata = (base, incoming) => {
436
436
  //#endregion
437
437
  //#region src/utilities/arkorm.ts
438
438
  /**
439
- * Type guard to check if a value is an Arkorm-like model, which is defined as an object
440
- * that has a toObject method and optionally getRawAttributes, getAttribute, and
441
- * setAttribute methods.
439
+ * Type guard to check if a value is an Arkorm-like model.
440
+ *
441
+ * Arkorm models expose `toObject()`. Some production model variants/proxies do
442
+ * not expose `getRawAttributes()`, so the serializer should not require it.
442
443
  *
443
444
  * @param value The value to check
444
445
  * @returns True if the value is an Arkorm-like model, false otherwise
445
446
  */
446
447
  const isArkormLikeModel = (value) => {
447
448
  if (!value || typeof value !== "object") return false;
448
- const candidate = value;
449
- return typeof candidate.toObject === "function" && typeof candidate.getRawAttributes === "function";
449
+ return typeof value.toObject === "function";
450
450
  };
451
451
  /**
452
452
  * Type guard to check if a value is an Arkorm-like collection, which is defined as an object
@@ -491,6 +491,30 @@ const normalizeSerializableData = (value) => {
491
491
  }, {});
492
492
  return value;
493
493
  };
494
+ const isPromiseLike = (value) => {
495
+ return !!value && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
496
+ };
497
+ /**
498
+ * Async variant of normalizeSerializableData. It resolves promise-like values at
499
+ * every nesting level before converting Arkorm-like models and collections.
500
+ *
501
+ * @param value The value to normalize
502
+ * @returns The normalized value, ready for serialization
503
+ */
504
+ const normalizeSerializableDataAsync = async (value) => {
505
+ const resolvedValue = isPromiseLike(value) ? await value : value;
506
+ if (Array.isArray(resolvedValue)) return Promise.all(resolvedValue.map((item) => normalizeSerializableDataAsync(item)));
507
+ if (isResoraCollectionLike(resolvedValue)) return normalizeSerializableDataAsync(resolvedValue.toObject());
508
+ if (isArkormLikeModel(resolvedValue)) return normalizeSerializableDataAsync(resolvedValue.toObject());
509
+ if (isArkormLikeCollection(resolvedValue)) return normalizeSerializableDataAsync(resolvedValue.all());
510
+ if (isPlainObject(resolvedValue)) return (await Promise.all(Object.entries(resolvedValue).map(async ([key, nestedValue]) => {
511
+ return [key, await normalizeSerializableDataAsync(nestedValue)];
512
+ }))).reduce((accumulator, [key, nestedValue]) => {
513
+ accumulator[key] = nestedValue;
514
+ return accumulator;
515
+ }, {});
516
+ return resolvedValue;
517
+ };
494
518
 
495
519
  //#endregion
496
520
  //#region src/utilities/pagination.ts
@@ -1901,6 +1925,48 @@ var GenericResource = class GenericResource extends BaseSerializer {
1901
1925
  }
1902
1926
  serialize(data);
1903
1927
  }
1928
+ async serializeGenericResourceAsync(resource, ctx) {
1929
+ let data = await normalizeSerializableDataAsync(resource);
1930
+ const serialize = (resolvedData) => {
1931
+ data = resolvedData;
1932
+ if (!Array.isArray(data) && data && typeof data.data !== "undefined") data = data.data;
1933
+ data = sanitizeConditionalAttributes(data);
1934
+ const paginationExtras = buildPaginationExtras(this.resource);
1935
+ const { metaKey } = getPaginationExtraKeys();
1936
+ const configuredMeta = metaKey ? paginationExtras[metaKey] : void 0;
1937
+ if (metaKey) delete paginationExtras[metaKey];
1938
+ const caseStyle = this.resolveSerializerCaseStyle(this.constructor, this.resolveCollectsConfig());
1939
+ if (caseStyle) {
1940
+ const transformer = getCaseTransformer(caseStyle);
1941
+ data = transformKeys(data, transformer);
1942
+ }
1943
+ const customMeta = this.resolveMergedMeta(GenericResource.prototype.with);
1944
+ const { wrap, rootKey, factory } = this.resolveResponseStructure();
1945
+ this.body = buildResponseEnvelope({
1946
+ payload: data,
1947
+ meta: configuredMeta,
1948
+ metaKey,
1949
+ wrap,
1950
+ rootKey,
1951
+ factory,
1952
+ context: {
1953
+ type: "generic",
1954
+ resource: this.resource
1955
+ }
1956
+ });
1957
+ this.body = appendRootProperties(this.body, {
1958
+ ...paginationExtras,
1959
+ ...customMeta || {}
1960
+ }, rootKey);
1961
+ this.body = this.applySerializePlugins(this.body);
1962
+ };
1963
+ if (Array.isArray(data) && this.collects) {
1964
+ const collected = data.map((item) => new this.collects(item).data(ctx));
1965
+ data = await Promise.all(collected);
1966
+ data = await normalizeSerializableDataAsync(data);
1967
+ }
1968
+ serialize(data);
1969
+ }
1904
1970
  /**
1905
1971
  * Convert resource to JSON response format
1906
1972
  *
@@ -1912,8 +1978,7 @@ var GenericResource = class GenericResource extends BaseSerializer {
1912
1978
  const ctx = this.resolveSerializationContext();
1913
1979
  const resource = this.data(ctx);
1914
1980
  if (this.isPromiseLike(resource)) this.serializationPromise = Promise.resolve(resource).then((resolved) => {
1915
- const result = this.serializeGenericResource(resolved, ctx);
1916
- if (this.isPromiseLike(result)) return result;
1981
+ return this.serializeGenericResourceAsync(resolved, ctx);
1917
1982
  });
1918
1983
  else {
1919
1984
  const result = this.serializeGenericResource(resource, ctx);
@@ -2244,6 +2309,38 @@ var ResourceCollection = class ResourceCollection extends BaseSerializer {
2244
2309
  }, rootKey);
2245
2310
  this.body = this.applySerializePlugins(this.body);
2246
2311
  }
2312
+ async serializeCollectionDataAsync(items) {
2313
+ let data = await normalizeSerializableDataAsync(items);
2314
+ data = sanitizeConditionalAttributes(data);
2315
+ const paginationExtras = !Array.isArray(this.resource) ? buildPaginationExtras(this.resource) : {};
2316
+ const { metaKey } = getPaginationExtraKeys();
2317
+ const configuredMeta = metaKey ? paginationExtras[metaKey] : void 0;
2318
+ if (metaKey) delete paginationExtras[metaKey];
2319
+ const caseStyle = this.resolveSerializerCaseStyle(this.constructor, this.resolveCollectsConfig());
2320
+ if (caseStyle) {
2321
+ const transformer = getCaseTransformer(caseStyle);
2322
+ data = transformKeys(data, transformer);
2323
+ }
2324
+ const customMeta = this.resolveMergedMeta(ResourceCollection.prototype.with);
2325
+ const { wrap, rootKey, factory } = this.resolveResponseStructure();
2326
+ this.body = buildResponseEnvelope({
2327
+ payload: data,
2328
+ meta: configuredMeta,
2329
+ metaKey,
2330
+ wrap,
2331
+ rootKey,
2332
+ factory,
2333
+ context: {
2334
+ type: "collection",
2335
+ resource: this.resource
2336
+ }
2337
+ });
2338
+ this.body = appendRootProperties(this.body, {
2339
+ ...paginationExtras,
2340
+ ...customMeta || {}
2341
+ }, rootKey);
2342
+ this.body = this.applySerializePlugins(this.body);
2343
+ }
2247
2344
  resolveCollectionDataForSerialization(items, ctx) {
2248
2345
  if (this.collects && this.data === ResourceCollection.prototype.data) {
2249
2346
  const collected = items.map((item) => new this.collects(item).data(ctx));
@@ -2265,11 +2362,16 @@ var ResourceCollection = class ResourceCollection extends BaseSerializer {
2265
2362
  const serialize = (items) => {
2266
2363
  const resolvedData = this.resolveCollectionDataForSerialization(items, ctx);
2267
2364
  if (this.isPromiseLike(resolvedData)) return resolvedData.then((resolved) => {
2268
- this.serializeCollectionData(resolved);
2365
+ return this.serializeCollectionDataAsync(resolved);
2269
2366
  });
2270
2367
  this.serializeCollectionData(resolvedData);
2271
2368
  };
2272
- if (this.isPromiseLike(data)) this.serializationPromise = Promise.resolve(data).then(serialize);
2369
+ if (this.isPromiseLike(data)) this.serializationPromise = Promise.resolve(data).then((items) => {
2370
+ const resolvedData = this.resolveCollectionDataForSerialization(items, ctx);
2371
+ return Promise.resolve(resolvedData).then((resolved) => {
2372
+ return this.serializeCollectionDataAsync(resolved);
2373
+ });
2374
+ });
2273
2375
  else {
2274
2376
  const result = serialize(data);
2275
2377
  if (this.isPromiseLike(result)) this.serializationPromise = Promise.resolve(result);
@@ -2568,6 +2670,30 @@ var Resource = class Resource extends BaseSerializer {
2568
2670
  this.body = appendRootProperties(this.body, customMeta, rootKey);
2569
2671
  this.body = this.applySerializePlugins(this.body);
2570
2672
  }
2673
+ async serializeResourceAsync(resource) {
2674
+ let data = await normalizeSerializableDataAsync(resource);
2675
+ if (!Array.isArray(data) && data && typeof data.data !== "undefined") data = data.data;
2676
+ data = sanitizeConditionalAttributes(data);
2677
+ const caseStyle = this.resolveSerializerCaseStyle(this.constructor);
2678
+ if (caseStyle) {
2679
+ const transformer = getCaseTransformer(caseStyle);
2680
+ data = transformKeys(data, transformer);
2681
+ }
2682
+ const customMeta = this.resolveMergedMeta(Resource.prototype.with);
2683
+ const { wrap, rootKey, factory } = this.resolveResponseStructure();
2684
+ this.body = buildResponseEnvelope({
2685
+ payload: data,
2686
+ wrap,
2687
+ rootKey,
2688
+ factory,
2689
+ context: {
2690
+ type: "resource",
2691
+ resource: this.resource
2692
+ }
2693
+ });
2694
+ this.body = appendRootProperties(this.body, customMeta, rootKey);
2695
+ this.body = this.applySerializePlugins(this.body);
2696
+ }
2571
2697
  /**
2572
2698
  * Convert resource to JSON response format
2573
2699
  *
@@ -2579,7 +2705,7 @@ var Resource = class Resource extends BaseSerializer {
2579
2705
  const ctx = this.resolveSerializationContext();
2580
2706
  const resource = this.data(ctx);
2581
2707
  if (this.isPromiseLike(resource)) this.serializationPromise = Promise.resolve(resource).then((resolved) => {
2582
- this.serializeResource(resolved);
2708
+ return this.serializeResourceAsync(resolved);
2583
2709
  });
2584
2710
  else this.serializeResource(resource);
2585
2711
  }
@@ -2795,6 +2921,7 @@ exports.isResoraCollectionLike = isResoraCollectionLike;
2795
2921
  exports.loadRuntimeConfig = loadRuntimeConfig;
2796
2922
  exports.mergeMetadata = mergeMetadata;
2797
2923
  exports.normalizeSerializableData = normalizeSerializableData;
2924
+ exports.normalizeSerializableDataAsync = normalizeSerializableDataAsync;
2798
2925
  exports.registerPlugin = registerPlugin;
2799
2926
  exports.registerUtility = registerUtility;
2800
2927
  exports.resetPluginsForTests = resetPluginsForTests;
package/dist/index.d.cts CHANGED
@@ -790,6 +790,7 @@ declare class ResourceCollection<R extends ResourceData[] | Collectible | Collec
790
790
  */
791
791
  private getPayloadKey;
792
792
  private serializeCollectionData;
793
+ private serializeCollectionDataAsync;
793
794
  private resolveCollectionDataForSerialization;
794
795
  /**
795
796
  * Convert resource to JSON response format
@@ -909,6 +910,7 @@ declare class Resource<R extends ResourceData | NonCollectible = ResourceData> e
909
910
  protected getSerializerType(): "resource";
910
911
  private getPayloadKey;
911
912
  private serializeResource;
913
+ private serializeResourceAsync;
912
914
  /**
913
915
  * Convert resource to JSON response format
914
916
  *
@@ -1031,6 +1033,7 @@ declare class GenericResource<R extends NonCollectible | Collectible | Collectio
1031
1033
  protected getSerializerType(): "generic";
1032
1034
  private getPayloadKey;
1033
1035
  private serializeGenericResource;
1036
+ private serializeGenericResourceAsync;
1034
1037
  /**
1035
1038
  * Convert resource to JSON response format
1036
1039
  *
@@ -1222,7 +1225,7 @@ declare const defineConfig: (config: ResoraConfig) => Config;
1222
1225
  //#endregion
1223
1226
  //#region src/utilities/arkorm.d.ts
1224
1227
  type ArkormLikeModel = {
1225
- toObject: () => Record<string, any>;
1228
+ toObject: () => Record<string, any> | PromiseLike<Record<string, any>>;
1226
1229
  getRawAttributes?: () => Record<string, any>;
1227
1230
  getAttribute?: (key: string) => unknown;
1228
1231
  setAttribute?: (key: string, value: unknown) => unknown;
@@ -1237,9 +1240,10 @@ type ResoraCollectionLike = {
1237
1240
  setCollects: (...args: unknown[]) => unknown;
1238
1241
  };
1239
1242
  /**
1240
- * Type guard to check if a value is an Arkorm-like model, which is defined as an object
1241
- * that has a toObject method and optionally getRawAttributes, getAttribute, and
1242
- * setAttribute methods.
1243
+ * Type guard to check if a value is an Arkorm-like model.
1244
+ *
1245
+ * Arkorm models expose `toObject()`. Some production model variants/proxies do
1246
+ * not expose `getRawAttributes()`, so the serializer should not require it.
1243
1247
  *
1244
1248
  * @param value The value to check
1245
1249
  * @returns True if the value is an Arkorm-like model, false otherwise
@@ -1267,6 +1271,14 @@ declare const isResoraCollectionLike: (value: unknown) => value is ResoraCollect
1267
1271
  * @returns The normalized value, ready for serialization
1268
1272
  */
1269
1273
  declare const normalizeSerializableData: (value: unknown) => unknown;
1274
+ /**
1275
+ * Async variant of normalizeSerializableData. It resolves promise-like values at
1276
+ * every nesting level before converting Arkorm-like models and collections.
1277
+ *
1278
+ * @param value The value to normalize
1279
+ * @returns The normalized value, ready for serialization
1280
+ */
1281
+ declare const normalizeSerializableDataAsync: (value: unknown) => Promise<unknown>;
1270
1282
  //#endregion
1271
1283
  //#region src/utilities/metadata.d.ts
1272
1284
  /**
@@ -1600,4 +1612,4 @@ declare const extractResponseFromCtx: (ctx: unknown) => any | undefined;
1600
1612
  */
1601
1613
  declare const setCtx: (ctx: unknown) => void;
1602
1614
  //#endregion
1603
- 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 };
1615
+ 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, normalizeSerializableDataAsync, 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
@@ -790,6 +790,7 @@ declare class ResourceCollection<R extends ResourceData[] | Collectible | Collec
790
790
  */
791
791
  private getPayloadKey;
792
792
  private serializeCollectionData;
793
+ private serializeCollectionDataAsync;
793
794
  private resolveCollectionDataForSerialization;
794
795
  /**
795
796
  * Convert resource to JSON response format
@@ -909,6 +910,7 @@ declare class Resource<R extends ResourceData | NonCollectible = ResourceData> e
909
910
  protected getSerializerType(): "resource";
910
911
  private getPayloadKey;
911
912
  private serializeResource;
913
+ private serializeResourceAsync;
912
914
  /**
913
915
  * Convert resource to JSON response format
914
916
  *
@@ -1031,6 +1033,7 @@ declare class GenericResource<R extends NonCollectible | Collectible | Collectio
1031
1033
  protected getSerializerType(): "generic";
1032
1034
  private getPayloadKey;
1033
1035
  private serializeGenericResource;
1036
+ private serializeGenericResourceAsync;
1034
1037
  /**
1035
1038
  * Convert resource to JSON response format
1036
1039
  *
@@ -1222,7 +1225,7 @@ declare const defineConfig: (config: ResoraConfig) => Config;
1222
1225
  //#endregion
1223
1226
  //#region src/utilities/arkorm.d.ts
1224
1227
  type ArkormLikeModel = {
1225
- toObject: () => Record<string, any>;
1228
+ toObject: () => Record<string, any> | PromiseLike<Record<string, any>>;
1226
1229
  getRawAttributes?: () => Record<string, any>;
1227
1230
  getAttribute?: (key: string) => unknown;
1228
1231
  setAttribute?: (key: string, value: unknown) => unknown;
@@ -1237,9 +1240,10 @@ type ResoraCollectionLike = {
1237
1240
  setCollects: (...args: unknown[]) => unknown;
1238
1241
  };
1239
1242
  /**
1240
- * Type guard to check if a value is an Arkorm-like model, which is defined as an object
1241
- * that has a toObject method and optionally getRawAttributes, getAttribute, and
1242
- * setAttribute methods.
1243
+ * Type guard to check if a value is an Arkorm-like model.
1244
+ *
1245
+ * Arkorm models expose `toObject()`. Some production model variants/proxies do
1246
+ * not expose `getRawAttributes()`, so the serializer should not require it.
1243
1247
  *
1244
1248
  * @param value The value to check
1245
1249
  * @returns True if the value is an Arkorm-like model, false otherwise
@@ -1267,6 +1271,14 @@ declare const isResoraCollectionLike: (value: unknown) => value is ResoraCollect
1267
1271
  * @returns The normalized value, ready for serialization
1268
1272
  */
1269
1273
  declare const normalizeSerializableData: (value: unknown) => unknown;
1274
+ /**
1275
+ * Async variant of normalizeSerializableData. It resolves promise-like values at
1276
+ * every nesting level before converting Arkorm-like models and collections.
1277
+ *
1278
+ * @param value The value to normalize
1279
+ * @returns The normalized value, ready for serialization
1280
+ */
1281
+ declare const normalizeSerializableDataAsync: (value: unknown) => Promise<unknown>;
1270
1282
  //#endregion
1271
1283
  //#region src/utilities/metadata.d.ts
1272
1284
  /**
@@ -1600,4 +1612,4 @@ declare const extractResponseFromCtx: (ctx: unknown) => any | undefined;
1600
1612
  */
1601
1613
  declare const setCtx: (ctx: unknown) => void;
1602
1614
  //#endregion
1603
- 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 };
1615
+ 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, normalizeSerializableDataAsync, 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
@@ -407,17 +407,17 @@ const mergeMetadata = (base, incoming) => {
407
407
  //#endregion
408
408
  //#region src/utilities/arkorm.ts
409
409
  /**
410
- * Type guard to check if a value is an Arkorm-like model, which is defined as an object
411
- * that has a toObject method and optionally getRawAttributes, getAttribute, and
412
- * setAttribute methods.
410
+ * Type guard to check if a value is an Arkorm-like model.
411
+ *
412
+ * Arkorm models expose `toObject()`. Some production model variants/proxies do
413
+ * not expose `getRawAttributes()`, so the serializer should not require it.
413
414
  *
414
415
  * @param value The value to check
415
416
  * @returns True if the value is an Arkorm-like model, false otherwise
416
417
  */
417
418
  const isArkormLikeModel = (value) => {
418
419
  if (!value || typeof value !== "object") return false;
419
- const candidate = value;
420
- return typeof candidate.toObject === "function" && typeof candidate.getRawAttributes === "function";
420
+ return typeof value.toObject === "function";
421
421
  };
422
422
  /**
423
423
  * Type guard to check if a value is an Arkorm-like collection, which is defined as an object
@@ -462,6 +462,30 @@ const normalizeSerializableData = (value) => {
462
462
  }, {});
463
463
  return value;
464
464
  };
465
+ const isPromiseLike = (value) => {
466
+ return !!value && (typeof value === "object" || typeof value === "function") && typeof value.then === "function";
467
+ };
468
+ /**
469
+ * Async variant of normalizeSerializableData. It resolves promise-like values at
470
+ * every nesting level before converting Arkorm-like models and collections.
471
+ *
472
+ * @param value The value to normalize
473
+ * @returns The normalized value, ready for serialization
474
+ */
475
+ const normalizeSerializableDataAsync = async (value) => {
476
+ const resolvedValue = isPromiseLike(value) ? await value : value;
477
+ if (Array.isArray(resolvedValue)) return Promise.all(resolvedValue.map((item) => normalizeSerializableDataAsync(item)));
478
+ if (isResoraCollectionLike(resolvedValue)) return normalizeSerializableDataAsync(resolvedValue.toObject());
479
+ if (isArkormLikeModel(resolvedValue)) return normalizeSerializableDataAsync(resolvedValue.toObject());
480
+ if (isArkormLikeCollection(resolvedValue)) return normalizeSerializableDataAsync(resolvedValue.all());
481
+ if (isPlainObject(resolvedValue)) return (await Promise.all(Object.entries(resolvedValue).map(async ([key, nestedValue]) => {
482
+ return [key, await normalizeSerializableDataAsync(nestedValue)];
483
+ }))).reduce((accumulator, [key, nestedValue]) => {
484
+ accumulator[key] = nestedValue;
485
+ return accumulator;
486
+ }, {});
487
+ return resolvedValue;
488
+ };
465
489
 
466
490
  //#endregion
467
491
  //#region src/utilities/pagination.ts
@@ -1872,6 +1896,48 @@ var GenericResource = class GenericResource extends BaseSerializer {
1872
1896
  }
1873
1897
  serialize(data);
1874
1898
  }
1899
+ async serializeGenericResourceAsync(resource, ctx) {
1900
+ let data = await normalizeSerializableDataAsync(resource);
1901
+ const serialize = (resolvedData) => {
1902
+ data = resolvedData;
1903
+ if (!Array.isArray(data) && data && typeof data.data !== "undefined") data = data.data;
1904
+ data = sanitizeConditionalAttributes(data);
1905
+ const paginationExtras = buildPaginationExtras(this.resource);
1906
+ const { metaKey } = getPaginationExtraKeys();
1907
+ const configuredMeta = metaKey ? paginationExtras[metaKey] : void 0;
1908
+ if (metaKey) delete paginationExtras[metaKey];
1909
+ const caseStyle = this.resolveSerializerCaseStyle(this.constructor, this.resolveCollectsConfig());
1910
+ if (caseStyle) {
1911
+ const transformer = getCaseTransformer(caseStyle);
1912
+ data = transformKeys(data, transformer);
1913
+ }
1914
+ const customMeta = this.resolveMergedMeta(GenericResource.prototype.with);
1915
+ const { wrap, rootKey, factory } = this.resolveResponseStructure();
1916
+ this.body = buildResponseEnvelope({
1917
+ payload: data,
1918
+ meta: configuredMeta,
1919
+ metaKey,
1920
+ wrap,
1921
+ rootKey,
1922
+ factory,
1923
+ context: {
1924
+ type: "generic",
1925
+ resource: this.resource
1926
+ }
1927
+ });
1928
+ this.body = appendRootProperties(this.body, {
1929
+ ...paginationExtras,
1930
+ ...customMeta || {}
1931
+ }, rootKey);
1932
+ this.body = this.applySerializePlugins(this.body);
1933
+ };
1934
+ if (Array.isArray(data) && this.collects) {
1935
+ const collected = data.map((item) => new this.collects(item).data(ctx));
1936
+ data = await Promise.all(collected);
1937
+ data = await normalizeSerializableDataAsync(data);
1938
+ }
1939
+ serialize(data);
1940
+ }
1875
1941
  /**
1876
1942
  * Convert resource to JSON response format
1877
1943
  *
@@ -1883,8 +1949,7 @@ var GenericResource = class GenericResource extends BaseSerializer {
1883
1949
  const ctx = this.resolveSerializationContext();
1884
1950
  const resource = this.data(ctx);
1885
1951
  if (this.isPromiseLike(resource)) this.serializationPromise = Promise.resolve(resource).then((resolved) => {
1886
- const result = this.serializeGenericResource(resolved, ctx);
1887
- if (this.isPromiseLike(result)) return result;
1952
+ return this.serializeGenericResourceAsync(resolved, ctx);
1888
1953
  });
1889
1954
  else {
1890
1955
  const result = this.serializeGenericResource(resource, ctx);
@@ -2215,6 +2280,38 @@ var ResourceCollection = class ResourceCollection extends BaseSerializer {
2215
2280
  }, rootKey);
2216
2281
  this.body = this.applySerializePlugins(this.body);
2217
2282
  }
2283
+ async serializeCollectionDataAsync(items) {
2284
+ let data = await normalizeSerializableDataAsync(items);
2285
+ data = sanitizeConditionalAttributes(data);
2286
+ const paginationExtras = !Array.isArray(this.resource) ? buildPaginationExtras(this.resource) : {};
2287
+ const { metaKey } = getPaginationExtraKeys();
2288
+ const configuredMeta = metaKey ? paginationExtras[metaKey] : void 0;
2289
+ if (metaKey) delete paginationExtras[metaKey];
2290
+ const caseStyle = this.resolveSerializerCaseStyle(this.constructor, this.resolveCollectsConfig());
2291
+ if (caseStyle) {
2292
+ const transformer = getCaseTransformer(caseStyle);
2293
+ data = transformKeys(data, transformer);
2294
+ }
2295
+ const customMeta = this.resolveMergedMeta(ResourceCollection.prototype.with);
2296
+ const { wrap, rootKey, factory } = this.resolveResponseStructure();
2297
+ this.body = buildResponseEnvelope({
2298
+ payload: data,
2299
+ meta: configuredMeta,
2300
+ metaKey,
2301
+ wrap,
2302
+ rootKey,
2303
+ factory,
2304
+ context: {
2305
+ type: "collection",
2306
+ resource: this.resource
2307
+ }
2308
+ });
2309
+ this.body = appendRootProperties(this.body, {
2310
+ ...paginationExtras,
2311
+ ...customMeta || {}
2312
+ }, rootKey);
2313
+ this.body = this.applySerializePlugins(this.body);
2314
+ }
2218
2315
  resolveCollectionDataForSerialization(items, ctx) {
2219
2316
  if (this.collects && this.data === ResourceCollection.prototype.data) {
2220
2317
  const collected = items.map((item) => new this.collects(item).data(ctx));
@@ -2236,11 +2333,16 @@ var ResourceCollection = class ResourceCollection extends BaseSerializer {
2236
2333
  const serialize = (items) => {
2237
2334
  const resolvedData = this.resolveCollectionDataForSerialization(items, ctx);
2238
2335
  if (this.isPromiseLike(resolvedData)) return resolvedData.then((resolved) => {
2239
- this.serializeCollectionData(resolved);
2336
+ return this.serializeCollectionDataAsync(resolved);
2240
2337
  });
2241
2338
  this.serializeCollectionData(resolvedData);
2242
2339
  };
2243
- if (this.isPromiseLike(data)) this.serializationPromise = Promise.resolve(data).then(serialize);
2340
+ if (this.isPromiseLike(data)) this.serializationPromise = Promise.resolve(data).then((items) => {
2341
+ const resolvedData = this.resolveCollectionDataForSerialization(items, ctx);
2342
+ return Promise.resolve(resolvedData).then((resolved) => {
2343
+ return this.serializeCollectionDataAsync(resolved);
2344
+ });
2345
+ });
2244
2346
  else {
2245
2347
  const result = serialize(data);
2246
2348
  if (this.isPromiseLike(result)) this.serializationPromise = Promise.resolve(result);
@@ -2539,6 +2641,30 @@ var Resource = class Resource extends BaseSerializer {
2539
2641
  this.body = appendRootProperties(this.body, customMeta, rootKey);
2540
2642
  this.body = this.applySerializePlugins(this.body);
2541
2643
  }
2644
+ async serializeResourceAsync(resource) {
2645
+ let data = await normalizeSerializableDataAsync(resource);
2646
+ if (!Array.isArray(data) && data && typeof data.data !== "undefined") data = data.data;
2647
+ data = sanitizeConditionalAttributes(data);
2648
+ const caseStyle = this.resolveSerializerCaseStyle(this.constructor);
2649
+ if (caseStyle) {
2650
+ const transformer = getCaseTransformer(caseStyle);
2651
+ data = transformKeys(data, transformer);
2652
+ }
2653
+ const customMeta = this.resolveMergedMeta(Resource.prototype.with);
2654
+ const { wrap, rootKey, factory } = this.resolveResponseStructure();
2655
+ this.body = buildResponseEnvelope({
2656
+ payload: data,
2657
+ wrap,
2658
+ rootKey,
2659
+ factory,
2660
+ context: {
2661
+ type: "resource",
2662
+ resource: this.resource
2663
+ }
2664
+ });
2665
+ this.body = appendRootProperties(this.body, customMeta, rootKey);
2666
+ this.body = this.applySerializePlugins(this.body);
2667
+ }
2542
2668
  /**
2543
2669
  * Convert resource to JSON response format
2544
2670
  *
@@ -2550,7 +2676,7 @@ var Resource = class Resource extends BaseSerializer {
2550
2676
  const ctx = this.resolveSerializationContext();
2551
2677
  const resource = this.data(ctx);
2552
2678
  if (this.isPromiseLike(resource)) this.serializationPromise = Promise.resolve(resource).then((resolved) => {
2553
- this.serializeResource(resolved);
2679
+ return this.serializeResourceAsync(resolved);
2554
2680
  });
2555
2681
  else this.serializeResource(resource);
2556
2682
  }
@@ -2722,4 +2848,4 @@ var Resource = class Resource extends BaseSerializer {
2722
2848
  };
2723
2849
 
2724
2850
  //#endregion
2725
- 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 };
2851
+ 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, normalizeSerializableDataAsync, 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.3.2",
3
+ "version": "1.3.4",
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",