@stone-js/resources 0.8.7 → 0.8.9
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 +1 -1
- package/dist/decorators/ApiResource.d.ts +21 -0
- package/dist/decorators/Resources.d.ts +27 -0
- package/dist/decorators/Returns.d.ts +37 -0
- package/dist/decorators/constants.d.ts +11 -0
- package/dist/defineResource.d.ts +2 -2
- package/dist/helpers.d.ts +1 -1
- package/dist/index.d.ts +11 -4
- package/dist/index.js +296 -1
- package/dist/middleware/BlueprintMiddleware.d.ts +18 -0
- package/dist/middleware/ResourceRouteMiddleware.d.ts +85 -0
- package/dist/options/ResourcesBlueprint.d.ts +46 -0
- package/package.json +7 -1
package/dist/Resource.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { IResource, ResourceContext, ResourceEnvelope, ResourceOutput } from './declarations';
|
|
1
|
+
import { IResource, ResourceContext, ResourceEnvelope, ResourceOutput } from './declarations.js';
|
|
2
2
|
/**
|
|
3
3
|
* Base API resource — the declarative way to shape what your domain exposes.
|
|
4
4
|
*
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Class decorator: register a resource class under a name.
|
|
3
|
+
*
|
|
4
|
+
* ```ts
|
|
5
|
+
* @ApiResource('user')
|
|
6
|
+
* export class UserResource extends Resource<User> {
|
|
7
|
+
* toArray (user: User) { return { id: user.id, name: user.name } }
|
|
8
|
+
* }
|
|
9
|
+
* ```
|
|
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
|
+
*/
|
|
21
|
+
export declare const ApiResource: (alias?: string) => ClassDecorator;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { ResourcesConfig } from '../options/ResourcesBlueprint.js';
|
|
2
|
+
import { ClassType } from '@stone-js/core';
|
|
3
|
+
/**
|
|
4
|
+
* Options for the `@Resources` decorator: the `stone.resources` bucket, every key optional.
|
|
5
|
+
*/
|
|
6
|
+
export interface ResourcesDecoratorOptions extends ResourcesConfig {
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Class decorator: shape what routes return, declaratively.
|
|
10
|
+
*
|
|
11
|
+
* `@Resources()` installs the route middleware that applies whatever a route declared under
|
|
12
|
+
* `resource`, so a handler returns its domain model and only what the resource allows leaves the
|
|
13
|
+
* application.
|
|
14
|
+
*
|
|
15
|
+
* @param options - The resources configuration. Everything is optional.
|
|
16
|
+
* @returns A class decorator.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* import { Resources } from '@stone-js/resources'
|
|
21
|
+
*
|
|
22
|
+
* @Resources({ registry: { user: userResource } })
|
|
23
|
+
* @StoneApp({ name: 'my-app' })
|
|
24
|
+
* export class Application {}
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export declare const Resources: <T extends ClassType = ClassType>(options?: ResourcesDecoratorOptions) => ClassDecorator;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { IResource } from '../declarations.js';
|
|
2
|
+
/**
|
|
3
|
+
* What a handler declares it exposes: a resource, or the alias of a resource class registered with
|
|
4
|
+
* `@ApiResource`.
|
|
5
|
+
*/
|
|
6
|
+
export type ReturnsInput = IResource<any, any> | string;
|
|
7
|
+
/**
|
|
8
|
+
* What `@Returns` records for one handler method.
|
|
9
|
+
*/
|
|
10
|
+
export interface ReturnsMetadata {
|
|
11
|
+
/** The decorated method's name, so the declaration can be found again at request time. */
|
|
12
|
+
action: string | symbol;
|
|
13
|
+
/** What that method exposes. */
|
|
14
|
+
resource: ReturnsInput;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Method decorator: declare what a handler exposes.
|
|
18
|
+
*
|
|
19
|
+
* ```ts
|
|
20
|
+
* @Returns(userResource) // the resource itself
|
|
21
|
+
* @Returns('user') // a registered resource class
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* The counterpart of `@Validate`: one says what comes in, the other what goes out, and between them
|
|
25
|
+
* the handler is free to return its domain model whole. Whatever the model gains later, a password
|
|
26
|
+
* hash, an internal flag, does not leak, because the resource decides what leaves.
|
|
27
|
+
*
|
|
28
|
+
* Like `@Validate`, this knows nothing about the router. The declaration is recorded on the handler
|
|
29
|
+
* under this module's own key, so it works in a routed application, a single-handler service, a CLI
|
|
30
|
+
* command or the browser. When a router is in play you may put it on the route instead
|
|
31
|
+
* (`@Get('/users/:id', { resource: userResource })`), which keeps everything a route does in one
|
|
32
|
+
* place; both forms end up in the same middleware.
|
|
33
|
+
*
|
|
34
|
+
* @param resource - What the handler exposes.
|
|
35
|
+
* @returns A method decorator.
|
|
36
|
+
*/
|
|
37
|
+
export declare const Returns: <T extends Function = Function>(resource: ReturnsInput) => MethodDecorator;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metadata key carrying what a handler method declared with `@Returns`.
|
|
3
|
+
*
|
|
4
|
+
* The module owns its key, which is what makes it independent: a resource shapes the output whether
|
|
5
|
+
* or not a router is in play, because the declaration lives on the handler, not on a route.
|
|
6
|
+
*/
|
|
7
|
+
export declare const RETURNS_KEY = "@stone-js/resources/returns";
|
|
8
|
+
/**
|
|
9
|
+
* Metadata key carrying the alias a resource class registered itself under.
|
|
10
|
+
*/
|
|
11
|
+
export declare const API_RESOURCE_KEY = "@stone-js/resources/resource";
|
package/dist/defineResource.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Resource } from './Resource';
|
|
2
|
-
import { ResourceContext, ResourceOutput } from './declarations';
|
|
1
|
+
import { Resource } from './Resource.js';
|
|
2
|
+
import { ResourceContext, ResourceOutput } from './declarations.js';
|
|
3
3
|
/**
|
|
4
4
|
* The imperative/functional way to define a resource — a plain transform function instead of a
|
|
5
5
|
* class. Returns a full {@link Resource} (so you still get `item`/`collection`/`response` and
|
package/dist/helpers.d.ts
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
export * from './Resource';
|
|
2
|
-
export * from './declarations';
|
|
3
|
-
export * from './
|
|
4
|
-
export * from './
|
|
1
|
+
export * from './Resource.js';
|
|
2
|
+
export * from './declarations.js';
|
|
3
|
+
export * from './decorators/ApiResource.js';
|
|
4
|
+
export * from './decorators/Resources.js';
|
|
5
|
+
export * from './decorators/Returns.js';
|
|
6
|
+
export * from './decorators/constants.js';
|
|
7
|
+
export * from './defineResource.js';
|
|
8
|
+
export * from './helpers.js';
|
|
9
|
+
export * from './middleware/BlueprintMiddleware.js';
|
|
10
|
+
export * from './middleware/ResourceRouteMiddleware.js';
|
|
11
|
+
export * from './options/ResourcesBlueprint.js';
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { hasMetadata, getMetadata, setClassMetadata, classDecoratorLegacyWrapper, addBlueprint, methodDecoratorLegacyWrapper, addMetadata } from '@stone-js/core';
|
|
2
|
+
import { cloneValue } from '@stone-js/config';
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Returns a copy of `object` without any `undefined` values (so conditional fields simply vanish).
|
|
3
6
|
*
|
|
@@ -188,4 +191,296 @@ function defineResource(transform) {
|
|
|
188
191
|
}();
|
|
189
192
|
}
|
|
190
193
|
|
|
191
|
-
|
|
194
|
+
/**
|
|
195
|
+
* Metadata key carrying what a handler method declared with `@Returns`.
|
|
196
|
+
*
|
|
197
|
+
* The module owns its key, which is what makes it independent: a resource shapes the output whether
|
|
198
|
+
* or not a router is in play, because the declaration lives on the handler, not on a route.
|
|
199
|
+
*/
|
|
200
|
+
const RETURNS_KEY = '@stone-js/resources/returns';
|
|
201
|
+
/**
|
|
202
|
+
* Metadata key carrying the alias a resource class registered itself under.
|
|
203
|
+
*/
|
|
204
|
+
const API_RESOURCE_KEY = '@stone-js/resources/resource';
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Build-phase middleware: collect every class registered with `@ApiResource` into the registry.
|
|
208
|
+
*
|
|
209
|
+
* The same scan the router does for its route definitions, applied to this module's own key. After it
|
|
210
|
+
* runs, `stone.resources.registry` maps each alias to its class, so a route or a handler can name a
|
|
211
|
+
* resource instead of importing it, and `@stone-js/openapi` can walk the registry to publish response
|
|
212
|
+
* shapes without loading anything itself.
|
|
213
|
+
*
|
|
214
|
+
* @param context - The blueprint context.
|
|
215
|
+
* @param next - The next blueprint middleware.
|
|
216
|
+
* @returns The blueprint.
|
|
217
|
+
*/
|
|
218
|
+
async function ApiResourceMiddleware(context, next) {
|
|
219
|
+
const registered = context
|
|
220
|
+
.modules
|
|
221
|
+
.filter((module) => hasMetadata(module, API_RESOURCE_KEY))
|
|
222
|
+
.reduce((registry, module) => {
|
|
223
|
+
const { alias } = getMetadata(module, API_RESOURCE_KEY, {});
|
|
224
|
+
return { ...registry, [alias ?? module.name]: module };
|
|
225
|
+
}, {});
|
|
226
|
+
if (Object.keys(registered).length > 0) {
|
|
227
|
+
context.blueprint.set('stone.resources.registry', {
|
|
228
|
+
...context.blueprint.get('stone.resources.registry', {}),
|
|
229
|
+
...registered
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return await next(context);
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Meta blueprint middleware for resource discovery.
|
|
236
|
+
*/
|
|
237
|
+
const MetaApiResourceMiddleware = {
|
|
238
|
+
module: ApiResourceMiddleware,
|
|
239
|
+
priority: 5
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Route middleware: shapes what a route returns, after its handler ran.
|
|
244
|
+
*
|
|
245
|
+
* A route says what it exposes, once, where the route is defined:
|
|
246
|
+
*
|
|
247
|
+
* ```ts
|
|
248
|
+
* @Get('/users/:id', { resource: userResource })
|
|
249
|
+
* ```
|
|
250
|
+
*
|
|
251
|
+
* The handler then returns its domain model, whole, and this middleware applies the resource on the
|
|
252
|
+
* way out. That is the point: a service should not have to know which fields are public, and a
|
|
253
|
+
* handler should not have to remember to strip them. Whatever the model gains later, a password
|
|
254
|
+
* hash, an internal flag, is not exposed by accident, because the resource decides what leaves.
|
|
255
|
+
*
|
|
256
|
+
* It runs on the raw value the handler returned, before any response wrapping, so it knows nothing
|
|
257
|
+
* of HTTP and works in every context. Sparse fieldsets are read from the event, so `?fields=id,name`
|
|
258
|
+
* narrows the output without the route changing.
|
|
259
|
+
*/
|
|
260
|
+
class ResourceRouteMiddleware {
|
|
261
|
+
blueprint;
|
|
262
|
+
container;
|
|
263
|
+
/**
|
|
264
|
+
* @param dependencies - Auto-wired container services.
|
|
265
|
+
*/
|
|
266
|
+
constructor({ blueprint, container }) {
|
|
267
|
+
this.blueprint = blueprint;
|
|
268
|
+
this.container = container;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Run the handler, then shape what it returned.
|
|
272
|
+
*
|
|
273
|
+
* @param event - The incoming event.
|
|
274
|
+
* @param next - The next middleware.
|
|
275
|
+
* @returns The shaped output, or the untouched result when the route declares no resource.
|
|
276
|
+
*/
|
|
277
|
+
async handle(event, next) {
|
|
278
|
+
const resource = this.resourceFor(event);
|
|
279
|
+
const result = await next(event);
|
|
280
|
+
if (resource === undefined || result === undefined || result === null) {
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
const context = contextFromEvent(event);
|
|
284
|
+
return (Array.isArray(result) ? resource.collection(result, context) : resource.item(result, context));
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* The resource the matched route declared, with a registered name resolved to its resource.
|
|
288
|
+
*
|
|
289
|
+
* @param event - The incoming event.
|
|
290
|
+
* @returns The resource, or `undefined` when the route declares none.
|
|
291
|
+
*/
|
|
292
|
+
resourceFor(event) {
|
|
293
|
+
const declared = this.declarationFor(event);
|
|
294
|
+
if (declared === undefined) {
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
if (typeof declared !== 'string') {
|
|
298
|
+
return this.resolve(declared);
|
|
299
|
+
}
|
|
300
|
+
const registry = this.blueprint.get('stone.resources', {}).registry ?? {};
|
|
301
|
+
const resource = registry[declared];
|
|
302
|
+
if (resource === undefined) {
|
|
303
|
+
throw new TypeError(`The route declares \`resource: '${declared}'\`, but no resource is registered under that ` +
|
|
304
|
+
'name. Register it with `blueprint.set(\'stone.resources.registry\', { ' + declared + ': … })`, ' +
|
|
305
|
+
'or declare the resource inline on the route.');
|
|
306
|
+
}
|
|
307
|
+
return this.resolve(resource);
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* What the handler about to run declared, from either of the two places it may live.
|
|
311
|
+
*
|
|
312
|
+
* The route's own option comes first, because when a router is in play a route is the single
|
|
313
|
+
* description of itself. Failing that, the handler's own `@Returns` metadata is read: that form owns
|
|
314
|
+
* its key and needs no router, so the same module shapes the output of a routed request, a
|
|
315
|
+
* single-handler service, a CLI command or a browser event.
|
|
316
|
+
*
|
|
317
|
+
* @param event - The incoming event.
|
|
318
|
+
* @returns What was declared, or `undefined`.
|
|
319
|
+
*/
|
|
320
|
+
declarationFor(event) {
|
|
321
|
+
// Duck-typed throughout: the kernel is agnostic, and an event without a router carries no route.
|
|
322
|
+
const route = event.getRoute?.();
|
|
323
|
+
const onRoute = route?.getOption?.('resource');
|
|
324
|
+
if (onRoute !== undefined) {
|
|
325
|
+
return onRoute;
|
|
326
|
+
}
|
|
327
|
+
const handler = route?.getOption?.('handler') ??
|
|
328
|
+
this.blueprint.get('stone.kernel.eventHandler', {});
|
|
329
|
+
return this.declaredOnHandler(handler);
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* What a handler declared with `@Returns`, if anything.
|
|
333
|
+
*
|
|
334
|
+
* @param handler - The handler about to run.
|
|
335
|
+
* @returns What the matching method declared, or `undefined`.
|
|
336
|
+
*/
|
|
337
|
+
declaredOnHandler(handler) {
|
|
338
|
+
const module = handler?.module;
|
|
339
|
+
if (module === undefined || !hasMetadata(module, RETURNS_KEY)) {
|
|
340
|
+
return undefined;
|
|
341
|
+
}
|
|
342
|
+
const declarations = getMetadata(module, RETURNS_KEY, []);
|
|
343
|
+
const action = handler?.action;
|
|
344
|
+
// A single-handler module declares one; a controller declares one per method.
|
|
345
|
+
return (action === undefined
|
|
346
|
+
? declarations[0]
|
|
347
|
+
: declarations.find((declaration) => declaration.action === action))?.resource;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Resolve a registered entry: a resource class goes through the container, so its constructor gets
|
|
351
|
+
* the services it asked for and `toArray` can use them, i18n included.
|
|
352
|
+
*
|
|
353
|
+
* @param entry - A resource, or a class to resolve into one.
|
|
354
|
+
* @returns The resource.
|
|
355
|
+
*/
|
|
356
|
+
resolve(entry) {
|
|
357
|
+
if (typeof entry !== 'function') {
|
|
358
|
+
return entry;
|
|
359
|
+
}
|
|
360
|
+
const ResourceClass = entry;
|
|
361
|
+
return this.container?.resolve?.(ResourceClass, true) ?? new ResourceClass({});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Meta middleware for route-declared resources.
|
|
366
|
+
*
|
|
367
|
+
* Registered on `stone.router.middleware` by `resourcesBlueprint`. Its priority puts it outside
|
|
368
|
+
* validation, so a request is shaped on the way out after having been validated on the way in.
|
|
369
|
+
*/
|
|
370
|
+
const MetaResourceRouteMiddleware = {
|
|
371
|
+
module: ResourceRouteMiddleware,
|
|
372
|
+
isClass: true,
|
|
373
|
+
priority: 4
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Class decorator: register a resource class under a name.
|
|
378
|
+
*
|
|
379
|
+
* ```ts
|
|
380
|
+
* @ApiResource('user')
|
|
381
|
+
* export class UserResource extends Resource<User> {
|
|
382
|
+
* toArray (user: User) { return { id: user.id, name: user.name } }
|
|
383
|
+
* }
|
|
384
|
+
* ```
|
|
385
|
+
*
|
|
386
|
+
* Routes and handlers then refer to it by name (`@Returns('user')`, or `{ resource: 'user' }`), so
|
|
387
|
+
* resources live in their own files, organised however the application likes, and nothing has to be
|
|
388
|
+
* imported at the route. The class is resolved by the container, so its constructor receives services
|
|
389
|
+
* and `toArray` can use them: a resource that formats dates for the caller's locale needs i18n, and
|
|
390
|
+
* this is how it gets it.
|
|
391
|
+
*
|
|
392
|
+
* @param alias - The name the resource is registered under. Defaults to the class name, which the
|
|
393
|
+
* discovery middleware fills in, since it is the one holding the class.
|
|
394
|
+
* @returns A class decorator.
|
|
395
|
+
*/
|
|
396
|
+
const ApiResource = (alias) => {
|
|
397
|
+
return setClassMetadata(API_RESOURCE_KEY, { alias });
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Opt-in blueprint: register it to shape what routes return.
|
|
402
|
+
*
|
|
403
|
+
* It contributes the route middleware that applies whatever a route declared under `resource`.
|
|
404
|
+
* `stone.router.middleware` is an array, so this merges with the rest of the app. The middleware is
|
|
405
|
+
* a no-op on routes that declare nothing.
|
|
406
|
+
*
|
|
407
|
+
* @example
|
|
408
|
+
* ```typescript
|
|
409
|
+
* import { resourcesBlueprint } from '@stone-js/resources'
|
|
410
|
+
*
|
|
411
|
+
* export const Application = defineStoneApp({ name: 'my-app' }, [resourcesBlueprint])
|
|
412
|
+
* ```
|
|
413
|
+
*/
|
|
414
|
+
const resourcesBlueprint = {
|
|
415
|
+
stone: {
|
|
416
|
+
resources: {},
|
|
417
|
+
blueprint: {
|
|
418
|
+
middleware: [
|
|
419
|
+
MetaApiResourceMiddleware
|
|
420
|
+
]
|
|
421
|
+
},
|
|
422
|
+
router: {
|
|
423
|
+
middleware: [
|
|
424
|
+
MetaResourceRouteMiddleware
|
|
425
|
+
]
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Class decorator: shape what routes return, declaratively.
|
|
432
|
+
*
|
|
433
|
+
* `@Resources()` installs the route middleware that applies whatever a route declared under
|
|
434
|
+
* `resource`, so a handler returns its domain model and only what the resource allows leaves the
|
|
435
|
+
* application.
|
|
436
|
+
*
|
|
437
|
+
* @param options - The resources configuration. Everything is optional.
|
|
438
|
+
* @returns A class decorator.
|
|
439
|
+
*
|
|
440
|
+
* @example
|
|
441
|
+
* ```typescript
|
|
442
|
+
* import { Resources } from '@stone-js/resources'
|
|
443
|
+
*
|
|
444
|
+
* @Resources({ registry: { user: userResource } })
|
|
445
|
+
* @StoneApp({ name: 'my-app' })
|
|
446
|
+
* export class Application {}
|
|
447
|
+
* ```
|
|
448
|
+
*/
|
|
449
|
+
const Resources = (options = {}) => {
|
|
450
|
+
return classDecoratorLegacyWrapper((target, context) => {
|
|
451
|
+
// The blueprint is the single source of truth for what the module declares; the decorator only
|
|
452
|
+
// overrides what it can, its options bucket.
|
|
453
|
+
const blueprint = cloneValue(resourcesBlueprint);
|
|
454
|
+
blueprint.stone.resources = { ...blueprint.stone.resources, ...options };
|
|
455
|
+
addBlueprint(target, context, blueprint);
|
|
456
|
+
});
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Method decorator: declare what a handler exposes.
|
|
461
|
+
*
|
|
462
|
+
* ```ts
|
|
463
|
+
* @Returns(userResource) // the resource itself
|
|
464
|
+
* @Returns('user') // a registered resource class
|
|
465
|
+
* ```
|
|
466
|
+
*
|
|
467
|
+
* The counterpart of `@Validate`: one says what comes in, the other what goes out, and between them
|
|
468
|
+
* the handler is free to return its domain model whole. Whatever the model gains later, a password
|
|
469
|
+
* hash, an internal flag, does not leak, because the resource decides what leaves.
|
|
470
|
+
*
|
|
471
|
+
* Like `@Validate`, this knows nothing about the router. The declaration is recorded on the handler
|
|
472
|
+
* under this module's own key, so it works in a routed application, a single-handler service, a CLI
|
|
473
|
+
* command or the browser. When a router is in play you may put it on the route instead
|
|
474
|
+
* (`@Get('/users/:id', { resource: userResource })`), which keeps everything a route does in one
|
|
475
|
+
* place; both forms end up in the same middleware.
|
|
476
|
+
*
|
|
477
|
+
* @param resource - What the handler exposes.
|
|
478
|
+
* @returns A method decorator.
|
|
479
|
+
*/
|
|
480
|
+
const Returns = (resource) => {
|
|
481
|
+
return methodDecoratorLegacyWrapper((_target, context) => {
|
|
482
|
+
addMetadata(context, RETURNS_KEY, { action: context.name, resource });
|
|
483
|
+
});
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
export { API_RESOURCE_KEY, ApiResource, ApiResourceMiddleware, MetaApiResourceMiddleware, MetaResourceRouteMiddleware, RETURNS_KEY, Resource, ResourceRouteMiddleware, Resources, Returns, applyFields, contextFromEvent, defineResource, except, only, resourcesBlueprint, stripUndefined };
|
|
@@ -0,0 +1,18 @@
|
|
|
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>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { IResource } from '../declarations.js';
|
|
2
|
+
import { IBlueprint, IContainer, IncomingEvent, NextMiddleware, OutgoingResponse, type MetaMiddleware } from '@stone-js/core';
|
|
3
|
+
/**
|
|
4
|
+
* The shape a route's `resource` option may take: the resource itself, or the name of one
|
|
5
|
+
* registered under `stone.resources.registry`.
|
|
6
|
+
*/
|
|
7
|
+
export type RouteResource = IResource<any, any> | string;
|
|
8
|
+
/**
|
|
9
|
+
* Route middleware: shapes what a route returns, after its handler ran.
|
|
10
|
+
*
|
|
11
|
+
* A route says what it exposes, once, where the route is defined:
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* @Get('/users/:id', { resource: userResource })
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* The handler then returns its domain model, whole, and this middleware applies the resource on the
|
|
18
|
+
* way out. That is the point: a service should not have to know which fields are public, and a
|
|
19
|
+
* handler should not have to remember to strip them. Whatever the model gains later, a password
|
|
20
|
+
* hash, an internal flag, is not exposed by accident, because the resource decides what leaves.
|
|
21
|
+
*
|
|
22
|
+
* It runs on the raw value the handler returned, before any response wrapping, so it knows nothing
|
|
23
|
+
* of HTTP and works in every context. Sparse fieldsets are read from the event, so `?fields=id,name`
|
|
24
|
+
* narrows the output without the route changing.
|
|
25
|
+
*/
|
|
26
|
+
export declare class ResourceRouteMiddleware {
|
|
27
|
+
private readonly blueprint;
|
|
28
|
+
private readonly container?;
|
|
29
|
+
/**
|
|
30
|
+
* @param dependencies - Auto-wired container services.
|
|
31
|
+
*/
|
|
32
|
+
constructor({ blueprint, container }: {
|
|
33
|
+
blueprint: IBlueprint;
|
|
34
|
+
container?: IContainer;
|
|
35
|
+
});
|
|
36
|
+
/**
|
|
37
|
+
* Run the handler, then shape what it returned.
|
|
38
|
+
*
|
|
39
|
+
* @param event - The incoming event.
|
|
40
|
+
* @param next - The next middleware.
|
|
41
|
+
* @returns The shaped output, or the untouched result when the route declares no resource.
|
|
42
|
+
*/
|
|
43
|
+
handle(event: IncomingEvent, next: NextMiddleware<IncomingEvent, OutgoingResponse>): Promise<OutgoingResponse>;
|
|
44
|
+
/**
|
|
45
|
+
* The resource the matched route declared, with a registered name resolved to its resource.
|
|
46
|
+
*
|
|
47
|
+
* @param event - The incoming event.
|
|
48
|
+
* @returns The resource, or `undefined` when the route declares none.
|
|
49
|
+
*/
|
|
50
|
+
private resourceFor;
|
|
51
|
+
/**
|
|
52
|
+
* What the handler about to run declared, from either of the two places it may live.
|
|
53
|
+
*
|
|
54
|
+
* The route's own option comes first, because when a router is in play a route is the single
|
|
55
|
+
* description of itself. Failing that, the handler's own `@Returns` metadata is read: that form owns
|
|
56
|
+
* its key and needs no router, so the same module shapes the output of a routed request, a
|
|
57
|
+
* single-handler service, a CLI command or a browser event.
|
|
58
|
+
*
|
|
59
|
+
* @param event - The incoming event.
|
|
60
|
+
* @returns What was declared, or `undefined`.
|
|
61
|
+
*/
|
|
62
|
+
private declarationFor;
|
|
63
|
+
/**
|
|
64
|
+
* What a handler declared with `@Returns`, if anything.
|
|
65
|
+
*
|
|
66
|
+
* @param handler - The handler about to run.
|
|
67
|
+
* @returns What the matching method declared, or `undefined`.
|
|
68
|
+
*/
|
|
69
|
+
private declaredOnHandler;
|
|
70
|
+
/**
|
|
71
|
+
* Resolve a registered entry: a resource class goes through the container, so its constructor gets
|
|
72
|
+
* the services it asked for and `toArray` can use them, i18n included.
|
|
73
|
+
*
|
|
74
|
+
* @param entry - A resource, or a class to resolve into one.
|
|
75
|
+
* @returns The resource.
|
|
76
|
+
*/
|
|
77
|
+
private resolve;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Meta middleware for route-declared resources.
|
|
81
|
+
*
|
|
82
|
+
* Registered on `stone.router.middleware` by `resourcesBlueprint`. Its priority puts it outside
|
|
83
|
+
* validation, so a request is shaped on the way out after having been validated on the way in.
|
|
84
|
+
*/
|
|
85
|
+
export declare const MetaResourceRouteMiddleware: MetaMiddleware<any, any>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { IResource } from '../declarations.js';
|
|
2
|
+
import { AppConfig, StoneBlueprint } from '@stone-js/core';
|
|
3
|
+
/**
|
|
4
|
+
* Resources configuration bucket (`stone.resources`).
|
|
5
|
+
*/
|
|
6
|
+
export interface ResourcesConfig {
|
|
7
|
+
/**
|
|
8
|
+
* Named resources a route can refer to by name, instead of importing them at the route.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* blueprint.set('stone.resources.registry', { user: userResource })
|
|
12
|
+
* // then, on the route: { resource: 'user' }
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* Naming a resource that is not registered fails loudly at request time rather than returning the
|
|
16
|
+
* model unshaped, because an unshaped model is exactly what a resource exists to prevent.
|
|
17
|
+
*/
|
|
18
|
+
registry?: Record<string, IResource<any, any>>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Application config augmented with the resources bucket.
|
|
22
|
+
*/
|
|
23
|
+
export interface ResourcesAppConfig extends Partial<AppConfig> {
|
|
24
|
+
resources: ResourcesConfig;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Blueprint for the resources module.
|
|
28
|
+
*/
|
|
29
|
+
export interface ResourcesBlueprint extends StoneBlueprint {
|
|
30
|
+
stone: ResourcesAppConfig;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Opt-in blueprint: register it to shape what routes return.
|
|
34
|
+
*
|
|
35
|
+
* It contributes the route middleware that applies whatever a route declared under `resource`.
|
|
36
|
+
* `stone.router.middleware` is an array, so this merges with the rest of the app. The middleware is
|
|
37
|
+
* a no-op on routes that declare nothing.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```typescript
|
|
41
|
+
* import { resourcesBlueprint } from '@stone-js/resources'
|
|
42
|
+
*
|
|
43
|
+
* export const Application = defineStoneApp({ name: 'my-app' }, [resourcesBlueprint])
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
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.9",
|
|
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",
|
|
@@ -68,6 +68,12 @@
|
|
|
68
68
|
"beforeEach"
|
|
69
69
|
]
|
|
70
70
|
},
|
|
71
|
+
"dependencies": {
|
|
72
|
+
"@stone-js/config": "0.8.9"
|
|
73
|
+
},
|
|
74
|
+
"peerDependencies": {
|
|
75
|
+
"@stone-js/core": "0.8.9"
|
|
76
|
+
},
|
|
71
77
|
"scripts": {
|
|
72
78
|
"lint": "ts-standard src",
|
|
73
79
|
"lint:fix": "ts-standard --fix src tests",
|