alepha 0.20.3 → 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.
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/jobs/index.d.ts +14 -14
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/keys/index.d.ts +4 -4
- package/dist/api/organizations/index.d.ts.map +1 -1
- package/dist/api/parameters/index.d.ts +8 -3
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +20 -4
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/api/payments/index.d.ts.map +1 -1
- package/dist/api/users/index.browser.js +6 -0
- package/dist/api/users/index.browser.js.map +1 -1
- package/dist/api/users/index.d.ts +5037 -139
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +58 -10
- package/dist/api/users/index.js.map +1 -1
- package/dist/bucket/index.d.ts +77 -107
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/bucket/index.js +148 -4
- package/dist/bucket/index.js.map +1 -1
- package/dist/bucket/index.workerd.js +7 -1
- package/dist/bucket/index.workerd.js.map +1 -1
- package/dist/cache/core/index.d.ts +26 -0
- package/dist/cache/core/index.d.ts.map +1 -1
- package/dist/cache/core/index.js +11 -1
- package/dist/cache/core/index.js.map +1 -1
- package/dist/cache/core/index.workerd.js +11 -1
- package/dist/cache/core/index.workerd.js.map +1 -1
- package/dist/cli/config/index.d.ts +7 -5
- package/dist/cli/config/index.d.ts.map +1 -1
- package/dist/cli/config/index.js +2 -3
- package/dist/cli/config/index.js.map +1 -1
- package/dist/cli/core/index.d.ts +420 -13
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +22 -511
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/devtools/index.d.ts +4 -8
- package/dist/cli/devtools/index.d.ts.map +1 -1
- package/dist/cli/devtools/index.js +13 -15
- package/dist/cli/devtools/index.js.map +1 -1
- package/dist/cli/platform/index.d.ts +10 -13
- package/dist/cli/platform/index.d.ts.map +1 -1
- package/dist/cli/platform/index.js +18 -15
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/cli/vendor/index.d.ts +10 -13
- package/dist/cli/vendor/index.d.ts.map +1 -1
- package/dist/cli/vendor/index.js +16 -13
- package/dist/cli/vendor/index.js.map +1 -1
- package/dist/core/index.browser.js +27 -3
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +6 -3
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +27 -3
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +27 -3
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js +27 -3
- package/dist/core/index.workerd.js.map +1 -1
- package/dist/datetime/index.d.ts +69 -10
- package/dist/datetime/index.d.ts.map +1 -1
- package/dist/datetime/index.js +135 -13
- package/dist/datetime/index.js.map +1 -1
- package/dist/email/smtp/index.js +10636 -2
- package/dist/email/smtp/index.js.map +1 -1
- package/dist/fake/index.d.ts +8085 -4
- package/dist/fake/index.d.ts.map +1 -1
- package/dist/fake/index.js +33554 -3
- package/dist/fake/index.js.map +1 -1
- package/dist/lock/core/index.d.ts +30 -2
- package/dist/lock/core/index.d.ts.map +1 -1
- package/dist/lock/core/index.js +35 -12
- package/dist/lock/core/index.js.map +1 -1
- package/dist/mcp/index.d.ts +238 -31
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +198 -71
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/core/index.browser.js +1 -1
- package/dist/orm/core/index.browser.js.map +1 -1
- package/dist/orm/core/index.bun.js +4 -3
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.d.ts +4877 -9
- package/dist/orm/core/index.d.ts.map +1 -1
- package/dist/orm/core/index.js +4 -3
- package/dist/orm/core/index.js.map +1 -1
- package/dist/orm/postgres/index.d.ts +608 -1
- package/dist/orm/postgres/index.d.ts.map +1 -1
- package/dist/react/core/index.d.ts +102 -1
- package/dist/react/core/index.d.ts.map +1 -1
- package/dist/react/core/index.js +65 -1
- package/dist/react/core/index.js.map +1 -1
- package/dist/react/form/index.d.ts +6 -0
- package/dist/react/form/index.d.ts.map +1 -1
- package/dist/react/form/index.js +7 -7
- package/dist/react/form/index.js.map +1 -1
- package/dist/react/i18n/index.d.ts +7 -1
- package/dist/react/i18n/index.d.ts.map +1 -1
- package/dist/react/i18n/index.js +6 -0
- package/dist/react/i18n/index.js.map +1 -1
- package/dist/react/router/index.browser.js +20 -2
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +36 -4
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js +20 -2
- package/dist/react/router/index.js.map +1 -1
- package/dist/react/testing/chunk-6Ep1yQYe.js +16 -0
- package/dist/react/testing/index.d.ts +411 -1
- package/dist/react/testing/index.d.ts.map +1 -1
- package/dist/react/testing/index.js +12293 -13
- package/dist/react/testing/index.js.map +1 -1
- package/dist/react/ui/index.d.ts +195 -1
- package/dist/react/ui/index.d.ts.map +1 -1
- package/dist/react/ui/index.js +61 -1
- package/dist/react/ui/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +84 -3
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +390 -1
- package/dist/scheduler/index.js.map +1 -1
- package/dist/scheduler/index.workerd.js +390 -1
- package/dist/scheduler/index.workerd.js.map +1 -1
- package/dist/security/index.d.ts +325 -2
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +1361 -2
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +1054 -1
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +1223 -1
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/core/index.browser.js +10 -3
- package/dist/server/core/index.browser.js.map +1 -1
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js +28 -5
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/metrics/index.d.ts +514 -1
- package/dist/server/metrics/index.d.ts.map +1 -1
- package/dist/server/metrics/index.js +4374 -4
- package/dist/server/metrics/index.js.map +1 -1
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/server/swagger/index.js +3 -4
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/websocket/index.browser.js +11 -5
- package/dist/websocket/index.browser.js.map +1 -1
- package/dist/websocket/index.d.ts +3 -1
- package/dist/websocket/index.d.ts.map +1 -1
- package/dist/websocket/index.js +21 -6
- package/dist/websocket/index.js.map +1 -1
- package/package.json +671 -263
- package/src/api/parameters/services/ParameterProvider.ts +21 -4
- package/src/api/users/__tests__/SessionService.spec.ts +99 -0
- package/src/api/users/__tests__/UserJobs.spec.ts +67 -0
- package/src/api/users/atoms/realmAuthSettingsAtom.ts +15 -0
- package/src/api/users/entities/sessions.ts +6 -0
- package/src/api/users/jobs/UserJobs.ts +44 -17
- package/src/api/users/providers/RealmProvider.ts +4 -0
- package/src/api/users/services/SessionService.ts +27 -0
- package/src/bucket/__tests__/NodeS3BucketProvider.spec.ts +74 -0
- package/src/bucket/index.ts +19 -2
- package/src/bucket/primitives/$bucket.ts +9 -1
- package/src/bucket/providers/CloudflareR2Provider.ts +2 -137
- package/src/bucket/providers/NodeS3BucketProvider.ts +218 -0
- package/src/cache/core/index.ts +29 -0
- package/src/cache/core/primitives/$cache.ts +14 -1
- package/src/cli/config/defineConfig.ts +13 -15
- package/src/cli/core/__tests__/init.spec.ts +6 -7
- package/src/cli/core/services/ProjectScaffolder.ts +18 -14
- package/src/cli/core/tasks/BuildCloudflareTask.ts +5 -0
- package/src/cli/core/templates/agentMd.ts +2 -10
- package/src/cli/core/templates/saasAdminLayoutTsx.ts +3 -3
- package/src/cli/devtools/index.ts +12 -26
- package/src/cli/platform/index.ts +15 -24
- package/src/cli/vendor/atoms/vendorOptions.ts +1 -1
- package/src/cli/vendor/index.ts +14 -23
- package/src/core/Alepha.ts +11 -1
- package/src/core/helpers/ref.ts +18 -0
- package/src/core/index.shared.ts +1 -0
- package/src/core/providers/SchemaValidator.ts +9 -1
- package/src/core/providers/TypeProvider.ts +1 -2
- package/src/datetime/REFACTORING.md +118 -0
- package/src/datetime/providers/DateTimeProvider.ts +203 -24
- package/src/lock/core/index.ts +31 -0
- package/src/lock/core/primitives/$lock.ts +14 -1
- package/src/mcp/__tests__/jsonrpc.spec.ts +1 -1
- package/src/mcp/helpers/jsonrpc.ts +26 -1
- package/src/mcp/index.ts +10 -5
- package/src/mcp/interfaces/McpTypes.ts +83 -6
- package/src/mcp/primitives/$prompt.ts +18 -1
- package/src/mcp/primitives/$resource.ts +18 -1
- package/src/mcp/primitives/$tool.ts +83 -7
- package/src/mcp/providers/McpServerProvider.ts +74 -16
- package/src/mcp/transports/StreamableHttpMcpTransport.ts +226 -0
- package/src/orm/REFACTORING.md +330 -0
- package/src/orm/core/primitives/$transactional.ts +11 -0
- package/src/orm/core/schemas/updateSchema.ts +1 -1
- package/src/orm/core/services/PgRelationManager.ts +4 -2
- package/src/react/core/__tests__/useQuery.browser.spec.tsx +86 -0
- package/src/react/core/hooks/useQuery.ts +153 -0
- package/src/react/core/index.ts +1 -0
- package/src/react/form/services/FormModel.ts +15 -6
- package/src/react/form/services/parseField.ts +8 -0
- package/src/react/i18n/providers/I18nProvider.ts +8 -2
- package/src/react/router/__tests__/$page.spec.tsx +0 -16
- package/src/react/router/__tests__/ssr.spec.tsx +339 -0
- package/src/react/router/primitives/$page.ts +28 -4
- package/src/react/router/providers/ReactPageProvider.ts +27 -9
- package/src/react/ui/atoms/uiThemeListAtom.ts +36 -0
- package/src/react/ui/index.ts +6 -0
- package/src/react/ui/services/SchemaControl.ts +209 -0
- package/src/security/primitives/$issuer.ts +6 -3
- package/src/server/core/__tests__/ServerRouterProvider-serializationError.spec.ts +75 -0
- package/src/server/core/__tests__/ServerRouterProvider-validationError.spec.ts +306 -0
- package/src/server/core/errors/ValidationError.ts +13 -1
- package/src/server/core/primitives/$action.ts +16 -5
- package/src/server/core/providers/ServerRouterProvider.ts +26 -4
- package/src/server/swagger/providers/ServerSwaggerProvider.ts +5 -7
- package/src/websocket/providers/NodeWebSocketServerProvider.ts +10 -4
- package/src/websocket/services/WebSocketClient.ts +11 -5
- package/src/mcp/transports/SseMcpTransport.ts +0 -182
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# ORM — Refactoring Roadmap
|
|
2
|
+
|
|
3
|
+
## Context
|
|
4
|
+
|
|
5
|
+
Alepha currently runs on `drizzle-orm@0.45.x` (stable). This document captures the planned overhaul to **Drizzle v1** — the version that ships **Relations v2**, the official `node:sqlite` driver, and a rewritten `drizzle-kit`.
|
|
6
|
+
|
|
7
|
+
This refactoring is **deferred** until Drizzle v1 reaches GA. As of writing (2026-05) v1 is still in beta (`v1.0.0-beta.20`, March 2025) with no GA date announced. The framework's stability needs the upstream library to be stable too — pulling in a beta would force every consumer to absorb breakage on someone else's release schedule.
|
|
8
|
+
|
|
9
|
+
When v1 GA ships, this becomes a **bigbang upgrade** (no incremental migration path). The plan below is the playbook to execute on that day.
|
|
10
|
+
|
|
11
|
+
## Why upgrade
|
|
12
|
+
|
|
13
|
+
Alepha wraps Drizzle at the wrong layer. The `Repository` uses the low-level SQL builder (`db.select().from().leftJoin()`) and re-implements join handling manually via `PgRelationManager`. Drizzle's real power — the relational query API — is completely bypassed.
|
|
14
|
+
|
|
15
|
+
Drizzle v1 Relations v2 solves every limitation of Alepha's current join system:
|
|
16
|
+
|
|
17
|
+
| Current limitation | Drizzle v1 solution |
|
|
18
|
+
|---|---|
|
|
19
|
+
| No one-to-many joins | `r.many()` — lateral join + `json_agg` under the hood |
|
|
20
|
+
| No many-to-many | `through()` — junction tables hidden from results |
|
|
21
|
+
| No per-relation `where`/`orderBy`/`limit` | First-class in `db.query.table.findMany({ with: { posts: { where, orderBy, limit } } })` |
|
|
22
|
+
| No `columns` on joined tables | `with: { posts: { columns: { id: true, title: true } } }` |
|
|
23
|
+
| No computed fields on relations | `extras: { fullName: sql\`...\` }` |
|
|
24
|
+
| Manual `.alias()` for self-joins | Handled by relation definition (`alias` param) |
|
|
25
|
+
| `node:sqlite` shim around `better-sqlite3` | Official `drizzle-orm/node-sqlite` driver |
|
|
26
|
+
|
|
27
|
+
## What Alepha keeps (the genuine value-add)
|
|
28
|
+
|
|
29
|
+
These layers justify the Repository pattern. None of this exists in raw Drizzle and they survive the upgrade unchanged:
|
|
30
|
+
|
|
31
|
+
- **Soft deletes** — automatic `deletedAt IS NULL` injection, transparent to all queries
|
|
32
|
+
- **Multi-tenancy** — automatic org scoping via `currentUserAtom`
|
|
33
|
+
- **Optimistic locking** — `save()` with version checking, `DbVersionMismatchError`
|
|
34
|
+
- **Typed error hierarchy** — `DbConflictError`, `DbForeignKeyError`, `DbDeadlockError`, etc.
|
|
35
|
+
- **Pagination** — `paginate()` with count, metadata, sort string parsing
|
|
36
|
+
- **Aggregate API** — type-safe `aggregate()` with GROUP BY, HAVING, dot-notation ordering
|
|
37
|
+
- **Transaction propagation** — implicit via `alepha.store`, no manual `{ tx }` drilling
|
|
38
|
+
- **Query caching** — per-table TTL cache with auto-invalidation on writes
|
|
39
|
+
- **Lifecycle events** — `repository:create:before/after`, `repository:read:before/after`, etc.
|
|
40
|
+
- **Codec integration** — `DateTime`, custom types auto-encoded in WHERE clauses
|
|
41
|
+
- **Schema transforms** — `insertSchema` / `updateSchema` — auto-exclude generated cols, handle defaults
|
|
42
|
+
- **DI integration** — `$repository()`, `$inject()`, service substitution for tests
|
|
43
|
+
- **JSON query DSL** — `{ where: { age: { gt: 18 } } }` — composable, serializable, loggable
|
|
44
|
+
|
|
45
|
+
## What the current wrapper blocks
|
|
46
|
+
|
|
47
|
+
Drizzle features the current Alepha layer hides from users:
|
|
48
|
+
|
|
49
|
+
| Blocked feature | Impact |
|
|
50
|
+
|---|---|
|
|
51
|
+
| Relational query API (`db.query.table.findMany({ with })`) | Critical — one-to-many, per-relation filtering/ordering/limiting |
|
|
52
|
+
| Lateral joins (`leftJoinLateral`) | High — top-N-per-group patterns |
|
|
53
|
+
| CTEs (`$with` / `with`) | High — recursive queries, complex analytics |
|
|
54
|
+
| Per-relation `where`/`orderBy`/`limit` | High |
|
|
55
|
+
| `columns` on relations | Medium |
|
|
56
|
+
| `extras` / computed fields | Medium |
|
|
57
|
+
| Set operators (`union`, `intersect`, `except`) | Medium |
|
|
58
|
+
| UPDATE with FROM/JOINs | Medium — join-based batch updates |
|
|
59
|
+
| INSERT from SELECT | Medium |
|
|
60
|
+
| `onConflictDoNothing` | Low-medium |
|
|
61
|
+
| `selectDistinctOn` | Low-medium |
|
|
62
|
+
|
|
63
|
+
## Drizzle v1 — feature reference
|
|
64
|
+
|
|
65
|
+
### Relations v2
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
const relations = defineRelations({ users, posts, comments }, (r) => ({
|
|
69
|
+
users: {
|
|
70
|
+
posts: r.many.posts(), // one-to-many
|
|
71
|
+
groups: r.many.groups({ // many-to-many via junction
|
|
72
|
+
from: r.users.id.through(r.usersToGroups.userId),
|
|
73
|
+
to: r.groups.id.through(r.usersToGroups.groupId),
|
|
74
|
+
}),
|
|
75
|
+
},
|
|
76
|
+
posts: {
|
|
77
|
+
author: r.one.users({ // many-to-one
|
|
78
|
+
from: r.posts.authorId,
|
|
79
|
+
to: r.users.id,
|
|
80
|
+
}),
|
|
81
|
+
comments: r.many.comments(),
|
|
82
|
+
},
|
|
83
|
+
comments: {
|
|
84
|
+
post: r.one.posts({
|
|
85
|
+
from: r.comments.postId,
|
|
86
|
+
to: r.posts.id,
|
|
87
|
+
}),
|
|
88
|
+
},
|
|
89
|
+
}));
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Key features:
|
|
93
|
+
- `r.one` / `r.many` — declarative relation types
|
|
94
|
+
- `through()` — many-to-many, junction table hidden from query results
|
|
95
|
+
- `optional: true` — nullable relations (type-level)
|
|
96
|
+
- `alias` — disambiguate multiple relations between same tables
|
|
97
|
+
- `where` — predefined filters on target table (polymorphic relations)
|
|
98
|
+
- `defineRelationsPart()` — modules define their own relations independently, then merge
|
|
99
|
+
|
|
100
|
+
### Relational Query Builder v2
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
const result = await db.query.users.findMany({
|
|
104
|
+
columns: { id: true, name: true },
|
|
105
|
+
where: { verified: true, age: { gt: 18 } },
|
|
106
|
+
orderBy: { name: "asc" },
|
|
107
|
+
limit: 10,
|
|
108
|
+
offset: 20,
|
|
109
|
+
extras: {
|
|
110
|
+
postCount: (users) => db.$count(posts, eq(posts.authorId, users.id)),
|
|
111
|
+
},
|
|
112
|
+
with: {
|
|
113
|
+
posts: {
|
|
114
|
+
columns: { id: true, title: true },
|
|
115
|
+
where: { publishedAt: { isNotNull: true } },
|
|
116
|
+
orderBy: { publishedAt: "desc" },
|
|
117
|
+
limit: 5,
|
|
118
|
+
with: {
|
|
119
|
+
comments: {
|
|
120
|
+
limit: 3,
|
|
121
|
+
orderBy: { createdAt: "desc" },
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Compiles to a single SQL statement using lateral joins with `json_agg`:
|
|
130
|
+
|
|
131
|
+
```sql
|
|
132
|
+
SELECT ...
|
|
133
|
+
FROM "users" AS "d0"
|
|
134
|
+
LEFT JOIN LATERAL (
|
|
135
|
+
SELECT coalesce(json_agg(row_to_json("t".*)), '[]') AS "r"
|
|
136
|
+
FROM (
|
|
137
|
+
SELECT ... FROM "posts" AS "d1"
|
|
138
|
+
WHERE "d0"."id" = "d1"."author_id"
|
|
139
|
+
AND "d1"."published_at" IS NOT NULL
|
|
140
|
+
ORDER BY "d1"."published_at" DESC
|
|
141
|
+
LIMIT 5
|
|
142
|
+
) AS "t"
|
|
143
|
+
) AS "posts" ON true
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Where syntax operators: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `notIn`, `like`, `ilike`, `notLike`, `notIlike`, `isNull`, `isNotNull`, `arrayOverlaps`, `arrayContained`, `arrayContains`, `OR`, `AND`, `NOT`, `RAW`.
|
|
147
|
+
|
|
148
|
+
### Official `node:sqlite` driver
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import { drizzle } from "drizzle-orm/node-sqlite";
|
|
152
|
+
|
|
153
|
+
// Simple — just a path
|
|
154
|
+
const db = drizzle("sqlite.db");
|
|
155
|
+
|
|
156
|
+
// Advanced — pass existing DatabaseSync
|
|
157
|
+
import { DatabaseSync } from "node:sqlite";
|
|
158
|
+
const sqlite = new DatabaseSync("sqlite.db");
|
|
159
|
+
const db = drizzle({ client: sqlite });
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
No more `better-sqlite3` shim, no `aliasSelectColumns` SQL rewriting.
|
|
163
|
+
|
|
164
|
+
### Other v1 features
|
|
165
|
+
|
|
166
|
+
- **MSSQL support** — full dialect with drizzle-orm, drizzle-kit, drizzle-seed
|
|
167
|
+
- **CockroachDB support** — new dialect
|
|
168
|
+
- **Migration folders v3** — no `journal.json`, grouped folders, fewer Git conflicts
|
|
169
|
+
- **`drizzle-kit` rewrite** — DDL snapshots, ~10x faster introspection
|
|
170
|
+
- **Validator consolidation** — `drizzle-zod` → `drizzle-orm/zod`, `drizzle-typebox` → `drizzle-orm/typebox`
|
|
171
|
+
- **Alternation engine** — advanced query branching (beta)
|
|
172
|
+
- **Commutativity checks** (`drizzle-kit check`) — detect migration collisions in teams
|
|
173
|
+
- **Column `.as()` alias** — direct column aliasing
|
|
174
|
+
- **Subqueries in select fields** — computed columns inline
|
|
175
|
+
- **RLS** — moved to `pgTable.withRLS()`
|
|
176
|
+
- **Prepared statements** — work inside relational queries with `sql.placeholder()`
|
|
177
|
+
- **PostgreSQL type fixes** — arrays of intervals, timestamps, dates now map correctly
|
|
178
|
+
|
|
179
|
+
## Breaking changes to handle
|
|
180
|
+
|
|
181
|
+
| Breaking change | Alepha impact |
|
|
182
|
+
|---|---|
|
|
183
|
+
| Migration folder restructure (v3 layout) | Run `drizzle-kit up` once. Update `DrizzleKitProvider`. |
|
|
184
|
+
| Relations v1 → v2 | Alepha doesn't use Drizzle relations today — adopt v2 fresh. |
|
|
185
|
+
| PostgreSQL array/timestamp type fixes | Audit `db.createdAt()`, `db.updatedAt()` and any array columns. |
|
|
186
|
+
| Database/session/migrator gain 2 new generics | Update `DatabaseProvider` types. |
|
|
187
|
+
| `DrizzleConfig` gains `TRelations` generic + `relations` field | Pass relations to `drizzle()` constructor. |
|
|
188
|
+
| Validator packages moved into `drizzle-orm/*` | Low impact — Alepha uses TypeBox directly. |
|
|
189
|
+
| `.enableRLS()` → `pgTable.withRLS()` | Audit Alepha for RLS usage (currently none expected). |
|
|
190
|
+
|
|
191
|
+
## Migration playbook (when v1 GA ships)
|
|
192
|
+
|
|
193
|
+
### 1. Bump dependencies
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
yarn add drizzle-orm@^1.0.0
|
|
197
|
+
yarn add -D drizzle-kit@^1.0.0
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### 2. Replace the `node:sqlite` shim
|
|
201
|
+
|
|
202
|
+
Delete the `better-sqlite3` shim code in `core/providers/drivers/`:
|
|
203
|
+
- `shimDatabaseSync()` (~50 lines)
|
|
204
|
+
- `aliasSelectColumns()` (~60 lines)
|
|
205
|
+
- `initDrizzle()` manual session construction (~20 lines)
|
|
206
|
+
|
|
207
|
+
Replace with the official driver:
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
import { drizzle } from "drizzle-orm/node-sqlite";
|
|
211
|
+
this.drizzleDb = drizzle({ client: this.sqlite, relations });
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### 3. Auto-generate `defineRelations()` from `db.ref()`
|
|
215
|
+
|
|
216
|
+
The FK info already exists in `$entity` schemas. Either extend `ModelBuilder` or add a new `RelationBuilder` that walks all registered entities and emits:
|
|
217
|
+
|
|
218
|
+
```typescript
|
|
219
|
+
const relations = defineRelations(tables, (r) => ({
|
|
220
|
+
players: {
|
|
221
|
+
team: r.one.teams({
|
|
222
|
+
from: r.players.teamId,
|
|
223
|
+
to: r.teams.id,
|
|
224
|
+
}),
|
|
225
|
+
},
|
|
226
|
+
teams: {
|
|
227
|
+
players: r.many.players(),
|
|
228
|
+
},
|
|
229
|
+
}));
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
This is fully derivable from `db.ref()` declarations — each ref knows its source column and target `EntityColumn`.
|
|
233
|
+
|
|
234
|
+
### 4. Add `relationalQuery()` to `Repository`
|
|
235
|
+
|
|
236
|
+
A new method that delegates to `db.query.table.findMany()` while applying Alepha's concerns:
|
|
237
|
+
|
|
238
|
+
```typescript
|
|
239
|
+
public async relationalQuery<Config>(config: RelationalQueryConfig<T>) {
|
|
240
|
+
// Inject soft-delete filter
|
|
241
|
+
// Inject org scoping
|
|
242
|
+
// Encode values via codec
|
|
243
|
+
// Emit repository:read:before/after events
|
|
244
|
+
// Handle caching
|
|
245
|
+
return await db.query[this.tableName].findMany(config);
|
|
246
|
+
}
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### 5. Deprecate `findMany({ with })` for joins
|
|
250
|
+
|
|
251
|
+
Keep it working for backward compatibility but log a deprecation warning pointing to `relationalQuery()`. Remove in a follow-up release once consumers migrate.
|
|
252
|
+
|
|
253
|
+
### 6. Run `drizzle-kit up`
|
|
254
|
+
|
|
255
|
+
Migrate existing migration folders to the v3 layout.
|
|
256
|
+
|
|
257
|
+
### 7. Audit timestamp/array column types
|
|
258
|
+
|
|
259
|
+
PostgreSQL type fixes in v1 may change runtime values for:
|
|
260
|
+
- `db.createdAt()` / `db.updatedAt()` — timestamp columns
|
|
261
|
+
- Any array columns (intervals, timestamps, dates)
|
|
262
|
+
|
|
263
|
+
### 8. Pass relations to `drizzle()`
|
|
264
|
+
|
|
265
|
+
Update `DatabaseProvider` subclasses to pass the auto-generated relations when constructing the Drizzle instance.
|
|
266
|
+
|
|
267
|
+
## Files affected
|
|
268
|
+
|
|
269
|
+
### Delete / heavily simplify
|
|
270
|
+
|
|
271
|
+
| File | Approx. lines | Reason |
|
|
272
|
+
|---|---|---|
|
|
273
|
+
| `core/services/PgRelationManager.ts` | 131 | Replaced by Drizzle's relational query engine |
|
|
274
|
+
| `node:sqlite` driver shim | ~150 | `shimDatabaseSync()`, `aliasSelectColumns()` gone |
|
|
275
|
+
| Join types in query layer (`PgRelation`, `PgRelationMap`, `PgStatic`) | ~40 | Drizzle handles relation types natively |
|
|
276
|
+
| Relation where types (`PgQueryWhereRelations`) | ~10 | Drizzle v2 where syntax handles this |
|
|
277
|
+
|
|
278
|
+
### Modify
|
|
279
|
+
|
|
280
|
+
| File | Change |
|
|
281
|
+
|---|---|
|
|
282
|
+
| `core/services/Repository.ts` | Add `relationalQuery()`, deprecate `findMany({ with })` joins |
|
|
283
|
+
| `core/providers/RepositoryProvider.ts` / `DatabaseTypeProvider.ts` | Accept and pass `relations` to Drizzle constructor |
|
|
284
|
+
| `core/services/ModelBuilder.ts` (or new `RelationBuilder.ts`) | Generate `defineRelations()` from `db.ref()` |
|
|
285
|
+
| `core/providers/DrizzleKitProvider.ts` | Update for migration folder v3 |
|
|
286
|
+
| `postgres/` ModelBuilder | Audit timestamp/array type changes |
|
|
287
|
+
| `core/providers/drivers/` (sqlite) | Replace shim with official driver |
|
|
288
|
+
|
|
289
|
+
## Where syntax — Alepha → Drizzle v2
|
|
290
|
+
|
|
291
|
+
Alepha's current syntax maps almost 1:1 to Drizzle v2:
|
|
292
|
+
|
|
293
|
+
```
|
|
294
|
+
Alepha Drizzle v2
|
|
295
|
+
────── ──────────
|
|
296
|
+
{ age: { gt: 18 } } { age: { gt: 18 } }
|
|
297
|
+
{ age: { gte: 18, lte: 65 } } { age: { gte: 18, lte: 65 } }
|
|
298
|
+
{ name: { contains: "foo" } } { name: { ilike: "%foo%" } }
|
|
299
|
+
{ status: { inArray: [...] } } { status: { in: [...] } }
|
|
300
|
+
{ isNull: true } { isNull: true }
|
|
301
|
+
{ and: [...] } { AND: [...] }
|
|
302
|
+
{ or: [...] } { OR: [...] }
|
|
303
|
+
{ not: {...} } { NOT: {...} }
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Differences to bridge:
|
|
307
|
+
- `inArray` → `in`, `notInArray` → `notIn`
|
|
308
|
+
- `and` / `or` / `not` → `AND` / `OR` / `NOT` (capitalized)
|
|
309
|
+
- `contains` / `startsWith` / `endsWith` — Alepha sugar with no direct Drizzle equivalent (expand to `ilike`)
|
|
310
|
+
- Drizzle adds `RAW: (table) => sql\`...\`` for inline raw SQL inside `where`
|
|
311
|
+
|
|
312
|
+
Alepha's JSON DSL stays the public API; the bridge to Drizzle v2 lives inside `Repository` / `QueryManager`.
|
|
313
|
+
|
|
314
|
+
## Why not now
|
|
315
|
+
|
|
316
|
+
- **Beta risk** — Drizzle v1 is still in beta. Pinning Alepha to a beta would couple every Alepha release to upstream's stabilization timeline.
|
|
317
|
+
- **Bigbang nature** — the upgrade is not incremental (Relations v2, migration folder layout, type generics, driver swap all land together). Better executed in one focused window post-GA than dripped over months.
|
|
318
|
+
- **No urgent user pain** — current Repository covers ~95% of usage. The blocked features (one-to-many joins, lateral joins, CTEs) are real gaps but have manual workarounds today.
|
|
319
|
+
|
|
320
|
+
## Estimated effort when triggered
|
|
321
|
+
|
|
322
|
+
Multi-day, not single-day:
|
|
323
|
+
- Replace `node:sqlite` shim — ~half day
|
|
324
|
+
- Auto-generate `defineRelations()` from entity refs — 1 day
|
|
325
|
+
- Add `relationalQuery()` with all Repository concerns wired through — 1 day
|
|
326
|
+
- Migration folder v3 + `drizzle-kit` provider update — half day
|
|
327
|
+
- PG type audit + test suite stabilization — 1 day
|
|
328
|
+
- Total: ~4 days of focused work, plus regression hunt against the existing test suite.
|
|
329
|
+
|
|
330
|
+
The framework's `~3400` ORM-touching tests are the safety net. If they pass, the upgrade landed cleanly.
|
|
@@ -31,6 +31,17 @@ export interface TransactionalOptions {
|
|
|
31
31
|
* }
|
|
32
32
|
* ```
|
|
33
33
|
*/
|
|
34
|
+
// TODO: support savepoints for nested transactions.
|
|
35
|
+
// Today, a nested $transactional reuses the outer tx (correct for ACID), but
|
|
36
|
+
// there is no way to roll back ONLY the inner block while preserving the
|
|
37
|
+
// outer one. Drizzle exposes savepoints (`tx.transaction(...)` inside an
|
|
38
|
+
// existing tx maps to `SAVEPOINT` on Postgres). Plan:
|
|
39
|
+
// 1. Detect "already inside a tx" in DatabaseProvider.transactional().
|
|
40
|
+
// 2. When nested, open a savepoint instead of reusing the outer scope.
|
|
41
|
+
// 3. On inner throw, ROLLBACK TO SAVEPOINT (don't bubble unless re-thrown).
|
|
42
|
+
// 4. Add an option { savepoint?: boolean } on $transactional (default true
|
|
43
|
+
// when nested) so users can opt out for "inherit-and-bubble" semantics.
|
|
44
|
+
// Needs care around connection-bound state and Drizzle's tx proxy.
|
|
34
45
|
export const $transactional = (options?: TransactionalOptions): Middleware => {
|
|
35
46
|
return createMiddleware({
|
|
36
47
|
name: "$transactional",
|
|
@@ -40,7 +40,7 @@ export const updateSchema = <T extends TObject>(
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
if (t.schema.isOptional(prop)) {
|
|
43
|
-
newProperties[key] = t.optional(t.union([prop, t.
|
|
43
|
+
newProperties[key] = t.optional(t.union([prop, t.null()]));
|
|
44
44
|
} else {
|
|
45
45
|
newProperties[key] = prop;
|
|
46
46
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type TObject, t
|
|
1
|
+
import { $inject, SchemaValidator, type TObject, t } from "alepha";
|
|
2
2
|
import { getTableName, type SQL, sql } from "drizzle-orm";
|
|
3
3
|
import type { PgSelectBase, PgTableWithColumns } from "drizzle-orm/pg-core";
|
|
4
4
|
import { isSQLWrapper } from "drizzle-orm/sql/sql";
|
|
@@ -8,6 +8,8 @@ import type { DatabaseProvider } from "../providers/drivers/DatabaseProvider.ts"
|
|
|
8
8
|
import type { PgJoin } from "./QueryManager.ts";
|
|
9
9
|
|
|
10
10
|
export class PgRelationManager {
|
|
11
|
+
protected readonly schemaValidator = $inject(SchemaValidator);
|
|
12
|
+
|
|
11
13
|
/**
|
|
12
14
|
* Recursively build joins for the query builder based on the relations map
|
|
13
15
|
*/
|
|
@@ -103,7 +105,7 @@ export class PgRelationManager {
|
|
|
103
105
|
joins: PgJoin[],
|
|
104
106
|
parentPath?: string,
|
|
105
107
|
): TObject {
|
|
106
|
-
const schema =
|
|
108
|
+
const schema = this.schemaValidator.clone(baseSchema) as TObject;
|
|
107
109
|
|
|
108
110
|
// Group joins by parent
|
|
109
111
|
const joinsAtThisLevel = joins.filter((j) => j.parent === parentPath);
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { act, renderHook, waitFor } from "@testing-library/react";
|
|
2
|
+
import { Alepha } from "alepha";
|
|
3
|
+
import { AlephaDateTime } from "alepha/datetime";
|
|
4
|
+
import { describe, test, vi } from "vitest";
|
|
5
|
+
import { AlephaContext } from "../contexts/AlephaContext.ts";
|
|
6
|
+
import { useQuery } from "../hooks/useQuery.ts";
|
|
7
|
+
|
|
8
|
+
describe("useQuery", () => {
|
|
9
|
+
test("runs on mount and exposes data/loading/error/refetch", async ({
|
|
10
|
+
expect,
|
|
11
|
+
}) => {
|
|
12
|
+
const alepha = Alepha.create().with(AlephaDateTime);
|
|
13
|
+
await alepha.start();
|
|
14
|
+
|
|
15
|
+
const handler = vi.fn(async () => ({ users: ["a", "b"] }));
|
|
16
|
+
|
|
17
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
18
|
+
<AlephaContext.Provider value={alepha}>{children}</AlephaContext.Provider>
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
const { result } = renderHook(() => useQuery({ handler }, []), { wrapper });
|
|
22
|
+
|
|
23
|
+
expect(result.current.data).toBe(undefined);
|
|
24
|
+
expect(typeof result.current.refetch).toBe("function");
|
|
25
|
+
|
|
26
|
+
await waitFor(() => {
|
|
27
|
+
expect(result.current.data).toEqual({ users: ["a", "b"] });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(handler).toHaveBeenCalledTimes(1);
|
|
31
|
+
expect(result.current.loading).toBe(false);
|
|
32
|
+
expect(result.current.error).toBe(undefined);
|
|
33
|
+
|
|
34
|
+
// refetch
|
|
35
|
+
handler.mockResolvedValueOnce({ users: ["c"] });
|
|
36
|
+
await act(async () => {
|
|
37
|
+
await result.current.refetch();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(result.current.data).toEqual({ users: ["c"] });
|
|
41
|
+
expect(handler).toHaveBeenCalledTimes(2);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("does not run when enabled is false", async ({ expect }) => {
|
|
45
|
+
const alepha = Alepha.create().with(AlephaDateTime);
|
|
46
|
+
await alepha.start();
|
|
47
|
+
|
|
48
|
+
const handler = vi.fn(async () => "value");
|
|
49
|
+
|
|
50
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
51
|
+
<AlephaContext.Provider value={alepha}>{children}</AlephaContext.Provider>
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const { result } = renderHook(
|
|
55
|
+
() => useQuery({ handler, enabled: false }, []),
|
|
56
|
+
{ wrapper },
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// give it a tick to ensure no spontaneous run
|
|
60
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
61
|
+
expect(handler).not.toHaveBeenCalled();
|
|
62
|
+
expect(result.current.data).toBe(undefined);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("supports initialData", async ({ expect }) => {
|
|
66
|
+
const alepha = Alepha.create().with(AlephaDateTime);
|
|
67
|
+
await alepha.start();
|
|
68
|
+
|
|
69
|
+
const handler = vi.fn(async () => "fetched");
|
|
70
|
+
|
|
71
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
72
|
+
<AlephaContext.Provider value={alepha}>{children}</AlephaContext.Provider>
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const { result } = renderHook(
|
|
76
|
+
() => useQuery({ handler, initialData: "seed" }, []),
|
|
77
|
+
{ wrapper },
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
expect(result.current.data).toBe("seed");
|
|
81
|
+
|
|
82
|
+
await waitFor(() => {
|
|
83
|
+
expect(result.current.data).toBe("fetched");
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -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
|
+
}
|
package/src/react/core/index.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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) {
|