alepha 0.20.2 → 0.20.4

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 (304) hide show
  1. package/README.md +0 -1
  2. package/assets/swagger-ui/swagger-ui-bundle.js +1 -1
  3. package/assets/swagger-ui/swagger-ui.css +1 -1
  4. package/dist/api/audits/index.browser.js +49 -0
  5. package/dist/api/audits/index.browser.js.map +1 -1
  6. package/dist/api/audits/index.js +49 -0
  7. package/dist/api/audits/index.js.map +1 -1
  8. package/dist/api/files/index.js.map +1 -1
  9. package/dist/api/jobs/index.d.ts +2 -61
  10. package/dist/api/jobs/index.d.ts.map +1 -1
  11. package/dist/api/jobs/index.js.map +1 -1
  12. package/dist/api/keys/index.d.ts +4 -4
  13. package/dist/api/keys/index.js.map +1 -1
  14. package/dist/api/notifications/index.d.ts +1 -10
  15. package/dist/api/notifications/index.d.ts.map +1 -1
  16. package/dist/api/parameters/index.browser.js +37 -0
  17. package/dist/api/parameters/index.browser.js.map +1 -1
  18. package/dist/api/parameters/index.d.ts +12 -68
  19. package/dist/api/parameters/index.d.ts.map +1 -1
  20. package/dist/api/parameters/index.js +57 -4
  21. package/dist/api/parameters/index.js.map +1 -1
  22. package/dist/api/payments/index.js.map +1 -1
  23. package/dist/api/users/index.browser.js +6 -0
  24. package/dist/api/users/index.browser.js.map +1 -1
  25. package/dist/api/users/index.d.ts +148 -227
  26. package/dist/api/users/index.d.ts.map +1 -1
  27. package/dist/api/users/index.js +60 -14
  28. package/dist/api/users/index.js.map +1 -1
  29. package/dist/api/verifications/index.d.ts.map +1 -1
  30. package/dist/api/verifications/index.js +2 -1
  31. package/dist/api/verifications/index.js.map +1 -1
  32. package/dist/bucket/index.d.ts +77 -107
  33. package/dist/bucket/index.d.ts.map +1 -1
  34. package/dist/bucket/index.js +153 -5
  35. package/dist/bucket/index.js.map +1 -1
  36. package/dist/bucket/index.workerd.js +12 -2
  37. package/dist/bucket/index.workerd.js.map +1 -1
  38. package/dist/cache/core/index.d.ts +26 -0
  39. package/dist/cache/core/index.d.ts.map +1 -1
  40. package/dist/cache/core/index.js +11 -1
  41. package/dist/cache/core/index.js.map +1 -1
  42. package/dist/cache/core/index.workerd.js +11 -1
  43. package/dist/cache/core/index.workerd.js.map +1 -1
  44. package/dist/captcha/index.js.map +1 -1
  45. package/dist/cli/config/index.d.ts +7 -5
  46. package/dist/cli/config/index.d.ts.map +1 -1
  47. package/dist/cli/config/index.js +2 -3
  48. package/dist/cli/config/index.js.map +1 -1
  49. package/dist/cli/core/index.d.ts +637 -11660
  50. package/dist/cli/core/index.d.ts.map +1 -1
  51. package/dist/cli/core/index.js +707 -532
  52. package/dist/cli/core/index.js.map +1 -1
  53. package/dist/cli/devtools/index.d.ts +4 -8
  54. package/dist/cli/devtools/index.d.ts.map +1 -1
  55. package/dist/cli/devtools/index.js +20 -16
  56. package/dist/cli/devtools/index.js.map +1 -1
  57. package/dist/cli/platform/index.d.ts +51 -77
  58. package/dist/cli/platform/index.d.ts.map +1 -1
  59. package/dist/cli/platform/index.js +65 -15
  60. package/dist/cli/platform/index.js.map +1 -1
  61. package/dist/cli/vendor/index.d.ts +10 -13
  62. package/dist/cli/vendor/index.d.ts.map +1 -1
  63. package/dist/cli/vendor/index.js +30 -12
  64. package/dist/cli/vendor/index.js.map +1 -1
  65. package/dist/command/index.js +1 -1
  66. package/dist/command/index.js.map +1 -1
  67. package/dist/core/index.browser.js +27 -3
  68. package/dist/core/index.browser.js.map +1 -1
  69. package/dist/core/index.d.ts +8 -11
  70. package/dist/core/index.d.ts.map +1 -1
  71. package/dist/core/index.js +27 -3
  72. package/dist/core/index.js.map +1 -1
  73. package/dist/core/index.native.js +27 -3
  74. package/dist/core/index.native.js.map +1 -1
  75. package/dist/core/index.workerd.js +27 -3
  76. package/dist/core/index.workerd.js.map +1 -1
  77. package/dist/crypto/index.js.map +1 -1
  78. package/dist/datetime/index.d.ts +69 -10
  79. package/dist/datetime/index.d.ts.map +1 -1
  80. package/dist/datetime/index.js +135 -13
  81. package/dist/datetime/index.js.map +1 -1
  82. package/dist/email/core/index.js.map +1 -1
  83. package/dist/email/smtp/index.js +130 -16
  84. package/dist/email/smtp/index.js.map +1 -1
  85. package/dist/fake/index.js.map +1 -1
  86. package/dist/lock/core/index.d.ts +30 -2
  87. package/dist/lock/core/index.d.ts.map +1 -1
  88. package/dist/lock/core/index.js +35 -12
  89. package/dist/lock/core/index.js.map +1 -1
  90. package/dist/lock/redis/index.js.map +1 -1
  91. package/dist/logger/index.js +32 -1
  92. package/dist/logger/index.js.map +1 -1
  93. package/dist/mcp/index.d.ts +238 -31
  94. package/dist/mcp/index.d.ts.map +1 -1
  95. package/dist/mcp/index.js +198 -67
  96. package/dist/mcp/index.js.map +1 -1
  97. package/dist/orm/core/index.browser.js +2 -362
  98. package/dist/orm/core/index.browser.js.map +1 -1
  99. package/dist/orm/core/index.bun.js +18 -409
  100. package/dist/orm/core/index.bun.js.map +1 -1
  101. package/dist/orm/core/index.d.ts +41 -194
  102. package/dist/orm/core/index.d.ts.map +1 -1
  103. package/dist/orm/core/index.js +27 -422
  104. package/dist/orm/core/index.js.map +1 -1
  105. package/dist/orm/postgres/index.bun.js +17 -20
  106. package/dist/orm/postgres/index.bun.js.map +1 -1
  107. package/dist/orm/postgres/index.d.ts +1 -5
  108. package/dist/orm/postgres/index.d.ts.map +1 -1
  109. package/dist/orm/postgres/index.js +17 -20
  110. package/dist/orm/postgres/index.js.map +1 -1
  111. package/dist/react/core/index.d.ts +102 -1
  112. package/dist/react/core/index.d.ts.map +1 -1
  113. package/dist/react/core/index.js +65 -1
  114. package/dist/react/core/index.js.map +1 -1
  115. package/dist/react/form/index.d.ts +6 -0
  116. package/dist/react/form/index.d.ts.map +1 -1
  117. package/dist/react/form/index.js +7 -7
  118. package/dist/react/form/index.js.map +1 -1
  119. package/dist/react/i18n/index.d.ts +7 -1
  120. package/dist/react/i18n/index.d.ts.map +1 -1
  121. package/dist/react/i18n/index.js +6 -0
  122. package/dist/react/i18n/index.js.map +1 -1
  123. package/dist/react/intro/index.js +22 -17
  124. package/dist/react/intro/index.js.map +1 -1
  125. package/dist/react/router/index.browser.js +98 -4
  126. package/dist/react/router/index.browser.js.map +1 -1
  127. package/dist/react/router/index.d.ts +58 -5
  128. package/dist/react/router/index.d.ts.map +1 -1
  129. package/dist/react/router/index.js +122 -6
  130. package/dist/react/router/index.js.map +1 -1
  131. package/dist/react/testing/{chunk-DBEY4PJZ.js → chunk-6Ep1yQYe.js} +1 -1
  132. package/dist/react/testing/index.js +1 -1
  133. package/dist/react/testing/index.js.map +1 -1
  134. package/dist/react/ui/index.d.ts +195 -1
  135. package/dist/react/ui/index.d.ts.map +1 -1
  136. package/dist/react/ui/index.js +64 -1
  137. package/dist/react/ui/index.js.map +1 -1
  138. package/dist/react/websocket/index.js.map +1 -1
  139. package/dist/redis/index.js.map +1 -1
  140. package/dist/scheduler/index.d.ts +1 -2
  141. package/dist/scheduler/index.d.ts.map +1 -1
  142. package/dist/scheduler/index.js +1 -1
  143. package/dist/scheduler/index.js.map +1 -1
  144. package/dist/scheduler/index.workerd.js +1 -1
  145. package/dist/scheduler/index.workerd.js.map +1 -1
  146. package/dist/security/index.browser.js.map +1 -1
  147. package/dist/security/index.d.ts.map +1 -1
  148. package/dist/security/index.js +2 -2
  149. package/dist/security/index.js.map +1 -1
  150. package/dist/server/auth/index.d.ts.map +1 -1
  151. package/dist/server/auth/index.js +24 -10
  152. package/dist/server/auth/index.js.map +1 -1
  153. package/dist/server/cookies/index.js.map +1 -1
  154. package/dist/server/core/index.browser.js +10 -3
  155. package/dist/server/core/index.browser.js.map +1 -1
  156. package/dist/server/core/index.d.ts +1 -4
  157. package/dist/server/core/index.d.ts.map +1 -1
  158. package/dist/server/core/index.js +47 -9
  159. package/dist/server/core/index.js.map +1 -1
  160. package/dist/server/links/index.browser.js.map +1 -1
  161. package/dist/server/links/index.js.map +1 -1
  162. package/dist/server/metrics/index.js +19 -1
  163. package/dist/server/metrics/index.js.map +1 -1
  164. package/dist/server/rate-limit/index.js.map +1 -1
  165. package/dist/server/static/index.js.map +1 -1
  166. package/dist/server/swagger/index.d.ts.map +1 -1
  167. package/dist/server/swagger/index.js +4 -5
  168. package/dist/server/swagger/index.js.map +1 -1
  169. package/dist/sms/index.js.map +1 -1
  170. package/dist/system/index.browser.js.map +1 -1
  171. package/dist/system/index.js.map +1 -1
  172. package/dist/system/index.workerd.js.map +1 -1
  173. package/dist/topic/core/index.js.map +1 -1
  174. package/dist/websocket/index.browser.js +32 -5
  175. package/dist/websocket/index.browser.js.map +1 -1
  176. package/dist/websocket/index.d.ts +3 -1
  177. package/dist/websocket/index.d.ts.map +1 -1
  178. package/dist/websocket/index.js +42 -6
  179. package/dist/websocket/index.js.map +1 -1
  180. package/package.json +685 -274
  181. package/src/api/files/__tests__/FileController.spec.ts +1 -1
  182. package/src/api/jobs/__tests__/$job.spec.ts +5 -1
  183. package/src/api/parameters/services/ParameterProvider.ts +21 -4
  184. package/src/api/users/__tests__/SessionService.spec.ts +99 -0
  185. package/src/api/users/__tests__/UserJobs.spec.ts +67 -0
  186. package/src/api/users/atoms/realmAuthSettingsAtom.ts +15 -0
  187. package/src/api/users/entities/sessions.ts +6 -0
  188. package/src/api/users/jobs/UserJobs.ts +44 -17
  189. package/src/api/users/providers/RealmProvider.ts +4 -0
  190. package/src/api/users/schemas/userQuerySchema.ts +0 -1
  191. package/src/api/users/services/SessionService.ts +27 -0
  192. package/src/api/users/services/UserService.ts +1 -5
  193. package/src/api/verifications/__tests__/CodeVerification.spec.ts +14 -0
  194. package/src/api/verifications/__tests__/LinkVerification.spec.ts +14 -0
  195. package/src/api/verifications/services/VerificationService.ts +1 -0
  196. package/src/bucket/__tests__/NodeS3BucketProvider.spec.ts +74 -0
  197. package/src/bucket/index.ts +19 -2
  198. package/src/bucket/primitives/$bucket.ts +9 -1
  199. package/src/bucket/providers/CloudflareR2Provider.ts +2 -137
  200. package/src/bucket/providers/NodeS3BucketProvider.ts +218 -0
  201. package/src/cache/core/index.ts +29 -0
  202. package/src/cache/core/primitives/$cache.ts +14 -1
  203. package/src/cli/config/defineConfig.ts +13 -15
  204. package/src/cli/core/__tests__/init.spec.ts +214 -7
  205. package/src/cli/core/commands/init.ts +12 -0
  206. package/src/cli/core/services/PackageManagerUtils.ts +23 -6
  207. package/src/cli/core/services/ProjectScaffolder.ts +315 -33
  208. package/src/cli/core/tasks/BuildCloudflareTask.ts +5 -0
  209. package/src/cli/core/tasks/BuildDockerTask.ts +9 -10
  210. package/src/cli/core/tasks/BuildServerTask.ts +8 -0
  211. package/src/cli/core/templates/agentMd.ts +2 -10
  212. package/src/cli/core/templates/apiIndexTs.ts +23 -1
  213. package/src/cli/core/templates/componentsJsonTs.ts +39 -0
  214. package/src/cli/core/templates/mainCss.ts +1 -0
  215. package/src/cli/core/templates/saasAdminLayoutTsx.ts +77 -0
  216. package/src/cli/core/templates/saasAdminPagesTsx.ts +26 -0
  217. package/src/cli/core/templates/saasAuthLayoutTsx.ts +20 -0
  218. package/src/cli/core/templates/saasAuthPagesTsx.ts +62 -0
  219. package/src/cli/core/templates/saasRealmProviderTs.ts +46 -0
  220. package/src/cli/core/templates/webAppRouterTs.ts +104 -1
  221. package/src/cli/core/templates/webIndexTs.ts +23 -1
  222. package/src/cli/devtools/index.ts +12 -26
  223. package/src/cli/platform/__tests__/SecretsCommand.spec.ts +2 -0
  224. package/src/cli/platform/index.ts +15 -24
  225. package/src/cli/vendor/atoms/vendorOptions.ts +1 -1
  226. package/src/cli/vendor/index.ts +14 -23
  227. package/src/command/providers/CliProvider.ts +1 -1
  228. package/src/core/Alepha.ts +11 -1
  229. package/src/core/helpers/ref.ts +18 -0
  230. package/src/core/index.shared.ts +1 -0
  231. package/src/core/interfaces/Service.ts +3 -1
  232. package/src/core/providers/SchemaValidator.ts +9 -1
  233. package/src/core/providers/TypeProvider.ts +2 -3
  234. package/src/datetime/REFACTORING.md +118 -0
  235. package/src/datetime/providers/DateTimeProvider.ts +203 -24
  236. package/src/lock/core/index.ts +31 -0
  237. package/src/lock/core/primitives/$lock.ts +14 -1
  238. package/src/logger/services/Logger.ts +1 -1
  239. package/src/mcp/__tests__/$resource.spec.ts +1 -1
  240. package/src/mcp/__tests__/$tool.spec.ts +1 -1
  241. package/src/mcp/__tests__/McpServerProvider.spec.ts +1 -1
  242. package/src/mcp/__tests__/jsonrpc.spec.ts +1 -1
  243. package/src/mcp/helpers/jsonrpc.ts +26 -1
  244. package/src/mcp/index.ts +10 -5
  245. package/src/mcp/interfaces/McpTypes.ts +83 -6
  246. package/src/mcp/primitives/$prompt.ts +18 -1
  247. package/src/mcp/primitives/$resource.ts +18 -1
  248. package/src/mcp/primitives/$tool.ts +83 -7
  249. package/src/mcp/providers/McpServerProvider.ts +74 -16
  250. package/src/mcp/transports/StreamableHttpMcpTransport.ts +226 -0
  251. package/src/orm/REFACTORING.md +330 -0
  252. package/src/orm/__tests__/$repository-tests.ts +1 -0
  253. package/src/orm/__tests__/orm-next-tests.ts +2 -67
  254. package/src/orm/__tests__/orm-next.spec.ts +0 -21
  255. package/src/orm/core/index.shared.ts +0 -2
  256. package/src/orm/core/index.ts +1 -2
  257. package/src/orm/core/primitives/$repository.ts +3 -6
  258. package/src/orm/core/primitives/$transactional.ts +11 -0
  259. package/src/orm/core/providers/drivers/DatabaseProvider.ts +0 -5
  260. package/src/orm/core/providers/drivers/NodeSqliteProvider.ts +11 -13
  261. package/src/orm/core/schemas/updateSchema.ts +1 -1
  262. package/src/orm/core/services/ModelBuilder.ts +1 -13
  263. package/src/orm/core/services/PgRelationManager.ts +4 -2
  264. package/src/orm/core/services/Repository.ts +1 -42
  265. package/src/orm/core/services/SqliteModelBuilder.ts +2 -33
  266. package/src/orm/postgres/services/PostgresModelBuilder.ts +10 -45
  267. package/src/react/core/__tests__/useQuery.browser.spec.tsx +86 -0
  268. package/src/react/core/hooks/useQuery.ts +153 -0
  269. package/src/react/core/index.ts +1 -0
  270. package/src/react/form/services/FormModel.ts +15 -6
  271. package/src/react/form/services/parseField.ts +8 -0
  272. package/src/react/i18n/providers/I18nProvider.ts +8 -2
  273. package/src/react/intro/components/GettingStartedAuthSlide.tsx +11 -4
  274. package/src/react/router/__tests__/$page.spec.tsx +0 -16
  275. package/src/react/router/__tests__/ReactBrowserProvider.browser.spec.ts +213 -2
  276. package/src/react/router/__tests__/ssr.spec.tsx +339 -0
  277. package/src/react/router/primitives/$page.ts +28 -4
  278. package/src/react/router/providers/ReactBrowserProvider.ts +73 -0
  279. package/src/react/router/providers/ReactBrowserRouterProvider.ts +1 -1
  280. package/src/react/router/providers/ReactPageProvider.ts +27 -9
  281. package/src/react/router/providers/ReactPreloadProvider.ts +1 -1
  282. package/src/react/router/providers/ReactServerProvider.ts +1 -0
  283. package/src/react/ui/atoms/uiThemeListAtom.ts +36 -0
  284. package/src/react/ui/index.ts +6 -0
  285. package/src/react/ui/services/SchemaControl.ts +209 -0
  286. package/src/scheduler/providers/CronProvider.ts +1 -1
  287. package/src/security/primitives/$basicAuth.ts +1 -1
  288. package/src/security/primitives/$issuer.ts +6 -3
  289. package/src/server/auth/providers/ServerAuthProvider.ts +5 -1
  290. package/src/server/core/__tests__/ServerRouterProvider-serializationError.spec.ts +75 -0
  291. package/src/server/core/__tests__/ServerRouterProvider-validationError.spec.ts +306 -0
  292. package/src/server/core/errors/ValidationError.ts +13 -1
  293. package/src/server/core/interfaces/ServerRequest.ts +1 -0
  294. package/src/server/core/primitives/$action.ts +16 -5
  295. package/src/server/core/providers/ServerProvider.ts +1 -1
  296. package/src/server/core/providers/ServerRouterProvider.ts +28 -6
  297. package/src/server/core/services/HttpClient.ts +1 -1
  298. package/src/server/swagger/providers/ServerSwaggerProvider.ts +6 -8
  299. package/src/websocket/providers/NodeWebSocketServerProvider.ts +10 -4
  300. package/src/websocket/services/WebSocketClient.ts +11 -5
  301. package/src/mcp/transports/SseMcpTransport.ts +0 -182
  302. package/src/orm/core/__tests__/parseQueryString.spec.ts +0 -196
  303. package/src/orm/core/helpers/parseQueryString.ts +0 -502
  304. package/src/orm/core/primitives/$view.ts +0 -88
