@afora/core 0.0.8 → 0.0.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.
Files changed (226) hide show
  1. package/README.md +149 -23
  2. package/dist/adapters.d.ts +25 -0
  3. package/dist/adapters.js +32 -0
  4. package/dist/core/application/application.d.ts +30 -0
  5. package/dist/core/application/application.js +1291 -0
  6. package/dist/core/application/exceptions.d.ts +17 -0
  7. package/dist/core/application/exceptions.js +85 -0
  8. package/dist/core/application/migrate.d.ts +19 -0
  9. package/dist/core/application/migrate.js +67 -0
  10. package/dist/core/application/providers.js +108 -0
  11. package/dist/core/application/registry.js +23 -0
  12. package/dist/core/application/schema.d.ts +30 -0
  13. package/dist/core/application/schema.js +398 -0
  14. package/dist/core/application/start.js +162 -0
  15. package/dist/core/application/types/application.interface.d.ts +60 -3
  16. package/dist/core/auth/jwt.d.ts +30 -0
  17. package/dist/core/auth/jwt.js +200 -0
  18. package/dist/core/auth/types/jwt.interface.d.ts +74 -0
  19. package/dist/core/blueprint/blueprint-plugin.js +7 -0
  20. package/dist/core/blueprint/blueprint.js +146 -0
  21. package/dist/core/blueprint/callable.js +13 -0
  22. package/dist/core/blueprint/exceptions.d.ts +5 -0
  23. package/dist/core/blueprint/exceptions.js +20 -0
  24. package/dist/core/blueprint/is-blueprint.js +4 -0
  25. package/dist/core/blueprint/list.js +19 -0
  26. package/dist/core/blueprint/optional.d.ts +3 -1
  27. package/dist/core/blueprint/optional.js +69 -0
  28. package/dist/core/blueprint/record-field.js +14 -0
  29. package/dist/core/blueprint/slice.js +127 -0
  30. package/dist/core/blueprint/types/blueprint.interface.d.ts +95 -5
  31. package/dist/core/blueprint/variant.d.ts +32 -0
  32. package/dist/core/blueprint/variant.js +72 -0
  33. package/dist/core/command/command.js +428 -0
  34. package/dist/core/command/exceptions.js +30 -0
  35. package/dist/core/command/types/command.interface.d.ts +10 -1
  36. package/dist/core/entity/abstract-entity.d.ts +24 -0
  37. package/dist/core/entity/abstract-entity.js +181 -0
  38. package/dist/core/entity/entity.d.ts +1 -1
  39. package/dist/core/entity/entity.js +209 -0
  40. package/dist/core/entity/event.d.ts +39 -0
  41. package/dist/core/entity/event.js +80 -0
  42. package/dist/core/entity/exceptions.d.ts +9 -0
  43. package/dist/core/entity/exceptions.js +27 -0
  44. package/dist/core/entity/trust.d.ts +24 -0
  45. package/dist/core/entity/trust.js +7 -0
  46. package/dist/core/entity/types/entity.interface.d.ts +30 -0
  47. package/dist/core/entity/version.js +13 -0
  48. package/dist/core/environment/environment.d.ts +26 -3
  49. package/dist/core/environment/environment.js +71 -0
  50. package/dist/core/event-policy/event-policy.js +76 -0
  51. package/dist/core/event-policy/exceptions.js +12 -0
  52. package/dist/core/event-policy/publish.js +98 -0
  53. package/dist/core/event-policy/publishers.js +18 -0
  54. package/dist/core/event-policy/types/event-policy.interface.d.ts +8 -2
  55. package/dist/core/exception/enums/exception-layers.js +15 -0
  56. package/dist/core/exception/exception.d.ts +15 -0
  57. package/dist/core/exception/exception.js +128 -0
  58. package/dist/core/exception/exported.js +49 -0
  59. package/dist/core/exception/guard.d.ts +20 -1
  60. package/dist/core/exception/guard.js +16 -0
  61. package/dist/core/exception/types/exception.interface.d.ts +13 -0
  62. package/dist/core/json.js +19 -0
  63. package/dist/core/logger/exceptions.js +7 -0
  64. package/dist/core/logger/logger.d.ts +14 -1
  65. package/dist/core/logger/logger.js +178 -0
  66. package/dist/core/openapi/exceptions.js +7 -0
  67. package/dist/core/openapi/openapi.js +132 -0
  68. package/dist/core/openapi/types/openapi.interface.d.ts +7 -1
  69. package/dist/core/output/access.d.ts +16 -0
  70. package/dist/core/output/access.js +66 -0
  71. package/dist/core/output/coerce.js +38 -0
  72. package/dist/core/output/exceptions.d.ts +16 -0
  73. package/dist/core/output/exceptions.js +22 -0
  74. package/dist/core/output/http.d.ts +36 -0
  75. package/dist/core/output/http.js +351 -0
  76. package/dist/core/output/openapi-sink.d.ts +33 -4
  77. package/dist/core/output/openapi-sink.js +571 -0
  78. package/dist/core/output/output.js +192 -0
  79. package/dist/core/output/problem.js +30 -0
  80. package/dist/core/output/select.js +28 -0
  81. package/dist/core/output/stage.d.ts +7 -2
  82. package/dist/core/output/stage.js +111 -0
  83. package/dist/core/output/status.d.ts +1 -1
  84. package/dist/core/output/status.js +52 -0
  85. package/dist/core/output/types/http.interface.d.ts +134 -0
  86. package/dist/core/output/types/output.interface.d.ts +49 -5
  87. package/dist/core/parse/issue.d.ts +28 -3
  88. package/dist/core/parse/issue.js +31 -0
  89. package/dist/core/parse/parse.d.ts +16 -0
  90. package/dist/core/parse/parse.js +323 -0
  91. package/dist/core/parse/strip.js +48 -0
  92. package/dist/core/property/abstract-property.d.ts +9 -0
  93. package/dist/core/property/abstract-property.js +37 -0
  94. package/dist/core/property/define-property.d.ts +34 -6
  95. package/dist/core/property/define-property.js +209 -0
  96. package/dist/core/property/enums/property-states.js +8 -0
  97. package/dist/core/property/exceptions.js +12 -0
  98. package/dist/core/property/input.js +10 -0
  99. package/dist/core/property/is-property.js +3 -0
  100. package/dist/core/property/lineage.js +8 -0
  101. package/dist/core/property/property.d.ts +27 -0
  102. package/dist/core/property/property.js +46 -0
  103. package/dist/core/property/refine.js +103 -0
  104. package/dist/core/property/types/property-options.interface.d.ts +10 -0
  105. package/dist/core/property/types/property-type.interface.d.ts +4 -0
  106. package/dist/core/record/abstract-record.d.ts +8 -0
  107. package/dist/core/record/abstract-record.js +307 -0
  108. package/dist/core/record/exceptions.js +49 -0
  109. package/dist/core/record/marks.js +7 -0
  110. package/dist/core/record/mask.js +60 -0
  111. package/dist/core/record/name.js +23 -0
  112. package/dist/core/record/plugin-catalog.js +115 -0
  113. package/dist/core/record/plugin.js +198 -0
  114. package/dist/core/record/record.js +117 -0
  115. package/dist/core/record/types/record.interface.d.ts +17 -1
  116. package/dist/core/repository/cursor.js +83 -0
  117. package/dist/core/repository/exceptions.d.ts +13 -0
  118. package/dist/core/repository/exceptions.js +72 -0
  119. package/dist/core/repository/in-memory.d.ts +2 -2
  120. package/dist/core/repository/in-memory.js +196 -0
  121. package/dist/core/repository/migrate.d.ts +20 -0
  122. package/dist/core/repository/migrate.js +254 -0
  123. package/dist/core/repository/plan.js +27 -0
  124. package/dist/core/repository/provider.js +29 -0
  125. package/dist/core/repository/query.js +153 -0
  126. package/dist/core/repository/references.js +196 -0
  127. package/dist/core/repository/repository.d.ts +22 -2
  128. package/dist/core/repository/repository.js +720 -0
  129. package/dist/core/repository/state.d.ts +2 -0
  130. package/dist/core/repository/state.js +138 -0
  131. package/dist/core/repository/types/query.interface.d.ts +13 -0
  132. package/dist/core/repository/types/repository.interface.d.ts +50 -5
  133. package/dist/core/repository/types/schema.interface.d.ts +132 -0
  134. package/dist/core/repository/upcast.d.ts +64 -0
  135. package/dist/core/repository/upcast.js +179 -0
  136. package/dist/core/schema/schema.d.ts +37 -0
  137. package/dist/core/schema/schema.js +542 -0
  138. package/dist/core/standard/standard-schema.js +1 -0
  139. package/dist/core/standard/standard.js +23 -0
  140. package/dist/core/symbols/demo.d.ts +4 -0
  141. package/dist/core/symbols/demo.js +1 -0
  142. package/dist/core/symbols/kind.d.ts +1 -1
  143. package/dist/core/symbols/kind.js +1 -0
  144. package/dist/core/symbols/meta.js +1 -0
  145. package/dist/core/symbols/state.d.ts +5 -0
  146. package/dist/core/symbols/state.js +1 -0
  147. package/dist/core/tracing/exceptions.d.ts +4 -0
  148. package/dist/core/tracing/exceptions.js +7 -0
  149. package/dist/core/tracing/tracing.d.ts +72 -0
  150. package/dist/core/tracing/tracing.js +101 -0
  151. package/dist/core/use-case/access.d.ts +12 -0
  152. package/dist/core/use-case/access.js +56 -0
  153. package/dist/core/use-case/decorators.d.ts +40 -0
  154. package/dist/core/use-case/decorators.js +65 -0
  155. package/dist/core/use-case/exceptions.d.ts +5 -1
  156. package/dist/core/use-case/exceptions.js +48 -0
  157. package/dist/core/use-case/plugin-catalog.js +68 -0
  158. package/dist/core/use-case/plugin.js +133 -0
  159. package/dist/core/use-case/route.d.ts +18 -0
  160. package/dist/core/use-case/route.js +200 -0
  161. package/dist/core/use-case/scope.d.ts +5 -0
  162. package/dist/core/use-case/scope.js +4 -0
  163. package/dist/core/use-case/steps/describe.d.ts +5 -1
  164. package/dist/core/use-case/steps/describe.js +24 -0
  165. package/dist/core/use-case/steps/references.js +105 -0
  166. package/dist/core/use-case/steps/run.js +139 -0
  167. package/dist/core/use-case/steps/types/check.interface.d.ts +233 -0
  168. package/dist/core/use-case/steps/types/steps.interface.d.ts +8 -1
  169. package/dist/core/use-case/steps/validate.js +368 -0
  170. package/dist/core/use-case/target.js +100 -0
  171. package/dist/core/use-case/types/access.interface.d.ts +39 -0
  172. package/dist/core/use-case/types/route.interface.d.ts +86 -0
  173. package/dist/core/use-case/use-case.d.ts +127 -2
  174. package/dist/core/use-case/use-case.js +493 -0
  175. package/dist/experimental.d.ts +18 -1
  176. package/dist/experimental.js +18 -123
  177. package/dist/index.d.ts +49 -51
  178. package/dist/index.js +213 -443
  179. package/dist/std/blueprint-plugin.js +42 -0
  180. package/dist/std/blueprint.d.ts +21 -0
  181. package/dist/std/blueprint.js +3 -0
  182. package/dist/std/command.js +3 -0
  183. package/dist/std/entity.d.ts +20 -1
  184. package/dist/std/entity.js +30 -0
  185. package/dist/std/event-policies.js +3 -0
  186. package/dist/std/properties/contact.d.ts +3 -0
  187. package/dist/std/properties/contact.js +29 -0
  188. package/dist/std/properties/enum.js +26 -0
  189. package/dist/std/properties/exceptions.js +14 -0
  190. package/dist/std/properties/headless.js +45 -0
  191. package/dist/std/properties/identity.js +28 -0
  192. package/dist/std/properties/json.js +161 -0
  193. package/dist/std/properties/password.d.ts +0 -2
  194. package/dist/std/properties/password.js +79 -0
  195. package/dist/std/properties/primitives.d.ts +4 -0
  196. package/dist/std/properties/primitives.js +28 -0
  197. package/dist/std/properties/time.js +55 -0
  198. package/dist/std/record-plugins.d.ts +2 -2
  199. package/dist/std/record-plugins.js +73 -0
  200. package/dist/std/record.d.ts +20 -1
  201. package/dist/std/record.js +3 -0
  202. package/dist/std/resource.d.ts +31 -0
  203. package/dist/std/resource.js +39 -0
  204. package/dist/std/runtime.d.ts +6 -0
  205. package/dist/std/runtime.js +193 -0
  206. package/dist/std/stages.js +4 -0
  207. package/dist/std/use-cases/auth.d.ts +5 -1
  208. package/dist/std/use-cases/auth.js +193 -0
  209. package/dist/std/use-cases/crud.js +261 -0
  210. package/dist/std/use-cases/index.d.ts +1 -1
  211. package/dist/std/use-cases/index.js +12 -0
  212. package/dist/std/use-cases/lifecycle.js +97 -0
  213. package/dist/std/use-cases/shared.d.ts +2 -2
  214. package/dist/std/use-cases/shared.js +232 -0
  215. package/dist/std/use-cases/types.d.ts +102 -2
  216. package/dist/std/use-cases/types.js +0 -0
  217. package/dist/testing/http-provider-contract.d.ts +32 -0
  218. package/dist/testing/http-provider-contract.js +344 -0
  219. package/dist/testing/index.d.ts +2 -0
  220. package/dist/testing/index.js +3 -944
  221. package/dist/testing/memory-publisher.js +12 -0
  222. package/dist/testing/provider-contract.d.ts +26 -1
  223. package/dist/testing/provider-contract.js +1071 -0
  224. package/package.json +23 -3
  225. package/dist/index-qxfkyd6k.js +0 -1721
  226. package/dist/index-t5qwtyj5.js +0 -8194
