t3code-cli 0.3.0 → 0.4.0
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 +1 -1
- package/dist/bin.js +412 -87
- package/dist/index.js +1 -1
- package/dist/{runtime-CMPZpQaG.js → runtime-Cq64iuZr.js} +4768 -2040
- package/dist/src/application/layer.d.ts +3 -3
- package/dist/src/application/models.d.ts +1 -1
- package/dist/src/application/projects.d.ts +1 -1
- package/dist/src/application/threads.d.ts +1 -1
- package/dist/src/auth/error.d.ts +20 -1
- package/dist/src/auth/layer.d.ts +9 -22
- package/dist/src/auth/local.d.ts +23 -15
- package/dist/src/auth/pairing.d.ts +20 -2
- package/dist/src/auth/schema.d.ts +25 -34
- package/dist/src/auth/service.d.ts +2 -2
- package/dist/src/auth/transport.d.ts +13 -20
- package/dist/src/auth/type.d.ts +0 -1
- package/dist/src/domain/model-config.d.ts +3 -3
- package/dist/src/orchestration/layer.d.ts +3 -3
- package/dist/src/rpc/layer.d.ts +6 -6
- package/dist/src/rpc/ws-group.d.ts +3 -3
- package/dist/src/runtime.d.ts +2 -2
- package/dist/src/sql/node-sqlite-client.d.ts +10 -0
- package/dist/src/sql/service.d.ts +17 -0
- package/dist/upstream-t3code/packages/contracts/src/auth.d.ts +14 -12
- package/dist/upstream-t3code/packages/contracts/src/environmentHttp.d.ts +167 -7
- package/dist/upstream-t3code/packages/contracts/src/index.d.ts +1 -0
- package/dist/upstream-t3code/packages/contracts/src/ipc.d.ts +24 -0
- package/dist/upstream-t3code/packages/contracts/src/providerRuntime.d.ts +90 -0
- package/dist/upstream-t3code/packages/contracts/src/relay.d.ts +1262 -0
- package/dist/upstream-t3code/packages/contracts/src/relayClient.d.ts +48 -0
- package/dist/upstream-t3code/packages/contracts/src/rpc.d.ts +78 -9
- package/dist/upstream-t3code/packages/contracts/src/server.d.ts +3 -3
- package/package.json +3 -3
- package/src/auth/error.ts +33 -1
- package/src/auth/layer.ts +11 -76
- package/src/auth/local.ts +342 -208
- package/src/auth/pairing.ts +44 -2
- package/src/auth/schema.ts +21 -28
- package/src/auth/service.ts +2 -2
- package/src/auth/transport.ts +59 -22
- package/src/auth/type.ts +0 -1
- package/src/cli/auth.ts +1 -3
- package/src/rpc/layer.ts +2 -2
- package/src/runtime.ts +14 -1
- package/src/sql/node-sqlite-client.ts +141 -0
- package/src/sql/service.ts +21 -0
package/src/auth/schema.ts
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import * as Schema from "effect/Schema";
|
|
2
2
|
|
|
3
|
-
export const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
export const AuthAccessTokenResultSchema = Schema.Struct({
|
|
4
|
+
access_token: Schema.String,
|
|
5
|
+
issued_token_type: Schema.String,
|
|
6
|
+
token_type: Schema.Literal("Bearer"),
|
|
7
|
+
expires_in: Schema.Number,
|
|
8
|
+
scope: Schema.String,
|
|
9
9
|
});
|
|
10
|
-
export type
|
|
10
|
+
export type AuthAccessTokenResult = typeof AuthAccessTokenResultSchema.Type;
|
|
11
|
+
|
|
12
|
+
export type AuthBearerBootstrapResult = {
|
|
13
|
+
readonly authenticated: true;
|
|
14
|
+
readonly role: "owner" | "client";
|
|
15
|
+
readonly sessionMethod: "bearer-access-token";
|
|
16
|
+
readonly expiresAt: string;
|
|
17
|
+
readonly sessionToken: string;
|
|
18
|
+
};
|
|
11
19
|
|
|
12
20
|
export const AuthSessionStateSchema = Schema.Struct({
|
|
13
21
|
authenticated: Schema.Boolean,
|
|
@@ -17,18 +25,11 @@ export const AuthSessionStateSchema = Schema.Struct({
|
|
|
17
25
|
});
|
|
18
26
|
export type AuthSessionState = typeof AuthSessionStateSchema.Type;
|
|
19
27
|
|
|
20
|
-
export const
|
|
21
|
-
|
|
22
|
-
expiresAt: Schema.String,
|
|
23
|
-
});
|
|
24
|
-
export type AuthWebSocketTokenResult = typeof AuthWebSocketTokenResultSchema.Type;
|
|
25
|
-
|
|
26
|
-
export const AuthLocalSessionIssueResultSchema = Schema.Struct({
|
|
27
|
-
token: Schema.String,
|
|
28
|
-
role: Schema.Literals(["owner", "client"]),
|
|
28
|
+
export const AuthWebSocketTicketResultSchema = Schema.Struct({
|
|
29
|
+
ticket: Schema.String,
|
|
29
30
|
expiresAt: Schema.String,
|
|
30
31
|
});
|
|
31
|
-
export type
|
|
32
|
+
export type AuthWebSocketTicketResult = typeof AuthWebSocketTicketResultSchema.Type;
|
|
32
33
|
|
|
33
34
|
export const AuthLocalRuntimeStateSchema = Schema.Struct({
|
|
34
35
|
version: Schema.Literal(1),
|
|
@@ -36,20 +37,12 @@ export const AuthLocalRuntimeStateSchema = Schema.Struct({
|
|
|
36
37
|
});
|
|
37
38
|
export type AuthLocalRuntimeState = typeof AuthLocalRuntimeStateSchema.Type;
|
|
38
39
|
|
|
39
|
-
export const
|
|
40
|
-
AuthBearerBootstrapResultSchema,
|
|
41
|
-
);
|
|
40
|
+
export const decodeAuthAccessTokenResult = Schema.decodeUnknownEffect(AuthAccessTokenResultSchema);
|
|
42
41
|
export const decodeAuthSessionState = Schema.decodeUnknownEffect(AuthSessionStateSchema);
|
|
43
|
-
export const
|
|
44
|
-
|
|
45
|
-
);
|
|
46
|
-
export const decodeAuthLocalSessionIssueResult = Schema.decodeUnknownEffect(
|
|
47
|
-
AuthLocalSessionIssueResultSchema,
|
|
42
|
+
export const decodeAuthWebSocketTicketResult = Schema.decodeUnknownEffect(
|
|
43
|
+
AuthWebSocketTicketResultSchema,
|
|
48
44
|
);
|
|
49
45
|
export const decodeAuthLocalRuntimeState = Schema.decodeUnknownEffect(AuthLocalRuntimeStateSchema);
|
|
50
|
-
export const decodeAuthLocalSessionIssueResultFromJson = Schema.decodeUnknownEffect(
|
|
51
|
-
Schema.fromJsonString(AuthLocalSessionIssueResultSchema),
|
|
52
|
-
);
|
|
53
46
|
export const decodeAuthLocalRuntimeStateFromJson = Schema.decodeUnknownEffect(
|
|
54
47
|
Schema.fromJsonString(AuthLocalRuntimeStateSchema),
|
|
55
48
|
);
|
package/src/auth/service.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as Context from "effect/Context";
|
|
2
2
|
import type * as Effect from "effect/Effect";
|
|
3
3
|
|
|
4
|
-
import type { AuthSessionState,
|
|
4
|
+
import type { AuthSessionState, AuthWebSocketTicketResult } from "./schema.ts";
|
|
5
5
|
import type { AuthError } from "./error.ts";
|
|
6
6
|
import type { LocalAuthInput, LocalAuthResult, PairResult } from "./type.ts";
|
|
7
7
|
|
|
@@ -11,6 +11,6 @@ export class T3Auth extends Context.Service<
|
|
|
11
11
|
readonly pair: (value: string) => Effect.Effect<PairResult, AuthError>;
|
|
12
12
|
readonly local: (input: LocalAuthInput) => Effect.Effect<LocalAuthResult, AuthError>;
|
|
13
13
|
readonly status: () => Effect.Effect<AuthSessionState, AuthError>;
|
|
14
|
-
readonly
|
|
14
|
+
readonly issueWebSocketTicket: () => Effect.Effect<AuthWebSocketTicketResult, AuthError>;
|
|
15
15
|
}
|
|
16
16
|
>()("t3cli/T3Auth") {}
|
package/src/auth/transport.ts
CHANGED
|
@@ -1,26 +1,58 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AuthAccessTokenType,
|
|
3
|
+
AuthEnvironmentBootstrapTokenType,
|
|
4
|
+
AuthTokenExchangeGrantType,
|
|
5
|
+
} from "#t3tools/contracts";
|
|
6
|
+
import * as Context from "effect/Context";
|
|
7
|
+
import * as DateTime from "effect/DateTime";
|
|
1
8
|
import * as Effect from "effect/Effect";
|
|
9
|
+
import * as Layer from "effect/Layer";
|
|
2
10
|
import { HttpClient, HttpClientError, HttpClientRequest } from "effect/unstable/http";
|
|
3
11
|
|
|
4
12
|
import type { ResolvedConfig } from "../config/service.ts";
|
|
5
13
|
import { toHttpEndpointUrl } from "../config/url.ts";
|
|
6
14
|
import { AuthTransportError } from "./error.ts";
|
|
7
15
|
import {
|
|
8
|
-
|
|
16
|
+
type AuthBearerBootstrapResult,
|
|
17
|
+
decodeAuthAccessTokenResult,
|
|
18
|
+
type AuthSessionState,
|
|
9
19
|
decodeAuthSessionState,
|
|
10
|
-
|
|
20
|
+
type AuthWebSocketTicketResult,
|
|
21
|
+
decodeAuthWebSocketTicketResult,
|
|
11
22
|
} from "./schema.ts";
|
|
12
23
|
|
|
13
|
-
export
|
|
24
|
+
export class T3AuthTransport extends Context.Service<
|
|
25
|
+
T3AuthTransport,
|
|
26
|
+
{
|
|
27
|
+
readonly bootstrapBearer: (input: {
|
|
28
|
+
readonly baseUrl: string;
|
|
29
|
+
readonly credential: string;
|
|
30
|
+
}) => Effect.Effect<AuthBearerBootstrapResult, AuthTransportError>;
|
|
31
|
+
readonly getSession: (
|
|
32
|
+
config: ResolvedConfig,
|
|
33
|
+
) => Effect.Effect<AuthSessionState, AuthTransportError>;
|
|
34
|
+
readonly issueWebSocketTicket: (
|
|
35
|
+
config: ResolvedConfig,
|
|
36
|
+
) => Effect.Effect<AuthWebSocketTicketResult, AuthTransportError>;
|
|
37
|
+
}
|
|
38
|
+
>()("t3cli/T3AuthTransport") {}
|
|
39
|
+
|
|
40
|
+
const makeT3AuthTransport = Effect.fn("makeT3AuthTransport")(function* () {
|
|
14
41
|
const client = HttpClient.filterStatusOk(yield* HttpClient.HttpClient);
|
|
15
42
|
|
|
16
43
|
const bootstrapBearer = Effect.fn("AuthTransport.bootstrapBearer")(function* (input: {
|
|
17
44
|
readonly baseUrl: string;
|
|
18
45
|
readonly credential: string;
|
|
19
46
|
}) {
|
|
20
|
-
const url = yield* makeHttpEndpointUrl(input.baseUrl, "/
|
|
47
|
+
const url = yield* makeHttpEndpointUrl(input.baseUrl, "/oauth/token");
|
|
21
48
|
const request = HttpClientRequest.post(url).pipe(
|
|
22
49
|
HttpClientRequest.acceptJson,
|
|
23
|
-
HttpClientRequest.
|
|
50
|
+
HttpClientRequest.bodyUrlParams({
|
|
51
|
+
grant_type: AuthTokenExchangeGrantType,
|
|
52
|
+
subject_token: input.credential,
|
|
53
|
+
subject_token_type: AuthEnvironmentBootstrapTokenType,
|
|
54
|
+
requested_token_type: AuthAccessTokenType,
|
|
55
|
+
}),
|
|
24
56
|
);
|
|
25
57
|
const response = yield* client.execute(request).pipe(
|
|
26
58
|
Effect.catchTags({
|
|
@@ -33,8 +65,8 @@ export const makeAuthTransport = Effect.fn("makeAuthTransport")(function* () {
|
|
|
33
65
|
),
|
|
34
66
|
}),
|
|
35
67
|
);
|
|
36
|
-
|
|
37
|
-
Effect.flatMap(
|
|
68
|
+
const result = yield* response.json.pipe(
|
|
69
|
+
Effect.flatMap(decodeAuthAccessTokenResult),
|
|
38
70
|
Effect.catchTags({
|
|
39
71
|
HttpClientError: (error) =>
|
|
40
72
|
Effect.fail(
|
|
@@ -49,10 +81,19 @@ export const makeAuthTransport = Effect.fn("makeAuthTransport")(function* () {
|
|
|
49
81
|
),
|
|
50
82
|
}),
|
|
51
83
|
);
|
|
84
|
+
const now = yield* DateTime.now;
|
|
85
|
+
return {
|
|
86
|
+
authenticated: true,
|
|
87
|
+
role: result.scope.split(/\s+/u).includes("access:write") ? "owner" : "client",
|
|
88
|
+
sessionMethod: "bearer-access-token",
|
|
89
|
+
expiresAt: DateTime.formatIso(DateTime.add(now, { seconds: result.expires_in })),
|
|
90
|
+
sessionToken: result.access_token,
|
|
91
|
+
} satisfies AuthBearerBootstrapResult;
|
|
52
92
|
});
|
|
53
93
|
|
|
54
94
|
const getSession = Effect.fn("AuthTransport.getSession")(function* (config: ResolvedConfig) {
|
|
55
|
-
const
|
|
95
|
+
const url = yield* makeHttpEndpointUrl(config.url, "/api/auth/session");
|
|
96
|
+
const request = HttpClientRequest.get(url).pipe(authenticatedRequest(config));
|
|
56
97
|
const response = yield* client.execute(request).pipe(
|
|
57
98
|
Effect.catchTags({
|
|
58
99
|
HttpClientError: (error) =>
|
|
@@ -82,10 +123,11 @@ export const makeAuthTransport = Effect.fn("makeAuthTransport")(function* () {
|
|
|
82
123
|
);
|
|
83
124
|
});
|
|
84
125
|
|
|
85
|
-
const
|
|
126
|
+
const issueWebSocketTicket = Effect.fn("AuthTransport.issueWebSocketTicket")(function* (
|
|
86
127
|
config: ResolvedConfig,
|
|
87
128
|
) {
|
|
88
|
-
const
|
|
129
|
+
const url = yield* makeHttpEndpointUrl(config.url, "/api/auth/websocket-ticket");
|
|
130
|
+
const request = HttpClientRequest.post(url).pipe(authenticatedRequest(config));
|
|
89
131
|
const response = yield* client.execute(request).pipe(
|
|
90
132
|
Effect.catchTags({
|
|
91
133
|
HttpClientError: (error) =>
|
|
@@ -98,7 +140,7 @@ export const makeAuthTransport = Effect.fn("makeAuthTransport")(function* () {
|
|
|
98
140
|
}),
|
|
99
141
|
);
|
|
100
142
|
return yield* response.json.pipe(
|
|
101
|
-
Effect.flatMap(
|
|
143
|
+
Effect.flatMap(decodeAuthWebSocketTicketResult),
|
|
102
144
|
Effect.catchTags({
|
|
103
145
|
HttpClientError: (error) =>
|
|
104
146
|
Effect.fail(
|
|
@@ -118,20 +160,15 @@ export const makeAuthTransport = Effect.fn("makeAuthTransport")(function* () {
|
|
|
118
160
|
return {
|
|
119
161
|
bootstrapBearer,
|
|
120
162
|
getSession,
|
|
121
|
-
|
|
163
|
+
issueWebSocketTicket,
|
|
122
164
|
};
|
|
123
165
|
});
|
|
124
166
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
)
|
|
130
|
-
Effect.map((request) =>
|
|
131
|
-
request.pipe(HttpClientRequest.acceptJson, HttpClientRequest.bearerToken(config.token)),
|
|
132
|
-
),
|
|
133
|
-
);
|
|
134
|
-
}
|
|
167
|
+
export const T3AuthTransportLive = Layer.effect(T3AuthTransport, makeT3AuthTransport());
|
|
168
|
+
|
|
169
|
+
const authenticatedRequest =
|
|
170
|
+
(config: ResolvedConfig) => (request: HttpClientRequest.HttpClientRequest) =>
|
|
171
|
+
request.pipe(HttpClientRequest.acceptJson, HttpClientRequest.bearerToken(config.token));
|
|
135
172
|
|
|
136
173
|
function makeHttpEndpointUrl(baseUrl: string, path: string) {
|
|
137
174
|
return toHttpEndpointUrl(baseUrl, path).pipe(
|
package/src/auth/type.ts
CHANGED
package/src/cli/auth.ts
CHANGED
|
@@ -47,14 +47,13 @@ const localCommand = Command.make(
|
|
|
47
47
|
"local",
|
|
48
48
|
{
|
|
49
49
|
baseDir: Flag.string("base-dir").pipe(Flag.optional),
|
|
50
|
-
t3Command: Flag.string("t3-command").pipe(Flag.optional),
|
|
51
50
|
origin: Flag.string("origin").pipe(Flag.optional),
|
|
52
51
|
role: Flag.choice("role", ["owner", "client"] as const).pipe(Flag.withDefault("owner")),
|
|
53
52
|
label: Flag.string("label").pipe(Flag.withDefault("t3cli")),
|
|
54
53
|
subject: Flag.string("subject").pipe(Flag.withDefault("t3cli-local")),
|
|
55
54
|
format: Flag.choice("format", humanJsonFormatChoices).pipe(Flag.withDefault("auto")),
|
|
56
55
|
},
|
|
57
|
-
({ baseDir,
|
|
56
|
+
({ baseDir, origin, role, label, subject, format }) =>
|
|
58
57
|
Effect.gen(function* () {
|
|
59
58
|
const auth = yield* T3Auth;
|
|
60
59
|
const environment = yield* Environment;
|
|
@@ -64,7 +63,6 @@ const localCommand = Command.make(
|
|
|
64
63
|
role,
|
|
65
64
|
label,
|
|
66
65
|
subject,
|
|
67
|
-
...(Option.isSome(t3Command) ? { t3Command: t3Command.value } : {}),
|
|
68
66
|
...(Option.isSome(baseDir) ? { baseDir: baseDir.value } : {}),
|
|
69
67
|
...(Option.isSome(origin) ? { origin: origin.value } : {}),
|
|
70
68
|
});
|
package/src/rpc/layer.ts
CHANGED
|
@@ -76,10 +76,10 @@ const makeWsUrl = Effect.fn("makeWsUrl")(function* (input: {
|
|
|
76
76
|
readonly auth: T3Auth["Service"];
|
|
77
77
|
}) {
|
|
78
78
|
const resolved = yield* input.config.resolve();
|
|
79
|
-
const
|
|
79
|
+
const wsTicket = yield* input.auth.issueWebSocketTicket();
|
|
80
80
|
const wsUrl = yield* toWebSocketEndpointUrl(resolved.url, "/ws");
|
|
81
81
|
const request = HttpClientRequest.get(wsUrl).pipe(
|
|
82
|
-
HttpClientRequest.setUrlParam("
|
|
82
|
+
HttpClientRequest.setUrlParam("wsTicket", wsTicket.ticket),
|
|
83
83
|
);
|
|
84
84
|
return Option.getOrThrow(HttpClientRequest.toUrl(request)).toString();
|
|
85
85
|
});
|
package/src/runtime.ts
CHANGED
|
@@ -3,13 +3,26 @@ import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient";
|
|
|
3
3
|
import * as NodeSocket from "@effect/platform-node/NodeSocket";
|
|
4
4
|
|
|
5
5
|
import { T3AuthLive } from "./auth/layer.ts";
|
|
6
|
+
import { T3LocalAuthLive } from "./auth/local.ts";
|
|
7
|
+
import { T3AuthPairingLive } from "./auth/pairing.ts";
|
|
8
|
+
import { T3AuthTransportLive } from "./auth/transport.ts";
|
|
6
9
|
import { T3ConfigLive } from "./config/layer.ts";
|
|
7
10
|
import { T3ApplicationLive } from "./application/layer.ts";
|
|
8
11
|
import { T3OrchestrationLive } from "./orchestration/layer.ts";
|
|
9
12
|
import { T3RpcLive } from "./rpc/layer.ts";
|
|
13
|
+
import { NodeSqlClientFactoryLive } from "./sql/node-sqlite-client.ts";
|
|
10
14
|
|
|
15
|
+
const T3AuthTransportLayer = T3AuthTransportLive.pipe(Layer.provide(NodeHttpClient.layerUndici));
|
|
16
|
+
const T3LocalAuthLayer = T3LocalAuthLive.pipe(
|
|
17
|
+
Layer.provide(Layer.mergeAll(T3ConfigLive, NodeSqlClientFactoryLive)),
|
|
18
|
+
);
|
|
19
|
+
const T3AuthPairingLayer = T3AuthPairingLive.pipe(
|
|
20
|
+
Layer.provide(Layer.mergeAll(T3ConfigLive, T3AuthTransportLayer)),
|
|
21
|
+
);
|
|
11
22
|
const T3AuthLayer = T3AuthLive.pipe(
|
|
12
|
-
Layer.provide(
|
|
23
|
+
Layer.provide(
|
|
24
|
+
Layer.mergeAll(T3ConfigLive, T3AuthTransportLayer, T3LocalAuthLayer, T3AuthPairingLayer),
|
|
25
|
+
),
|
|
13
26
|
);
|
|
14
27
|
const T3RpcLayer = T3RpcLive.pipe(
|
|
15
28
|
Layer.provide(Layer.mergeAll(T3ConfigLive, T3AuthLayer, NodeSocket.layerWebSocketConstructor)),
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { DatabaseSync, type SQLInputValue } from "node:sqlite";
|
|
2
|
+
import * as Context from "effect/Context";
|
|
3
|
+
import * as Effect from "effect/Effect";
|
|
4
|
+
import { identity } from "effect/Function";
|
|
5
|
+
import * as Layer from "effect/Layer";
|
|
6
|
+
import * as Scope from "effect/Scope";
|
|
7
|
+
import * as Stream from "effect/Stream";
|
|
8
|
+
import * as Reactivity from "effect/unstable/reactivity/Reactivity";
|
|
9
|
+
import * as SqlClient from "effect/unstable/sql/SqlClient";
|
|
10
|
+
import type { Connection } from "effect/unstable/sql/SqlConnection";
|
|
11
|
+
import { classifySqliteError, SqlError } from "effect/unstable/sql/SqlError";
|
|
12
|
+
import * as Statement from "effect/unstable/sql/Statement";
|
|
13
|
+
|
|
14
|
+
import { SqlClientFactory, type SqliteClientConfig } from "./service.ts";
|
|
15
|
+
|
|
16
|
+
const ATTR_DB_SYSTEM_NAME = "db.system.name";
|
|
17
|
+
|
|
18
|
+
const classifyError = (cause: unknown, message: string, operation: string) =>
|
|
19
|
+
classifySqliteError(cause, { message, operation });
|
|
20
|
+
|
|
21
|
+
export const makeNodeSqliteClient = Effect.fn("makeNodeSqliteClient")(function* (
|
|
22
|
+
config: SqliteClientConfig,
|
|
23
|
+
) {
|
|
24
|
+
const compiler = Statement.makeCompilerSqlite(config.transformQueryNames);
|
|
25
|
+
const transformRows =
|
|
26
|
+
config.transformResultNames === undefined
|
|
27
|
+
? undefined
|
|
28
|
+
: Statement.defaultTransforms(config.transformResultNames).array;
|
|
29
|
+
const scope = yield* Effect.scope;
|
|
30
|
+
const db = yield* Effect.try({
|
|
31
|
+
try: () => new DatabaseSync(config.filename),
|
|
32
|
+
catch: (cause) =>
|
|
33
|
+
new SqlError({
|
|
34
|
+
reason: classifyError(cause, "Failed to open sqlite database", "connect"),
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
yield* Scope.addFinalizer(
|
|
38
|
+
scope,
|
|
39
|
+
Effect.sync(() => {
|
|
40
|
+
try {
|
|
41
|
+
db.close();
|
|
42
|
+
} catch {
|
|
43
|
+
// Ignore close failures after the scoped client is done.
|
|
44
|
+
}
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const runRows = (sql: string, params: ReadonlyArray<unknown>) =>
|
|
49
|
+
Effect.try({
|
|
50
|
+
try: () => {
|
|
51
|
+
const statement = db.prepare(sql);
|
|
52
|
+
const bound = params.map(toSqlInputValue);
|
|
53
|
+
if (statement.columns().length > 0) {
|
|
54
|
+
return statement.all(...bound);
|
|
55
|
+
}
|
|
56
|
+
statement.run(...bound);
|
|
57
|
+
return [];
|
|
58
|
+
},
|
|
59
|
+
catch: (cause) =>
|
|
60
|
+
new SqlError({
|
|
61
|
+
reason: classifyError(cause, "Failed to execute sqlite statement", "execute"),
|
|
62
|
+
}),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const runRaw = (sql: string, params: ReadonlyArray<unknown>) =>
|
|
66
|
+
Effect.try({
|
|
67
|
+
try: () => {
|
|
68
|
+
const statement = db.prepare(sql);
|
|
69
|
+
const bound = params.map(toSqlInputValue);
|
|
70
|
+
return statement.columns().length > 0 ? statement.all(...bound) : statement.run(...bound);
|
|
71
|
+
},
|
|
72
|
+
catch: (cause) =>
|
|
73
|
+
new SqlError({
|
|
74
|
+
reason: classifyError(cause, "Failed to execute sqlite statement", "execute"),
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const runValues = (sql: string, params: ReadonlyArray<unknown>) =>
|
|
79
|
+
Effect.map(runRows(sql, params), (rows) =>
|
|
80
|
+
rows.map((row) => Object.values(row as Record<string, unknown>)),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
const connection = identity<Connection>({
|
|
84
|
+
execute(sql, params, rowTransform) {
|
|
85
|
+
const effect = runRows(sql, params);
|
|
86
|
+
return rowTransform === undefined ? effect : Effect.map(effect, rowTransform);
|
|
87
|
+
},
|
|
88
|
+
executeRaw(sql, params) {
|
|
89
|
+
return runRaw(sql, params);
|
|
90
|
+
},
|
|
91
|
+
executeValues(sql, params) {
|
|
92
|
+
return runValues(sql, params);
|
|
93
|
+
},
|
|
94
|
+
executeUnprepared(sql, params, rowTransform) {
|
|
95
|
+
const effect = runRows(sql, params);
|
|
96
|
+
return rowTransform === undefined ? effect : Effect.map(effect, rowTransform);
|
|
97
|
+
},
|
|
98
|
+
executeStream(sql, params, rowTransform) {
|
|
99
|
+
return Stream.fromIterableEffect(this.execute(sql, params, rowTransform));
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const acquirer = Effect.succeed(connection);
|
|
104
|
+
return yield* SqlClient.make({
|
|
105
|
+
acquirer,
|
|
106
|
+
compiler,
|
|
107
|
+
spanAttributes: [[ATTR_DB_SYSTEM_NAME, "sqlite"]],
|
|
108
|
+
transformRows,
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
export const NodeSqliteClientLive = (config: SqliteClientConfig) =>
|
|
113
|
+
Layer.effectContext(
|
|
114
|
+
Effect.map(makeNodeSqliteClient(config), (client) => Context.make(SqlClient.SqlClient, client)),
|
|
115
|
+
).pipe(Layer.provide(Reactivity.layer));
|
|
116
|
+
|
|
117
|
+
export const NodeSqlClientFactoryLive = Layer.succeed(SqlClientFactory, {
|
|
118
|
+
sqliteClient: (config) => makeNodeSqliteClient(config).pipe(Effect.provide(Reactivity.layer)),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
function toSqlInputValue(value: unknown): SQLInputValue {
|
|
122
|
+
if (value === undefined || value === null) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "bigint") {
|
|
126
|
+
return value;
|
|
127
|
+
}
|
|
128
|
+
if (typeof value === "boolean") {
|
|
129
|
+
return value ? 1 : 0;
|
|
130
|
+
}
|
|
131
|
+
if (value instanceof Date) {
|
|
132
|
+
return value.toISOString();
|
|
133
|
+
}
|
|
134
|
+
if (ArrayBuffer.isView(value)) {
|
|
135
|
+
const bytes = new Uint8Array(value.buffer, value.byteOffset, value.byteLength);
|
|
136
|
+
const copy = new Uint8Array(bytes.byteLength);
|
|
137
|
+
copy.set(bytes);
|
|
138
|
+
return copy;
|
|
139
|
+
}
|
|
140
|
+
return JSON.stringify(value) ?? null;
|
|
141
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type * as Effect from "effect/Effect";
|
|
2
|
+
import * as Context from "effect/Context";
|
|
3
|
+
import type * as Scope from "effect/Scope";
|
|
4
|
+
import type * as SqlClient from "effect/unstable/sql/SqlClient";
|
|
5
|
+
import type { SqlError } from "effect/unstable/sql/SqlError";
|
|
6
|
+
|
|
7
|
+
export type SqliteClientConfig = {
|
|
8
|
+
readonly filename: string;
|
|
9
|
+
readonly transformResultNames?: ((str: string) => string) | undefined;
|
|
10
|
+
readonly transformQueryNames?: ((str: string) => string) | undefined;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type SqlClientFactoryShape = {
|
|
14
|
+
readonly sqliteClient: (
|
|
15
|
+
config: SqliteClientConfig,
|
|
16
|
+
) => Effect.Effect<SqlClient.SqlClient, SqlError, Scope.Scope>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export class SqlClientFactory extends Context.Service<SqlClientFactory, SqlClientFactoryShape>()(
|
|
20
|
+
"t3cli/SqlClientFactory",
|
|
21
|
+
) {}
|