@@ -0,0 +1,153 @@
1
+ import type { Async } from "alepha";
2
+ import type { DurationLike } from "alepha/datetime";
3
+ import { type DependencyList, useCallback, useState } from "react";
4
+ import type { ActionContext } from "./useAction.ts";
5
+ import { useAction } from "./useAction.ts";
6
+
7
+ /**
8
+ * Hook for declarative data fetching with automatic execution and refetch.
9
+ *
10
+ * Thin wrapper over {@link useAction}: it pre-applies `runOnInit: true`,
11
+ * exposes the last result as `data`, and provides a stable `refetch()` to
12
+ * re-run the query on demand. For optimistic mutations and side-effects,
13
+ * use {@link useAction} directly — `useQuery` is for the read path.
14
+ *
15
+ * Caching, request deduplication, and AbortSignal cancellation come from
16
+ * `useAction` + `HttpClient`. There is no separate cache layer — pass
17
+ * `localCache` to your `HttpClient.fetch()`/`fetchAction()` call inside
18
+ * the query handler if you want per-call caching.
19
+ *
20
+ * @example Basic
21
+ * ```tsx
22
+ * const client = useInject(HttpClient);
23
+ * const { data, loading, error, refetch } = useQuery({
24
+ * handler: async ({ signal }) => {
25
+ * const res = await client.fetch("/api/users", { request: { signal } });
26
+ * return res.data;
27
+ * },
28
+ * }, []);
29
+ * ```
30
+ *
31
+ * @example Re-fetch when a dep changes
32
+ * ```tsx
33
+ * const { data } = useQuery({
34
+ * handler: async () => api.getUser(userId),
35
+ * }, [userId]);
36
+ * ```
37
+ *
38
+ * @example Polling
39
+ * ```tsx
40
+ * const { data } = useQuery({
41
+ * handler: async () => api.getStatus(),
42
+ * runEvery: [5, "seconds"],
43
+ * }, []);
44
+ * ```
45
+ */
46
+ export function useQuery<Result>(
47
+ options: UseQueryOptions<Result>,
48
+ deps: DependencyList,
49
+ ): UseQueryReturn<Result> {
50
+ const [data, setData] = useState<Result | undefined>(options.initialData);
51
+
52
+ const action = useAction<[], Result>(
53
+ {
54
+ id: options.id,
55
+ handler: options.handler,
56
+ runOnInit: options.enabled !== false,
57
+ runEvery: options.runEvery,
58
+ debounce: options.debounce,
59
+ onError: options.onError,
60
+ onSuccess: async (result) => {
61
+ setData(result);
62
+ if (options.onSuccess) {
63
+ await options.onSuccess(result);
64
+ }
65
+ },
66
+ },
67
+ deps,
68
+ );
69
+
70
+ const refetch = useCallback(() => action.run(), [action.run]);
71
+
72
+ return {
73
+ data,
74
+ loading: action.loading,
75
+ error: action.error,
76
+ refetch,
77
+ cancel: action.cancel,
78
+ };
79
+ }
80
+
81
+ // ---------------------------------------------------------------------------------------------------------------------
82
+
83
+ export interface UseQueryOptions<Result> {
84
+ /**
85
+ * Async query handler. Receives an {@link ActionContext} with an
86
+ * AbortSignal that fires on unmount, dependency change, or `cancel()`.
87
+ */
88
+ handler: (context: ActionContext) => Async<Result>;
89
+
90
+ /**
91
+ * Optional identifier (used in lifecycle events for debugging/analytics).
92
+ */
93
+ id?: string;
94
+
95
+ /**
96
+ * If `false`, skip automatic execution on mount and dep change. Use
97
+ * `refetch()` to trigger manually. Defaults to `true`.
98
+ */
99
+ enabled?: boolean;
100
+
101
+ /**
102
+ * Initial value for `data` before the first successful fetch.
103
+ */
104
+ initialData?: Result;
105
+
106
+ /**
107
+ * Re-run periodically. See {@link useAction} `runEvery`.
108
+ */
109
+ runEvery?: DurationLike;
110
+
111
+ /**
112
+ * Debounce delay in milliseconds. See {@link useAction} `debounce`.
113
+ */
114
+ debounce?: number;
115
+
116
+ /**
117
+ * Called on success with the resolved value.
118
+ */
119
+ onSuccess?: (result: Result) => void | Promise<void>;
120
+
121
+ /**
122
+ * Custom error handler. If provided, prevents default error re-throw.
123
+ */
124
+ onError?: (error: Error) => void | Promise<void>;
125
+ }
126
+
127
+ export interface UseQueryReturn<Result> {
128
+ /**
129
+ * The last successful result. `undefined` until the first fetch resolves
130
+ * (or the value of `initialData` if provided).
131
+ */
132
+ data: Result | undefined;
133
+
134
+ /**
135
+ * Loading state — `true` while a fetch is in flight.
136
+ */
137
+ loading: boolean;
138
+
139
+ /**
140
+ * Error from the last failed fetch, if any.
141
+ */
142
+ error?: Error;
143
+
144
+ /**
145
+ * Re-run the query. The previous in-flight request, if any, is aborted.
146
+ */
147
+ refetch: () => Promise<Result | undefined>;
148
+
149
+ /**
150
+ * Abort the in-flight request without scheduling another.
151
+ */
152
+ cancel: () => void;
153
+ }
@@ -13,6 +13,7 @@ export * from "./hooks/useAlepha.ts";
13
13
  export * from "./hooks/useClient.ts";
