feat: some shit
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m4s
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m4s
This commit is contained in:
parent
a70e8195a2
commit
15ff1cc52d
@ -7,12 +7,13 @@ CREATE TABLE "users" (
|
|||||||
CREATE TABLE "nodes" (
|
CREATE TABLE "nodes" (
|
||||||
"id" serial PRIMARY KEY NOT NULL,
|
"id" serial PRIMARY KEY NOT NULL,
|
||||||
"userId" varchar NOT NULL,
|
"userId" varchar NOT NULL,
|
||||||
|
"createdAt" timestamp DEFAULT now(),
|
||||||
"systemId" varchar NOT NULL,
|
"systemId" varchar NOT NULL,
|
||||||
"nodeId" varchar NOT NULL,
|
"nodeId" varchar NOT NULL,
|
||||||
"content" "bytea" NOT NULL,
|
"content" "bytea" NOT NULL,
|
||||||
"definition" json NOT NULL,
|
"definition" json NOT NULL,
|
||||||
"hash" varchar(8) NOT NULL,
|
"hash" varchar(16) NOT NULL,
|
||||||
"previous" varchar(8),
|
"previous" varchar(16),
|
||||||
CONSTRAINT "nodes_hash_unique" UNIQUE("hash")
|
CONSTRAINT "nodes_hash_unique" UNIQUE("hash")
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
@ -1 +0,0 @@
|
|||||||
ALTER TABLE "nodes" ADD COLUMN "createdAt" timestamp DEFAULT now();
|
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"id": "b5fc8bcf-82d4-4d2e-bcd1-89d5a238f5e2",
|
"id": "15ad729d-5756-4c06-87ed-cb8b721201f9",
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
@ -54,6 +54,13 @@
|
|||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
|
"createdAt": {
|
||||||
|
"name": "createdAt",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
"systemId": {
|
"systemId": {
|
||||||
"name": "systemId",
|
"name": "systemId",
|
||||||
"type": "varchar",
|
"type": "varchar",
|
||||||
@ -80,13 +87,13 @@
|
|||||||
},
|
},
|
||||||
"hash": {
|
"hash": {
|
||||||
"name": "hash",
|
"name": "hash",
|
||||||
"type": "varchar(8)",
|
"type": "varchar(16)",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"previous": {
|
"previous": {
|
||||||
"name": "previous",
|
"name": "previous",
|
||||||
"type": "varchar(8)",
|
"type": "varchar(16)",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
}
|
}
|
||||||
|
@ -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": {}
|
|
||||||
}
|
|
||||||
}
|
|
@ -5,15 +5,8 @@
|
|||||||
{
|
{
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1734695353420,
|
"when": 1734703963242,
|
||||||
"tag": "0000_steep_bromley",
|
"tag": "0000_known_kid_colt",
|
||||||
"breakpoints": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"idx": 1,
|
|
||||||
"version": "7",
|
|
||||||
"when": 1734696211359,
|
|
||||||
"tag": "0001_amazing_weapon_omega",
|
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
File diff suppressed because one or more lines are too long
33
store/src/routes/node/errors.ts
Normal file
33
store/src/routes/node/errors.ts
Normal 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");
|
||||||
|
}
|
||||||
|
}
|
@ -3,280 +3,360 @@ import { HTTPException } from "hono/http-exception";
|
|||||||
import { idRegex, NodeDefinitionSchema } from "./validations/types.ts";
|
import { idRegex, NodeDefinitionSchema } from "./validations/types.ts";
|
||||||
import * as service from "./node.service.ts";
|
import * as service from "./node.service.ts";
|
||||||
import { bodyLimit } from "hono/body-limit";
|
import { bodyLimit } from "hono/body-limit";
|
||||||
|
import { ZodSchema } from "zod";
|
||||||
|
import { CustomError } from "./errors.ts";
|
||||||
|
|
||||||
const nodeRouter = new OpenAPIHono();
|
const nodeRouter = new OpenAPIHono();
|
||||||
|
|
||||||
const SingleParam = (name: string) =>
|
const createParamSchema = (name: string) =>
|
||||||
z
|
z
|
||||||
.string()
|
.string()
|
||||||
.min(3)
|
.min(3)
|
||||||
.max(20)
|
.max(20)
|
||||||
.refine(
|
.refine(
|
||||||
(value) => idRegex.test(value),
|
(value) => idRegex.test(value),
|
||||||
`${name} should contain only letters, numbers, "-" or "_"`,
|
`${name} must contain only letters, numbers, "-", or "_"`,
|
||||||
)
|
)
|
||||||
.openapi({ param: { name, in: "path" } });
|
.openapi({ param: { name, in: "path" } });
|
||||||
|
|
||||||
const ParamsSchema = z.object({
|
const createResponseSchema = <T extends ZodSchema>(
|
||||||
user: SingleParam("user"),
|
description: string,
|
||||||
system: SingleParam("system"),
|
schema: T,
|
||||||
nodeId: SingleParam("nodeId"),
|
) => ({
|
||||||
});
|
200: {
|
||||||
|
content: { "application/json": { schema } },
|
||||||
const getUserNodesRoute = createRoute({
|
description,
|
||||||
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",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
nodeRouter.openapi(getNodeVersionWasmRoute, async (c) => {
|
async function getNodeByVersion(
|
||||||
const { user, system, nodeId, hash } = c.req.valid("param");
|
user: string,
|
||||||
|
system: string,
|
||||||
const nodes = await service.getNodeVersionWasm(user, system, nodeId, hash);
|
nodeId: string,
|
||||||
|
hash?: string,
|
||||||
return c.json(nodes);
|
) {
|
||||||
});
|
console.log("Get Node by Version", { user, system, nodeId, hash });
|
||||||
|
if (hash) {
|
||||||
const getNodeVersionRoute = createRoute({
|
if (nodeId.includes("wasm")) {
|
||||||
method: "get",
|
return await service.getNodeVersionWasm(
|
||||||
path: "/{user}/{system}/{nodeId}@{hash}.json",
|
user,
|
||||||
request: {
|
system,
|
||||||
params: z.object({
|
nodeId.replace(".wasm", ""),
|
||||||
user: SingleParam("user"),
|
hash,
|
||||||
system: SingleParam("system"),
|
);
|
||||||
nodeId: SingleParam("nodeId"),
|
} else {
|
||||||
hash: SingleParam("hash"),
|
const wasmContent = await service.getNodeVersion(
|
||||||
}),
|
user,
|
||||||
},
|
system,
|
||||||
responses: {
|
nodeId,
|
||||||
200: {
|
hash,
|
||||||
content: {
|
);
|
||||||
"application/json": {
|
return wasmContent;
|
||||||
schema: NodeDefinitionSchema,
|
}
|
||||||
},
|
} else {
|
||||||
},
|
if (nodeId.includes(".wasm")) {
|
||||||
description: "Create a single node",
|
const [id, version] = nodeId.replace(/\.wasm$/, "").split("@");
|
||||||
},
|
console.log({ user, system, id, version });
|
||||||
},
|
if (version) {
|
||||||
});
|
return service.getNodeVersionWasm(
|
||||||
|
user,
|
||||||
nodeRouter.openapi(getNodeVersionRoute, async (c) => {
|
system,
|
||||||
const { user, system, nodeId, hash } = c.req.valid("param");
|
id,
|
||||||
|
version,
|
||||||
const nodes = await service.getNodeVersion(user, system, nodeId, hash);
|
);
|
||||||
|
} else {
|
||||||
return c.json(nodes);
|
return service.getNodeWasmById(
|
||||||
});
|
user,
|
||||||
|
system,
|
||||||
const getNodeVersionsRoute = createRoute({
|
id,
|
||||||
method: "get",
|
);
|
||||||
path: "/{user}/{system}/{nodeId}/versions.json",
|
}
|
||||||
request: {
|
} else {
|
||||||
params: z.object({
|
const [id, version] = nodeId.replace(/\.json$/, "").split("@");
|
||||||
user: SingleParam("user"),
|
if (!version) {
|
||||||
system: SingleParam("system"),
|
return service.getNodeDefinitionById(
|
||||||
nodeId: SingleParam("nodeId"),
|
user,
|
||||||
}),
|
system,
|
||||||
},
|
id,
|
||||||
responses: {
|
);
|
||||||
200: {
|
} else {
|
||||||
content: {
|
return await service.getNodeVersion(
|
||||||
"application/json": {
|
user,
|
||||||
schema: z.array(NodeDefinitionSchema),
|
system,
|
||||||
},
|
id,
|
||||||
},
|
version,
|
||||||
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" });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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 };
|
export { nodeRouter };
|
||||||
|
@ -3,7 +3,8 @@ import { nodeTable } from "./node.schema.ts";
|
|||||||
import { NodeDefinition, NodeDefinitionSchema } from "./validations/types.ts";
|
import { NodeDefinition, NodeDefinitionSchema } from "./validations/types.ts";
|
||||||
import { and, asc, eq } from "drizzle-orm";
|
import { and, asc, eq } from "drizzle-orm";
|
||||||
import { createHash } from "node:crypto";
|
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 = {
|
export type CreateNodeDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -18,37 +19,6 @@ function getNodeHash(content: Uint8Array) {
|
|||||||
return hash.digest("hex").slice(0, 16);
|
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(
|
export async function createNode(
|
||||||
wasmBuffer: ArrayBuffer,
|
wasmBuffer: ArrayBuffer,
|
||||||
content: Uint8Array,
|
content: Uint8Array,
|
||||||
@ -133,7 +103,6 @@ export async function getNodeWasmById(
|
|||||||
systemId: string,
|
systemId: string,
|
||||||
nodeId: string,
|
nodeId: string,
|
||||||
) {
|
) {
|
||||||
const a = performance.now();
|
|
||||||
const node = await db.select({ content: nodeTable.content }).from(nodeTable)
|
const node = await db.select({ content: nodeTable.content }).from(nodeTable)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
@ -144,10 +113,9 @@ export async function getNodeWasmById(
|
|||||||
)
|
)
|
||||||
.orderBy(asc(nodeTable.createdAt))
|
.orderBy(asc(nodeTable.createdAt))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
console.log("Time to load wasm", performance.now() - a);
|
|
||||||
|
|
||||||
if (!node[0]) {
|
if (!node[0]) {
|
||||||
throw new Error("Node not found");
|
throw new NodeNotFoundError();
|
||||||
}
|
}
|
||||||
|
|
||||||
return node[0].content;
|
return node[0].content;
|
||||||
@ -174,13 +142,13 @@ export async function getNodeDefinitionById(
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!node[0]) {
|
if (!node[0]) {
|
||||||
return;
|
throw new NodeNotFoundError();
|
||||||
}
|
}
|
||||||
|
|
||||||
const definition = NodeDefinitionSchema.safeParse(node[0]?.definition);
|
const definition = NodeDefinitionSchema.safeParse(node[0]?.definition);
|
||||||
|
|
||||||
if (!definition.data) {
|
if (!definition.success) {
|
||||||
throw new Error("Invalid definition");
|
throw new InvalidNodeDefinitionError();
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...definition.data, id: definition.data.id + "@" + node[0].hash };
|
return { ...definition.data, id: definition.data.id + "@" + node[0].hash };
|
||||||
@ -231,7 +199,7 @@ export async function getNodeVersion(
|
|||||||
).limit(1);
|
).limit(1);
|
||||||
|
|
||||||
if (nodes.length === 0) {
|
if (nodes.length === 0) {
|
||||||
throw new Error("Node not found");
|
throw new NodeNotFoundError();
|
||||||
}
|
}
|
||||||
|
|
||||||
return nodes[0].definition;
|
return nodes[0].definition;
|
||||||
@ -257,7 +225,7 @@ export async function getNodeVersionWasm(
|
|||||||
).limit(1);
|
).limit(1);
|
||||||
|
|
||||||
if (node.length === 0) {
|
if (node.length === 0) {
|
||||||
throw new Error("Node not found");
|
throw new NodeNotFoundError();
|
||||||
}
|
}
|
||||||
|
|
||||||
return node[0].content;
|
return node[0].content;
|
||||||
|
36
store/src/routes/node/worker/index.ts
Normal file
36
store/src/routes/node/worker/index.ts
Normal 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());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user