JUMBO refactor, still work in progress

This commit is contained in:
2025-10-07 00:14:31 +02:00
parent 3694492e1a
commit dc0ec5c635
50 changed files with 4283 additions and 3698 deletions

View File

@@ -0,0 +1,67 @@
/// <reference types="bun" />
import { describe, expect, test } from "bun:test";
import { HashMap, Schema } from "effect";
import * as Api from "./Api";
describe("bundle", () => {
test("constructs a HashMap", () => {
const foo = Api.make(Schema.Void, Schema.String, Schema.String);
const bar = Api.make(Schema.Void, Schema.Number, Schema.String);
const bundle = Api.bundle({ foo, bar });
expect(HashMap.unsafeGet(bundle.map, "foo")).toBe(foo);
expect(HashMap.unsafeGet(bundle.map, "bar")).toBe(bar);
});
test("constructs a record", () => {
const foo = Api.make(Schema.Void, Schema.String, Schema.String);
const bar = Api.make(Schema.Void, Schema.Number, Schema.String);
const bundle = Api.bundle({ foo, bar });
expect(bundle.record.foo).toBe(foo);
expect(bundle.record.bar).toBe(bar);
});
test("freezes the object", () => {
const bundle = Api.bundle({
foo: Api.make(Schema.Void, Schema.String, Schema.String),
bar: Api.make(Schema.Void, Schema.Number, Schema.String),
});
expect(Object.isFrozen(bundle)).toBeTrue();
});
test("freezes the record", () => {
const bundle = Api.bundle({
foo: Api.make(Schema.Void, Schema.String, Schema.String),
bar: Api.make(Schema.Void, Schema.Number, Schema.String),
});
expect(Object.isFrozen(bundle.record)).toBeTrue();
});
});
describe("make", () => {
test("constructs an object", () => {
const request = Schema.String.annotations({ title: "request" });
const response = Schema.String.annotations({ title: "response" });
const error = Schema.String.annotations({ title: "error" });
const api = Api.make(request, response, error);
expect(api.request).toBe(request);
expect(api.response).toBe(response);
expect(api.error).toBe(error);
});
test("freezes the object", () => {
const request = Schema.String.annotations({ title: "request" });
const response = Schema.String.annotations({ title: "response" });
const error = Schema.String.annotations({ title: "error" });
const api = Api.make(request, response, error);
expect(Object.isFrozen(api)).toBeTrue();
})
});

View File

@@ -0,0 +1,59 @@
import { Data, Effect, HashMap, ParseResult, Schema } from "effect";
export class InternalServerError extends Data.Error<{ data: unknown }> { }
export class RequestParseError extends Data.Error<{ cause: ParseResult.ParseError }> { }
export class ResponseParseError extends Data.Error<{ cause: ParseResult.ParseError }> { }
export class UnexpectedResponseContentType extends Data.Error<{ contentType: string }> { }
export class UnexpectedResponseStatus extends Data.Error<{ status: number, statusText: string }> { }
export interface Api<
RequestSchema extends Schema.Schema.AnyNoContext,
ResponseSchema extends Schema.Schema.AnyNoContext,
ErrorSchema extends Schema.Schema.AnyNoContext,
> {
readonly request: RequestSchema;
readonly response: ResponseSchema;
readonly error: ErrorSchema;
}
export type ApiAny = Api<
Schema.Schema.AnyNoContext,
Schema.Schema.AnyNoContext,
Schema.Schema.AnyNoContext
>;
export type ApiBundle<
Record extends { readonly [_: string]: ApiAny },
> = {
readonly map: HashMap.HashMap<string, ApiAny>;
readonly record: Record;
}
export type ApiBundleAny = ApiBundle<{ readonly [_: string]: ApiAny }>;
export type ApiBundleImpl<
Record extends { readonly [_: string]: ApiAny },
> = {
readonly [K in keyof Record]: (request: Record[K]["request"]["Type"]) => Effect.Effect<Record[K]["response"]["Type"], Record[K]["error"]["Type"], any>;
}
export function bundle<
const Record extends { readonly [_: string]: ApiAny; }
>(record: Record): ApiBundle<Record> {
return Object.freeze({
map: HashMap.fromIterable(Object.entries(record)),
record: Object.freeze(Object.assign({}, record)),
});
}
export function make<
RequestSchema extends Schema.Schema.AnyNoContext = typeof Schema.Void,
ResponseSchema extends Schema.Schema.AnyNoContext = typeof Schema.Void,
ErrorSchema extends Schema.Schema.AnyNoContext = typeof Schema.Void,
>(
request: RequestSchema = Schema.Void as unknown as RequestSchema,
response: ResponseSchema = Schema.Void as unknown as ResponseSchema,
error: ErrorSchema = Schema.Void as unknown as ErrorSchema,
): Api<RequestSchema, ResponseSchema, ErrorSchema> {
return Object.freeze({ request, response, error });
};

