@harperfast/harper 5.2.0-alpha.5 → 5.2.0-beta.1

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 (311) hide show
  1. package/agent/agent.ts +152 -10
  2. package/agent/bestPractices.ts +58 -0
  3. package/agent/mcpTools.ts +122 -0
  4. package/agent/operations.ts +10 -1
  5. package/agent/registryTools.ts +117 -0
  6. package/agent/session.ts +16 -7
  7. package/agent/tools/fsTools.ts +74 -27
  8. package/agent/tools/inspectorTool.ts +459 -0
  9. package/agent/toolset.ts +27 -7
  10. package/agent/types.ts +2 -0
  11. package/components/Application.ts +566 -85
  12. package/components/OptionsWatcher.ts +62 -2
  13. package/components/Scope.ts +31 -8
  14. package/components/componentLoader.ts +5 -1
  15. package/components/componentSecrets.ts +514 -55
  16. package/components/deploymentRecorder.ts +7 -6
  17. package/components/gitCredentialHelper.js +115 -0
  18. package/components/gitCredentialServer.ts +251 -0
  19. package/components/mcp/tools/application.ts +197 -23
  20. package/components/mcp/tools/operations.ts +1 -1
  21. package/components/mcp/tools/schemas/derive.ts +6 -2
  22. package/components/operations.js +21 -19
  23. package/components/operationsValidation.js +86 -21
  24. package/components/secretOperations.ts +110 -30
  25. package/config/harperConfigEnvVars.ts +90 -5
  26. package/config-root.schema.json +4 -0
  27. package/dataLayer/harperBridge/ResourceBridge.ts +7 -0
  28. package/dist/agent/agent.d.ts +24 -3
  29. package/dist/agent/agent.js +172 -10
  30. package/dist/agent/agent.js.map +1 -1
  31. package/dist/agent/bestPractices.d.ts +24 -0
  32. package/dist/agent/bestPractices.js +60 -0
  33. package/dist/agent/bestPractices.js.map +1 -0
  34. package/dist/agent/mcpTools.d.ts +24 -0
  35. package/dist/agent/mcpTools.js +115 -0
  36. package/dist/agent/mcpTools.js.map +1 -0
  37. package/dist/agent/operations.js +10 -1
  38. package/dist/agent/operations.js.map +1 -1
  39. package/dist/agent/registryTools.d.ts +45 -0
  40. package/dist/agent/registryTools.js +113 -0
  41. package/dist/agent/registryTools.js.map +1 -0
  42. package/dist/agent/session.js +16 -7
  43. package/dist/agent/session.js.map +1 -1
  44. package/dist/agent/tools/fsTools.js +70 -28
  45. package/dist/agent/tools/fsTools.js.map +1 -1
  46. package/dist/agent/tools/inspectorTool.d.ts +40 -0
  47. package/dist/agent/tools/inspectorTool.js +428 -0
  48. package/dist/agent/tools/inspectorTool.js.map +1 -0
  49. package/dist/agent/toolset.d.ts +16 -6
  50. package/dist/agent/toolset.js +17 -7
  51. package/dist/agent/toolset.js.map +1 -1
  52. package/dist/agent/types.d.ts +2 -0
  53. package/dist/components/Application.d.ts +73 -18
  54. package/dist/components/Application.js +475 -64
  55. package/dist/components/Application.js.map +1 -1
  56. package/dist/components/OptionsWatcher.d.ts +1 -1
  57. package/dist/components/OptionsWatcher.js +65 -2
  58. package/dist/components/OptionsWatcher.js.map +1 -1
  59. package/dist/components/Scope.d.ts +8 -6
  60. package/dist/components/Scope.js +22 -6
  61. package/dist/components/Scope.js.map +1 -1
  62. package/dist/components/componentLoader.js +5 -1
  63. package/dist/components/componentLoader.js.map +1 -1
  64. package/dist/components/componentSecrets.d.ts +33 -7
  65. package/dist/components/componentSecrets.js +473 -59
  66. package/dist/components/componentSecrets.js.map +1 -1
  67. package/dist/components/deploymentRecorder.d.ts +2 -2
  68. package/dist/components/deploymentRecorder.js +1 -1
  69. package/dist/components/deploymentRecorder.js.map +1 -1
  70. package/dist/components/gitCredentialHelper.d.ts +1 -0
  71. package/dist/components/gitCredentialHelper.js +113 -0
  72. package/dist/components/gitCredentialHelper.js.map +1 -0
  73. package/dist/components/gitCredentialServer.d.ts +33 -0
  74. package/dist/components/gitCredentialServer.js +236 -0
  75. package/dist/components/gitCredentialServer.js.map +1 -0
  76. package/dist/components/mcp/tools/application.d.ts +19 -0
  77. package/dist/components/mcp/tools/application.js +180 -22
  78. package/dist/components/mcp/tools/application.js.map +1 -1
  79. package/dist/components/mcp/tools/operations.d.ts +13 -0
  80. package/dist/components/mcp/tools/operations.js +1 -0
  81. package/dist/components/mcp/tools/operations.js.map +1 -1
  82. package/dist/components/mcp/tools/schemas/derive.js +6 -2
  83. package/dist/components/mcp/tools/schemas/derive.js.map +1 -1
  84. package/dist/components/operations.js +23 -21
  85. package/dist/components/operations.js.map +1 -1
  86. package/dist/components/operationsValidation.js +84 -21
  87. package/dist/components/operationsValidation.js.map +1 -1
  88. package/dist/components/secretOperations.d.ts +33 -11
  89. package/dist/components/secretOperations.js +90 -26
  90. package/dist/components/secretOperations.js.map +1 -1
  91. package/dist/config/harperConfigEnvVars.d.ts +21 -0
  92. package/dist/config/harperConfigEnvVars.js +95 -5
  93. package/dist/config/harperConfigEnvVars.js.map +1 -1
  94. package/dist/dataLayer/harperBridge/ResourceBridge.js +8 -0
  95. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  96. package/dist/index.d.ts +4 -0
  97. package/dist/index.js +14 -1
  98. package/dist/index.js.map +1 -1
  99. package/dist/resources/Resource.d.ts +19 -0
  100. package/dist/resources/Resource.js +98 -4
  101. package/dist/resources/Resource.js.map +1 -1
  102. package/dist/resources/Table.d.ts +12 -1
  103. package/dist/resources/Table.js +269 -53
  104. package/dist/resources/Table.js.map +1 -1
  105. package/dist/resources/analytics/read.js +28 -25
  106. package/dist/resources/analytics/read.js.map +1 -1
  107. package/dist/resources/databases.js +21 -0
  108. package/dist/resources/databases.js.map +1 -1
  109. package/dist/resources/defineResource.d.ts +180 -0
  110. package/dist/resources/defineResource.js +505 -0
  111. package/dist/resources/defineResource.js.map +1 -0
  112. package/dist/resources/defineTable.d.ts +221 -0
  113. package/dist/resources/defineTable.js +227 -0
  114. package/dist/resources/defineTable.js.map +1 -0
  115. package/dist/resources/jsonSchemaTypes.d.ts +2 -0
  116. package/dist/resources/jsonSchemaTypes.js +12 -4
  117. package/dist/resources/jsonSchemaTypes.js.map +1 -1
  118. package/dist/resources/openApi.js +69 -13
  119. package/dist/resources/openApi.js.map +1 -1
  120. package/dist/resources/search.js +6 -8
  121. package/dist/resources/search.js.map +1 -1
  122. package/dist/server/graphqlQuerying.js +4 -2
  123. package/dist/server/graphqlQuerying.js.map +1 -1
  124. package/dist/server/http.d.ts +12 -0
  125. package/dist/server/http.js +35 -15
  126. package/dist/server/http.js.map +1 -1
  127. package/dist/server/serverHelpers/serverUtilities.js +8 -5
  128. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  129. package/dist/server/serverHelpers/uwsServer.js +23 -0
  130. package/dist/server/serverHelpers/uwsServer.js.map +1 -1
  131. package/dist/sqlEngine/binder/bind.d.ts +72 -0
  132. package/dist/sqlEngine/binder/bind.js +289 -0
  133. package/dist/sqlEngine/binder/bind.js.map +1 -0
  134. package/dist/sqlEngine/config.d.ts +35 -0
  135. package/dist/sqlEngine/config.js +62 -0
  136. package/dist/sqlEngine/config.js.map +1 -0
  137. package/dist/sqlEngine/diff/differential.d.ts +23 -0
  138. package/dist/sqlEngine/diff/differential.js +90 -0
  139. package/dist/sqlEngine/diff/differential.js.map +1 -0
  140. package/dist/sqlEngine/errors.d.ts +22 -0
  141. package/dist/sqlEngine/errors.js +36 -0
  142. package/dist/sqlEngine/errors.js.map +1 -0
  143. package/dist/sqlEngine/executor/runMutation.d.ts +35 -0
  144. package/dist/sqlEngine/executor/runMutation.js +352 -0
  145. package/dist/sqlEngine/executor/runMutation.js.map +1 -0
  146. package/dist/sqlEngine/executor/runSelect.d.ts +7 -0
  147. package/dist/sqlEngine/executor/runSelect.js +14 -0
  148. package/dist/sqlEngine/executor/runSelect.js.map +1 -0
  149. package/dist/sqlEngine/expressions/compile.d.ts +24 -0
  150. package/dist/sqlEngine/expressions/compile.js +326 -0
  151. package/dist/sqlEngine/expressions/compile.js.map +1 -0
  152. package/dist/sqlEngine/functions/aggregates.d.ts +12 -0
  153. package/dist/sqlEngine/functions/aggregates.js +299 -0
  154. package/dist/sqlEngine/functions/aggregates.js.map +1 -0
  155. package/dist/sqlEngine/functions/registry.d.ts +36 -0
  156. package/dist/sqlEngine/functions/registry.js +33 -0
  157. package/dist/sqlEngine/functions/registry.js.map +1 -0
  158. package/dist/sqlEngine/functions/standard.d.ts +9 -0
  159. package/dist/sqlEngine/functions/standard.js +86 -0
  160. package/dist/sqlEngine/functions/standard.js.map +1 -0
  161. package/dist/sqlEngine/index.d.ts +29 -0
  162. package/dist/sqlEngine/index.js +62 -0
  163. package/dist/sqlEngine/index.js.map +1 -0
  164. package/dist/sqlEngine/logical/build.d.ts +20 -0
  165. package/dist/sqlEngine/logical/build.js +326 -0
  166. package/dist/sqlEngine/logical/build.js.map +1 -0
  167. package/dist/sqlEngine/logical/op.d.ts +105 -0
  168. package/dist/sqlEngine/logical/op.js +10 -0
  169. package/dist/sqlEngine/logical/op.js.map +1 -0
  170. package/dist/sqlEngine/optimizer/joinAnalysis.d.ts +37 -0
  171. package/dist/sqlEngine/optimizer/joinAnalysis.js +144 -0
  172. package/dist/sqlEngine/optimizer/joinAnalysis.js.map +1 -0
  173. package/dist/sqlEngine/optimizer/optimize.d.ts +13 -0
  174. package/dist/sqlEngine/optimizer/optimize.js +53 -0
  175. package/dist/sqlEngine/optimizer/optimize.js.map +1 -0
  176. package/dist/sqlEngine/optimizer/ruleEngine.d.ts +7 -0
  177. package/dist/sqlEngine/optimizer/ruleEngine.js +26 -0
  178. package/dist/sqlEngine/optimizer/ruleEngine.js.map +1 -0
  179. package/dist/sqlEngine/optimizer/rules/limitPushdown.d.ts +19 -0
  180. package/dist/sqlEngine/optimizer/rules/limitPushdown.js +55 -0
  181. package/dist/sqlEngine/optimizer/rules/limitPushdown.js.map +1 -0
  182. package/dist/sqlEngine/optimizer/rules/planJoins.d.ts +19 -0
  183. package/dist/sqlEngine/optimizer/rules/planJoins.js +62 -0
  184. package/dist/sqlEngine/optimizer/rules/planJoins.js.map +1 -0
  185. package/dist/sqlEngine/optimizer/rules/predicateNormalize.d.ts +18 -0
  186. package/dist/sqlEngine/optimizer/rules/predicateNormalize.js +80 -0
  187. package/dist/sqlEngine/optimizer/rules/predicateNormalize.js.map +1 -0
  188. package/dist/sqlEngine/optimizer/rules/predicatePushdown.d.ts +11 -0
  189. package/dist/sqlEngine/optimizer/rules/predicatePushdown.js +48 -0
  190. package/dist/sqlEngine/optimizer/rules/predicatePushdown.js.map +1 -0
  191. package/dist/sqlEngine/optimizer/rules/projectionPushdown.d.ts +23 -0
  192. package/dist/sqlEngine/optimizer/rules/projectionPushdown.js +160 -0
  193. package/dist/sqlEngine/optimizer/rules/projectionPushdown.js.map +1 -0
  194. package/dist/sqlEngine/optimizer/rules/sortFromIndex.d.ts +14 -0
  195. package/dist/sqlEngine/optimizer/rules/sortFromIndex.js +49 -0
  196. package/dist/sqlEngine/optimizer/rules/sortFromIndex.js.map +1 -0
  197. package/dist/sqlEngine/optimizer/rules/validateScannable.d.ts +15 -0
  198. package/dist/sqlEngine/optimizer/rules/validateScannable.js +60 -0
  199. package/dist/sqlEngine/optimizer/rules/validateScannable.js.map +1 -0
  200. package/dist/sqlEngine/optimizer/whereToConditions.d.ts +85 -0
  201. package/dist/sqlEngine/optimizer/whereToConditions.js +484 -0
  202. package/dist/sqlEngine/optimizer/whereToConditions.js.map +1 -0
  203. package/dist/sqlEngine/parser/ast.d.ts +139 -0
  204. package/dist/sqlEngine/parser/ast.js +10 -0
  205. package/dist/sqlEngine/parser/ast.js.map +1 -0
  206. package/dist/sqlEngine/parser/normalizer.d.ts +28 -0
  207. package/dist/sqlEngine/parser/normalizer.js +413 -0
  208. package/dist/sqlEngine/parser/normalizer.js.map +1 -0
  209. package/dist/sqlEngine/parser/parse.d.ts +14 -0
  210. package/dist/sqlEngine/parser/parse.js +20 -0
  211. package/dist/sqlEngine/parser/parse.js.map +1 -0
  212. package/dist/sqlEngine/physical/PhysicalDistinct.d.ts +9 -0
  213. package/dist/sqlEngine/physical/PhysicalDistinct.js +29 -0
  214. package/dist/sqlEngine/physical/PhysicalDistinct.js.map +1 -0
  215. package/dist/sqlEngine/physical/PhysicalFilter.d.ts +10 -0
  216. package/dist/sqlEngine/physical/PhysicalFilter.js +25 -0
  217. package/dist/sqlEngine/physical/PhysicalFilter.js.map +1 -0
  218. package/dist/sqlEngine/physical/PhysicalHashAggregate.d.ts +27 -0
  219. package/dist/sqlEngine/physical/PhysicalHashAggregate.js +100 -0
  220. package/dist/sqlEngine/physical/PhysicalHashAggregate.js.map +1 -0
  221. package/dist/sqlEngine/physical/PhysicalHashJoin.d.ts +29 -0
  222. package/dist/sqlEngine/physical/PhysicalHashJoin.js +93 -0
  223. package/dist/sqlEngine/physical/PhysicalHashJoin.js.map +1 -0
  224. package/dist/sqlEngine/physical/PhysicalIndexNestedLoopJoin.d.ts +36 -0
  225. package/dist/sqlEngine/physical/PhysicalIndexNestedLoopJoin.js +78 -0
  226. package/dist/sqlEngine/physical/PhysicalIndexNestedLoopJoin.js.map +1 -0
  227. package/dist/sqlEngine/physical/PhysicalIndexScan.d.ts +22 -0
  228. package/dist/sqlEngine/physical/PhysicalIndexScan.js +77 -0
  229. package/dist/sqlEngine/physical/PhysicalIndexScan.js.map +1 -0
  230. package/dist/sqlEngine/physical/PhysicalLimit.d.ts +5 -0
  231. package/dist/sqlEngine/physical/PhysicalLimit.js +28 -0
  232. package/dist/sqlEngine/physical/PhysicalLimit.js.map +1 -0
  233. package/dist/sqlEngine/physical/PhysicalNestedLoopJoin.d.ts +20 -0
  234. package/dist/sqlEngine/physical/PhysicalNestedLoopJoin.js +51 -0
  235. package/dist/sqlEngine/physical/PhysicalNestedLoopJoin.js.map +1 -0
  236. package/dist/sqlEngine/physical/PhysicalProject.d.ts +18 -0
  237. package/dist/sqlEngine/physical/PhysicalProject.js +117 -0
  238. package/dist/sqlEngine/physical/PhysicalProject.js.map +1 -0
  239. package/dist/sqlEngine/physical/PhysicalQualify.d.ts +13 -0
  240. package/dist/sqlEngine/physical/PhysicalQualify.js +29 -0
  241. package/dist/sqlEngine/physical/PhysicalQualify.js.map +1 -0
  242. package/dist/sqlEngine/physical/PhysicalSort.d.ts +12 -0
  243. package/dist/sqlEngine/physical/PhysicalSort.js +59 -0
  244. package/dist/sqlEngine/physical/PhysicalSort.js.map +1 -0
  245. package/dist/sqlEngine/physical/op.d.ts +11 -0
  246. package/dist/sqlEngine/physical/op.js +9 -0
  247. package/dist/sqlEngine/physical/op.js.map +1 -0
  248. package/dist/sqlEngine/physical/plan.d.ts +23 -0
  249. package/dist/sqlEngine/physical/plan.js +162 -0
  250. package/dist/sqlEngine/physical/plan.js.map +1 -0
  251. package/dist/sqlEngine/router.d.ts +26 -0
  252. package/dist/sqlEngine/router.js +48 -0
  253. package/dist/sqlEngine/router.js.map +1 -0
  254. package/dist/sqlEngine/types.d.ts +37 -0
  255. package/dist/sqlEngine/types.js +13 -0
  256. package/dist/sqlEngine/types.js.map +1 -0
  257. package/dist/sqlTranslator/index.js +7 -1
  258. package/dist/sqlTranslator/index.js.map +1 -1
  259. package/dist/utility/errors/hdbError.d.ts +21 -0
  260. package/dist/utility/errors/hdbError.js +23 -1
  261. package/dist/utility/errors/hdbError.js.map +1 -1
  262. package/dist/validation/configValidator.js +22 -6
  263. package/dist/validation/configValidator.js.map +1 -1
  264. package/dist/validation/deleteValidator.js +10 -2
  265. package/dist/validation/deleteValidator.js.map +1 -1
  266. package/index.ts +33 -0
  267. package/npm-shrinkwrap.json +9197 -15066
  268. package/package.json +3 -1
  269. package/resources/DESIGN.md +43 -15
  270. package/resources/Resource.ts +104 -4
  271. package/resources/Table.ts +298 -74
  272. package/resources/analytics/read.ts +30 -25
  273. package/resources/databases.ts +24 -0
  274. package/resources/defineResource.ts +651 -0
  275. package/resources/defineTable.ts +407 -0
  276. package/resources/jsonSchemaTypes.ts +12 -4
  277. package/resources/openApi.ts +68 -16
  278. package/resources/search.ts +5 -8
  279. package/server/graphqlQuerying.ts +4 -2
  280. package/server/http.ts +40 -16
  281. package/server/serverHelpers/serverUtilities.ts +19 -6
  282. package/server/serverHelpers/uwsServer.ts +24 -0
  283. package/sqlTranslator/index.ts +16 -6
  284. package/studio/web/assets/{Chat-Cv_2paZE.js → Chat-BZks8dVF.js} +2 -2
  285. package/studio/web/assets/{Chat-Cv_2paZE.js.map → Chat-BZks8dVF.js.map} +1 -1
  286. package/studio/web/assets/{FloatingChat-CPyPIcVR.js → FloatingChat-Dic8paVO.js} +4 -4
  287. package/studio/web/assets/{FloatingChat-CPyPIcVR.js.map → FloatingChat-Dic8paVO.js.map} +1 -1
  288. package/studio/web/assets/{applications-C0jT2vdZ.js → applications-uOXkeUIN.js} +2 -2
  289. package/studio/web/assets/{applications-C0jT2vdZ.js.map → applications-uOXkeUIN.js.map} +1 -1
  290. package/studio/web/assets/{index-D_GKOkhn.js → index-i-2wrKhv.js} +6 -6
  291. package/studio/web/assets/{index-D_GKOkhn.js.map → index-i-2wrKhv.js.map} +1 -1
  292. package/studio/web/assets/{index.lazy-CAmCIA65.js → index.lazy-Csk8eCoB.js} +4 -4
  293. package/studio/web/assets/{index.lazy-CAmCIA65.js.map → index.lazy-Csk8eCoB.js.map} +1 -1
  294. package/studio/web/assets/{profile-CaF-4aZe.js → profile-Sb3mGDl6.js} +2 -2
  295. package/studio/web/assets/{profile-CaF-4aZe.js.map → profile-Sb3mGDl6.js.map} +1 -1
  296. package/studio/web/assets/{setComponentFile-DLW1DHCt.js → setComponentFile-BgZcaPJ2.js} +2 -2
  297. package/studio/web/assets/{setComponentFile-DLW1DHCt.js.map → setComponentFile-BgZcaPJ2.js.map} +1 -1
  298. package/studio/web/assets/{setup-TGCErIhq.js → setup-DKtlLgmT.js} +2 -2
  299. package/studio/web/assets/{setup-TGCErIhq.js.map → setup-DKtlLgmT.js.map} +1 -1
  300. package/studio/web/assets/{status-DG0Maoao.js → status-B45iLeug.js} +2 -2
  301. package/studio/web/assets/{status-DG0Maoao.js.map → status-B45iLeug.js.map} +1 -1
  302. package/studio/web/assets/{swagger-ui-react-8T4SgQ1m.js → swagger-ui-react-Csu4026e.js} +2 -2
  303. package/studio/web/assets/{swagger-ui-react-8T4SgQ1m.js.map → swagger-ui-react-Csu4026e.js.map} +1 -1
  304. package/studio/web/assets/{tsMode-DJ6Sl24Q.js → tsMode-DVgxUr_l.js} +2 -2
  305. package/studio/web/assets/{tsMode-DJ6Sl24Q.js.map → tsMode-DVgxUr_l.js.map} +1 -1
  306. package/studio/web/assets/{useEntityRestURL-JO2mfWTQ.js → useEntityRestURL-yfDQMV1f.js} +2 -2
  307. package/studio/web/assets/{useEntityRestURL-JO2mfWTQ.js.map → useEntityRestURL-yfDQMV1f.js.map} +1 -1
  308. package/studio/web/index.html +1 -1
  309. package/utility/errors/hdbError.ts +32 -0
  310. package/validation/configValidator.ts +23 -6
  311. package/validation/deleteValidator.ts +11 -2
