diff options
Diffstat (limited to 'hooks/server.ts')
-rwxr-xr-x | hooks/server.ts | 272 |
1 files changed, 272 insertions, 0 deletions
diff --git a/hooks/server.ts b/hooks/server.ts new file mode 100755 index 0000000..000c391 --- /dev/null +++ b/hooks/server.ts @@ -0,0 +1,272 @@ +#!/usr/bin/env -S deno run --allow-env --allow-net --allow-run + +import { + Either, + getRequiredEnv, + getStdout, + IEither, + isObject, + Traceable, + validateExecutionEntries, +} from "@liz-ci/utils"; +import { IJobQueuer } from "./mod.ts"; +import type { Job } from "@liz-ci/model"; + +const SERVER_CONFIG = { + host: "0.0.0.0", + port: 9000, +}; +interface IHealthCheckActivity<R> { + healthCheck(req: R): Promise<Response>; +} + +class HealthCheckActivity implements IHealthCheckActivity<Traceable<Request>> { + public async healthCheck( + req: Traceable<Request>, + ) { + return await req.bimap(Traceable.withClassTrace(this)) + .map(async (r) => { + const { logger } = r; + try { + getRequiredEnv("LAMINAR_HOST"); + await getStdout(r.map(() => ["laminarc", "show-jobs"])); + const msg = `think im healthy!! (✿˘◡˘) ready to do work~\n`; + logger.log(msg); + return new Response( + msg, + { status: 200 }, + ); + } catch (error) { + logger.error(error); + return new Response( + "oh no, i need to eat more vegetables (。•́︿•̀。)...\n", + { status: 500 }, + ); + } + }).item; + } +} + +const aPost = (req: Traceable<Request>): IEither<Response, Request> => { + const { item: request, logger: _logger } = req; + const logger = _logger.addTracer(() => "[aPost]"); + + if (request.method !== "POST") { + const msg = "that's not how you pet me (⋟﹏⋞) try post instead~"; + logger.warn(msg); + return Either.left(new Response(msg + "\n", { status: 405 })); + } + return Either.right(request); +}; + +type JsonTransformer<JsonT, R> = (json: Traceable<JsonT>) => Either<string, R>; +const aJson = + <BodyT, JsonT = unknown>(jsonTransformer: JsonTransformer<JsonT, BodyT>) => + async (r: Traceable<Request>): Promise<Either<string, BodyT>> => { + const { item: request, logger } = r; + try { + return Either.right<string, JsonT>(await request.json()) + .mapRight(r.move) + .flatMap(jsonTransformer); + } catch (_e) { + const err = "seems to be invalid JSON (>//<) can you fix?"; + logger.warn(err); + return Either.left(err); + } + }; + +interface IJobHookActivity<R> { + processHook(req: R): Promise<Response>; +} +type GetJobRequest = { jobType: string; args: unknown }; +class JobHookActivityImpl implements IJobHookActivity<Traceable<Request>> { + constructor(private readonly queuer: IJobQueuer<Traceable<Job>>) {} + + private getJob<JsonT>( + u: Traceable<JsonT>, + ): Either<string, Job> { + const { logger: _logger, item } = u; + const logger = _logger.addTracer(() => "[getJob]"); + const couldBeJsonJob = isObject(item) && "arguments" in item && + "type" in item && item; + const couldBeArguments = couldBeJsonJob && isObject(item.arguments) && + item.arguments; + if (!couldBeJsonJob) { + const err = "seems like a pwetty mawfomed job \\(-.-)/"; + logger.warn(err); + return Either.left(err); + } + + return validateExecutionEntries({ + type: item.type, + ...couldBeArguments, + }).mapBoth((err) => { + const e = "your reqwest seems invawid (´。﹏。`) can you fix? uwu\n" + + err.toString(); + logger.warn(e); + return e; + }, (_ok) => <Job> item); + } + + public async processHook(r: Traceable<Request>) { + return await r.bimap(Traceable.withClassTrace(this)).map(aPost) + .map(aJson(this.getJob)); + } + // flatMapAsync(aJsonPost(this.getJob)) + // .map(TraceableImpl.promiseify((g) => { + // if (jobRequest) { + // return g.map(() => jobRequest) + // .map(this.getJob) + // .map( + // ({ item: { ok: jobRequest, err } }) => { + // if (err) return { err: new Response(err, { status: 400 }) }; + // return { ok: jobRequest }; + // }, + // ); + // } + // return g.map(() => ({ ok: undefined, err })); + // })) + // .map(TraceableImpl.promiseify(({ item: t }) => { + // const { item: { ok: job, err } } = t; + // if (err) return t.map(() => Promise.resolve(err)); + // + // return t.map(() => job!) + // .map(this.queuer.queue) + // .map(TraceableImpl.promiseify(({ item, logger }) => { + // if (item.ok) { + // return new Response(item.ok, { status: 200 }); + // } + // logger.error(item.err); + // return new Response("i messed up D:\n", { status: 500 }); + // })); + // })); + // } +} + +class LizCIServerImpl implements ILizCIServer { + constructor( + private readonly healthCheckActivity: IHealthCheckActivity, + private readonly jobHookActivity: IJobHookActivity, + ) {} + + private route( + req: Traceable<Request & { pathname: string }>, + ): Traceable<Promise<Response>> { + return req.flatMap((req) => { + const { logger, item: { method, pathname } } = req; + if (pathname === "/health") { + return this.healthCheckActivity.healthCheck(req); + } + return this.jobHookActivity.processHook(req); + }); + } + + public async serve(req: Request): Promise<Response> { + const traceId = crypto.randomUUID(); + const { pathname } = new URL(req.url); + const traceSupplier = () => `[${traceId} <- ${req.method}'d @ ${pathname}]`; + return TraceableImpl.from(req) + .bimap(({ item: req }) => [{ req, pathname }, traceSupplier]) + .flatMap(this.route) + .map(({ item, logger }) => + item.catch((e) => { + const errorMessage = `oh noes! something went wrong (ಥ_ಥ) so sowwy!`; + logger.error(errorMessage, e); + return new Response(`${errorMessage}\n`, { status: 500 }); + }) + ) + .item; + } +} + +class JobQueue { + private readonly logger: PrefixLogger; + private readonly url: URL; + private readonly pathname: string; + + constructor(private readonly request: Request, private readonly) { + this.url = new URL(request.url); + this.pathname = this.url.pathname; + this.logger = this.createLogger(); + } + + /** + * Creates a logger with request-specific context + */ + + /** + * Performs health checks on dependent services + */ + private async performHealthCheck(): Promise<void> { + } + + /** + * Handles health check requests + */ + private async handleHealthCheck(): Promise<Response> { + try { + await this.performHealthCheck(); + } catch (error) { + } + } + + /** + * Queues a job in the laminar system + */ + private async queueJob(jobName: string, args: JobRequest): Promise<Response> { + } + + /** + * Validates job request parameters + */ + private validateJobRequest( + jobName: string, + args: unknown, + ): { valid: boolean; response?: Response } { + } + + /** + * Main method to handle the request + */ + public async handle(): Promise<Response> { + this.logger.log("go! :DDD"); + + // Handle health check requests + if (this.pathname === "/health") { + return this.handleHealthCheck(); + } + + // Validate HTTP method + if (this.request.method !== "POST") { + } + + // Extract job name from path + + if (!validation.valid) { + return validation.response!; + } + + // Queue the job + return this.queueJob(jobName, requestBody as JobRequest); + } + + /** + * Handles the entire request lifecycle, including error handling + */ + public async processRequest(): Promise<Response> { + try { + return await this.handle(); + } catch (error) { + } finally { + this.logger.log("allll done!"); + } + } +} + +/** + * Entry point - starts the server + */ +Deno.serve(SERVER_CONFIG, async (request: Request) => { + const handler = new RequestHandler(request); + return handler.processRequest(); +}); |