14
14
  export * from "./hooks/useEvents.ts";
15
15
  export * from "./hooks/useInject.ts";
16
+ export * from "./hooks/useQuery.ts";
16
17
  export * from "./hooks/useStore.ts";
17
18
 
18
19
  // ---------------------------------------------------------------------------------------------------------------------
@@ -140,15 +140,23 @@ export class FormModel<T extends TObject> {
140
140
 
141
141
  public readonly reset = (event?: FormEventLike) => {
142
142
  event?.preventDefault?.();
143
+ // Snapshot all keys that need notification — both keys present
144
+ // before reset (so subscribers learn the cleared value) and keys
145
+ // restored from initialValues. Without the union, fields that were
146
+ // typed but absent from initialValues stay visually stale.
147
+ const keys = new Set<string>([
148
+ ...Object.keys(this.values),
149
+ ...Object.keys(this.initialValues),
150
+ ]);
143
151
  for (const key in this.values) {
144
152
  delete this.values[key];
145
153
  }
146
154
  Object.assign(this.values, { ...this.initialValues });
147
- for (const [key, value] of Object.entries(this.values)) {
155
+ for (const key of keys) {
148
156
  const path = `/${key.replaceAll(".", "/")}`;
149
157
  this.alepha.events.emit(
150
158
  "form:change",
151
- { id: this.id, path, value },
159
+ { id: this.id, path, value: this.values[key] },
152
160
  { catch: true },
153
161
  );
154
162
  }
@@ -356,10 +364,11 @@ export class FormModel<T extends TObject> {
356
364
  name: key,
357
365
  };
358
366
 
359
- if (options.id) {
360
- attr.id = `${options.id}-${key}`;
361
- (attr as any)["data-testid"] = attr.id;
362
- }
367
+ // Use the form's runtime id (always set — comes from `useId()` when
368
+ // no explicit `options.id` was provided). This guarantees stable
369
+ // per-field DOM ids without forcing callers to pass `id`.
370
+ attr.id = `${this.id}-${key}`;
371
+ (attr as any)["data-testid"] = attr.id;
363
372
 
364
373
  if (t.schema.isString(field)) {
365
374
  if (field.maxLength != null) {
@@ -45,6 +45,12 @@ export interface FieldMeta {
45
45
  constraints: FieldConstraints;
46
46
  testId?: string;
47
47
  schema: TSchema;
48
+ /**
49
+ * Raw `$control` value from the schema, untyped here. The UI layer
50
+ * (`alepha/react/ui`) provides the strict {@link SchemaControl} type and
51
+ * a `resolveSchemaControl` helper to evaluate the function form.
52
+ */
53
+ control?: unknown;
48
54
  }
49
55
 
50
56
  export interface ParseFieldOptions {
@@ -77,6 +83,7 @@ export const parseField = (
77
83
  pattern?: string;
78
84
  properties?: unknown;
79
85
  items?: { properties?: unknown };
86
+ $control?: unknown;
80
87
  };
81
88
 
82
89
  const label =
@@ -132,6 +139,7 @@ export const parseField = (
132
139
  | string
133
140
  | undefined,
134
141
  schema: input.schema,
142
+ control: schema.$control,
135
143
  };
136
144
  };
137
145
 
@@ -230,15 +230,21 @@ export class I18nProvider<
230
230
  return value;
231
231
  };
232
232
 
233
+ /**
234
+ * Look up `key` in the registered dictionaries. The `(string & {})` arm
235
+ * keeps autocomplete for the typed dictionary keys while allowing shared
236
+ * library components to pass arbitrary string keys (with a `default`
237
+ * fallback) without casting to `as never`.
238
+ */
233
239
  public readonly tr = (
234
- key: keyof ServiceDictionary<S>[K],
240
+ key: keyof ServiceDictionary<S>[K] | (string & {}),
235
241
  options: {
236
242
  args?: string[];
237
243
  default?: string;
238
244
  } = {},
239
245
  ) => {
240
246
  const translation = this.translate(key as string, options.args || []);
241
- if (translation === key && options.default) {
247
+ if (translation === (key as string) && options.default) {
242
248
  return options.default;
243
249
  }
244
250
  return translation;
@@ -8,7 +8,7 @@ import type { GettingStartedSlide } from "./GettingStarted.tsx";
8
8
  * Returns undefined if auth routes are not configured.
9
9
  */
10
10
  export const useAuthSlide = (): GettingStartedSlide | undefined => {
11
- const { user } = useAuth();
11
+ const { user, logout } = useAuth();
12
12
  const router = useRouter();
13
13
 
14
14
  // Check if auth routes exist
@@ -19,8 +19,6 @@ export const useAuthSlide = (): GettingStartedSlide | undefined => {
19
19
 
20
20
  // User is logged in - show user info and logout option
21
21
  if (user) {
22
- const logoutAnchorProps = router.anchor(router.path("logout"));
23
-
24
22
  return {
25
23
  text: "Welcome back!",
26
24
  sub: `You're signed in as ${user.email || user.username || "user"}.`,
@@ -33,7 +31,16 @@ export const useAuthSlide = (): GettingStartedSlide | undefined => {
33
31
  num: "→",
34
32
  text: (
35
33
  <>
36
- <a {...logoutAnchorProps}>Sign out</a> to test the login flow
34
+ <a
35
+ href="#"
36
+ onClick={(e) => {
37
+ e.preventDefault();
38
+ logout();
39
+ }}
40
+ >
41
+ Sign out
42
+ </a>{" "}
43
+ to test the login flow
37
44
  </>
38
45
  ),
39
46
  },
@@ -419,22 +419,6 @@ describe("$page primitive tests", () => {
419
419
  expect(await app.staticPage.fetch().then((it) => it.html)).toBe(html);
420
420
  });
421
421
 
422
- test("$page - client-side only rendering", async ({ expect }) => {
423
- class App {
424
- clientOnly = $page({
425
- path: "/client",
426
- client: true,
427
- component: () => "Client only",
428
- });
429
- }
430
-
431
- const app = alepha.inject(App);
432
- await alepha.start();
433
-
434
- const clientRendered = await app.clientOnly.fetch();
435
- expect(clientRendered.html).toBe("");
436
- });
437
-
438
422
  test("$page - server response handler", async ({ expect }) => {
439
423
  const mockHandler = vi.fn();
440
424
 
@@ -1,9 +1,22 @@
1
1
  import { Alepha } from "alepha";
2
- import { beforeEach, describe, expect, it, vi } from "vitest";
3
- import { ReactBrowserProvider } from "../providers/ReactBrowserProvider.ts";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import {
4
+ ReactBrowserProvider,
5
+ type RouterPushOptions,
6
+ reactBrowserOptions,
7
+ } from "../providers/ReactBrowserProvider.ts";
4
8
 
5
9
  class TestReactBrowserProvider extends ReactBrowserProvider {
6
10
  public testGetHydrationState = this.getHydrationState.bind(this);
11
+ public testAttachAnchorInterceptor = this.attachAnchorInterceptor.bind(this);
12
+ public pushCalls: Array<{ url: string; options?: RouterPushOptions }> = [];
13
+
14
+ public override async push(
15
+ url: string,
16
+ options?: RouterPushOptions,
17
+ ): Promise<void> {
18
+ this.pushCalls.push({ url, options });
19
+ }
7
20
  }
8
21
 
9
22
  describe("ReactBrowserProvider", () => {
@@ -102,4 +115,202 @@ describe("ReactBrowserProvider", () => {
102
115
  expect(result?.["alepha.i18n.locale"]).toBe("en");
103
116
  });
104
117
  });
118
+
119
+ describe("attachAnchorInterceptor", () => {
120
+ let alepha: Alepha;
121
+ let provider: TestReactBrowserProvider;
122
+ let detach: () => void;
123
+ let container: HTMLDivElement;
124
+
125
+ const createAnchor = (
126
+ attrs: Record<string, string>,
127
+ inner?: HTMLElement,
128
+ ): HTMLAnchorElement => {
129
+ const a = document.createElement("a");
130
+ for (const [k, v] of Object.entries(attrs)) {
131
+ a.setAttribute(k, v);
132
+ }
133
+ if (inner) {
134
+ a.appendChild(inner);
135
+ } else {
136
+ a.textContent = "link";
137
+ }
138
+ container.appendChild(a);
139
+ return a;
140
+ };
141
+
142
+ const click = (
143
+ target: HTMLElement,
144
+ init: MouseEventInit = {},
145
+ ): MouseEvent => {
146
+ const ev = new MouseEvent("click", {
147
+ bubbles: true,
148
+ cancelable: true,
149
+ button: 0,
150
+ ...init,
151
+ });
152
+ target.dispatchEvent(ev);
153
+ return ev;
154
+ };
155
+
156
+ beforeEach(() => {
157
+ alepha = Alepha.create();
158
+ provider = alepha.inject(TestReactBrowserProvider);
159
+ container = document.createElement("div");
160
+ document.body.appendChild(container);
161
+ detach = provider.testAttachAnchorInterceptor();
162
+ });
163
+
164
+ afterEach(() => {
165
+ detach();
166
+ container.remove();
167
+ });
168
+
169
+ it("intercepts plain internal /foo anchor clicks", () => {
170
+ const a = createAnchor({ href: "/foo" });
171
+
172
+ const ev = click(a);
173
+
174
+ expect(provider.pushCalls).toHaveLength(1);
175
+ expect(provider.pushCalls[0].url).toBe("/foo");
176
+ expect(ev.defaultPrevented).toBe(true);
177
+ });
178
+
179
+ it("preserves query and hash when intercepting", () => {
180
+ const a = createAnchor({ href: "/foo?x=1#bar" });
181
+
182
+ click(a);
183
+
184
+ expect(provider.pushCalls[0].url).toBe("/foo?x=1#bar");
185
+ });
186
+
187
+ it("ignores cmd-click (metaKey)", () => {
188
+ const a = createAnchor({ href: "/foo" });
189
+
190
+ const ev = click(a, { metaKey: true });
191
+
192
+ expect(provider.pushCalls).toHaveLength(0);
193
+ expect(ev.defaultPrevented).toBe(false);
194
+ });
195
+
196
+ it("ignores ctrl-click", () => {
197
+ const a = createAnchor({ href: "/foo" });
198
+
199
+ click(a, { ctrlKey: true });
200
+
201
+ expect(provider.pushCalls).toHaveLength(0);
202
+ });
203
+
204
+ it("ignores shift-click", () => {
205
+ const a = createAnchor({ href: "/foo" });
206
+
207
+ click(a, { shiftKey: true });
208
+
209
+ expect(provider.pushCalls).toHaveLength(0);
210
+ });
211
+
212
+ it("ignores alt-click", () => {
213
+ const a = createAnchor({ href: "/foo" });
214
+
215
+ click(a, { altKey: true });
216
+
217
+ expect(provider.pushCalls).toHaveLength(0);
218
+ });
219
+
220
+ it("ignores non-primary mouse buttons", () => {
221
+ const a = createAnchor({ href: "/foo" });
222
+
223
+ click(a, { button: 1 });
224
+
225
+ expect(provider.pushCalls).toHaveLength(0);
226
+ });
227
+
228
+ it("ignores anchors with target='_blank'", () => {
229
+ const a = createAnchor({ href: "/foo", target: "_blank" });
230
+
231
+ click(a);
232
+
233
+ expect(provider.pushCalls).toHaveLength(0);
234
+ });
235
+
236
+ it("ignores anchors with download attribute", () => {
237
+ const a = createAnchor({ href: "/foo", download: "" });
238
+
239
+ click(a);
240
+
241
+ expect(provider.pushCalls).toHaveLength(0);
242
+ });
243
+
244
+ it("ignores anchors with data-no-router attribute", () => {
245
+ const a = createAnchor({ href: "/foo", "data-no-router": "" });
246
+
247
+ click(a);
248
+
249
+ expect(provider.pushCalls).toHaveLength(0);
250
+ });
251
+
252
+ it("ignores mailto: hrefs", () => {
253
+ const a = createAnchor({ href: "mailto:foo@bar.com" });
254
+
255
+ click(a);
256
+
257
+ expect(provider.pushCalls).toHaveLength(0);
258
+ });
259
+
260
+ it("ignores tel: hrefs", () => {
261
+ const a = createAnchor({ href: "tel:+15555555" });
262
+
263
+ click(a);
264
+
265
+ expect(provider.pushCalls).toHaveLength(0);
266
+ });
267
+
268
+ it("ignores hrefs to external origins", () => {
269
+ const a = createAnchor({ href: "https://example.com/foo" });
270
+
271
+ click(a);
272
+
273
+ expect(provider.pushCalls).toHaveLength(0);
274
+ });
275
+
276
+ it("ignores hash-only #section hrefs", () => {
277
+ const a = createAnchor({ href: "#section" });
278
+
279
+ click(a);
280
+
281
+ expect(provider.pushCalls).toHaveLength(0);
282
+ });
283
+
284
+ it("intercepts when click target is nested inside the anchor", () => {
285
+ const span = document.createElement("span");
286
+ span.textContent = "inner";
287
+ createAnchor({ href: "/foo" }, span);
288
+
289
+ click(span);
290
+
291
+ expect(provider.pushCalls).toHaveLength(1);
292
+ expect(provider.pushCalls[0].url).toBe("/foo");
293
+ });
294
+
295
+ it("skips when defaultPrevented is already true", () => {
296
+ const a = createAnchor({ href: "/foo" });
297
+ a.addEventListener("click", (ev) => ev.preventDefault());
298
+
299
+ click(a);
300
+
301
+ expect(provider.pushCalls).toHaveLength(0);
302
+ });
303
+
304
+ it("respects interceptAnchorClicks=false at runtime", () => {
305
+ alepha.store.set(reactBrowserOptions.key, {
306
+ ...alepha.store.get(reactBrowserOptions.key)!,
307
+ interceptAnchorClicks: false,
308
+ });
309
+ const a = createAnchor({ href: "/foo" });
310
+
311
+ click(a);
312
+
313
+ expect(provider.pushCalls).toHaveLength(0);
314
+ });
315
+ });
105
316
  });