feat: some shit
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m4s

This commit is contained in:
max_richter 2024-12-20 15:24:54 +01:00
parent a70e8195a2
commit 15ff1cc52d
10 changed files with 430 additions and 530 deletions

View File

@ -7,12 +7,13 @@ CREATE TABLE "users" (
CREATE TABLE "nodes" (
"id" serial PRIMARY KEY NOT NULL,
"userId" varchar NOT NULL,
"createdAt" timestamp DEFAULT now(),
"systemId" varchar NOT NULL,
"nodeId" varchar NOT NULL,
"content" "bytea" NOT NULL,
"definition" json NOT NULL,
"hash" varchar(8) NOT NULL,
"previous" varchar(8),
"hash" varchar(16) NOT NULL,
"previous" varchar(16),
CONSTRAINT "nodes_hash_unique" UNIQUE("hash")
);
--> statement-breakpoint

View File

@ -1 +0,0 @@
ALTER TABLE "nodes" ADD COLUMN "createdAt" timestamp DEFAULT now();

View File

@ -1,5 +1,5 @@
{
"id": "b5fc8bcf-82d4-4d2e-bcd1-89d5a238f5e2",
"id": "15ad729d-5756-4c06-87ed-cb8b721201f9",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
@ -54,6 +54,13 @@
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"systemId": {
"name": "systemId",
"type": "varchar",
@ -80,13 +87,13 @@
},
"hash": {
"name": "hash",
"type": "varchar(8)",
"type": "varchar(16)",
"primaryKey": false,
"notNull": true
},
"previous": {
"name": "previous",
"type": "varchar(8)",
"type": "varchar(16)",
"primaryKey": false,
"notNull": false
}

View File

@ -1,217 +0,0 @@
{
"id": "080ee514-5516-4400-9286-295826df6f8a",
"prevId": "b5fc8bcf-82d4-4d2e-bcd1-89d5a238f5e2",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"users_name_unique": {
"name": "users_name_unique",
"nullsNotDistinct": false,
"columns": [
"name"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.nodes": {
"name": "nodes",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"userId": {
"name": "userId",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": false,
"default": "now()"
},
"systemId": {
"name": "systemId",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"nodeId": {
"name": "nodeId",
"type": "varchar",
"primaryKey": false,
"notNull": true
},
"content": {
"name": "content",
"type": "bytea",
"primaryKey": false,
"notNull": true
},
"definition": {
"name": "definition",
"type": "json",
"primaryKey": false,
"notNull": true
},
"hash": {
"name": "hash",
"type": "varchar(8)",
"primaryKey": false,
"notNull": true
},
"previous": {
"name": "previous",
"type": "varchar(8)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {
"user_id_idx": {
"name": "user_id_idx",
"columns": [
{
"expression": "userId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"system_id_idx": {
"name": "system_id_idx",
"columns": [
{
"expression": "systemId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"node_id_idx": {
"name": "node_id_idx",
"columns": [
{
"expression": "nodeId",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
},
"hash_idx": {
"name": "hash_idx",
"columns": [
{
"expression": "hash",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": false,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {
"nodes_userId_users_name_fk": {
"name": "nodes_userId_users_name_fk",
"tableFrom": "nodes",
"tableTo": "users",
"columnsFrom": [
"userId"
],
"columnsTo": [
"name"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"node_previous_fk": {
"name": "node_previous_fk",
"tableFrom": "nodes",
"tableTo": "nodes",
"columnsFrom": [
"previous"
],
"columnsTo": [
"hash"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {
"nodes_hash_unique": {
"name": "nodes_hash_unique",
"nullsNotDistinct": false,
"columns": [
"hash"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}

View File

@ -5,15 +5,8 @@
{
"idx": 0,
"version": "7",
"when": 1734695353420,
"tag": "0000_steep_bromley",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1734696211359,
"tag": "0001_amazing_weapon_omega",
"when": 1734703963242,
"tag": "0000_known_kid_colt",
"breakpoints": true
}
]

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,33 @@
import { StatusCode } from "hono";
export class CustomError extends Error {
constructor(public status: StatusCode, message: string) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
export class NodeNotFoundError extends CustomError {
constructor() {
super(404, "Node not found");
}
}
export class InvalidNodeDefinitionError extends CustomError {
constructor() {
super(400, "Invalid node definition");
}
}
export class WorkerTimeoutError extends CustomError {
constructor() {
super(500, "Worker timed out");
}
}
export class UnknownWorkerResponseError extends CustomError {
constructor() {
super(500, "Unknown worker response");
}
}

View File

@ -3,280 +3,360 @@ import { HTTPException } from "hono/http-exception";
import { idRegex, NodeDefinitionSchema } from "./validations/types.ts";
import * as service from "./node.service.ts";
import { bodyLimit } from "hono/body-limit";
import { ZodSchema } from "zod";
import { CustomError } from "./errors.ts";
const nodeRouter = new OpenAPIHono();
const SingleParam = (name: string) =>
const createParamSchema = (name: string) =>
z
.string()
.min(3)
.max(20)
.refine(
(value) => idRegex.test(value),
`${name} should contain only letters, numbers, "-" or "_"`,
`${name} must contain only letters, numbers, "-", or "_"`,
)
.openapi({ param: { name, in: "path" } });
const ParamsSchema = z.object({
user: SingleParam("user"),
system: SingleParam("system"),
nodeId: SingleParam("nodeId"),
});
const getUserNodesRoute = createRoute({
method: "get",
path: "/{user}.json",
request: {
params: z.object({
user: SingleParam("user").optional(),
}),
},
responses: {
200: {
content: {
"application/json": {
schema: z.array(NodeDefinitionSchema),
},
},
description: "Retrieve a single node definition",
},
},
});
nodeRouter.openapi(getUserNodesRoute, async (c) => {
const userId = c.req.param("user.json").replace(/\.json$/, "");
const nodes = await service.getNodeDefinitionsByUser(
userId,
);
return c.json(nodes);
});
const getNodeCollectionRoute = createRoute({
method: "get",
path: "/{user}/{system}.json",
request: {
params: z.object({
user: SingleParam("user"),
system: SingleParam("system").optional(),
}),
},
responses: {
200: {
content: {
"application/json": {
schema: z.array(NodeDefinitionSchema),
},
},
description: "Retrieve a single node definition",
},
},
});
nodeRouter.openapi(getNodeCollectionRoute, async (c) => {
const { user } = c.req.valid("param");
const nodeSystemId = c.req.param("system.json").replace(/\.json$/, "");
const nodes = await service.getNodesBySystem(user, nodeSystemId);
return c.json(nodes);
});
const getNodeDefinitionRoute = createRoute({
method: "get",
path: "/{user}/{system}/{nodeId}.json",
request: {
params: z.object({
user: SingleParam("user"),
system: SingleParam("system"),
nodeId: SingleParam("nodeId").optional(),
}),
},
responses: {
200: {
content: {
"application/json": {
schema: NodeDefinitionSchema,
},
},
description: "Retrieve a single node definition",
},
},
});
nodeRouter.openapi(getNodeDefinitionRoute, async (c) => {
const { user, system } = c.req.valid("param");
const nodeId = c.req.param("nodeId.json").replace(/\.json$/, "");
const node = await service.getNodeDefinitionById(
user,
system,
nodeId,
);
if (!node) {
throw new HTTPException(404);
}
return c.json(node);
});
const getNodeWasmRoute = createRoute({
method: "get",
path: "/{user}/{system}/{nodeId}.wasm",
request: {
params: ParamsSchema,
},
responses: {
200: {
content: {
"application/wasm": {
schema: z.any(),
},
},
description: "Retrieve a single node",
},
},
});
nodeRouter.openapi(getNodeWasmRoute, async (c) => {
const { user, system, nodeId } = c.req.valid("param");
const wasmContent = await service.getNodeWasmById(
user,
system,
nodeId.replace(/\.wasm/, ""),
);
c.header("Content-Type", "application/wasm");
return c.body(wasmContent);
});
const getNodeVersionWasmRoute = createRoute({
method: "get",
path: "/{user}/{system}/{nodeId}@{hash}.wasm",
request: {
params: z.object({
user: SingleParam("user"),
system: SingleParam("system"),
nodeId: SingleParam("nodeId"),
hash: SingleParam("hash"),
}),
},
responses: {
200: {
content: {
"application/wasm": {
schema: z.any(),
},
},
description: "Create a single node",
},
const createResponseSchema = <T extends ZodSchema>(
description: string,
schema: T,
) => ({
200: {
content: { "application/json": { schema } },
description,
},
});
nodeRouter.openapi(getNodeVersionWasmRoute, async (c) => {
const { user, system, nodeId, hash } = c.req.valid("param");
const nodes = await service.getNodeVersionWasm(user, system, nodeId, hash);
return c.json(nodes);
});
const getNodeVersionRoute = createRoute({
method: "get",
path: "/{user}/{system}/{nodeId}@{hash}.json",
request: {
params: z.object({
user: SingleParam("user"),
system: SingleParam("system"),
nodeId: SingleParam("nodeId"),
hash: SingleParam("hash"),
}),
},
responses: {
200: {
content: {
"application/json": {
schema: NodeDefinitionSchema,
},
},
description: "Create a single node",
},
},
});
nodeRouter.openapi(getNodeVersionRoute, async (c) => {
const { user, system, nodeId, hash } = c.req.valid("param");
const nodes = await service.getNodeVersion(user, system, nodeId, hash);
return c.json(nodes);
});
const getNodeVersionsRoute = createRoute({
method: "get",
path: "/{user}/{system}/{nodeId}/versions.json",
request: {
params: z.object({
user: SingleParam("user"),
system: SingleParam("system"),
nodeId: SingleParam("nodeId"),
}),
},
responses: {
200: {
content: {
"application/json": {
schema: z.array(NodeDefinitionSchema),
},
},
description: "Create a single node",
},
},
});
nodeRouter.openapi(getNodeVersionsRoute, async (c) => {
const { user, system, nodeId } = c.req.valid("param");
const node = await service.getNodeVersions(user, system, nodeId);
return c.json(node);
});
const createNodeRoute = createRoute({
method: "post",
path: "/",
responses: {
200: {
content: {
"application/json": {
schema: NodeDefinitionSchema,
},
},
description: "Create a single node",
},
},
middleware: [
bodyLimit({
maxSize: 128 * 1024, // 128kb
onError: (c) => {
return c.text("Node content too large", 413);
},
}),
],
});
nodeRouter.openapi(createNodeRoute, async (c) => {
const buffer = await c.req.arrayBuffer();
const bytes = await (await c.req.blob()).bytes();
try {
const node = await service.createNode(buffer, bytes);
return c.json(node);
} catch (error) {
if (error instanceof Error && "code" in error) {
switch (error.code) {
case "23505":
throw new HTTPException(409, { message: "node already exists" });
async function getNodeByVersion(
user: string,
system: string,
nodeId: string,
hash?: string,
) {
console.log("Get Node by Version", { user, system, nodeId, hash });
if (hash) {
if (nodeId.includes("wasm")) {
return await service.getNodeVersionWasm(
user,
system,
nodeId.replace(".wasm", ""),
hash,
);
} else {
const wasmContent = await service.getNodeVersion(
user,
system,
nodeId,
hash,
);
return wasmContent;
}
} else {
if (nodeId.includes(".wasm")) {
const [id, version] = nodeId.replace(/\.wasm$/, "").split("@");
console.log({ user, system, id, version });
if (version) {
return service.getNodeVersionWasm(
user,
system,
id,
version,
);
} else {
return service.getNodeWasmById(
user,
system,
id,
);
}
} else {
const [id, version] = nodeId.replace(/\.json$/, "").split("@");
if (!version) {
return service.getNodeDefinitionById(
user,
system,
id,
);
} else {
return await service.getNodeVersion(
user,
system,
id,
version,
);
}
}
}
throw new HTTPException(500);
});
}
nodeRouter.openapi(
createRoute({
method: "post",
path: "/",
responses: createResponseSchema(
"Create a single node",
NodeDefinitionSchema,
),
middleware: [
bodyLimit({
maxSize: 128 * 1024, // 128 KB
onError: (c) => c.text("Node content too large", 413),
}),
],
}),
async (c) => {
const buffer = await c.req.arrayBuffer();
const bytes = new Uint8Array(buffer);
try {
const node = await service.createNode(buffer, bytes);
return c.json(node);
} catch (error) {
if (error instanceof CustomError) {
throw new HTTPException(error.status, { message: error.message });
}
throw new HTTPException(500, { message: "Internal server error" });
}
},
);
nodeRouter.openapi(
createRoute({
method: "get",
path: "/{user}.json",
request: {
params: z.object({
user: createParamSchema("user").optional(),
}),
},
responses: createResponseSchema(
"Retrieve nodes for a user",
z.array(NodeDefinitionSchema),
),
}),
async (c) => {
const user = c.req.param("user.json").replace(/\.json$/, "");
try {
const nodes = await service.getNodeDefinitionsByUser(user);
return c.json(nodes);
} catch (error) {
if (error instanceof CustomError) {
throw new HTTPException(error.status, { message: error.message });
}
throw new HTTPException(500, { message: "Internal server error" });
}
},
);
nodeRouter.openapi(
createRoute({
method: "get",
path: "/{user}/{system}.json",
request: {
params: z.object({
user: createParamSchema("user"),
system: createParamSchema("system").optional(),
}),
},
responses: createResponseSchema(
"Retrieve nodes for a system",
z.array(NodeDefinitionSchema),
),
}),
async (c) => {
const { user } = c.req.valid("param");
const system = c.req.param("system.json").replace(/\.json$/, "");
console.log("Get Nodes by System", { user, system });
try {
const nodes = await service.getNodesBySystem(user, system);
return c.json(nodes);
} catch (error) {
if (error instanceof CustomError) {
throw new HTTPException(error.status, { message: error.message });
}
throw new HTTPException(500, { message: "Internal server error" });
}
},
);
nodeRouter.openapi(
createRoute({
method: "get",
path: "/{user}/{system}/{nodeId}.json",
request: {
params: z.object({
user: createParamSchema("user"),
system: createParamSchema("system"),
nodeId: createParamSchema("nodeId").optional(),
}),
},
responses: createResponseSchema(
"Retrieve a single node definition",
NodeDefinitionSchema,
),
}),
async (c) => {
const { user, system } = c.req.valid("param");
const nodeId = c.req.param("nodeId.json").replace(/\.json$/, "");
console.log("Get Node by Id", { user, system, nodeId });
try {
const node = await service.getNodeDefinitionById(user, system, nodeId);
return c.json(node);
} catch (error) {
if (error instanceof CustomError) {
throw new HTTPException(error.status, { message: error.message });
}
throw new HTTPException(500, { message: "Internal server error" });
}
},
);
nodeRouter.openapi(
createRoute({
method: "get",
path: "/{user}/{system}/{nodeId}@{version}.json",
request: {
params: z.object({
user: createParamSchema("user"),
system: createParamSchema("system"),
nodeId: createParamSchema("nodeId"),
version: createParamSchema("version").optional(),
}),
},
responses: createResponseSchema(
"Retrieve a single node definition",
NodeDefinitionSchema,
),
}),
async (c) => {
const { user, system, nodeId } = c.req.valid("param");
const hash = c.req.param("version.json");
try {
const res = await getNodeByVersion(user, system, nodeId, hash);
if (res instanceof ArrayBuffer) {
c.header("Content-Type", "application/wasm");
return c.body(res);
} else {
return c.json(res);
}
} catch (error) {
if (error instanceof CustomError) {
throw new HTTPException(error.status, { message: error.message });
}
throw new HTTPException(500, { message: "Internal server error" });
}
},
);
nodeRouter.openapi(
createRoute({
method: "get",
path: "/{user}/{system}/{nodeId}/versions.json",
request: {
params: z.object({
user: createParamSchema("user"),
system: createParamSchema("system"),
nodeId: createParamSchema("nodeId"),
}),
},
responses: createResponseSchema(
"Retrieve a single node definition",
z.array(NodeDefinitionSchema),
),
}),
async (c) => {
const { user, system, nodeId } = c.req.valid("param");
try {
const node = await service.getNodeVersions(user, system, nodeId);
return c.json(node);
} catch (error) {
if (error instanceof CustomError) {
throw new HTTPException(error.status, { message: error.message });
}
throw new HTTPException(500, { message: "Internal server error" });
}
},
);
nodeRouter.openapi(
createRoute({
method: "get",
path: "/{user}/{system}/{nodeId}.wasm",
request: {
params: z.object({
user: createParamSchema("user"),
system: createParamSchema("system"),
nodeId: createParamSchema("nodeId").optional(),
}),
},
responses: {
200: {
content: { "application/wasm": { schema: z.any() } },
description: "Retrieve a node's WASM file",
},
},
}),
async (c) => {
const { user, system } = c.req.valid("param");
const nodeId = c.req.param("nodeId.wasm");
console.log("Get NodeWasm by Id", { user, system, nodeId });
try {
const res = await getNodeByVersion(user, system, nodeId);
if (res instanceof ArrayBuffer) {
c.header("Content-Type", "application/wasm");
return c.body(res);
} else {
return c.json(res);
}
} catch (error) {
if (error instanceof CustomError) {
throw new HTTPException(error.status, { message: error.message });
}
throw new HTTPException(500, { message: "Internal server error" });
}
},
);
nodeRouter.openapi(
createRoute({
method: "get",
path: "/{user}/{system}/{nodeId}@{version}.wasm",
request: {
params: z.object({
user: createParamSchema("user"),
system: createParamSchema("system"),
nodeId: createParamSchema("nodeId"),
version: createParamSchema("version").optional(),
}),
},
responses: {
200: {
content: { "application/wasm": { schema: z.any() } },
description: "Retrieve a node's WASM file",
},
},
}),
async (c) => {
const { user, system, nodeId } = c.req.valid("param");
const hash = c.req.param("version.wasm");
try {
const res = await getNodeByVersion(user, system, nodeId, hash);
if (res instanceof ArrayBuffer) {
c.header("Content-Type", "application/wasm");
return c.body(res);
} else {
return c.json(res);
}
} catch (error) {
if (error instanceof CustomError) {
throw new HTTPException(error.status, { message: error.message });
}
throw new HTTPException(500, { message: "Internal server error" });
}
},
);
export { nodeRouter };

View File

@ -3,7 +3,8 @@ import { nodeTable } from "./node.schema.ts";
import { NodeDefinition, NodeDefinitionSchema } from "./validations/types.ts";
import { and, asc, eq } from "drizzle-orm";
import { createHash } from "node:crypto";
import { WorkerMessage } from "./worker/messages.ts";
import { extractDefinition } from "./worker/index.ts";
import { InvalidNodeDefinitionError, NodeNotFoundError } from "./errors.ts";
export type CreateNodeDTO = {
id: string;
@ -18,37 +19,6 @@ function getNodeHash(content: Uint8Array) {
return hash.digest("hex").slice(0, 16);
}
function extractDefinition(content: ArrayBuffer): Promise<NodeDefinition> {
const worker = new Worker(
new URL("./worker/node.worker.ts", import.meta.url).href,
{
type: "module",
},
) as Worker & {
postMessage: (message: WorkerMessage) => void;
};
return new Promise((res, rej) => {
worker.postMessage({ action: "extract-definition", content });
setTimeout(() => {
worker.terminate();
rej(new Error("Worker timeout out"));
}, 100);
worker.onmessage = function (e) {
switch (e.data.action) {
case "result":
res(e.data.result);
break;
case "error":
rej(e.data.result);
break;
default:
rej(new Error("Unknown worker response"));
}
};
});
}
export async function createNode(
wasmBuffer: ArrayBuffer,
content: Uint8Array,
@ -133,7 +103,6 @@ export async function getNodeWasmById(
systemId: string,
nodeId: string,
) {
const a = performance.now();
const node = await db.select({ content: nodeTable.content }).from(nodeTable)
.where(
and(
@ -144,10 +113,9 @@ export async function getNodeWasmById(
)
.orderBy(asc(nodeTable.createdAt))
.limit(1);
console.log("Time to load wasm", performance.now() - a);
if (!node[0]) {
throw new Error("Node not found");
throw new NodeNotFoundError();
}
return node[0].content;
@ -174,13 +142,13 @@ export async function getNodeDefinitionById(
.limit(1);
if (!node[0]) {
return;
throw new NodeNotFoundError();
}
const definition = NodeDefinitionSchema.safeParse(node[0]?.definition);
if (!definition.data) {
throw new Error("Invalid definition");
if (!definition.success) {
throw new InvalidNodeDefinitionError();
}
return { ...definition.data, id: definition.data.id + "@" + node[0].hash };
@ -231,7 +199,7 @@ export async function getNodeVersion(
).limit(1);
if (nodes.length === 0) {
throw new Error("Node not found");
throw new NodeNotFoundError();
}
return nodes[0].definition;
@ -257,7 +225,7 @@ export async function getNodeVersionWasm(
).limit(1);
if (node.length === 0) {
throw new Error("Node not found");
throw new NodeNotFoundError();
}
return node[0].content;

View File

@ -0,0 +1,36 @@
import { UnknownWorkerResponseError, WorkerTimeoutError } from "../errors.ts";
import { NodeDefinition } from "../validations/types.ts";
import { WorkerMessage } from "./messages.ts";
export function extractDefinition(
content: ArrayBuffer,
): Promise<NodeDefinition> {
const worker = new Worker(
new URL("./node.worker.ts", import.meta.url).href,
{
type: "module",
},
) as Worker & {
postMessage: (message: WorkerMessage) => void;
};
return new Promise((res, rej) => {
worker.postMessage({ action: "extract-definition", content });
setTimeout(() => {
worker.terminate();
rej(new WorkerTimeoutError());
}, 100);
worker.onmessage = function (e) {
switch (e.data.action) {
case "result":
res(e.data.result);
break;
case "error":
rej(e.data.error);
break;
default:
rej(new UnknownWorkerResponseError());
}
};
});
}