@@ -1,10 +1,13 @@
1
+ import type { DescribedJwtOptions } from "../../core/auth/types/jwt.interface.js";
1
2
  import type { AnyBlueprint, AnyBlueprintList, AnyRecordClass, BlueprintField, BlueprintValue, IBlueprint, IBlueprintCollection, IBlueprintList, OptionalBlueprint, OptionalBlueprintList, OptionalRecord } from "../../core/blueprint/types/blueprint.interface.js";
2
3
  import type { EntityIdentityKey } from "../../core/entity/types/entity.interface.js";
3
4
  import type { EntityVersion } from "../../core/entity/version.js";
4
5
  import type { OptionalProperty } from "../../core/property/types/optional-property.type.js";
5
6
  import type { AnyPropertyConstructor } from "../../core/property/types/property-collection.type.js";
6
7
  import type { AnyRepository, AnyRepositoryEntity, EntityInstanceOf } from "../../core/repository/types/repository.interface.js";
8
+ import type { AccessDeclaration } from "../../core/use-case/types/access.interface.js";
7
9
  import type { IUseCasePlugin, UseCaseTarget } from "../../core/use-case/types/plugin.interface.js";
10
+ import type { RouteSettings } from "../../core/use-case/types/route.interface.js";
8
11
  import type { AnyExceptionClass, DependencyFunctions, StepsUseCaseConstructor, UseCaseCollection } from "../../core/use-case/use-case.js";
