ddd-ts-kit 0.12.0 → 0.14.0

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 +19 -4
  2. package/dist/check/catalogue/architecture-rules.d.ts.map +1 -1
  3. package/dist/check/catalogue/architecture-rules.js +6 -0
  4. package/dist/check/catalogue/architecture-rules.js.map +1 -1
  5. package/dist/check/errors/kit-error-codes.d.ts.map +1 -1
  6. package/dist/check/errors/kit-error-codes.js +1 -0
  7. package/dist/check/errors/kit-error-codes.js.map +1 -1
  8. package/dist/check/explain/authz-docs.d.ts.map +1 -1
  9. package/dist/check/explain/authz-docs.js +12 -0
  10. package/dist/check/explain/authz-docs.js.map +1 -1
  11. package/dist/check/explain/errors-docs.js +1 -1
  12. package/dist/check/explain/errors-docs.js.map +1 -1
  13. package/dist/check/explain/rule-docs.d.ts.map +1 -1
  14. package/dist/check/explain/rule-docs.js +2 -0
  15. package/dist/check/explain/rule-docs.js.map +1 -1
  16. package/dist/check/explain/schema-docs.d.ts +3 -0
  17. package/dist/check/explain/schema-docs.d.ts.map +1 -0
  18. package/dist/check/explain/schema-docs.js +28 -0
  19. package/dist/check/explain/schema-docs.js.map +1 -0
  20. package/dist/check/fixtures/authz-fixtures.d.ts.map +1 -1
  21. package/dist/check/fixtures/authz-fixtures.js +38 -1
  22. package/dist/check/fixtures/authz-fixtures.js.map +1 -1
  23. package/dist/check/fixtures/fixtures.d.ts.map +1 -1
  24. package/dist/check/fixtures/fixtures.js +2 -0
  25. package/dist/check/fixtures/fixtures.js.map +1 -1
  26. package/dist/check/fixtures/schema-fixtures.d.ts +3 -0
  27. package/dist/check/fixtures/schema-fixtures.d.ts.map +1 -0
  28. package/dist/check/fixtures/schema-fixtures.js +129 -0
  29. package/dist/check/fixtures/schema-fixtures.js.map +1 -0
  30. package/dist/check/rules/authz-fields-by-schema.d.ts +3 -0
  31. package/dist/check/rules/authz-fields-by-schema.d.ts.map +1 -0
  32. package/dist/check/rules/authz-fields-by-schema.js +33 -0
  33. package/dist/check/rules/authz-fields-by-schema.js.map +1 -0
  34. package/dist/check/rules/schema-no-duplicate-shape.d.ts +3 -0
  35. package/dist/check/rules/schema-no-duplicate-shape.d.ts.map +1 -0
  36. package/dist/check/rules/schema-no-duplicate-shape.js +60 -0
  37. package/dist/check/rules/schema-no-duplicate-shape.js.map +1 -0
  38. package/dist/check/rules/schema-presentation-only.d.ts +3 -0
  39. package/dist/check/rules/schema-presentation-only.d.ts.map +1 -0
  40. package/dist/check/rules/schema-presentation-only.js +53 -0
  41. package/dist/check/rules/schema-presentation-only.js.map +1 -0
  42. package/dist/check/schemas/class-shapes.d.ts +9 -0
  43. package/dist/check/schemas/class-shapes.d.ts.map +1 -0
  44. package/dist/check/schemas/class-shapes.js +52 -0
  45. package/dist/check/schemas/class-shapes.js.map +1 -0
  46. package/dist/check/schemas/declared-shape.d.ts +9 -0
  47. package/dist/check/schemas/declared-shape.d.ts.map +1 -0
  48. package/dist/check/schemas/declared-shape.js +2 -0
  49. package/dist/check/schemas/declared-shape.js.map +1 -0
  50. package/dist/check/schemas/holds-schemas.d.ts +5 -0
  51. package/dist/check/schemas/holds-schemas.d.ts.map +1 -0
  52. package/dist/check/schemas/holds-schemas.js +12 -0
  53. package/dist/check/schemas/holds-schemas.js.map +1 -0
  54. package/dist/check/schemas/object-shapes.d.ts +8 -0
  55. package/dist/check/schemas/object-shapes.d.ts.map +1 -0
  56. package/dist/check/schemas/object-shapes.js +31 -0
  57. package/dist/check/schemas/object-shapes.js.map +1 -0
  58. package/dist/check/schemas/restored-code.d.ts +7 -0
  59. package/dist/check/schemas/restored-code.d.ts.map +1 -0
  60. package/dist/check/schemas/restored-code.js +17 -0
  61. package/dist/check/schemas/restored-code.js.map +1 -0
  62. package/dist/check/schemas/shared-presentation.d.ts +4 -0
  63. package/dist/check/schemas/shared-presentation.d.ts.map +1 -0
  64. package/dist/check/schemas/shared-presentation.js +5 -0
  65. package/dist/check/schemas/shared-presentation.js.map +1 -0
  66. package/dist/check/schemas/shares-schemas.d.ts +4 -0
  67. package/dist/check/schemas/shares-schemas.d.ts.map +1 -0
  68. package/dist/check/schemas/shares-schemas.js +5 -0
  69. package/dist/check/schemas/shares-schemas.js.map +1 -0
  70. package/dist/check/schemas/still-generated.d.ts +8 -0
  71. package/dist/check/schemas/still-generated.d.ts.map +1 -0
  72. package/dist/check/schemas/still-generated.js +11 -0
  73. package/dist/check/schemas/still-generated.js.map +1 -0
  74. package/dist/check/schemas/top-level-statements.d.ts +3 -0
  75. package/dist/check/schemas/top-level-statements.d.ts.map +1 -0
  76. package/dist/check/schemas/top-level-statements.js +21 -0
  77. package/dist/check/schemas/top-level-statements.js.map +1 -0
  78. package/dist/check/schemas/validation-packages.d.ts +3 -0
  79. package/dist/check/schemas/validation-packages.d.ts.map +1 -0
  80. package/dist/check/schemas/validation-packages.js +13 -0
  81. package/dist/check/schemas/validation-packages.js.map +1 -0
  82. package/dist/commands/init.d.ts.map +1 -1
  83. package/dist/commands/init.js +3 -0
  84. package/dist/commands/init.js.map +1 -1
  85. package/dist/config/default-allowed-packages.d.ts.map +1 -1
  86. package/dist/config/default-allowed-packages.js +1 -0
  87. package/dist/config/default-allowed-packages.js.map +1 -1
  88. package/dist/config/index.d.ts +1 -1
  89. package/dist/config/index.d.ts.map +1 -1
  90. package/dist/config/index.js +1 -1
  91. package/dist/config/index.js.map +1 -1
  92. package/dist/config/parse-stack.d.ts.map +1 -1
  93. package/dist/config/parse-stack.js +4 -1
  94. package/dist/config/parse-stack.js.map +1 -1
  95. package/dist/config/stack.d.ts +25 -2
  96. package/dist/config/stack.d.ts.map +1 -1
  97. package/dist/config/stack.js +5 -2
  98. package/dist/config/stack.js.map +1 -1
  99. package/dist/describe/context-description.d.ts +6 -0
  100. package/dist/describe/context-description.d.ts.map +1 -1
  101. package/dist/describe/format-description.js +9 -1
  102. package/dist/describe/format-description.js.map +1 -1
  103. package/dist/describe/presentation-of-route.d.ts +4 -0
  104. package/dist/describe/presentation-of-route.d.ts.map +1 -0
  105. package/dist/describe/presentation-of-route.js +15 -0
  106. package/dist/describe/presentation-of-route.js.map +1 -0
  107. package/dist/describe/read-controllers.d.ts.map +1 -1
  108. package/dist/describe/read-controllers.js +4 -1
  109. package/dist/describe/read-controllers.js.map +1 -1
  110. package/dist/describe/read-routes.d.ts.map +1 -1
  111. package/dist/describe/read-routes.js +2 -0
  112. package/dist/describe/read-routes.js.map +1 -1
  113. package/dist/generate/kinds/form-kind.d.ts +3 -0
  114. package/dist/generate/kinds/form-kind.d.ts.map +1 -0
  115. package/dist/generate/kinds/form-kind.js +38 -0
  116. package/dist/generate/kinds/form-kind.js.map +1 -0
  117. package/dist/generate/kinds/presentation/controller-of.d.ts +10 -0
  118. package/dist/generate/kinds/presentation/controller-of.d.ts.map +1 -0
  119. package/dist/generate/kinds/presentation/controller-of.js +13 -0
  120. package/dist/generate/kinds/presentation/controller-of.js.map +1 -0
  121. package/dist/generate/kinds/presentation/controller-source.d.ts +13 -0
  122. package/dist/generate/kinds/presentation/controller-source.d.ts.map +1 -0
  123. package/dist/generate/kinds/presentation/controller-source.js +44 -0
  124. package/dist/generate/kinds/presentation/controller-source.js.map +1 -0
  125. package/dist/generate/kinds/representation-kind.d.ts +3 -0
  126. package/dist/generate/kinds/representation-kind.d.ts.map +1 -0
  127. package/dist/generate/kinds/representation-kind.js +54 -0
  128. package/dist/generate/kinds/representation-kind.js.map +1 -0
  129. package/dist/generate/kinds/route/controller-imports.d.ts.map +1 -1
  130. package/dist/generate/kinds/route/controller-imports.js +2 -1
  131. package/dist/generate/kinds/route/controller-imports.js.map +1 -1
  132. package/dist/generate/kinds/route/controller-types.d.ts.map +1 -1
  133. package/dist/generate/kinds/route/controller-types.js +4 -2
  134. package/dist/generate/kinds/route/controller-types.js.map +1 -1
  135. package/dist/generate/kinds/route/controller-vars.d.ts.map +1 -1
  136. package/dist/generate/kinds/route/controller-vars.js +21 -9
  137. package/dist/generate/kinds/route/controller-vars.js.map +1 -1
  138. package/dist/generate/kinds/route/dto-classes.d.ts +5 -2
  139. package/dist/generate/kinds/route/dto-classes.d.ts.map +1 -1
  140. package/dist/generate/kinds/route/dto-classes.js +6 -3
  141. package/dist/generate/kinds/route/dto-classes.js.map +1 -1
  142. package/dist/generate/kinds/route/route-vars.d.ts.map +1 -1
  143. package/dist/generate/kinds/route/route-vars.js +17 -4
  144. package/dist/generate/kinds/route/route-vars.js.map +1 -1
  145. package/dist/generate/kinds/route/search-line-libraries.d.ts +3 -0
  146. package/dist/generate/kinds/route/search-line-libraries.d.ts.map +1 -0
  147. package/dist/generate/kinds/route/search-line-libraries.js +3 -0
  148. package/dist/generate/kinds/route/search-line-libraries.js.map +1 -0
  149. package/dist/generate/kinds/route/shares-schemas.d.ts +4 -0
  150. package/dist/generate/kinds/route/shares-schemas.d.ts.map +1 -0
  151. package/dist/generate/kinds/route/shares-schemas.js +5 -0
  152. package/dist/generate/kinds/route/shares-schemas.js.map +1 -0
  153. package/dist/generate/kinds/route/write-controller.d.ts.map +1 -1
  154. package/dist/generate/kinds/route/write-controller.js +4 -2
  155. package/dist/generate/kinds/route/write-controller.js.map +1 -1
  156. package/dist/generate/kinds/route-kind.d.ts.map +1 -1
  157. package/dist/generate/kinds/route-kind.js +4 -2
  158. package/dist/generate/kinds/route-kind.js.map +1 -1
  159. package/dist/generate/kinds/schema-kind.d.ts +3 -0
  160. package/dist/generate/kinds/schema-kind.d.ts.map +1 -0
  161. package/dist/generate/kinds/schema-kind.js +36 -0
  162. package/dist/generate/kinds/schema-kind.js.map +1 -0
  163. package/dist/generate/kinds.d.ts.map +1 -1
  164. package/dist/generate/kinds.js +6 -0
  165. package/dist/generate/kinds.js.map +1 -1
  166. package/dist/generate/shared-writes.d.ts.map +1 -1
  167. package/dist/generate/shared-writes.js +5 -0
  168. package/dist/generate/shared-writes.js.map +1 -1
  169. package/dist/help/usage.d.ts +1 -1
  170. package/dist/help/usage.d.ts.map +1 -1
  171. package/dist/help/usage.js +5 -2
  172. package/dist/help/usage.js.map +1 -1
  173. package/dist/init/files/platform-files.js +3 -1
  174. package/dist/init/files/platform-files.js.map +1 -1
  175. package/dist/init/manifest/dependencies-for.d.ts.map +1 -1
  176. package/dist/init/manifest/dependencies-for.js +2 -0
  177. package/dist/init/manifest/dependencies-for.js.map +1 -1
  178. package/dist/init/manifest/versions.d.ts.map +1 -1
  179. package/dist/init/manifest/versions.js +27 -24
  180. package/dist/init/manifest/versions.js.map +1 -1
  181. package/dist/init/stack/resolve-stack.d.ts.map +1 -1
  182. package/dist/init/stack/resolve-stack.js +18 -2
  183. package/dist/init/stack/resolve-stack.js.map +1 -1
  184. package/dist/init/stack/stack-options.d.ts +3 -0
  185. package/dist/init/stack/stack-options.d.ts.map +1 -1
  186. package/dist/init/validation-dependencies.d.ts.map +1 -1
  187. package/dist/init/validation-dependencies.js +1 -0
  188. package/dist/init/validation-dependencies.js.map +1 -1
  189. package/dist/stack/stack-vars.js +25 -7
  190. package/dist/stack/stack-vars.js.map +1 -1
  191. package/dist/version.d.ts +1 -1
  192. package/dist/version.js +1 -1
  193. package/package.json +2 -2
  194. package/templates/controller/controller.class-validator.ts.tpl +2 -2
  195. package/templates/controller/controller.none.ts.tpl +2 -2
  196. package/templates/controller/controller.search.ts.tpl +14 -0
  197. package/templates/controller/controller.standard-schema.ts.tpl +2 -2
  198. package/templates/controller/controller.typebox.ts.tpl +98 -0
  199. package/templates/controller/controller.zod.ts.tpl +2 -2
  200. package/templates/docs/openapi.json.tpl +15 -0
  201. package/templates/init/CLAUDE.md.tpl +2 -2
  202. package/templates/init/platform/auth/http-boundary.ts.tpl +20 -3
  203. package/templates/init/platform/compose-application.manual.ts.tpl +18 -0
  204. package/templates/init/platform/compose-application.tsyringe.ts.tpl +23 -0
  205. package/templates/init/platform/http/presentation-of.ts.tpl +27 -0
  206. package/templates/init/platform/http/start-http-server.express.ts.tpl +1 -1
  207. package/templates/init/platform/http/start-http-server.fastify.ts.tpl +1 -1
  208. package/templates/init/platform/http/start-http-server.hono.ts.tpl +84 -0
  209. package/templates/init/platform/http/start-http-server.nest.ts.tpl +2 -2
  210. package/templates/init/platform/http/with-documentation.ts.tpl +13 -3
  211. package/templates/init/platform/http/with-forms.ts.tpl +7 -0
  212. package/templates/init/rules/application.md.tpl +1 -1
  213. package/templates/init/rules/presentation.md.tpl +4 -1
  214. package/templates/init/scripts/openapi.ts.tpl +27 -6
  215. package/templates/pipeline/biome.json.tpl +1 -0
  216. package/templates/pipeline/package-scripts.json +1 -1
  217. package/templates/route/route.class-validator.ts.tpl +3 -3
  218. package/templates/route/route.none.ts.tpl +3 -3
  219. package/templates/route/route.search.ts.tpl +14 -0
  220. package/templates/route/route.standard-schema.ts.tpl +3 -3
  221. package/templates/route/route.typebox.ts.tpl +79 -0
  222. package/templates/route/route.zod.ts.tpl +3 -3
  223. package/templates/schema/schema.class-validator.ts.tpl +3 -0
  224. package/templates/schema/schema.custom.ts.tpl +14 -0
  225. package/templates/schema/schema.typebox.ts.tpl +8 -0
  226. package/templates/schema/schema.zod.ts.tpl +7 -0
