summaryrefslogtreecommitdiff
path: root/u/server
diff options
context:
space:
mode:
authorElizabeth Alexander Hunt <me@liz.coffee>2025-05-12 09:40:12 -0700
committerElizabeth <me@liz.coffee>2025-05-26 14:15:42 -0700
commitd51c9d74857aca3c2f172609297266968bc7f809 (patch)
tree64327f9cc4219729aa11af32d7d4c70cddfc2292 /u/server
parent30729a0cf707d9022bae0a7baaba77379dc31fd5 (diff)
downloadci-d51c9d74857aca3c2f172609297266968bc7f809.tar.gz
ci-d51c9d74857aca3c2f172609297266968bc7f809.zip
The big refactor TM
Diffstat (limited to 'u/server')
-rw-r--r--u/server/activity/fourohfour.ts36
-rw-r--r--u/server/activity/health.ts67
-rw-r--r--u/server/activity/mod.ts13
-rw-r--r--u/server/filter/json.ts51
-rw-r--r--u/server/filter/method.ts41
-rw-r--r--u/server/filter/mod.ts33
-rw-r--r--u/server/mod.ts7
-rw-r--r--u/server/request.ts57
-rw-r--r--u/server/response.ts84
9 files changed, 389 insertions, 0 deletions
diff --git a/u/server/activity/fourohfour.ts b/u/server/activity/fourohfour.ts
new file mode 100644
index 0000000..6449abd
--- /dev/null
+++ b/u/server/activity/fourohfour.ts
@@ -0,0 +1,36 @@
+import {
+ type IActivity,
+ type ITraceable,
+ JsonResponse,
+ type PenguenoRequest,
+ type ServerTrace,
+} from "@emprespresso/pengueno";
+
+const messages = [
+ "(≧ω≦)ゞ Oopsie! This endpoint has gone a-404-dable!",
+ "。゚(。ノωヽ。)゚。 Meow-t found! Your API call ran away!",
+ "404-bidden! But like...in a cute way (・`ω´・) !",
+ "(=①ω①=) This endpoint is hiss-terically missing!",
+ "┐(´∀`)┌ Whoopsie fluff! No API here!",
+ "(つ≧▽≦)つ Your data went on a paw-sible vacation!",
+ "(ꈍᴗꈍ) Uwu~ not found, but found our hearts instead!",
+ "ヽ(;▽;)ノ Eep! This route has ghosted you~",
+];
+const randomFourOhFour = () => messages[Math.random() * messages.length];
+
+export interface IFourOhFourActivity {
+ fourOhFour: IActivity;
+}
+
+export class FourOhFourActivityImpl implements IFourOhFourActivity {
+ public fourOhFour(
+ req: ITraceable<PenguenoRequest, ServerTrace>,
+ ) {
+ return req
+ .move(
+ new JsonResponse(req, randomFourOhFour(), { status: 404 }),
+ )
+ .map((resp) => Promise.resolve(resp.get()))
+ .get();
+ }
+}
diff --git a/u/server/activity/health.ts b/u/server/activity/health.ts
new file mode 100644
index 0000000..0f54a99
--- /dev/null
+++ b/u/server/activity/health.ts
@@ -0,0 +1,67 @@
+import {
+ type IActivity,
+ type IEither,
+ type ITraceable,
+ JsonResponse,
+ LogLevel,
+ type Mapper,
+ Metric,
+ type PenguenoRequest,
+ type ServerTrace,
+ TraceUtil,
+} from "@emprespresso/pengueno";
+
+export enum HealthCheckInput {
+ CHECK,
+}
+export enum HealthCheckOutput {
+ YAASSSLAYQUEEN,
+}
+
+export interface IHealthCheckActivity {
+ checkHealth: IActivity;
+}
+
+const healthCheckMetric = Metric.fromName("Health");
+export interface HealthChecker extends
+ Mapper<
+ ITraceable<HealthCheckInput, ServerTrace>,
+ Promise<IEither<Error, HealthCheckOutput>>
+ > {}
+export class HealthCheckActivityImpl implements IHealthCheckActivity {
+ constructor(
+ private readonly check: HealthChecker,
+ ) {}
+
+ public checkHealth(req: ITraceable<PenguenoRequest, ServerTrace>) {
+ return req
+ .bimap(TraceUtil.withFunctionTrace(this.checkHealth))
+ .bimap(TraceUtil.withMetricTrace(healthCheckMetric))
+ .flatMap((r) => r.move(HealthCheckInput.CHECK).map(this.check))
+ .peek(TraceUtil.promiseify((h) =>
+ h.get().fold((err) => {
+ if (err) {
+ h.trace.trace(healthCheckMetric.failure);
+ h.trace.addTrace(LogLevel.ERROR).trace(`${err}`);
+ return;
+ }
+ h.trace.trace(healthCheckMetric.success);
+ })
+ ))
+ .map(TraceUtil.promiseify((h) =>
+ h.get()
+ .mapBoth(
+ () => "oh no, i need to eat more vegetables (。•́︿•̀。)...",
+ () => "think im healthy!! (✿˘◡˘) ready to do work~",
+ )
+ .fold((errMsg, okMsg) =>
+ new JsonResponse(
+ req,
+ errMsg ?? okMsg,
+ { status: errMsg ? 500 : 200 },
+ )
+ )
+ ))
+ .get();
+ }
+}
diff --git a/u/server/activity/mod.ts b/u/server/activity/mod.ts
new file mode 100644
index 0000000..82d8ec4
--- /dev/null
+++ b/u/server/activity/mod.ts
@@ -0,0 +1,13 @@
+import type {
+ ITraceable,
+ PenguenoRequest,
+ PenguenoResponse,
+ ServerTrace,
+} from "@emprespresso/pengueno";
+
+export interface IActivity {
+ (req: ITraceable<PenguenoRequest, ServerTrace>): Promise<PenguenoResponse>;
+}
+
+export * from "./health.ts";
+export * from "./fourohfour.ts";
diff --git a/u/server/filter/json.ts b/u/server/filter/json.ts
new file mode 100644
index 0000000..4a2961e
--- /dev/null
+++ b/u/server/filter/json.ts
@@ -0,0 +1,51 @@
+import {
+ Either,
+ type IEither,
+ type ITraceable,
+ LogLevel,
+ Metric,
+ PenguenoError,
+ type PenguenoRequest,
+ type RequestFilter,
+ type ServerTrace,
+ TraceUtil,
+} from "@emprespresso/pengueno";
+
+export interface JsonTransformer<R, ParsedJson = unknown> {
+ (json: ITraceable<ParsedJson, ServerTrace>): IEither<PenguenoError, R>;
+}
+
+const ParseJsonMetric = Metric.fromName("JsonParse");
+export const jsonModel = <MessageT>(
+ jsonTransformer: JsonTransformer<MessageT>,
+): RequestFilter<MessageT> =>
+(r: ITraceable<PenguenoRequest, ServerTrace>) =>
+ r.bimap(TraceUtil.withMetricTrace(ParseJsonMetric))
+ .map((j) =>
+ Either.fromFailableAsync<Error, MessageT>(j.get().json())
+ .then((either) =>
+ either.mapLeft((errReason) => {
+ j.trace.addTrace(LogLevel.WARN).trace(`${errReason}`);
+ return new PenguenoError(
+ "seems to be invalid JSON (>//<) can you fix?",
+ 400,
+ );
+ })
+ )
+ )
+ .peek(
+ TraceUtil.promiseify((traceableEither) =>
+ traceableEither.get().mapBoth(
+ () => traceableEither.trace.trace(ParseJsonMetric.failure),
+ () => traceableEither.trace.trace(ParseJsonMetric.success),
+ )
+ ),
+ )
+ .map(
+ TraceUtil.promiseify((traceableEitherJson) =>
+ traceableEitherJson.get()
+ .mapRight(traceableEitherJson.move)
+ .flatMap(jsonTransformer)
+ ),
+ )
+ .get();
diff --git a/u/server/filter/method.ts b/u/server/filter/method.ts
new file mode 100644
index 0000000..6b0419d
--- /dev/null
+++ b/u/server/filter/method.ts
@@ -0,0 +1,41 @@
+import {
+ Either,
+ type ITraceable,
+ LogLevel,
+ PenguenoError,
+ type PenguenoRequest,
+ type RequestFilter,
+ type ServerTrace,
+ TraceUtil,
+} from "@emprespresso/pengueno";
+
+type HttpMethod =
+ | "POST"
+ | "GET"
+ | "HEAD"
+ | "PUT"
+ | "DELETE"
+ | "CONNECT"
+ | "OPTIONS"
+ | "TRACE"
+ | "PATCH";
+
+export const requireMethod = (
+ methods: Array<HttpMethod>,
+): RequestFilter<HttpMethod> =>
+(req: ITraceable<PenguenoRequest, ServerTrace>) =>
+ req.bimap(TraceUtil.withFunctionTrace(requireMethod))
+ .move(Promise.resolve(req.get()))
+ .map(TraceUtil.promiseify((t) => {
+ const { method: _method } = t.get();
+ const method = <HttpMethod> _method;
+ if (!methods.includes(method)) {
+ const msg = "that's not how you pet me (⋟﹏⋞)~";
+ t.trace.addTrace(LogLevel.WARN).trace(msg);
+ return Either.left<PenguenoError, HttpMethod>(
+ new PenguenoError(msg, 405),
+ );
+ }
+ return Either.right<PenguenoError, HttpMethod>(method);
+ }))
+ .get();
diff --git a/u/server/filter/mod.ts b/u/server/filter/mod.ts
new file mode 100644
index 0000000..bbf37df
--- /dev/null
+++ b/u/server/filter/mod.ts
@@ -0,0 +1,33 @@
+import {
+ type IEither,
+ type ITraceable,
+ LogLevel,
+ type PenguenoRequest,
+ type ServerTrace,
+} from "@emprespresso/pengueno";
+
+export enum ErrorSource {
+ USER = LogLevel.WARN,
+ SYSTEM = LogLevel.ERROR,
+}
+
+export class PenguenoError extends Error {
+ public readonly source: ErrorSource;
+ constructor(message: string, public readonly status: number) {
+ super(message);
+ this.source = Math.floor(status / 100) === 4
+ ? ErrorSource.USER
+ : ErrorSource.SYSTEM;
+ }
+}
+
+export interface RequestFilter<
+ T,
+ Err extends PenguenoError = PenguenoError,
+ RIn = ITraceable<PenguenoRequest, ServerTrace>,
+> {
+ (req: RIn): Promise<IEither<Err, T>>;
+}
+
+export * from "./method.ts";
+export * from "./json.ts";
diff --git a/u/server/mod.ts b/u/server/mod.ts
new file mode 100644
index 0000000..866b5f9
--- /dev/null
+++ b/u/server/mod.ts
@@ -0,0 +1,7 @@
+import type { LogMetricTraceSupplier } from "@emprespresso/pengueno";
+export type ServerTrace = LogMetricTraceSupplier;
+
+export * from "./activity/mod.ts";
+export * from "./filter/mod.ts";
+export * from "./response.ts";
+export * from "./request.ts";
diff --git a/u/server/request.ts b/u/server/request.ts
new file mode 100644
index 0000000..7aa9917
--- /dev/null
+++ b/u/server/request.ts
@@ -0,0 +1,57 @@
+import { LogMetricTraceable } from "@emprespresso/pengueno";
+
+const greetings = [
+ "hewwo :D",
+ "hiya cutie (✿◠‿◠)",
+ "boop! ૮・ᴥ・ა",
+ "sending virtual hugs! (づ。◕‿‿◕。)づ",
+ "stay pawsitive ₍^..^₎⟆",
+ "⋆。‧˚❆🐧❆˚‧。⋆",
+];
+const penguenoGreeting = () =>
+ greetings[Math.floor(Math.random() * greetings.length)];
+
+export class PenguenoRequest extends Request {
+ private constructor(
+ _input: URL,
+ _requestInit: RequestInit,
+ public readonly id: string,
+ public readonly at: Date,
+ ) {
+ super(_input, _requestInit);
+ }
+
+ public baseResponseHeaders(): Record<string, string> {
+ const ServerRequestTime = this.at.getTime();
+ const ServerResponseTime = Date.now();
+ const DeltaTime = ServerResponseTime - ServerRequestTime;
+ const RequestId = this.id;
+
+ return Object.entries({
+ RequestId,
+ ServerRequestTime,
+ ServerResponseTime,
+ DeltaTime,
+ Hai: penguenoGreeting(),
+ }).reduce((acc, [key, val]) => ({ ...acc, [key]: (val.toString()) }), {});
+ }
+
+ public static from(
+ request: Request,
+ ): LogMetricTraceable<PenguenoRequest> {
+ const id = crypto.randomUUID();
+ const url = new URL(request.url);
+ const { pathname } = url;
+ const traceSupplier = () => `[${id} <- ${request.method}'d @ ${pathname}]`;
+ return LogMetricTraceable
+ .from(
+ new PenguenoRequest(
+ url,
+ { ...request },
+ id,
+ new Date(),
+ ),
+ )
+ .bimap((_request) => [_request.get(), traceSupplier]);
+ }
+}
diff --git a/u/server/response.ts b/u/server/response.ts
new file mode 100644
index 0000000..c21819a
--- /dev/null
+++ b/u/server/response.ts
@@ -0,0 +1,84 @@
+import {
+ type IEither,
+ isEither,
+ type ITraceable,
+ Metric,
+ type PenguenoRequest,
+ type ServerTrace,
+} from "@emprespresso/pengueno";
+
+export type ResponseBody = object | string;
+export type TResponseInit = ResponseInit & {
+ status: number;
+ headers?: Record<string, string>;
+};
+
+const getResponse = (
+ req: PenguenoRequest,
+ opts: TResponseInit,
+): TResponseInit => {
+ return {
+ ...opts,
+ headers: {
+ ...(req.baseResponseHeaders()),
+ ...(opts?.headers),
+ "Content-Type": (opts?.headers?.["Content-Type"] ?? "text/plain") +
+ "; charset=utf-8",
+ },
+ };
+};
+
+const ResponseCodeMetrics = [1, 2, 3, 4, 5].map((x) =>
+ Metric.fromName(`response.${x}xx`)
+);
+export const getResponseMetric = (status: number) => {
+ const index = (Math.floor(status / 100)) + 1;
+ return ResponseCodeMetrics[index] ?? ResponseCodeMetrics[5 - 1];
+};
+
+export class PenguenoResponse extends Response {
+ constructor(
+ req: ITraceable<PenguenoRequest, ServerTrace>,
+ msg: BodyInit,
+ opts: TResponseInit,
+ ) {
+ const responseOpts = getResponse(req.get(), opts);
+ const resMetric = getResponseMetric(opts.status);
+ req.trace.trace(resMetric.count.withValue(1.0));
+ responseOpts.headers;
+ super(msg, responseOpts);
+ }
+}
+
+export class JsonResponse extends PenguenoResponse {
+ constructor(
+ req: ITraceable<PenguenoRequest, ServerTrace>,
+ e: BodyInit | IEither<ResponseBody, ResponseBody>,
+ opts: TResponseInit,
+ ) {
+ const optsWithJsonContentType = {
+ ...opts,
+ headers: {
+ ...opts?.headers,
+ "Content-Type": "application/json",
+ },
+ };
+ if (isEither<ResponseBody, ResponseBody>(e)) {
+ super(
+ req,
+ JSON.stringify(
+ e.fold((err, ok) => err ? ({ error: err! }) : ({ ok: ok! })),
+ ),
+ optsWithJsonContentType,
+ );
+ return;
+ }
+ super(
+ req,
+ JSON.stringify(
+ (Math.floor(opts.status / 100) < 4) ? { ok: e } : { error: e },
+ ),
+ optsWithJsonContentType,
+ );
+ }
+}