@stone-js/resources 0.8.10 → 0.8.11

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
  *
@@ -62,7 +63,7 @@ export interface IResource<Model = unknown, Output = ResourceOutput> {
62
63
  * Asynchronous, and resolved from the container, so it may reach any service: fetch a relation,
63
64
  * translate a label, compute a total. Whatever it returns is what the schema then validates.
64
65
  */
65
- data?: (model: Model, context: ResourceContext) => unknown | Promise<unknown>;
66
+ data?: (model: Model, context: ResourceContext) => Promiseable<unknown>;
66
67
  /** Project one model. */
67
68
  item: (model: Model, context?: ResourceContext) => Promise<Output>;
68
69
  /** 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,13 @@
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
10
  export * from './middleware/BlueprintMiddleware.js';
12
11
  export * from './middleware/ResourceRouteMiddleware.js';
13
12
  export * from './options/ResourcesBlueprint.js';
13
+ 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
  *
@@ -690,7 +652,9 @@ class ResourceRouteMiddleware {
690
652
  return entry;
691
653
  }
692
654
  const ResourceClass = entry;
693
- return this.container?.resolve?.(ResourceClass, true) ?? new ResourceClass({});
655
+ // `resolve(Class, true)` uses the binding `@ApiResource` declared, and binds it as a singleton
656
+ // when there is none, so a resource is built once with its dependencies wired either way.
657
+ return this.container?.resolve?.(ResourceClass, true) ?? new ResourceClass();
694
658
  }
695
659
  }
696
660
  /**
@@ -755,9 +719,24 @@ const MetaApiResourceMiddleware = {
755
719
  * export const Application = defineStoneApp({ name: 'my-app' }, [resourcesBlueprint])
756
720
  * ```
757
721
  */
722
+ /**
723
+ * The reader every resource holds its contract against, as a service.
724
+ *
725
+ * Bound so a resource's constructor can simply ask for it. A dependency read off the container that
726
+ * nothing ever bound is not optional, it is a crash, which is what made every container-resolved
727
+ * resource fail on a service nobody was told to register. The fix is the registration, not a
728
+ * conditional read.
729
+ */
730
+ const MetaContractChecker = {
731
+ module: ContractChecker,
732
+ isClass: true,
733
+ singleton: true,
734
+ alias: 'contractChecker'
735
+ };
758
736
  const resourcesBlueprint = {
759
737
  stone: {
760
738
  resources: {},
739
+ services: [MetaContractChecker],
761
740
  blueprint: {
762
741
  middleware: [
763
742
  MetaApiResourceMiddleware
@@ -771,6 +750,49 @@ const resourcesBlueprint = {
771
750
  }
772
751
  };
773
752
 
753
+ /**
754
+ * Declare a class as an API resource.
755
+ *
756
+ * Three statements in one, which is why nothing has to be wired by hand:
757
+ *
758
+ * 1. **It is a service.** The container builds it, as a singleton, which means its constructor is
759
+ * auto-wired like any other class: whatever it destructures is resolved for it, from the checker
760
+ * it holds its contract against to the repository its `data()` needs to complete a model. Nothing
761
+ * reads dependencies conditionally, because the container has them.
762
+ * 2. **It is reachable by name.** The alias is bound in the container as `resource:<name>`, prefixed
763
+ * on purpose: an application is free to bind its own `user` service, and a resource named `user`
764
+ * must not compete for that name.
765
+ * 3. **It activates the module.** The blueprint comes with the decorator, so a resource declared this
766
+ * way is registered and projected without a second gesture, and `resource: 'user'` on a route
767
+ * resolves to this class.
768
+ *
769
+ * @param alias - The name a route refers to it by. Defaults to the class name.
770
+ * @returns A class decorator.
771
+ *
772
+ * @example
773
+ * ```ts
774
+ * @ApiResource('user')
775
+ * export class UserResource extends Resource<User> {
776
+ * constructor (private readonly posts: PostRepository) { super() }
777
+ * schema (): unknown { return z.object({ id: z.number(), name: z.string() }) }
778
+ * }
779
+ * ```
780
+ */
781
+ const ApiResource = (alias) => {
782
+ return classDecoratorLegacyWrapper((target, context) => {
783
+ const name = alias ?? target.name;
784
+ setMetadata(context, API_RESOURCE_KEY, { alias: name });
785
+ setMetadata(context, SERVICE_KEY, { singleton: true, isClass: true, alias: `resource:${name}` });
786
+ addBlueprint(target, context, resourcesBlueprint, {
787
+ stone: {
788
+ resources: {
789
+ registry: { [name]: target }
790
+ }
791
+ }
792
+ });
793
+ });
794
+ };
795
+
774
796
  /**
775
797
  * Class decorator: shape what routes return, declaratively.
776
798
  *
@@ -827,4 +849,4 @@ const Returns = (resource) => {
827
849
  });
828
850
  };
829
851
 
830
- export { API_RESOURCE_KEY, ApiResource, ApiResourceMiddleware, ContractChecker, MetaApiResourceMiddleware, MetaResourceRouteMiddleware, RETURNS_KEY, Resource, ResourceContractError, ResourceRouteMiddleware, Resources, Returns, applyFields, contextFromEvent, defineResource, except, only, resourcesBlueprint, stripUndefined };
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 };
@@ -1,5 +1,5 @@
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
  */
@@ -62,4 +62,13 @@ export interface ResourcesBlueprint extends StoneBlueprint {
62
62
  * export const Application = defineStoneApp({ name: 'my-app' }, [resourcesBlueprint])
63
63
  * ```
64
64
  */
65
+ /**
66
+ * The reader every resource holds its contract against, as a service.
67
+ *
68
+ * Bound so a resource's constructor can simply ask for it. A dependency read off the container that
69
+ * nothing ever bound is not optional, it is a crash, which is what made every container-resolved
70
+ * resource fail on a service nobody was told to register. The fix is the registration, not a
71
+ * conditional read.
72
+ */
73
+ export declare const MetaContractChecker: MetaService;
65
74
  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.11",
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.11"
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.11"
74
75
  },
75
76
  "peerDependencies": {
76
- "@stone-js/core": "0.8.10"
77
+ "@stone-js/core": "0.8.11"
77
78
  },
78
79
  "scripts": {
79
80
  "lint": "ts-standard src",