diff --git a/.changeset/unlucky-dingos-search.md b/.changeset/unlucky-dingos-search.md
new file mode 100644
index 00000000000..a0a911259f8
--- /dev/null
+++ b/.changeset/unlucky-dingos-search.md
@@ -0,0 +1,6 @@
+---
+"@effect/platform-node": minor
+"@effect/platform": minor
+---
+
+support array of values in /platform url param schemas
diff --git a/packages/platform-node/test/HttpServer.test.ts b/packages/platform-node/test/HttpServer.test.ts
index 69c8bf19277..ee622e45969 100644
--- a/packages/platform-node/test/HttpServer.test.ts
+++ b/packages/platform-node/test/HttpServer.test.ts
@@ -64,6 +64,7 @@ describe("HttpServer", () => {
const part = formData.file
assert(typeof part !== "string")
const file = part[0]
+ assert(typeof file !== "string")
expect(file.path.endsWith("/test.txt")).toEqual(true)
expect(file.contentType).toEqual("text/plain")
return yield* HttpServerResponse.json({ ok: "file" in formData })
diff --git a/packages/platform/src/HttpIncomingMessage.ts b/packages/platform/src/HttpIncomingMessage.ts
index c59d3f8d1ce..9d37cbc17f6 100644
--- a/packages/platform/src/HttpIncomingMessage.ts
+++ b/packages/platform/src/HttpIncomingMessage.ts
@@ -13,7 +13,7 @@ import type { ParseOptions } from "effect/SchemaAST"
import type * as Stream from "effect/Stream"
import * as FileSystem from "./FileSystem.js"
import type * as Headers from "./Headers.js"
-import type * as UrlParams from "./UrlParams.js"
+import * as UrlParams from "./UrlParams.js"
/**
* @since 1.0.0
@@ -56,13 +56,17 @@ export const schemaBodyJson = (schema: Schema.Schema, options?
* @since 1.0.0
* @category schema
*/
-export const schemaBodyUrlParams = >, R>(
+export const schemaBodyUrlParams = <
+ A,
+ I extends Readonly | undefined>>,
+ R
+>(
schema: Schema.Schema,
options?: ParseOptions | undefined
) => {
- const parse = Schema.decodeUnknown(schema, options)
+ const decode = UrlParams.schemaStruct(schema, options)
return (self: HttpIncomingMessage): Effect.Effect =>
- Effect.flatMap(self.urlParamsBody, (_) => parse(Object.fromEntries(_)))
+ Effect.flatMap(self.urlParamsBody, decode)
}
/**
diff --git a/packages/platform/src/HttpServerRequest.ts b/packages/platform/src/HttpServerRequest.ts
index 27bc396554f..3382467ea27 100644
--- a/packages/platform/src/HttpServerRequest.ts
+++ b/packages/platform/src/HttpServerRequest.ts
@@ -183,7 +183,11 @@ export const schemaBodyForm: , R>(
* @since 1.0.0
* @category schema
*/
-export const schemaBodyUrlParams: >, R>(
+export const schemaBodyUrlParams: <
+ A,
+ I extends Readonly | undefined>>,
+ R
+>(
schema: Schema.Schema,
options?: ParseOptions | undefined
) => Effect.Effect = internal.schemaBodyUrlParams
diff --git a/packages/platform/src/Multipart.ts b/packages/platform/src/Multipart.ts
index ee0dce2c453..d3b4a07896a 100644
--- a/packages/platform/src/Multipart.ts
+++ b/packages/platform/src/Multipart.ts
@@ -114,7 +114,7 @@ export const isPersistedFile: (u: unknown) => u is PersistedFile = internal.isPe
* @category models
*/
export interface Persisted {
- readonly [key: string]: ReadonlyArray | string
+ readonly [key: string]: ReadonlyArray | ReadonlyArray | string
}
/**
diff --git a/packages/platform/src/UrlParams.ts b/packages/platform/src/UrlParams.ts
index 1dd2269d361..69d3c25d821 100644
--- a/packages/platform/src/UrlParams.ts
+++ b/packages/platform/src/UrlParams.ts
@@ -84,40 +84,6 @@ export const getAll: {
})
)
-/**
- * Builds a `Record` containing all the key-value pairs in the given `UrlParams`
- * as `string` (if only one value for a key) or a `NonEmptyArray`
- * (when more than one value for a key)
- *
- * @example
- * import { UrlParams } from "@effect/platform"
- *
- * const urlParams = UrlParams.fromInput({ a: 1, b: true, c: "string", e: [1, 2, 3] })
- * const result = UrlParams.extractAll(urlParams)
- *
- * assert.deepStrictEqual(
- * result,
- * { "a": "1", "b": "true", "c": "string", "e": ["1", "2", "3"] }
- * )
- *
- * @since 1.0.0
- * @category combinators
- */
-export const extractAll = (self: UrlParams): Record> => {
- const out: Record> = {}
- for (const [k, value] of self) {
- const curr = out[k]
- if (curr === undefined) {
- out[k] = value
- } else if (typeof curr === "string") {
- out[k] = [curr, value]
- } else {
- curr.push(value)
- }
- }
- return out
-}
-
/**
* @since 1.0.0
* @category combinators
@@ -206,13 +172,7 @@ export const remove: {
/**
* @since 1.0.0
- * @category combinators
- */
-export const toString = (self: UrlParams): string => new URLSearchParams(self as any).toString()
-
-/**
- * @since 1.0.0
- * @category constructors
+ * @category conversions
*/
export const makeUrl = (url: string, params: UrlParams, hash: Option.Option): Either.Either => {
try {
@@ -232,6 +192,12 @@ export const makeUrl = (url: string, params: UrlParams, hash: Option.Option new URLSearchParams(self as any).toString()
+
const baseUrl = (): string | undefined => {
if (
"location" in globalThis &&
@@ -244,6 +210,40 @@ const baseUrl = (): string | undefined => {
return undefined
}
+/**
+ * Builds a `Record` containing all the key-value pairs in the given `UrlParams`
+ * as `string` (if only one value for a key) or a `NonEmptyArray`
+ * (when more than one value for a key)
+ *
+ * @example
+ * import { UrlParams } from "@effect/platform"
+ *
+ * const urlParams = UrlParams.fromInput({ a: 1, b: true, c: "string", e: [1, 2, 3] })
+ * const result = UrlParams.toRecord(urlParams)
+ *
+ * assert.deepStrictEqual(
+ * result,
+ * { "a": "1", "b": "true", "c": "string", "e": ["1", "2", "3"] }
+ * )
+ *
+ * @since 1.0.0
+ * @category conversions
+ */
+export const toRecord = (self: UrlParams): Record> => {
+ const out: Record> = {}
+ for (const [k, value] of self) {
+ const curr = out[k]
+ if (curr === undefined) {
+ out[k] = value
+ } else if (typeof curr === "string") {
+ out[k] = [curr, value]
+ } else {
+ curr.push(value)
+ }
+ }
+ return out
+}
+
/**
* @since 1.0.0
* @category schema
@@ -273,7 +273,7 @@ export const schemaJson = (schema: Schema.Schema, options?: Pa
*
* Effect.gen(function* () {
* const urlParams = UrlParams.fromInput({ "a": [10, "string"], "b": false })
- * const result = yield* UrlParams.extractSchema(Schema.Struct({
+ * const result = yield* UrlParams.schemaStruct(Schema.Struct({
* a: Schema.Tuple(Schema.NumberFromString, Schema.String),
* b: Schema.BooleanFromString
* }))(urlParams)
@@ -287,9 +287,11 @@ export const schemaJson = (schema: Schema.Schema, options?: Pa
* @since 1.0.0
* @category schema
*/
-export const extractSchema =
- (schema: Schema.Schema, options?: ParseOptions | undefined) =>
- (self: UrlParams): Effect.Effect => {
- const parse = Schema.decodeUnknown(schema, options)
- return parse(extractAll(self))
- }
+export const schemaStruct = | undefined>, R>(
+ schema: Schema.Schema,
+ options?: ParseOptions | undefined
+) =>
+(self: UrlParams): Effect.Effect => {
+ const parse = Schema.decodeUnknown(schema, options)
+ return parse(toRecord(self))
+}
diff --git a/packages/platform/src/internal/httpServerRequest.ts b/packages/platform/src/internal/httpServerRequest.ts
index 56880eac11d..dc9d8eade58 100644
--- a/packages/platform/src/internal/httpServerRequest.ts
+++ b/packages/platform/src/internal/httpServerRequest.ts
@@ -122,7 +122,11 @@ export const schemaBodyForm = , R>(
}
/** @internal */
-export const schemaBodyUrlParams = >, R>(
+export const schemaBodyUrlParams = <
+ A,
+ I extends Readonly | undefined>>,
+ R
+>(
schema: Schema.Schema,
options?: ParseOptions | undefined
) => {
diff --git a/packages/platform/src/internal/multipart.ts b/packages/platform/src/internal/multipart.ts
index b73d6ac0d7b..c58b63a8f14 100644
--- a/packages/platform/src/internal/multipart.ts
+++ b/packages/platform/src/internal/multipart.ts
@@ -397,30 +397,34 @@ export const toPersisted = (
const fs = yield* FileSystem.FileSystem
const path_ = yield* Path.Path
const dir = yield* fs.makeTempDirectoryScoped()
- return yield* Stream.runFoldEffect(
- stream,
- Object.create(null) as Record | string>,
- (persisted, part) => {
- if (part._tag === "Field") {
+ const persisted: Record | Array | string> = Object.create(null)
+ yield* Stream.runForEach(stream, (part) => {
+ if (part._tag === "Field") {
+ if (!(part.key in persisted)) {
persisted[part.key] = part.value
- return Effect.succeed(persisted)
+ } else if (typeof persisted[part.key] === "string") {
+ persisted[part.key] = [persisted[part.key] as string, part.value]
+ } else {
+ ;(persisted[part.key] as Array).push(part.value)
}
- const file = part
- const path = path_.join(dir, path_.basename(file.name).slice(-128))
- if (!Array.isArray(persisted[part.key])) {
- persisted[part.key] = []
- }
- ;(persisted[part.key] as Array).push(
- new PersistedFileImpl(
- file.key,
- file.name,
- file.contentType,
- path
- )
- )
- return Effect.as(writeFile(path, file), persisted)
+ return Effect.void
}
- )
+ const file = part
+ const path = path_.join(dir, path_.basename(file.name).slice(-128))
+ const filePart = new PersistedFileImpl(
+ file.key,
+ file.name,
+ file.contentType,
+ path
+ )
+ if (Array.isArray(persisted[part.key])) {
+ ;(persisted[part.key] as Array).push(filePart)
+ } else {
+ persisted[part.key] = [filePart]
+ }
+ return writeFile(path, file)
+ })
+ return persisted
}).pipe(
Effect.catchTags({
SystemError: (cause) => Effect.fail(new MultipartError({ reason: "InternalError", cause })),
diff --git a/packages/platform/test/UrlParams.test.ts b/packages/platform/test/UrlParams.test.ts
index 30b719fa63b..30b7e103f09 100644
--- a/packages/platform/test/UrlParams.test.ts
+++ b/packages/platform/test/UrlParams.test.ts
@@ -85,24 +85,24 @@ describe("UrlParams", () => {
})
})
- describe("extractAll", () => {
+ describe("toRecord", () => {
it("works when empty", () => {
assert.deepStrictEqual(
- UrlParams.extractAll(UrlParams.empty),
+ UrlParams.toRecord(UrlParams.empty),
{}
)
})
it("builds non empty array from same keys", () => {
assert.deepStrictEqual(
- UrlParams.extractAll(UrlParams.fromInput({ "a": [10, "string", false] })),
+ UrlParams.toRecord(UrlParams.fromInput({ "a": [10, "string", false] })),
{ a: ["10", "string", "false"] }
)
})
it("works with non-strings", () => {
const urlParams = UrlParams.fromInput({ a: 1, b: true, c: "string", e: [1, 2, 3] })
- const result = UrlParams.extractAll(urlParams)
+ const result = UrlParams.toRecord(urlParams)
assert.deepStrictEqual(
result,
{ "a": "1", "b": "true", "c": "string", "e": ["1", "2", "3"] }
@@ -110,17 +110,17 @@ describe("UrlParams", () => {
})
})
- describe("extractSchema", () => {
+ describe("schemaStruct", () => {
it.effect("works when empty", () =>
Effect.gen(function*() {
- const result = yield* UrlParams.extractSchema(Schema.Struct({}))(UrlParams.empty)
+ const result = yield* UrlParams.schemaStruct(Schema.Struct({}))(UrlParams.empty)
assert.deepStrictEqual(result, {})
}))
it.effect("parse original values", () =>
Effect.gen(function*() {
const urlParams = UrlParams.fromInput({ "a": [10, "string", false] })
- const result = yield* UrlParams.extractSchema(Schema.Struct({
+ const result = yield* UrlParams.schemaStruct(Schema.Struct({
a: Schema.Tuple(Schema.NumberFromString, Schema.String, Schema.BooleanFromString)
}))(urlParams)
assert.deepStrictEqual(result, {
@@ -131,7 +131,7 @@ describe("UrlParams", () => {
it.effect("parse multiple keys", () =>
Effect.gen(function*() {
const urlParams = UrlParams.fromInput({ "a": [10, "string"], "b": false })
- const result = yield* UrlParams.extractSchema(Schema.Struct({
+ const result = yield* UrlParams.schemaStruct(Schema.Struct({
a: Schema.Tuple(Schema.NumberFromString, Schema.String),
b: Schema.BooleanFromString
}))(urlParams)