@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.
- package/dist/Resource.d.ts +27 -10
- package/dist/declarations.d.ts +19 -7
- package/dist/decorators/ApiResource.d.ts +22 -13
- package/dist/defineResource.d.ts +2 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.js +128 -110
- package/dist/middleware/ResourceRouteMiddleware.d.ts +24 -0
- package/dist/options/ResourcesBlueprint.d.ts +33 -1
- package/package.json +5 -4
- package/dist/middleware/BlueprintMiddleware.d.ts +0 -18
package/dist/Resource.d.ts
CHANGED
|
@@ -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
|
-
|
|
38
|
-
|
|
49
|
+
protected checker: IContractChecker;
|
|
50
|
+
protected onViolation: ContractViolationPolicy;
|
|
39
51
|
/**
|
|
40
|
-
* @param dependencies - Auto-wired services.
|
|
41
|
-
*
|
|
42
|
-
*
|
|
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(
|
|
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) =>
|
|
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
|
*/
|
package/dist/declarations.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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) =>
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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;
|
package/dist/defineResource.d.ts
CHANGED
|
@@ -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) =>
|
|
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,
|
|
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.
|
|
314
|
-
*
|
|
315
|
-
*
|
|
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(
|
|
318
|
-
this.checker =
|
|
319
|
-
this.onViolation =
|
|
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
|
-
}(
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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.
|
|
74
|
+
"@stone-js/config": "0.8.12"
|
|
74
75
|
},
|
|
75
76
|
"peerDependencies": {
|
|
76
|
-
"@stone-js/core": "0.8.
|
|
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>;
|