effect-cf 0.4.0 → 0.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # effect-cf
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#14](https://github.com/danieljvdm/effect-cf/pull/14) [`a31a930`](https://github.com/danieljvdm/effect-cf/commit/a31a93020a679e42a748aed54626ce7387d7e685) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Add a D1 binding helper with native Effect wrappers and an `@effect/sql-d1` backed SQL layer.
8
+
3
9
  ## 0.4.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # effect-cf
2
2
 
3
- Effect-native Cloudflare primitives for Workers, Durable Objects, bindings, KV, Queues, Workflows, and Durable Object storage.
3
+ Effect-native Cloudflare primitives for Workers, Durable Objects, bindings, KV, D1, Queues, Workflows, and Durable Object storage.
4
4
 
5
5
  ## Install
6
6
 
@@ -31,6 +31,7 @@ Runtime creation belongs at Cloudflare entrypoints, not inside binding helpers.
31
31
  - `DurableObjectState` / `DurableObjectStorage` - Effect wrappers for state, alarms, SQL, and embedded KV
32
32
  - `DurableObjectWebSocket` - WebSocket upgrade helpers for Durable Objects
33
33
  - `Kv` - typed KV namespace helper
34
+ - `D1` - typed D1 database binding helper with an `@effect/sql-d1` backed SQL layer
34
35
  - `Queue` / `QueueBinding` - typed Queue producer bindings and consumer handlers
35
36
  - `Workflow` / `WorkflowBinding` - typed Workflow entrypoints, steps, and starter bindings
36
37
  - `Rpc` - Cloudflare RPC type helpers and scoped disposal utilities
package/dist/index.d.mts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Cause, Config, ConfigProvider, Context, DateTime, Duration, Effect, Layer, Option, Schema, Scope } from "effect";
2
+ import { D1Client } from "@effect/sql-d1";
2
3
  import { DurableObject, RpcStub, RpcTarget, WorkerEntrypoint, WorkflowEntrypoint, WorkflowEvent, WorkflowSleepDuration, WorkflowStep, WorkflowStepConfig, WorkflowStepContext, WorkflowStepEvent, WorkflowTimeoutDuration } from "cloudflare:workers";
3
4
  import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
4
5
  import * as RpcSerialization from "effect/unstable/rpc/RpcSerialization";
@@ -7,9 +8,11 @@ import * as _$effect_Types0 from "effect/Types";
7
8
  import * as _$effect_Cause0 from "effect/Cause";
8
9
  import * as _$effect_Redacted0 from "effect/Redacted";
9
10
  import * as _$effect_Layer0 from "effect/Layer";
10
- import * as _$effect_Schema0 from "effect/Schema";
11
11
  import * as _$effect_Context0 from "effect/Context";
12
+ import * as _$effect_unstable_sql_SqlClient0 from "effect/unstable/sql/SqlClient";
13
+ import * as _$effect_Config0 from "effect/Config";
12
14
  import * as _$effect_Scope0 from "effect/Scope";
15
+ import * as _$effect_Schema0 from "effect/Schema";
13
16
 
14
17
  //#region src/Environment.d.ts
15
18
  /** Cloudflare worker environment object (`env`). */
@@ -83,7 +86,7 @@ declare namespace WorkerConfig {
83
86
  const layerWith: (makeProvider: (env: WorkerEnv) => ConfigProvider.ConfigProvider) => _$effect_Layer0.Layer<never, never, WorkerEnvironment>;
84
87
  }
85
88
  declare namespace Binding_d_exports {
86
- export { BindingNotFoundError, BindingValidationError, Service$5 as Service, TypeId$2 as TypeId };
89
+ export { BindingNotFoundError, BindingValidationError, Service$6 as Service, TypeId$2 as TypeId };
87
90
  }
88
91
  /** Internal type id marker used by binding helper services. */
89
92
  declare const TypeId$2: "effect-cf/Binding";