View File

@@ -0,0 +1,92 @@
/// <reference types="bun" />
import { describe, test } from "bun:test";
import { Cause, Effect } from "effect";
import * as Body from "./Body";
import * as Test from "./Test";
describe("arrayBuffer", () => {
test("succeeds", async () => {
const buffer = new ArrayBuffer(4);
const body = { arrayBuffer: () => Promise.resolve(buffer) };
const effect = Body.arrayBuffer(body);
const exit = await Effect.runPromiseExit(effect);
Test.expectSuccess(exit, buffer);
});
test("fails", async () => {
const error = new RangeError();
const body = { arrayBuffer: () => Promise.reject(error) };
const effect = Body.arrayBuffer(body);
const exit = await Effect.runPromiseExit(effect);
Test.expectFailure(exit, Cause.fail(new Body.BodyError({ cause: error })));
});
test("dies", async () => {
const error = new Error();
const body = { arrayBuffer: () => Promise.reject(error) };
const effect = Body.arrayBuffer(body);
const exit = await Effect.runPromiseExit(effect);
Test.expectFailure(exit, Cause.die(error));
});
});
describe("blob", () => {
test("succeeds", async () => {
const blob = new Blob(["foo"]);
const body = { blob: () => Promise.resolve(blob) };
const effect = Body.blob(body);
const exit = await Effect.runPromiseExit(effect);
Test.expectSuccess(exit, blob);
});
test("dies", async () => {
const error = new Error();
const body = { blob: () => Promise.reject(error) };
const effect = Body.blob(body);
const exit = await Effect.runPromiseExit(effect);
Test.expectFailure(exit, Cause.die(error));
})
});
describe("body", () => {
test.todo("succeeds with none");
test.todo("succeeds with some");
});
describe("bytes", () => {
test.todo("succeeds");
test.todo("fails");
test.todo("dies");
});
describe("formData", () => {
test.todo("succeeds");
test.todo("fails");
test.todo("dies");
});
describe("json", () => {
test.todo("succeeds");
test.todo("fails");
test.todo("dies");
});
describe("stream", () => {
test.todo("succeeds");
});
describe("json", () => {
test.todo("succeeds");
test.todo("dies");
});

View File

@@ -0,0 +1,70 @@
import { Data, Effect, Option, pipe, Stream } from "effect";
export class BodyError extends Data.TaggedClass("BodyError")<{ cause: RangeError | TypeError | SyntaxError }> { }
export class StreamError extends Data.TaggedClass("StreamError")<{ cause: unknown }> { }
export function arrayBuffer(body: { arrayBuffer(): Promise<ArrayBuffer> }): Effect.Effect<ArrayBuffer, BodyError> {
return Effect.tryPromise({
try: () => body.arrayBuffer(),
catch: (cause) => {
if (cause instanceof RangeError) return new BodyError({ cause });
else throw cause;
},
});
}
export function blob(body: { blob(): Promise<Blob> }): Effect.Effect<Blob> {
return Effect.promise(() => body.blob());
}
export function body(body: { readonly body: ReadableStream<Uint8Array> | null; }): Option.Option<Stream.Stream<Uint8Array, StreamError>> {
return pipe(
body.body,
Option.fromNullable,
Option.map((stream) => Stream.fromReadableStream({
evaluate: () => stream,
onError: (cause) => new StreamError({ cause }),
}))
);
}
export function bytes(body: { bytes(): Promise<Uint8Array> }): Effect.Effect<Uint8Array, BodyError> {
return Effect.tryPromise({
try: () => body.bytes(),
catch: (cause) => {
if (cause instanceof RangeError) return new BodyError({ cause });
else throw cause;
},
});
}
export function formData(body: { formData(): Promise<FormData> }): Effect.Effect<FormData, BodyError> {
return Effect.tryPromise({
try: () => body.formData(),
catch: (cause) => {
if (cause instanceof TypeError) return new BodyError({ cause });
else throw cause;
},
});
}
export function json(body: { json(): Promise<unknown> }): Effect.Effect<unknown, BodyError> {
return Effect.tryPromise({
try: () => body.json(),
catch: (cause) => {
if (cause instanceof SyntaxError) return new BodyError({ cause });
else throw cause;
}
});
}
export function stream(body: { stream(): ReadableStream<Uint8Array> }): Stream.Stream<Uint8Array, StreamError> {
return Stream.fromReadableStream({
evaluate: () => body.stream(),
onError: (cause) => new StreamError({ cause }),
});
}
export function text(body: { text(): Promise<string> }): Effect.Effect<string> {
return Effect.promise(() => body.text());
}

