hexok 1.0.1 → 2.1.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 (262) hide show
  1. package/README.md +9 -22
  2. package/dist/adapter.d.ts +18 -0
  3. package/dist/adapter.d.ts.map +1 -0
  4. package/dist/adapter.js +21 -0
  5. package/dist/adapter.js.map +1 -0
  6. package/dist/catalog.d.ts +88 -0
  7. package/dist/catalog.d.ts.map +1 -0
  8. package/dist/catalog.js +79 -0
  9. package/dist/catalog.js.map +1 -0
  10. package/dist/{core/coded-error.d.ts → coded-error.d.ts} +9 -4
  11. package/dist/coded-error.d.ts.map +1 -0
  12. package/dist/{core/coded-error.js → coded-error.js} +9 -4
  13. package/dist/coded-error.js.map +1 -0
  14. package/dist/cow-draft.d.ts.map +1 -0
  15. package/dist/cow-draft.js.map +1 -0
  16. package/dist/entity.d.ts +63 -0
  17. package/dist/entity.d.ts.map +1 -0
  18. package/dist/{domain/entity.js → entity.js} +52 -80
  19. package/dist/entity.js.map +1 -0
  20. package/dist/error-map.d.ts +22 -0
  21. package/dist/error-map.d.ts.map +1 -0
  22. package/dist/error-map.js +2 -0
  23. package/dist/error-map.js.map +1 -0
  24. package/dist/errors.d.ts +85 -0
  25. package/dist/errors.d.ts.map +1 -0
  26. package/dist/errors.js +85 -0
  27. package/dist/errors.js.map +1 -0
  28. package/dist/event-handler.d.ts +31 -0
  29. package/dist/event-handler.d.ts.map +1 -0
  30. package/dist/event-handler.js +28 -0
  31. package/dist/event-handler.js.map +1 -0
  32. package/dist/event.d.ts +37 -0
  33. package/dist/event.d.ts.map +1 -0
  34. package/dist/event.js +44 -0
  35. package/dist/event.js.map +1 -0
  36. package/dist/index.d.ts +16 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +14 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/mapper.d.ts +100 -0
  41. package/dist/mapper.d.ts.map +1 -0
  42. package/dist/mapper.js +167 -0
  43. package/dist/mapper.js.map +1 -0
  44. package/dist/port.d.ts +20 -0
  45. package/dist/port.d.ts.map +1 -0
  46. package/dist/port.js +25 -0
  47. package/dist/port.js.map +1 -0
  48. package/dist/result.d.ts.map +1 -0
  49. package/dist/result.js.map +1 -0
  50. package/dist/schema.d.ts +37 -0
  51. package/dist/schema.d.ts.map +1 -0
  52. package/dist/schema.js +50 -0
  53. package/dist/schema.js.map +1 -0
  54. package/dist/{core/standard-schema.d.ts → standard-schema.d.ts} +1 -1
  55. package/dist/standard-schema.d.ts.map +1 -0
  56. package/dist/standard-schema.js.map +1 -0
  57. package/dist/use-case.d.ts +183 -0
  58. package/dist/use-case.d.ts.map +1 -0
  59. package/dist/use-case.js +221 -0
  60. package/dist/use-case.js.map +1 -0
  61. package/dist/{core/schema.d.ts → validate.d.ts} +1 -1
  62. package/dist/validate.d.ts.map +1 -0
  63. package/dist/{core/schema.js → validate.js} +1 -1
  64. package/dist/validate.js.map +1 -0
  65. package/package.json +7 -26
  66. package/skills/hexok/SKILL.md +127 -0
  67. package/skills/hexok/references/adapter.md +66 -0
  68. package/skills/hexok/references/entity.md +74 -0
  69. package/skills/hexok/references/error.md +140 -0
  70. package/skills/hexok/references/event-catalog.md +57 -0
  71. package/skills/hexok/references/event-handler.md +56 -0
  72. package/skills/hexok/references/event.md +53 -0
  73. package/skills/hexok/references/mapper.md +77 -0
  74. package/skills/hexok/references/port.md +58 -0
  75. package/skills/hexok/references/schema.md +47 -0
  76. package/skills/hexok/references/use-case.md +164 -0
  77. package/dist/app/callable-use-case.d.ts +0 -34
  78. package/dist/app/callable-use-case.d.ts.map +0 -1
  79. package/dist/app/callable-use-case.js +0 -41
  80. package/dist/app/callable-use-case.js.map +0 -1
  81. package/dist/app/contract.d.ts +0 -46
  82. package/dist/app/contract.d.ts.map +0 -1
  83. package/dist/app/contract.js +0 -28
  84. package/dist/app/contract.js.map +0 -1
  85. package/dist/app/error-factory.d.ts +0 -9
  86. package/dist/app/error-factory.d.ts.map +0 -1
  87. package/dist/app/error-factory.js +0 -30
  88. package/dist/app/error-factory.js.map +0 -1
  89. package/dist/app/event-channel.d.ts +0 -133
  90. package/dist/app/event-channel.d.ts.map +0 -1
  91. package/dist/app/event-channel.js +0 -49
  92. package/dist/app/event-channel.js.map +0 -1
  93. package/dist/app/event-use-case.d.ts +0 -47
  94. package/dist/app/event-use-case.d.ts.map +0 -1
  95. package/dist/app/event-use-case.js +0 -54
  96. package/dist/app/event-use-case.js.map +0 -1
  97. package/dist/app/execute-ctx.d.ts +0 -92
  98. package/dist/app/execute-ctx.d.ts.map +0 -1
  99. package/dist/app/execute-ctx.js +0 -2
  100. package/dist/app/execute-ctx.js.map +0 -1
  101. package/dist/app/external-use-case.d.ts +0 -25
  102. package/dist/app/external-use-case.d.ts.map +0 -1
  103. package/dist/app/external-use-case.js +0 -25
  104. package/dist/app/external-use-case.js.map +0 -1
  105. package/dist/app/index.d.ts +0 -9
  106. package/dist/app/index.d.ts.map +0 -1
  107. package/dist/app/index.js +0 -8
  108. package/dist/app/index.js.map +0 -1
  109. package/dist/app/internal-use-case.d.ts +0 -26
  110. package/dist/app/internal-use-case.d.ts.map +0 -1
  111. package/dist/app/internal-use-case.js +0 -26
  112. package/dist/app/internal-use-case.js.map +0 -1
  113. package/dist/app/nest.d.ts +0 -9
  114. package/dist/app/nest.d.ts.map +0 -1
  115. package/dist/app/nest.js +0 -28
  116. package/dist/app/nest.js.map +0 -1
  117. package/dist/app/types.d.ts +0 -153
  118. package/dist/app/types.d.ts.map +0 -1
  119. package/dist/app/types.js +0 -17
  120. package/dist/app/types.js.map +0 -1
  121. package/dist/core/coded-error.d.ts.map +0 -1
  122. package/dist/core/coded-error.js.map +0 -1
  123. package/dist/core/error-map.d.ts +0 -21
  124. package/dist/core/error-map.d.ts.map +0 -1
  125. package/dist/core/error-map.js +0 -5
  126. package/dist/core/error-map.js.map +0 -1
  127. package/dist/core/index.d.ts +0 -6
  128. package/dist/core/index.d.ts.map +0 -1
  129. package/dist/core/index.js +0 -5
  130. package/dist/core/index.js.map +0 -1
  131. package/dist/core/result.d.ts.map +0 -1
  132. package/dist/core/result.js.map +0 -1
  133. package/dist/core/schema.d.ts.map +0 -1
  134. package/dist/core/schema.js.map +0 -1
  135. package/dist/core/standard-schema.d.ts.map +0 -1
  136. package/dist/core/standard-schema.js.map +0 -1
  137. package/dist/domain/capabilities.d.ts +0 -42
  138. package/dist/domain/capabilities.d.ts.map +0 -1
  139. package/dist/domain/capabilities.js +0 -2
  140. package/dist/domain/capabilities.js.map +0 -1
  141. package/dist/domain/catalog.d.ts +0 -46
  142. package/dist/domain/catalog.d.ts.map +0 -1
  143. package/dist/domain/catalog.js +0 -114
  144. package/dist/domain/catalog.js.map +0 -1
  145. package/dist/domain/channel.d.ts +0 -36
  146. package/dist/domain/channel.d.ts.map +0 -1
  147. package/dist/domain/channel.js +0 -2
  148. package/dist/domain/channel.js.map +0 -1
  149. package/dist/domain/cow-draft.d.ts.map +0 -1
  150. package/dist/domain/cow-draft.js.map +0 -1
  151. package/dist/domain/entity.d.ts +0 -120
  152. package/dist/domain/entity.d.ts.map +0 -1
  153. package/dist/domain/entity.js.map +0 -1
  154. package/dist/domain/envelope.d.ts +0 -59
  155. package/dist/domain/envelope.d.ts.map +0 -1
  156. package/dist/domain/envelope.js +0 -2
  157. package/dist/domain/envelope.js.map +0 -1
  158. package/dist/domain/event.d.ts +0 -44
  159. package/dist/domain/event.d.ts.map +0 -1
  160. package/dist/domain/event.js +0 -21
  161. package/dist/domain/event.js.map +0 -1
  162. package/dist/domain/index.d.ts +0 -8
  163. package/dist/domain/index.d.ts.map +0 -1
  164. package/dist/domain/index.js +0 -5
  165. package/dist/domain/index.js.map +0 -1
  166. package/dist/domain/port.d.ts +0 -45
  167. package/dist/domain/port.d.ts.map +0 -1
  168. package/dist/domain/port.js +0 -41
  169. package/dist/domain/port.js.map +0 -1
  170. package/dist/infra/adapter.d.ts +0 -22
  171. package/dist/infra/adapter.d.ts.map +0 -1
  172. package/dist/infra/adapter.js +0 -18
  173. package/dist/infra/adapter.js.map +0 -1
  174. package/dist/infra/index.d.ts +0 -3
  175. package/dist/infra/index.d.ts.map +0 -1
  176. package/dist/infra/index.js +0 -3
  177. package/dist/infra/index.js.map +0 -1
  178. package/dist/infra/mapper.d.ts +0 -48
  179. package/dist/infra/mapper.d.ts.map +0 -1
  180. package/dist/infra/mapper.js +0 -77
  181. package/dist/infra/mapper.js.map +0 -1
  182. package/dist/runtime/app.d.ts +0 -100
  183. package/dist/runtime/app.d.ts.map +0 -1
  184. package/dist/runtime/app.js +0 -292
  185. package/dist/runtime/app.js.map +0 -1
  186. package/dist/runtime/channel.d.ts +0 -34
  187. package/dist/runtime/channel.d.ts.map +0 -1
  188. package/dist/runtime/channel.js +0 -110
  189. package/dist/runtime/channel.js.map +0 -1
  190. package/dist/runtime/client.d.ts +0 -21
  191. package/dist/runtime/client.d.ts.map +0 -1
  192. package/dist/runtime/client.js +0 -2
  193. package/dist/runtime/client.js.map +0 -1
  194. package/dist/runtime/completeness.d.ts +0 -89
  195. package/dist/runtime/completeness.d.ts.map +0 -1
  196. package/dist/runtime/completeness.js +0 -2
  197. package/dist/runtime/completeness.js.map +0 -1
  198. package/dist/runtime/envelope.d.ts +0 -7
  199. package/dist/runtime/envelope.d.ts.map +0 -1
  200. package/dist/runtime/envelope.js +0 -69
  201. package/dist/runtime/envelope.js.map +0 -1
  202. package/dist/runtime/events.d.ts +0 -6
  203. package/dist/runtime/events.d.ts.map +0 -1
  204. package/dist/runtime/events.js +0 -48
  205. package/dist/runtime/events.js.map +0 -1
  206. package/dist/runtime/http-status.d.ts +0 -3
  207. package/dist/runtime/http-status.d.ts.map +0 -1
  208. package/dist/runtime/http-status.js +0 -11
  209. package/dist/runtime/http-status.js.map +0 -1
  210. package/dist/runtime/http.d.ts +0 -4
  211. package/dist/runtime/http.d.ts.map +0 -1
  212. package/dist/runtime/http.js +0 -58
  213. package/dist/runtime/http.js.map +0 -1
  214. package/dist/runtime/index.d.ts +0 -10
  215. package/dist/runtime/index.d.ts.map +0 -1
  216. package/dist/runtime/index.js +0 -6
  217. package/dist/runtime/index.js.map +0 -1
  218. package/dist/runtime/interceptor.d.ts +0 -61
  219. package/dist/runtime/interceptor.d.ts.map +0 -1
  220. package/dist/runtime/interceptor.js +0 -7
  221. package/dist/runtime/interceptor.js.map +0 -1
  222. package/dist/runtime/invoke.d.ts +0 -34
  223. package/dist/runtime/invoke.d.ts.map +0 -1
  224. package/dist/runtime/invoke.js +0 -235
  225. package/dist/runtime/invoke.js.map +0 -1
  226. package/dist/runtime/middleware.d.ts +0 -20
  227. package/dist/runtime/middleware.d.ts.map +0 -1
  228. package/dist/runtime/middleware.js +0 -2
  229. package/dist/runtime/middleware.js.map +0 -1
  230. package/dist/runtime/rpc.d.ts +0 -24
  231. package/dist/runtime/rpc.d.ts.map +0 -1
  232. package/dist/runtime/rpc.js +0 -16
  233. package/dist/runtime/rpc.js.map +0 -1
  234. package/dist/testing/app-test.d.ts +0 -77
  235. package/dist/testing/app-test.d.ts.map +0 -1
  236. package/dist/testing/app-test.js +0 -109
  237. package/dist/testing/app-test.js.map +0 -1
  238. package/dist/testing/in-memory-bus.d.ts +0 -19
  239. package/dist/testing/in-memory-bus.d.ts.map +0 -1
  240. package/dist/testing/in-memory-bus.js +0 -31
  241. package/dist/testing/in-memory-bus.js.map +0 -1
  242. package/dist/testing/in-memory-channel.d.ts +0 -32
  243. package/dist/testing/in-memory-channel.d.ts.map +0 -1
  244. package/dist/testing/in-memory-channel.js +0 -65
  245. package/dist/testing/in-memory-channel.js.map +0 -1
  246. package/dist/testing/in-memory-queue.d.ts +0 -24
  247. package/dist/testing/in-memory-queue.d.ts.map +0 -1
  248. package/dist/testing/in-memory-queue.js +0 -66
  249. package/dist/testing/in-memory-queue.js.map +0 -1
  250. package/dist/testing/in-memory-repository.d.ts +0 -44
  251. package/dist/testing/in-memory-repository.d.ts.map +0 -1
  252. package/dist/testing/in-memory-repository.js +0 -60
  253. package/dist/testing/in-memory-repository.js.map +0 -1
  254. package/dist/testing/index.d.ts +0 -6
  255. package/dist/testing/index.d.ts.map +0 -1
  256. package/dist/testing/index.js +0 -6
  257. package/dist/testing/index.js.map +0 -1
  258. /package/dist/{domain/cow-draft.d.ts → cow-draft.d.ts} +0 -0
  259. /package/dist/{domain/cow-draft.js → cow-draft.js} +0 -0
  260. /package/dist/{core/result.d.ts → result.d.ts} +0 -0
  261. /package/dist/{core/result.js → result.js} +0 -0
  262. /package/dist/{core/standard-schema.js → standard-schema.js} +0 -0
