@stone-js/resources 0.8.11 → 0.8.12

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.
@@ -15,17 +15,28 @@ export type ResourceSchema = unknown;
15
15
  * carried. The authenticated principal is part of it, because deciding what a caller may see is the
16
16
  * most common reason a projection differs between two callers.
17
17
  */
18
- export interface ResourceContext {
19
- /** Requested sparse fieldset narrows the output to these top-level keys. */
18
+ export interface ResourceContext<EventType = unknown, PrincipalType = unknown> {
19
+ /** Requested sparse fieldset: narrows the output to these top-level keys. */
20
20
  fields?: string[];
21
21
  /** Requested relations to embed. */
22
22
  include?: string[];
23
23
  /** The requested fragment, when the caller asked for one by name. */
24
24
  fragment?: string;
25
- /** The authenticated principal, when the application has one. */
26
- principal?: unknown;
27
- /** The event being answered, for a resource that needs more than the parameters above. */
28
- event?: unknown;
25
+ /**
26
+ * The authenticated principal, when the application has one.
27
+ *
28
+ * Typed by the resource, because deciding what a caller may see is the most common reason two
29
+ * callers get different shapes, and `unknown` makes every such decision a cast. The default stays
30
+ * `unknown`: this module never assumes an application has users, let alone what a user is.
31
+ */
32
+ principal?: PrincipalType;
33
+ /**
34
+ * The event being answered, for a resource that needs more than the parameters above.
35
+ *
36
+ * Typed by the resource for the same reason, and `unknown` by default because the module is
37
+ * agnostic of the platform the event came from.
38
+ */
39
+ event?: EventType;
29
40
  /** Anything else a resource needs. */
30
41
  [key: string]: unknown;
31
42
  }
package/dist/index.d.ts CHANGED
@@ -7,7 +7,6 @@ export * from './decorators/Returns.js';
7
7
  export * from './defineResource.js';
8
8
  export * from './errors/ResourceContractError.js';
9
9
  export * from './helpers.js';
10
- export * from './middleware/BlueprintMiddleware.js';
11
10
  export * from './middleware/ResourceRouteMiddleware.js';
12
11
  export * from './options/ResourcesBlueprint.js';
13
12
  export * from './Resource.js';
package/dist/index.js CHANGED
@@ -541,11 +541,48 @@ class ResourceRouteMiddleware {
541
541
  onViolation: this.blueprint.get('stone.resources', {}).onViolation
542
542
  });
543
543
  if (this.isContentBearing(result)) {
544
- const shaped = await this.shape(resource, result.content, context);
544
+ const shaped = await this.shapePayload(resource, result.content, context);
545
545
  result.setContent(shaped);
546
546
  return result;
547
547
  }
548
- return await this.shape(resource, result, context);
548
+ return await this.shapePayload(resource, result, context);
549
+ }
550
+ /**
551
+ * Shape a value, or shape what is inside the envelope the application declared.
552
+ *
553
+ * A page is `{ items: [...], meta: { total } }`, and `items` and `meta` are not fields of a model:
554
+ * projecting that object would publish the envelope as if it were the thing. An application names its
555
+ * own wrapper once, in `stone.resources.envelope`, and everything around the payload is left as it
556
+ * was, counts and cursors included.
557
+ *
558
+ * Nothing is assumed when nothing is declared, because guessing which key holds the payload would
559
+ * quietly mangle a model that happens to have one by that name.
560
+ *
561
+ * @param resource - The resource to apply.
562
+ * @param value - The value the handler produced.
563
+ * @param context - The resource context.
564
+ * @returns The projected value, wrapper intact.
565
+ */
566
+ async shapePayload(resource, value, context) {
567
+ const key = this.envelopeKeyOf(value);
568
+ if (key === undefined) {
569
+ return await this.shape(resource, value, context);
570
+ }
571
+ const envelope = value;
572
+ return { ...envelope, [key]: await this.shape(resource, envelope[key], context) };
573
+ }
574
+ /**
575
+ * Which declared envelope key this value carries, if any.
576
+ *
577
+ * @param value - The value the handler produced.
578
+ * @returns The key holding the payload, or nothing when this is not an envelope.
579
+ */
580
+ envelopeKeyOf(value) {
581
+ const declared = this.blueprint.get('stone.resources', {}).envelope;
582
+ if (declared === undefined || typeof value !== 'object' || value === null || Array.isArray(value)) {
583
+ return undefined;
584
+ }
585
+ return [declared.payload].flat().find((candidate) => candidate in value);
549
586
  }