@@ -0,0 +1,651 @@
1
+ /**
2
+ * The per-method request contract: `defineResource(contract, impl)` (primary, function form) and
3
+ * `Resource.withSchema(contract)` (class form).
4
+ *
5
+ * A resource declares a contract as runtime VALUES — `{ path, record?, get?/post?/put?/patch?/delete?:
6
+ * { query?, body?, response? } }` — and TypeScript DERIVES the handler types from it (the same
7
+ * value-first bargain `--conditions=typestrip` forces, and that `defineTable` already lives).
8
+ *
9
+ * This is a SUBSET, not a fork. A handler
10
+ * receives the SAME `RequestTarget` it receives today, structurally NARROWED: `target.id` is a string
11
+ * when the path declares `:id`, `target.get('expand')` is typed by the declared query schema, and the
12
+ * built-in filter/sort/limit grammar stays reachable. The enforced resource is assignable wherever the
13
+ * untyped resource shape goes — it still registers and serves like a plain one.
14
+ *
15
+ * The narrowed types are JUSTIFIED by runtime enforcement: each declared verb is wrapped to
16
+ * validate/coerce `query`/`body` before dispatch and returns a structured 400 (`ValidationError`) on
17
+ * failure, so by the time a handler runs the narrowed types are true. Same bargain the DOM makes; same
18
+ * role `Table.validate` plays for tables.
19
+ *
20
+ * ONE vocabulary. The built-in `t`/`schemaOf` builders reduce to `JsonSchemaFragment`
21
+ * (`resources/jsonSchemaTypes.ts`) — the identical IR `defineTable`/GraphQL fields reduce to via
22
+ * `attributeToFragment` — so a single declaration feeds validation, OpenAPI, and MCP. A `defineTable`
23
+ * projection slots straight in: `schemaOf<(typeof Track)['$insert']>({ table: Track, projection:
24
+ * 'insert' })` derives both the compile-time type (from the projection) and the runtime fragment (from
25
+ * the table's attributes).
26
+ */
27
+
28
+ import type { JsonSchemaFragment } from './jsonSchemaTypes.ts';
29
+ import { attributeToFragment, type AttributeLike } from './jsonSchemaTypes.ts';
30
+ import { ValidationError, type ValidationIssue } from '../utility/errors/hdbError.ts';
31
+
32
+ // ─────────────────────────────────────────────────────────────────────────────
33
+ // Schema vocabulary — phantom-typed carriers backed by a runtime JsonSchemaFragment.
34
+ // `_type` is a phantom (never present at runtime); the fragment + `isOptional` are the
35
+ // runtime metadata. `.optional` yields a fresh carrier (immutable chaining).
36
+ // ─────────────────────────────────────────────────────────────────────────────
37
+
38
+ export interface Schema<T> {
39
+ /** phantom — the TS type this schema validates to; never present at runtime. */
40
+ readonly _type?: T;
41
+ /** mark this schema optional within its containing object/query (the property may be absent). */
42
+ readonly optional: Schema<T | undefined>;
43
+ /** allow an explicit `null` value. Non-nullable is the default — a bare `t.string` rejects `null`. */
44
+ readonly nullable: Schema<T | null>;
45
+ }
46
+
47
+ /** Runtime view of a {@link Schema}: its fragment, whether it is optional, and passthrough marker. */
48
+ interface SchemaInternal {
49
+ fragment: JsonSchemaFragment;
50
+ isOptional: boolean;
51
+ /** true when no shape was supplied (`schemaOf<T>()` with no source) — validation is a no-op. */
52
+ passthrough: boolean;
53
+ }
54
+
55
+ function makeSchema(fragment: JsonSchemaFragment, isOptional: boolean, passthrough = false): any {
56
+ return {
57
+ _fragment: fragment,
58
+ _optional: isOptional,
59
+ _passthrough: passthrough,
60
+ get optional() {
61
+ return makeSchema(fragment, true, passthrough);
62
+ },
63
+ get nullable() {
64
+ // `nullable: true` on the fragment is what the validator checks; the TS type gains `| null`.
65
+ return makeSchema({ ...fragment, nullable: true }, isOptional, passthrough);
66
+ },
67
+ };
68
+ }
69
+
70
+ function internalOf(schema: any): SchemaInternal {
71
+ return { fragment: schema?._fragment ?? {}, isOptional: !!schema?._optional, passthrough: !!schema?._passthrough };
72
+ }
73
+
74
+ type TypeOf<S> = S extends Schema<infer T> ? T : never;
75
+ type OptionalKeys<P> = { [K in keyof P]-?: undefined extends TypeOf<P[K]> ? K : never }[keyof P];
76
+ type RequiredKeys<P> = Exclude<keyof P, OptionalKeys<P>>;
77
+ /** The object type of a `t.object(props)` — fields whose schema includes `undefined` become optional. */
78
+ type ObjectType<P> = { [K in RequiredKeys<P>]: TypeOf<P[K]> } & { [K in OptionalKeys<P>]?: TypeOf<P[K]> };
79
+
80
+ function objectFragment(props: Record<string, any>): JsonSchemaFragment {
81
+ const properties: Record<string, JsonSchemaFragment> = {};
82
+ const required: string[] = [];
83
+ for (const [key, schema] of Object.entries(props)) {
84
+ const internal = internalOf(schema);
85
+ properties[key] = internal.fragment;
86
+ if (!internal.isOptional) required.push(key);
87
+ }
88
+ const fragment: JsonSchemaFragment = { type: 'object', properties, additionalProperties: false };
89
+ if (required.length) fragment.required = required;
90
+ return fragment;
91
+ }
92
+
93
+ /**
94
+ * The zero-dependency built-in schema vocabulary for query params and bodies. Every builder reduces to
95
+ * a `JsonSchemaFragment`, so a contract's declarations feed validation + OpenAPI + MCP from one source.
96
+ */
97
+ export const t = {
98
+ string: makeSchema({ type: 'string' }, false) as Schema<string>,
99
+ number: makeSchema({ type: 'number' }, false) as Schema<number>,
100
+ integer: makeSchema({ type: 'integer' }, false) as Schema<number>,
101
+ boolean: makeSchema({ type: 'boolean' }, false) as Schema<boolean>,
102
+ /** ISO-8601 timestamp; validates as a string, coerced to `Date` in bodies. */
103
+ date: makeSchema({ type: 'string', format: 'date-time' }, false) as Schema<Date>,
104
+ enum: <const E extends readonly string[]>(values: E) =>
105
+ makeSchema({ type: 'string', enum: values }, false) as Schema<E[number]>,
106
+ array: <T>(item: Schema<T>) => makeSchema({ type: 'array', items: internalOf(item).fragment }, false) as Schema<T[]>,
107
+ object: <P extends Record<string, Schema<any>>>(props: P) =>
108
+ makeSchema(objectFragment(props), false) as Schema<ObjectType<P>>,
109
+ };
110
+
111
+ // ─────────────────────────────────────────────────────────────────────────────
112
+ // schemaOf — a type-carrier for an existing TS type (an interface or a `defineTable`
113
+ // projection). The TYPE argument carries the compile-time shape; the optional runtime
114
+ // `source` supplies the fragment (a raw fragment, a table handle, or `{ table, projection }`).
115
+ // With no source it is a passthrough (typed, but not runtime-validated).
116
+ // ─────────────────────────────────────────────────────────────────────────────
117
+
118
+ export type Projection = 'record' | 'insert' | 'upsert' | 'patch' | 'query';
119
+
120
+ /** Minimal runtime shape of a table handle (`defineTable(...)` / `tables.Foo`) needed to project a fragment. */
121
+ interface TableLike {
122
+ attributes?: AttributeLike[];
123
+ primaryKey?: string;
124
+ }
125
+
126
+ export type SchemaSource = JsonSchemaFragment | TableLike | { table: TableLike; projection?: Projection };
127
+
128
+ /** Type-only schema for an existing TS type — an interface, or a `defineTable` projection like `(typeof Track)['$insert']`. */
129
+ export function schemaOf<T>(source?: SchemaSource): Schema<T> {
130
+ if (source == null) return makeSchema({}, false, true);
131
+ return makeSchema(resolveSourceFragment(source), false);
132
+ }
133
+
134
+ function isTableLike(value: any): value is TableLike {
135
+ return value && Array.isArray(value.attributes);
136
+ }
137
+
138
+ function resolveSourceFragment(source: SchemaSource): JsonSchemaFragment {
139
+ if (isTableLike(source)) return projectTableFragment(source, 'record');
140
+ if ((source as any).table && isTableLike((source as any).table)) {
141
+ const { table, projection } = source as { table: TableLike; projection?: Projection };
142
+ return projectTableFragment(table, projection ?? 'record');
143
+ }
144
+ return source as JsonSchemaFragment;
145
+ }
146
+
147
+ /**
148
+ * Project a table handle's attributes into a JSON-Schema object fragment for the given projection.
149
+ * Reuses the shared `attributeToFragment` per field (NOT a parallel translator) and applies the same
150
+ * inclusion/required rules as `defineTable`'s type projections and the MCP `derive` schemas:
151
+ * • record — all scalar fields, readonly; server-managed/PK/required fields marked required
152
+ * • insert — writable fields, PK optional, server-managed excluded
153
+ * • upsert — writable fields, PK required
154
+ * • patch — writable non-PK fields, all optional
155
+ * • query — indexed fields only, all optional
156
+ * Relationship and computed attributes are excluded (they aren't client-writable and their fragment
157
+ * type is the related table name, not a JSON type).
158
+ */
159
+ export function projectTableFragment(table: TableLike, projection: Projection): JsonSchemaFragment {
160
+ const attributes = table.attributes ?? [];
161
+ const properties: Record<string, JsonSchemaFragment> = {};
162
+ const required: string[] = [];
163
+ for (const attr of attributes) {
164
+ const a = attr as any;
165
+ if (a.relationship || a.computed || a.computedFromExpression) continue;
166
+ const serverManaged = a.assignCreatedTime || a.assignUpdatedTime || a.expiresAt;
167
+ const isPk = a.isPrimaryKey;
168
+ if (projection !== 'record' && projection !== 'query' && serverManaged) continue; // never client-writable
169
+ if (projection === 'query' && !a.indexed) continue;
170
+ if (projection === 'patch' && isPk) continue;
171
+ const fieldFragment = attributeToFragment(attr);
172
+ // Mirror Table.validate's null policy so a table-derived contract body validates identically:
173
+ // a column rejects `null` only when `nullable === false` (a required column). Everything else
174
+ // (nullable columns, PK, server-managed) accepts an explicit null.
175
+ if (a.nullable !== false) fieldFragment.nullable = true;
176
+ if (a.enum) fieldFragment.enum = a.enum;
177
+ properties[attr.name] = fieldFragment;
178
+ switch (projection) {
179
+ case 'record':
180
+ if (a.nullable === false || serverManaged || isPk) required.push(attr.name);
181
+ break;
182
+ case 'insert':
183
+ if (a.nullable === false && !isPk) required.push(attr.name);
184
+ break;
185
+ case 'upsert':
186
+ if (a.nullable === false || isPk) required.push(attr.name);
187
+ break;
188
+ // patch + query: everything optional
189
+ }
190
+ }
191
+ const fragment: JsonSchemaFragment = { type: 'object', properties, additionalProperties: false };
192
+ if (required.length) fragment.required = required;
193
+ return fragment;
194
+ }
195
+
196
+ // ─────────────────────────────────────────────────────────────────────────────
197
+ // The contract shape.
198
+ // ─────────────────────────────────────────────────────────────────────────────
199
+
200
+ export interface VerbSchemas {
201
+ readonly query?: Record<string, Schema<any>>;
202
+ readonly body?: Schema<any>;
203
+ readonly response?: Schema<any>;
204
+ }
205
+
206
+ export interface Contract {
207
+ readonly path: string;
208
+ /** Optional human description, surfaced on OpenAPI/MCP. */
209
+ readonly description?: string;
210
+ /** The resource's record type; defaults `get`'s response and `put`'s body when not overridden. */
211
+ readonly record?: Schema<any>;
212
+ readonly get?: VerbSchemas;
213
+ readonly post?: VerbSchemas;
214
+ readonly put?: VerbSchemas;
215
+ readonly patch?: VerbSchemas;
216
+ readonly delete?: VerbSchemas;
217
+ }
218
+
219
+ export type VerbName = 'get' | 'post' | 'put' | 'patch' | 'delete';
220
+ const BODY_VERBS: ReadonlySet<string> = new Set(['post', 'put', 'patch']);
221
+ const ALL_VERBS: readonly VerbName[] = ['get', 'post', 'put', 'patch', 'delete'];
222
+
223
+ // ─────────────────────────────────────────────────────────────────────────────
224
+ // Type-level derivation of handler signatures from a contract.
225
+ // ─────────────────────────────────────────────────────────────────────────────
226
+
227
+ type MaybePromise<T> = T | Promise<T>;
228
+
229
+ /** Template-literal parse of the declared path (matches the runtime, which Object.assigns matched segments). */
230
+ export type PathParams<S extends string> = S extends `${string}:${infer P}/${infer Rest}`
231
+ ? { [K in P]: string } & PathParams<`/${Rest}`>
232
+ : S extends `${string}:${infer P}`
233
+ ? { [K in P]: string }
234
+ : S extends `${string}*${infer W}`
235
+ ? { [K in W extends '' ? 'wildcard' : W]: string }
236
+ : {};
237
+
238
+ type InferSchema<S> = S extends Schema<infer T> ? T : never;
239
+ type RecordOf<C> = C extends { record: Schema<infer R> } ? R : unknown;
240
+ type QueryOf<VS> = VS extends { query: infer Q } ? { [K in keyof Q]: InferSchema<Q[K]> } : {};
241
+ type ResponseOf<VS, R> = VS extends { response: Schema<infer T> } ? T : R;
242
+ type BodyOf<VS, V, R> = VS extends { body: Schema<infer T> } ? T : V extends 'put' ? R : unknown;
243
+
244
+ /** `RequestTarget.get` overloaded for declared query params; anything else falls back to `string | null`. */
245
+ export interface TypedSearchParams<Q> {
246
+ get<K extends keyof Q & string>(name: K): Q[K];
247
+ get(name: string): string | null;
248
+ }
249
+
250
+ /** The built-in query grammar that stays reachable on the narrowed target (a subset of RequestTarget). */
251
+ interface QueryGrammar {
252
+ conditions?: unknown[];
253
+ sort?: unknown;
254
+ select?: unknown;
255
+ limit?: number;
256
+ offset?: number;
257
+ }
258
+
259
+ /** The SAME RequestTarget, narrowed: path params intersected on, `.get` typed by the query schema. */
260
+ export type TypedTarget<P extends string, Q> = QueryGrammar & PathParams<P> & TypedSearchParams<Q>;
261
+
262
+ type HandlerFor<C extends Contract, V extends keyof C & VerbName, R> = V extends 'get'
263
+ ? (target: TypedTarget<C['path'], QueryOf<C[V]>>) => MaybePromise<ResponseOf<C[V], R>>
264
+ : V extends 'delete'
265
+ ? (target: TypedTarget<C['path'], QueryOf<C[V]>>) => MaybePromise<ResponseOf<C[V], R>>
266
+ : (target: TypedTarget<C['path'], QueryOf<C[V]>>, data: BodyOf<C[V], V, R>) => MaybePromise<ResponseOf<C[V], R>>;
267
+
268
+ /** Every verb the contract declares is REQUIRED of the impl; a misspelled/missing verb is an error. */
269
+ export type ImplFor<C extends Contract> = {
270
+ [V in keyof C & VerbName]: HandlerFor<C, V, RecordOf<C>>;
271
+ };
272
+
273
+ // ─────────────────────────────────────────────────────────────────────────────
274
+ // Runtime validation.
275
+ // ─────────────────────────────────────────────────────────────────────────────
276
+
277
+ const JSON_TYPE_CHECK: Record<string, (v: any) => boolean> = {
278
+ string: (v) => typeof v === 'string',
279
+ integer: (v) => typeof v === 'number' && Number.isInteger(v),
280
+ number: (v) => typeof v === 'number' && !Number.isNaN(v),
281
+ boolean: (v) => typeof v === 'boolean',
282
+ object: (v) => v != null && typeof v === 'object' && !Array.isArray(v),
283
+ array: (v) => Array.isArray(v),
284
+ null: (v) => v === null,
285
+ };
286
+
287
+ /** A fragment accepts `null` only when explicitly `nullable` or its type union includes `'null'`. */
288
+ function isNullable(fragment: JsonSchemaFragment): boolean {
289
+ return fragment.nullable === true || (Array.isArray(fragment.type) && fragment.type.includes('null'));
290
+ }
291
+
292
+ /** Validate a JSON value against a fragment (a subset of JSON Schema), pushing structured issues. Returns coerced value. */
293
+ function validateValue(value: any, fragment: JsonSchemaFragment, path: string, issues: ValidationIssue[]): any {
294
+ if (!fragment || fragment.type == null) return value; // passthrough / untyped
295
+ if (value === undefined) return value; // absence is enforced by the parent object's `required`
296
+ if (value === null) {
297
+ // Non-nullable is the default; only `.nullable` (fragment.nullable) or a `null` type accepts null.
298
+ if (!isNullable(fragment)) issues.push({ path, code: 'nullable', message: `${path} may not be null` });
299
+ return value;
300
+ }
301
+ const types = Array.isArray(fragment.type) ? fragment.type : [fragment.type];
302
+ const primary = types[0] as string;
303
+ // Body dates arrive as ISO strings; coerce to Date when the schema calls for a date-time.
304
+ if (primary === 'string' && fragment.format === 'date-time' && typeof value === 'string') {
305
+ const d = new Date(value);
306
+ if (Number.isNaN(d.getTime())) {
307
+ issues.push({ path, code: 'format', message: `${path} must be an ISO-8601 date-time` });
308
+ return value;
309
+ }
310
+ value = d;
311
+ }
312
+ // A Date is a valid value ONLY for a `string`/`date-time` field (we coerced an ISO string above);
313
+ // it must not slip past the type check for a number/boolean/array/object schema.
314
+ const matches = types.some((ty) => {
315
+ if (ty === 'string' && fragment.format === 'date-time' && value instanceof Date) return true;
316
+ return JSON_TYPE_CHECK[ty as string]?.(value) ?? true;
317
+ });
318
+ if (!matches) {
319
+ issues.push({ path, code: 'type', message: `${path} must be of type ${types.join(' | ')}` });
320
+ return value;
321
+ }
322
+ if (fragment.enum && !fragment.enum.includes(value)) {
323
+ issues.push({ path, code: 'enum', message: `${path} must be one of: ${fragment.enum.join(', ')}` });
324
+ return value;
325
+ }
326
+ if (primary === 'array' && fragment.items && Array.isArray(value)) {
327
+ return value.map((el, i) => validateValue(el, fragment.items as JsonSchemaFragment, `${path}[${i}]`, issues));
328
+ }
329
+ if (primary === 'object' && fragment.properties && value && typeof value === 'object') {
330
+ return validateObject(value, fragment, path, issues);
331
+ }
332
+ return value;
333
+ }
334
+
335
+ function validateObject(value: any, fragment: JsonSchemaFragment, path: string, issues: ValidationIssue[]): any {
336
+ const props = fragment.properties ?? {};
337
+ const scope = path ? `${path}.` : '';
338
+ for (const key of fragment.required ?? []) {
339
+ // `required` is about presence; an explicit `null` is present (and rejected by the per-property
340
+ // nullable check below unless the field is nullable).
341
+ if (value[key] === undefined)
342
+ issues.push({ path: `${scope}${key}`, code: 'required', message: `${scope}${key} is required` });
343
+ }
344
+ for (const [key, sub] of Object.entries(props)) {
345
+ if (value[key] !== undefined) {
346
+ const coerced = validateValue(value[key], sub, `${scope}${key}`, issues);
347
+ if (coerced !== value[key]) value[key] = coerced;
348
+ }
349
+ }
350
+ if (fragment.additionalProperties === false) {
351
+ for (const key of Object.keys(value)) {
352
+ if (!(key in props))
353
+ issues.push({
354
+ path: `${scope}${key}`,
355
+ code: 'unknown_property',
356
+ message: `${scope}${key} is not an allowed property`,
357
+ });
358
+ }
359
+ }
360
+ return value;
361
+ }
362
+
363
+ /** Validate/coerce a request body against a fragment. Returns the (possibly coerced) body. */
364
+ function validateBody(fragment: JsonSchemaFragment, body: any, issues: ValidationIssue[]): any {
365
+ if (!fragment || fragment.type == null) return body;
366
+ if (body === undefined) {
367
+ // a missing body fails each declared required field
368
+ for (const key of fragment.required ?? [])
369
+ issues.push({ path: `body.${key}`, code: 'required', message: `body.${key} is required` });
370
+ return body;
371
+ }
372
+ // a `null` body is rejected unless the top-level body schema is nullable (validateValue handles it)
373
+ return validateValue(body, fragment, 'body', issues);
374
+ }
375
+
376
+ /**
377
+ * Read the declared query params off the target (a URLSearchParams), validate/coerce them, and install
378
+ * a coercing `.get` so a handler reads the typed values (`target.get('expand')` → the coerced array),
379
+ * while undeclared keys still return the raw `string | null`. Pushes structured issues for
380
+ * required-missing / bad-enum / bad-type params.
381
+ */
382
+ function coerceAndValidateQuery(fragment: JsonSchemaFragment, target: any, issues: ValidationIssue[]): void {
383
+ const props = fragment.properties ?? {};
384
+ const required = new Set(fragment.required ?? []);
385
+ const coerced: Record<string, any> = Object.create(null);
386
+ const rawGet = URLSearchParams.prototype.get;
387
+ const rawGetAll = URLSearchParams.prototype.getAll;
388
+ for (const [key, sub] of Object.entries(props)) {
389
+ const subType = Array.isArray(sub.type) ? sub.type[0] : sub.type;
390
+ const present = URLSearchParams.prototype.has.call(target, key);
391
+ if (!present) {
392
+ if (required.has(key))
393
+ issues.push({ path: `query.${key}`, code: 'required', message: `query.${key} is required` });
394
+ coerced[key] = undefined;
395
+ continue;
396
+ }
397
+ if (subType === 'array') {
398
+ const raw = rawGetAll.call(target, key);
399
+ coerced[key] = raw.map((el: string, i: number) =>
400
+ coerceScalar(el, sub.items as JsonSchemaFragment, `query.${key}[${i}]`, issues)
401
+ );
402
+ } else {
403
+ coerced[key] = coerceScalar(rawGet.call(target, key) as string, sub, `query.${key}`, issues);
404
+ }
405
+ }
406
+ Object.defineProperty(target, 'get', {
407
+ configurable: true,
408
+ writable: true,
409
+ value: function (name: string) {
410
+ return name in coerced ? coerced[name] : rawGet.call(this, name);
411
+ },
412
+ });
413
+ // Keep `getAll` consistent with the coercing `get`: a declared key returns its coerced value as an
414
+ // array (arrays pass through; a scalar is wrapped, undefined → []); undeclared keys read raw.
415
+ Object.defineProperty(target, 'getAll', {
416
+ configurable: true,
417
+ writable: true,
418
+ value: function (name: string) {
419
+ if (name in coerced) {
420
+ const val = coerced[name];
421
+ return Array.isArray(val) ? val : val === undefined ? [] : [val];
422
+ }
423
+ return rawGetAll.call(this, name);
424
+ },
425
+ });
426
+ }
427
+
428
+ /** Coerce a single query string to the fragment's scalar type and validate enum membership. */
429
+ function coerceScalar(
430
+ raw: string,
431
+ fragment: JsonSchemaFragment | undefined,
432
+ path: string,
433
+ issues: ValidationIssue[]
434
+ ): any {
435
+ if (!fragment) return raw;
436
+ const type = Array.isArray(fragment.type) ? fragment.type[0] : fragment.type;
437
+ let value: any = raw;
438
+ if (type === 'integer' || type === 'number') {
439
+ // `Number('')`/`Number(' ')` are 0 — treat an empty/whitespace param as a type error, not a silent 0.
440
+ if (raw == null || raw.trim() === '') {
441
+ issues.push({ path, code: 'type', message: `${path} must be ${type === 'integer' ? 'an integer' : 'a number'}` });
442
+ return raw;
443
+ }
444
+ value = Number(raw);
445
+ if (Number.isNaN(value) || (type === 'integer' && !Number.isInteger(value))) {
446
+ issues.push({ path, code: 'type', message: `${path} must be ${type === 'integer' ? 'an integer' : 'a number'}` });
447
+ return raw;
448
+ }
449
+ } else if (type === 'boolean') {
450
+ if (raw === 'true') value = true;
451
+ else if (raw === 'false') value = false;
452
+ else {
453
+ issues.push({ path, code: 'type', message: `${path} must be a boolean` });
454
+ return raw;
455
+ }
456
+ } else if (type === 'string' && fragment.format === 'date-time') {
457
+ const d = new Date(raw);
458
+ if (Number.isNaN(d.getTime())) {
459
+ issues.push({ path, code: 'format', message: `${path} must be an ISO-8601 date-time` });
460
+ return raw;
461
+ }
462
+ value = d;
463
+ }
464
+ if (fragment.enum && !fragment.enum.includes(value)) {
465
+ issues.push({ path, code: 'enum', message: `${path} must be one of: ${fragment.enum.join(', ')}` });
466
+ }
467
+ return value;
468
+ }
469
+
470
+ // ─────────────────────────────────────────────────────────────────────────────
471
+ // Contract → static metadata + verb wrappers.
472
+ // ─────────────────────────────────────────────────────────────────────────────
473
+
474
+ /** Per-verb input schemas landed on `static inputSchemas` for OpenAPI/MCP to read. */
475
+ export interface VerbInputSchema {
476
+ query?: JsonSchemaFragment;
477
+ body?: JsonSchemaFragment;
478
+ }
479
+
480
+ interface CompiledVerb {
481
+ queryFragment?: JsonSchemaFragment;
482
+ bodyFragment?: JsonSchemaFragment;
483
+ hasBody: boolean;
484
+ }
485
+
486
+ function compileVerb(contract: Contract, verb: VerbName): CompiledVerb {
487
+ const vs = (contract as any)[verb] as VerbSchemas;
488
+ const queryFragment = vs?.query ? objectFragment(vs.query) : undefined;
489
+ let bodyFragment: JsonSchemaFragment | undefined;
490
+ if (BODY_VERBS.has(verb)) {
491
+ const bodySchema = vs?.body ?? (verb === 'put' ? contract.record : undefined);
492
+ if (bodySchema) {
493
+ const internal = internalOf(bodySchema);
494
+ if (!internal.passthrough) bodyFragment = internal.fragment;
495
+ }
496
+ }
497
+ return { queryFragment, bodyFragment, hasBody: BODY_VERBS.has(verb) };
498
+ }
499
+
500
+ /** Build the `{ verb: { query?, body? } }` map for OpenAPI/MCP. */
501
+ function buildInputSchemas(contract: Contract): Record<string, VerbInputSchema> {
502
+ const inputSchemas: Record<string, VerbInputSchema> = {};
503
+ for (const verb of ALL_VERBS) {
504
+ if (!(contract as any)[verb]) continue;
505
+ const { queryFragment, bodyFragment } = compileVerb(contract, verb);
506
+ const entry: VerbInputSchema = {};
507
+ if (queryFragment) entry.query = queryFragment;
508
+ if (bodyFragment) entry.body = bodyFragment;
509
+ inputSchemas[verb] = entry;
510
+ }
511
+ return inputSchemas;
512
+ }
513
+
514
+ /** Build the `{ verb: responseFragment }` map (get/delete default to the record). */
515
+ function buildOutputSchemas(contract: Contract): Record<string, JsonSchemaFragment> {
516
+ const outputSchemas: Record<string, JsonSchemaFragment> = {};
517
+ const recordFragment = contract.record ? unwrapFragment(contract.record) : undefined;
518
+ for (const verb of ALL_VERBS) {
519
+ const vs = (contract as any)[verb] as VerbSchemas;
520
+ if (!vs) continue;
521
+ const responseFragment = vs.response ? unwrapFragment(vs.response) : verb === 'get' ? recordFragment : undefined;
522
+ if (responseFragment) outputSchemas[verb] = responseFragment;
523
+ }
524
+ return outputSchemas;
525
+ }
526
+
527
+ function unwrapFragment(schema: Schema<any>): JsonSchemaFragment | undefined {
528
+ const internal = internalOf(schema);
529
+ return internal.passthrough ? undefined : internal.fragment;
530
+ }
531
+
532
+ /** Attach the contract metadata to a resource carrier (class or plain object). */
533
+ function applyContractMetadata(carrier: any, contract: Contract): void {
534
+ carrier.path = contract.path;
535
+ carrier.requestContract = contract;
536
+ carrier.inputSchemas = buildInputSchemas(contract);
537
+ const outputSchemas = buildOutputSchemas(contract);
538
+ if (Object.keys(outputSchemas).length) carrier.outputSchemas = { ...carrier.outputSchemas, ...outputSchemas };
539
+ if (contract.record) {
540
+ const recordFragment = unwrapFragment(contract.record);
541
+ if (recordFragment?.properties) carrier.properties = { ...carrier.properties, ...recordFragment.properties };
542
+ }
543
+ if (contract.description && !carrier.description) carrier.description = contract.description;
544
+ }
545
+
546
+ /**
547
+ * Wrap a verb entry so `query`/`body` are validated/coerced before dispatch, returning a structured
548
+ * 400 (`ValidationError`) on failure. Only acts on the HTTP shape (first arg is a `URLSearchParams` /
549
+ * `RequestTarget`); a programmatic `.post(record)` call passes through untouched to `original`.
550
+ */
551
+ function wrapStaticVerb(original: Function, compiled: CompiledVerb): Function {
552
+ const { queryFragment, bodyFragment, hasBody } = compiled;
553
+ return function (this: any, target: any, ...rest: any[]) {
554
+ if (target instanceof URLSearchParams) {
555
+ const issues: ValidationIssue[] = [];
556
+ if (queryFragment) coerceAndValidateQuery(queryFragment, target, issues);
557
+ if (hasBody && bodyFragment) rest[0] = validateBody(bodyFragment, rest[0], issues);
558
+ if (issues.length) throw new ValidationError(issues);
559
+ }
560
+ return original.call(this, target, ...rest);
561
+ };
562
+ }
563
+
564
+ /** Install validating overrides for every declared verb onto a class's static verb entries. */
565
+ function installStaticVerbValidators(cls: any, contract: Contract): void {
566
+ for (const verb of ALL_VERBS) {
567
+ if (!(contract as any)[verb]) continue;
568
+ const original = cls[verb];
569
+ if (typeof original !== 'function') continue;
570
+ Object.defineProperty(cls, verb, {
571
+ configurable: true,
572
+ writable: true,
573
+ value: wrapStaticVerb(original, compileVerb(contract, verb)),
574
+ });
575
+ }
576
+ }
577
+
578
+ // ─────────────────────────────────────────────────────────────────────────────
579
+ // Public API — class form (`Resource.withSchema`) + function form (`defineResource`).
580
+ // ─────────────────────────────────────────────────────────────────────────────
581
+
582
+ /** The instance-side verb handlers a `withSchema` base class declares, so a subclass's overrides narrow. */
583
+ type SchemaHandlers<C extends Contract> = {
584
+ [V in keyof C & VerbName]?: HandlerFor<C, V, RecordOf<C>>;
585
+ };
586
+
587
+ /** The class returned by `Resource.withSchema(contract)`: extend it and implement the declared verbs. */
588
+ export type SchemaClass<Base, C extends Contract> = Base & {
589
+ new (...args: any[]): SchemaHandlers<C>;
590
+ readonly path: C['path'];
591
+ readonly requestContract: C;
592
+ };
593
+
594
+ /**
595
+ * Build a Resource subclass carrying `contract` — its static verb entries validate before dispatch and
596
+ * its static metadata slots (`path`, `requestContract`, `inputSchemas`, `outputSchemas`, `properties`)
597
+ * feed OpenAPI/MCP. Wired as `Resource.withSchema` (see `Resource.ts`).
598
+ */
599
+ export function makeSchemaClass<Base extends new (...args: any[]) => any, C extends Contract>(
600
+ BaseClass: Base,
601
+ contract: C
602
+ ): SchemaClass<Base, C> {
603
+ const Enforced = class extends BaseClass {};
604
+ // The narrowed handler types (and the function form) use the converged `(target, data)` arg
605
+ // order. Harper's dispatch only calls instance verbs that way when `loadAsInstance === false`
606
+ // (the default, undefined, gives the legacy `(data, target)`), so pin it — withSchema is for custom
607
+ // resources, which handle their own loading. Without this the class-form types would lie about the
608
+ // arg order (Resource.post/put/patch dispatch, Resource.ts).
609
+ (Enforced as any).loadAsInstance = false;
610
+ applyContractMetadata(Enforced, contract);
611
+ installStaticVerbValidators(Enforced, contract);
612
+ return Enforced as unknown as SchemaClass<Base, C>;
613
+ }
614
+
615
+ /**
616
+ * Function-form twin of `withSchema`: wrap an object literal of verb handlers with the contract. Every
617
+ * declared verb is required of `impl` (a missing/misspelled verb is a type error) and undeclared extras
618
+ * on an inline literal are rejected. Returns a plain resource object (drop-in wherever the untyped shape
619
+ * goes) whose verbs validate/coerce before running the handler.
620
+ */
621
+ export function defineResource<const C extends Contract>(
622
+ contract: C,
623
+ impl: ImplFor<C>
624
+ ): ImplFor<C> & { path: C['path'] } {
625
+ if ((impl as any).path != null && (impl as any).path !== contract.path) {
626
+ throw new Error(
627
+ `defineResource: impl.path (${(impl as any).path}) does not match contract.path (${contract.path})`
628
+ );
629
+ }
630
+ const resource: any = {};
631
+ // A synthetic prototype mirroring the verb methods: OpenAPI (`prototype.post !== Resource.prototype
632
+ // .post`, `typeof prototype.put === 'function'`) and MCP (`detectVerbs` reads the prototype) key
633
+ // verb presence off `.prototype`. A bare object has none, so without this a function-form resource
634
+ // is invisible to MCP and crashes OpenAPI's non-parameterised loop at `prototype.post`. REST still
635
+ // dispatches the own methods; this only advertises which verbs exist.
636
+ const prototype: any = {};
637
+ // carry any non-verb members of a pre-existing impl object through unchanged
638
+ for (const key of Object.keys(impl as any)) {
639
+ if (!ALL_VERBS.includes(key as VerbName)) resource[key] = (impl as any)[key];
640
+ }
641
+ for (const verb of ALL_VERBS) {
642
+ const handler = (impl as any)[verb];
643
+ if (typeof handler !== 'function') continue;
644
+ const wrapped = (contract as any)[verb] ? wrapStaticVerb(handler, compileVerb(contract, verb)) : handler; // undeclared-but-present verb: pass through unvalidated
645
+ resource[verb] = wrapped;
646
+ prototype[verb] = wrapped;
647
+ }
648
+ resource.prototype = prototype;
649
+ applyContractMetadata(resource, contract);
650
+ return resource as ImplFor<C> & { path: C['path'] };
651
+ }