@@ -104,11 +107,82 @@ declare class BindingValidationError extends BindingValidationError_base<{
104
107
  /**
105
108
  * Creates a Context tag + layer for reading and validating a Cloudflare binding.
106
109
  */
107
- declare const Service$5: <Self>() => <Id extends string, Resource>(id: Id, binding: string, isResource: (value: unknown) => value is Resource) => Context.ServiceClass<Self, Id, Resource> & {
110
+ declare const Service$6: <Self>() => <Id extends string, Resource>(id: Id, binding: string, isResource: (value: unknown) => value is Resource) => Context.ServiceClass<Self, Id, Resource> & {
108
111
  "effect-cf/Binding": "effect-cf/Binding";
109
112
  binding: string;
110
113
  layer: Layer.Layer<Self, BindingNotFoundError | BindingValidationError, WorkerEnvironment>;
111
114
  };
115
+ declare namespace D1_d_exports {
116
+ export { D1Definition, D1OperationError, D1Service, D1SqlLayerOptions, Service$5 as Service, make$11 as make };
117
+ }
118
+ declare const D1OperationError_base: new <A extends Record<string, any> = {}>(args: _$effect_Types0.VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P] }>) => _$effect_Cause0.YieldableError & {
119
+ readonly _tag: "D1OperationError";
120
+ } & Readonly<A>;
121
+ /** Error raised when a D1 operation fails. */
122
+ declare class D1OperationError extends D1OperationError_base<{
123
+ readonly binding: string;
124
+ readonly operation: string;
125
+ readonly cause: unknown;
126
+ }> {}
127
+ /** Typed D1 binding definition. */
128
+ interface D1Definition {
129
+ /** Binding name as configured in `wrangler.jsonc`. */
130
+ readonly binding: string;
131
+ }
132
+ /** Options forwarded to `@effect/sql-d1` when building a SQL client layer. */
133
+ type D1SqlLayerOptions = Omit<D1Client.D1ClientConfig, "db">;
134
+ declare const D1ServiceTypeId: unique symbol;
135
+ /** Nominal service marker for D1 services created with {@link make}. */
136
+ interface D1Service<Id extends string> {
137
+ readonly [D1ServiceTypeId]: {
138
+ readonly id: Id;
139
+ };
140
+ }
141
+ /**
142
+ * Creates a typed D1 service tag plus Effect helpers.
143
+ */
144
+ declare const make$11: <Id extends string>(id: Id, definition: D1Definition) => _$effect_Context0.ServiceClass<D1Service<Id>, Id, D1Database> & {
145
+ "effect-cf/Binding": "effect-cf/Binding";
146
+ binding: string;
147
+ layer: Layer.Layer<D1Service<Id>, BindingNotFoundError | BindingValidationError, WorkerEnvironment>;
148
+ } & {
149
+ "effect-cf/D1": "effect-cf/D1";
150
+ definition: D1Definition;
151
+ sqlLayer: (options?: D1SqlLayerOptions) => Layer.Layer<D1Client.D1Client | _$effect_unstable_sql_SqlClient0.SqlClient, BindingNotFoundError | BindingValidationError | _$effect_Config0.ConfigError, WorkerEnvironment>;
152
+ prepare: (query: string) => Effect.Effect<D1PreparedStatement, D1OperationError, D1Service<Id>>;
153
+ batch: <T = unknown>(statements: readonly D1PreparedStatement[]) => Effect.Effect<D1Result<T>[], D1OperationError, D1Service<Id>>;
154
+ exec: (query: string) => Effect.Effect<D1ExecResult, D1OperationError, D1Service<Id>>;
155
+ unsafeRaw: () => Effect.Effect<D1Database, never, D1Service<Id>>;
156
+ };
157
+ /**
158
+ * Builds a D1 service around a Cloudflare D1 database binding.
159
+ *
160
+ * The returned service exposes the raw `D1Database` binding, small native D1
161
+ * helpers, and `sqlLayer(...)` for providing `effect/unstable/sql` via
162
+ * `@effect/sql-d1`.
163
+ *
164
+ * @example
165
+ * ```ts
166
+ * class TodoDatabase extends D1.Service<TodoDatabase>()("TodoDatabase", {
167
+ * binding: "TODO_DB",
168
+ * }) {}
169
+ *
170
+ * const SqlLive = TodoDatabase.sqlLayer();
171
+ * ```
172
+ */
173
+ declare const Service$5: <Self>() => <Id extends string>(id: Id, definition: D1Definition) => _$effect_Context0.ServiceClass<Self, Id, D1Database> & {
174
+ "effect-cf/Binding": "effect-cf/Binding";
175
+ binding: string;
176
+ layer: Layer.Layer<Self, BindingNotFoundError | BindingValidationError, WorkerEnvironment>;
177
+ } & {
178
+ "effect-cf/D1": "effect-cf/D1";
179
+ definition: D1Definition;
180
+ sqlLayer: (options?: D1SqlLayerOptions) => Layer.Layer<D1Client.D1Client | _$effect_unstable_sql_SqlClient0.SqlClient, BindingNotFoundError | BindingValidationError | _$effect_Config0.ConfigError, WorkerEnvironment | Exclude<Exclude<Self, _$effect_Scope0.Scope>, Self>>;
181
+ prepare: (query: string) => Effect.Effect<D1PreparedStatement, D1OperationError, Self>;
182
+ batch: <T = unknown>(statements: readonly D1PreparedStatement[]) => Effect.Effect<D1Result<T>[], D1OperationError, Self>;
183
+ exec: (query: string) => Effect.Effect<D1ExecResult, D1OperationError, Self>;
184
+ unsafeRaw: () => Effect.Effect<D1Database, never, Self>;
185
+ };
112
186
  declare namespace Rpc_d_exports {
113
187
  export { DisposableValue, MethodArgs, MethodKey, MethodReturn, Provider, Result, RpcStub, RpcTarget, Serializable, Stub, Stubable, Stubify, dispose, isDisposable, resolve, scoped };
114
188
  }
