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.
- package/README.md +0 -1
- package/assets/swagger-ui/swagger-ui-bundle.js +1 -1
- package/assets/swagger-ui/swagger-ui.css +1 -1
- package/dist/api/audits/index.browser.js +49 -0
- package/dist/api/audits/index.browser.js.map +1 -1
- package/dist/api/audits/index.js +49 -0
- package/dist/api/audits/index.js.map +1 -1
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.d.ts +2 -61
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/keys/index.d.ts +4 -4
- package/dist/api/keys/index.js.map +1 -1
- package/dist/api/notifications/index.d.ts +1 -10
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/parameters/index.browser.js +37 -0
- package/dist/api/parameters/index.browser.js.map +1 -1
- package/dist/api/parameters/index.d.ts +12 -68
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +57 -4
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/api/payments/index.js.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 +148 -227
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +60 -14
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/api/verifications/index.js +2 -1
- package/dist/api/verifications/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 +153 -5
- package/dist/bucket/index.js.map +1 -1
- package/dist/bucket/index.workerd.js +12 -2
- 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/captcha/index.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 +637 -11660
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +707 -532
- 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 +20 -16
- package/dist/cli/devtools/index.js.map +1 -1
- package/dist/cli/platform/index.d.ts +51 -77
- package/dist/cli/platform/index.d.ts.map +1 -1
- package/dist/cli/platform/index.js +65 -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 +30 -12
- package/dist/cli/vendor/index.js.map +1 -1
- package/dist/command/index.js +1 -1
- package/dist/command/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 +8 -11
- 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/crypto/index.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/core/index.js.map +1 -1
- package/dist/email/smtp/index.js +130 -16
- package/dist/email/smtp/index.js.map +1 -1
- 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/lock/redis/index.js.map +1 -1
- package/dist/logger/index.js +32 -1
- package/dist/logger/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 -67
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/core/index.browser.js +2 -362
- package/dist/orm/core/index.browser.js.map +1 -1
- package/dist/orm/core/index.bun.js +18 -409
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.d.ts +41 -194
- package/dist/orm/core/index.d.ts.map +1 -1
- package/dist/orm/core/index.js +27 -422
- package/dist/orm/core/index.js.map +1 -1
- package/dist/orm/postgres/index.bun.js +17 -20
- package/dist/orm/postgres/index.bun.js.map +1 -1
- package/dist/orm/postgres/index.d.ts +1 -5
- package/dist/orm/postgres/index.d.ts.map +1 -1
- package/dist/orm/postgres/index.js +17 -20
- package/dist/orm/postgres/index.js.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/intro/index.js +22 -17
- package/dist/react/intro/index.js.map +1 -1
- package/dist/react/router/index.browser.js +98 -4
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +58 -5
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js +122 -6
- package/dist/react/router/index.js.map +1 -1
- package/dist/react/testing/{chunk-DBEY4PJZ.js → chunk-6Ep1yQYe.js} +1 -1
- package/dist/react/testing/index.js +1 -1
- 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 +64 -1
- package/dist/react/ui/index.js.map +1 -1
- package/dist/react/websocket/index.js.map +1 -1
- package/dist/redis/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +1 -2
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +1 -1
- package/dist/scheduler/index.js.map +1 -1
- package/dist/scheduler/index.workerd.js +1 -1
- package/dist/scheduler/index.workerd.js.map +1 -1
- package/dist/security/index.browser.js.map +1 -1
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +2 -2
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +24 -10
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/cookies/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 +1 -4
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js +47 -9
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/links/index.browser.js.map +1 -1
- package/dist/server/links/index.js.map +1 -1
- package/dist/server/metrics/index.js +19 -1
- package/dist/server/metrics/index.js.map +1 -1
- package/dist/server/rate-limit/index.js.map +1 -1
- package/dist/server/static/index.js.map +1 -1
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/server/swagger/index.js +4 -5
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/sms/index.js.map +1 -1
- package/dist/system/index.browser.js.map +1 -1
- package/dist/system/index.js.map +1 -1
- package/dist/system/index.workerd.js.map +1 -1
- package/dist/topic/core/index.js.map +1 -1
- package/dist/websocket/index.browser.js +32 -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 +42 -6
- package/dist/websocket/index.js.map +1 -1
- package/package.json +685 -274
- package/src/api/files/__tests__/FileController.spec.ts +1 -1
- package/src/api/jobs/__tests__/$job.spec.ts +5 -1
- 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/schemas/userQuerySchema.ts +0 -1
- package/src/api/users/services/SessionService.ts +27 -0
- package/src/api/users/services/UserService.ts +1 -5
- package/src/api/verifications/__tests__/CodeVerification.spec.ts +14 -0
- package/src/api/verifications/__tests__/LinkVerification.spec.ts +14 -0
- package/src/api/verifications/services/VerificationService.ts +1 -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 +214 -7
- package/src/cli/core/commands/init.ts +12 -0
- package/src/cli/core/services/PackageManagerUtils.ts +23 -6
- package/src/cli/core/services/ProjectScaffolder.ts +315 -33
- package/src/cli/core/tasks/BuildCloudflareTask.ts +5 -0
- package/src/cli/core/tasks/BuildDockerTask.ts +9 -10
- package/src/cli/core/tasks/BuildServerTask.ts +8 -0
- package/src/cli/core/templates/agentMd.ts +2 -10
- package/src/cli/core/templates/apiIndexTs.ts +23 -1
- package/src/cli/core/templates/componentsJsonTs.ts +39 -0
- package/src/cli/core/templates/mainCss.ts +1 -0
- package/src/cli/core/templates/saasAdminLayoutTsx.ts +77 -0
- package/src/cli/core/templates/saasAdminPagesTsx.ts +26 -0
- package/src/cli/core/templates/saasAuthLayoutTsx.ts +20 -0
- package/src/cli/core/templates/saasAuthPagesTsx.ts +62 -0
- package/src/cli/core/templates/saasRealmProviderTs.ts +46 -0
- package/src/cli/core/templates/webAppRouterTs.ts +104 -1
- package/src/cli/core/templates/webIndexTs.ts +23 -1
- package/src/cli/devtools/index.ts +12 -26
- package/src/cli/platform/__tests__/SecretsCommand.spec.ts +2 -0
- 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/command/providers/CliProvider.ts +1 -1
- 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/interfaces/Service.ts +3 -1
- package/src/core/providers/SchemaValidator.ts +9 -1
- package/src/core/providers/TypeProvider.ts +2 -3
- 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/logger/services/Logger.ts +1 -1
- package/src/mcp/__tests__/$resource.spec.ts +1 -1
- package/src/mcp/__tests__/$tool.spec.ts +1 -1
- package/src/mcp/__tests__/McpServerProvider.spec.ts +1 -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/__tests__/$repository-tests.ts +1 -0
- package/src/orm/__tests__/orm-next-tests.ts +2 -67
- package/src/orm/__tests__/orm-next.spec.ts +0 -21
- package/src/orm/core/index.shared.ts +0 -2
- package/src/orm/core/index.ts +1 -2
- package/src/orm/core/primitives/$repository.ts +3 -6
- package/src/orm/core/primitives/$transactional.ts +11 -0
- package/src/orm/core/providers/drivers/DatabaseProvider.ts +0 -5
- package/src/orm/core/providers/drivers/NodeSqliteProvider.ts +11 -13
- package/src/orm/core/schemas/updateSchema.ts +1 -1
- package/src/orm/core/services/ModelBuilder.ts +1 -13
- package/src/orm/core/services/PgRelationManager.ts +4 -2
- package/src/orm/core/services/Repository.ts +1 -42
- package/src/orm/core/services/SqliteModelBuilder.ts +2 -33
- package/src/orm/postgres/services/PostgresModelBuilder.ts +10 -45
- 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/intro/components/GettingStartedAuthSlide.tsx +11 -4
- package/src/react/router/__tests__/$page.spec.tsx +0 -16
- package/src/react/router/__tests__/ReactBrowserProvider.browser.spec.ts +213 -2
- package/src/react/router/__tests__/ssr.spec.tsx +339 -0
- package/src/react/router/primitives/$page.ts +28 -4
- package/src/react/router/providers/ReactBrowserProvider.ts +73 -0
- package/src/react/router/providers/ReactBrowserRouterProvider.ts +1 -1
- package/src/react/router/providers/ReactPageProvider.ts +27 -9
- package/src/react/router/providers/ReactPreloadProvider.ts +1 -1
- package/src/react/router/providers/ReactServerProvider.ts +1 -0
- 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/scheduler/providers/CronProvider.ts +1 -1
- package/src/security/primitives/$basicAuth.ts +1 -1
- package/src/security/primitives/$issuer.ts +6 -3
- package/src/server/auth/providers/ServerAuthProvider.ts +5 -1
- 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/interfaces/ServerRequest.ts +1 -0
- package/src/server/core/primitives/$action.ts +16 -5
- package/src/server/core/providers/ServerProvider.ts +1 -1
- package/src/server/core/providers/ServerRouterProvider.ts +28 -6
- package/src/server/core/services/HttpClient.ts +1 -1
- package/src/server/swagger/providers/ServerSwaggerProvider.ts +6 -8
- package/src/websocket/providers/NodeWebSocketServerProvider.ts +10 -4
- package/src/websocket/services/WebSocketClient.ts +11 -5
- package/src/mcp/transports/SseMcpTransport.ts +0 -182
- package/src/orm/core/__tests__/parseQueryString.spec.ts +0 -196
- package/src/orm/core/helpers/parseQueryString.ts +0 -502
- package/src/orm/core/primitives/$view.ts +0 -88
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { $atom, $inject, $state, t } from "alepha";
|
|
2
|
+
import { $logger } from "alepha/logger";
|
|
3
|
+
import { $route } from "alepha/server";
|
|
4
|
+
import {
|
|
5
|
+
createErrorResponse,
|
|
6
|
+
createParseError,
|
|
7
|
+
JsonRpcParseError,
|
|
8
|
+
parseMessage,
|
|
9
|
+
} from "../helpers/jsonrpc.ts";
|
|
10
|
+
import type { McpContext } from "../interfaces/McpTypes.ts";
|
|
11
|
+
import { McpServerProvider } from "../providers/McpServerProvider.ts";
|
|
12
|
+
|
|
13
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
export const mcpStreamableHttpOptions = $atom({
|
|
16
|
+
name: "alepha.mcp.streamableHttp.options",
|
|
17
|
+
description: "Configuration options for the MCP Streamable HTTP transport.",
|
|
18
|
+
schema: t.object({
|
|
19
|
+
/**
|
|
20
|
+
* Path for the MCP endpoint. Single endpoint for both requests and
|
|
21
|
+
* (optional) server-streamed responses, per spec 2025-03-26+.
|
|
22
|
+
*/
|
|
23
|
+
path: t.text({ default: "/mcp" }),
|
|
24
|
+
/**
|
|
25
|
+
* Allow-list of `Origin` header values accepted on incoming requests.
|
|
26
|
+
* Empty array (default) means "allow any". When set, browser-originated
|
|
27
|
+
* requests with a non-matching `Origin` are rejected with 403 Forbidden,
|
|
28
|
+
* blocking DNS-rebinding attacks against localhost MCP servers.
|
|
29
|
+
*
|
|
30
|
+
* Server-to-server callers (no `Origin` header) are always allowed.
|
|
31
|
+
*
|
|
32
|
+
* Spec 2025-11-25, PR #1439.
|
|
33
|
+
*/
|
|
34
|
+
allowedOrigins: t.array(t.text(), { default: [] }),
|
|
35
|
+
}),
|
|
36
|
+
default: {
|
|
37
|
+
path: "/mcp",
|
|
38
|
+
allowedOrigins: [],
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Backward-compat alias for the legacy atom name. Prefer
|
|
43
|
+
// `mcpStreamableHttpOptions` going forward; this re-export keeps existing
|
|
44
|
+
// consumer imports compiling and will be removed once they migrate.
|
|
45
|
+
export const mcpSseOptions = mcpStreamableHttpOptions;
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Streamable HTTP transport for MCP communication.
|
|
51
|
+
*
|
|
52
|
+
* Implements the 2025-03-26+ Streamable HTTP transport: a single `/mcp`
|
|
53
|
+
* endpoint that accepts JSON-RPC over POST and returns either
|
|
54
|
+
* `application/json` (single response, the default) or
|
|
55
|
+
* `text/event-stream` (when the server wants to stream multiple messages).
|
|
56
|
+
*
|
|
57
|
+
* Designed for serverless deployment (Cloudflare Workers, etc.) — there is
|
|
58
|
+
* no long-lived GET stream. GET on the endpoint returns 405 Method Not
|
|
59
|
+
* Allowed; clients that want server-initiated push must rely on the POST
|
|
60
|
+
* response stream when the server upgrades to SSE for that particular call.
|
|
61
|
+
*
|
|
62
|
+
* Spec compliance:
|
|
63
|
+
* - 2025-06-18: validates `MCP-Protocol-Version` header on every request
|
|
64
|
+
* after `initialize` against the version negotiated and stored on
|
|
65
|
+
* `McpServerProvider`.
|
|
66
|
+
* - 2025-11-25: rejects requests with a non-allow-listed `Origin` header
|
|
67
|
+
* (PR #1439). See {@link mcpStreamableHttpOptions.allowedOrigins}.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* ```ts
|
|
71
|
+
* import { Alepha, run } from "alepha";
|
|
72
|
+
* import { AlephaServer } from "alepha/server";
|
|
73
|
+
* import { AlephaMcp, StreamableHttpMcpTransport } from "alepha/mcp";
|
|
74
|
+
*
|
|
75
|
+
* class MyTools {
|
|
76
|
+
* // ... tool definitions
|
|
77
|
+
* }
|
|
78
|
+
*
|
|
79
|
+
* run(
|
|
80
|
+
* Alepha.create()
|
|
81
|
+
* .with(AlephaServer)
|
|
82
|
+
* .with(AlephaMcp)
|
|
83
|
+
* .with(StreamableHttpMcpTransport)
|
|
84
|
+
* .with(MyTools)
|
|
85
|
+
* );
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export class StreamableHttpMcpTransport {
|
|
89
|
+
protected readonly log = $logger();
|
|
90
|
+
protected readonly options = $state(mcpStreamableHttpOptions);
|
|
91
|
+
protected readonly mcpServer = $inject(McpServerProvider);
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* GET on the MCP endpoint is not supported in this transport. Returning
|
|
95
|
+
* 405 (rather than serving the legacy two-endpoint SSE pattern) is the
|
|
96
|
+
* spec-allowed response for servers that don't offer server-initiated
|
|
97
|
+
* push outside of an active POST.
|
|
98
|
+
*/
|
|
99
|
+
notAllowed = $route({
|
|
100
|
+
method: "GET",
|
|
101
|
+
path: this.options.path,
|
|
102
|
+
handler: (request) => {
|
|
103
|
+
request.reply.status = 405;
|
|
104
|
+
request.reply.headers.allow = "POST";
|
|
105
|
+
request.reply.headers["content-type"] = "application/json";
|
|
106
|
+
request.reply.body = JSON.stringify({
|
|
107
|
+
error: "Method Not Allowed. Use POST for MCP messages.",
|
|
108
|
+
});
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* POST endpoint for client-to-server JSON-RPC messages.
|
|
114
|
+
* Returns `application/json` for single responses; tools that need to
|
|
115
|
+
* stream progress would upgrade to `text/event-stream` (deferred until a
|
|
116
|
+
* concrete need exists).
|
|
117
|
+
*/
|
|
118
|
+
message = $route({
|
|
119
|
+
method: "POST",
|
|
120
|
+
path: this.options.path,
|
|
121
|
+
schema: {
|
|
122
|
+
body: t.json(),
|
|
123
|
+
},
|
|
124
|
+
handler: async (request) => {
|
|
125
|
+
try {
|
|
126
|
+
// Origin allow-list check (spec 2025-11-25 / PR #1439).
|
|
127
|
+
const originRaw = request.headers.origin;
|
|
128
|
+
const origin = Array.isArray(originRaw) ? originRaw[0] : originRaw;
|
|
129
|
+
if (
|
|
130
|
+
origin &&
|
|
131
|
+
this.options.allowedOrigins.length > 0 &&
|
|
132
|
+
!this.options.allowedOrigins.includes(origin)
|
|
133
|
+
) {
|
|
134
|
+
this.log.warn("Rejected MCP request with non-allowed Origin", {
|
|
135
|
+
origin,
|
|
136
|
+
allowed: this.options.allowedOrigins,
|
|
137
|
+
});
|
|
138
|
+
request.reply.status = 403;
|
|
139
|
+
request.reply.headers["content-type"] = "application/json";
|
|
140
|
+
request.reply.body = JSON.stringify({
|
|
141
|
+
error: "Forbidden: Origin not allowed",
|
|
142
|
+
});
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const body =
|
|
147
|
+
typeof request.body === "string"
|
|
148
|
+
? request.body
|
|
149
|
+
: JSON.stringify(request.body);
|
|
150
|
+
|
|
151
|
+
this.log.debug("MCP request body", {
|
|
152
|
+
body,
|
|
153
|
+
bodyType: typeof request.body,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const rpcRequest = parseMessage(body);
|
|
157
|
+
|
|
158
|
+
// Build context from request headers
|
|
159
|
+
const headers = { ...request.headers } as Record<
|
|
160
|
+
string,
|
|
161
|
+
string | string[] | undefined
|
|
162
|
+
>;
|
|
163
|
+
|
|
164
|
+
// Spec 2025-06-18+: every HTTP request after `initialize` MUST carry
|
|
165
|
+
// an `MCP-Protocol-Version` header matching the negotiated version.
|
|
166
|
+
// Reject mismatches with 400 so the client doesn't silently drift.
|
|
167
|
+
if (rpcRequest.method !== "initialize") {
|
|
168
|
+
const headerRaw = headers["mcp-protocol-version"];
|
|
169
|
+
const headerVersion = Array.isArray(headerRaw)
|
|
170
|
+
? headerRaw[0]
|
|
171
|
+
: headerRaw;
|
|
172
|
+
if (
|
|
173
|
+
headerVersion &&
|
|
174
|
+
headerVersion !== this.mcpServer.negotiatedVersion
|
|
175
|
+
) {
|
|
176
|
+
this.log.warn("MCP-Protocol-Version header mismatch", {
|
|
177
|
+
header: headerVersion,
|
|
178
|
+
negotiated: this.mcpServer.negotiatedVersion,
|
|
179
|
+
});
|
|
180
|
+
request.reply.status = 400;
|
|
181
|
+
request.reply.headers["content-type"] = "application/json";
|
|
182
|
+
request.reply.body = JSON.stringify({
|
|
183
|
+
error: `MCP-Protocol-Version mismatch: expected ${this.mcpServer.negotiatedVersion}, got ${headerVersion}`,
|
|
184
|
+
});
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const context: McpContext = { headers };
|
|
190
|
+
|
|
191
|
+
const response = await this.mcpServer.handleMessage(
|
|
192
|
+
rpcRequest,
|
|
193
|
+
context,
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
if (response) {
|
|
197
|
+
request.reply.headers["content-type"] = "application/json";
|
|
198
|
+
request.reply.body = JSON.stringify(response);
|
|
199
|
+
} else {
|
|
200
|
+
request.reply.status = 204;
|
|
201
|
+
}
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (error instanceof JsonRpcParseError) {
|
|
204
|
+
request.reply.status = 400;
|
|
205
|
+
request.reply.headers["content-type"] = "application/json";
|
|
206
|
+
request.reply.body = JSON.stringify(
|
|
207
|
+
createErrorResponse(0, createParseError(error.message)),
|
|
208
|
+
);
|
|
209
|
+
} else {
|
|
210
|
+
this.log.error("Failed to process MCP message", error);
|
|
211
|
+
request.reply.status = 500;
|
|
212
|
+
request.reply.body = JSON.stringify({
|
|
213
|
+
error: (error as Error).message,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* @deprecated Use {@link StreamableHttpMcpTransport}. The 2024-11-05
|
|
223
|
+
* two-endpoint HTTP+SSE pattern was replaced by Streamable HTTP in spec
|
|
224
|
+
* 2025-03-26. This alias is preserved for one release to ease migration.
|
|
225
|
+
*/
|
|
226
|
+
export const SseMcpTransport = StreamableHttpMcpTransport;
|
|
@@ -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.
|
|
@@ -1014,6 +1014,7 @@ export const testTransactionThrowsWhenUnsupported = async (alepha: Alepha) => {
|
|
|
1014
1014
|
original,
|
|
1015
1015
|
);
|
|
1016
1016
|
} else {
|
|
1017
|
+
// biome-ignore lint/performance/noDelete: setting to undefined fails because the prototype has a getter
|
|
1017
1018
|
delete provider.supportsTransactions;
|
|
1018
1019
|
}
|
|
1019
1020
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { type Alepha,
|
|
1
|
+
import { type Alepha, t } from "alepha";
|
|
2
2
|
import { sql } from "drizzle-orm";
|
|
3
3
|
import { expect } from "vitest";
|
|
4
4
|
import { PG_GENERATED } from "../core/constants/PG_SYMBOLS.ts";
|
|
5
|
-
import { $entity, $repository,
|
|
5
|
+
import { $entity, $repository, db, pgAttr } from "../core/index.ts";
|
|
6
6
|
|
|
7
7
|
// ============================================================================
|
|
8
8
|
// Shared entity definitions
|
|
@@ -425,68 +425,3 @@ export const testQueryCacheCustomKey = async (alepha: Alepha) => {
|
|
|
425
425
|
);
|
|
426
426
|
expect(cached).toHaveLength(1);
|
|
427
427
|
};
|
|
428
|
-
|
|
429
|
-
// ============================================================================
|
|
430
|
-
// Feature 8: Database Views
|
|
431
|
-
// ============================================================================
|
|
432
|
-
|
|
433
|
-
export const testViewReadOnly = async (alepha: Alepha) => {
|
|
434
|
-
// Create the underlying table
|
|
435
|
-
const itemEntity = $entity({
|
|
436
|
-
name: "view_items",
|
|
437
|
-
schema: t.object({
|
|
438
|
-
id: db.primaryKey(),
|
|
439
|
-
name: t.text(),
|
|
440
|
-
price: t.number(),
|
|
441
|
-
}),
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
// Create a view
|
|
445
|
-
const itemView = $view({
|
|
446
|
-
name: "view_items_summary",
|
|
447
|
-
schema: t.object({
|
|
448
|
-
id: t.integer(),
|
|
449
|
-
name: t.text(),
|
|
450
|
-
price: t.number(),
|
|
451
|
-
}),
|
|
452
|
-
query: sql`SELECT id, name, price FROM view_items`,
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
class App {
|
|
456
|
-
items = $repository(itemEntity);
|
|
457
|
-
summary = $repository(itemView);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
const app = alepha.inject(App);
|
|
461
|
-
await alepha.start();
|
|
462
|
-
|
|
463
|
-
// Verify the repository detects it's a view
|
|
464
|
-
expect(app.summary.isReadOnly).toBe(true);
|
|
465
|
-
expect(app.items.isReadOnly).toBe(false);
|
|
466
|
-
|
|
467
|
-
// Write operations should throw on views
|
|
468
|
-
await expect(
|
|
469
|
-
app.summary.create({ id: 1, name: "test", price: 10 } as any),
|
|
470
|
-
).rejects.toThrow(AlephaError);
|
|
471
|
-
};
|
|
472
|
-
|
|
473
|
-
export const testViewRefreshThrowsForNonMaterialized = async (
|
|
474
|
-
alepha: Alepha,
|
|
475
|
-
) => {
|
|
476
|
-
const view = $view({
|
|
477
|
-
name: "non_mat_view",
|
|
478
|
-
schema: t.object({
|
|
479
|
-
id: t.integer(),
|
|
480
|
-
}),
|
|
481
|
-
query: sql`SELECT 1 as id`,
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
class App {
|
|
485
|
-
repo = $repository(view);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
const app = alepha.inject(App);
|
|
489
|
-
await alepha.start();
|
|
490
|
-
|
|
491
|
-
await expect(app.repo.refresh()).rejects.toThrow(AlephaError);
|
|
492
|
-
};
|
|
@@ -14,8 +14,6 @@ import {
|
|
|
14
14
|
testPartialIndex,
|
|
15
15
|
testQueryCache,
|
|
16
16
|
testQueryCacheCustomKey,
|
|
17
|
-
testViewReadOnly,
|
|
18
|
-
testViewRefreshThrowsForNonMaterialized,
|
|
19
17
|
} from "./orm-next-tests.ts";
|
|
20
18
|
|
|
21
19
|
const sqlite = () =>
|
|
@@ -119,22 +117,3 @@ describe("query caching", () => {
|
|
|
119
117
|
await testQueryCacheCustomKey(postgres());
|
|
120
118
|
});
|
|
121
119
|
});
|
|
122
|
-
|
|
123
|
-
// =============================================================================
|
|
124
|
-
// Feature 8: Database Views
|
|
125
|
-
// =============================================================================
|
|
126
|
-
|
|
127
|
-
describe("database views", () => {
|
|
128
|
-
it("should block writes on view repositories (sqlite)", async () => {
|
|
129
|
-
await testViewReadOnly(sqlite());
|
|
130
|
-
});
|
|
131
|
-
it("should block writes on view repositories (postgres)", async () => {
|
|
132
|
-
await testViewReadOnly(postgres());
|
|
133
|
-
});
|
|
134
|
-
it("should throw on refresh for non-materialized view (sqlite)", async () => {
|
|
135
|
-
await testViewRefreshThrowsForNonMaterialized(sqlite());
|
|
136
|
-
});
|
|
137
|
-
it("should throw on refresh for non-materialized view (postgres)", async () => {
|
|
138
|
-
await testViewRefreshThrowsForNonMaterialized(postgres());
|
|
139
|
-
});
|
|
140
|
-
});
|
|
@@ -12,13 +12,11 @@ export * from "./errors/DbEntityNotFoundError.ts";
|
|
|
12
12
|
export * from "./errors/DbForeignKeyError.ts";
|
|
13
13
|
export * from "./errors/DbNotNullError.ts";
|
|
14
14
|
export * from "./errors/DbTableNotFoundError.ts";
|
|
15
|
-
export * from "./helpers/parseQueryString.ts";
|
|
16
15
|
export * from "./helpers/pgAttr.ts";
|
|
17
16
|
export * from "./interfaces/AggregateQuery.ts";
|
|
18
17
|
export * from "./interfaces/FilterOperators.ts";
|
|
19
18
|
export * from "./interfaces/PgQuery.ts";
|
|
20
19
|
export * from "./interfaces/PgQueryWhere.ts";
|
|
21
20
|
export * from "./primitives/$entity.ts";
|
|
22
|
-
export * from "./primitives/$view.ts";
|
|
23
21
|
export * from "./providers/DatabaseTypeProvider.ts";
|
|
24
22
|
export * from "./schemas/legacyIdSchema.ts";
|