View File

@@ -0,0 +1,117 @@
/// <reference types="bun" />
import { describe, test } from "bun:test";
import * as cbor from "cbor2";
import { Effect, Schema } from "effect";
import * as Cbor from "./Cbor";
import * as Test from "./Test";
describe("encode", () => {
test("succeeds", async () => {
const object = {
x: "foo",
y: new Date(2025, 0, 1),
z: new Uint8Array([0, 1, 2, 3]),
};
const effect = Cbor.encode(object);
const exit = await Effect.runPromiseExit(effect);
Test.expectSuccess(exit, cbor.encode(object));
});
test("fails", async () => {
const object = {
x: Symbol(),
};
const effect = Cbor.encode(object);
const exit = await Effect.runPromiseExit(effect);
Test.expectFailureTag(exit, "CborEncodeError");
});
});
describe("decode", () => {
test("succeeds", async () => {
const object = {
x: "foo",
y: new Date(2025, 0, 1),
z: new Uint8Array([0, 1, 2, 3]),
};
const effect = Cbor.decode(cbor.encode(object));
const exit = await Effect.runPromiseExit(effect);
Test.expectSuccess(exit, object);
});
test("fails", async () => {
const array = new Uint8Array([0xFE]);
const effect = Cbor.decode(array);
const exit = await Effect.runPromiseExit(effect);
Test.expectFailureTag(exit, "CborDecodeError");
});
});
describe("encodeSchema", () => {
test("succeeds", async () => {
const schema = Schema.Struct({
x: Schema.String,
y: Schema.DateFromSelf,
z: Schema.Uint8ArrayFromSelf,
});
const object = schema.make({
x: "foo",
y: new Date(2025, 0, 1),
z: new Uint8Array([0, 1, 2, 3]),
});
const effect = Cbor.encodeSchema(schema)(object);
const exit = await Effect.runPromiseExit(effect);
Test.expectSuccess(exit, cbor.encode(object));
});
test.todo("fails");
});
describe("decodeSchema", () => {
test("succeeds", async () => {
const schema = Schema.Struct({
x: Schema.String,
y: Schema.DateFromSelf,
z: Schema.Uint8ArrayFromSelf,
});
const object = schema.make({
x: "foo",
y: new Date(2025, 0, 1),
z: new Uint8Array([0, 1, 2, 3]),
});
const effect = Cbor.decodeSchema(schema)(cbor.encode(object));
const exit = await Effect.runPromiseExit(effect);
Test.expectSuccess(exit, object);
});
test("fails with CborDecodeError", async () => {
const array = new Uint8Array([0xFE]);
const effect = Cbor.decodeSchema(Schema.Any)(array);
const exit = await Effect.runPromiseExit(effect);
Test.expectFailureTag(exit, "CborDecodeError");
});
test("fails with ParseError", async () => {
const effect = Cbor.decodeSchema(Schema.Number)(cbor.encode("foo"));
const exit = await Effect.runPromiseExit(effect);
Test.expectFailureTag(exit, "ParseError");
});
});