@@ -2382,4 +2456,4 @@ declare namespace Definition {
2382
2456
  type Handler<ROut, Self extends Definition$1.Any> = Handler$1<ROut, Self>;
2383
2457
  type Options<ROut, Self extends Definition$1.Any> = Options$1<ROut, Self>;
2384
2458
  //#endregion
2385
- export { Binding_d_exports as Binding, DurableObject_d_exports as DurableObject, DurableObjectAlarm_d_exports as DurableObjectAlarm, DurableObjectDefinition_d_exports as DurableObjectDefinition, DurableObjectNamespace_d_exports as DurableObjectNamespace, DurableObjectRpcWebSocket_d_exports as DurableObjectRpcWebSocket, DurableObjectState_d_exports as DurableObjectState, DurableObjectStorage_d_exports as DurableObjectStorage, DurableObjectWebSocket_d_exports as DurableObjectWebSocket, Kv_d_exports as Kv, Queue_d_exports as Queue, QueueBinding_d_exports as QueueBinding, QueueDefinition_d_exports as QueueDefinition, Rpc_d_exports as Rpc, RpcDefinition_d_exports as RpcDefinition, ServiceBinding_d_exports as ServiceBinding, Worker_d_exports as Worker, WorkerConfig, WorkerDefinition_d_exports as WorkerDefinition, type WorkerEnv, WorkerEnvironment, Workflow_d_exports as Workflow, WorkflowBinding_d_exports as WorkflowBinding, WorkflowDefinition_d_exports as WorkflowDefinition };
2459
+ export { Binding_d_exports as Binding, D1_d_exports as D1, DurableObject_d_exports as DurableObject, DurableObjectAlarm_d_exports as DurableObjectAlarm, DurableObjectDefinition_d_exports as DurableObjectDefinition, DurableObjectNamespace_d_exports as DurableObjectNamespace, DurableObjectRpcWebSocket_d_exports as DurableObjectRpcWebSocket, DurableObjectState_d_exports as DurableObjectState, DurableObjectStorage_d_exports as DurableObjectStorage, DurableObjectWebSocket_d_exports as DurableObjectWebSocket, Kv_d_exports as Kv, Queue_d_exports as Queue, QueueBinding_d_exports as QueueBinding, QueueDefinition_d_exports as QueueDefinition, Rpc_d_exports as Rpc, RpcDefinition_d_exports as RpcDefinition, ServiceBinding_d_exports as ServiceBinding, Worker_d_exports as Worker, WorkerConfig, WorkerDefinition_d_exports as WorkerDefinition, type WorkerEnv, WorkerEnvironment, Workflow_d_exports as Workflow, WorkflowBinding_d_exports as WorkflowBinding, WorkflowDefinition_d_exports as WorkflowDefinition };
package/dist/index.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
2
2
  import { Cause, Clock, Config, ConfigProvider, Context, Data, DateTime, Duration, Effect, Exit, Layer, ManagedRuntime, Option, Queue, Schema } from "effect";
3
+ import { D1Client } from "@effect/sql-d1";
3
4
  import { DurableObject, RpcStub, RpcTarget, WorkerEntrypoint, WorkflowEntrypoint } from "cloudflare:workers";
4
5
  import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
5
6
  import * as RpcMessage from "effect/unstable/rpc/RpcMessage";
@@ -28,11 +29,11 @@ let WorkerConfig;
28
29
  var Binding_exports = /* @__PURE__ */ __exportAll({
29
30
  BindingNotFoundError: () => BindingNotFoundError,
30
31
  BindingValidationError: () => BindingValidationError,
31
- Service: () => Service$5,
32
- TypeId: () => TypeId$5
32
+ Service: () => Service$6,
33
+ TypeId: () => TypeId$6
33
34
  });
34
35
  /** Internal type id marker used by binding helper services. */
35
- const TypeId$5 = "effect-cf/Binding";
36
+ const TypeId$6 = "effect-cf/Binding";
36
37
  /** Error raised when a configured binding does not exist on `env`. */
37
38
  var BindingNotFoundError = class extends Data.TaggedError("BindingNotFoundError") {};
38
39
  /** Error raised when a binding exists but does not match the expected shape. */
@@ -48,18 +49,100 @@ const getBinding = (env, binding, isResource) => Effect.gen(function* () {
48
49
  /**
49
50
  * Creates a Context tag + layer for reading and validating a Cloudflare binding.
50
51
  */
51
- const Service$5 = () => (id, binding, isResource) => {
52
+ const Service$6 = () => (id, binding, isResource) => {
52
53
  const tag = Context.Service()(id);
53
54
  const layer = Layer.effect(tag, Effect.gen(function* () {
54
55
  return yield* getBinding(yield* WorkerEnvironment, binding, isResource);
55
56
  }));
56
57
  return Object.assign(tag, {
57
- [TypeId$5]: TypeId$5,
58
+ [TypeId$6]: TypeId$6,
58
59
  binding,
59
60
  layer
60
61
  });
61
62
  };
62
63
  //#endregion
64
+ //#region src/D1.ts
65
+ var D1_exports = /* @__PURE__ */ __exportAll({
66
+ D1OperationError: () => D1OperationError,
67
+ Service: () => Service$5,
68
+ make: () => make$11
69
+ });
70
+ const TypeId$5 = "effect-cf/D1";
71
+ /** Error raised when a D1 operation fails. */
72
+ var D1OperationError = class extends Data.TaggedError("D1OperationError") {};
73
+ const isD1Database = (value) => {
74
+ if (typeof value !== "object" || value === null) return false;
75
+ const resource = value;
76
+ return typeof resource.prepare === "function" && typeof resource.batch === "function" && typeof resource.exec === "function";
77
+ };
78
+ const d1Error = (binding, operation, cause) => new D1OperationError({
79
+ binding,
80
+ operation,
81
+ cause
82
+ });
83
+ const tryD1Promise = (binding, operation, evaluate) => Effect.tryPromise({
84
+ try: evaluate,
85
+ catch: (cause) => d1Error(binding, operation, cause)
86
+ });
87
+ const tryD1Sync = (binding, operation, evaluate) => Effect.try({
88
+ try: evaluate,
89
+ catch: (cause) => d1Error(binding, operation, cause)
90
+ });
91
+ /**
92
+ * Creates a typed D1 service tag plus Effect helpers.
93
+ */
94
+ const make$11 = (id, definition) => Service$5()(id, definition);
95
+ /**
96
+ * Builds a D1 service around a Cloudflare D1 database binding.
97
+ *
98
+ * The returned service exposes the raw `D1Database` binding, small native D1
99
+ * helpers, and `sqlLayer(...)` for providing `effect/unstable/sql` via
100
+ * `@effect/sql-d1`.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * class TodoDatabase extends D1.Service<TodoDatabase>()("TodoDatabase", {
105
+ * binding: "TODO_DB",
106
+ * }) {}
107
+ *
108
+ * const SqlLive = TodoDatabase.sqlLayer();
109
+ * ```
110
+ */
111
+ const Service$5 = () => (id, definition) => {
112
+ const tag = Service$6()(id, definition.binding, isD1Database);
113
+ const sqlLayer = (options) => Layer.unwrap(Effect.gen(function* () {
114
+ const db = yield* tag;
115
+ return D1Client.layer({
116
+ ...options,
117
+ db
118
+ });
119
+ })).pipe(Layer.provide(tag.layer));
120
+ const prepare = Effect.fnUntraced(function* (query) {
121
+ const db = yield* tag;
122
+ return yield* tryD1Sync(definition.binding, "prepare", () => db.prepare(query));
123
+ });
124
+ const batch = Effect.fnUntraced(function* (statements) {
125
+ const db = yield* tag;
126
+ return yield* tryD1Promise(definition.binding, "batch", () => db.batch([...statements]));
127
+ });
128
+ const exec = Effect.fnUntraced(function* (query) {
129
+ const db = yield* tag;
130
+ return yield* tryD1Promise(definition.binding, "exec", () => db.exec(query));
131
+ });
132
+ const unsafeRaw = Effect.fnUntraced(function* () {
133
+ return yield* tag;
134
+ });
135
+ return Object.assign(tag, {
136
+ [TypeId$5]: TypeId$5,
137
+ definition,
138
+ sqlLayer,
139
+ prepare,
140
+ batch,
141
+ exec,
142
+ unsafeRaw
143
+ });
144
+ };
145
+ //#endregion
63
146
  //#region src/QueueBinding.ts
64
147
  var QueueBinding_exports = /* @__PURE__ */ __exportAll({
65
148
  QueueOperationError: () => QueueOperationError,
@@ -88,7 +171,7 @@ const make$10 = (id, definition) => Service$4()(id, definition);
88
171
  * Builds an Effect service around a Cloudflare Queue producer binding.
89
172
  */
90
173
  const Service$4 = () => (id, definition) => {
91
- const tag = Service$5()(id, definition.binding, (value) => isQueue(value));
174
+ const tag = Service$6()(id, definition.binding, (value) => isQueue(value));
92
175
  const encodeMessage = Schema.encodeEffect(definition.message);
93
176
  const send = Effect.fnUntraced(function* (message, options) {
94
177
  const queue = yield* tag;
@@ -421,7 +504,7 @@ var ServiceBindingRpcError = class extends Data.TaggedError("ServiceBindingRpcEr
421
504
  * ```
422
505
  */
423
506
  const Service$3 = () => (id, definition) => {
424
- const tag = Service$5()(id, definition.binding, (value) => typeof value === "object" && value !== null && "fetch" in value);
507
+ const tag = Service$6()(id, definition.binding, (value) => typeof value === "object" && value !== null && "fetch" in value);
425
508
  const fetch = (input, init) => Effect.gen(function* () {
426
509
  const service = yield* tag;
427
510
  return yield* Effect.tryPromise({
@@ -1006,7 +1089,7 @@ var DurableObjectFetchError = class extends Data.TaggedError("DurableObjectFetch
1006
1089
  const Service$2 = () => (id, definition) => {
1007
1090
  const hasFunction = (value, key) => typeof Reflect.get(value, key) === "function";
1008
1091
  const isDurableObjectNamespaceClient = (value) => typeof value === "object" && value !== null && hasFunction(value, "getByName") && hasFunction(value, "get") && hasFunction(value, "idFromName") && hasFunction(value, "idFromString") && hasFunction(value, "newUniqueId") && hasFunction(value, "jurisdiction");
1009
- const tag = Service$5()(id, definition.binding, (value) => isDurableObjectNamespaceClient(value));
1092
+ const tag = Service$6()(id, definition.binding, (value) => isDurableObjectNamespaceClient(value));
1010
1093
  const newUniqueId = Effect.fnUntraced(function* (options) {
1011
1094
  return (yield* tag).newUniqueId(options);
1012
1095
  });
@@ -1770,7 +1853,7 @@ const Tag$2 = () => (id, definition) => {
1770
1853
  * ```
1771
1854
  */
1772
1855
  const Service$1 = () => (id, definition) => {
1773
- const tag = Service$5()(id, definition.binding, (value) => {
1856
+ const tag = Service$6()(id, definition.binding, (value) => {
1774
1857
  if (typeof value !== "object" || value === null) return false;
1775
1858
  const resource = value;
1776
1859
  return typeof resource.get === "function" && typeof resource.put === "function" && typeof resource.delete === "function" && typeof resource.getWithMetadata === "function" && typeof resource.list === "function";
@@ -1872,7 +1955,7 @@ const isWorkflow = (value) => {
1872
1955
  };
1873
1956
  const make$2 = (id, definition) => Service()(id, definition);
1874
1957
  const Service = () => (id, definition) => {
1875
- const tag = Service$5()(id, definition.binding, (value) => isWorkflow(value));
1958
+ const tag = Service$6()(id, definition.binding, (value) => isWorkflow(value));
1876
1959
  const encodePayload = Schema.encodeEffect(definition.payload);
1877
1960
  const decodeResult = Schema.decodeUnknownEffect(definition.result);
1878
1961
  const wrapInstance = (raw) => {
@@ -2056,4 +2139,4 @@ const waitForEvent = (name, options) => Effect.flatMap(WorkflowStep$1.asEffect()
2056
2139
  const Tag = Tag$1;
2057
2140
  const implement = implement$1;
2058
2141
  //#endregion
2059
- export { Binding_exports as Binding, DurableObject_exports as DurableObject, DurableObjectAlarm_exports as DurableObjectAlarm, DurableObjectDefinition_exports as DurableObjectDefinition, DurableObjectNamespace_exports as DurableObjectNamespace, DurableObjectRpcWebSocket_exports as DurableObjectRpcWebSocket, DurableObjectState_exports as DurableObjectState, DurableObjectStorage_exports as DurableObjectStorage, DurableObjectWebSocket_exports as DurableObjectWebSocket, Kv_exports as Kv, Queue_exports as Queue, QueueBinding_exports as QueueBinding, QueueDefinition_exports as QueueDefinition, Rpc_exports as Rpc, RpcDefinition_exports as RpcDefinition, ServiceBinding_exports as ServiceBinding, Worker_exports as Worker, WorkerConfig, WorkerDefinition_exports as WorkerDefinition, WorkerEnvironment, Workflow_exports as Workflow, WorkflowBinding_exports as WorkflowBinding, WorkflowDefinition_exports as WorkflowDefinition };
2142
+ export { Binding_exports as Binding, D1_exports as D1, DurableObject_exports as DurableObject, DurableObjectAlarm_exports as DurableObjectAlarm, DurableObjectDefinition_exports as DurableObjectDefinition, DurableObjectNamespace_exports as DurableObjectNamespace, DurableObjectRpcWebSocket_exports as DurableObjectRpcWebSocket, DurableObjectState_exports as DurableObjectState, DurableObjectStorage_exports as DurableObjectStorage, DurableObjectWebSocket_exports as DurableObjectWebSocket, Kv_exports as Kv, Queue_exports as Queue, QueueBinding_exports as QueueBinding, QueueDefinition_exports as QueueDefinition, Rpc_exports as Rpc, RpcDefinition_exports as RpcDefinition, ServiceBinding_exports as ServiceBinding, Worker_exports as Worker, WorkerConfig, WorkerDefinition_exports as WorkerDefinition, WorkerEnvironment, Workflow_exports as Workflow, WorkflowBinding_exports as WorkflowBinding, WorkflowDefinition_exports as WorkflowDefinition };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effect-cf",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Effect-native primitives for Cloudflare Workers and bindings.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,6 +36,7 @@
36
36
  "devDependencies": {
37
37
  "@cloudflare/vitest-pool-workers": "^0.15.0",
38
38
  "@cloudflare/workers-types": "^4.20260421.1",
39
+ "@effect/sql-d1": "catalog:",
39
40
  "@effect/vitest": "catalog:",
40
41
  "effect": "catalog:",
41
42
  "typescript": "^6.0.2",
@@ -43,6 +44,7 @@
43
44
  "vitest": "catalog:"
44
45
  },
45
46
  "peerDependencies": {
47
+ "@effect/sql-d1": "^4.0.0-beta.65",
46
48
  "effect": "^4.0.0-beta.65"
47
49
  }
48
50
  }
package/src/D1.ts ADDED
@@ -0,0 +1,135 @@
1
+ import { D1Client } from "@effect/sql-d1";
2
+ import { Data, Effect, Layer } from "effect";
3
+
4
+ import * as Binding from "./Binding";
5
+
6
+ const TypeId = "effect-cf/D1" as const;
7
+
8
+ /** Error raised when a D1 operation fails. */
9
+ export class D1OperationError extends Data.TaggedError("D1OperationError")<{
10
+ readonly binding: string;
11
+ readonly operation: string;
12
+ readonly cause: unknown;
13
+ }> {}
14
+
15
+ /** Typed D1 binding definition. */
16
+ export interface D1Definition {
17
+ /** Binding name as configured in `wrangler.jsonc`. */
18
+ readonly binding: string;
19
+ }
20
+
21
+ /** Options forwarded to `@effect/sql-d1` when building a SQL client layer. */
22
+ export type D1SqlLayerOptions = Omit<D1Client.D1ClientConfig, "db">;
23
+
24
+ declare const D1ServiceTypeId: unique symbol;
25
+
26
+ /** Nominal service marker for D1 services created with {@link make}. */
27
+ export interface D1Service<Id extends string> {
28
+ readonly [D1ServiceTypeId]: {
29
+ readonly id: Id;
30
+ };
31
+ }
32
+
33
+ const isD1Database = (value: unknown): value is D1Database => {
34
+ if (typeof value !== "object" || value === null) {
35
+ return false;
36
+ }
37
+
38
+ const resource = value as Record<string, unknown>;
39
+
40
+ return (
41
+ typeof resource.prepare === "function" &&
42
+ typeof resource.batch === "function" &&
43
+ typeof resource.exec === "function"
44
+ );
45
+ };
46
+
47
+ const d1Error = (binding: string, operation: string, cause: unknown) =>
48
+ new D1OperationError({ binding, operation, cause });
49
+
50
+ const tryD1Promise = <A>(
51
+ binding: string,
52
+ operation: string,
53
+ evaluate: () => Promise<A>,
54
+ ): Effect.Effect<A, D1OperationError> =>
55
+ Effect.tryPromise({
56
+ try: evaluate,
57
+ catch: (cause) => d1Error(binding, operation, cause),
58
+ });
59
+
60
+ const tryD1Sync = <A>(
61
+ binding: string,
62
+ operation: string,
63
+ evaluate: () => A,
64
+ ): Effect.Effect<A, D1OperationError> =>
65
+ Effect.try({
66
+ try: evaluate,
67
+ catch: (cause) => d1Error(binding, operation, cause),
68
+ });
69
+
70
+ /**
71
+ * Creates a typed D1 service tag plus Effect helpers.
72
+ */
73
+ export const make = <Id extends string>(id: Id, definition: D1Definition) =>
74
+ Service<D1Service<Id>>()(id, definition);
75
+
76
+ /**
77
+ * Builds a D1 service around a Cloudflare D1 database binding.
78
+ *
79
+ * The returned service exposes the raw `D1Database` binding, small native D1
80
+ * helpers, and `sqlLayer(...)` for providing `effect/unstable/sql` via
81
+ * `@effect/sql-d1`.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * class TodoDatabase extends D1.Service<TodoDatabase>()("TodoDatabase", {
86
+ * binding: "TODO_DB",
87
+ * }) {}
88
+ *
89
+ * const SqlLive = TodoDatabase.sqlLayer();
90
+ * ```
91
+ */
92
+ export const Service =
93
+ <Self>() =>
94
+ <Id extends string>(id: Id, definition: D1Definition) => {
95
+ const tag = Binding.Service<Self>()(id, definition.binding, isD1Database);
96
+
97
+ const sqlLayer = (options?: D1SqlLayerOptions) =>
98
+ Layer.unwrap(
99
+ Effect.gen(function* () {
100
+ const db = yield* tag;
101
+ return D1Client.layer({ ...options, db });
102
+ }),
103
+ ).pipe(Layer.provide(tag.layer));
104
+
105
+ const prepare = Effect.fnUntraced(function* (query: string) {
106
+ const db = yield* tag;
107
+ return yield* tryD1Sync(definition.binding, "prepare", () => db.prepare(query));
108
+ });
109
+
110
+ const batch = Effect.fnUntraced(function* <T = unknown>(
111
+ statements: ReadonlyArray<D1PreparedStatement>,
112
+ ) {
113
+ const db = yield* tag;
114
+ return yield* tryD1Promise(definition.binding, "batch", () => db.batch<T>([...statements]));
115
+ });
116
+
117
+ const exec = Effect.fnUntraced(function* (query: string) {
118
+ const db = yield* tag;
119
+ return yield* tryD1Promise(definition.binding, "exec", () => db.exec(query));
120
+ });
121
+
122
+ const unsafeRaw = Effect.fnUntraced(function* () {
123
+ return yield* tag;
124
+ });
125
+
126
+ return Object.assign(tag, {
127
+ [TypeId]: TypeId,
128
+ definition,
129
+ sqlLayer,
130
+ prepare,
131
+ batch,
132
+ exec,
133
+ unsafeRaw,
134
+ });
135
+ };
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export * as Binding from "./Binding";
2
+ export * as D1 from "./D1";
2
3
  export * as DurableObject from "./DurableObject";
3
4
  export * as DurableObjectAlarm from "./DurableObjectAlarm";
4
5
  export * as DurableObjectDefinition from "./DurableObjectDefinition";