550
587
  /**
551
588
  * Project a value, whether it is one model or many.
@@ -669,42 +706,6 @@ const MetaResourceRouteMiddleware = {
669
706
  priority: 4
670
707
  };
671
708
 
672
- /**
673
- * Build-phase middleware: collect every class registered with `@ApiResource` into the registry.
674
- *
675
- * The same scan the router does for its route definitions, applied to this module's own key. After it
676
- * runs, `stone.resources.registry` maps each alias to its class, so a route or a handler can name a
677
- * resource instead of importing it, and `@stone-js/openapi` can walk the registry to publish response
678
- * shapes without loading anything itself.
679
- *
680
- * @param context - The blueprint context.
681
- * @param next - The next blueprint middleware.
682
- * @returns The blueprint.
683
- */
684
- async function ApiResourceMiddleware(context, next) {
685
- const registered = context
686
- .modules
687
- .filter((module) => hasMetadata(module, API_RESOURCE_KEY))
688
- .reduce((registry, module) => {
689
- const { alias } = getMetadata(module, API_RESOURCE_KEY, {});
690
- return { ...registry, [alias ?? module.name]: module };
691
- }, {});
692
- if (Object.keys(registered).length > 0) {
693
- context.blueprint.set('stone.resources.registry', {
694
- ...context.blueprint.get('stone.resources.registry', {}),
695
- ...registered
696
- });
697
- }
698
- return await next(context);
699
- }
700
- /**
701
- * Meta blueprint middleware for resource discovery.
702
- */
703
- const MetaApiResourceMiddleware = {
704
- module: ApiResourceMiddleware,
705
- priority: 5
706
- };
707
-
708
709
  /**
709
710
  * Opt-in blueprint: register it to shape what routes return.
710
711
  *
@@ -737,11 +738,6 @@ const resourcesBlueprint = {
737
738
  stone: {
738
739
  resources: {},
739
740
  services: [MetaContractChecker],
740
- blueprint: {
741
- middleware: [
742
- MetaApiResourceMiddleware
743
- ]
744
- },
745
741
  router: {
746
742
  middleware: [
747
743
  MetaResourceRouteMiddleware
@@ -849,4 +845,4 @@ const Returns = (resource) => {
849
845
  });
850
846
  };
851
847
 
852
- export { API_RESOURCE_KEY, ApiResource, ApiResourceMiddleware, ContractChecker, MetaApiResourceMiddleware, MetaContractChecker, MetaResourceRouteMiddleware, RETURNS_KEY, Resource, ResourceContractError, ResourceRouteMiddleware, Resources, Returns, applyFields, contextFromEvent, defineResource, except, only, resourcesBlueprint, stripUndefined };
848
+ export { API_RESOURCE_KEY, ApiResource, ContractChecker, MetaContractChecker, MetaResourceRouteMiddleware, RETURNS_KEY, Resource, ResourceContractError, ResourceRouteMiddleware, Resources, Returns, applyFields, contextFromEvent, defineResource, except, only, resourcesBlueprint, stripUndefined };
@@ -43,6 +43,30 @@ export declare class ResourceRouteMiddleware {
43
43
  * @returns The shaped output, or the untouched result when the route declares no resource.
44
44
  */
45
45
  handle(event: IncomingEvent, next: NextMiddleware<IncomingEvent, OutgoingResponse>): Promise<OutgoingResponse>;
46
+ /**
47
+ * Shape a value, or shape what is inside the envelope the application declared.
48
+ *
49
+ * A page is `{ items: [...], meta: { total } }`, and `items` and `meta` are not fields of a model:
50
+ * projecting that object would publish the envelope as if it were the thing. An application names its
51
+ * own wrapper once, in `stone.resources.envelope`, and everything around the payload is left as it
52
+ * was, counts and cursors included.
53
+ *
54
+ * Nothing is assumed when nothing is declared, because guessing which key holds the payload would
55
+ * quietly mangle a model that happens to have one by that name.
56
+ *
57
+ * @param resource - The resource to apply.
58
+ * @param value - The value the handler produced.
59
+ * @param context - The resource context.
60
+ * @returns The projected value, wrapper intact.
61
+ */
62
+ private shapePayload;
63
+ /**
64
+ * Which declared envelope key this value carries, if any.
65
+ *
66
+ * @param value - The value the handler produced.
67
+ * @returns The key holding the payload, or nothing when this is not an envelope.
68
+ */
69
+ private envelopeKeyOf;
46
70
  /**
47
71
  * Project a value, whether it is one model or many.
48
72
  *
@@ -3,6 +3,13 @@ import { AppConfig, MetaService, StoneBlueprint } from '@stone-js/core';
3
3
  /**
4
4
  * Resources configuration bucket (`stone.resources`).
5
5
  */
