diff --git a/.changeset/afraid-taxis-reflect.md b/.changeset/afraid-taxis-reflect.md new file mode 100644 index 00000000000..eb402043221 --- /dev/null +++ b/.changeset/afraid-taxis-reflect.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +Add Layer.setRandom, for over-riding the default Random service diff --git a/.changeset/calm-zebras-relate.md b/.changeset/calm-zebras-relate.md new file mode 100644 index 00000000000..9ffdcdf4838 --- /dev/null +++ b/.changeset/calm-zebras-relate.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +`Brand.unbranded` getter has been added diff --git a/.changeset/fast-garlics-burn.md b/.changeset/fast-garlics-burn.md new file mode 100644 index 00000000000..28f54f541ee --- /dev/null +++ b/.changeset/fast-garlics-burn.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +Add Record.findFirst diff --git a/.changeset/friendly-geese-boil.md b/.changeset/friendly-geese-boil.md new file mode 100644 index 00000000000..1fbd35fedb6 --- /dev/null +++ b/.changeset/friendly-geese-boil.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +Default `never` type has been added to `MutableHasMap.empty` & `MutableList.empty` ctors diff --git a/.changeset/funny-islands-relate.md b/.changeset/funny-islands-relate.md new file mode 100644 index 00000000000..5224b969bb1 --- /dev/null +++ b/.changeset/funny-islands-relate.md @@ -0,0 +1,18 @@ +--- +"effect": minor +--- + +add Stream.toAsyncIterable\* apis + +```ts +import { Stream } from "effect" + +// Will print: +// 1 +// 2 +// 3 +const stream = Stream.make(1, 2, 3) +for await (const result of Stream.toAsyncIterable(stream)) { + console.log(result) +} +``` diff --git a/.changeset/kind-poems-cough.md b/.changeset/kind-poems-cough.md new file mode 100644 index 00000000000..80f534b48a0 --- /dev/null +++ b/.changeset/kind-poems-cough.md @@ -0,0 +1,35 @@ +--- +"effect": minor +--- + +Simplified the creation of pipeable classes. + +```ts +class MyClass extends Pipeable.Class() { + constructor(public a: number) { + super() + } + methodA() { + return this.a + } +} +console.log(new MyClass(2).pipe((x) => x.methodA())) // 2 +``` + +```ts +class A { + constructor(public a: number) {} + methodA() { + return this.a + } +} +class B extends Pipeable.Class(A) { + constructor(private b: string) { + super(b.length) + } + methodB() { + return [this.b, this.methodA()] + } +} +console.log(new B("pipe").pipe((x) => x.methodB())) // ['pipe', 4] +``` diff --git a/.changeset/sharp-mirrors-vanish.md b/.changeset/sharp-mirrors-vanish.md new file mode 100644 index 00000000000..92bff6fae06 --- /dev/null +++ b/.changeset/sharp-mirrors-vanish.md @@ -0,0 +1,5 @@ +--- +"@effect/sql-drizzle": minor +--- + +Export `layerWithConfig` method to build a layer with some Drizzle config diff --git a/.changeset/sixty-ducks-tie.md b/.changeset/sixty-ducks-tie.md new file mode 100644 index 00000000000..07e4000bfd0 --- /dev/null +++ b/.changeset/sixty-ducks-tie.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +property `message: string` has been added to `ConfigError.And` & `Or` members diff --git a/.changeset/sweet-maps-return.md b/.changeset/sweet-maps-return.md new file mode 100644 index 00000000000..7684f514d4c --- /dev/null +++ b/.changeset/sweet-maps-return.md @@ -0,0 +1,5 @@ +--- +"effect": minor +--- + +allow catching multiple different tags in Effect.catchTag diff --git a/.changeset/wild-melons-scream.md b/.changeset/wild-melons-scream.md new file mode 100644 index 00000000000..799950da6c6 --- /dev/null +++ b/.changeset/wild-melons-scream.md @@ -0,0 +1,5 @@ +--- +"@effect/platform": minor +--- + +Allow removing multiple Headers diff --git a/packages/effect/dtslint/Record.tst.ts b/packages/effect/dtslint/Record.tst.ts index 2589282e59c..81071322078 100644 --- a/packages/effect/dtslint/Record.tst.ts +++ b/packages/effect/dtslint/Record.tst.ts @@ -589,4 +589,15 @@ describe("Record", () => { expect(Record.intersection(string$structAB, { b: 2 }, (a, _) => a)) .type.toBe>() }) + + it("findFirst", () => { + expect(Record.findFirst(string$numbersOrStrings, (a, _) => predicateNumbersOrStrings(a))) + .type.toBe>() + expect(pipe(string$numbersOrStrings, Record.findFirst((a, _) => predicateNumbersOrStrings(a)))) + .type.toBe>() + expect(Record.findFirst(string$numbersOrStrings, Predicate.isString)) + .type.toBe>() + expect(pipe(string$numbersOrStrings, Record.findFirst(Predicate.isString))) + .type.toBe>() + }) }) diff --git a/packages/effect/src/Brand.ts b/packages/effect/src/Brand.ts index 1b597b9351c..e6576dea094 100644 --- a/packages/effect/src/Brand.ts +++ b/packages/effect/src/Brand.ts @@ -18,7 +18,7 @@ */ import * as Arr from "./Array.js" import * as Either from "./Either.js" -import { identity } from "./Function.js" +import { identity, unsafeCoerce } from "./Function.js" import * as Option from "./Option.js" import type { Predicate } from "./Predicate.js" import type * as Types from "./Types.js" @@ -350,3 +350,11 @@ export const all: , ...Array Either.isRight(either(args)) }) } + +/** + * Retrieves the unbranded value from a `Brand` instance. + * + * @since 3.15.0 + * @category getters + */ +export const unbranded: >(branded: A) => Brand.Unbranded = unsafeCoerce diff --git a/packages/effect/src/ConfigError.ts b/packages/effect/src/ConfigError.ts index ee84fb23b15..df6819a4ee1 100644 --- a/packages/effect/src/ConfigError.ts +++ b/packages/effect/src/ConfigError.ts @@ -76,6 +76,7 @@ export interface And extends ConfigError.Proto { readonly _op: "And" readonly left: ConfigError readonly right: ConfigError + readonly message: string } /** @@ -86,6 +87,7 @@ export interface Or extends ConfigError.Proto { readonly _op: "Or" readonly left: ConfigError readonly right: ConfigError + readonly message: string } /** diff --git a/packages/effect/src/Effect.ts b/packages/effect/src/Effect.ts index 795b5e70194..3b524e4bb4e 100644 --- a/packages/effect/src/Effect.ts +++ b/packages/effect/src/Effect.ts @@ -3861,15 +3861,13 @@ export const catchSomeDefect: { * @category Error handling */ export const catchTag: { - ( - k: K, - f: (e: NoInfer>) => Effect - ): (self: Effect) => Effect, R1 | R> - ( + , A1, E1, R1>( + ...args: [...tags: K, f: (e: Extract, { _tag: K[number] }>) => Effect] + ): (self: Effect) => Effect | E1, R | R1> + , R1, E1, A1>( self: Effect, - k: K, - f: (e: Extract) => Effect - ): Effect, R | R1> + ...args: [...tags: K, f: (e: Extract, { _tag: K[number] }>) => Effect] + ): Effect | E1, R | R1> } = effect.catchTag /** diff --git a/packages/effect/src/Layer.ts b/packages/effect/src/Layer.ts index 7bbee0d1b5c..df8fea56d2d 100644 --- a/packages/effect/src/Layer.ts +++ b/packages/effect/src/Layer.ts @@ -32,9 +32,11 @@ import * as fiberRuntime from "./internal/fiberRuntime.js" import * as internal from "./internal/layer.js" import * as circularLayer from "./internal/layer/circular.js" import * as query from "./internal/query.js" +import { randomTag } from "./internal/random.js" import type { LogLevel } from "./LogLevel.js" import type * as Option from "./Option.js" import type { Pipeable } from "./Pipeable.js" +import type * as Random from "./Random.js" import type * as Request from "./Request.js" import type * as Runtime from "./Runtime.js" import type * as Schedule from "./Schedule.js" @@ -945,6 +947,15 @@ export const setConfigProvider: (configProvider: ConfigProvider) => Layer */ export const parentSpan: (span: Tracer.AnySpan) => Layer = circularLayer.parentSpan +/** + * @since 3.15.0 + * @category Random + */ +export const setRandom = (random: A): Layer => + scopedDiscard( + fiberRuntime.fiberRefLocallyScopedWith(defaultServices.currentServices, Context.add(randomTag, random)) + ) + /** * @since 2.0.0 * @category requests & batching diff --git a/packages/effect/src/MutableHashMap.ts b/packages/effect/src/MutableHashMap.ts index af8c15ba76c..3a53cb39ede 100644 --- a/packages/effect/src/MutableHashMap.ts +++ b/packages/effect/src/MutableHashMap.ts @@ -102,7 +102,7 @@ class BucketIterator implements Iterator<[K, V]> { * @since 2.0.0 * @category constructors */ -export const empty = (): MutableHashMap => { +export const empty = (): MutableHashMap => { const self = Object.create(MutableHashMapProto) self.referential = new Map() self.buckets = new Map() diff --git a/packages/effect/src/MutableList.ts b/packages/effect/src/MutableList.ts index 1027aaaa5c6..be61d233ed6 100644 --- a/packages/effect/src/MutableList.ts +++ b/packages/effect/src/MutableList.ts @@ -97,7 +97,7 @@ const makeNode = (value: T): LinkedListNode => ({ * @since 2.0.0 * @category constructors */ -export const empty = (): MutableList => { +export const empty = (): MutableList => { const list = Object.create(MutableListProto) list.head = undefined list.tail = undefined diff --git a/packages/effect/src/Pipeable.ts b/packages/effect/src/Pipeable.ts index c41c7428b0f..2b54e6c9657 100644 --- a/packages/effect/src/Pipeable.ts +++ b/packages/effect/src/Pipeable.ts @@ -2,9 +2,11 @@ * @since 2.0.0 */ +import type { Ctor } from "./Types.js" + /** * @since 2.0.0 - * @category models + * @category Models */ export interface Pipeable { pipe(this: A): A @@ -522,3 +524,43 @@ export const pipeArguments = (self: A, args: IArguments): unknown => { } } } + +/** + * @since 3.15.0 + * @category Models + */ +export interface PipeableConstructor { + new(...args: Array): Pipeable +} + +/** + * @since 3.15.0 + * @category Prototypes + */ +export const Prototype: Pipeable = { + pipe() { + return pipeArguments(this, arguments) + } +} + +const Base: PipeableConstructor = (function() { + function PipeableBase() {} + PipeableBase.prototype = Prototype + return PipeableBase as any +})() + +/** + * @since 3.15.0 + * @category Constructors + */ +export const Class: { + (): PipeableConstructor + (klass: TBase): TBase & PipeableConstructor +} = (klass?: Ctor) => + klass ? + class extends klass { + pipe() { + return pipeArguments(this, arguments) + } + } + : Base diff --git a/packages/effect/src/Record.ts b/packages/effect/src/Record.ts index 201d6f6ca32..93bead7c91a 100644 --- a/packages/effect/src/Record.ts +++ b/packages/effect/src/Record.ts @@ -1227,3 +1227,47 @@ export const getEquivalence = ( export const singleton = (key: K, value: A): Record => ({ [key]: value } as any) + +/** + * Returns the first entry that satisfies the specified + * predicate, or `None` if no such entry exists. + * + * @example + * ```ts + * import { Record, Option } from "effect" + * + * const record = { a: 1, b: 2, c: 3 } + * const result = Record.findFirst(record, (value, key) => value > 1 && key !== "b") + * console.log(result) // Option.Some(["c", 3]) + * ``` + * + * @category elements + * @since 3.14.0 + */ +export const findFirst: { + ( + refinement: (value: NoInfer, key: NoInfer) => value is V2 + ): (self: Record) => Option.Option<[K, V2]> + ( + predicate: (value: NoInfer, key: NoInfer) => boolean + ): (self: Record) => Option.Option<[K, V]> + ( + self: Record, + refinement: (value: NoInfer, key: NoInfer) => value is V2 + ): Option.Option<[K, V2]> + ( + self: Record, + predicate: (value: NoInfer, key: NoInfer) => boolean + ): Option.Option<[K, V]> +} = dual( + 2, + (self: Record, f: (value: V, key: K) => boolean) => { + for (const a of Object.entries(self)) { + const o = f(a[1], a[0] as K) + if (o) { + return Option.some(a) + } + } + return Option.none() + } +) diff --git a/packages/effect/src/Stream.ts b/packages/effect/src/Stream.ts index 6f65703d8f4..34444aa2ae8 100644 --- a/packages/effect/src/Stream.ts +++ b/packages/effect/src/Stream.ts @@ -5332,6 +5332,34 @@ export const toReadableStreamRuntime: { ): ReadableStream } = internal.toReadableStreamRuntime +/** + * Converts the stream to a `AsyncIterable` using the provided runtime. + * + * @since 3.15.0 + * @category destructors + */ +export const toAsyncIterableRuntime: { + (runtime: Runtime): (self: Stream) => AsyncIterable + (self: Stream, runtime: Runtime): AsyncIterable +} = internal.toAsyncIterableRuntime + +/** + * Converts the stream to a `AsyncIterable` capturing the required dependencies. + * + * @since 3.15.0 + * @category destructors + */ +export const toAsyncIterableEffect: (self: Stream) => Effect.Effect, never, R> = + internal.toAsyncIterableEffect + +/** + * Converts the stream to a `AsyncIterable`. + * + * @since 3.15.0 + * @category destructors + */ +export const toAsyncIterable: (self: Stream) => AsyncIterable = internal.toAsyncIterable + /** * Applies the transducer to the stream and emits its outputs. * diff --git a/packages/effect/src/Types.ts b/packages/effect/src/Types.ts index 0a989174d37..3e17e662be8 100644 --- a/packages/effect/src/Types.ts +++ b/packages/effect/src/Types.ts @@ -336,3 +336,8 @@ export type NotFunction = T extends Function ? never : T * @since 3.9.0 */ export type NoExcessProperties = T & { readonly [K in Exclude]: never } + +/** + * @since 3.15.0 + */ +export type Ctor = new(...args: Array) => T diff --git a/packages/effect/src/internal/configError.ts b/packages/effect/src/internal/configError.ts index 6d8e6d1944b..436b1075e21 100644 --- a/packages/effect/src/internal/configError.ts +++ b/packages/effect/src/internal/configError.ts @@ -32,6 +32,12 @@ export const And = (self: ConfigError.ConfigError, that: ConfigError.ConfigError return `${this.left} and ${this.right}` } }) + Object.defineProperty(error, "message", { + enumerable: false, + get(this: ConfigError.And) { + return this.toString() + } + }) return error } @@ -47,6 +53,12 @@ export const Or = (self: ConfigError.ConfigError, that: ConfigError.ConfigError) return `${this.left} or ${this.right}` } }) + Object.defineProperty(error, "message", { + enumerable: false, + get(this: ConfigError.Or) { + return this.toString() + } + }) return error } diff --git a/packages/effect/src/internal/core-effect.ts b/packages/effect/src/internal/core-effect.ts index 52ad8c7df8f..83e9bba8d10 100644 --- a/packages/effect/src/internal/core-effect.ts +++ b/packages/effect/src/internal/core-effect.ts @@ -237,21 +237,36 @@ export const catchSomeDefect = dual< /* @internal */ export const catchTag = dual< - ( - k: K, - f: (e: Extract) => Effect.Effect - ) => (self: Effect.Effect) => Effect.Effect | E1, R | R1>, - ( + , A1, E1, R1>( + ...args: [...tags: K, f: (e: Extract, { _tag: K[number] }>) => Effect.Effect] + ) => (self: Effect.Effect) => Effect.Effect | E1, R | R1>, + , R1, E1, A1>( self: Effect.Effect, - k: K, - f: (e: Extract) => Effect.Effect - ) => Effect.Effect | E1, R | R1> ->(3, ( - self: Effect.Effect, - k: K, - f: (e: Extract) => Effect.Effect -): Effect.Effect | E1, R | R1> => - core.catchIf(self, Predicate.isTagged(k) as Predicate.Refinement>, f) as any) + ...args: [...tags: K, f: (e: Extract, { _tag: K[number] }>) => Effect.Effect] + ) => Effect.Effect | E1, R | R1> +>( + (args: any) => core.isEffect(args[0]), + , R1, E1, A1>( + self: Effect.Effect, + ...args: [...tags: K, f: (e: Extract, { _tag: K[number] }>) => Effect.Effect] + ): Effect.Effect | E1, R | R1> => { + const f = args[args.length - 1] as any + let predicate: Predicate.Predicate + if (args.length === 2) { + predicate = Predicate.isTagged(args[0] as string) + } else { + predicate = (e) => { + const tag = Predicate.hasProperty(e, "_tag") ? e["_tag"] : undefined + if (!tag) return false + for (let i = 0; i < args.length - 1; i++) { + if (args[i] === tag) return true + } + return false + } + } + return core.catchIf(self, predicate as Predicate.Refinement>, f) as any + } +) /** @internal */ export const catchTags: { diff --git a/packages/effect/src/internal/stream.ts b/packages/effect/src/internal/stream.ts index 9d5d22f4db0..4da8fcaba7a 100644 --- a/packages/effect/src/internal/stream.ts +++ b/packages/effect/src/internal/stream.ts @@ -7200,6 +7200,74 @@ export const transduce = dual< } ) +/** @internal */ +export const toAsyncIterableRuntime = dual< + ( + runtime: Runtime.Runtime + ) => (self: Stream.Stream) => AsyncIterable, + ( + self: Stream.Stream, + runtime: Runtime.Runtime + ) => AsyncIterable +>( + (args) => isStream(args[0]), + ( + self: Stream.Stream, + runtime: Runtime.Runtime + ): AsyncIterable => { + const runFork = Runtime.runFork(runtime) + return { + [Symbol.asyncIterator]() { + let currentResolve: ((value: IteratorResult) => void) | undefined = undefined + let currentReject: ((reason: any) => void) | undefined = undefined + let fiber: Fiber.RuntimeFiber | undefined = undefined + const latch = Effect.unsafeMakeLatch(false) + return { + next() { + if (!fiber) { + fiber = runFork(runForEach(self, (value) => + latch.whenOpen(Effect.sync(() => { + latch.unsafeClose() + currentResolve!({ done: false, value }) + currentResolve = currentReject = undefined + })))) + fiber.addObserver((exit) => { + fiber = Effect.runFork(latch.whenOpen(Effect.sync(() => { + if (exit._tag === "Failure") { + currentReject!(Cause.squash(exit.cause)) + } else { + currentResolve!({ done: true, value: void 0 }) + } + currentResolve = currentReject = undefined + }))) + }) + } + return new Promise>((resolve, reject) => { + currentResolve = resolve + currentReject = reject + latch.unsafeOpen() + }) + }, + return() { + if (!fiber) return Promise.resolve({ done: true, value: void 0 }) + return Effect.runPromise(Effect.as(Fiber.interrupt(fiber), { done: true, value: void 0 })) + } + } + } + } + } +) + +/** @internal */ +export const toAsyncIterable = (self: Stream.Stream): AsyncIterable => + toAsyncIterableRuntime(self, Runtime.defaultRuntime) + +/** @internal */ +export const toAsyncIterableEffect = ( + self: Stream.Stream +): Effect.Effect, never, R> => + Effect.map(Effect.runtime(), (runtime) => toAsyncIterableRuntime(self, runtime)) + /** @internal */ export const unfold = (s: S, f: (s: S) => Option.Option): Stream.Stream => unfoldChunk(s, (s) => pipe(f(s), Option.map(([a, s]) => [Chunk.of(a), s]))) diff --git a/packages/effect/test/Config.test.ts b/packages/effect/test/Config.test.ts index f10aba95881..275afa24e2a 100644 --- a/packages/effect/test/Config.test.ts +++ b/packages/effect/test/Config.test.ts @@ -632,4 +632,20 @@ describe("Config", () => { ) deepStrictEqual(result, [1, 2, 3]) }) + + it("ConfigError message", () => { + const missingData = ConfigError.MissingData(["PATH"], "missing PATH") + const invalidData = ConfigError.InvalidData(["PATH1"], "invalid PATH1") + const andError = ConfigError.And(missingData, invalidData) + const orError = ConfigError.Or(missingData, invalidData) + + strictEqual( + andError.message, + "(Missing data at PATH: \"missing PATH\") and (Invalid data at PATH1: \"invalid PATH1\")" + ) + strictEqual( + orError.message, + "(Missing data at PATH: \"missing PATH\") or (Invalid data at PATH1: \"invalid PATH1\")" + ) + }) }) diff --git a/packages/effect/test/Pipeable.test.ts b/packages/effect/test/Pipeable.test.ts index ccf33d25ded..4fd08b06587 100644 --- a/packages/effect/test/Pipeable.test.ts +++ b/packages/effect/test/Pipeable.test.ts @@ -1,6 +1,6 @@ import { describe, it } from "@effect/vitest" -import { assertSome } from "@effect/vitest/utils" -import { Option } from "effect" +import { assertInstanceOf, assertSome, deepStrictEqual } from "@effect/vitest/utils" +import { Option, Pipeable } from "effect" describe("Pipeable", () => { it("pipeArguments", () => { @@ -70,4 +70,42 @@ describe("Pipeable", () => { 126 ) }) + it("pipeable", () => { + class A { + constructor(public a: number) {} + methodA() { + return this.a + } + } + class B extends Pipeable.Class(A) { + constructor(private b: string) { + super(b.length) + } + methodB() { + return [this.b, this.methodA()] + } + } + const b = new B("bb") + + assertInstanceOf(b, A) + assertInstanceOf(b, B) + deepStrictEqual(b.methodB(), ["bb", 2]) + deepStrictEqual(b.pipe((x) => x.methodB()), ["bb", 2]) + }) + it("Class", () => { + class A extends Pipeable.Class() { + constructor(public a: number) { + super() + } + methodA() { + return this.a + } + } + const a = new A(2) + + assertInstanceOf(a, A) + assertInstanceOf(a, Pipeable.Class()) + deepStrictEqual(a.methodA(), 2) + deepStrictEqual(a.pipe((x) => x.methodA()), 2) + }) }) diff --git a/packages/effect/test/Record.test.ts b/packages/effect/test/Record.test.ts index 3df42bf9579..c9a3e6f9b7b 100644 --- a/packages/effect/test/Record.test.ts +++ b/packages/effect/test/Record.test.ts @@ -373,5 +373,31 @@ describe("Record", () => { it("mapEntries", () => { deepStrictEqual(pipe(stringRecord, Record.mapEntries((a, key) => [key.toUpperCase(), a + 1])), { A: 2 }) }) + + describe("findFirst", () => { + it("refinement/predicate", () => { + const record = { + a: 1, + b: 2, + c: 1 + } + deepStrictEqual( + pipe(record, Record.findFirst((v) => v < 2)), + Option.some(["a", 1]) + ) + deepStrictEqual( + pipe(record, Record.findFirst((v, k) => v < 2 && k !== "a")), + Option.some(["c", 1]) + ) + deepStrictEqual( + pipe(record, Record.findFirst((v) => v > 2)), + Option.none() + ) + deepStrictEqual( + Record.findFirst(record, (v) => v < 2), + Option.some(["a", 1]) + ) + }) + }) }) }) diff --git a/packages/effect/test/Stream/conversions.test.ts b/packages/effect/test/Stream/conversions.test.ts index b53cf6a6c1b..e1e23c992fb 100644 --- a/packages/effect/test/Stream/conversions.test.ts +++ b/packages/effect/test/Stream/conversions.test.ts @@ -73,4 +73,13 @@ describe("Stream", () => { ) deepStrictEqual(queue, Exit.die(new Cause.RuntimeException("die"))) })) + + it("toAsyncIterable", async () => { + const stream = Stream.make(1, 2, 3) + const results: Array = [] + for await (const result of Stream.toAsyncIterable(stream)) { + results.push(result) + } + deepStrictEqual(results, [1, 2, 3]) + }) }) diff --git a/packages/platform/src/Headers.ts b/packages/platform/src/Headers.ts index 406a7ea279f..2182fdad01f 100644 --- a/packages/platform/src/Headers.ts +++ b/packages/platform/src/Headers.ts @@ -197,14 +197,34 @@ export const merge: { * @category combinators */ export const remove: { - (key: string): (self: Headers) => Headers - (self: Headers, key: string): Headers + (key: string | RegExp | ReadonlyArray): (self: Headers) => Headers + (self: Headers, key: string | RegExp | ReadonlyArray): Headers } = dual< - (key: string) => (self: Headers) => Headers, - (self: Headers, key: string) => Headers + (key: string | RegExp | ReadonlyArray) => (self: Headers) => Headers, + (self: Headers, key: string | RegExp | ReadonlyArray) => Headers >(2, (self, key) => { const out = make(self) - delete out[key.toLowerCase()] + const modify = (key: string | RegExp) => { + if (typeof key === "string") { + const k = key.toLowerCase() + if (k in self) { + delete out[k] + } + } else { + for (const name in self) { + if (key.test(name)) { + delete out[name] + } + } + } + } + if (Array.isArray(key)) { + for (let i = 0; i < key.length; i++) { + modify(key[i]) + } + } else { + modify(key as string | RegExp) + } return out }) diff --git a/packages/platform/test/Headers.test.ts b/packages/platform/test/Headers.test.ts index 7e365a1a9e8..75fecfe23ab 100644 --- a/packages/platform/test/Headers.test.ts +++ b/packages/platform/test/Headers.test.ts @@ -151,4 +151,58 @@ describe("Headers", () => { }) }) }) + + describe("remove", () => { + it("one key", () => { + const headers = Headers.fromInput({ + "Content-Type": "application/json", + "Authorization": "Bearer some-token", + "X-Api-Key": "some-key" + }) + + const removed = Headers.remove(headers, "Authorization") + + deepStrictEqual( + removed, + Headers.fromInput({ + "content-type": "application/json", + "x-api-key": "some-key" + }) + ) + }) + + it("multiple keys", () => { + const headers = Headers.fromInput({ + "Content-Type": "application/json", + "Authorization": "Bearer some-token", + "X-Api-Key": "some-key" + }) + + const removed = Headers.remove(headers, ["Authorization", "authorization", "X-Api-Token", "x-api-key"]) + + deepStrictEqual( + removed, + Headers.fromInput({ + "content-type": "application/json" + }) + ) + }) + + it("RegExp", () => { + const headers = Headers.fromInput({ + "Authorization": "Bearer some-token", + "sec-ret": "some", + "sec-ret-2": "some" + }) + + const removed = Headers.remove(headers, [/^sec-/]) + + deepStrictEqual( + removed, + Headers.fromInput({ + "authorization": "Bearer some-token" + }) + ) + }) + }) }) diff --git a/packages/sql-drizzle/package.json b/packages/sql-drizzle/package.json index 2e6f38dafc6..314dbc3800f 100644 --- a/packages/sql-drizzle/package.json +++ b/packages/sql-drizzle/package.json @@ -59,12 +59,12 @@ "@effect/sql-sqlite-node": "workspace:^", "@testcontainers/mysql": "^10.18.0", "@testcontainers/postgresql": "^10.18.0", - "drizzle-orm": "^0.31.0", + "drizzle-orm": "^0.42.0", "effect": "workspace:^" }, "peerDependencies": { "@effect/sql": "workspace:^", - "drizzle-orm": "^0.31", + "drizzle-orm": "^0.42", "effect": "workspace:^" } } diff --git a/packages/sql-drizzle/src/Mysql.ts b/packages/sql-drizzle/src/Mysql.ts index bad13c8d4bb..f14f23724fb 100644 --- a/packages/sql-drizzle/src/Mysql.ts +++ b/packages/sql-drizzle/src/Mysql.ts @@ -3,6 +3,7 @@ */ import * as Client from "@effect/sql/SqlClient" import type { SqlError } from "@effect/sql/SqlError" +import type { DrizzleConfig } from "drizzle-orm" import { MySqlSelectBase } from "drizzle-orm/mysql-core" import type { MySqlRemoteDatabase } from "drizzle-orm/mysql-proxy" import { drizzle } from "drizzle-orm/mysql-proxy" @@ -23,6 +24,20 @@ export const make: Effect.Effect = return db }) +/** + * @since 1.0.0 + * @category constructors + */ +export const makeWithConfig: (config: DrizzleConfig) => Effect.Effect = ( + config +) => + Effect.gen(function*() { + const client = yield* Client.SqlClient + const db = drizzle(yield* makeRemoteCallback, config) + registerDialect((db as any).dialect, client) + return db + }) + /** * @since 1.0.0 * @category tags @@ -38,6 +53,14 @@ export class MysqlDrizzle extends Context.Tag("@effect/sql-drizzle/Mysql")< */ export const layer: Layer.Layer = Layer.effect(MysqlDrizzle, make) +/** + * @since 1.0.0 + * @category layers + */ +export const layerWithConfig: (config: DrizzleConfig) => Layer.Layer = ( + config +) => Layer.effect(MysqlDrizzle, makeWithConfig(config)) + // patch declare module "drizzle-orm" { diff --git a/packages/sql-drizzle/src/Pg.ts b/packages/sql-drizzle/src/Pg.ts index f55fed1c738..b4d471e0ded 100644 --- a/packages/sql-drizzle/src/Pg.ts +++ b/packages/sql-drizzle/src/Pg.ts @@ -3,6 +3,7 @@ */ import * as Client from "@effect/sql/SqlClient" import type { SqlError } from "@effect/sql/SqlError" +import type { DrizzleConfig } from "drizzle-orm" import { PgSelectBase } from "drizzle-orm/pg-core" import { drizzle } from "drizzle-orm/pg-proxy" import type { PgRemoteDatabase } from "drizzle-orm/pg-proxy" @@ -23,6 +24,20 @@ export const make: Effect.Effect = Ef return db }) +/** + * @since 1.0.0 + * @category constructors + */ +export const makeWithConfig: (config: DrizzleConfig) => Effect.Effect = ( + config +) => + Effect.gen(function*() { + const client = yield* Client.SqlClient + const db = drizzle(yield* makeRemoteCallback, config) + registerDialect((db as any).dialect, client) + return db + }) + /** * @since 1.0.0 * @category tags @@ -38,6 +53,13 @@ export class PgDrizzle extends Context.Tag("@effect/sql-drizzle/Pg")< */ export const layer: Layer.Layer = Layer.effect(PgDrizzle, make) +/** + * @since 1.0.0 + * @category layers + */ +export const layerWithConfig: (config: DrizzleConfig) => Layer.Layer = (config) => + Layer.effect(PgDrizzle, makeWithConfig(config)) + // patch declare module "drizzle-orm" { diff --git a/packages/sql-drizzle/src/Sqlite.ts b/packages/sql-drizzle/src/Sqlite.ts index 75a549fb2d1..e7408dee937 100644 --- a/packages/sql-drizzle/src/Sqlite.ts +++ b/packages/sql-drizzle/src/Sqlite.ts @@ -3,6 +3,7 @@ */ import * as Client from "@effect/sql/SqlClient" import type { SqlError } from "@effect/sql/SqlError" +import type { DrizzleConfig } from "drizzle-orm" import { QueryPromise } from "drizzle-orm/query-promise" import { SQLiteSelectBase } from "drizzle-orm/sqlite-core" import type { SqliteRemoteDatabase } from "drizzle-orm/sqlite-proxy" @@ -24,6 +25,20 @@ export const make: Effect.Effect Effect.Effect = ( + config +) => + Effect.gen(function*() { + const client = yield* Client.SqlClient + const db = drizzle(yield* makeRemoteCallback, config) + registerDialect((db as any).dialect, client) + return db + }) + /** * @since 1.0.0 * @category tags @@ -39,6 +54,14 @@ export class SqliteDrizzle extends Context.Tag("@effect/sql-drizzle/Sqlite")< */ export const layer: Layer.Layer = Layer.scoped(SqliteDrizzle, make) +/** + * @since 1.0.0 + * @category layers + */ +export const layerWithConfig: (config: DrizzleConfig) => Layer.Layer = ( + config +) => Layer.effect(SqliteDrizzle, makeWithConfig(config)) + // patch declare module "drizzle-orm" { diff --git a/packages/sql-drizzle/src/internal/patch.ts b/packages/sql-drizzle/src/internal/patch.ts index 7a0b9ceb831..5376674c91d 100644 --- a/packages/sql-drizzle/src/internal/patch.ts +++ b/packages/sql-drizzle/src/internal/patch.ts @@ -73,6 +73,8 @@ export const makeRemoteCallback = Effect.gen(function*() { : statement.withoutTransform if (method === "get") { effect = Effect.map(effect, (rows) => rows[0] ?? []) + } else if (method === "execute") { + effect = Effect.map(effect, (rows) => rows.length ? rows : [{}]) } return runPromise(Effect.map(effect, (rows) => ({ rows }))) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 54ddbc3b538..3492fb72cdd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -675,8 +675,8 @@ importers: specifier: ^10.18.0 version: 10.18.0 drizzle-orm: - specifier: ^0.31.0 - version: 0.31.4(@cloudflare/workers-types@4.20250204.0)(@libsql/client@0.14.0)(@op-engineering/op-sqlite@11.4.7(react-native@0.77.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@react-native-community/cli-server-api@15.1.3)(react@19.0.0))(react@19.0.0))(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.12)(better-sqlite3@11.8.1)(bun-types@1.2.2)(kysely@0.27.5)(mysql2@3.12.0)(postgres@3.4.5)(react@19.0.0) + specifier: ^0.42.0 + version: 0.42.0(@cloudflare/workers-types@4.20250204.0)(@libsql/client@0.14.0)(@op-engineering/op-sqlite@11.4.7(react-native@0.77.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@react-native-community/cli-server-api@15.1.3)(react@19.0.0))(react@19.0.0))(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.12)(better-sqlite3@11.8.1)(bun-types@1.2.2)(kysely@0.27.5)(mysql2@3.12.0)(postgres@3.4.5) effect: specifier: workspace:^ version: link:../effect @@ -4325,35 +4325,35 @@ packages: resolution: {integrity: sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==} engines: {node: '>=10'} - drizzle-orm@0.31.4: - resolution: {integrity: sha512-VGD9SH9aStF2z4QOTnVlVX/WghV/EnuEzTmsH3fSVp2E4fFgc8jl3viQrS/XUJx1ekW4rVVLJMH42SfGQdjX3Q==} + drizzle-orm@0.42.0: + resolution: {integrity: sha512-pS8nNJm2kBNZwrOjTHJfdKkaU+KuUQmV/vk5D57NojDq4FG+0uAYGMulXtYT///HfgsMF0hnFFvu1ezI3OwOkg==} peerDependencies: '@aws-sdk/client-rds-data': '>=3' - '@cloudflare/workers-types': '>=3' - '@electric-sql/pglite': '>=0.1.1' - '@libsql/client': '*' - '@neondatabase/serverless': '>=0.1' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' '@op-engineering/op-sqlite': '>=2' '@opentelemetry/api': ^1.4.1 - '@planetscale/database': '>=1' + '@planetscale/database': '>=1.13' '@prisma/client': '*' '@tidbcloud/serverless': '*' '@types/better-sqlite3': '*' '@types/pg': '*' - '@types/react': '>=18' '@types/sql.js': '*' '@vercel/postgres': '>=0.8.0' '@xata.io/client': '*' better-sqlite3: '>=7' bun-types: '*' - expo-sqlite: '>=13.2.0' + expo-sqlite: '>=14.0.0' + gel: '>=2' knex: '*' kysely: '*' mysql2: '>=2' pg: '>=8' postgres: '>=3' prisma: '*' - react: '>=18' sql.js: '>=1' sqlite3: '>=5' peerDependenciesMeta: @@ -4365,6 +4365,8 @@ packages: optional: true '@libsql/client': optional: true + '@libsql/client-wasm': + optional: true '@neondatabase/serverless': optional: true '@op-engineering/op-sqlite': @@ -4381,8 +4383,6 @@ packages: optional: true '@types/pg': optional: true - '@types/react': - optional: true '@types/sql.js': optional: true '@vercel/postgres': @@ -4395,6 +4395,8 @@ packages: optional: true expo-sqlite: optional: true + gel: + optional: true knex: optional: true kysely: @@ -4407,8 +4409,6 @@ packages: optional: true prisma: optional: true - react: - optional: true sql.js: optional: true sqlite3: @@ -12194,7 +12194,7 @@ snapshots: dotenv@8.6.0: {} - drizzle-orm@0.31.4(@cloudflare/workers-types@4.20250204.0)(@libsql/client@0.14.0)(@op-engineering/op-sqlite@11.4.7(react-native@0.77.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@react-native-community/cli-server-api@15.1.3)(react@19.0.0))(react@19.0.0))(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.12)(better-sqlite3@11.8.1)(bun-types@1.2.2)(kysely@0.27.5)(mysql2@3.12.0)(postgres@3.4.5)(react@19.0.0): + drizzle-orm@0.42.0(@cloudflare/workers-types@4.20250204.0)(@libsql/client@0.14.0)(@op-engineering/op-sqlite@11.4.7(react-native@0.77.1(@babel/core@7.26.10)(@babel/preset-env@7.26.9(@babel/core@7.26.10))(@react-native-community/cli-server-api@15.1.3)(react@19.0.0))(react@19.0.0))(@opentelemetry/api@1.9.0)(@types/better-sqlite3@7.6.12)(better-sqlite3@11.8.1)(bun-types@1.2.2)(kysely@0.27.5)(mysql2@3.12.0)(postgres@3.4.5): optionalDependencies: '@cloudflare/workers-types': 4.20250204.0 '@libsql/client': 0.14.0 @@ -12206,7 +12206,6 @@ snapshots: kysely: 0.27.5 mysql2: 3.12.0 postgres: 3.4.5 - react: 19.0.0 eastasianwidth@0.2.0: {}