summaryrefslogtreecommitdiff
path: root/hooks/server.ts
diff options
context:
space:
mode:
Diffstat (limited to 'hooks/server.ts')
-rwxr-xr-xhooks/server.ts272
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();
+});