The rewrite

This commit is contained in:
Szymon Nowakowski 2024-06-25 20:00:42 +02:00
parent 5c60fbd93d
commit a67e7efb40
23 changed files with 721 additions and 461 deletions

View File

@ -9,5 +9,10 @@ insert_final_newline = true
tab_width = 4
trim_trailing_whitespace = true
[*.json]
indent_size = 2
indent_style = space
tab_width = 2
[*.md]
trim_trailing_whitespace = false

View File

@ -1 +0,0 @@
Szymon Nowakowski <smnbdg13@gmail.com> (https://renati.me)

View File

@ -1,2 +1,4 @@
# effect-api
A library for defining, using and implementing HTTP JSON APIs using Effect ecosystem
A library for defining, using and implementing HTTP JSON APIs using Effect
ecosystem.

BIN
bun.lockb Executable file

Binary file not shown.

View File

@ -1,41 +1,26 @@
{
"name": "effect-api",
"version": "0.1.0",
"description": "A library for defining, using and implementing HTTP JSON APIs using Effect ecosystem",
"keywords": [
"api",
"effect",
"fetch",
"http",
"json"
],
"private": true,
"type": "module",
"author": {
"name": "Szymon Nowakowski",
"email": "renati@renati.me",
"url": "https://renati.me/"
},
"homepage": "https://renati.me/effect-api",
"license": "MPL-2.0",
"browser": "./dist/oktaeder.js",
"repository": {
"type": "git",
"url": "https://gitea.renati.me/renati/effect-api.git"
},
"scripts": {
"build": "tsc --build"
"workspaces": [
"./packages/*"
],
"devDependencies": {
"typescript": "^5.5.2"
},
"dependencies": {
"@effect/schema": "^0.64.3",
"effect": "^2.4.6",
"fast-check": "^3.16.0",
"tslib": "^2.6.2"
},
"devDependencies": {
"typescript": "^5.4.2"
},
"exports": {
".": {
"types": "./dist/effect-api.d.ts",
"import": "./dist/effect-api.js"
},
"./Api": {
"types": "./dist/Api.d.ts",
"import": "./dist/Api.js"
}
"@effect/schema": "^0.68.9",
"effect": "^3.4.2",
"fast-check": "^3.19.0"
}
}

View File

@ -0,0 +1,4 @@
{
"name": "@make-api/api",
"type": "module"
}

148
packages/api/src/Api.ts Normal file
View File

@ -0,0 +1,148 @@
/*!
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Schema as S } from "@effect/schema";
import type { Field } from "./Field";
import type { Method } from "./Method";
import type { MimeTypePredicate } from "./MimeType";
import { QueryEmpty, type Query } from "./Query";
import { RequestBodyFile, RequestBodyJson, RequestBodyMultipart, RequestBodyNone, RequestBodyText, RequestBodyUrlEncoded, type RequestBody } from "./RequestBody";
import { ResponseBodyFile, ResponseBodyJson, ResponseBodyText, ResponseEmpty, type ResponseBody } from "./ResponseBody";
import { Route, RouteLiteral, RouteParam } from "./Route";
export interface Api<Props extends Api.PropsAny> {
readonly method: Method;
readonly props: Props;
}
export namespace Api {
export type Any = Api<PropsAny>;
export type Props<T extends Api.Any> = T extends Api<infer Props> ? Props : never;
export type PropsAny = {
readonly route: Route.Any;
readonly request: RequestBody.Any;
readonly response: { readonly [_: number]: ResponseBody.Any };
readonly query: Query.Any;
};
}
export const Api = <const Props extends Api.PropsAny>(method: Method, props: Props): Api<Props> => Object.freeze<Api<Props>>({
method,
props,
});
// --- BUILDERS ----------------------------------------------------------------
export type RouteInputLiteral<Literal extends string> = Literal;
export type RouteInputParam<Name extends string, Schema extends S.Schema<any, string, unknown>> = readonly [Name, Schema];
export namespace RouteInputLiteral {
export type Any = RouteInputLiteral<string>;
}
export namespace RouteInputParam {
export type Any = RouteInputParam<string, S.Schema<any, string, unknown>>;
}
export namespace RouteInput {
export type Any = readonly (
| RouteInputLiteral.Any
| RouteInputParam.Any
)[];
export type Route<R extends RouteInput.Any> = { readonly [K in keyof R]:
R[K] extends RouteInputLiteral<infer Literal> ? RouteLiteral<Literal>
: R[K] extends RouteInputParam<infer Name, infer Schema> ? RouteParam<Name, Schema>
: never;
};
}
export const make = <const R extends RouteInput.Any>(method: Method, ...routeInput: R): Api<{
readonly route: RouteInput.Route<R>,
readonly request: RequestBodyNone,
readonly response: {},
readonly query: {},
}> => Api(method, {
route: Route(...routeInput.map((r) => typeof r === "string" ? RouteLiteral(r) : RouteParam(r[0], r[1])) as RouteInput.Route<R>),
request: RequestBodyNone,
response: ResponseEmpty,
query: QueryEmpty,
});
export const requestBodyText = <const Schema extends S.Schema<any, string, unknown>>(schema: Schema) =>
<const T extends Api.Any>({ method, props }: T): Api<{
readonly route: T["props"]["route"],
readonly request: RequestBodyText<Schema>,
readonly response: T["props"]["response"],
readonly query: T["props"]["query"],
}> => Api(method, { ...props, request: RequestBodyText(schema) });
export const requestBodyJson = <const Schema extends S.Schema.Any>(schema: Schema) =>
<const T extends Api.Any>({ method, props }: T): Api<{
readonly route: T["props"]["route"],
readonly request: RequestBodyJson<Schema>,
readonly response: T["props"]["response"],
readonly query: T["props"]["query"],
}> => Api(method, { ...props, request: RequestBodyJson(schema) });
export const requestBodyUrlEncoded = <const Fields extends { readonly [_: string]: Field.AnyUrlEncoded }>(fields: Fields) =>
<const T extends Api.Any>({ method, props }: T): Api<{
readonly route: T["props"]["route"],
readonly request: RequestBodyUrlEncoded<Fields>,
readonly response: T["props"]["response"],
readonly query: T["props"]["query"],
}> => Api(method, { ...props, request: RequestBodyUrlEncoded(fields) });
export const requestBodyMultipart = <const Fields extends { readonly [_: string]: Field.Any }>(fields: Fields) =>
<const T extends Api.Any>({ method, props }: T): Api<{
readonly route: T["props"]["route"],
readonly request: RequestBodyMultipart<Fields>,
readonly response: T["props"]["response"],
readonly query: T["props"]["query"],
}> => Api(method, { ...props, request: RequestBodyMultipart(fields) });
export const requestBodyFile = <const MimeType extends string>(mimeType: MimeTypePredicate<MimeType>) =>
<const T extends Api.Any>({ method, props }: T): Api<{
readonly route: T["props"]["route"],
readonly request: RequestBodyFile<MimeType>,
readonly response: T["props"]["response"],
readonly query: T["props"]["query"],
}> => Api(method, { ...props, request: RequestBodyFile(mimeType) });
export const responseBodyText = <const Status extends number, const Schema extends S.Schema<any, string, unknown>>(status: Status, schema: Schema) =>
<const T extends Api.Any>({ method, props: { response, ...props } }: T): Api<{
readonly route: T["props"]["route"],
readonly request: T["props"]["request"],
readonly response: { readonly [K in keyof T["props"]["response"] | Status]: K extends Status ? ResponseBodyText<Schema> : T["props"]["response"][K] },
readonly query: T["props"]["query"],
}> => Api(method, { ...props, response: Object.freeze<any>({ ...response, [status]: ResponseBodyText(schema) }) });
export const responseBodyJson = <const Status extends number, const Schema extends S.Schema.Any>(status: Status, schema: Schema) =>
<const T extends Api.Any>({ method, props: { response, ...props } }: T): Api<{
readonly route: T["props"]["route"],
readonly request: T["props"]["request"],
readonly response: { readonly [K in keyof T["props"]["response"] | Status]: K extends Status ? ResponseBodyJson<Schema> : T["props"]["response"][K] },
readonly query: T["props"]["query"],
}> => Api(method, { ...props, response: Object.freeze<any>({ ...response, [status]: ResponseBodyJson(schema) }) });
export const responseBodyFile = <const Status extends number, const MimeType extends string>(status: Status, mimeType: MimeTypePredicate<MimeType>) =>
<const T extends Api.Any>({ method, props: { response, ...props } }: T): Api<{
readonly route: T["props"]["route"],
readonly request: T["props"]["request"],
readonly response: { readonly [K in keyof T["props"]["response"] | Status]: K extends Status ? ResponseBodyFile<MimeType> : T["props"]["response"][K] },
readonly query: T["props"]["query"],
}> => Api(method, { ...props, response: Object.freeze<any>({ ...response, [status]: ResponseBodyFile(mimeType) }) });
export const query = <const Query extends Query.Any>(query: Query) =>
<const T extends Api.Any>({ method, props: { query: _, ...props } }: T): Api<{
readonly route: T["props"]["route"],
readonly request: T["props"]["request"],
readonly response: T["props"]["response"],
readonly query: Query,
}> => Api(method, { ...props, query });

74
packages/api/src/Field.ts Normal file
View File

@ -0,0 +1,74 @@
/*!
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Schema as S } from "@effect/schema";
import type { MimeTypePredicate } from "./MimeType";
import type { Multiplicity } from "./Multiplicity";
// --- INTERFACES --------------------------------------------------------------
export interface FieldText<M extends Multiplicity, Schema extends S.Schema<any, string, unknown>> {
readonly _tag: "Text";
readonly multiplicity: M;
readonly schema: Schema;
}
export interface FieldJson<M extends Multiplicity, Schema extends S.Schema.Any> {
readonly _tag: "Json";
readonly multiplicity: M;
readonly schema: Schema;
}
export interface FieldFile<M extends Multiplicity, MimeType extends string> {
readonly _tag: "File";
readonly multiplicity: M;
readonly mimeType: MimeTypePredicate<MimeType>;
}
export namespace FieldText {
export type Any = FieldText<Multiplicity, S.Schema<any, string, unknown>>;
}
export namespace FieldJson {
export type Any = FieldJson<Multiplicity, S.Schema.Any>;
}
export namespace FieldFile {
export type Any = FieldFile<Multiplicity, string>;
}
export namespace Field {
export type Any =
| FieldText.Any
| FieldJson.Any
| FieldFile.Any
;
export type AnyUrlEncoded =
| FieldText.Any
| FieldJson.Any
;
}
// --- CONSTRUCTORS ------------------------------------------------------------
export const FieldText = <const M extends Multiplicity, const Schema extends S.Schema<any, string, unknown>>(multiplicity: M, schema: Schema): FieldText<M, Schema> => Object.freeze<FieldText<M, Schema>>({
_tag: "Text",
multiplicity,
schema,
});
export const FieldJson = <const M extends Multiplicity, const Schema extends S.Schema.Any>(multiplicity: M, schema: Schema): FieldJson<M, Schema> => Object.freeze<FieldJson<M, Schema>>({
_tag: "Json",
multiplicity,
schema,
});
export const FieldFile = <const M extends Multiplicity, const MimeType extends string>(multiplicity: M, mimeType: MimeTypePredicate<MimeType>): FieldFile<M, MimeType> => Object.freeze<FieldFile<M, MimeType>>({
_tag: "File",
multiplicity,
mimeType,
});

View File

@ -0,0 +1,13 @@
/*!
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/.
*/
export type Method =
| "GET"
| "POST"
| "PUT"
| "DELETE"
| "PATCH"
;

View File

@ -0,0 +1,9 @@
/*!
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Predicate } from "effect"
export type MimeTypePredicate<T extends string> = T | readonly T[] | Predicate.Refinement<string, T>;

View File

@ -0,0 +1,23 @@
/*!
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/.
*/
import type { Option as O } from "effect";
export type Multiplicity =
| "single"
| "optional"
| "multiple"
| "any"
;
export namespace Multiplicity {
export type With<M extends Multiplicity, T> = {
single: T,
optional: O.Option<T>,
multiple: readonly [T, ...T[]],
any: readonly T[],
}[M];
}

13
packages/api/src/Query.ts Normal file
View File

@ -0,0 +1,13 @@
/*!
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/.
*/
import type { Field } from "./Field";
export namespace Query {
export type Any = { readonly [_: string]: Field.AnyUrlEncoded };
}
export const QueryEmpty: {} = Object.freeze<{}>({});

View File

@ -0,0 +1,102 @@
/*!
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Schema as S } from "@effect/schema";
import type { Field } from "./Field";
import type { MimeTypePredicate } from "./MimeType";
// --- INTERFACES --------------------------------------------------------------
export interface RequestBodyNone {
readonly _tag: "None";
}
export interface RequestBodyText<Schema extends S.Schema<any, string, unknown>> {
readonly _tag: "Text";
readonly schema: Schema;
}
export interface RequestBodyJson<Schema extends S.Schema.Any> {
readonly _tag: "Json";
readonly schema: Schema;
}
export interface RequestBodyUrlEncoded<Fields extends { readonly [_: string]: Field.AnyUrlEncoded }> {
readonly _tag: "UrlEncoded";
readonly fields: Readonly<Fields>;
}
export interface RequestBodyMultipart<Fields extends { readonly [_: string]: Field.Any }> {
readonly _tag: "Multipart";
readonly fields: Readonly<Fields>;
}
export interface RequestBodyFile<MimeType extends string> {
readonly _tag: "File";
readonly mimeType: MimeTypePredicate<MimeType>;
}
export namespace RequestBodyText {
export type Any = RequestBodyText<S.Schema<any, string, unknown>>;
}
export namespace RequestBodyJson {
export type Any = RequestBodyJson<S.Schema.Any>;
}
export namespace RequestBodyUrlEncoded {
export type Any = RequestBodyUrlEncoded<{}>;
}
export namespace RequestBodyMultipart {
export type Any = RequestBodyMultipart<{}>;
}
export namespace RequestBodyFile {
export type Any = RequestBodyFile<string>;
}
export namespace RequestBody {
export type Any =
| RequestBodyNone
| RequestBodyText.Any
| RequestBodyJson.Any
| RequestBodyUrlEncoded.Any
| RequestBodyMultipart.Any
| RequestBodyFile.Any
;
}
// --- CONSTRUCTORS ------------------------------------------------------------
export const RequestBodyNone: RequestBodyNone = Object.freeze<RequestBodyNone>({
_tag: "None",
});
export const RequestBodyText = <const Schema extends S.Schema<any, string, unknown>>(schema: Schema): RequestBodyText<Schema> => Object.freeze<RequestBodyText<Schema>>({
_tag: "Text",
schema,
});
export const RequestBodyJson = <const Schema extends S.Schema.Any>(schema: Schema): RequestBodyJson<Schema> => Object.freeze<RequestBodyJson<Schema>>({
_tag: "Json",
schema,
});
export const RequestBodyUrlEncoded = <const Fields extends { readonly [_: string]: Field.AnyUrlEncoded }>(fields: Fields): RequestBodyUrlEncoded<Fields> => Object.freeze<RequestBodyUrlEncoded<Fields>>({
_tag: "UrlEncoded",
fields,
});
export const RequestBodyMultipart = <const Fields extends { readonly [_: string]: Field.Any }>(fields: Fields): RequestBodyMultipart<Fields> => Object.freeze<RequestBodyMultipart<Fields>>({
_tag: "Multipart",
fields,
});
export const RequestBodyFile = <const MimeType extends string>(mimeType: MimeTypePredicate<MimeType>): RequestBodyFile<MimeType> => Object.freeze<RequestBodyFile<MimeType>>({
_tag: "File",
mimeType,
});

View File

@ -0,0 +1,90 @@
/*!
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Schema as S } from "@effect/schema";
import type { MimeTypePredicate } from "./MimeType";
// --- INTERFACES --------------------------------------------------------------
export interface ResponseBodyNone {
readonly _tag: "None";
}
export interface ResponseBodyText<Schema extends S.Schema<any, string, unknown>> {
readonly _tag: "Text";
readonly schema: Schema;
}
export interface ResponseBodyJson<Schema extends S.Schema.Any> {
readonly _tag: "Json";
readonly schema: Schema;
}
export interface ResponseBodyFile<MimeType extends string> {
readonly _tag: "File";
readonly mimeType: MimeTypePredicate<MimeType>;
}
export namespace ResponseBodyText {
export type Any = ResponseBodyText<S.Schema<any, string, unknown>>;
}
export namespace ResponseBodyJson {
export type Any = ResponseBodyJson<S.Schema.Any>;
}
export namespace ResponseBodyFile {
export type Any = ResponseBodyFile<string>;
}
export namespace ResponseBody {
export type Any =
| ResponseBodyNone
| ResponseBodyText.Any
| ResponseBodyJson.Any
| ResponseBodyFile.Any
;
}
// --- CONSTRUCTORS ------------------------------------------------------------
export const ResponseBodyNone: ResponseBodyNone = Object.freeze<ResponseBodyNone>({
_tag: "None",
});
export const ResponseBodyText = <const Schema extends S.Schema<any, string, unknown>>(schema: Schema): ResponseBodyText<Schema> => Object.freeze<ResponseBodyText<Schema>>({
_tag: "Text",
schema,
});
export const ResponseBodyJson = <const Schema extends S.Schema.Any>(schema: Schema): ResponseBodyJson<Schema> => Object.freeze<ResponseBodyJson<Schema>>({
_tag: "Json",
schema,
});
export const ResponseBodyFile = <const MimeType extends string>(mimeType: MimeTypePredicate<MimeType>): ResponseBodyFile<MimeType> => Object.freeze<ResponseBodyFile<MimeType>>({
_tag: "File",
mimeType,
});
export const ResponseEmpty: {} = Object.freeze<{}>({});
import type { Option as O } from "effect";
export type Multiplicity =
| "single"
| "optional"
| "multiple"
| "any"
;
export namespace Multiplicity {
export type With<M extends Multiplicity, T> = {
single: T,
optional: O.Option<T>,
multiple: readonly [T, ...T[]],
any: readonly T[],
}[M];
}

50
packages/api/src/Route.ts Normal file
View File

@ -0,0 +1,50 @@
/*!
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Schema as S } from "@effect/schema";
// --- INTERFACES --------------------------------------------------------------
export interface RouteLiteral<Literal extends string> {
readonly _tag: "Literal";
readonly literal: Literal;
}
export interface RouteParam<Name extends string, Schema extends S.Schema<any, string, unknown>> {
readonly _tag: "Param";
readonly name: Name;
readonly schema: Schema;
}
export namespace RouteLiteral {
export type Any = RouteLiteral<string>;
}
export namespace RouteParam {
export type Any = RouteParam<string, S.Schema<any, string, unknown>>;
}
export namespace Route {
export type Any = readonly (
| RouteLiteral.Any
| RouteParam.Any
)[];
}
// --- CONSTRUCTORS ------------------------------------------------------------
export const RouteLiteral = <const Literal extends string>(literal: Literal): RouteLiteral<Literal> => Object.freeze<RouteLiteral<Literal>>({
_tag: "Literal",
literal,
});
export const RouteParam = <const Name extends string, const Schema extends S.Schema<any, string, unknown>>(name: Name, schema: Schema): RouteParam<Name, Schema> => Object.freeze<RouteParam<Name, Schema>>({
_tag: "Param",
name,
schema,
});
export const Route = <const Route extends Route.Any>(...route: Route): Route => Object.freeze<Route>(route);

View File

@ -0,0 +1,7 @@
{
"name": "@make-api/backend",
"type": "module",
"devDependencies": {
"@types/bun": "latest"
}
}

107
packages/backend/src/Api.ts Normal file
View File

@ -0,0 +1,107 @@
/*!
* This Source Code Form is subject to the terms of the Mozilla Public License,
* v. 2.0. If a copy of the MPL was not distributed with this file, You can
* obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Schema as S } from "@effect/schema";
import * as Api from "@make-api/api/Api";
import * as Query from "@make-api/api/Query";
import * as RequestBody from "@make-api/api/RequestBody";
import * as Route from "@make-api/api/Route";
import { Array as A, Effect, Match, Option as O, Record as R, pipe } from "effect";
const getUsers = pipe(
Api.make("GET", "/users"),
Api.responseBodyJson(200, S.Array(S.Struct({
userId: S.UUID,
username: S.String,
email: S.String,
}))),
Api.responseBodyJson(500, S.Struct({
error: S.String,
stackTrace: S.optional(S.String, { exact: true }),
})),
);
type RouteParams<T extends Route.Route.Any> = { readonly [_: string]: unknown }; // TODO
const matchesRoute = <T extends Route.Route.Any>(route: T, pathComponents: readonly string[]): boolean => {
if (route.length !== pathComponents.length) return false;
return pipe(
A.zip(route, pathComponents),
A.every(([token, pathComponent]) => token._tag !== "Literal" || token.literal === pathComponent),
);
};
const parseRoute = <T extends Route.Route.Any>(route: T) => {
const decoder = pipe(
route,
A.filterMap((r) => r._tag === "Param" ? O.some([r.name, r.schema] as const) : O.none()),
R.fromEntries,
S.Struct,
(_) => S.decode(_, { errors: "all" }),
);
return (pathComponents: readonly string[]) => pipe(
pathComponents,
A.filterMap((c, i) => {
const r = route[i];
return r._tag === "Param" ? O.some([r.name, c] as const) : O.none();
}),
R.fromEntries,
decoder,
);
};
const parseQuery = <T extends Query.Query.Any>(query: T) => {
const decoder = pipe();
return (searchParams: URLSearchParams) => pipe();
};
const parseBody = <T extends RequestBody.RequestBody.Any>(requestBody: T) => {
const decoder = pipe(
Match.value<RequestBody.RequestBody.Any>(requestBody),
Match.tags({
None: () => {},
Text: () => {},
Json: () => {},
UrlEncoded: () => {},
Multipart: () => {},
File: () => {},
}),
);
return (req: Request) => pipe();
};
const routeParser = parseRoute(getUsers.props.route);
const queryParser = parseQuery(getUsers.props.query);
const bodyParser = parseBody(getUsers.props.request);
Bun.serve({
fetch: (req) => {
const url = new URL(req.url);
const pathname = url.pathname;
const pathComponents = pathname.replaceAll(/^\/|\/$/g, "").split("/");
const searchParams = url.searchParams;
if (matchesRoute(getUsers.props.route, pathComponents)) {
const paramsEffect = routeParser(pathComponents);
const queryEffect = queryParser(searchParams);
const bodyEffect = bodyParser(req);
const effect = Effect.all({
params: paramsEffect,
query: queryEffect,
body: bodyEffect,
});
}
},
websocket: {
message: (ws, message) => {}
},
});

View File

@ -1,61 +0,0 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
'@effect/schema':
specifier: ^0.64.3
version: 0.64.3(effect@2.4.6)(fast-check@3.16.0)
effect:
specifier: ^2.4.6
version: 2.4.6
fast-check:
specifier: ^3.16.0
version: 3.16.0
tslib:
specifier: ^2.6.2
version: 2.6.2
devDependencies:
typescript:
specifier: ^5.4.2
version: 5.4.2
packages:
/@effect/schema@0.64.3(effect@2.4.6)(fast-check@3.16.0):
resolution: {integrity: sha512-Pn/IG+X9GTXLVvDtdjptr3QWxpcH+m7LEgM6loLWk7DP8SPKqWdo3rFaZiILpJFci8QWEk7K23C3IJZOnMfTtA==}
peerDependencies:
effect: ^2.4.6
fast-check: ^3.13.2
dependencies:
effect: 2.4.6
fast-check: 3.16.0
dev: false
/effect@2.4.6:
resolution: {integrity: sha512-gaZC+aopWvS67SX8RFT3mirabjpVBk5qshj/Z3IKW35DjzpQ8WhAE7VWwfBBWWMBuQ/B0RsbzKR8YGN2AivIgQ==}
dev: false
/fast-check@3.16.0:
resolution: {integrity: sha512-k8GtQHi4pJoRQ1gVDFQno+/FVkowo/ehiz/aCj9O/D7HRWb1sSFzNrw+iPVU8QlWtH+jNwbuN+dDVg3QkS56DQ==}
engines: {node: '>=8.0.0'}
dependencies:
pure-rand: 6.0.4
dev: false
/pure-rand@6.0.4:
resolution: {integrity: sha512-LA0Y9kxMYv47GIPJy6MI84fqTd2HmYZI83W/kM/SkKfDlajnZYfmXFTxkbY+xSBPkLJxltMa9hIkmdc29eguMA==}
dev: false
/tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
dev: false
/typescript@5.4.2:
resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==}
engines: {node: '>=14.17'}
hasBin: true
dev: true

View File

@ -1,147 +0,0 @@
import * as S from "@effect/schema/Schema";
import * as O from "effect/Option";
import * as Types from "effect/Types";
// --- METHOD ------------------------------------------------------------------
export type Method =
| "GET"
| "POST"
| "PUT"
| "PATCH"
| "DELETE"
;
// --- ROUTE INPUT -------------------------------------------------------------
export type RouteInputLiteral<K extends string> = K;
export type RouteInputSchema<K extends string, T> = readonly [name: K, schema: S.Schema<T, string>];
export type RouteInputToken =
| RouteInputLiteral<string>
| RouteInputSchema<string, any>
;
// --- ROUTE -------------------------------------------------------------------
export enum RouteTokenType {
Literal = "literal",
Schema = "schema",
}
export interface RouteTokenLiteral<K extends string> {
readonly type: RouteTokenType.Literal,
readonly literal: K,
}
export interface RouteTokenSchema<K extends string, T> {
readonly type: RouteTokenType.Schema;
readonly name: K;
readonly schema: S.Schema<T, string>;
}
export type RouteToken =
| RouteTokenLiteral<string>
| RouteTokenSchema<string, any>
;
export function routeTokenLiteral<const K extends string>(literal: K): RouteTokenLiteral<K> {
return Object.freeze<RouteTokenLiteral<K>>({
type: RouteTokenType.Literal,
literal,
});
}
export function routeTokenSchema<const K extends string, const T>(name: K, schema: S.Schema<T, string>): RouteTokenSchema<K, T> {
return Object.freeze<RouteTokenSchema<K, T>>({
type: RouteTokenType.Schema,
name,
schema,
});
}
// --- ROUTE FROM INPUT --------------------------------------------------------
export type RouteFromInput<I extends readonly RouteInputToken[]> = {
readonly [K in keyof I]:
I[K] extends RouteInputLiteral<infer _K> ? RouteTokenLiteral<_K>
: I[K] extends RouteInputSchema<infer _K, infer _T> ? RouteTokenSchema<_K, _T>
: never;
}
export type ParamFromRouteToken<T extends RouteToken> =
T extends RouteTokenSchema<infer K, infer V> ? { readonly [_ in K]: V } : never;
export type ParamsFromRoute<I extends readonly RouteToken[]> =
Types.Simplify<Types.UnionToIntersection<ParamFromRouteToken<I[number]>>>;
export function makeRoute<const RouteInput extends readonly RouteInputToken[]>(...tokens: RouteInput): RouteFromInput<RouteInput> {
const names = new Set<string>();
const ret = new Array<RouteToken>(tokens.length);
for (let i = 0; i < tokens.length; ++i) {
const token = tokens[i];
if (typeof token === "string") {
ret[i] = routeTokenLiteral(token);
} else {
const [name, schema] = token;
if (names.has(name)) {
throw new Error(`Duplicate URL parameter name in route: ${name}`);
}
ret[i] = routeTokenSchema(name, schema);
}
}
return Object.freeze(ret) as RouteFromInput<RouteInput>;
}
// --- API ---------------------------------------------------------------------
export interface ApiBase {
readonly requestBody: O.Option<S.Schema<any, any>>;
readonly query: O.Option<S.Schema<any, { readonly [_: string]: string }>>;
readonly responseBody: O.Option<S.Schema<any, any>>;
readonly method: Method;
readonly route: readonly RouteToken[];
}
export type RequestType<T extends ApiBase> = S.Schema.Type<O.Option.Value<T["requestBody"]>>;
export type RequestEncoded<T extends ApiBase> = S.Schema.Encoded<O.Option.Value<T["requestBody"]>>;
export type QueryType<T extends ApiBase> = S.Schema.Type<O.Option.Value<T["query"]>>;
export type QueryEncoded<T extends ApiBase> = S.Schema.Encoded<O.Option.Value<T["query"]>>;
export type ResponseType<T extends ApiBase> = S.Schema.Type<O.Option.Value<T["responseBody"]>>;
export type ResponseEncoded<T extends ApiBase> = S.Schema.Encoded<O.Option.Value<T["responseBody"]>>;
export type Params<T extends ApiBase> = ParamsFromRoute<T["route"]>;
// --- MAKE API ----------------------------------------------------------------
export interface MakeApiProps {
readonly requestBody?: S.Schema<any, any>;
readonly query?: S.Schema<any, any>;
readonly responseBody?: S.Schema<any, any>;
readonly method: Method;
readonly route: readonly RouteInputToken[];
}
export interface MakeApi<P extends MakeApiProps> {
readonly requestBody: P extends { readonly requestBody: S.Schema<infer A, infer I> } ? O.Some<S.Schema<A, I>> : O.None<never>;
readonly query: P extends { readonly query: S.Schema<infer A, infer I> } ? O.Some<S.Schema<A, I>> : O.None<never>;
readonly responseBody: P extends { readonly responseBody: S.Schema<infer A, infer I> } ? O.Some<S.Schema<A, I>> : O.None<never>;
readonly method: P["method"];
readonly route: RouteFromInput<P["route"]>;
}
export function make<const P extends MakeApiProps>(props: P): MakeApi<P> {
return Object.freeze<any>({
requestBody: O.fromNullable(props.requestBody),
query: O.fromNullable(props.query),
responseBody: O.fromNullable(props.responseBody),
method: props.method,
route: makeRoute(...props.route),
} satisfies ApiBase) as MakeApi<P>;
}

View File

@ -1,102 +0,0 @@
/// <reference lib="dom" />
import * as Effect from "effect/Effect";
import { pipe } from "effect/Function";
import * as Option from "effect/Option";
import * as Predicate from "effect/Predicate";
import * as Stream from "effect/Stream";
export const FetchExceptionTypeId: unique symbol = Symbol.for("effect-api/Fetch/errors/FetchException");
export const RequestArrayBufferExceptionTypeId: unique symbol = Symbol.for("effect-api/Fetch/errors/RequestArrayBufferException");
export type FetchExceptionTypeId = typeof FetchExceptionTypeId;
export type RequestArrayBufferExceptionTypeId = typeof RequestArrayBufferExceptionTypeId;
export interface FetchException extends Error {
readonly _tag: "FetchException"
readonly [FetchExceptionTypeId]: FetchExceptionTypeId;
readonly error: TypeError;
}
export interface RequestArrayBufferException extends Error {
readonly _tag: "RequestArrayBufferException"
readonly [RequestArrayBufferExceptionTypeId]: RequestArrayBufferExceptionTypeId;
readonly error: RangeError;
}
const FetchExceptionProto = Object.create(Error.prototype, {
_tag: { value: "FetchException" },
[FetchExceptionTypeId]: { value: FetchExceptionTypeId },
});
const RequestArrayBufferExceptionProto = Object.create(Error.prototype, {
_tag: { value: "RequestArrayBufferException" },
[RequestArrayBufferExceptionTypeId]: { value: RequestArrayBufferExceptionTypeId },
});
export const FetchException: (error: TypeError) => FetchException = (error) => Object.create(FetchExceptionProto, {
error: { value: error, enumerable: true },
});
export const RequestArrayBufferException: (error: RangeError) => RequestArrayBufferException = (error) => Object.create(RequestArrayBufferExceptionProto, {
error: { value: error, enumerable: true },
});
export const isFetchException: (u: unknown) => u is FetchException = (u): u is FetchException => Predicate.hasProperty(u, FetchExceptionTypeId);
export const isRequestArrayBufferException: (u: unknown) => u is RequestArrayBufferException = (u): u is RequestArrayBufferException => Predicate.hasProperty(u, RequestArrayBufferExceptionTypeId);
const _wrapResponseHandler: ProxyHandler<Response> = Object.freeze<ProxyHandler<Response>>({
get: (target, p, receiver) => {
switch (p) {
case "body":
return <E>(onError: (error: unknown) => E): Option.Option<Stream.Stream<never, E, Uint8Array>> => {
return pipe(
Reflect.get(target, p, receiver),
Option.fromNullable,
Option.map(Stream.fromReadableStream())
);
};
}
}
});
const _wrapResponse: (response: Response) => _Response = (response) => {
};
const _fetch: (...args: Parameters<typeof fetch>) => Effect.Effect<never, FetchException, _Response> = (input, init) => {
return pipe(
Effect.tryPromise({
try: (signal) => fetch(input, { signal, ...init }),
catch: (error) => {
if (error instanceof TypeError) {
return FetchException(error);
}
throw error;
},
}),
Effect.map(_wrapResponse),
);
};
interface _Response {
body<E>(onError: (error: unknown) => E): Option.Option<Stream.Stream<never, E, Uint8Array>>;
readonly bodyUsed: boolean;
arrayBuffer(): Effect.Effect<never, RequestArrayBufferException, ArrayBuffer>;
blob(): Effect.Effect<never, ___, Blob>;
formData(): Effect.Effect<never, ___, FormData>;
json(): Effect.Effect<never, ___, unknown>;
text(): Effect.Effect<never, ___, string>;
readonly headers: Headers;
readonly ok: boolean;
readonly redirected: boolean;
readonly status: number;
readonly statusText: string;
readonly type: ResponseType;
readonly url: string;
clone(): _Response;
}
export {
_fetch as fetch,
type _Request as Request
};

View File

@ -1,66 +0,0 @@
export * as Api from "./Api.js";
// --- EXPERIMENTS -------------------------------------------------------------
import * as Api from "./Api.js";
import * as S from "@effect/schema/Schema";
const Id = S.struct({ _id: S.ULID });
const Product = S.struct({
name: S.string,
price: S.bigint,
});
const WithId = S.extend(Id);
const LoginRequest = S.struct({
username: S.string,
password: S.string,
});
const login = Api.make({
method: "POST",
route: ["login"],
requestBody: LoginRequest,
});
const logout = Api.make({
method: "POST",
route: ["logout"],
});
const getProducts = Api.make({
method: "GET",
route: ["products"],
responseBody: S.array(WithId(Product)),
});
const getProduct = Api.make({
method: "GET",
route: ["products", ["_id", S.ULID]],
responseBody: WithId(Product),
});
const postProduct = Api.make({
method: "POST",
route: ["products"],
requestBody: Product, responseBody: Id,
});
const putProduct = Api.make({
method: "PUT",
route: ["products", ["_id", S.ULID]],
requestBody: Product,
});
const patchProduct = Api.make({
method: "PATCH",
route: ["products", ["_id", S.ULID]],
requestBody: S.partial(Product),
});
const deleteProduct = Api.make({
method: "POST",
route: ["products", ["_id", S.ULID]],
});

View File

@ -3,15 +3,12 @@
"incremental": true,
"composite": true,
"module": "NodeNext",
"moduleResolution": "NodeNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"moduleDetection": "force",
"outDir": "./dist/",
"rootDir": "./src/",
"tsBuildInfoFile": "./.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2022"],
"target": "ESNext",
"lib": ["ESNext", "DOM"],
"sourceMap": true,
"declaration": true,
@ -48,9 +45,17 @@
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"isolatedModules": true,
"importHelpers": true,
"importHelpers": false,
"skipLibCheck": false,
},
"include": ["./src/**/*"],
"paths": {
"@make-api/api": ["./packages/api/src/index.ts"],
"@make-api/api/*": ["./packages/api/src/*"],
"@make-api/backend": ["./packages/backend/src/index.ts"],
"@make-api/backend/*": ["./packages/backend/src/*"],
"@make-api/frontend": ["./packages/frontend/src/index.ts"],
"@make-api/frontend/*": ["./packages/frontend/src/*"],
}
}
}