View File

@@ -0,0 +1,35 @@
import * as cbor from "cbor2";
import { Data, Effect, pipe, Schema } from "effect";
export class CborDecodeError extends Data.TaggedError("CborDecodeError")<{ cause: unknown }> { }
export class CborEncodeError extends Data.TaggedError("CborEncodeError")<{ cause: unknown }> { }
export function encode(u: unknown): Effect.Effect<Uint8Array, CborEncodeError> {
return Effect.try({
try: () => cbor.encode(u),
catch: (cause) => new CborEncodeError({ cause }),
});
}
export function decode(u: Uint8Array): Effect.Effect<unknown, CborDecodeError> {
return Effect.try({
try: () => cbor.decode(u),
catch: (cause) => new CborDecodeError({ cause }),
});
}
export const encodeSchema = <A, I>(schema: Schema.Schema<A, I>) => {
const schemaEncoder = Schema.encode(schema);
return (a: A) => pipe(
schemaEncoder(a),
Effect.flatMap((i) => encode(i)),
);
};
export const decodeSchema = <A>(schema: Schema.Schema<A, any>) => {
const schemaDecoder = Schema.decodeUnknown(schema);
return (u: Uint8Array) => pipe(
decode(u),
Effect.flatMap((u) => schemaDecoder(u)),
);
};

View File

@@ -0,0 +1,109 @@
import { Data, Effect, Option, pipe, Record, Schema } from "effect";
import { constant } from "effect/Function";
import * as Api from "./Api";
import * as Body from "./Body";
import * as Cbor from "./Cbor";
import { fetch, FetchError } from "./Fetch";
export class ServerDie extends Data.TaggedError("ServerDie")<{ cause: unknown }> { }
export class UnexpectedStatus extends Data.TaggedError("UnexpectedStatusCode")<{ status: number }> { }
export interface ClientOptions {
readonly baseUrl?: string | URL | undefined;
}
export const client = <const Record extends { readonly [_: string]: Api.ApiAny }>(
bundle: Api.ApiBundle<Record>,
{ baseUrl }: ClientOptions = {},
) => {
return Object.freeze(Record.map(bundle.record, (api, key) => {
const url = new URL(`/api/${key}`, baseUrl);
const requestEncoder = encodeBody(api.request);
const responseDecoder = decodeBody(api.response);
const errorDecoder = decodeBody(api.error);
return Effect.fn(`call.${key}`)(function* (request: any) {
const body = yield* pipe(
request,
requestEncoder,
Effect.flatMap(({ body, headers }) => fetch(url, {
method: "POST",
credentials: "include",
body,
headers,
})),
Effect.catchTags({
CborEncodeError: Effect.die,
ParseError: Effect.die,
}),
);
switch (body.status) {
case 200: {
const result = yield* responseDecoder(body).pipe(Effect.orDie);
return result;
}
case 400: {
const result = yield* errorDecoder(body).pipe(Effect.orDie);
return yield* Effect.fail(result);
}
case 500: {
const result = yield* readBody(body).pipe(Effect.orDie);
return yield* Effect.die(new ServerDie({ cause: result }));
}
default: {
return yield* Effect.die(new UnexpectedStatus({ status: body.status }));
}
}
});
})) as { readonly [K in keyof Record]: (request: Record[K]["request"]["Type"]) => Effect.Effect<Record[K]["response"]["Type"], Record[K]["error"]["Type"] | FetchError, never> };
};
export const readBody = (body: Body) => pipe(
body,
Body.bytes,
Effect.map((bytes) => bytes.byteLength > 0 ? Option.some(bytes) : Option.none()),
Effect.flatMap((maybeBytes) => pipe(
maybeBytes,
Effect.transposeMapOption(Cbor.decode),
)),
Effect.map(Option.getOrUndefined),
);
export const decodeBody = <A>(schema: Schema.Schema<A, any>) => {
const decoder = Schema.decodeUnknown(schema);
return (body: Body) => Effect.flatMap(readBody(body), decoder);
};
export interface BodyData {
readonly body: null | Uint8Array;
readonly headers: { readonly [_: string]: string };
}
export const encodeBody = <A>(schema: Schema.Schema<A, any>) => {
const encoder = Cbor.encodeSchema(schema);
return (a: A) => pipe(
a,
Option.fromNullable,
Effect.transposeMapOption(encoder),
Effect.map(Option.match({
onNone: emptyBody,
onSome: cborBody,
}))
);
};
export const emptyBody: () => BodyData = constant(Object.freeze<BodyData>({
body: null,
headers: Object.freeze({}),
}));
export const cborBody: (bytes: Uint8Array) => BodyData = (bytes) => Object.freeze<BodyData>({
body: bytes,
headers: Object.freeze({
"Content-Type": "application/cbor",
"Content-Length": String(bytes.byteLength),
}),
});