@@ -23,7 +23,7 @@ const anyObject = customSchema<Record<string, unknown>>(
23
23
  const body = anyObject;
24
24
 
25
25
  {{/if}}
26
- {{#if route.returnsId}}
26
+ {{#if route.ownAccepted}}
27
27
  const accepted = customSchema<{ readonly id: string }>(
28
28
  (input) =>
29
29
  typeof input === 'object' && input !== null && 'id' in input && typeof input.id === 'string'
@@ -50,7 +50,7 @@ const answer = anyObject;
50
50
  body,
51
51
  {{/if}}
52
52
  {{#if route.returnsId}}
53
- response: { status: {{route.status}}, description: 'Done; the body carries the identifier.', body: accepted },
53
+ response: { status: {{route.status}}, description: 'Done; the body carries the identifier.', body: {{route.acceptedBody}} },
54
54
  {{/if}}
55
55
  {{#if route.returnsNothing}}
56
56
  response: { status: {{route.status}}, description: 'Done.' },
@@ -1,6 +1,9 @@
1
1
  {{#if validation.zod}}
2
2
  import { z } from 'zod';
3
3
  {{/if}}
4
+ {{#if validation.typebox}}
5
+ import Type from 'typebox';
6
+ {{/if}}
4
7
  import {
5
8
  Controller,
6
9
  pageQuerySchema,
@@ -23,6 +26,9 @@ import { zodSchema } from '@ddd-ts-kit/validation/zod';
23
26
  {{#if validation.classValidator}}
24
27
  import { classSchema } from '@ddd-ts-kit/validation/class-validator';
25
28
  {{/if}}
29
+ {{#if validation.typebox}}
30
+ import { typeboxSchema } from '@ddd-ts-kit/validation/typebox';
31
+ {{/if}}
26
32
  {{route.imports}}
27
33
 
28
34
  // TODO(domain): list here the fields of {{name.pascal}}Sort a client may sort by, and bound the size of a page.
@@ -40,6 +46,14 @@ const line = zodSchema(
40
46
  {{#if validation.classValidator}}
41
47
  const line = classSchema({{name.pascal}}Line);
42
48
  {{/if}}
49
+ {{#if validation.typebox}}
50
+ const line = typeboxSchema(
51
+ Type.Object({
52
+ id: Type.String(),
53
+ // TODO(domain): declare the fields of {{name.pascal}}Item that are part of the public contract.
54
+ }),
55
+ );
56
+ {{/if}}
43
57
  {{#if search.customSchema}}
44
58
  interface {{name.pascal}}Line {
45
59
  readonly id: string;
@@ -26,7 +26,7 @@ const anyObject: StandardSchemaV1<unknown, Record<string, unknown>> = {
26
26
  const body = standardSchema(anyObject, { type: 'object' });
27
27
 
28
28
  {{/if}}
29
- {{#if route.returnsId}}
29
+ {{#if route.ownAccepted}}
30
30
  const acceptedShape: StandardSchemaV1<unknown, { readonly id: string }> = {
31
31
  '~standard': {
32
32
  version: 1,
@@ -62,7 +62,7 @@ const answer = standardSchema(anyObject, { type: 'object' });
62
62
  body,
63
63
  {{/if}}
64
64
  {{#if route.returnsId}}
65
- response: { status: {{route.status}}, description: 'Done; the body carries the identifier.', body: accepted },
65
+ response: { status: {{route.status}}, description: 'Done; the body carries the identifier.', body: {{route.acceptedBody}} },
66
66
  {{/if}}
67
67
  {{#if route.returnsNothing}}
68
68
  response: { status: {{route.status}}, description: 'Done.' },
@@ -0,0 +1,98 @@
1
+ {{#if route.declaresSchema}}
2
+ import Type from 'typebox';
3
+ {{/if}}
4
+ import { {{route.httpImports}} } from '@ddd-ts-kit/http';
5
+ import type { {{route.busType}} } from '@ddd-ts-kit/kernel';
6
+ {{#if route.usesInfer}}
7
+ import type { Infer } from '@ddd-ts-kit/validation';
8
+ {{/if}}
9
+ {{#if route.declaresSchema}}
10
+ import { typeboxSchema } from '@ddd-ts-kit/validation/typebox';
11
+ {{/if}}
12
+ {{route.imports}}
13
+
14
+ {{#if route.hasParams}}
15
+ const params = typeboxSchema(Type.Object({ {{route.typeboxParams}} }));
16
+
17
+ {{/if}}
18
+ {{#if route.hasBody}}
19
+ const body = typeboxSchema(
20
+ Type.Object({
21
+ // TODO(domain): declare the fields the client sends for "{{name.words}}", with shape constraints only (types, formats, lengths); business rules stay in the domain.
22
+ }),
23
+ );
24
+
25
+ {{/if}}
26
+ {{#if route.ownAccepted}}
27
+ const accepted = typeboxSchema(Type.Object({ id: Type.String() }));
28
+
29
+ {{/if}}
30
+ {{#if route.isQuery}}
31
+ const answer = typeboxSchema(
32
+ Type.Object({
33
+ // TODO(domain): declare the fields of {{name.pascal}}Result that are part of the public contract.
34
+ }),
35
+ );
36
+
37
+ {{/if}}
38
+ @Controller({
39
+ {{route.access}}
40
+ method: '{{route.method}}',
41
+ path: '{{route.groupPath}}',
42
+ summary: '{{name.title}}',
43
+ {{#if route.hasTypedParams}}
44
+ params,
45
+ {{/if}}
46
+ {{#if route.hasBody}}
47
+ body,
48
+ {{/if}}
49
+ {{#if route.returnsId}}
50
+ response: { status: {{route.status}}, description: 'Done; the body carries the identifier.', body: {{route.acceptedBody}} },
51
+ {{/if}}
52
+ {{#if route.returnsNothing}}
53
+ response: { status: {{route.status}}, description: 'Done.' },
54
+ {{/if}}
55
+ {{#if route.isQuery}}
56
+ response: { status: {{route.status}}, description: 'The answer.', body: answer },
57
+ {{/if}}
58
+ // TODO(domain): list the codes of the refusals "{{name.words}}" can raise, as their factories set them, so that they appear in the contract with their status (`errors/known-code`).
59
+ errors: [],
60
+ })
61
+ export class {{name.pascal}}Controller {
62
+ constructor({{route.constructorParams}}) {}
63
+
64
+ {{#if route.isQuery}}
65
+ async handle({{route.requestParam}}): Promise<{{route.answerType}}> {
66
+ {{#unless route.mapper}}
67
+ // TODO(domain): build the query from `request` (params, query string); the actor is already there.
68
+ {{/unless}}
69
+ {{#if route.serializer}}
70
+ return this.serializer.serialize(await this.{{route.bus}}.execute({{route.message}}));
71
+ {{/if}}
72
+ {{#unless route.serializer}}
73
+ // TODO(domain): map the answer of the query to the public fields.
74
+ await this.{{route.bus}}.execute({{route.message}});
75
+
76
+ return {};
77
+ {{/unless}}
78
+ }
79
+ {{/if}}
80
+ {{#if route.returnsId}}
81
+ async handle({{route.requestParam}}): Promise<{{route.acceptedType}}> {
82
+ {{#unless route.mapper}}
83
+ // TODO(domain): build the command from `request` (body, params); the actor is already there.
84
+ {{/unless}}
85
+ const id = await this.{{route.bus}}.execute({{route.message}});
86
+
87
+ return { id };
88
+ }
89
+ {{/if}}
90
+ {{#if route.returnsNothing}}
91
+ async handle({{route.requestParam}}): Promise<void> {
92
+ {{#unless route.mapper}}
93
+ // TODO(domain): build the command from `request` (body, params); the actor is already there.
94
+ {{/unless}}
95
+ await this.{{route.bus}}.execute({{route.message}});
96
+ }
97
+ {{/if}}
98
+ }
@@ -23,7 +23,7 @@ const body = zodSchema(
23
23
  );
24
24
 
25
25
  {{/if}}
26
- {{#if route.returnsId}}
26
+ {{#if route.ownAccepted}}
27
27
  const accepted = zodSchema(z.object({ id: z.string() }), { io: 'output' });
28
28
 
29
29
  {{/if}}
@@ -48,7 +48,7 @@ const answer = zodSchema(
48
48
  body,
49
49
  {{/if}}
50
50
  {{#if route.returnsId}}
51
- response: { status: {{route.status}}, description: 'Done; the body carries the identifier.', body: accepted },
51
+ response: { status: {{route.status}}, description: 'Done; the body carries the identifier.', body: {{route.acceptedBody}} },
52
52
  {{/if}}
53
53
  {{#if route.returnsNothing}}
54
54
  response: { status: {{route.status}}, description: 'Done.' },
@@ -262,6 +262,21 @@
262
262
  "FORBIDDEN"
263
263
  ]
264
264
  },
265
+ {{#if presentation.negotiated}}
266
+ "406": {
267
+ "description": "Not Acceptable. Error codes: NOT_ACCEPTABLE.",
268
+ "content": {
269
+ "application/problem+json": {
270
+ "schema": {
271
+ "$ref": "#/components/schemas/Problem"
272
+ }
273
+ }
274
+ },
275
+ "x-error-codes": [
276
+ "NOT_ACCEPTABLE"
277
+ ]
278
+ },
279
+ {{/if}}
265
280
  "422": {
266
281
  "description": "Unprocessable Content. Error codes: VALIDATION_FAILED.",
267
282
  "content": {
@@ -8,7 +8,7 @@ A ddd-ts-kit project: one bounded context per directory of `{{config.contextsDir
8
8
  - `npx ddd-ts-kit check --changed-since <ref>`: the architecture rules on what changed; `npx ddd-ts-kit explain <rule-id>` gives the reason and an example.
9
9
  - `npx ddd-ts-kit list --json`: the map of the contexts. `npx ddd-ts-kit describe <context> --json`: one context as data (use cases, routes, events, ports, dependencies).
10
10
  - `npx ddd-ts-kit status`: the next step of the plan, the slices by status, the markers left per context. `npx ddd-ts-kit todo [<context>]`: each remaining `TODO(domain)` and `it.todo` as `file:line`.
11
- - `npx ddd-ts-kit docs`: regenerates the context map and the generated sections of each `CONTEXT.md`; `npm run docs:generate` the OpenAPI contract and the event catalog.
11
+ - `npx ddd-ts-kit docs`: regenerates the context map and the generated sections of each `CONTEXT.md`; `npm run docs:generate` the OpenAPI contract, the forms and the event catalog.
12
12
 
13
13
  ## Read less
14
14
 
@@ -29,7 +29,7 @@ Never edit `<context>.declarations.ts`: `generate`, `docs` and `npm run dev` rew
29
29
  {{#if assembly.discovery}}
30
30
  Never edit `<context>.dependencies.ts`: `generate`, `docs` and `npm run dev` rewrite it from the constructor signatures (it registers what each constructor is injected). After changing a constructor by hand, `npx ddd-ts-kit docs`.
31
31
  {{/if}}
32
- Never edit the contracts under `openapi/` (`openapi/openapi.yaml`, one file per document when `openapi.documents` splits it), `docs/integration/asyncapi.yaml`, `docs/integration/event-catalog.md`, `{{config.platformDir}}/http/openapi-documents.ts`, `.dependency-cruiser.cjs` or the generated sections of a `CONTEXT.md`: change the code or the config, then `npm run docs:generate`, `npx ddd-ts-kit docs` and `npx ddd-ts-kit depcruise-config --write`. `stored-events.lock.json` is written by `npx ddd-ts-kit docs --stored-events` once the shape of a version is final (`docs --check` fails while one is unrecorded), never by hand: a stored shape changes only by a new version with its upcaster (`event-sourcing/upcaster-required`).
32
+ Never edit the contracts under `openapi/` (`openapi/openapi.yaml`, one file per document when `openapi.documents` splits it, the forms of `openapi/forms/`), `docs/integration/asyncapi.yaml`, `docs/integration/event-catalog.md`, `{{config.platformDir}}/http/openapi-documents.ts`, `.dependency-cruiser.cjs` or the generated sections of a `CONTEXT.md`: change the code or the config, then `npm run docs:generate`, `npx ddd-ts-kit docs` and `npx ddd-ts-kit depcruise-config --write`. `stored-events.lock.json` is written by `npx ddd-ts-kit docs --stored-events` once the shape of a version is final (`docs --check` fails while one is unrecorded), never by hand: a stored shape changes only by a new version with its upcaster (`event-sourcing/upcaster-required`).
33
33
  {{#if sdk.on}}
34
34
  {{sdk.instruction}}
35
35
  {{/if}}
@@ -45,9 +45,12 @@ import { multipartUploads } from '@ddd-ts-kit/http/uploads';
45
45
  {{/if}}
46
46
  import { tracedRoutes } from '@ddd-ts-kit/tracing';
47
47
  import { principalOf } from '{{alias.platform}}/auth/principal-of{{alias.ext}}';
48
+ {{#if presentation.any}}
49
+ import type { Presentation } from '{{alias.platform}}/http/presentation-of{{alias.ext}}';
50
+ {{/if}}
48
51
 
49
52
  /** What the HTTP adapter needs at the edge of the application: spread into its mount options, for every route. */
50
- export interface HttpBoundary {
53
+ export interface HttpBoundary{{#if presentation.any}} extends Presentation{{/if}} {
51
54
  readonly resolvePrincipal: (raw: unknown) => Promise<Session>;
52
55
  /** Every handler runs inside it, with the correlation id, the actor and the session of its request. */
53
56
  readonly executionScope: ExecutionScope;
@@ -92,21 +95,29 @@ interface Edge {
92
95
  /** Where a request answered 5xx is reported, unless a bus already reported its failure. */
93
96
  readonly errors: ErrorReporter;
94
97
  {{/if}}
98
+ {{#if presentation.any}}
99
+ readonly presentation: Presentation;
100
+ {{/if}}
95
101
  }
96
102
 
97
103
  /** A request is authenticated once, then handled inside the execution scope, so infrastructure sees who acts and under which correlation. */
98
104
  {{#if errors.none}}
99
- {{#unless audit.on}}
105
+ {{#unless presentation.widensEdge}}
100
106
  export function httpBoundary({ authenticator, executionScope, errorMapper, clock, logger }: Edge): HttpBoundary {
101
107
  {{/unless}}
102
- {{#if audit.on}}
108
+ {{#if presentation.widensEdge}}
103
109
  export function httpBoundary({
104
110
  authenticator,
105
111
  executionScope,
106
112
  errorMapper,
107
113
  clock,
108
114
  logger,
115
+ {{#if audit.on}}
109
116
  refusals,
117
+ {{/if}}
118
+ {{#if presentation.any}}
119
+ presentation,
120
+ {{/if}}
110
121
  }: Edge): HttpBoundary {
111
122
  {{/if}}
112
123
  {{/if}}
@@ -121,11 +132,17 @@ export function httpBoundary({
121
132
  refusals,
122
133
  {{/if}}
123
134
  errors,
135
+ {{#if presentation.any}}
136
+ presentation,
137
+ {{/if}}
124
138
  }: Edge): HttpBoundary {
125
139
  const report = reportHttpErrors(errors);
126
140
 
127
141
  {{/if}}
128
142
  return {
143
+ {{#if presentation.any}}
144
+ ...presentation,
145
+ {{/if}}
129
146
  resolvePrincipal: principalOf(authenticator),
130
147
  executionScope,
131
148
  errorMapper,
@@ -26,6 +26,12 @@ import { auditModule } from '{{alias.platform}}/audit/audit.module{{alias.ext}}'
26
26
  {{#if http.any}}
27
27
  import { httpBoundary, type HttpBoundary } from '{{alias.platform}}/auth/http-boundary{{alias.ext}}';
28
28
  {{/if}}
29
+ {{#if presentation.any}}
30
+ import { presentationOf } from '{{alias.platform}}/http/presentation-of{{alias.ext}}';
31
+ {{/if}}
32
+ {{#if presentation.forms}}
33
+ import { withForms } from '{{alias.platform}}/http/with-forms{{alias.ext}}';
34
+ {{/if}}
29
35
  {{#if authz.rebac}}
30
36
  import { relateSystemActors } from '{{alias.platform}}/auth/relate-system-actors{{alias.ext}}';
31
37
  {{/if}}
@@ -123,10 +129,19 @@ export function composeApplication(
123
129
  {{/unless}}
124
130
 
125
131
  platform.protection.declare(contexts.flatMap((context) => context.useCases));
132
+ {{#if presentation.any}}
133
+ // What the server answers in and which fields each caller sees: the forms and the contract are given the same.
134
+ const presentation = presentationOf(platform.access, clock);
135
+ {{/if}}
126
136
 
127
137
  return {
128
138
  {{#if http.any}}
139
+ {{#if presentation.forms}}
140
+ routes: withForms(defineRoutes(...contexts.map((context) => context.routes)), presentation),
141
+ {{/if}}
142
+ {{#unless presentation.forms}}
129
143
  routes: defineRoutes(...contexts.map((context) => context.routes)),
144
+ {{/unless}}
130
145
  boundary: httpBoundary({
131
146
  authenticator: options.authenticator,
132
147
  executionScope: platform.scope,
@@ -138,6 +153,9 @@ export function composeApplication(
138
153
  {{/if}}
139
154
  {{#if errors.sentry}}
140
155
  errors: platform.errors,
156
+ {{/if}}
157
+ {{#if presentation.any}}
158
+ presentation,
141
159
  {{/if}}
142
160
  }),
143
161
  {{/if}}
@@ -1,6 +1,11 @@
1
1
  import { installAspectRuntime } from '@ddd-ts-kit/aspects';
2
2
  {{#if http.any}}
3
+ {{#if presentation.any}}
4
+ import { Access, Authenticator } from '@ddd-ts-kit/auth';
5
+ {{/if}}
6
+ {{#unless presentation.any}}
3
7
  import { Authenticator } from '@ddd-ts-kit/auth';
8
+ {{/unless}}
4
9
  {{/if}}
5
10
  import {
6
11
  {{#if routes.declaredControllers}}
@@ -94,6 +99,12 @@ import { securitySchemes } from '{{alias.platform}}/auth/security-schemes{{alias
94
99
  {{#if http.any}}
95
100
  import { httpBoundary, type HttpBoundary } from '{{alias.platform}}/auth/http-boundary{{alias.ext}}';
96
101
  {{/if}}
102
+ {{#if presentation.any}}
103
+ import { presentationOf } from '{{alias.platform}}/http/presentation-of{{alias.ext}}';
104
+ {{/if}}
105
+ {{#if presentation.forms}}
106
+ import { withForms } from '{{alias.platform}}/http/with-forms{{alias.ext}}';
107
+ {{/if}}
97
108
  {{#if authz.rebac}}
98
109
  import { relateSystemActors } from '{{alias.platform}}/auth/relate-system-actors{{alias.ext}}';
99
110
  {{/if}}
@@ -271,10 +282,19 @@ function kernel(foundations: Foundations, persistence: Persistence, options: App
271
282
 
272
283
  function running(container: Container, options: ApplicationOptions, contexts: ContextSelection): Application {
273
284
  const logger = container.resolve(Logger);
285
+ {{#if presentation.any}}
286
+ // What the server answers in and which fields each caller sees: the forms and the contract are given the same.
287
+ const presentation = presentationOf(container.resolve(Access), container.resolve(Clock));
288
+ {{/if}}
274
289
 
275
290
  return {
276
291
  {{#if http.any}}
292
+ {{#if presentation.forms}}
293
+ routes: withForms(routesOf(container, contexts), presentation),
294
+ {{/if}}
295
+ {{#unless presentation.forms}}
277
296
  routes: routesOf(container, contexts),
297
+ {{/unless}}
278
298
  boundary: httpBoundary({
279
299
  authenticator: container.resolve(Authenticator),
280
300
  executionScope: container.resolve(ExecutionScope),
@@ -289,6 +309,9 @@ function running(container: Container, options: ApplicationOptions, contexts: Co
289
309
  {{/if}}
290
310
  {{#if errors.sentry}}
291
311
  errors: container.resolve(ErrorReporter),
312
+ {{/if}}
313
+ {{#if presentation.any}}
314
+ presentation,
292
315
  {{/if}}
293
316
  }),
294
317
  {{/if}}
@@ -0,0 +1,27 @@
1
+ import type { Access } from '@ddd-ts-kit/auth';
2
+ {{#if presentation.negotiated}}
3
+ import { Representations, fieldDeciderOf, type FieldDecider } from '@ddd-ts-kit/http';
4
+ {{/if}}
5
+ {{#unless presentation.negotiated}}
6
+ import { fieldDeciderOf, type FieldDecider } from '@ddd-ts-kit/http';
7
+ {{/unless}}
8
+ import type { Clock } from '@ddd-ts-kit/kernel';
9
+
10
+ /** How the answers of the application are presented, the same for the server, the forms and the contract. */
11
+ export interface Presentation {
12
+ {{#if presentation.negotiated}}
13
+ /** JSON, XML and CSV: each request is answered in the one its `Accept` header prefers among those its route declares in `produces`. */
14
+ readonly representations: Representations;
15
+ {{/if}}
16
+ /** Decides the fields a schema reserves to some callers (`withFieldAccess`), through the policies and the authorizer of the application. */
17
+ readonly fieldAccess: FieldDecider;
18
+ }
19
+
20
+ export function presentationOf(access: Access, clock: Clock): Presentation {
21
+ return {
22
+ {{#if presentation.negotiated}}
23
+ representations: Representations.standard(),
24
+ {{/if}}
25
+ fieldAccess: fieldDeciderOf({ access, clock }),
26
+ };
27
+ }
@@ -34,7 +34,7 @@ export async function startHttpServer(served: Served, config: AppConfig): Promis
34
34
  {{/if}}
35
35
  app.use(express.json());
36
36
  {{#if docs.any}}
37
- mountExpress(app, withDocumentation(served.routes, config), served.boundary);
37
+ mountExpress(app, withDocumentation(served.routes, config, served.boundary), served.boundary);
38
38
  {{/if}}
39
39
  {{#unless docs.any}}
40
40
  mountExpress(app, served.routes, served.boundary);
@@ -29,7 +29,7 @@ export async function startHttpServer(served: Served, config: AppConfig): Promis
29
29
  mountFastify(app, served.fileRoutes, served.boundary);
30
30
  {{/if}}
31
31
  {{#if docs.any}}
32
- mountFastify(app, withDocumentation(served.routes, config), served.boundary);
32
+ mountFastify(app, withDocumentation(served.routes, config, served.boundary), served.boundary);
33
33
  {{/if}}
34
34
  {{#unless docs.any}}
35
35
  mountFastify(app, served.routes, served.boundary);
@@ -0,0 +1,84 @@
1
+ import type { AddressInfo } from 'node:net';
2
+ import { serve, type ServerType } from '@hono/node-server';
3
+ {{#if cache.rateLimit}}
4
+ import { getConnInfo } from '@hono/node-server/conninfo';
5
+ {{/if}}
6
+ import { Hono } from 'hono';
7
+ import { mountHono } from '@ddd-ts-kit/http/hono';
8
+ import type { Application } from '{{alias.platform}}/compose-application{{alias.ext}}';
9
+ import type { AppConfig } from '{{alias.platform}}/config/app-config{{alias.ext}}';
10
+ {{#if cache.rateLimit}}
11
+ import { refusalFor } from '{{alias.platform}}/http/refusal-for{{alias.ext}}';
12
+ {{/if}}
13
+ {{#if docs.any}}
14
+ import { withDocumentation } from '{{alias.platform}}/http/with-documentation{{alias.ext}}';
15
+ {{/if}}
16
+
17
+ export interface HttpServer {
18
+ /** The port really bound: differs from the configured one when that was 0. */
19
+ readonly port: number;
20
+
21
+ close(): Promise<void>;
22
+ }
23
+
24
+ /** What the server needs of the application: its routes and what stands at its edge. */
25
+ type Served = Pick<Application, 'routes' | 'boundary'{{#if cache.rateLimit}} | 'rateLimiter' | 'ready'{{/if}}>;
26
+
27
+ export async function startHttpServer(served: Served, config: AppConfig): Promise<HttpServer> {
28
+ const app = new Hono();
29
+
30
+ {{#if cache.rateLimit}}
31
+ guardTheDoor(app, served, config);
32
+ {{/if}}
33
+ {{#if docs.any}}
34
+ mountHono(app, withDocumentation(served.routes, config, served.boundary), served.boundary);
35
+ {{/if}}
36
+ {{#unless docs.any}}
37
+ mountHono(app, served.routes, served.boundary);
38
+ {{/unless}}
39
+
40
+ const server = await listen(app, config.port);
41
+
42
+ return { port: boundPort(server), close: () => close(server) };
43
+ }
44
+ {{#if cache.rateLimit}}
45
+
46
+ /** The readiness probe, then the limiter, before any route. */
47
+ function guardTheDoor(app: Hono, served: Served, config: AppConfig): void {
48
+ app.get('/health/ready', async (context) => {
49
+ const ready = await served.ready();
50
+
51
+ return context.json({ status: ready ? 'up' : 'down' }, ready ? 200 : 503);
52
+ });
53
+
54
+ app.use(async (context, next) => {
55
+ const refusal = await refusalFor(served.rateLimiter, config.rateLimit, getConnInfo(context).remote.address);
56
+
57
+ if (refusal === undefined) {
58
+ await next();
59
+
60
+ return undefined;
61
+ }
62
+
63
+ return new Response(JSON.stringify(refusal.problem), { status: refusal.problem.status, headers: refusal.headers });
64
+ });
65
+ }
66
+ {{/if}}
67
+
68
+ function listen(app: Hono, port: number): Promise<ServerType> {
69
+ return new Promise<ServerType>((resolve) => {
70
+ const server = serve({ fetch: app.fetch, port }, () => resolve(server));
71
+ });
72
+ }
73
+
74
+ function boundPort(server: ServerType): number {
75
+ const address: AddressInfo | string | null = server.address();
76
+
77
+ return typeof address === 'object' && address !== null ? address.port : 0;
78
+ }
79
+
80
+ function close(server: ServerType): Promise<void> {
81
+ return new Promise<void>((resolve, reject) => {
82
+ server.close((error) => (error === undefined ? resolve() : reject(error)));
83
+ });
84
+ }
@@ -38,7 +38,7 @@ interface Outgoing {
38
38
  export async function startHttpServer(served: Served, config: AppConfig): Promise<HttpServer> {
39
39
  {{#if files.http}}
40
40
  {{#if docs.any}}
41
- const routes = [...withDocumentation(served.routes, config), ...served.fileRoutes];
41
+ const routes = [...withDocumentation(served.routes, config, served.boundary), ...served.fileRoutes];
42
42
  {{/if}}
43
43
  {{#unless docs.any}}
44
44
  const routes = [...served.routes, ...served.fileRoutes];
@@ -47,7 +47,7 @@ export async function startHttpServer(served: Served, config: AppConfig): Promis
47
47
  {{/if}}
48
48
  {{#unless files.http}}
49
49
  {{#if docs.any}}
50
- const app = await NestFactory.create(createAppModule(withDocumentation(served.routes, config), served.boundary), {
50
+ const app = await NestFactory.create(createAppModule(withDocumentation(served.routes, config, served.boundary), served.boundary), {
51
51
  logger: ['error', 'warn'],
52
52
  });
53
53
  {{/if}}
@@ -1,12 +1,20 @@
1
- import type { Route } from '@ddd-ts-kit/http';
1
+ import type { ExecuteOptions, Route } from '@ddd-ts-kit/http';
2
2
  import { openApiDocumentsRoutes } from '@ddd-ts-kit/http/docs';
3
3
  import { generateOpenApi, routesByDocument } from '@ddd-ts-kit/http/openapi';
4
4
  import { securitySchemes } from '{{alias.platform}}/auth/security-schemes{{alias.ext}}';
5
5
  import type { AppConfig } from '{{alias.platform}}/config/app-config{{alias.ext}}';
6
6
  import { OPENAPI_DOCUMENTS } from '{{alias.platform}}/http/openapi-documents{{alias.ext}}';
7
7
 
8
- /** Adds the contract and its interactive page next to the routes, one per document of a split contract, unless documentation is switched off (API_DOCS, off in production by default). */
9
- export function withDocumentation(routes: readonly Route[], config: AppConfig): readonly Route[] {
8
+ /**
9
+ * Adds the contract and its interactive page next to the routes, one per document of a split
10
+ * contract, unless documentation is switched off (API_DOCS, off in production by default). The
11
+ * contract is written with the error mapper and the representations the routes are mounted with.
12
+ */
13
+ export function withDocumentation(
14
+ routes: readonly Route[],
15
+ config: AppConfig,
16
+ boundary: Pick<ExecuteOptions, 'errorMapper' | 'representations'>,
17
+ ): readonly Route[] {
10
18
  if (!config.docsEnabled) {
11
19
  return routes;
12
20
  }
@@ -17,6 +25,8 @@ export function withDocumentation(routes: readonly Route[], config: AppConfig):
17
25
  info: { title: title ?? config.serviceName, version: config.serviceVersion },
18
26
  routes: selected,
19
27
  securitySchemes,
28
+ ...(boundary.errorMapper === undefined ? {} : { errorMapper: boundary.errorMapper }),
29
+ ...(boundary.representations === undefined ? {} : { representations: boundary.representations }),
20
30
  }),
21
31
  }));
22
32
 
@@ -0,0 +1,7 @@
1
+ import { defineRoutes, formRoutes, type Route } from '@ddd-ts-kit/http';
2
+ import type { Presentation } from '{{alias.platform}}/http/presentation-of{{alias.ext}}';
3
+
4
+ /** The routes, and the form of each one that declares `form`, at `GET /forms/<operationId>`, for the caller who asks. */
5
+ export function withForms(routes: readonly Route[], { fieldAccess }: Presentation): readonly Route[] {
6
+ return defineRoutes(routes, formRoutes(routes, { fieldAccess }));
7
+ }
@@ -30,7 +30,7 @@ paths:
30
30
  {{#if files.any}}
31
31
  - A file arrives in the command as a `FileUpload` of its area, which the bus stages before the transaction under a key of `FileKeys` (never a name or a path from the user) and scans; the handler records `upload.file`, its `StoredFile`, on the aggregate and calls `FileStaging.promoteOnCommit`, which moves it to its key with the commit. A direct upload is confirmed from the `PendingUpload` the aggregate kept, never from a key the client sends. The ports are named here, never a connector (`files/port-not-connector`); `npx ddd-ts-kit generate file-upload` writes the slice.
32
32
  {{/if}}
33
- - A query never saves, deletes or publishes (`application/command-query-separation`); it hands the access scope of the actor to its read port and never filters after loading.
33
+ - A query never saves, deletes or publishes (`application/command-query-separation`); it hands the access scope of the actor to its read port and never filters after loading, and answers every field its read model holds: a field reserved to some callers is marked on the response schema with `withFieldAccess` of `@ddd-ts-kit/http`, never removed by a test on the roles of the actor (`authz/fields-by-schema`).
34
34
  - A read that takes criteria, a scope or a page is a search, never a list (`naming/search-not-list`): `Search<Things>Query` in `queries/search-<things>/` (`generate query ... --search`), `search...` on the port, its adapters and its fakes; one element or none stays `findById` or `findBy...`.
35
35
  {{#if literals.named}}
36
36
  - A permission, a role or a status is read from its declaration, `billingPermissions['invoice.issue']`, `InvoiceStatus.Issued`, never written as a string (`language/declared-vocabulary`); a number has a name (`clean/no-magic-number`).
@@ -27,8 +27,11 @@ paths:
27
27
  {{#if refusals.classes}}
28
28
  - Every route says who may call it (its group or its guards for a controller, `security` for a function) and the `DomainError` codes it can answer in `errors`; the mapping from code to status lives in one place, the error mapper.
29
29
  {{/if}}
30
- - No role or ownership test in a controller or a route (`authz/in-application`): a guard of the group asks for a role at the edge, the bus asks for the permission, a policy decides on loaded state.
30
+ - No role or ownership test in a controller or a route (`authz/in-application`): a guard of the group asks for a role at the edge, the bus asks for the permission, a policy decides on loaded state. A field reserved to some callers is marked on its schema, `withFieldAccess(schema, { field: { read, write } })`, never by an `if` on a role (`authz/fields-by-schema`).{{#if presentation.negotiated}} Other media types are `produces` and `consumes` of the declaration (`generate representation`), from the same schema. A streamed export is `everyPage` over the search query of one page, sent through the bus page by page, never a query handler that answers a lazy list.{{/if}}{{#if presentation.forms}} A form is `defineForm` hints next to the body (`generate form`): shape and presentation, never a business rule; it `requires` the permission of its use case, asked as its `@Requires` asks it.{{/if}}
31
31
  - Request and response types are presentation types, one class or schema per file next to the controller (`*.body.ts`, `*.params.ts`, `*.answer.ts`...; `*.dto.ts` for function routes and under `shared/`): never an aggregate, never a row.
32
+ {{#if schemas.shared}}
33
+ - A shape several routes use is declared once, in `presentation/http/shared/<name>.schema.ts` (`npx ddd-ts-kit generate schema <context> <Name>`), or in `presentation/` of the shared kernel when several contexts answer with it (`generate schema shared-kernel <Name>`), and shared: the contract documents it once and every route refers to it. A route takes the part it needs from it (pick, omit, partial, extend) instead of copying it, and answers an identifier with `acceptedSchema` of `@ddd-ts-kit/http` (`schema/no-duplicate-shape`). Only presentation layers import a schema (`schema/presentation-only`).
34
+ {{/if}}
32
35
  - A read declares how caches may keep its answer in `cache` (its controller, its route group, or the spec of a function route): `private` for what a caller reads again, with `etag`; `public` only for what is the same for everybody, with `tags` naming the resources it shows. A route that knows its caller is `no-store` unless it says otherwise, and a `public` answer of such a route is refused unless `vary` names who calls (`Authorization`, `Cookie`) or the header its tenant is read from (`presentation/shared-cache-varies`, then the mount): never cache a decision of authorization across actors. A group policy reaches the reads of the group only. A write that must not overwrite a newer version declares `ifMatch` and hands `context.expectedVersion` to its command; the handler calls `expectVersion` (412). A change purges the answers it makes stale from a `before-commit` handler of its domain event, `CacheInvalidation.invalidate([cacheTag(...)])` (`platform.cacheInvalidation` on a platform wired by hand): the purge then runs after the commit, through the outbox. Every event that changes what a kept answer shows, its version included, purges it, not only the one the route is about.
33
36
  - Never put the message of an unexpected failure in a response or a log.
34
37
  - Files: the canonical tree of `presentation/http/` (`naming/suffix`, `tree/closed`): a folder per controller, `*.routes.ts` for the groups, `guards/`, `shared/`; function routes stay flat (`*.route.ts`, `*.dto.ts`).