From 15ff1cc52d1abdc705d69daa3ced540401e38916 Mon Sep 17 00:00:00 2001 From: Max Richter Date: Fri, 20 Dec 2024 15:24:54 +0100 Subject: [PATCH] feat: some shit --- ...ep_bromley.sql => 0000_known_kid_colt.sql} | 5 +- store/drizzle/0001_amazing_weapon_omega.sql | 1 - store/drizzle/meta/0000_snapshot.json | 13 +- store/drizzle/meta/0001_snapshot.json | 217 ------- store/drizzle/meta/_journal.json | 11 +- store/openapi.json | 2 +- store/src/routes/node/errors.ts | 33 + store/src/routes/node/node.controller.ts | 594 ++++++++++-------- store/src/routes/node/node.service.ts | 48 +- store/src/routes/node/worker/index.ts | 36 ++ 10 files changed, 430 insertions(+), 530 deletions(-) rename store/drizzle/{0000_steep_bromley.sql => 0000_known_kid_colt.sql} (92%) delete mode 100644 store/drizzle/0001_amazing_weapon_omega.sql delete mode 100644 store/drizzle/meta/0001_snapshot.json create mode 100644 store/src/routes/node/errors.ts create mode 100644 store/src/routes/node/worker/index.ts diff --git a/store/drizzle/0000_steep_bromley.sql b/store/drizzle/0000_known_kid_colt.sql similarity index 92% rename from store/drizzle/0000_steep_bromley.sql rename to store/drizzle/0000_known_kid_colt.sql index e7f3917..79e4a3c 100644 --- a/store/drizzle/0000_steep_bromley.sql +++ b/store/drizzle/0000_known_kid_colt.sql @@ -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 diff --git a/store/drizzle/0001_amazing_weapon_omega.sql b/store/drizzle/0001_amazing_weapon_omega.sql deleted file mode 100644 index a2230a1..0000000 --- a/store/drizzle/0001_amazing_weapon_omega.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE "nodes" ADD COLUMN "createdAt" timestamp DEFAULT now(); \ No newline at end of file diff --git a/store/drizzle/meta/0000_snapshot.json b/store/drizzle/meta/0000_snapshot.json index 073363e..a433496 100644 --- a/store/drizzle/meta/0000_snapshot.json +++ b/store/drizzle/meta/0000_snapshot.json @@ -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 } diff --git a/store/drizzle/meta/0001_snapshot.json b/store/drizzle/meta/0001_snapshot.json deleted file mode 100644 index adbd0a9..0000000 --- a/store/drizzle/meta/0001_snapshot.json +++ /dev/null @@ -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": {} - } -} \ No newline at end of file diff --git a/store/drizzle/meta/_journal.json b/store/drizzle/meta/_journal.json index 7a2bfe6..a9ab8d0 100644 --- a/store/drizzle/meta/_journal.json +++ b/store/drizzle/meta/_journal.json @@ -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 } ] diff --git a/store/openapi.json b/store/openapi.json index c8dfe94..d4ee70d 100644 --- a/store/openapi.json +++ b/store/openapi.json @@ -1 +1 @@ -{"openapi":"3.0.0","info":{"version":"1.0.0","title":"Nodarium API"},"components":{"schemas":{"NodeInput":{"anyOf":[{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["seed"]},"value":{"type":"number"}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["boolean"]},"value":{"type":"boolean"}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["float"]},"element":{"type":"string","enum":["slider"]},"value":{"type":"number"},"min":{"type":"number"},"max":{"type":"number"},"step":{"type":"number"}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["integer"]},"element":{"type":"string","enum":["slider"]},"value":{"type":"number"},"min":{"type":"number"},"max":{"type":"number"}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["select"]},"options":{"type":"array","items":{"type":"string"}},"value":{"type":"number"}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["seed"]},"value":{"type":"number"}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["vec3"]},"value":{"type":"array","items":{"type":"number"}}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["geometry"]}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["path"]}},"required":["type"]}]},"NodeDefinition":{"type":"object","properties":{"id":{"type":"string","pattern":"^([a-z0-9-]+)\\/([a-z0-9-]+)\\/([a-z0-9-]+)$"},"inputs":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/NodeInput"}},"outputs":{"type":"array","items":{"type":"string"}},"meta":{"type":"object","properties":{"description":{"type":"string"},"title":{"type":"string"}}}},"required":["id"]},"User":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","minLength":1}},"required":["id","name"]}},"parameters":{}},"paths":{"/nodes/{user}.json":{"get":{"parameters":[{"schema":{"type":"string","minLength":3,"maxLength":20},"required":false,"name":"user","in":"path"}],"responses":{"200":{"description":"Retrieve a single node definition","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/NodeDefinition"}}}}}}}},"/nodes/{user}/{system}.json":{"get":{"parameters":[{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"user","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":false,"name":"system","in":"path"}],"responses":{"200":{"description":"Retrieve a single node definition","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/NodeDefinition"}}}}}}}},"/nodes/{user}/{system}/{nodeId}.json":{"get":{"parameters":[{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"user","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"system","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":false,"name":"nodeId","in":"path"}],"responses":{"200":{"description":"Retrieve a single node definition","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NodeDefinition"}}}}}}},"/nodes/{user}/{system}/{nodeId}.wasm":{"get":{"parameters":[{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"user","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"system","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"nodeId","in":"path"}],"responses":{"200":{"description":"Retrieve a single node","content":{"application/wasm":{"schema":{"nullable":true}}}}}}},"/nodes/{user}/{system}/{nodeId}@{hash}.wasm":{"get":{"parameters":[{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"user","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"system","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"nodeId","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"hash","in":"path"}],"responses":{"200":{"description":"Create a single node","content":{"application/wasm":{"schema":{"nullable":true}}}}}}},"/nodes/{user}/{system}/{nodeId}@{hash}.json":{"get":{"parameters":[{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"user","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"system","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"nodeId","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"hash","in":"path"}],"responses":{"200":{"description":"Create a single node","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NodeDefinition"}}}}}}},"/nodes/{user}/{system}/{nodeId}/versions.json":{"get":{"parameters":[{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"user","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"system","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"nodeId","in":"path"}],"responses":{"200":{"description":"Create a single node","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/NodeDefinition"}}}}}}}},"/nodes":{"post":{"responses":{"200":{"description":"Create a single node","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NodeDefinition"}}}}}}},"/users/users.json":{"get":{"responses":{"200":{"description":"Retrieve a single node definition","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/User"}}}}}}}},"/users/{userId}.json":{"get":{"parameters":[{"schema":{"type":"string"},"required":false,"name":"userId","in":"path"}],"responses":{"200":{"description":"Retrieve a single node definition","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}}}}}}} \ No newline at end of file +{"openapi":"3.0.0","info":{"version":"1.0.0","title":"Nodarium API"},"components":{"schemas":{"NodeInput":{"anyOf":[{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["seed"]},"value":{"type":"number"}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["boolean"]},"value":{"type":"boolean"}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["float"]},"element":{"type":"string","enum":["slider"]},"value":{"type":"number"},"min":{"type":"number"},"max":{"type":"number"},"step":{"type":"number"}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["integer"]},"element":{"type":"string","enum":["slider"]},"value":{"type":"number"},"min":{"type":"number"},"max":{"type":"number"}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["select"]},"options":{"type":"array","items":{"type":"string"}},"value":{"type":"number"}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["seed"]},"value":{"type":"number"}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["vec3"]},"value":{"type":"array","items":{"type":"number"}}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["geometry"]}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["path"]}},"required":["type"]}]},"NodeDefinition":{"type":"object","properties":{"id":{"type":"string","pattern":"^([a-z0-9-]+)\\/([a-z0-9-]+)\\/([a-z0-9-]+)$"},"inputs":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/NodeInput"}},"outputs":{"type":"array","items":{"type":"string"}},"meta":{"type":"object","properties":{"description":{"type":"string"},"title":{"type":"string"}}}},"required":["id"]},"User":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","minLength":1}},"required":["id","name"]}},"parameters":{}},"paths":{"/nodes":{"post":{"responses":{"200":{"description":"Create a single node","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NodeDefinition"}}}}}}},"/nodes/{user}.json":{"get":{"parameters":[{"schema":{"type":"string","minLength":3,"maxLength":20},"required":false,"name":"user","in":"path"}],"responses":{"200":{"description":"Retrieve nodes for a user","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/NodeDefinition"}}}}}}}},"/nodes/{user}/{system}.json":{"get":{"parameters":[{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"user","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":false,"name":"system","in":"path"}],"responses":{"200":{"description":"Retrieve nodes for a system","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/NodeDefinition"}}}}}}}},"/nodes/{user}/{system}/{nodeId}.json":{"get":{"parameters":[{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"user","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"system","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":false,"name":"nodeId","in":"path"}],"responses":{"200":{"description":"Retrieve a single node definition","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NodeDefinition"}}}}}}},"/nodes/{user}/{system}/{nodeId}@{version}.json":{"get":{"parameters":[{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"user","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"system","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"nodeId","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":false,"name":"version","in":"path"}],"responses":{"200":{"description":"Retrieve a single node definition","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NodeDefinition"}}}}}}},"/nodes/{user}/{system}/{nodeId}/versions.json":{"get":{"parameters":[{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"user","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"system","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"nodeId","in":"path"}],"responses":{"200":{"description":"Retrieve a single node definition","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/NodeDefinition"}}}}}}}},"/nodes/{user}/{system}/{nodeId}.wasm":{"get":{"parameters":[{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"user","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"system","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":false,"name":"nodeId","in":"path"}],"responses":{"200":{"description":"Retrieve a node's WASM file","content":{"application/wasm":{"schema":{"nullable":true}}}}}}},"/nodes/{user}/{system}/{nodeId}@{version}.wasm":{"get":{"parameters":[{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"user","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"system","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"nodeId","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":false,"name":"version","in":"path"}],"responses":{"200":{"description":"Retrieve a node's WASM file","content":{"application/wasm":{"schema":{"nullable":true}}}}}}},"/users/users.json":{"get":{"responses":{"200":{"description":"Retrieve a single node definition","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/User"}}}}}}}},"/users/{userId}.json":{"get":{"parameters":[{"schema":{"type":"string"},"required":false,"name":"userId","in":"path"}],"responses":{"200":{"description":"Retrieve a single node definition","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}}}}}}} \ No newline at end of file diff --git a/store/src/routes/node/errors.ts b/store/src/routes/node/errors.ts new file mode 100644 index 0000000..24a7d55 --- /dev/null +++ b/store/src/routes/node/errors.ts @@ -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"); + } +} diff --git a/store/src/routes/node/node.controller.ts b/store/src/routes/node/node.controller.ts index 3494ada..0b58166 100644 --- a/store/src/routes/node/node.controller.ts +++ b/store/src/routes/node/node.controller.ts @@ -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 = ( + 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 }; diff --git a/store/src/routes/node/node.service.ts b/store/src/routes/node/node.service.ts index cbe8050..198058f 100644 --- a/store/src/routes/node/node.service.ts +++ b/store/src/routes/node/node.service.ts @@ -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 { - 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; diff --git a/store/src/routes/node/worker/index.ts b/store/src/routes/node/worker/index.ts new file mode 100644 index 0000000..84ecc89 --- /dev/null +++ b/store/src/routes/node/worker/index.ts @@ -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 { + 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()); + } + }; + }); +}