View File

@@ -0,0 +1,15 @@
import { Data, Effect } from "effect";
export class FetchError extends Data.TaggedError("FetchError")<{ cause: TypeError }> { }
const _fetch = (input: string | URL, init?: RequestInit) => Effect.tryPromise({
try: (signal) => fetch(input, { ...init, signal }),
catch: (cause) => {
if (cause instanceof TypeError) return new FetchError({ cause });
else throw cause;
},
});
export {
_fetch as fetch,
};

View File

@@ -0,0 +1,40 @@
/// <reference types="bun" />
import { expect } from "bun:test";
import { Cause, Exit, Predicate } from "effect";
export function expectSuccess<A, E>(
actual: Exit.Exit<A, E>,
expected: A,
): asserts actual is Exit.Success<A, never> {
expect<Exit.Exit<A, E>>(Exit.succeed(expected)).toStrictEqual(actual);
}
export function expectFailure<A, E>(
actual: Exit.Exit<A, E>,
expected: Cause.Cause<E>,
): asserts actual is Exit.Failure<never, E> {
expect<Exit.Exit<A, E>>(Exit.failCause(expected)).toStrictEqual(actual);
}
export function expectFailureTag<A, E, Tag extends string>(
actual: Exit.Exit<A, E>,
tag: Tag,
): asserts actual is Exit.Failure<never, E & { readonly _tag: Tag }> {
if (!Exit.isFailure(actual)) {
expect(actual).fail("Expected Exit to be a Failure");
throw new Error("Unreachable");
}
const { cause } = actual;
if (!Cause.isFailType(cause)) {
expect(cause).fail("Expected Cause to be a Fail");
throw new Error("Unreachable");
}
const { error } = cause;
if (!Predicate.isTagged(tag)) {
expect(error).fail(`Expected error to be tagged with ${JSON.stringify(tag)}`);
throw new Error("Unreachable");
}
}

View File

@@ -1,28 +1,26 @@
import { Brand } from "effect";
import { pipe, Schema } from "effect";
export type UUID = Brand.Branded<string, "UUID">;
export const UUID = Brand.nominal<UUID>();
export const Sha256 = pipe(
Schema.Uint8ArrayFromSelf,
Schema.filter((arr) => arr.byteLength === 32),
Schema.brand("Sha256"),
);
export type Sha256 = typeof Sha256.Type;
export type Sha256_Bin = Brand.Branded<Uint8Array, "Sha256">;
export const Sha256_Bin = Brand.nominal<Sha256_Bin>();
export const SessionId = pipe(Schema.String, Schema.brand("SessionId"));
export type SessionId = typeof SessionId.Type;
export type Sha256_Hex = Brand.Branded<string, "Sha256">;
export const Sha256_Hex = Brand.nominal<Sha256_Hex>();
export const AttachmentId = pipe(Schema.UUID, Schema.brand("AttachmentId"));
export type AttachmentId = typeof AttachmentId.Type;
export type AttachmentId = Brand.Branded<UUID, "AttachmentId">;
export const AttachmentId = Brand.nominal<AttachmentId>();
export const PieceId = pipe(Schema.UUID, Schema.brand("PieceId"));
export type PieceId = typeof PieceId.Type;
export type PieceId = Brand.Branded<UUID, "PieceId">;
export const PieceId = Brand.nominal<PieceId>();
export const RepertoireId = pipe(Schema.UUID, Schema.brand("RepertoireId"));
export type RepertoireId = typeof RepertoireId.Type;
export type RepertoireId = Brand.Branded<UUID, "RepertoireId">;
export const RepertoireId = Brand.nominal<RepertoireId>();
export const RequestId = pipe(Schema.UUID, Schema.brand("RequestId"));
export type RequestId = typeof RequestId.Type;
export type RequestId = Brand.Branded<UUID, "RequestId">;
export const RequestId = Brand.nominal<RequestId>();
export type SessionId = Brand.Branded<string, "SessionId">;
export const SessionId = Brand.nominal<SessionId>();
export type UserId = Brand.Branded<UUID, "UserId">;
export const UserId = Brand.nominal<UserId>();
export const UserId = pipe(Schema.UUID, Schema.brand("UserId"));
export type UserId = typeof UserId.Type;

