Compare commits
No commits in common. "15ff1cc52d1abdc705d69daa3ced540401e38916" and "4ca36b324bc21ebb452a1887917e8cda0df7b2a4" have entirely different histories.
15ff1cc52d
...
4ca36b324b
@ -7,13 +7,12 @@ 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(16) NOT NULL,
|
"hash" varchar(8) NOT NULL,
|
||||||
"previous" varchar(16),
|
"previous" varchar(8),
|
||||||
CONSTRAINT "nodes_hash_unique" UNIQUE("hash")
|
CONSTRAINT "nodes_hash_unique" UNIQUE("hash")
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
1
store/drizzle/0001_amazing_weapon_omega.sql
Normal file
1
store/drizzle/0001_amazing_weapon_omega.sql
Normal file
@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "nodes" ADD COLUMN "createdAt" timestamp DEFAULT now();
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"id": "15ad729d-5756-4c06-87ed-cb8b721201f9",
|
"id": "b5fc8bcf-82d4-4d2e-bcd1-89d5a238f5e2",
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
@ -54,13 +54,6 @@
|
|||||||
"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",
|
||||||
@ -87,13 +80,13 @@
|
|||||||
},
|
},
|
||||||
"hash": {
|
"hash": {
|
||||||
"name": "hash",
|
"name": "hash",
|
||||||
"type": "varchar(16)",
|
"type": "varchar(8)",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"previous": {
|
"previous": {
|
||||||
"name": "previous",
|
"name": "previous",
|
||||||
"type": "varchar(16)",
|
"type": "varchar(8)",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
}
|
}
|
||||||
|
217
store/drizzle/meta/0001_snapshot.json
Normal file
217
store/drizzle/meta/0001_snapshot.json
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
{
|
||||||
|
"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,8 +5,15 @@
|
|||||||
{
|
{
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1734703963242,
|
"when": 1734695353420,
|
||||||
"tag": "0000_known_kid_colt",
|
"tag": "0000_steep_bromley",
|
||||||
|
"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
@ -1,33 +0,0 @@
|
|||||||
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,360 +3,238 @@ 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 createParamSchema = (name: string) =>
|
const SingleParam = (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} must contain only letters, numbers, "-", or "_"`,
|
`${name} should contain only letters, numbers, "-" or "_"`,
|
||||||
)
|
)
|
||||||
.openapi({ param: { name, in: "path" } });
|
.openapi({ param: { name, in: "path" } });
|
||||||
|
|
||||||
const createResponseSchema = <T extends ZodSchema>(
|
const ParamsSchema = z.object({
|
||||||
description: string,
|
user: SingleParam("user"),
|
||||||
schema: T,
|
system: SingleParam("system"),
|
||||||
) => ({
|
nodeId: SingleParam("nodeId"),
|
||||||
200: {
|
});
|
||||||
content: { "application/json": { schema } },
|
|
||||||
description,
|
const getUserNodesRoute = createRoute({
|
||||||
|
method: "get",
|
||||||
|
path: "/{user}.json",
|
||||||
|
request: {
|
||||||
|
params: z.object({
|
||||||
|
user: SingleParam("user"),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
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: ParamsSchema,
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: NodeDefinitionSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: "Retrieve a single node definition",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
nodeRouter.openapi(getNodeDefinitionRoute, async (c) => {
|
||||||
|
const { user, system, nodeId } = c.req.valid("param");
|
||||||
|
|
||||||
|
const node = await service.getNodeDefinitionById(
|
||||||
|
user,
|
||||||
|
system,
|
||||||
|
nodeId.replace(/\.json$/, ""),
|
||||||
|
);
|
||||||
|
|
||||||
|
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 getNodeVersionRoute = createRoute({
|
||||||
|
method: "get",
|
||||||
|
path: "/{user}/{system}/{nodeId}@{hash}.json",
|
||||||
|
request: {
|
||||||
|
params: ParamsSchema,
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
200: {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: NodeDefinitionSchema,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
description: "Create a single node",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
async function getNodeByVersion(
|
nodeRouter.openapi(getNodeVersionRoute, async (c) => {
|
||||||
user: string,
|
const { user, system, nodeId } = c.req.valid("param");
|
||||||
system: string,
|
|
||||||
nodeId: string,
|
const nodes = await service.getNodeVersions(user, system, nodeId);
|
||||||
hash?: string,
|
|
||||||
) {
|
return c.json(nodes);
|
||||||
console.log("Get Node by Version", { user, system, nodeId, hash });
|
});
|
||||||
if (hash) {
|
|
||||||
if (nodeId.includes("wasm")) {
|
const getNodeVersionsRoute = createRoute({
|
||||||
return await service.getNodeVersionWasm(
|
method: "get",
|
||||||
user,
|
path: "/{user}/{system}/{nodeId}/versions.json",
|
||||||
system,
|
request: {
|
||||||
nodeId.replace(".wasm", ""),
|
params: z.object({
|
||||||
hash,
|
user: SingleParam("user"),
|
||||||
);
|
system: SingleParam("system"),
|
||||||
} else {
|
nodeId: SingleParam("nodeId"),
|
||||||
const wasmContent = await service.getNodeVersion(
|
hash: SingleParam("hash"),
|
||||||
user,
|
}),
|
||||||
system,
|
},
|
||||||
nodeId,
|
responses: {
|
||||||
hash,
|
200: {
|
||||||
);
|
content: {
|
||||||
return wasmContent;
|
"application/json": {
|
||||||
}
|
schema: NodeDefinitionSchema,
|
||||||
} else {
|
},
|
||||||
if (nodeId.includes(".wasm")) {
|
},
|
||||||
const [id, version] = nodeId.replace(/\.wasm$/, "").split("@");
|
description: "Create a single node",
|
||||||
console.log({ user, system, id, version });
|
},
|
||||||
if (version) {
|
},
|
||||||
return service.getNodeVersionWasm(
|
});
|
||||||
user,
|
|
||||||
system,
|
nodeRouter.openapi(getNodeVersionsRoute, async (c) => {
|
||||||
id,
|
const { user, system, nodeId, hash } = c.req.valid("param");
|
||||||
version,
|
|
||||||
);
|
const node = await service.getNodeVersion(user, system, nodeId, hash);
|
||||||
} else {
|
|
||||||
return service.getNodeWasmById(
|
return c.json(node);
|
||||||
user,
|
});
|
||||||
system,
|
|
||||||
id,
|
const createNodeRoute = createRoute({
|
||||||
);
|
method: "post",
|
||||||
}
|
path: "/",
|
||||||
} else {
|
responses: {
|
||||||
const [id, version] = nodeId.replace(/\.json$/, "").split("@");
|
200: {
|
||||||
if (!version) {
|
content: {
|
||||||
return service.getNodeDefinitionById(
|
"application/json": {
|
||||||
user,
|
schema: NodeDefinitionSchema,
|
||||||
system,
|
},
|
||||||
id,
|
},
|
||||||
);
|
description: "Create a single node",
|
||||||
} else {
|
},
|
||||||
return await service.getNodeVersion(
|
},
|
||||||
user,
|
middleware: [
|
||||||
system,
|
bodyLimit({
|
||||||
id,
|
maxSize: 128 * 1024, // 128kb
|
||||||
version,
|
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,8 +3,7 @@ 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 { extractDefinition } from "./worker/index.ts";
|
import { WorkerMessage } from "./worker/messages.ts";
|
||||||
import { InvalidNodeDefinitionError, NodeNotFoundError } from "./errors.ts";
|
|
||||||
|
|
||||||
export type CreateNodeDTO = {
|
export type CreateNodeDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -19,6 +18,37 @@ 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,
|
||||||
@ -53,10 +83,7 @@ export async function createNode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function getNodeDefinitionsByUser(userName: string) {
|
export async function getNodeDefinitionsByUser(userName: string) {
|
||||||
const nodes = await db.select({
|
const nodes = await db.select({ definition: nodeTable.definition }).from(
|
||||||
definition: nodeTable.definition,
|
|
||||||
hash: nodeTable.hash,
|
|
||||||
}).from(
|
|
||||||
nodeTable,
|
nodeTable,
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
@ -65,10 +92,7 @@ export async function getNodeDefinitionsByUser(userName: string) {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return nodes.map((n) => ({
|
return nodes.map((n) => n.definition);
|
||||||
...n.definition,
|
|
||||||
id: n.definition.id + "@" + n.hash,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNodesBySystem(
|
export async function getNodesBySystem(
|
||||||
@ -78,7 +102,7 @@ export async function getNodesBySystem(
|
|||||||
const nodes = await db
|
const nodes = await db
|
||||||
.selectDistinctOn(
|
.selectDistinctOn(
|
||||||
[nodeTable.userId, nodeTable.systemId, nodeTable.nodeId],
|
[nodeTable.userId, nodeTable.systemId, nodeTable.nodeId],
|
||||||
{ definition: nodeTable.definition, hash: nodeTable.hash },
|
{ definition: nodeTable.definition },
|
||||||
)
|
)
|
||||||
.from(nodeTable)
|
.from(nodeTable)
|
||||||
.where(
|
.where(
|
||||||
@ -86,14 +110,9 @@ export async function getNodesBySystem(
|
|||||||
).orderBy(nodeTable.userId, nodeTable.systemId, nodeTable.nodeId);
|
).orderBy(nodeTable.userId, nodeTable.systemId, nodeTable.nodeId);
|
||||||
|
|
||||||
const definitions = nodes
|
const definitions = nodes
|
||||||
.map((node) =>
|
.map((node) => NodeDefinitionSchema.safeParse(node.definition))
|
||||||
[NodeDefinitionSchema.safeParse(node.definition), node.hash] as const
|
.filter((v) => v.success)
|
||||||
)
|
.map((v) => v.data);
|
||||||
.filter(([v]) => v.success)
|
|
||||||
.map(([v, hash]) => ({
|
|
||||||
...v.data,
|
|
||||||
id: v?.data?.id + "@" + hash,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return definitions;
|
return definitions;
|
||||||
}
|
}
|
||||||
@ -103,6 +122,7 @@ 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(
|
||||||
@ -113,9 +133,10 @@ 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 NodeNotFoundError();
|
throw new Error("Node not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
return node[0].content;
|
return node[0].content;
|
||||||
@ -126,10 +147,7 @@ export async function getNodeDefinitionById(
|
|||||||
systemId: string,
|
systemId: string,
|
||||||
nodeId: string,
|
nodeId: string,
|
||||||
) {
|
) {
|
||||||
const node = await db.select({
|
const node = await db.select({ definition: nodeTable.definition }).from(
|
||||||
definition: nodeTable.definition,
|
|
||||||
hash: nodeTable.hash,
|
|
||||||
}).from(
|
|
||||||
nodeTable,
|
nodeTable,
|
||||||
).where(
|
).where(
|
||||||
and(
|
and(
|
||||||
@ -142,16 +160,16 @@ export async function getNodeDefinitionById(
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (!node[0]) {
|
if (!node[0]) {
|
||||||
throw new NodeNotFoundError();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const definition = NodeDefinitionSchema.safeParse(node[0]?.definition);
|
const definition = NodeDefinitionSchema.safeParse(node[0]?.definition);
|
||||||
|
|
||||||
if (!definition.success) {
|
if (!definition.data) {
|
||||||
throw new InvalidNodeDefinitionError();
|
throw new Error("Invalid definition");
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...definition.data, id: definition.data.id + "@" + node[0].hash };
|
return definition.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNodeVersions(
|
export async function getNodeVersions(
|
||||||
@ -187,6 +205,7 @@ export async function getNodeVersion(
|
|||||||
) {
|
) {
|
||||||
const nodes = await db.select({
|
const nodes = await db.select({
|
||||||
definition: nodeTable.definition,
|
definition: nodeTable.definition,
|
||||||
|
hash: nodeTable.hash,
|
||||||
}).from(
|
}).from(
|
||||||
nodeTable,
|
nodeTable,
|
||||||
).where(
|
).where(
|
||||||
@ -199,34 +218,8 @@ export async function getNodeVersion(
|
|||||||
).limit(1);
|
).limit(1);
|
||||||
|
|
||||||
if (nodes.length === 0) {
|
if (nodes.length === 0) {
|
||||||
throw new NodeNotFoundError();
|
throw new Error("Node not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
return nodes[0].definition;
|
return nodes[0].definition;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNodeVersionWasm(
|
|
||||||
user: string,
|
|
||||||
system: string,
|
|
||||||
nodeId: string,
|
|
||||||
hash: string,
|
|
||||||
) {
|
|
||||||
const node = await db.select({
|
|
||||||
content: nodeTable.content,
|
|
||||||
}).from(
|
|
||||||
nodeTable,
|
|
||||||
).where(
|
|
||||||
and(
|
|
||||||
eq(nodeTable.userId, user),
|
|
||||||
eq(nodeTable.systemId, system),
|
|
||||||
eq(nodeTable.nodeId, nodeId),
|
|
||||||
eq(nodeTable.hash, hash),
|
|
||||||
),
|
|
||||||
).limit(1);
|
|
||||||
|
|
||||||
if (node.length === 0) {
|
|
||||||
throw new NodeNotFoundError();
|
|
||||||
}
|
|
||||||
|
|
||||||
return node[0].content;
|
|
||||||
}
|
|
||||||
|
@ -1,36 +0,0 @@
|
|||||||
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