9
12
  import type { Integer, StringProperty } from "../properties/primitives.js";
10
13
  type EntityOf<Repository> = Repository extends {
@@ -44,7 +47,41 @@ export interface StdUseCaseOptions {
44
47
  readonly tags?: readonly string[];
45
48
  readonly deprecated?: boolean;
46
49
  readonly errors?: StdUseCaseErrors;
50
+ /** Where the use case is served over HTTP: the `route` of `useCaseOf`. */
51
+ readonly route?: RouteSettings;
52
+ /** Who may call it: the `access` of `useCaseOf`. */
53
+ readonly access?: AccessDeclaration;
47
54
  }
55
+ /**
56
+ * The tokens `login` and `refresh` issue: the options of `jwtOf`, plus what
57
+ * a token says of the entity. JSON, so it lives in an `afora.json`: the
58
+ * secret is named by its environment variable (`{ env: "JWT_SECRET" }`).
59
+ * Experimental: it may change in any release.
60
+ */
61
+ export interface StdTokenOptions extends DescribedJwtOptions {
62
+ /** Also issues a refresh token, which `refresh` exchanges; `expiresIn` is its life in seconds. */
63
+ readonly refresh?: {
64
+ readonly expiresIn: number;
65
+ };
66
+ /** Scopes every token gets. */
67
+ readonly scopes?: readonly string[];
68
+ /** A key of the entity that holds a list of scopes, added to `scopes`. */
69
+ readonly scopesFrom?: string;
70
+ /** Claims of the token, each from a key of the entity: `{ tenant: "tenantId" }`. */
71
+ readonly claims?: {
72
+ readonly [claim: string]: string;
73
+ };
74
+ }
75
+ /** The response keys the `token` option adds. */
76
+ type TokenResponse<Options> = Options extends {
77
+ readonly token: infer Token;
78
+ } ? {
79
+ readonly token: typeof StringProperty;
80
+ } & (Token extends {
81
+ readonly refresh: object;
82
+ } ? {
83
+ readonly refreshToken: typeof StringProperty;
84
+ } : unknown) : unknown;
48
85
  /** Who owns the entities: the caller, whose id the context gives. */
49
86
  export interface StdOwnerOption<Repository> {
50
87
  /**
@@ -61,7 +98,11 @@ export interface StdOwnerOption<Repository> {
61
98
  }
62
99
  /** The options of a standard use case that gives one entity. */
63
100
  export interface StdEntityOptions<Repository> extends StdUseCaseOptions, StdOwnerOption<Repository> {
64
- /** The key of the response that holds the entity; `item` by default. */
101
+ /**
102
+ * The key of the response that holds the entity.
103
+ *
104
+ * @default "item"
105
+ */
65
106
  readonly as?: string;
66
107
  }
67
108
  /** The options of `create` and `register`. */
@@ -258,12 +299,34 @@ export type RegisterMaker = <Repository extends AnyRepository, const Options ext
258
299
  /** `login`: for an authenticatable entity, with `findByEmail`. */
259
300
  export type LoginMaker = <Repository extends AnyRepository, const Options extends StdUseCaseOptions & {
260
301
  readonly as?: string;
302
+ /** Issues a token for the entity (and a refresh token, with `refresh`), in the response. */
303
+ readonly token?: StdTokenOptions;
261
304
  } & StdNoHookOptions = None>(repository: Repository & UseCaseTarget<Repository, "findByEmail", Authenticatable, "authenticatable">, options?: Options) => StepsUseCaseConstructor<{
262
305
  readonly email: FieldAt<Props<Repository>, "email">;
263
306
  readonly password: typeof StringProperty;
264
- }, EntityResponse<Repository, Options>, {
307
+ }, EntityResponse<Repository, Options> & TokenResponse<Options>, {
265
308
  readonly repository: Repository;
266
309
  }>;
310
+ /** `refresh`: exchanges a refresh token of `login` for new tokens; needs `findById`. */
311
+ export type RefreshMaker = <Repository extends AnyRepository>(repository: Repository & UseCaseTarget<Repository, "findById">, options: StdUseCaseOptions & {
312
+ readonly token: StdTokenOptions & {
313
+ readonly refresh: {
314
+ readonly expiresIn: number;
315
+ };
316
+ };
317
+ } & StdNoHookOptions) => StepsUseCaseConstructor<{
318
+ readonly refreshToken: typeof StringProperty;
319
+ }, {
320
+ readonly token: typeof StringProperty;
321
+ readonly refreshToken: typeof StringProperty;
322
+ }, {
323
+ readonly repository: Repository;
324
+ }>;
325
+ /** `logout`: gives empty tokens, which a route with `setCookie` turns into removed cookies. */
326
+ export type LogoutMaker = <Repository extends AnyRepository>(repository: Repository, options?: StdUseCaseOptions & StdNoHookOptions) => StepsUseCaseConstructor<Record<never, never>, {
327
+ readonly token: typeof StringProperty;
328
+ readonly refreshToken: typeof StringProperty;
329
+ }, Record<never, never>>;
267
330
  /**
268
331
  * The standard use cases, by name: what the `useCases` of `commandOf` gets
269
332
  * to pick from, each called with a repository and its options.
@@ -283,9 +346,46 @@ export interface StdUseCaseContext {
283
346
  readonly untag: TagMaker;
284
347
  readonly register: RegisterMaker;
285
348
  readonly login: LoginMaker;
349
+ readonly refresh: RefreshMaker;
350
+ readonly logout: LogoutMaker;
286
351
  }
287
352
  /** The plugins of `StdUseCases`, by name. */
288
353
  export type StdUseCasePlugins = {
289
354
  readonly [Name in keyof StdUseCaseContext]: IUseCasePlugin<Name, StdUseCaseContext[Name]>;
290
355
  };
356
+ /** The operations `resourceOf` can give. */
357
+ export type ResourceAction = "create" | "get" | "list" | "update" | "delete";
358
+ /** The use cases of `resourceOf`, by operation: the standard ones, with no option but their route and access. */
359
+ export interface ResourceUseCases<Repository extends AnyRepository> {
360
+ readonly create: EntityUseCase<Repository, None, CreateInput<Repository, None>>;
361
+ readonly get: EntityUseCase<Repository, None, ById<Repository>>;
362
+ readonly list: StepsUseCaseConstructor<{
363
+ readonly limit: OptionalProperty<typeof Integer>;
364
+ readonly offset: OptionalProperty<typeof Integer>;
365
+ }, {
366
+ readonly items: IBlueprintList<EntityOf<Repository>>;
367
+ readonly total: typeof Integer;
368
+ readonly limit: typeof Integer;
369
+ readonly offset: typeof Integer;
370
+ }, {
371
+ readonly repository: Repository;
372
+ }>;
373
+ readonly update: EntityUseCase<Repository, None, UpdateInput<Repository>>;
374
+ readonly delete: EntityUseCase<Repository, None, ById<Repository>>;
375
+ }
376
+ /** The options of `resourceOf`. */
377
+ export interface ResourceOptions<Actions extends readonly ResourceAction[] = readonly ResourceAction[]> {
378
+ /** The path of the collection, e.g. `"/posts"`; one entity is at `{path}/{id}`. */
379
+ readonly path: string;
380
+ /** The operations to give; all five by default. Each needs its repository action. */
381
+ readonly actions?: Actions;
382
+ /**
383
+ * Who may call: one declaration for every operation, or one per operation
384
+ * (`{ create: { scopes: ["posts:write"] }, get: { public: true } }`; an
385
+ * operation left out declares none).
386
+ */
387
+ readonly access?: AccessDeclaration | {
388
+ readonly [Action in ResourceAction]?: AccessDeclaration;
389
+ };
390
+ }
291
391
  export {};
File without changes
@@ -0,0 +1,32 @@
1
+ import type { Authenticator, HttpOptions, SinkStage } from "../adapters.js";
2
+ import type { ContractRunner } from "./provider-contract.js";
3
+ /** What the kit needs of a sink that serves HTTP. */
4
+ export interface HttpProviderContractSettings<Server> {
5
+ /** The sink under test, made with these options and this authenticator. */
6
+ sink(options: HttpOptions, hooks: {
7
+ readonly authenticator?: Authenticator;
8
+ }): SinkStage<Server, HttpOptions>;
9
+ /** Sends a request to the server the sink built, and gives its response. */
10
+ fetch(server: Server, request: Request): Response | Promise<Response>;
11
+ /** Releases the server (a listening socket), after each case. */
12
+ close?(server: Server): void | Promise<void>;
13
+ }
14
+ /**
15
+ * The cases every sink that serves HTTP must pass, whatever its server: the
16
+ * routes use cases declare, where each input key is read from, the context,
17
+ * access, statuses, problems, cookies, the request id and the document. A
18
+ * sink built on `compileRoutes` passes when it routes each method and path
19
+ * to `handle` and sends the result as is.
20
+ *
21
+ * ```ts
22
+ * import { describe, expect, it } from "bun:test";
23
+ * import { httpProviderContract } from "@afora/core/testing";
24
+ *
25
+ * httpProviderContract(
26
+ * "hono",
27
+ * { sink: (options, hooks) => hono(options, hooks), fetch: (app, request) => app.fetch(request) },
28
+ * { describe, it, expect },
29
+ * );
30
+ * ```
31
+ */
32
+ export declare function httpProviderContract<Server>(name: string, settings: HttpProviderContractSettings<Server>, runner: ContractRunner): void;
@@ -0,0 +1,344 @@
1
+ import {
2
+ blueprintOf,
3
+ commandOf,
4
+ ExceptionLayers,
5
+ exceptionOf,
6
+ outputOf,
7
+ recordOf,
8
+ useCaseOf
9
+ } from "../index.js";
10
+ const Note = recordOf(blueprintOf((ctx) => ({ title: ctx.string, secret: ctx.password })));
11
+ const Rejected = exceptionOf({
12
+ layer: ExceptionLayers.UseCase,
13
+ name: "ContractRejected",
14
+ message: "rejected"
15
+ });
16
+ const Echo = blueprintOf((ctx) => ({
17
+ id: ctx.optional.string,
18
+ page: ctx.optional.integer,
19
+ flag: ctx.optional.boolean,
20
+ locale: ctx.optional.string,
21
+ session: ctx.optional.string,
22
+ title: ctx.optional.string,
23
+ caller: ctx.optional.string,
24
+ tenant: ctx.optional.string
25
+ }));
26
+ function operations() {
27
+
28
+ class Get extends useCaseOf({
29
+ name: "ContractGet",
30
+ blueprint: blueprintOf((ctx) => ({
31
+ id: ctx.string,
32
+ page: ctx.optional.integer,
33
+ flag: ctx.optional.boolean
34
+ })),
35
+ response: Echo,
36
+ route: { method: "get", path: "/items/{id}" }
37
+ }) {
38
+ async execute() {
39
+ return { ...this.input };
40
+ }
41
+ }
42
+
43
+ class Change extends useCaseOf({
44
+ name: "ContractChange",
45
+ blueprint: blueprintOf((ctx) => ({
46
+ id: ctx.string,
47
+ page: ctx.integer,
48
+ locale: ctx.optional.string,
49
+ session: ctx.optional.string,
50
+ title: ctx.string
51
+ })),
52
+ response: Echo,
53
+ context: blueprintOf((ctx) => ({ tenant: ctx.optional.string })),
54
+ errors: [Rejected],
55
+ route: {
56
+ method: "put",
57
+ path: "/items/{id}",
58
+ status: 201,
59
+ query: ["page"],
60
+ headers: { locale: "accept-language" },
61
+ cookie: { session: "sid" }
62
+ }
63
+ }) {
64
+ async execute() {
65
+ if (this.input.title === "reject")
66
+ throw new Rejected({ source: "x" });
67
+ if (this.input.title === "crash")
68
+ throw new Error("internal detail");
69
+ return { ...this.input, tenant: this.context.tenant };
70
+ }
71
+ }
72
+
73
+ class Remove extends useCaseOf({
74
+ name: "ContractRemove",
75
+ blueprint: blueprintOf((ctx) => ({ id: ctx.string })),
76
+ response: blueprintOf(() => ({})),
77
+ route: { method: "delete", path: "/items/{id}", status: 204 }
78
+ }) {
79
+ async execute() {
80
+ return {};
81
+ }
82
+ }
83
+
84
+ class Reveal extends useCaseOf({
85
+ name: "ContractReveal",
86
+ blueprint: blueprintOf(() => ({})),
87
+ response: blueprintOf(() => ({ note: Note })),
88
+ route: { method: "get", path: "/note" }
89
+ }) {
90
+ async execute() {
91
+ return { note: new Note({ title: "t", secret: await hashed() }) };
92
+ }
93
+ }
94
+
95
+ class Mine extends useCaseOf({
96
+ name: "ContractMine",
97
+ blueprint: blueprintOf(() => ({})),
98
+ response: Echo,
99
+ context: blueprintOf((ctx) => ({ caller: ctx.string })),
100
+ route: { method: "get", path: "/mine" },
101
+ access: { scopes: ["items:read"], context: { caller: "subject" } }
102
+ }) {
103
+ async execute() {
104
+ return { caller: this.context.caller };
105
+ }
106
+ }
107
+
108
+ class Enter extends useCaseOf({
109
+ name: "ContractEnter",
110
+ blueprint: blueprintOf((ctx) => ({ title: ctx.string })),
111
+ response: blueprintOf((ctx) => ({
112
+ title: ctx.string,
113
+ token: ctx.string,
114
+ refreshToken: ctx.string
115
+ })),
116
+ route: {
117
+ method: "post",
118
+ path: "/enter",
119
+ setCookie: {
120
+ token: { name: "sid", secure: false },
121
+ refreshToken: { name: "rid", secure: false, path: "/refresh" }
122
+ }
123
+ }
124
+ }) {
125
+ async execute() {
126
+ return { title: this.input.title, token: "t 1", refreshToken: "" };
127
+ }
128
+ }
129
+ return commandOf({
130
+ name: "Contract",
131
+ useCases: {
132
+ get: Get,
133
+ change: Change,
134
+ remove: Remove,
135
+ reveal: Reveal,
136
+ mine: Mine,
137
+ enter: Enter
138
+ }
139
+ });
140
+ }
141
+ async function hashed() {
142
+ return "$argon2id$v=19$m=65536,t=2,p=1$c29tZXNhbHRzb21lc2FsdA$4VbQ3OqJ7xk8Y0mG0iZ1c2k1m2Qe0sQeJ7xk8Y0mG0i";
143
+ }
144
+ const authenticator = (request) => {
145
+ const token = request.header("authorization");
146
+ if (token === "Bearer reader")
147
+ return { subject: "u1", scopes: ["items:read"] };
148
+ if (token === "Bearer other")
149
+ return { subject: "u2", scopes: [] };
150
+ return null;
151
+ };
152
+ export function httpProviderContract(name, settings, runner) {
153
+ const { describe, it, expect } = runner;
154
+ const origin = "http://contract.test";
155
+ const withServer = async (options, body, hooks = { authenticator }) => {
156
+ const server = outputOf({
157
+ name: "Contract",
158
+ commands: [operations()],
159
+ statuses: { ContractRejected: 409 },
160
+ pipe: [settings.sink(options, hooks)]
161
+ }).build();
162
+ try {
163
+ await body(async (method, path, init = {}) => {
164
+ const hasBody = init.body !== undefined;
165
+ const response = await settings.fetch(server, new Request(`${origin}${path}`, {
166
+ method,
167
+ headers: {
168
+ ...hasBody ? { "content-type": "application/json" } : {},
169
+ ...init.headers
170
+ },
171
+ ...hasBody ? {
172
+ body: typeof init.body === "string" ? init.body : JSON.stringify(init.body)
173
+ } : {}
174
+ }));
175
+ const text = await response.text();
176
+ let json;
177
+ try {
178
+ json = text === "" ? undefined : JSON.parse(text);
179
+ } catch {
180
+ json = undefined;
181
+ }
182
+ return {
183
+ status: response.status,
184
+ type: (response.headers.get("content-type") ?? "").split(";")[0],
185
+ headers: response.headers,
186
+ text,
187
+ json
188
+ };
189
+ });
190
+ } finally {
191
+ await settings.close?.(server);
192
+ }
193
+ };
194
+ describe(`${name}: HTTP provider contract`, () => {
195
+ it("serves the route a use case declares, path and query coerced", async () => {
196
+ await withServer({ basePath: "/api" }, async (send) => {
197
+ const found = await send("GET", "/api/items/a%20b?page=2&flag=true");
198
+ expect(found.status).toBe(200);
199
+ expect(found.type).toBe("application/json");
200
+ expect(found.json).toEqual({ id: "a b", page: 2, flag: true });
201
+ });
202
+ });
203
+ it("reads each key where the route places it, over the body", async () => {
204
+ await withServer({}, async (send) => {
205
+ const changed = await send("PUT", "/items/p?page=3", {
206
+ headers: { "accept-language": "pt-BR", cookie: "x=1; sid=s%201" },
207
+ body: { id: "from-body", page: 9, title: "T" }
208
+ });
209
+ expect(changed.status).toBe(201);
210
+ expect(changed.json).toEqual({
211
+ id: "p",
212
+ page: 3,
213
+ locale: "pt-BR",
214
+ session: "s 1",
215
+ title: "T"
216
+ });
217
+ });
218
+ });
219
+ it("fills the context from headers", async () => {
220
+ await withServer({ context: { headers: { tenant: "x-tenant" } } }, async (send) => {
221
+ const changed = await send("PUT", "/items/p?page=1", {
222
+ headers: { "x-tenant": "acme" },
223
+ body: { title: "T" }
224
+ });
225
+ expect(changed.json.tenant).toBe("acme");
226
+ });
227
+ });
228
+ it("answers invalid input and invalid JSON with a 400 problem", async () => {
229
+ await withServer({}, async (send) => {
230
+ const invalid = await send("PUT", "/items/p?page=1", {
231
+ body: { title: 1 }
232
+ });
233
+ expect(invalid.status).toBe(400);
234
+ expect(invalid.type).toBe("application/problem+json");
235
+ const problem = invalid.json;
236
+ expect(problem.title).toBe("InvalidUseCaseInput");
237
+ expect(problem.issues.map((issue) => issue.path)).toEqual([["title"]]);
238
+ const notJson = await send("PUT", "/items/p?page=1", {
239
+ body: "{nope"
240
+ });
241
+ expect(notJson.status).toBe(400);
242
+ expect(notJson.json.title).toBe("InvalidRequestBody");
243
+ });
244
+ });
245
+ it("gives errors the status of the output, and hides internal ones", async () => {
246
+ await withServer({}, async (send) => {
247
+ const rejected = await send("PUT", "/items/p?page=1", {
248
+ body: { title: "reject" }
249
+ });
250
+ expect(rejected.status).toBe(409);
251
+ expect(rejected.type).toBe("application/problem+json");
252
+ const crashed = await send("PUT", "/items/p?page=1", {
253
+ body: { title: "crash" }
254
+ });
255
+ expect(crashed.status).toBe(500);
256
+ expect(crashed.text.includes("internal detail")).toBe(false);
257
+ });
258
+ await withServer({ debug: true }, async (send) => {
259
+ const crashed = await send("PUT", "/items/p?page=1", {
260
+ body: { title: "crash" }
261
+ });
262
+ expect(crashed.text.includes("internal detail")).toBe(true);
263
+ });
264
+ });
265
+ it("answers a 204 without a body, and an unknown route with a 404 problem", async () => {
266
+ await withServer({}, async (send) => {
267
+ const removed = await send("DELETE", "/items/p");
268
+ expect(removed.status).toBe(204);
269
+ expect(removed.text).toBe("");
270
+ const unknown = await send("GET", "/nowhere");
271
+ expect(unknown.status).toBe(404);
272
+ expect(unknown.type).toBe("application/problem+json");
273
+ expect(unknown.json.title).toBe("NotFound");
274
+ });
275
+ });
276
+ it("masks sensitive values", async () => {
277
+ await withServer({}, async (send) => {
278
+ const revealed = await send("GET", "/note");
279
+ expect(revealed.json).toEqual({
280
+ note: { title: "t", secret: "********" }
281
+ });
282
+ });
283
+ });
284
+ it("enforces the access of a use case", async () => {
285
+ await withServer({}, async (send) => {
286
+ const anonymous = await send("GET", "/mine");
287
+ expect(anonymous.status).toBe(401);
288
+ expect(anonymous.json.title).toBe("Unauthenticated");
289
+ const other = await send("GET", "/mine", {
290
+ headers: { authorization: "Bearer other" }
291
+ });
292
+ expect(other.status).toBe(403);
293
+ const reader = await send("GET", "/mine", {
294
+ headers: { authorization: "Bearer reader" }
295
+ });
296
+ expect(reader.status).toBe(200);
297
+ expect(reader.json).toEqual({ caller: "u1" });
298
+ });
299
+ await withServer({}, async (send) => {
300
+ const closed = await send("GET", "/mine", {
301
+ headers: { authorization: "Bearer reader" }
302
+ });
303
+ expect(closed.status).toBe(401);
304
+ }, {});
305
+ });
306
+ it("sends the cookies of a route, out of the body", async () => {
307
+ await withServer({}, async (send) => {
308
+ const entered = await send("POST", "/enter", { body: { title: "T" } });
309
+ expect(entered.status).toBe(200);
310
+ expect(entered.json).toEqual({ title: "T" });
311
+ expect(entered.headers.getSetCookie()).toEqual([
312
+ "sid=t%201; Path=/; HttpOnly; SameSite=Lax",
313
+ "rid=; Path=/refresh; HttpOnly; SameSite=Lax; Max-Age=0"
314
+ ]);
315
+ });
316
+ });
317
+ it("puts the request id on successes and problems", async () => {
318
+ await withServer({ requestId: true }, async (send) => {
319
+ const sent = await send("GET", "/items/a", {
320
+ headers: { "x-request-id": "abc-1" }
321
+ });
322
+ expect(sent.headers.get("x-request-id")).toBe("abc-1");
323
+ const problem = await send("GET", "/mine");
324
+ expect((problem.headers.get("x-request-id") ?? "").length).toBe(36);
325
+ });
326
+ });
327
+ it("serves the document of the same routes", async () => {
328
+ await withServer({
329
+ basePath: "/api",
330
+ docs: { path: "/openapi.json", info: { title: "C", version: "1" } }
331
+ }, async (send) => {
332
+ const docs = await send("GET", "/openapi.json");
333
+ expect(docs.status).toBe(200);
334
+ expect(docs.type).toBe("application/json");
335
+ expect(Object.keys(docs.json.paths).sort()).toEqual([
336
+ "/api/enter",
337
+ "/api/items/{id}",
338
+ "/api/mine",
339
+ "/api/note"
340
+ ]);
341
+ });
342
+ });
343
+ });
344
+ }
@@ -1,3 +1,5 @@
1
+ export type { HttpProviderContractSettings } from "./http-provider-contract.js";
2
+ export { httpProviderContract } from "./http-provider-contract.js";
1
3
  export type { MemoryPublisher } from "./memory-publisher.js";
2
4
  export { memoryPublisher } from "./memory-publisher.js";
3
5
  export type { ContractRunner, ProviderContractOptions, } from "./provider-contract.js";