@afora/core 0.0.8 → 0.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (226) hide show
  1. package/README.md +149 -23
  2. package/dist/adapters.d.ts +25 -0
  3. package/dist/adapters.js +32 -0
  4. package/dist/core/application/application.d.ts +30 -0
  5. package/dist/core/application/application.js +1291 -0
  6. package/dist/core/application/exceptions.d.ts +17 -0
  7. package/dist/core/application/exceptions.js +85 -0
  8. package/dist/core/application/migrate.d.ts +19 -0
  9. package/dist/core/application/migrate.js +67 -0
  10. package/dist/core/application/providers.js +108 -0
  11. package/dist/core/application/registry.js +23 -0
  12. package/dist/core/application/schema.d.ts +30 -0
  13. package/dist/core/application/schema.js +398 -0
  14. package/dist/core/application/start.js +162 -0
  15. package/dist/core/application/types/application.interface.d.ts +60 -3
  16. package/dist/core/auth/jwt.d.ts +30 -0
  17. package/dist/core/auth/jwt.js +200 -0
  18. package/dist/core/auth/types/jwt.interface.d.ts +74 -0
  19. package/dist/core/blueprint/blueprint-plugin.js +7 -0
  20. package/dist/core/blueprint/blueprint.js +146 -0
  21. package/dist/core/blueprint/callable.js +13 -0
  22. package/dist/core/blueprint/exceptions.d.ts +5 -0
  23. package/dist/core/blueprint/exceptions.js +20 -0
  24. package/dist/core/blueprint/is-blueprint.js +4 -0
  25. package/dist/core/blueprint/list.js +19 -0
  26. package/dist/core/blueprint/optional.d.ts +3 -1
  27. package/dist/core/blueprint/optional.js +69 -0
  28. package/dist/core/blueprint/record-field.js +14 -0
  29. package/dist/core/blueprint/slice.js +127 -0
  30. package/dist/core/blueprint/types/blueprint.interface.d.ts +95 -5
  31. package/dist/core/blueprint/variant.d.ts +32 -0
  32. package/dist/core/blueprint/variant.js +72 -0
  33. package/dist/core/command/command.js +428 -0
  34. package/dist/core/command/exceptions.js +30 -0
  35. package/dist/core/command/types/command.interface.d.ts +10 -1
  36. package/dist/core/entity/abstract-entity.d.ts +24 -0
  37. package/dist/core/entity/abstract-entity.js +181 -0
  38. package/dist/core/entity/entity.d.ts +1 -1
  39. package/dist/core/entity/entity.js +209 -0
  40. package/dist/core/entity/event.d.ts +39 -0
  41. package/dist/core/entity/event.js +80 -0
  42. package/dist/core/entity/exceptions.d.ts +9 -0
  43. package/dist/core/entity/exceptions.js +27 -0
  44. package/dist/core/entity/trust.d.ts +24 -0
  45. package/dist/core/entity/trust.js +7 -0
  46. package/dist/core/entity/types/entity.interface.d.ts +30 -0
  47. package/dist/core/entity/version.js +13 -0
  48. package/dist/core/environment/environment.d.ts +26 -3
  49. package/dist/core/environment/environment.js +71 -0
  50. package/dist/core/event-policy/event-policy.js +76 -0
  51. package/dist/core/event-policy/exceptions.js +12 -0
  52. package/dist/core/event-policy/publish.js +98 -0
  53. package/dist/core/event-policy/publishers.js +18 -0
  54. package/dist/core/event-policy/types/event-policy.interface.d.ts +8 -2
  55. package/dist/core/exception/enums/exception-layers.js +15 -0
  56. package/dist/core/exception/exception.d.ts +15 -0
  57. package/dist/core/exception/exception.js +128 -0
  58. package/dist/core/exception/exported.js +49 -0
  59. package/dist/core/exception/guard.d.ts +20 -1
  60. package/dist/core/exception/guard.js +16 -0
  61. package/dist/core/exception/types/exception.interface.d.ts +13 -0
  62. package/dist/core/json.js +19 -0
  63. package/dist/core/logger/exceptions.js +7 -0
  64. package/dist/core/logger/logger.d.ts +14 -1
  65. package/dist/core/logger/logger.js +178 -0
  66. package/dist/core/openapi/exceptions.js +7 -0
  67. package/dist/core/openapi/openapi.js +132 -0
  68. package/dist/core/openapi/types/openapi.interface.d.ts +7 -1
  69. package/dist/core/output/access.d.ts +16 -0
  70. package/dist/core/output/access.js +66 -0
  71. package/dist/core/output/coerce.js +38 -0
  72. package/dist/core/output/exceptions.d.ts +16 -0
  73. package/dist/core/output/exceptions.js +22 -0
  74. package/dist/core/output/http.d.ts +36 -0
  75. package/dist/core/output/http.js +351 -0
  76. package/dist/core/output/openapi-sink.d.ts +33 -4
  77. package/dist/core/output/openapi-sink.js +571 -0
  78. package/dist/core/output/output.js +192 -0
  79. package/dist/core/output/problem.js +30 -0
  80. package/dist/core/output/select.js +28 -0
  81. package/dist/core/output/stage.d.ts +7 -2
  82. package/dist/core/output/stage.js +111 -0
  83. package/dist/core/output/status.d.ts +1 -1
  84. package/dist/core/output/status.js +52 -0
  85. package/dist/core/output/types/http.interface.d.ts +134 -0
  86. package/dist/core/output/types/output.interface.d.ts +49 -5
  87. package/dist/core/parse/issue.d.ts +28 -3
  88. package/dist/core/parse/issue.js +31 -0
  89. package/dist/core/parse/parse.d.ts +16 -0
  90. package/dist/core/parse/parse.js +323 -0
  91. package/dist/core/parse/strip.js +48 -0
  92. package/dist/core/property/abstract-property.d.ts +9 -0
  93. package/dist/core/property/abstract-property.js +37 -0
  94. package/dist/core/property/define-property.d.ts +34 -6
  95. package/dist/core/property/define-property.js +209 -0
  96. package/dist/core/property/enums/property-states.js +8 -0
  97. package/dist/core/property/exceptions.js +12 -0
  98. package/dist/core/property/input.js +10 -0
  99. package/dist/core/property/is-property.js +3 -0
  100. package/dist/core/property/lineage.js +8 -0
  101. package/dist/core/property/property.d.ts +27 -0
  102. package/dist/core/property/property.js +46 -0
  103. package/dist/core/property/refine.js +103 -0
  104. package/dist/core/property/types/property-options.interface.d.ts +10 -0
  105. package/dist/core/property/types/property-type.interface.d.ts +4 -0
  106. package/dist/core/record/abstract-record.d.ts +8 -0
  107. package/dist/core/record/abstract-record.js +307 -0
  108. package/dist/core/record/exceptions.js +49 -0
  109. package/dist/core/record/marks.js +7 -0
  110. package/dist/core/record/mask.js +60 -0
  111. package/dist/core/record/name.js +23 -0
  112. package/dist/core/record/plugin-catalog.js +115 -0
  113. package/dist/core/record/plugin.js +198 -0
  114. package/dist/core/record/record.js +117 -0
  115. package/dist/core/record/types/record.interface.d.ts +17 -1
  116. package/dist/core/repository/cursor.js +83 -0
  117. package/dist/core/repository/exceptions.d.ts +13 -0
  118. package/dist/core/repository/exceptions.js +72 -0
  119. package/dist/core/repository/in-memory.d.ts +2 -2
  120. package/dist/core/repository/in-memory.js +196 -0
  121. package/dist/core/repository/migrate.d.ts +20 -0
  122. package/dist/core/repository/migrate.js +254 -0
  123. package/dist/core/repository/plan.js +27 -0
  124. package/dist/core/repository/provider.js +29 -0
  125. package/dist/core/repository/query.js +153 -0
  126. package/dist/core/repository/references.js +196 -0
  127. package/dist/core/repository/repository.d.ts +22 -2
  128. package/dist/core/repository/repository.js +720 -0
  129. package/dist/core/repository/state.d.ts +2 -0
  130. package/dist/core/repository/state.js +138 -0
  131. package/dist/core/repository/types/query.interface.d.ts +13 -0
  132. package/dist/core/repository/types/repository.interface.d.ts +50 -5
  133. package/dist/core/repository/types/schema.interface.d.ts +132 -0
  134. package/dist/core/repository/upcast.d.ts +64 -0
  135. package/dist/core/repository/upcast.js +179 -0
  136. package/dist/core/schema/schema.d.ts +37 -0
  137. package/dist/core/schema/schema.js +542 -0
  138. package/dist/core/standard/standard-schema.js +1 -0
  139. package/dist/core/standard/standard.js +23 -0
  140. package/dist/core/symbols/demo.d.ts +4 -0
  141. package/dist/core/symbols/demo.js +1 -0
  142. package/dist/core/symbols/kind.d.ts +1 -1
  143. package/dist/core/symbols/kind.js +1 -0
  144. package/dist/core/symbols/meta.js +1 -0
  145. package/dist/core/symbols/state.d.ts +5 -0
  146. package/dist/core/symbols/state.js +1 -0
  147. package/dist/core/tracing/exceptions.d.ts +4 -0
  148. package/dist/core/tracing/exceptions.js +7 -0
  149. package/dist/core/tracing/tracing.d.ts +72 -0
  150. package/dist/core/tracing/tracing.js +101 -0
  151. package/dist/core/use-case/access.d.ts +12 -0
  152. package/dist/core/use-case/access.js +56 -0
  153. package/dist/core/use-case/decorators.d.ts +40 -0
  154. package/dist/core/use-case/decorators.js +65 -0
  155. package/dist/core/use-case/exceptions.d.ts +5 -1
  156. package/dist/core/use-case/exceptions.js +48 -0
  157. package/dist/core/use-case/plugin-catalog.js +68 -0
  158. package/dist/core/use-case/plugin.js +133 -0
  159. package/dist/core/use-case/route.d.ts +18 -0
  160. package/dist/core/use-case/route.js +200 -0
  161. package/dist/core/use-case/scope.d.ts +5 -0
  162. package/dist/core/use-case/scope.js +4 -0
  163. package/dist/core/use-case/steps/describe.d.ts +5 -1
  164. package/dist/core/use-case/steps/describe.js +24 -0
  165. package/dist/core/use-case/steps/references.js +105 -0
  166. package/dist/core/use-case/steps/run.js +139 -0
  167. package/dist/core/use-case/steps/types/check.interface.d.ts +233 -0
  168. package/dist/core/use-case/steps/types/steps.interface.d.ts +8 -1
  169. package/dist/core/use-case/steps/validate.js +368 -0
  170. package/dist/core/use-case/target.js +100 -0
  171. package/dist/core/use-case/types/access.interface.d.ts +39 -0
  172. package/dist/core/use-case/types/route.interface.d.ts +86 -0
  173. package/dist/core/use-case/use-case.d.ts +127 -2
  174. package/dist/core/use-case/use-case.js +493 -0
  175. package/dist/experimental.d.ts +18 -1
  176. package/dist/experimental.js +18 -123
  177. package/dist/index.d.ts +49 -51
  178. package/dist/index.js +213 -443
  179. package/dist/std/blueprint-plugin.js +42 -0
  180. package/dist/std/blueprint.d.ts +21 -0
  181. package/dist/std/blueprint.js +3 -0
  182. package/dist/std/command.js +3 -0
  183. package/dist/std/entity.d.ts +20 -1
  184. package/dist/std/entity.js +30 -0
  185. package/dist/std/event-policies.js +3 -0
  186. package/dist/std/properties/contact.d.ts +3 -0
  187. package/dist/std/properties/contact.js +29 -0
  188. package/dist/std/properties/enum.js +26 -0
  189. package/dist/std/properties/exceptions.js +14 -0
  190. package/dist/std/properties/headless.js +45 -0
  191. package/dist/std/properties/identity.js +28 -0
  192. package/dist/std/properties/json.js +161 -0
  193. package/dist/std/properties/password.d.ts +0 -2
  194. package/dist/std/properties/password.js +79 -0
  195. package/dist/std/properties/primitives.d.ts +4 -0
  196. package/dist/std/properties/primitives.js +28 -0
  197. package/dist/std/properties/time.js +55 -0
  198. package/dist/std/record-plugins.d.ts +2 -2
  199. package/dist/std/record-plugins.js +73 -0
  200. package/dist/std/record.d.ts +20 -1
  201. package/dist/std/record.js +3 -0
  202. package/dist/std/resource.d.ts +31 -0
  203. package/dist/std/resource.js +39 -0
  204. package/dist/std/runtime.d.ts +6 -0
  205. package/dist/std/runtime.js +193 -0
  206. package/dist/std/stages.js +4 -0
  207. package/dist/std/use-cases/auth.d.ts +5 -1
  208. package/dist/std/use-cases/auth.js +193 -0
  209. package/dist/std/use-cases/crud.js +261 -0
  210. package/dist/std/use-cases/index.d.ts +1 -1
  211. package/dist/std/use-cases/index.js +12 -0
  212. package/dist/std/use-cases/lifecycle.js +97 -0
  213. package/dist/std/use-cases/shared.d.ts +2 -2
  214. package/dist/std/use-cases/shared.js +232 -0
  215. package/dist/std/use-cases/types.d.ts +102 -2
  216. package/dist/std/use-cases/types.js +0 -0
  217. package/dist/testing/http-provider-contract.d.ts +32 -0
  218. package/dist/testing/http-provider-contract.js +344 -0
  219. package/dist/testing/index.d.ts +2 -0
  220. package/dist/testing/index.js +3 -944
  221. package/dist/testing/memory-publisher.js +12 -0
  222. package/dist/testing/provider-contract.d.ts +26 -1
  223. package/dist/testing/provider-contract.js +1071 -0
  224. package/package.json +23 -3
  225. package/dist/index-qxfkyd6k.js +0 -1721
  226. package/dist/index-t5qwtyj5.js +0 -8194