View File

@@ -0,0 +1,206 @@
import { pipe, Schema } from "effect";
import * as Api from "./Api";
import { AttachmentId, PieceId, RepertoireId, Sha256, UserId } from "common";
import { constant } from "effect/Function";
// --- MARK: COMMON TYPES ------------------------------------------------------
export enum Role {
Viewer = "Viewer",
Editor = "Editor",
}
export const SystemInformation = Schema.Struct({
createdBy: pipe(UserId, Schema.optionalWith({ as: "Option", exact: true })),
createdAt: Schema.DateTimeUtc,
modifiedBy: pipe(UserId, Schema.optionalWith({ as: "Option", exact: true })),
modifiedAt: pipe(Schema.DateTimeUtc, Schema.optionalWith({ as: "Option", exact: true })),
});
export const Pagination = Schema.Struct({
offset: pipe(Schema.Int, Schema.greaterThanOrEqualTo(0), Schema.optionalWith({ default: constant(0), exact: true })),
limit: pipe(Schema.Int, Schema.between(1, 100), Schema.optionalWith({ default: constant(100), exact: true })),
});
export type SystemInformation = typeof SystemInformation.Type;
export type Pagination = typeof Pagination.Type;
// --- MARK: RESPONSE TYPES ----------------------------------------------------
export const Me = Schema.Struct({
userId: UserId,
displayName: Schema.NonEmptyString,
roles: Schema.HashSet(Schema.Enums(Role)),
});
export const Other = Schema.Struct({
userId: UserId,
displayName: Schema.NonEmptyString,
});
export const Attachment = Schema.Struct({
attachmentId: AttachmentId,
pieceId: PieceId,
filename: Schema.NonEmptyString,
sha256: Sha256,
mediaType: Schema.NonEmptyString,
}).pipe(Schema.extend(SystemInformation));
export const Piece = Schema.Struct({
pieceId: PieceId,
name: Schema.NonEmptyString,
composer: pipe(Schema.NonEmptyString, Schema.optionalWith({ as: "Option", exact: true })),
lyricist: pipe(Schema.NonEmptyString, Schema.optionalWith({ as: "Option", exact: true })),
arranger: pipe(Schema.NonEmptyString, Schema.optionalWith({ as: "Option", exact: true })),
attachments: pipe(Attachment, Schema.Array),
}).pipe(Schema.extend(SystemInformation));
export const Piece_Create = Schema.Struct({
name: Schema.NonEmptyString,
composer: pipe(Schema.NonEmptyString, Schema.optionalWith({ as: "Option", exact: true })),
lyricist: pipe(Schema.NonEmptyString, Schema.optionalWith({ as: "Option", exact: true })),
arranger: pipe(Schema.NonEmptyString, Schema.optionalWith({ as: "Option", exact: true })),
});
export const Piece_Query = Schema.Struct({
name: pipe(Schema.NonEmptyString, Schema.optionalWith({ as: "Option", exact: true })),
author: pipe(Schema.NonEmptyString, Schema.optionalWith({ as: "Option", exact: true })),
}).pipe(Schema.extend(Pagination));
export const Repertoire = Schema.Struct({
repertoireId: RepertoireId,
name: Schema.NonEmptyString,
entries: pipe(PieceId, Schema.Array),
}).pipe(Schema.extend(SystemInformation));
export const Repertoire_Create = Schema.Struct({
name: Schema.NonEmptyString,
entries: pipe(PieceId, Schema.Array),
});
export const Repertoire_Query = Schema.Struct({
name: pipe(Schema.NonEmptyString, Schema.optionalWith({ as: "Option", exact: true })),
}).pipe(Schema.extend(Pagination));
export type Me = typeof Me.Type;
export type Other = typeof Other.Type;
export type Attachment = typeof Attachment.Type;
export type Piece = typeof Piece.Type;
export type Piece_Create = typeof Piece_Create.Type;
export type Piece_Query = typeof Piece_Query.Type;
export type Repertoire = typeof Repertoire.Type;
export type Repertoire_Query = typeof Repertoire_Query.Type;
// --- MARK: ERROR TYPES -------------------------------------------------------
export const Unauthenticated = Schema.TaggedStruct("Unauthenticated", {});
export const Unauthorized = Schema.TaggedStruct("Unauthorized", {});
export const NotFound = Schema.TaggedStruct("NotFound", {});
export type Unauthenticated = typeof Unauthenticated.Type;
export type Unauthorized = typeof Unauthorized.Type;
export type NotFound = typeof NotFound.Type;
export default Api.bundle({
// --- Authentication ---
me: Api.make(Schema.Void, Me, Unauthenticated),
logout: Api.make(Schema.Void, Schema.Void),
getUser: Api.make(
UserId,
Other,
Schema.Union(Unauthenticated, NotFound),
),
// --- Piece CRUD ---
createPiece: Api.make(
Piece_Create,
Piece,
Schema.Union(Unauthenticated, Unauthorized),
),
getPiece: Api.make(
PieceId,
Piece,
Schema.Union(Unauthenticated, Unauthorized, NotFound),
),
queryPieces: Api.make(
Piece_Query,
pipe(PieceId, Schema.Array),
Schema.Union(Unauthenticated, Unauthorized),
),
updatePiece: Api.make(
Piece_Create.pipe(Schema.extend(Schema.Struct({ pieceId: PieceId }))),
Piece,
Schema.Union(Unauthenticated, Unauthorized, NotFound),
),
deletePiece: Api.make(
PieceId,
Schema.Void,
Schema.Union(Unauthenticated, Unauthorized, NotFound),
),
// --- Attachment CRUD ---
createAttachment: Api.make(
Schema.Struct({
pieceId: PieceId,
filename: Schema.NonEmptyString,
mediaType: Schema.NonEmptyString,
data: Schema.Uint8ArrayFromSelf,
}),
Attachment,
Schema.Union(Unauthenticated, Unauthorized, NotFound),
),
getAttachment: Api.make(
AttachmentId,
Schema.Struct({
filename: Schema.NonEmptyString,
mediaType: Schema.NonEmptyString,
data: Schema.Uint8ArrayFromSelf,
}),
Schema.Union(Unauthenticated, Unauthorized, NotFound),
),
updateAttachment: Api.make(
Schema.Struct({
attachmentId: AttachmentId,
filename: Schema.NonEmptyString,
}),
Attachment,
Schema.Union(Unauthenticated, Unauthorized, NotFound),
),
deleteAttachment: Api.make(
AttachmentId,
Schema.Void,
Schema.Union(Unauthenticated, Unauthorized, NotFound),
),
// --- Repertoire CRUD ---
createRepertoire: Api.make(
Repertoire_Create,
Repertoire,
Schema.Union(Unauthenticated, Unauthorized),
),
getRepertoire: Api.make(
RepertoireId,
Repertoire,
Schema.Union(Unauthenticated, Unauthorized, NotFound),
),
queryRepertoire: Api.make(
Repertoire_Query,
pipe(RepertoireId, Schema.Array),
Schema.Union(Unauthenticated, Unauthorized),
),
updateRepertoire: Api.make(
Repertoire_Create.pipe(Schema.extend(Schema.Struct({ repertoireId: RepertoireId }))),
Repertoire,
Schema.Union(Unauthenticated, Unauthorized, NotFound),
),
deleteRepertoire: Api.make(
RepertoireId,
Schema.Void,
Schema.Union(Unauthenticated, Unauthorized, NotFound),
),
});