@@ -0,0 +1,66 @@
1
+ # Adapter
2
+
3
+ An adapter is a port with its methods filled in. Pass the port class to `Adapter` and implement every abstract method. `start` and `stop` are already there for connecting and disconnecting.
4
+
5
+ ```ts
6
+ class InMemoryPosts extends Adapter(PostRepository) {
7
+ override async get(id: string): Promise<Post> {
8
+ /* … */
9
+ }
10
+ override async save(post: Post): Promise<void> {
11
+ /* … */
12
+ }
13
+ }
14
+ ```
15
+
16
+ ## Why it exists
17
+
18
+ The use case asks a `PostRepository` to save. Something has to actually keep the post: a `Map` while you sketch the feature, a database when the API runs, a fake in a test that records the calls. Those are adapters of one port. Swapping them changes the constructor arguments at the composition root, and `execute` stays put.
19
+
20
+ `start` and `stop` give a connection a home that is still outside the use case. The process opens the database before it serves traffic, and closes it when it shuts down.
21
+
22
+ ## How to use it
23
+
24
+ An in-memory repository is enough to walk the publish story. The API process can later substitute a database class that extends the same port. The composition root constructs the adapter, starts it, and hands it to `PublishPost`.
25
+
26
+ ```ts
27
+ class InMemoryPosts extends Adapter(PostRepository) {
28
+ private readonly rows = new Map<string, ReturnType<Post['toProps']>>();
29
+
30
+ override async get(id: string): Promise<Post> {
31
+ const props = this.rows.get(id);
32
+ if (props === undefined) throw BlogError.PostMissing({ id });
33
+ return Post.restore(props);
34
+ }
35
+
36
+ override async save(post: Post): Promise<void> {
37
+ this.rows.set(post.props.id, post.toProps());
38
+ post.commit();
39
+ }
40
+ }
41
+
42
+ const posts = new InMemoryPosts();
43
+ await posts.start();
44
+ const publishPost = new PublishPost(posts, events);
45
+ ```
46
+
47
+ Store `toProps()` (or a mapped document) and rebuild with `restore` on the way out. Call `commit()` after the write succeeds.
48
+
49
+ The defaults of `start` and `stop` resolve immediately, so an in-memory adapter can leave them alone. A database adapter overrides `start` to connect and `stop` to disconnect. The application calls them. The use case calls `get` and `save`.
50
+
51
+ A publisher adapter accepts the catalog instance and, on a real transport, sends `BlogEvents.message(event)`. A subscription adapter owns the consumer loop. It parses with `BlogEvents.parse`, then calls each subscribed handler. It does not hide handler errors. Ack, retry, prefetch, and consumer groups live here, and only when the broker has them.
52
+
53
+ One adapter implements one port. A second port is a second class.
54
+
55
+ ## API
56
+
57
+ `Adapter(PortClass)` returns an abstract class that extends that port. Implement the port's methods. The compiler reports a missing one on the class.
58
+
59
+ | Member | Role |
60
+ | --- | --- |
61
+ | `token` | The port's token. An adapter keeps it and cannot replace it. |
62
+ | Port methods | Implement each abstract method from the port. |
63
+ | `start()` | Called by the application before the adapter is used. Override it to connect. The default resolves immediately. |
64
+ | `stop()` | Called by the application when it is finished with the adapter. Override it to disconnect. The default resolves immediately. |
65
+
66
+ `InMemoryPosts.token` is `PostRepository.token`.
@@ -0,0 +1,74 @@
1
+ # Entity
2
+
3
+ An entity is one thing the software stores and changes, such as a blog post. Pair a name with a schema, then add methods for the rules. Callers build an instance with `create`, `restore`, or `parse`, change it through those methods, and read the props back out when it is time to save.
4
+
5
+ ```ts
6
+ class Post extends Entity('Post', PostSchema) {}
7
+ ```
8
+
9
+ `schema` is a Standard Schema or a Schema class.
10
+
11
+ ## Why it exists
12
+
13
+ A post has data and rules. The data is an id, a title, a body, and whether it is published. A rule says that publishing records `publishedAt`, and that a blank title is refused. The entity keeps both on one object.
14
+
15
+ The schema runs when the post is created from typed props, when unknown input is parsed, and again when a method writes. A failed write restores the previous props. The instance remembers whether it is new and which fields changed, so a repository can save it without a second copy of those rules.
16
+
17
+ ## How to use it
18
+
19
+ Publishing is a method on the entity. The use case loads the post, calls the method, and saves. The method refuses a blank title with a catalog error, then updates the draft.
20
+
21
+ ```ts
22
+ class Post extends Entity('Post', PostSchema) {
23
+ publish(now: Date): this {
24
+ if (this.props.title.trim() === '') throw BlogError.BlankTitle();
25
+ if (this.props.published) throw BlogError.AlreadyPublished();
26
+ return this.set((draft) => {
27
+ draft.published = true;
28
+ draft.publishedAt = now;
29
+ });
30
+ }
31
+ }
32
+
33
+ const post = Post.create({
34
+ id: 'p1',
35
+ title: 'Hello',
36
+ body: 'First post',
37
+ published: false,
38
+ });
39
+ post.publish(new Date());
40
+ await posts.save(post);
41
+ ```
42
+
43
+ `create` checks the props and marks the post new. A repository loading a stored row uses `restore`, which keeps those props and treats the post as already saved. A request body is untyped, so the edge uses `parse`: the schema runs, and the post is treated as existing.
44
+
45
+ Read `props` for the current data. Write only through `set` or a method that calls it. `toProps()` is the frozen snapshot you persist or return from a use case.
46
+
47
+ The adapter calls `commit()` after the driver write succeeds. That is what clears `isNew` and the dirty set. The use case does not commit, and a mapper does not commit.
48
+
49
+ ## API
50
+
51
+ `Entity(token, schema)` returns the class you extend. `token` is a string literal.
52
+
53
+ | Member | Role |
54
+ | --- | --- |
55
+ | `Post.token` | The name you passed in. |
56
+ | `Post.schema` | The Standard Schema used by `create`, `parse`, `set`, and `validate`. |
57
+ | `Post.create(props)` | Check `props` and return a new instance. `isNew` stays true until `commit`. |
58
+ | `Post.restore(props)` | Build an instance from stored props. The schema waits until `validate` or a writing `set`. `isNew` is false. |
59
+ | `Post.parse(value)` | Check an unknown value, then build an instance the way `restore` does. `isNew` is false. |
60
+ | `props` | The current data, typed read-only. Change it with `set`. |
61
+ | `set(draft => …)` | Copy-on-write edit. A write runs the schema. A failed check restores the previous props and throws. |
62
+ | `validate()` | Run the schema when this instance has not already passed. A passed instance returns itself. |
63
+ | `commit()` | Treat the current props as the saved original. `isNew` becomes false. |
64
+ | `isNew` | True after `create`, until `commit`. |
65
+ | `isValidated` | True after `create`, `parse`, a writing `set`, or `validate`. False after `restore` until one of those runs. |
66
+ | `original` | Props from before the first change on a restored instance. `undefined` while the instance is clean, and while a created instance has not been committed. |
67
+ | `isDirty()` | True when the post is new, or a shallow field differs from the saved original. |
68
+ | `getChangedKeys()` | Shallow keys that differ. `{ deep: true }` returns dotted paths. |
69
+ | `toProps()` | A deep-frozen snapshot for saving or returning. Reused until the next `set`. |
70
+ | `toJSON()` | The same snapshot as `toProps()`. |
71
+
72
+ A schema failure throws `CodedError` with code `VALIDATION` and the message `hexok: Post validation failed`. A domain rule throws a member of the error catalog.
73
+
74
+ `DeepReadonly<T>` is the type of `props`, `original`, and `toProps()`. `Date` values stay `Date`. `EntityInstance` is the props-only instance type.
@@ -0,0 +1,140 @@
1
+ # Error
2
+
3
+ An error catalog is the named set of refusals the application throws. Define it with `Errors`, giving the catalog a name and one entry per code. Call sites throw a member. The edge catches the catalog and decides what a caller sees.
4
+
5
+ ```ts
6
+ class BlogError extends Errors('blog', {
7
+ BlankTitle: { message: 'Title is blank' },
8
+ AlreadyPublished: { message: 'Post is already published' },
9
+ }) {}
10
+ ```
11
+
12
+ The shape of each entry is an error map. A failed schema check is a different failure: `CodedError` with code `VALIDATION`.
13
+
14
+ ## Why it exists
15
+
16
+ Publishing can refuse for reasons the caller can act on. The title is blank. The post is already published. The trace id is missing. A catalog gives each refusal a stable code, a message, and, when the caller needs a value back, a payload such as the post id.
17
+
18
+ The catalog stays a list of domain facts. Mapping `BlankTitle` to an HTTP status, an RPC code, or a queue retry lives in the gateway or the worker.
19
+
20
+ ## How to use it
21
+
22
+ The entity throws. The use case lets the error travel. The gateway matches and chooses the reply.
23
+
24
+ ```ts
25
+ class Post extends Entity('Post', PostSchema) {
26
+ publish(now: Date): this {
27
+ if (this.props.title.trim() === '') throw BlogError.BlankTitle();
28
+ if (this.props.published) throw BlogError.AlreadyPublished();
29
+ return this.set((draft) => {
30
+ draft.published = true;
31
+ draft.publishedAt = now;
32
+ });
33
+ }
34
+ }
35
+
36
+ try {
37
+ await publishPost.execute({ id });
38
+ } catch (error) {
39
+ if (!BlogError.is(error)) throw error;
40
+ return BlogError.match(error, {
41
+ BlankTitle: () => reply(400, error.message),
42
+ AlreadyPublished: () => reply(409, error.message),
43
+ });
44
+ }
45
+ ```
46
+
47
+ Call the member. `BlogError.BlankTitle()` uses the catalog message. `BlogError.BlankTitle('Title cannot be empty')` replaces it for this throw. `new BlogError.BlankTitle()` throws `TypeError`, because the member is a factory.
48
+
49
+ `UseCase`, `Entity`, `Port`, `Adapter`, `Schema`, `Mapper`, `Event`, `EventCatalog`, and `EventHandler` do not take an error map. There is no `error()` method on those classes.
50
+
51
+ ## Error map
52
+
53
+ The object passed to `Errors` is the error map. Each key is a code. Each value is an `ErrorDef`: an optional `message`, and an optional `data` Standard Schema. `ErrorMap` and `ErrorDef` are exported because declaration emit names them.
54
+
55
+ ```ts
56
+ class BlogError extends Errors('blog', {
57
+ BlankTitle: { message: 'Title is blank' },
58
+ TraceIdMissing: {},
59
+ PostMissing: {
60
+ message: 'Post was not found',
61
+ data: z.object({ id: z.string() }),
62
+ },
63
+ }) {}
64
+
65
+ throw BlogError.BlankTitle();
66
+ throw BlogError.TraceIdMissing();
67
+ throw BlogError.PostMissing({ id: 'p1' });
68
+ throw BlogError.PostMissing('No post p1', { id: 'p1' });
69
+ ```
70
+
71
+ `TraceIdMissing` omits `message`, so the factory's fallback text is the code, `'TraceIdMissing'`. `PostMissing` requires `{ id: string }` because that is the schema's output. The `data` schema types the argument. The factory stores the value it was given and does not validate it. Run the schema yourself before the throw when a bad payload should fail as a schema check.
72
+
73
+ | Entry | Call |
74
+ | --- | --- |
75
+ | No `data` | `Code()` or `Code(message)`. |
76
+ | `data` whose output is an object or number | `Code(data)` or `Code(message, data)`. |
77
+ | `data` whose output is a string | One argument is the payload. The message is first only when the payload follows it: `Code(message, data)`. |
78
+
79
+ Keys `is`, `match`, `prototype`, and `token` are reserved. Defining one of them throws `hexok: error code "<code>" is reserved`.
80
+
81
+ ## Catalog API
82
+
83
+ `Errors(token, defs)` returns the class you extend. `token` is the catalog name.
84
+
85
+ | Member | Role |
86
+ | --- | --- |
87
+ | `BlogError.token` | The catalog name. |
88
+ | `BlogError.BlankTitle(...)` | Builds that member. You throw the return value. |
89
+ | `BlogError.is(error)` | True when `error` is a member of this catalog. Narrows to the union of members. |
90
+ | `BlogError.match(error, cases)` | Runs the handler for `error.code`. Every code in the catalog is required. A code that is missing at runtime throws `hexok: unmatched blog.<code>`. |
91
+ | `error.catalog` | The catalog token. |
92
+ | `error.code` | The member code, such as `'BlankTitle'`. |
93
+ | `error.message` | The catalog message, or the override passed at the call. |
94
+ | `error.data` | The payload, or `undefined` when the definition has no `data` schema. |
95
+ | `error.name` | `` `${token}.${code}` ``, such as `'blog.BlankTitle'`. |
96
+
97
+ The message on the returned error keeps the literal you passed, including a template literal. `is` and `match` type `message` as `string`, because one code can be thrown with the catalog text or an override.
98
+
99
+ `instanceof BlogError` is the same catalog check as `is`. `is` is what narrows the union so `match` and `code` see each member.
100
+
101
+ ## Coded error
102
+
103
+ A coded error is a failure identified by a `code` string, with a message and an optional payload. Schema checks throw one whose code is `VALIDATION`. Application refusals use the catalog.
104
+
105
+ ```ts
106
+ new CodedError({
107
+ code: 'VALIDATION',
108
+ message: 'hexok: Post validation failed',
109
+ data: { issues },
110
+ });
111
+ ```
112
+
113
+ Checking a shape and refusing a story are different events. `Post.parse` can reject a body that has no title. `post.publish()` can reject a post that is already live. The gateway shows field errors from `data.issues` and matches catalog members on their own codes.
114
+
115
+ ```ts
116
+ try {
117
+ const post = Post.parse(body);
118
+ await posts.save(post);
119
+ } catch (error) {
120
+ if (error instanceof CodedError && error.code === 'VALIDATION') {
121
+ return reply(400, error.data);
122
+ }
123
+ throw error;
124
+ }
125
+ ```
126
+
127
+ The same error is thrown by `Schema.parse`, entity `create`, `parse`, a writing `set`, or `validate`, `Event.parse`, and `EventCatalog.parse` when the payload schema rejects the message. `Entity.restore` does not validate. The next writing `set`, or an explicit `validate()`, can throw.
128
+
129
+ | Member | Role |
130
+ | --- | --- |
131
+ | `new CodedError({ code, message?, data? })` | `message` defaults to `code`. `data` is set only when the argument object has a `data` key. |
132
+ | `error.code` | The code. Schema checks use `'VALIDATION'`. |
133
+ | `error.message` | The message. Schema checks use `` `hexok: ${token} validation failed` ``. |
134
+ | `error.data` | Optional payload. For a schema check it is `{ issues }` when the schema reported issues. |
135
+ | `error.name` | `'CodedError'`. |
136
+ | `validationError(message, issues?)` | Builds a `CodedError<'VALIDATION'>`. Passes `data: { issues }` when `issues` is present. |
137
+
138
+ An issue is a Standard Schema issue: a `message` and an optional `path`. An async schema throws a plain `Error` with the message `hexok: async schemas belong at the application edge`.
139
+
140
+ `ok`, `fail`, and `Result` are exported for application code that already returns a result. Entity methods, `execute`, and `handle` throw. They do not return `Result`.
@@ -0,0 +1,57 @@
1
+ # EventCatalog
2
+
3
+ An event catalog is the set of events one publisher and its subscribers share. Name the catalog and pass a map of event classes. The map key is the name in a serialized message. The event's own token stays the name on the class.
4
+
5
+ ```ts
6
+ class BlogEvents extends EventCatalog('blog', {
7
+ postPublished: PostPublished,
8
+ }) {}
9
+ ```
10
+
11
+ ## Why it exists
12
+
13
+ A use case has a `PostPublished` object. A queue, a log, or another process needs something it can store: a key and a payload. The catalog is the agreement on which events exist, and the two conversions between those forms. `message` turns an instance into `{ key, payload }`. `parse` turns that message back into the event class, checking the payload with the event's schema.
14
+
15
+ The map key and the event token are different strings on purpose. `postPublished` is the name in the catalog and on the wire message. `post.published` is the token on the class, the same string passed to `Event`.
16
+
17
+ ## How to use it
18
+
19
+ The publisher port accepts any instance from the catalog. The adapter that talks to a transport calls `message` on the way out and `parse` on the way in. A handler then receives the event object.
20
+
21
+ ```ts
22
+ abstract class BlogEventPublisher extends Port('BlogEventPublisher') {
23
+ abstract publish(event: EventInstance<typeof BlogEvents>): Promise<void>;
24
+ }
25
+
26
+ const message = BlogEvents.message(new PostPublished(post.toProps()));
27
+ // { key: 'postPublished', payload: { id, title, body, published } }
28
+
29
+ const event = BlogEvents.parse(message);
30
+ await emailSubscribers.handle(event);
31
+ ```
32
+
33
+ `publish` takes the instance union. A generic `publish<K>(key, payload)` does not keep the payload paired with the key inside the method. The use case publishes the instance. The adapter converts it.
34
+
35
+ `BlogEvents.get('postPublished').token` is `'post.published'`.
36
+
37
+ Narrow one entry when a function handles a single key: `EventInstance<typeof BlogEvents, 'postPublished'>` and `EventMessage<typeof BlogEvents, 'postPublished'>`.
38
+
39
+ A port parameter typed as the catalog class does not check event key or payload, because a catalog instance has no fields. Use `typeof BlogEvents` when the type you need is the static side.
40
+
41
+ ## API
42
+
43
+ `EventCatalog(token, events)` returns the class you extend. `token` is a string literal. `events` is a map of event classes. Duplicate event tokens throw `hexok: duplicate event "<token>" in catalog "<name>"` when the class is defined. The map is frozen on the catalog.
44
+
45
+ | Member | Role |
46
+ | --- | --- |
47
+ | `BlogEvents.token` | The catalog name. |
48
+ | `BlogEvents.events` | The map you passed in. |
49
+ | `BlogEvents.get(name)` | The class registered under that map key. An unknown key throws `hexok: event "<name>" is not in catalog "blog"`. |
50
+ | `BlogEvents.message(event)` | `{ key, payload }` for an instance of a class in this catalog. `key` is the map key. An event from elsewhere throws `hexok: event "<token>" is not in catalog "blog"`. |
51
+ | `BlogEvents.parse(value)` | Checks a `{ key, payload }` message and returns the event instance. |
52
+
53
+ `parse` throws `hexok: catalog "blog" expected { key, payload }` when `value` lacks a string `key` or a `payload`. An unknown key throws `hexok: event "<key>" is not in catalog "blog"`. A payload the event schema rejects throws `CodedError` `VALIDATION` from that event's `parse`.
54
+
55
+ `EventInstance<typeof BlogEvents>` is the union of event instances in the catalog. Each instance carries its token. `EventMessage<typeof BlogEvents>` is the matching `{ key, payload }` union. The second type argument keeps one entry. `key` on a message is the map key. The event token is `BlogEvents.get(message.key).token`, and `event.token` on the instance.
56
+
57
+ Narrow a handler or a switch with `event.token` or `instanceof`. Indexing a handler map by `event.token` does not narrow the payload. A `never` default on the whole event reports an unhandled catalog entry.
@@ -0,0 +1,56 @@
1
+ # EventHandler
2
+
3
+ An event handler is the work that follows one event. Name the handler and pass the event class. `handle` receives an instance of that event. Ports, such as a mail sender, arrive through the constructor.
4
+
5
+ ```ts
6
+ class EmailSubscribers extends EventHandler('email.subscribers', PostPublished) {}
7
+ ```
8
+
9
+ The application subscribes the handler. An in-process dispatcher or a queue worker calls `handle` once it has the event. The handler does not subscribe itself.
10
+
11
+ ## Why it exists
12
+
13
+ The use case that publishes a post is finished once the post is saved and the event is published. Emailing every subscriber can be slow, and it can run in another process. The handler is that second story. It stays tied to one event class, so `event.payload` is the published post and the compiler notices a handler pointed at a different event.
14
+
15
+ ## How to use it
16
+
17
+ `EmailSubscribers` loads the subscriber list and sends one mail per address. The published post is `event.payload`.
18
+
19
+ ```ts
20
+ class EmailSubscribers extends EventHandler('email.subscribers', PostPublished) {
21
+ constructor(
22
+ private readonly subscribers: SubscriberDirectory,
23
+ private readonly mail: MailSender,
24
+ ) {
25
+ super();
26
+ }
27
+
28
+ async handle(event: PostPublished): Promise<void> {
29
+ const addresses = await this.subscribers.list();
30
+ for (const address of addresses) {
31
+ await this.mail.send(address, event.payload.title);
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
37
+ Annotate `event` with `PostPublished`. An unannotated `handle(event)` is implicit `any`. Keep `handle` a method.
38
+
39
+ A process that shares the API's memory can pass the instance straight to `handle`. A worker in another process reads a catalog message, runs `BlogEvents.parse`, and then calls `handle` with the result. Subscription, ack, and retry belong to that adapter.
40
+
41
+ One handler is bound to one event class. A second event is a second handler.
42
+
43
+ A static field such as `groupId` is yours to read when you subscribe the handler. Hexok does not read it. Same group string is one competing consumer set only for a real broker. Different groups each receive the event. An in-memory adapter may ignore the group.
44
+
45
+ ## API
46
+
47
+ `EventHandler(token, event)` returns the class you extend. `token` is a string literal, the handler's name. `event` is an event class.
48
+
49
+ | Member | Role |
50
+ | --- | --- |
51
+ | `EmailSubscribers.token` | The name you passed in. |
52
+ | `EmailSubscribers.event` | The event class this handler receives. |
53
+ | `handle(event)` | Required. Annotate `event` with that class. Return `void` or `Promise<void>`. |
54
+ | `constructor` | Receive ports here and call `super()`. |
55
+
56
+ The compiler reports a missing `handle`. The factory records the token and the event class. It takes no consumer-group argument.
@@ -0,0 +1,53 @@
1
+ # Event
2
+
3
+ An event is a fact the application records, such as "this post was published." Name it and give it the schema of its payload. `new` stores a payload you already typed. `parse` checks an unknown value and then constructs the event.
4
+
5
+ ```ts
6
+ class PostPublished extends Event('post.published', PostSchema) {}
7
+ ```
8
+
9
+ Register the class on an event catalog when a publisher and a subscriber need to agree on the set. An event handler performs the work that follows from one event.
10
+
11
+ ## Why it exists
12
+
13
+ Publishing a post saves the post. Emailing subscribers is a consequence, and it is a different story: it can fail, retry, and run in another process without changing whether the post is published. The event is the fact the use case records. The handler is where the email happens. The payload schema keeps that fact the same shape on both sides.
14
+
15
+ ## How to use it
16
+
17
+ The use case constructs the event from the post it just saved and hands it to a publisher port. The handler later reads `event.payload`.
18
+
19
+ ```ts
20
+ class PostPublished extends Event('post.published', PostSchema) {}
21
+
22
+ await events.publish(
23
+ new PostPublished({
24
+ id: post.props.id,
25
+ title: post.props.title,
26
+ body: post.props.body,
27
+ published: true,
28
+ }),
29
+ );
30
+ ```
31
+
32
+ `new PostPublished(...)` trusts that object. When the payload arrives as unknown JSON, the boundary uses `PostPublished.parse(value)`, or the catalog's `parse` for a `{ key, payload }` message.
33
+
34
+ The string `'post.published'` is the event's token. A catalog map key, such as `postPublished`, is a separate name.
35
+
36
+ ## API
37
+
38
+ `Event(token, schema)` returns the class. `token` is a string literal. `schema` is a Standard Schema or a Schema class. A Schema class is stored as its definition.
39
+
40
+ | Member | Role |
41
+ | --- | --- |
42
+ | `PostPublished.token` | The name you passed in. |
43
+ | `PostPublished.schema` | The payload schema. |
44
+ | `new PostPublished(payload)` | Stores `payload`. The constructor expects the schema's output type. |
45
+ | `PostPublished.parse(value)` | Checks `value` and returns an instance of the class you called it on. |
46
+ | `event.token` | The same literal as the static token. |
47
+ | `event.payload` | The payload. |
48
+
49
+ A rejected `parse` throws `CodedError` with code `VALIDATION` and the message `hexok: post.published validation failed`.
50
+
51
+ Override `parse` when the boundary needs to preprocess the value. `parse` uses the class it was called on, so a catalog `parse` still returns the registered subclass and `instanceof` holds.
52
+
53
+ Callers tell two event classes apart with `instanceof` or by comparing `event.token`. Two classes that share a schema stay assignable to each other until the instance token separates them. Prefer the catalog's `EventInstance` union on a publisher port so the port accepts every event in the set.
@@ -0,0 +1,77 @@
1
+ # Mapper
2
+
3
+ A mapper translates the value a port speaks into the document an adapter stores, and back. Use one when those shapes differ: `_id` instead of `id`, a GeoJSON point instead of a `[lat, lng]` pair, a driver row instead of entity props. Persisting `toProps()` as JSON needs no mapper.
4
+
5
+ The mapper is stateless. Domain code does not import the stored schema. The adapter owns the mapper and calls it. The adapter commits the entity after the driver write succeeds. The mapper does not commit.
6
+
7
+ ```ts
8
+ class MongoUser extends Mapper('mongo.User', User, mongoUserSchema) {
9
+ protected fromSource(user: User) {
10
+ const props = user.toProps();
11
+ const [lat, lng] = props.location;
12
+ return {
13
+ _id: props.id,
14
+ name: props.name,
15
+ location: { type: 'Point' as const, coordinates: [lng, lat] },
16
+ };
17
+ }
18
+
19
+ protected toSource(doc: { _id: string; name: string; location: { coordinates: [number, number] } }) {
20
+ const [lng, lat] = doc.location.coordinates;
21
+ return { id: doc._id, name: doc.name, location: [lat, lng] };
22
+ }
23
+ }
24
+ ```
25
+
26
+ Extend `Mapper(...)`, not `MapperClass`. Export a named class. An anonymous class expression cannot emit the protected methods.
27
+
28
+ ## Why it exists
29
+
30
+ The domain port stays in domain types. Each adapter has its own stored shape. One mapper keeps that translation next to the adapter, so `execute` still calls `posts.save(post)` and never sees `_id` or coordinate order.
31
+
32
+ ## How to use it
33
+
34
+ `fromSource` feeds `toModel`. `toSource` feeds `fromModel`. The adapter calls the public pair.
35
+
36
+ ```ts
37
+ class MongoUsers extends Adapter(UserRepository) {
38
+ constructor(private readonly users: MongoUser) {
39
+ super();
40
+ }
41
+
42
+ override async save(user: User): Promise<void> {
43
+ const doc = this.users.toModel(user);
44
+ await this.collection.updateOne({ _id: doc._id }, { $set: doc }, { upsert: true });
45
+ user.commit();
46
+ }
47
+
48
+ override async get(id: string): Promise<User | null> {
49
+ const doc = await this.collection.findOne({ _id: id });
50
+ if (doc === null) return null;
51
+ return this.users.fromModel(doc);
52
+ }
53
+ }
54
+ ```
55
+
56
+ The source argument is an Entity class, a Schema class, or a raw Standard Schema. The same primitive covers a schema or a plain object, not only an entity.
57
+
58
+ Checks are off unless the factory or that call passes `{ validate: true }`. A call overrides the factory.
59
+
60
+ - Off: `toModel` returns the `fromSource` draft unchecked. `fromModel` uses `Entity.restore` or returns a schema draft as given, so `isValidated` stays false.
61
+ - On: both directions run the stored schema. An entity load uses `parse`. A stored-schema failure throws `CodedError` `VALIDATION` with the mapper token. An entity parse failure uses the entity token.
62
+
63
+ ## API
64
+
65
+ `Mapper(token, source, storedSchema, options?)` returns the class you extend. `token` is a string literal. `options` is `{ validate?: boolean }`.
66
+
67
+ | Member | Role |
68
+ | --- | --- |
69
+ | `fromSource(source)` | Protected. Required. Entity source receives the instance. A schema source receives its output. Return the stored draft. |
70
+ | `toSource(model)` | Protected. Required. Receives the stored output. Return entity props, or the source schema's output. |
71
+ | `toModel(source, options?)` | Public. Adapters call this. Do not override. |
72
+ | `fromModel(value, options?)` | Public. Adapters call this. Do not override. |
73
+ | `token` | The name passed to `Mapper`. |
74
+ | `schema` | The stored Standard Schema. A Schema class is stored as its definition. |
75
+ | `source` | The entity, Schema class, or Standard Schema this mapper reads and builds. |
76
+
77
+ `MapperClass` is exported so an exported subclass's `.d.ts` can name the base and keep `fromSource` and `toSource` protected. A protected constructor on the subclass makes `new Subclass()` a type error. Leave the constructor public.
@@ -0,0 +1,58 @@
1
+ # Port
2
+
3
+ A port is the contract a use case or an event handler calls. Name it with `Port` and declare the methods the caller needs. An adapter is the class that implements those methods.
4
+
5
+ ```ts
6
+ abstract class PostRepository extends Port('PostRepository') {
7
+ abstract get(id: string): Promise<Post>;
8
+ abstract save(post: Post): Promise<void>;
9
+ }
10
+ ```
11
+
12
+ ## Why it exists
13
+
14
+ Publishing a post has to save the post and announce the fact. Those are two different jobs, and each can be done more than one way: memory in a test, a database in the API, a broker when a worker lives in another process. The use case names the jobs. The port is the name. The adapter is the current way the job is done, and it can be replaced without editing `execute`.
15
+
16
+ ## How to use it
17
+
18
+ `PublishPost` asks a `PostRepository` for the post and asks a `BlogEventPublisher` to take the published-post event. It never imports a database client or a queue.
19
+
20
+ ```ts
21
+ abstract class BlogEventPublisher extends Port('BlogEventPublisher') {
22
+ abstract publish(event: EventInstance<typeof BlogEvents>): Promise<void>;
23
+ }
24
+
25
+ class PublishPost extends UseCase('post.publish') {
26
+ constructor(
27
+ private readonly posts: PostRepository,
28
+ private readonly events: BlogEventPublisher,
29
+ ) {
30
+ super();
31
+ }
32
+
33
+ async execute(input: { id: string }): Promise<{ id: string }> {
34
+ const post = await this.posts.get(input.id);
35
+ post.publish(new Date());
36
+ await this.posts.save(post);
37
+ await this.events.publish(new PostPublished(post.toProps()));
38
+ return { id: post.props.id };
39
+ }
40
+ }
41
+ ```
42
+
43
+ The methods are whatever this seam needs. A repository offers `get` and `save`. A publisher offers `publish`. A mail port offers `send`. A subscription port offers `subscribe`. Keep publishing and subscribing as two ports. `start` and `stop` stay on the adapter. The use case depends on the port class, and the composition root passes an adapter.
44
+
45
+ Search for an existing port before adding one. A second seam is a second `Port(...)` call, even when the method names match.
46
+
47
+ ## API
48
+
49
+ `Port(token)` returns an abstract class. `token` is a string literal.
50
+
51
+ | Member | Role |
52
+ | --- | --- |
53
+ | `PostRepository.token` | The name you passed in. |
54
+ | Methods you declare | The contract. Mark them `abstract`. A subclass that leaves one unimplemented is a compile error. |
55
+
56
+ Each `Port(...)` call is its own class, with its own nominal identity. A class with the same method names is a different type. Extend one port. One class cannot extend two ports.
57
+
58
+ The adapter reuses this token. `InMemoryPosts.token` is `'PostRepository'` when `InMemoryPosts` extends `Adapter(PostRepository)`. An adapter cannot redeclare a different literal.
@@ -0,0 +1,47 @@
1
+ # Schema
2
+
3
+ A schema names a shape and checks values against it. Pass the name and a [Standard Schema](https://standardschema.dev) to `Schema`. `parse` returns the schema output, or throws when the value does not fit.
4
+
5
+ ```ts
6
+ class PostSchema extends Schema('Post', postSchema) {}
7
+ ```
8
+
9
+ Zod, Valibot, ArkType, and any other Standard Schema library are passed straight through. `@hexok/typebox` adapts a TypeBox 1 schematic first: `typebox(schema)` infers `Static`, and `typeboxDecode(schema)` infers `StaticDecode` and decodes. Decode is a second function. Entity and event construction use the non-decoding wrapper.
10
+
11
+ ## Why it exists
12
+
13
+ A post is an id, a title, a body, and a published flag. That shape shows up on the entity, on the event that says the post was published, and at the edge where a request body arrives. One named schema is the definition those sites share. The name is what appears in the failure message, so a rejected post is distinguishable from a rejected subscriber.
14
+
15
+ ## How to use it
16
+
17
+ Define the post once. The entity and the published-post event both take `PostSchema`. At a trust boundary, `parse` turns unknown JSON into the typed output.
18
+
19
+ ```ts
20
+ const post = PostSchema.parse({
21
+ id: 'p1',
22
+ title: 'Hello',
23
+ body: 'First post',
24
+ published: false,
25
+ });
26
+ ```
27
+
28
+ `post.title` is a string because the schema says so. A missing title throws a coded error whose code is `VALIDATION` and whose message is `hexok: Post validation failed`.
29
+
30
+ An entity or an event can take the Standard Schema directly, without this class. Use `Schema` when you want the name, `parse`, and a place to customize the check.
31
+
32
+ ## API
33
+
34
+ `Schema(token, definition)` returns the class you extend. `token` is a string literal. `definition` is a Standard Schema.
35
+
36
+ | Member | Role |
37
+ | --- | --- |
38
+ | `PostSchema.token` | The name you passed in. |
39
+ | `PostSchema.definition` | The Standard Schema this class wraps. |
40
+ | `PostSchema.parse(value)` | Check `value` and return the schema output. |
41
+ | `PostSchema.check(value)` | The same check. Overrides of `parse` call this when they want the default result. |
42
+
43
+ A rejected value throws `CodedError` with code `VALIDATION`. `data.issues` is the Standard Schema issue list when the schema reported one. Each issue has a `message` and an optional `path`.
44
+
45
+ Override `parse` to preprocess or postprocess. Override `check` when the check itself should change.
46
+
47
+ `InferSchema<S>` is the output type of a Schema class or a raw Standard Schema. `SchemaSource` is that input. Both are exported from `hexok`. An async schema throws `Error` with the message `hexok: async schemas belong at the application edge`.