@@ -0,0 +1,571 @@
1
+ import { isBlueprint } from "../blueprint/is-blueprint.js";
2
+ import { isBlueprintList } from "../blueprint/list.js";
3
+ import { recordFieldOf } from "../blueprint/record-field.js";
4
+ import { isBlueprintVariant } from "../blueprint/variant.js";
5
+ import { ExceptionLayers } from "../exception/enums/exception-layers.js";
6
+ import { Exception } from "../exception/exception.js";
7
+ import { toOpenAPI } from "../openapi/openapi.js";
8
+ import { isProperty } from "../property/is-property.js";
9
+ import { recordNameOf } from "../record/name.js";
10
+ import {
11
+ HEADER_NAME,
12
+ isRoutePath as isPath,
13
+ isScalarKey as isScalarInputKey,
14
+ isScalarSchema,
15
+ parametersOfPath
16
+ } from "../use-case/route.js";
17
+ import { InvalidOutputStage } from "./exceptions.js";
18
+ import { PROBLEM_MEDIA_TYPE } from "./problem.js";
19
+ import { sinkOf } from "./stage.js";
20
+ import { statusOf } from "./status.js";
21
+ const METHODS = ["get", "post", "put", "patch", "delete"];
22
+ const SUCCESS_STATUSES = [200, 201, 202, 204];
23
+ const REQUEST_ID_HEADER = "x-request-id";
24
+ function isObject(value) {
25
+ return typeof value === "object" && value !== null && !Array.isArray(value);
26
+ }
27
+ function onlyKeys(value, allowed, where) {
28
+ for (const key of Object.keys(value)) {
29
+ if (!allowed.includes(key))
30
+ return `${where} has no option "${key}".`;
31
+ }
32
+ return;
33
+ }
34
+ const COMPONENT_NAME = /^[A-Za-z0-9._-]+$/;
35
+ function isHeaderValue(value) {
36
+ if (typeof value !== "string")
37
+ return false;
38
+ for (let i = 0;i < value.length; i++) {
39
+ const code = value.charCodeAt(i);
40
+ if (code < 32 && code !== 9 || code === 127)
41
+ return false;
42
+ }
43
+ return true;
44
+ }
45
+ function isScopes(value) {
46
+ return value === undefined || Array.isArray(value) && value.every((scope) => typeof scope === "string" && scope !== "");
47
+ }
48
+ const SCHEME_KEYS = {
49
+ http: ["scheme", "bearerFormat"],
50
+ apiKey: ["name", "in"],
51
+ oauth2: ["flows"],
52
+ openIdConnect: ["openIdConnectUrl"],
53
+ mutualTLS: []
54
+ };
55
+ function schemeIssue(scheme, where) {
56
+ if (!isObject(scheme) || typeof scheme.type !== "string") {
57
+ return `Expected ${where} to be a security scheme with a "type".`;
58
+ }
59
+ const keys = Object.hasOwn(SCHEME_KEYS, scheme.type) ? SCHEME_KEYS[scheme.type] : undefined;
60
+ if (!keys) {
61
+ return `Expected ${where}.type to be one of ${Object.keys(SCHEME_KEYS).join(", ")}.`;
62
+ }
63
+ const bad = onlyKeys(scheme, ["type", "description", ...keys], where);
64
+ if (bad)
65
+ return bad;
66
+ if (scheme.description !== undefined && typeof scheme.description !== "string") {
67
+ return `Expected ${where}.description to be a string.`;
68
+ }
69
+ switch (scheme.type) {
70
+ case "http":
71
+ if (typeof scheme.scheme !== "string" || !scheme.scheme) {
72
+ return `Expected ${where}.scheme to be a scheme such as "bearer".`;
73
+ }
74
+ if (scheme.bearerFormat !== undefined && typeof scheme.bearerFormat !== "string") {
75
+ return `Expected ${where}.bearerFormat to be a string.`;
76
+ }
77
+ return;
78
+ case "apiKey":
79
+ if (typeof scheme.name !== "string" || !scheme.name) {
80
+ return `Expected ${where}.name to be a non-empty string.`;
81
+ }
82
+ if (!["header", "query", "cookie"].includes(scheme.in)) {
83
+ return `Expected ${where}.in to be "header", "query" or "cookie".`;
84
+ }
85
+ return;
86
+ case "oauth2":
87
+ return isObject(scheme.flows) ? undefined : `Expected ${where}.flows to be an object.`;
88
+ case "openIdConnect":
89
+ return typeof scheme.openIdConnectUrl === "string" && scheme.openIdConnectUrl ? undefined : `Expected ${where}.openIdConnectUrl to be a URL.`;
90
+ default:
91
+ return;
92
+ }
93
+ }
94
+ function requirementIssue(requirement, schemes, where) {
95
+ if (!isObject(requirement) || onlyKeys(requirement, ["scheme", "scopes"], where) || typeof requirement.scheme !== "string" || !isScopes(requirement.scopes)) {
96
+ return `Expected ${where} to be { scheme, scopes? }.`;
97
+ }
98
+ if (!Object.hasOwn(schemes, requirement.scheme)) {
99
+ return `${where}.scheme names the unknown security scheme "${requirement.scheme}".`;
100
+ }
101
+ return;
102
+ }
103
+ function securityIssue(security, where) {
104
+ if (!isObject(security))
105
+ return `Expected ${where} to be an object.`;
106
+ const bad = onlyKeys(security, ["schemes", "operations", "default"], where);
107
+ if (bad)
108
+ return bad;
109
+ const { schemes, operations } = security;
110
+ if (!isObject(schemes))
111
+ return `Expected ${where}.schemes to be an object.`;
112
+ for (const [name, scheme] of Object.entries(schemes)) {
113
+ if (!COMPONENT_NAME.test(name)) {
114
+ return `Expected the security scheme "${name}" to be named with letters, digits, ".", "_" or "-".`;
115
+ }
116
+ const schemeBad = schemeIssue(scheme, `${where}.schemes["${name}"]`);
117
+ if (schemeBad)
118
+ return schemeBad;
119
+ }
120
+ if (operations !== undefined) {
121
+ if (!isObject(operations)) {
122
+ return `Expected ${where}.operations to be an object.`;
123
+ }
124
+ for (const [id, requirement] of Object.entries(operations)) {
125
+ if (requirement === false)
126
+ continue;
127
+ const requirementBad = requirementIssue(requirement, schemes, `${where}.operations["${id}"]`);
128
+ if (requirementBad)
129
+ return requirementBad;
130
+ }
131
+ }
132
+ if (security.default !== undefined) {
133
+ return requirementIssue(security.default, schemes, `${where}.default`);
134
+ }
135
+ return;
136
+ }
137
+ export const ROUTE_OPTION_KEYS = [
138
+ "basePath",
139
+ "route",
140
+ "responses",
141
+ "requestId",
142
+ "security"
143
+ ];
144
+ export function routeOptionsIssue(options, where) {
145
+ const { basePath, route, responses, requestId, security } = options;
146
+ if (basePath !== undefined && (typeof basePath !== "string" || basePath !== "" && !isPath(basePath, false))) {
147
+ return `Expected ${where}.basePath to be "" or a path such as "/api".`;
148
+ }
149
+ if (route !== undefined) {
150
+ if (!isObject(route))
151
+ return `Expected ${where}.route to be an object.`;
152
+ for (const [id, entry] of Object.entries(route)) {
153
+ if (!isObject(entry) || onlyKeys(entry, ["method", "path", "status"], "route")) {
154
+ return `Expected ${where}.route["${id}"] to be { method?, path?, status? }.`;
155
+ }
156
+ if (entry.method !== undefined && !METHODS.includes(entry.method)) {
157
+ return `Expected ${where}.route["${id}"].method to be one of ${METHODS.join(", ")}.`;
158
+ }
159
+ if (entry.path !== undefined) {
160
+ if (!isPath(entry.path)) {
161
+ return `Expected ${where}.route["${id}"].path to start with "/", with parameters only as whole "{name}" segments and no query.`;
162
+ }
163
+ const names = parametersOfPath(entry.path);
164
+ if (new Set(names).size !== names.length) {
165
+ return `Expected ${where}.route["${id}"].path to name each parameter once.`;
166
+ }
167
+ }
168
+ if (entry.status !== undefined && !SUCCESS_STATUSES.includes(entry.status)) {
169
+ return `Expected ${where}.route["${id}"].status to be one of ${SUCCESS_STATUSES.join(", ")}.`;
170
+ }
171
+ }
172
+ }
173
+ if (responses !== undefined) {
174
+ if (!isObject(responses)) {
175
+ return `Expected ${where}.responses to be an object.`;
176
+ }
177
+ for (const [id, entry] of Object.entries(responses)) {
178
+ const at = `${where}.responses["${id}"]`;
179
+ if (!isObject(entry) || onlyKeys(entry, ["headers"], at) || entry.headers !== undefined && !isObject(entry.headers)) {
180
+ return `Expected ${at} to be { headers?: { name: value } }.`;
181
+ }
182
+ for (const [name, value] of Object.entries(entry.headers ?? {})) {
183
+ if (!HEADER_NAME.test(name)) {
184
+ return `Expected ${at}.headers to be named by header names, not "${name}".`;
185
+ }
186
+ if (name.toLowerCase() === "content-type") {
187
+ return `${at}.headers cannot set "content-type": the sink sets it.`;
188
+ }
189
+ if (!isHeaderValue(value)) {
190
+ return `Expected ${at}.headers["${name}"] to be text without line breaks.`;
191
+ }
192
+ }
193
+ }
194
+ }
195
+ if (requestId !== undefined && requestId !== true && (typeof requestId !== "string" || !HEADER_NAME.test(requestId))) {
196
+ return `Expected ${where}.requestId to be true or a header name.`;
197
+ }
198
+ if (security !== undefined) {
199
+ return securityIssue(security, `${where}.security`);
200
+ }
201
+ return;
202
+ }
203
+ export function requestIdHeaderOf(options) {
204
+ return options.requestId === true ? REQUEST_ID_HEADER : options.requestId ?? undefined;
205
+ }
206
+ function validate(options) {
207
+ const bad = onlyKeys(options, ["info", "servers", ...ROUTE_OPTION_KEYS], "openapi");
208
+ if (bad)
209
+ return bad;
210
+ const { info, servers } = options;
211
+ if (!isObject(info))
212
+ return "Expected openapi.info to be an object.";
213
+ const infoBad = onlyKeys(info, ["title", "version", "description"], "info");
214
+ if (infoBad)
215
+ return infoBad;
216
+ for (const key of ["title", "version"]) {
217
+ if (typeof info[key] !== "string" || !info[key]) {
218
+ return `Expected openapi.info.${key} to be a non-empty string.`;
219
+ }
220
+ }
221
+ if (info.description !== undefined && typeof info.description !== "string") {
222
+ return "Expected openapi.info.description to be a string.";
223
+ }
224
+ if (servers !== undefined) {
225
+ if (!Array.isArray(servers))
226
+ return "Expected openapi.servers to be a list.";
227
+ for (const server of servers) {
228
+ if (!isObject(server) || typeof server.url !== "string" || !server.url || server.description !== undefined && typeof server.description !== "string" || onlyKeys(server, ["url", "description"], "server")) {
229
+ return "Expected each openapi.servers entry to be { url, description? }.";
230
+ }
231
+ }
232
+ }
233
+ return routeOptionsIssue(options, "openapi");
234
+ }
235
+ function invalid(reason, source = "openapi") {
236
+ throw new InvalidOutputStage({ source, details: { reason } });
237
+ }
238
+ function kebab(text) {
239
+ return text.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
240
+ }
241
+ function isScalarKey(operation, name) {
242
+ return isScalarInputKey(operation.input, name);
243
+ }
244
+ export function operationRoutes(operations, options, source = "openapi") {
245
+ const base = options.basePath ?? "";
246
+ const route = options.route ?? {};
247
+ const responses = options.responses ?? {};
248
+ const ids = new Set(operations.map(({ id }) => id));
249
+ for (const [where, entries] of [
250
+ ["route", route],
251
+ ["responses", responses],
252
+ ["security.operations", options.security?.operations ?? {}]
253
+ ]) {
254
+ for (const id of Object.keys(entries)) {
255
+ if (!ids.has(id)) {
256
+ invalid(`${source}.${where} names the unknown operation "${id}".`, source);
257
+ }
258
+ }
259
+ }
260
+ const routes = new Map;
261
+ const taken = new Map;
262
+ for (const operation of operations) {
263
+ const chosen = route[operation.id] ?? {};
264
+ const declared = operation.route;
265
+ const method = chosen.method ?? declared?.method ?? "post";
266
+ const path = base + (chosen.path ?? declared?.path ?? `/${kebab(operation.command)}/${kebab(operation.name)}`);
267
+ const parameters = parametersOfPath(path);
268
+ const query = declared?.query ?? [];
269
+ const requestHeaders = declared?.headers ?? {};
270
+ const cookies = declared?.cookie ?? {};
271
+ for (const name of parameters) {
272
+ if (!isScalarKey(operation, name)) {
273
+ invalid(`${source}.route["${operation.id}"].path has the parameter "{${name}}", which is not a scalar key of the input of ${operation.id}.`, source);
274
+ }
275
+ if (query.includes(name) || Object.hasOwn(requestHeaders, name) || Object.hasOwn(cookies, name)) {
276
+ invalid(`${source}.route["${operation.id}"].path has the parameter "{${name}}", which the route of ${operation.useCase} reads from another place.`, source);
277
+ }
278
+ }
279
+ const key = `${method} ${path.replace(/\{[^}]*\}/g, "{}")}`;
280
+ const other = taken.get(key);
281
+ if (other !== undefined) {
282
+ invalid(`Two operations are routed to ${method.toUpperCase()} ${path}: ${other} and ${operation.id}. Give one another route, on its use case or in ${source}.route["${operation.id}"].`, source);
283
+ }
284
+ taken.set(key, operation.id);
285
+ routes.set(operation.id, {
286
+ method,
287
+ path,
288
+ parameters,
289
+ status: chosen.status ?? declared?.status ?? 200,
290
+ headers: { ...responses[operation.id]?.headers ?? {} },
291
+ body: method !== "get" && method !== "delete",
292
+ query,
293
+ requestHeaders,
294
+ cookies,
295
+ setCookies: declared?.setCookie ?? {}
296
+ });
297
+ }
298
+ return routes;
299
+ }
300
+ function collectRecords(properties, found) {
301
+ const visit = (field) => {
302
+ if (isBlueprintList(field)) {
303
+ visit(field.item);
304
+ return;
305
+ }
306
+ if (isBlueprint(field)) {
307
+ collectRecords(field.properties, found);
308
+ return;
309
+ }
310
+ if (isBlueprintVariant(field)) {
311
+ for (const variant of Object.values(field.variants)) {
312
+ visit(variant);
313
+ }
314
+ return;
315
+ }
316
+ const record = recordFieldOf(field)?.record;
317
+ if (record && !found.has(record)) {
318
+ found.add(record);
319
+ collectRecords(record.blueprint.properties, found);
320
+ }
321
+ };
322
+ for (const field of Object.values(properties))
323
+ visit(field);
324
+ }
325
+ function problemSchema() {
326
+ return {
327
+ type: "object",
328
+ properties: {
329
+ type: { type: "string" },
330
+ title: { type: "string" },
331
+ status: { type: "integer" },
332
+ detail: { type: "string" },
333
+ layer: { type: "string" },
334
+ issues: {
335
+ type: "array",
336
+ items: {
337
+ type: "object",
338
+ properties: {
339
+ path: {
340
+ type: "array",
341
+ items: { type: ["string", "integer"] }
342
+ },
343
+ code: { type: "string" },
344
+ message: { type: "string" }
345
+ },
346
+ required: ["path", "code", "message"]
347
+ }
348
+ },
349
+ trace: { type: "string" }
350
+ },
351
+ required: ["type", "title", "status", "detail"]
352
+ };
353
+ }
354
+ const PROBLEM_REF = "#/components/schemas/Problem";
355
+ const problemContent = {
356
+ [PROBLEM_MEDIA_TYPE]: { schema: { $ref: PROBLEM_REF } }
357
+ };
358
+ const STATUS_TEXT = {
359
+ 400: "Invalid input or context.",
360
+ 401: "Authentication is required.",
361
+ 403: "The caller may not do this.",
362
+ 404: "Not found.",
363
+ 409: "Conflict.",
364
+ 422: "Declared error of the operation.",
365
+ 500: "Internal error."
366
+ };
367
+ const SUCCESS_TEXT = {
368
+ 200: "Success.",
369
+ 201: "Created.",
370
+ 202: "Accepted.",
371
+ 204: "No content."
372
+ };
373
+ function responsesOf(operation, statuses, response, route, requestIdHeader) {
374
+ const names = new Map([
375
+ [400, []],
376
+ [500, []]
377
+ ]);
378
+ const access = operation.access;
379
+ if (access && access.public !== true) {
380
+ names.set(401, []);
381
+ if ((access.scopes?.length ?? 0) > 0 || access.owner)
382
+ names.set(403, []);
383
+ }
384
+ for (const Declared of operation.errors) {
385
+ const synthetic = new Exception({
386
+ layer: ExceptionLayers.UseCase,
387
+ name: Declared.name,
388
+ source: "openapi",
389
+ message: Declared.name
390
+ });
391
+ const status = statusOf(synthetic, operation, statuses);
392
+ const list = names.get(status) ?? [];
393
+ if (!list.includes(Declared.name))
394
+ list.push(Declared.name);
395
+ names.set(status, list);
396
+ }
397
+ const idHeaders = requestIdHeader ? {
398
+ [requestIdHeader]: {
399
+ description: "The id of the request: the one it sent in this header, else a new one.",
400
+ schema: { type: "string" }
401
+ }
402
+ } : {};
403
+ const successHeaders = { ...idHeaders };
404
+ const setCookies = Object.values(route.setCookies);
405
+ if (setCookies.length > 0) {
406
+ successHeaders["set-cookie"] = {
407
+ description: `Sets ${setCookies.map(({ name }) => `\`${name}\``).join(", ")}.`,
408
+ schema: { type: "string" }
409
+ };
410
+ }
411
+ for (const [name, value] of Object.entries(route.headers)) {
412
+ successHeaders[name] = { schema: { type: "string", const: value } };
413
+ }
414
+ const withHeaders = (headers) => Object.keys(headers).length > 0 ? { headers } : {};
415
+ const responses = {
416
+ [String(route.status)]: {
417
+ description: SUCCESS_TEXT[route.status],
418
+ ...withHeaders(successHeaders),
419
+ ...route.status === 204 ? {} : { content: { "application/json": { schema: response } } }
420
+ }
421
+ };
422
+ for (const status of [...names.keys()].sort((a, b) => a - b)) {
423
+ const declared = names.get(status) ?? [];
424
+ const text = STATUS_TEXT[status] ?? `Error ${status}.`;
425
+ responses[String(status)] = {
426
+ description: declared.length > 0 ? `${text} Declared: ${declared.join(", ")}.` : text,
427
+ ...withHeaders(idHeaders),
428
+ content: problemContent
429
+ };
430
+ }
431
+ return responses;
432
+ }
433
+ function parameterOf(name, property, where, required) {
434
+ const { description, ...rest } = property;
435
+ return {
436
+ name,
437
+ in: where,
438
+ required: where === "path" || required,
439
+ ...typeof description === "string" ? { description } : {},
440
+ schema: rest
441
+ };
442
+ }
443
+ function queryParametersOf(operation, schema) {
444
+ const required = new Set(schema.required ?? []);
445
+ return Object.entries(schema.properties ?? {}).map(([name, property]) => {
446
+ if (!isProperty(operation.input.properties[name]) || !isScalarSchema(property)) {
447
+ invalid(`${operation.id} uses a get/delete route, whose input becomes query parameters, but "${name}" is not a scalar; route it with method "post".`);
448
+ }
449
+ return parameterOf(name, property, "query", required.has(name));
450
+ });
451
+ }
452
+ function withoutKeys(schema, keys) {
453
+ if (keys.length === 0)
454
+ return schema;
455
+ const properties = { ...schema.properties ?? {} };
456
+ for (const key of keys)
457
+ delete properties[key];
458
+ const required = (schema.required ?? []).filter((key) => !keys.includes(key));
459
+ const { required: _, ...rest } = schema;
460
+ return {
461
+ ...rest,
462
+ properties,
463
+ ...required.length > 0 ? { required } : {}
464
+ };
465
+ }
466
+ function requirementOf(requirement) {
467
+ return { [requirement.scheme]: [...requirement.scopes ?? []] };
468
+ }
469
+ export const openapi = sinkOf({
470
+ name: "openapi",
471
+ validate,
472
+ build: (operations, options, { output }) => {
473
+ const routes = operationRoutes(operations, options);
474
+ const requestIdHeader = requestIdHeaderOf(options);
475
+ const security = options.security;
476
+ const records = new Set;
477
+ for (const operation of operations) {
478
+ collectRecords(operation.input.properties, records);
479
+ collectRecords(operation.response.properties, records);
480
+ }
481
+ const models = {};
482
+ const classes = new Map;
483
+ for (const record of records) {
484
+ const name = recordNameOf(record);
485
+ if (!COMPONENT_NAME.test(name) || name === "Problem") {
486
+ invalid(`The record class "${name}" cannot be a component name; give the class a name that is not "Problem".`);
487
+ }
488
+ if (classes.has(name)) {
489
+ invalid(`Two record classes are named "${name}"; rename one.`);
490
+ }
491
+ classes.set(name, record);
492
+ models[name] = record;
493
+ }
494
+ for (const operation of operations) {
495
+ models[`${operation.id}#input`] = operation.input;
496
+ models[`${operation.id}#response`] = operation.response;
497
+ }
498
+ const { schemas } = toOpenAPI(models);
499
+ const paths = {};
500
+ const operationIds = new Set;
501
+ const components = {};
502
+ for (const name of classes.keys())
503
+ components[name] = schemas[name];
504
+ components.Problem = problemSchema();
505
+ for (const operation of operations) {
506
+ const route = routes.get(operation.id);
507
+ const { method, path } = route;
508
+ const operationId = operation.id.replaceAll(".", "_");
509
+ if (operationIds.has(operationId)) {
510
+ invalid(`The operationId "${operationId}" is produced twice.`);
511
+ }
512
+ operationIds.add(operationId);
513
+ const slot = paths[path] ?? {};
514
+ paths[path] = slot;
515
+ const full = schemas[`${operation.id}#input`];
516
+ const required = new Set(full.required ?? []);
517
+ const hasBody = route.body;
518
+ const input = withoutKeys(full, [
519
+ ...route.parameters,
520
+ ...hasBody ? route.query : [],
521
+ ...Object.keys(route.requestHeaders),
522
+ ...Object.keys(route.cookies)
523
+ ]);
524
+ const response = withoutKeys(schemas[`${operation.id}#response`], Object.keys(route.setCookies));
525
+ const hasInput = Object.keys(input.properties ?? {}).length > 0;
526
+ const placed = (keys, where) => Object.entries(keys).map(([key, name]) => parameterOf(name, full.properties?.[key], where, required.has(key)));
527
+ const parameters = [
528
+ ...route.parameters.map((name) => parameterOf(name, full.properties?.[name], "path", true)),
529
+ ...hasBody ? route.query.map((name) => parameterOf(name, full.properties?.[name], "query", required.has(name))) : queryParametersOf(operation, input),
530
+ ...placed(route.requestHeaders, "header"),
531
+ ...placed(route.cookies, "cookie")
532
+ ];
533
+ const explicit = security?.operations?.[operation.id];
534
+ const access = operation.access;
535
+ const guarded = access !== undefined && access.public !== true;
536
+ if (guarded && explicit === false) {
537
+ invalid(`openapi.security.operations["${operation.id}"] is false, but ${operation.useCase} declares an access that the output enforces.`);
538
+ }
539
+ const schemeNames = Object.keys(security?.schemes ?? {});
540
+ const scheme = security?.default?.scheme ?? (schemeNames.length === 1 ? schemeNames[0] : undefined);
541
+ const requirement = explicit !== undefined ? explicit : guarded && scheme !== undefined ? { scheme, scopes: access.scopes ?? [] } : access?.public === true && security?.default ? false : undefined;
542
+ slot[method] = {
543
+ operationId,
544
+ summary: operation.id,
545
+ ...operation.description ? { description: operation.description } : {},
546
+ ...operation.tags.length > 0 ? { tags: [...operation.tags] } : {},
547
+ ...operation.deprecated ? { deprecated: true } : {},
548
+ ...!hasBody || parameters.length > 0 ? { parameters } : {},
549
+ ...hasBody && hasInput ? {
550
+ requestBody: {
551
+ required: true,
552
+ content: { "application/json": { schema: input } }
553
+ }
554
+ } : {},
555
+ responses: responsesOf(operation, output.statuses, response, route, requestIdHeader),
556
+ ...requirement === false ? { security: [] } : requirement ? { security: [requirementOf(requirement)] } : {}
557
+ };
558
+ }
559
+ return {
560
+ openapi: "3.1.0",
561
+ info: options.info,
562
+ ...options.servers ? { servers: options.servers } : {},
563
+ ...security?.default ? { security: [requirementOf(security.default)] } : {},
564
+ paths,
565
+ components: {
566
+ schemas: components,
567
+ ...security ? { securitySchemes: security.schemes } : {}
568
+ }
569
+ };
570
+ }
571
+ });