Skip to content

Instantly share code, notes, and snippets.

@SandroMaglione
Created May 1, 2024 20:56
Show Gist options
  • Save SandroMaglione/1691267277e16081f2755621d6ff5a31 to your computer and use it in GitHub Desktop.
Save SandroMaglione/1691267277e16081f2755621d6ff5a31 to your computer and use it in GitHub Desktop.
File upload validation service created using `effect` only
import { Brand, Context, Data, Effect, Layer } from "effect";
type FileMetadata = Readonly<{ type: string; size: number }>;
export type ValidFile = FileMetadata & Brand.Brand<"ValidFile">;
const ValidFile = Brand.nominal<ValidFile>();
class FileTooLargeError extends Data.TaggedError("FileTooLargeError")<
Readonly<{ size: number }>
> {}
class FileFormatNotAllowedError extends Data.TaggedError(
"FileFormatNotAllowedError"
)<Readonly<{ format: string }>> {}
class FileSource extends Context.Tag("FileSource")<FileSource, FileMetadata>() {
static readonly LiveLayer = (file: globalThis.File) =>
Layer.succeed(this, file);
}
const makeSource = (file: Effect.Effect.Success<typeof FileSource>) => {
const allowOnlyImage = file.type.match("image.*")
? Effect.succeed(file)
: Effect.fail(new FileFormatNotAllowedError({ format: file.type }));
const maxFileSize = (maxSize: number) =>
file.size <= maxSize
? Effect.succeed(file)
: Effect.fail(new FileTooLargeError({ size: file.size }));
return {
parseValidFile: allowOnlyImage.pipe(
Effect.andThen(maxFileSize(3_000_000)),
Effect.map((file) => ValidFile(file))
),
};
};
class File extends Context.Tag("File")<File, ReturnType<typeof makeSource>>() {
static readonly Live = Layer.effect(this, Effect.map(FileSource, makeSource));
}
export const onUploadFile = (file: globalThis.File) =>
File.pipe(
Effect.flatMap(({ parseValidFile }) => parseValidFile),
Effect.provide(
File.Live.pipe(Layer.provide(Layer.succeed(FileSource, file)))
),
Effect.catchTags({
FileFormatNotAllowedError: () =>
Effect.succeed(
globalThis.Response.json({
error: "Only SVG, PNG, JPG or GIF files allowed",
})
),
FileTooLargeError: () =>
Effect.succeed(
globalThis.Response.json({
error: "Max file size allowed is 3MB",
})
),
})
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment