@stone-js/resources 0.8.10 → 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.
@@ -1,3 +1,4 @@
1
+ import { Promiseable } from '@stone-js/core';
1
2
  import { IContractChecker } from './ContractChecker.js';
2
3
  import { ContractViolationPolicy, IResource, ResourceContext, ResourceEnvelope, ResourceOutput, ResourceSchema } from './declarations.js';
3
4
  /**
@@ -33,18 +34,34 @@ import { ContractViolationPolicy, IResource, ResourceContext, ResourceEnvelope,
33
34
  * }
34
35
  * ```
35
36
  */
37
+ /**
38
+ * What a resource may be handed when the container builds it.
39
+ *
40
+ * Only names this module binds itself, because destructuring reads every one of them: a name nothing
41
+ * bound would throw before the resource ever exists. Configuration is not in here on purpose, it
42
+ * comes from the blueprint, which is where configuration lives.
43
+ */
44
+ export interface ResourceDependencies {
45
+ /** The reader every projection is held against, bound as `contractChecker`. */
46
+ contractChecker?: IContractChecker;
47
+ }
36
48
  export declare abstract class Resource<Model = unknown, Output extends ResourceOutput = ResourceOutput> implements IResource<Model, Output> {
37
- private readonly checker;
38
- private readonly onViolation;
49
+ protected checker: IContractChecker;
50
+ protected onViolation: ContractViolationPolicy;
39
51
  /**
40
- * @param dependencies - Auto-wired services. Nothing is required: this module reads schemas with its
41
- * own checker, so exposing data never depends on a validation module being
42
- * enabled. Pass `checker` to substitute a dialect of your own.
52
+ * @param dependencies - Auto-wired services.
53
+ *
54
+ * One name, bound by this module's own blueprint, so the container resolves it like any other service
55
+ * and this constructor reads it plainly. That is the whole point: a dependency read off a container
56
+ * that never bound it is not optional, it throws, which is what made every container-resolved
57
+ * resource fail. The answer was to register the checker, not to test for its presence.
58
+ *
59
+ * One name and no more, because destructuring reads each one: a resource must not have to know
60
+ * which services happen to be bound. The violation policy is configuration, and it travels with the
61
+ * request in the context, from `stone.resources.onViolation`. Substituting the dialect is a matter
62
+ * of binding `contractChecker` yourself.
43
63
  */
44
- constructor(dependencies?: {
45
- checker?: IContractChecker;
46
- onViolation?: ContractViolationPolicy;
47
- });
64
+ constructor({ contractChecker }?: ResourceDependencies);
48
65
  /**
49
66
  * The contract: what this resource exposes.
50
67
  *
@@ -93,7 +110,7 @@ export declare abstract class Resource<Model = unknown, Output extends ResourceO
93
110
  * which would shadow the very method a subclass wrote — the override would exist on the prototype
94
111
  * and never be reached. This states the type and emits nothing.
95
112
  */
96
- data?: (model: Model, context: ResourceContext) => unknown | Promise<unknown>;
113
+ data?: (model: Model, context: ResourceContext) => Promiseable<unknown>;
97
114
  /**
98
115
  * Named subsets a caller may ask for. Override to expose fragments.
99
116
  */
@@ -1,3 +1,4 @@
1
+ import { Promiseable } from '@stone-js/core';
1
2
  /**
2
3
  * A schema, in whatever shape the application already writes them.
3
4
  *
@@ -14,17 +15,28 @@ export type ResourceSchema = unknown;
14
15
  * carried. The authenticated principal is part of it, because deciding what a caller may see is the
15
16
  * most common reason a projection differs between two callers.
16
17
  */
17
- export interface ResourceContext {
18
- /** 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. */
19
20
  fields?: string[];
20
21
  /** Requested relations to embed. */
21
22
  include?: string[];
22
23
  /** The requested fragment, when the caller asked for one by name. */
23
24
  fragment?: string;
24
- /** The authenticated principal, when the application has one. */
25
- principal?: unknown;
26
- /** The event being answered, for a resource that needs more than the parameters above. */
27
- 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;
28
40
  /** Anything else a resource needs. */
29
41
  [key: string]: unknown;
30
42
  }
@@ -62,7 +74,7 @@ export interface IResource<Model = unknown, Output = ResourceOutput> {
62
74
  * Asynchronous, and resolved from the container, so it may reach any service: fetch a relation,
63
75
  * translate a label, compute a total. Whatever it returns is what the schema then validates.
64
76
  */
65
- data?: (model: Model, context: ResourceContext) => unknown | Promise<unknown>;
77
+ data?: (model: Model, context: ResourceContext) => Promiseable<unknown>;
66
78
  /** Project one model. */
67
79
  item: (model: Model, context?: ResourceContext) => Promise<Output>;
68
80
  /** Project a collection. */
@@ -1,21 +1,30 @@
1
+ import { ClassType } from '@stone-js/core';
1
2
  /**
2
- * Class decorator: register a resource class under a name.
3
+ * Declare a class as an API resource.
3
4
  *
5
+ * Three statements in one, which is why nothing has to be wired by hand:
6
+ *
7
+ * 1. **It is a service.** The container builds it, as a singleton, which means its constructor is
8
+ * auto-wired like any other class: whatever it destructures is resolved for it, from the checker
9
+ * it holds its contract against to the repository its `data()` needs to complete a model. Nothing
10
+ * reads dependencies conditionally, because the container has them.
11
+ * 2. **It is reachable by name.** The alias is bound in the container as `resource:<name>`, prefixed
12
+ * on purpose: an application is free to bind its own `user` service, and a resource named `user`
13
+ * must not compete for that name.
14
+ * 3. **It activates the module.** The blueprint comes with the decorator, so a resource declared this
15
+ * way is registered and projected without a second gesture, and `resource: 'user'` on a route
16
+ * resolves to this class.
17
+ *
18
+ * @param alias - The name a route refers to it by. Defaults to the class name.
19
+ * @returns A class decorator.
20
+ *
21
+ * @example
4
22
  * ```ts
5
23
  * @ApiResource('user')
6
24
  * export class UserResource extends Resource<User> {
7
- * toArray (user: User) { return { id: user.id, name: user.name } }
25
+ * constructor (private readonly posts: PostRepository) { super() }
26
+ * schema (): unknown { return z.object({ id: z.number(), name: z.string() }) }
8
27
  * }
9
28
  * ```
10
- *
11
- * Routes and handlers then refer to it by name (`@Returns('user')`, or `{ resource: 'user' }`), so
12
- * resources live in their own files, organised however the application likes, and nothing has to be
13
- * imported at the route. The class is resolved by the container, so its constructor receives services
14
- * and `toArray` can use them: a resource that formats dates for the caller's locale needs i18n, and
15
- * this is how it gets it.
16
- *
17
- * @param alias - The name the resource is registered under. Defaults to the class name, which the
18
- * discovery middleware fills in, since it is the one holding the class.
19
- * @returns A class decorator.
20
29
  */
21
- export declare const ApiResource: (alias?: string) => ClassDecorator;
30
+ export declare const ApiResource: <T extends ClassType = ClassType>(alias?: string) => ClassDecorator;
@@ -1,3 +1,4 @@
1
+ import { Promiseable } from '@stone-js/core';
1
2
  import { Resource } from './Resource.js';
2
3
  import { IContractChecker } from './ContractChecker.js';
3
4
  import { ContractViolationPolicy, ResourceContext, ResourceOutput, ResourceSchema } from './declarations.js';
@@ -12,7 +13,7 @@ export interface ResourceDefinition<Model = unknown> {
12
13
  /** Named subsets a caller may ask for, each with its own schema. */
13
14
  fragments?: Record<string, ResourceSchema> | ((context: ResourceContext) => Record<string, ResourceSchema> | Promise<Record<string, ResourceSchema>>);
14
15
  /** Optional hook to shape or complete the model before it meets the schema. */
15
- data?: (model: Model, context: ResourceContext) => unknown | Promise<unknown>;
16
+ data?: (model: Model, context: ResourceContext) => Promiseable<unknown>;
16
17
  }
17
18
  /**
18
19
  * The imperative way to define a resource: an object instead of a class.
package/dist/index.d.ts CHANGED
@@ -1,13 +1,12 @@
1
1
  export * from './ContractChecker.js';
2
- export * from './Resource.js';
3
2
  export * from './declarations.js';
4
3
  export * from './decorators/ApiResource.js';
4
+ export * from './decorators/constants.js';
5
5
  export * from './decorators/Resources.js';
6
6
  export * from './decorators/Returns.js';
7
- export * from './decorators/constants.js';
8
7
  export * from './defineResource.js';
9
8
  export * from './errors/ResourceContractError.js';
10
9
  export * from './helpers.js';
11
- export * from './middleware/BlueprintMiddleware.js';
12
10
  export * from './middleware/ResourceRouteMiddleware.js';
13
11
  export * from './options/ResourcesBlueprint.js';
12
+ export * from './Resource.js';
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { RuntimeError, setClassMetadata, hasMetadata, getMetadata, classDecoratorLegacyWrapper, addBlueprint, methodDecoratorLegacyWrapper, addMetadata } from '@stone-js/core';
1
+ import { RuntimeError, hasMetadata, getMetadata, classDecoratorLegacyWrapper, setMetadata, SERVICE_KEY, addBlueprint, methodDecoratorLegacyWrapper, addMetadata } from '@stone-js/core';
2
2
  import { cloneValue } from '@stone-js/config';
3
3
 
4
4
  /**
@@ -273,50 +273,25 @@ class ResourceContractError extends RuntimeError {
273
273
  }
274
274
  }
275
275
 
276
- /**
277
- * Base API resource — the layer responsible for exposing data.
278
- *
279
- * A resource answers two questions, and the second is what makes it worth having. *What leaves?* —
280
- * and *what did you promise leaves?* The promise is a schema, so the same declaration validates the
281
- * response, documents it in the published contract, and lets a caller ask for a named subset of it.
282
- *
283
- * Projection is the schema's own work: what the schema does not describe is not exposed, so a field
284
- * added to a model later — a password hash, an internal flag — cannot leak by being forgotten.
285
- *
286
- * @example
287
- * ```ts
288
- * @ApiResource('user')
289
- * export class UserResource extends Resource<User> {
290
- * constructor ({ posts }: { posts: PostService }) {
291
- * super()
292
- * this.posts = posts
293
- * }
294
- *
295
- * schema () {
296
- * return z.object({ id: z.number(), name: z.string(), posts: z.array(z.string()).optional() })
297
- * }
298
- *
299
- * fragments () {
300
- * return { summary: z.object({ id: z.number(), name: z.string() }) }
301
- * }
302
- *
303
- * async data (user: User) {
304
- * return { ...user, posts: await this.posts.titlesOf(user.id) }
305
- * }
306
- * }
307
- * ```
308
- */
309
276
  class Resource {
310
277
  checker;
311
278
  onViolation;
312
279
  /**
313
- * @param dependencies - Auto-wired services. Nothing is required: this module reads schemas with its
314
- * own checker, so exposing data never depends on a validation module being
315
- * enabled. Pass `checker` to substitute a dialect of your own.
280
+ * @param dependencies - Auto-wired services.
281
+ *
282
+ * One name, bound by this module's own blueprint, so the container resolves it like any other service
283
+ * and this constructor reads it plainly. That is the whole point: a dependency read off a container
284
+ * that never bound it is not optional, it throws, which is what made every container-resolved
285
+ * resource fail. The answer was to register the checker, not to test for its presence.
286
+ *
287
+ * One name and no more, because destructuring reads each one: a resource must not have to know
288
+ * which services happen to be bound. The violation policy is configuration, and it travels with the
289
+ * request in the context, from `stone.resources.onViolation`. Substituting the dialect is a matter
290
+ * of binding `contractChecker` yourself.
316
291
  */
317
- constructor(dependencies = {}) {
318
- this.checker = dependencies.checker ?? ContractChecker.create();
319
- this.onViolation = dependencies.onViolation ?? 'throw';
292
+ constructor({ contractChecker } = {}) {
293
+ this.checker = contractChecker ?? ContractChecker.create();
294
+ this.onViolation = 'throw';
320
295
  }
321
296
  /**
322
297
  * Project one model.
@@ -472,7 +447,18 @@ class Resource {
472
447
  * ```
473
448
  */
474
449
  function defineResource(definition, dependencies = {}) {
450
+ // Assigned after construction, not through it: the class constructor is the container's, and it
451
+ // only reads names this module binds. An explicit object is the imperative form's business.
475
452
  const resource = new class extends Resource {
453
+ constructor() {
454
+ super();
455
+ if (dependencies.checker !== undefined) {
456
+ this.checker = dependencies.checker;
457
+ }
458
+ if (dependencies.onViolation !== undefined) {
459
+ this.onViolation = dependencies.onViolation;
460
+ }
461
+ }
476
462
  async schema(context) {
477
463
  if (typeof definition.schema !== 'function') {
478
464
  return definition.schema;
@@ -480,7 +466,7 @@ function defineResource(definition, dependencies = {}) {
480
466
  const build = definition.schema;
481
467
  return await build(context);
482
468
  }
483
- }(dependencies);
469
+ }();
484
470
  if (definition.fragments !== undefined) {
485
471
  const declared = definition.fragments;
486
472
  resource.fragments = async (context) => {
@@ -508,30 +494,6 @@ const RETURNS_KEY = '@stone-js/resources/returns';
508
494
  */
509
495
  const API_RESOURCE_KEY = '@stone-js/resources/resource';
510
496
 
511
- /**
512
- * Class decorator: register a resource class under a name.
513
- *
514
- * ```ts
515
- * @ApiResource('user')
516
- * export class UserResource extends Resource<User> {
517
- * toArray (user: User) { return { id: user.id, name: user.name } }
518
- * }
519
- * ```
520
- *
521
- * Routes and handlers then refer to it by name (`@Returns('user')`, or `{ resource: 'user' }`), so
522
- * resources live in their own files, organised however the application likes, and nothing has to be
523
- * imported at the route. The class is resolved by the container, so its constructor receives services
524
- * and `toArray` can use them: a resource that formats dates for the caller's locale needs i18n, and
525
- * this is how it gets it.
526
- *
527
- * @param alias - The name the resource is registered under. Defaults to the class name, which the
528
- * discovery middleware fills in, since it is the one holding the class.
529
- * @returns A class decorator.
530
- */
531
- const ApiResource = (alias) => {
532
- return setClassMetadata(API_RESOURCE_KEY, { alias });
533
- };
534
-
535
497
  /**
536
498
  * Route middleware: shapes what a route returns, after its handler ran.
537
499
  *
@@ -579,11 +541,48 @@ class ResourceRouteMiddleware {
579
541
  onViolation: this.blueprint.get('stone.resources', {}).onViolation
580
542
  });
581
543
  if (this.isContentBearing(result)) {
582
- const shaped = await this.shape(resource, result.content, context);
544
+ const shaped = await this.shapePayload(resource, result.content, context);
583
545
  result.setContent(shaped);
584
546
  return result;
585
547
  }
586
- 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);
587
586
  }
588
587
  /**
589
588
  * Project a value, whether it is one model or many.
@@ -690,7 +689,9 @@ class ResourceRouteMiddleware {
690
689
  return entry;
691
690
  }
692
691
  const ResourceClass = entry;
693
- return this.container?.resolve?.(ResourceClass, true) ?? new ResourceClass({});
692
+ // `resolve(Class, true)` uses the binding `@ApiResource` declared, and binds it as a singleton
693
+ // when there is none, so a resource is built once with its dependencies wired either way.
694
+ return this.container?.resolve?.(ResourceClass, true) ?? new ResourceClass();
694
695
  }
695
696
  }
696
697
  /**
@@ -705,42 +706,6 @@ const MetaResourceRouteMiddleware = {
705
706
  priority: 4
706
707
  };
707
708
 
708
- /**
709
- * Build-phase middleware: collect every class registered with `@ApiResource` into the registry.
710
- *
711
- * The same scan the router does for its route definitions, applied to this module's own key. After it
712
- * runs, `stone.resources.registry` maps each alias to its class, so a route or a handler can name a
713
- * resource instead of importing it, and `@stone-js/openapi` can walk the registry to publish response
714
- * shapes without loading anything itself.
715
- *
716
- * @param context - The blueprint context.
717
- * @param next - The next blueprint middleware.
718
- * @returns The blueprint.
719
- */
720
- async function ApiResourceMiddleware(context, next) {
721
- const registered = context
722
- .modules
723
- .filter((module) => hasMetadata(module, API_RESOURCE_KEY))
724
- .reduce((registry, module) => {
725
- const { alias } = getMetadata(module, API_RESOURCE_KEY, {});
726
- return { ...registry, [alias ?? module.name]: module };
727
- }, {});
728
- if (Object.keys(registered).length > 0) {
729
- context.blueprint.set('stone.resources.registry', {
730
- ...context.blueprint.get('stone.resources.registry', {}),
731
- ...registered
732
- });
733
- }
734
- return await next(context);
735
- }
736
- /**
737
- * Meta blueprint middleware for resource discovery.
738
- */
739
- const MetaApiResourceMiddleware = {
740
- module: ApiResourceMiddleware,
741
- priority: 5
742
- };
743
-
744
709
  /**
745
710
  * Opt-in blueprint: register it to shape what routes return.
746
711
  *
@@ -755,14 +720,24 @@ const MetaApiResourceMiddleware = {
755
720
  * export const Application = defineStoneApp({ name: 'my-app' }, [resourcesBlueprint])
756
721
  * ```
757
722
  */
723
+ /**
724
+ * The reader every resource holds its contract against, as a service.
725
+ *
726
+ * Bound so a resource's constructor can simply ask for it. A dependency read off the container that
727
+ * nothing ever bound is not optional, it is a crash, which is what made every container-resolved
728
+ * resource fail on a service nobody was told to register. The fix is the registration, not a
729
+ * conditional read.
730
+ */
731
+ const MetaContractChecker = {
732
+ module: ContractChecker,
733
+ isClass: true,
734
+ singleton: true,
735
+ alias: 'contractChecker'
736
+ };
758
737
  const resourcesBlueprint = {
759
738
  stone: {
760
739
  resources: {},
761
- blueprint: {
762
- middleware: [
763
- MetaApiResourceMiddleware
764
- ]
765
- },
740
+ services: [MetaContractChecker],
766
741
  router: {
767
742
  middleware: [
768
743
  MetaResourceRouteMiddleware
@@ -771,6 +746,49 @@ const resourcesBlueprint = {
771
746
  }
772
747
  };
773
748
 
749
+ /**
750
+ * Declare a class as an API resource.
751
+ *
752
+ * Three statements in one, which is why nothing has to be wired by hand:
753
+ *
754
+ * 1. **It is a service.** The container builds it, as a singleton, which means its constructor is
755
+ * auto-wired like any other class: whatever it destructures is resolved for it, from the checker
756
+ * it holds its contract against to the repository its `data()` needs to complete a model. Nothing
757
+ * reads dependencies conditionally, because the container has them.
758
+ * 2. **It is reachable by name.** The alias is bound in the container as `resource:<name>`, prefixed
759
+ * on purpose: an application is free to bind its own `user` service, and a resource named `user`
760
+ * must not compete for that name.
761
+ * 3. **It activates the module.** The blueprint comes with the decorator, so a resource declared this
762
+ * way is registered and projected without a second gesture, and `resource: 'user'` on a route
763
+ * resolves to this class.
764
+ *
765
+ * @param alias - The name a route refers to it by. Defaults to the class name.
766
+ * @returns A class decorator.
767
+ *
768
+ * @example
769
+ * ```ts
770
+ * @ApiResource('user')
771
+ * export class UserResource extends Resource<User> {
772
+ * constructor (private readonly posts: PostRepository) { super() }
773
+ * schema (): unknown { return z.object({ id: z.number(), name: z.string() }) }
774
+ * }
775
+ * ```
776
+ */
777
+ const ApiResource = (alias) => {
778
+ return classDecoratorLegacyWrapper((target, context) => {
779
+ const name = alias ?? target.name;
780
+ setMetadata(context, API_RESOURCE_KEY, { alias: name });
781
+ setMetadata(context, SERVICE_KEY, { singleton: true, isClass: true, alias: `resource:${name}` });
782
+ addBlueprint(target, context, resourcesBlueprint, {
783
+ stone: {
784
+ resources: {
785
+ registry: { [name]: target }
786
+ }
787
+ }
788
+ });
789
+ });
790
+ };
791
+
774
792
  /**
775
793
  * Class decorator: shape what routes return, declaratively.
776
794
  *
@@ -827,4 +845,4 @@ const Returns = (resource) => {
827
845
  });
828
846
  };
829
847
 
830
- export { API_RESOURCE_KEY, ApiResource, ApiResourceMiddleware, ContractChecker, MetaApiResourceMiddleware, 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
  *
@@ -1,8 +1,15 @@
1
1
  import { IResource } from '../declarations.js';
2
- import { AppConfig, StoneBlueprint } from '@stone-js/core';
2
+ 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.
@@ -62,4 +85,13 @@ export interface ResourcesBlueprint extends StoneBlueprint {
62
85
  * export const Application = defineStoneApp({ name: 'my-app' }, [resourcesBlueprint])
63
86
  * ```
64
87
  */
88
+ /**
89
+ * The reader every resource holds its contract against, as a service.
90
+ *
91
+ * Bound so a resource's constructor can simply ask for it. A dependency read off the container that
92
+ * nothing ever bound is not optional, it is a crash, which is what made every container-resolved
93
+ * resource fail on a service nobody was told to register. The fix is the registration, not a
94
+ * conditional read.
95
+ */
96
+ export declare const MetaContractChecker: MetaService;
65
97
  export declare const resourcesBlueprint: ResourcesBlueprint;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stone-js/resources",
3
- "version": "0.8.10",
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",
@@ -57,7 +57,8 @@
57
57
  "typedoc-plugin-markdown": "^4.7.0",
58
58
  "typescript": "^5.6.3",
59
59
  "vitest": "^3.2.4",
60
- "zod": "^3.25.76"
60
+ "zod": "^3.25.76",
61
+ "@stone-js/service-container": "0.8.12"
61
62
  },
62
63
  "ts-standard": {
63
64
  "globals": [
@@ -70,10 +71,10 @@
70
71
  ]
71
72
  },
72
73
  "dependencies": {
73
- "@stone-js/config": "0.8.10"
74
+ "@stone-js/config": "0.8.12"
74
75
  },
75
76
  "peerDependencies": {
76
- "@stone-js/core": "0.8.10"
77
+ "@stone-js/core": "0.8.12"
77
78
  },
78
79
  "scripts": {
79
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>;