6
+ /**
7
+ * The envelope an application wraps its payloads in.
8
+ */
9
+ export interface ResourceEnvelopeConfig {
10
+ /** The key, or keys, that hold the payload to shape. */
11
+ payload: string | string[];
12
+ }
6
13
  export interface ResourcesConfig {
7
14
  /**
8
15
  * The query parameters a caller uses to ask for a shape.
@@ -35,6 +42,22 @@ export interface ResourcesConfig {
35
42
  * model unshaped, because an unshaped model is exactly what a resource exists to prevent.
36
43
  */
37
44
  registry?: Record<string, IResource<any, any>>;
45
+ /**
46
+ * The envelope this application wraps its payloads in, if it wraps them at all.
47
+ *
48
+ * A handler answering a page returns something like `{ items: [...], meta: { total } }`, and `items`
49
+ * and `meta` are not fields of a model: shaping that object would publish the wrapper as if it were
50
+ * the thing. Naming the word once is enough, and everything around the payload is left as it was,
51
+ * counts and cursors included.
52
+ *
53
+ * ```ts
54
+ * blueprint.set('stone.resources.envelope', { payload: 'items' })
55
+ * ```
56
+ *
57
+ * Undeclared by default, because guessing which key holds the payload would quietly mangle a model
58
+ * that happens to have one by that name.
59
+ */
60
+ envelope?: ResourceEnvelopeConfig;
38
61
  }
39
62
  /**
40
63
  * Application config augmented with the resources bucket.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stone-js/resources",
3
- "version": "0.8.11",
3
+ "version": "0.8.12",
4
4
  "description": "Framework-agnostic API resources for Stone.js. Shape what your domain exposes — sparse fieldsets, conditional fields, includes and envelopes — decoupled from controllers, the same on backend and frontend.",
5
5
  "author": "Mr. Stone <evensstone@gmail.com>",
6
6
  "license": "MIT",
@@ -58,7 +58,7 @@
58
58
  "typescript": "^5.6.3",
59
59
  "vitest": "^3.2.4",
60
60
  "zod": "^3.25.76",
61
- "@stone-js/service-container": "0.8.11"
61
+ "@stone-js/service-container": "0.8.12"
62
62
  },
63
63
  "ts-standard": {
64
64
  "globals": [
@@ -71,10 +71,10 @@
71
71
  ]
72
72
  },
73
73
  "dependencies": {
74
- "@stone-js/config": "0.8.11"
74
+ "@stone-js/config": "0.8.12"
75
75
  },
76
76
  "peerDependencies": {
77
- "@stone-js/core": "0.8.11"
77
+ "@stone-js/core": "0.8.12"
78
78
  },
79
79
  "scripts": {
80
80
  "lint": "ts-standard src",
@@ -1,18 +0,0 @@
1
- import { BlueprintContext, ClassType, IBlueprint, NextMiddleware, type MetaMiddleware } from '@stone-js/core';
2
- /**
3
- * Build-phase middleware: collect every class registered with `@ApiResource` into the registry.
4
- *
5
- * The same scan the router does for its route definitions, applied to this module's own key. After it
6
- * runs, `stone.resources.registry` maps each alias to its class, so a route or a handler can name a
7
- * resource instead of importing it, and `@stone-js/openapi` can walk the registry to publish response
8
- * shapes without loading anything itself.
9
- *
10
- * @param context - The blueprint context.
11
- * @param next - The next blueprint middleware.
12
- * @returns The blueprint.
13
- */
14
- export declare function ApiResourceMiddleware(context: BlueprintContext<IBlueprint, ClassType>, next: NextMiddleware<BlueprintContext<IBlueprint, ClassType>, IBlueprint>): Promise<IBlueprint>;
15
- /**
16
- * Meta blueprint middleware for resource discovery.
17
- */
18
- export declare const MetaApiResourceMiddleware: MetaMiddleware<any, any>;