Compare commits

...

19 Commits

Author SHA1 Message Date
05b192e7ab commit to trigger deploy 2025-01-15 19:30:12 +01:00
edcaab4bd4 fix: use correct url 2025-01-15 18:17:04 +01:00
a99040f42e feat: some shit
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 47s
2024-12-20 16:35:23 +01:00
fca59e87e5 feat: some shit 2024-12-20 16:35:16 +01:00
05e8970475 feat: use remote registry
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m4s
2024-12-20 16:11:30 +01:00
385d1dd831 feat: use remote registry
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m2s
2024-12-20 16:06:18 +01:00
dc46c4b64c feat: some stuff
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m0s
2024-12-20 15:55:45 +01:00
15ff1cc52d feat: some shit
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m4s
2024-12-20 15:24:54 +01:00
a70e8195a2 feat: add some more versining stuff 2024-12-20 14:06:33 +01:00
4ca36b324b fix: some shit
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 1m55s
2024-12-20 13:40:14 +01:00
221817fc16 fix: some node
All checks were successful
Deploy to GitHub Pages / build_site (push) Successful in 2m1s
2024-12-20 12:51:56 +01:00
7060b37df5 fix: error in schema
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 36s
2024-12-20 12:49:25 +01:00
ec037a3bbd fix: error in schema
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 36s
2024-12-20 12:46:44 +01:00
2814165ee6 fix: run migrations in code
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 36s
2024-12-20 12:42:45 +01:00
c6badff1ee fix: dockerfile
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 36s
2024-12-20 12:36:11 +01:00
a0d420517c feat: some shit
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 36s
2024-12-20 12:21:50 +01:00
eadd37bfa4 feat: some shit
Some checks failed
Deploy to GitHub Pages / build_site (push) Has been cancelled
2024-12-20 12:21:46 +01:00
9d698be86f fix: dockerfile
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 38s
2024-12-20 12:09:30 +01:00
540d0549d7 feat: some shit
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 37s
2024-12-19 23:55:07 +01:00
31 changed files with 869 additions and 349 deletions

View File

@@ -23,7 +23,7 @@
<div class="wrapper">
{#if !activeUser}
{#await registry.fetchUsers()}
<div>Loading...</div>
<div>Loading Users...</div>
{:then users}
{#each users as user}
<button
@@ -37,7 +37,7 @@
{/await}
{:else if !activeCollection}
{#await registry.fetchUser(activeUser)}
<div>Loading...</div>
<div>Loading User...</div>
{:then user}
{#each user.collections as collection}
<button
@@ -53,11 +53,11 @@
{/await}
{:else if !activeNode}
{#await registry.fetchCollection(`${activeUser}/${activeCollection}`)}
<div>Loading...</div>
<div>Loading Collection...</div>
{:then collection}
{#each collection.nodes as node}
{#await registry.fetchNodeDefinition(node.id)}
<div>Loading... {node.id}</div>
<div>Loading Node... {node.id}</div>
{:then node}
{#if node}
<DraggableNode {node} />

View File

@@ -32,8 +32,10 @@
let performanceStore = createPerformanceStore();
const registryCache = new IndexDBCache("node-registry");
const nodeRegistry = new RemoteNodeRegistry("");
nodeRegistry.cache = registryCache;
const nodeRegistry = new RemoteNodeRegistry(
"https://node-store.app.max-richter.dev",
registryCache,
);
const workerRuntime = new WorkerRuntimeExecutor();
const runtimeCache = new MemoryRuntimeCache();
const memoryRuntime = new MemoryRuntimeExecutor(nodeRegistry, runtimeCache);

View File

@@ -18,14 +18,14 @@
"hidden": true,
"min": 0,
"max": 1,
"value": 0.5,
"value": 0.5
},
"depth": {
"type": "integer",
"min": 1,
"max": 10,
"hidden": true,
"value": 1,
"value": 1
}
}
}

View File

@@ -13,7 +13,7 @@ export class RemoteNodeRegistry implements NodeRegistry {
fetch: typeof fetch = globalThis.fetch.bind(globalThis);
constructor(private url: string) { }
constructor(private url: string, private cache?: AsyncCache<ArrayBuffer>) { }
async fetchUsers() {
const response = await this.fetch(`${this.url}/nodes/users.json`);
@@ -24,7 +24,7 @@ export class RemoteNodeRegistry implements NodeRegistry {
}
async fetchUser(userId: `${string}`) {
const response = await this.fetch(`${this.url}/nodes/${userId}.json`);
const response = await this.fetch(`${this.url}/user/${userId}.json`);
if (!response.ok) {
throw new Error(`Failed to load user ${userId}`);
}

13
store/Dockerfile Normal file
View File

@@ -0,0 +1,13 @@
FROM denoland/deno:alpine
ARG GIT_REVISION
ENV DENO_DEPLOYMENT_ID=${GIT_REVISION}
WORKDIR /app
COPY . .
RUN deno cache src/server.ts
EXPOSE 8000
CMD ["task", "run"]

View File

@@ -44,7 +44,8 @@ for await (const dir of dirs) {
async function postNode(node: Node) {
const wasmContent = await Deno.readFile(node.path);
const url = `http://localhost:8000/v1/nodes`;
const url = `http://localhost:8000/nodes`;
// const url = "https://node-store.app.max-richter.dev/nodes";
const res = await fetch(url, {
method: "POST",
@@ -55,7 +56,7 @@ async function postNode(node: Node) {
console.log(`Uploaded ${node.id}`);
} else {
const text = await res.text();
console.log(`Failed to upload ${node.id}: ${text}`);
console.log(`Failed to upload ${node.id}: ${res.status} ${text}`);
}
}

View File

@@ -1,6 +1,7 @@
{
"tasks": {
"dev": "deno run -A --watch src/main.ts",
"dev": "deno run -A --watch src/server.ts",
"run": "deno run -A src/server.ts",
"test": "deno run vitest",
"drizzle": "podman-compose exec app deno --env -A --node-modules-dir npm:drizzle-kit",
"upload": "deno run --allow-read --allow-net bin/upload.ts"
@@ -11,6 +12,7 @@
"@hono/zod-openapi": "npm:@hono/zod-openapi@^0.18.3",
"@std/assert": "jsr:@std/assert@1",
"@types/pg": "npm:@types/pg@^8.11.10",
"drizzle-kit": "npm:drizzle-kit@^0.30.1",
"drizzle-orm": "npm:drizzle-orm@^0.38.2",
"hono": "npm:hono@^4.6.14",
"pg": "npm:pg@^8.13.1",

2
store/deno.lock generated
View File

@@ -15,6 +15,7 @@
"npm:@types/node@*": "22.5.4",
"npm:@types/pg@^8.11.10": "8.11.10",
"npm:drizzle-kit@*": "0.30.1_esbuild@0.19.12",
"npm:drizzle-kit@~0.30.1": "0.30.1_esbuild@0.19.12",
"npm:drizzle-orm@~0.38.2": "0.38.2_@types+pg@8.11.10_pg@8.13.1",
"npm:hono@^4.6.14": "4.6.14",
"npm:pg@^8.13.1": "8.13.1",
@@ -871,6 +872,7 @@
"npm:@hono/swagger-ui@0.5",
"npm:@hono/zod-openapi@~0.18.3",
"npm:@types/pg@^8.11.10",
"npm:drizzle-kit@~0.30.1",
"npm:drizzle-orm@~0.38.2",
"npm:hono@^4.6.14",
"npm:pg@^8.13.1",

View File

@@ -1,10 +0,0 @@
CREATE TABLE "nodes" (
"id" serial NOT NULL,
"content" "bytea" NOT NULL,
"definition" json NOT NULL
);
--> statement-breakpoint
CREATE TABLE "users" (
"id" serial PRIMARY KEY NOT NULL,
"name" text
);

View File

@@ -0,0 +1,25 @@
CREATE TABLE "users" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"name" text NOT NULL,
CONSTRAINT "users_name_unique" UNIQUE("name")
);
--> statement-breakpoint
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(16) NOT NULL,
"previous" varchar(16),
CONSTRAINT "nodes_hash_unique" UNIQUE("hash")
);
--> statement-breakpoint
ALTER TABLE "nodes" ADD CONSTRAINT "nodes_userId_users_name_fk" FOREIGN KEY ("userId") REFERENCES "public"."users"("name") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "nodes" ADD CONSTRAINT "node_previous_fk" FOREIGN KEY ("previous") REFERENCES "public"."nodes"("hash") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "user_id_idx" ON "nodes" USING btree ("userId");--> statement-breakpoint
CREATE INDEX "system_id_idx" ON "nodes" USING btree ("systemId");--> statement-breakpoint
CREATE INDEX "node_id_idx" ON "nodes" USING btree ("nodeId");--> statement-breakpoint
CREATE INDEX "hash_idx" ON "nodes" USING btree ("hash");

View File

@@ -1,9 +1,43 @@
{
"id": "53dea8d7-01be-4983-ac75-9de9c9a7f592",
"id": "15ad729d-5756-4c06-87ed-cb8b721201f9",
"prevId": "00000000-0000-0000-0000-000000000000",
"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": "",
@@ -11,6 +45,31 @@
"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
},
@@ -25,37 +84,120 @@
"type": "json",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.users": {
"name": "users",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"hash": {
"name": "hash",
"type": "varchar(16)",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"previous": {
"name": "previous",
"type": "varchar(16)",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {},
"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": {},
"uniqueConstraints": {
"nodes_hash_unique": {
"name": "nodes_hash_unique",
"nullsNotDistinct": false,
"columns": [
"hash"
]
}
},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false

View File

@@ -5,8 +5,8 @@
{
"idx": 0,
"version": "7",
"when": 1734446124519,
"tag": "0000_dark_squirrel_girl",
"when": 1734703963242,
"tag": "0000_known_kid_colt",
"breakpoints": true
}
]

File diff suppressed because one or more lines are too long

View File

@@ -2,14 +2,21 @@ import { drizzle } from "drizzle-orm/node-postgres";
import pg from "pg";
import * as schema from "./schema.ts";
import { migrate } from "drizzle-orm/node-postgres/migrator";
// Use pg driver.
const { Pool } = pg;
// Instantiate Drizzle client with pg driver and schema.
export const db = drizzle({
client: new Pool({
max: 20,
connectionString: Deno.env.get("DATABASE_URL"),
}),
schema,
});
export async function migrateDb() {
await migrate(db, { migrationsFolder: "drizzle" });
console.log("Database migrated");
}

View File

@@ -1,2 +1,2 @@
export * from "../routes/user/user.schema.ts";
export * from "../routes/node/schemas/node.schema.ts";
export * from "../routes/node/node.schema.ts";

View File

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

View File

@@ -1,172 +1,352 @@
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
import { HTTPException } from "hono/http-exception";
import { idRegex, NodeDefinitionSchema } from "./schemas/types.ts";
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 alphabets",
`${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 createResponseSchema = <T extends ZodSchema>(
description: string,
schema: T,
) => ({
200: {
content: { "application/json": { schema } },
description,
},
});
const getUserNodesRoute = createRoute({
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);
}
}
}
}
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: SingleParam("user"),
user: createParamSchema("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);
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" });
}
},
);
const getNodeCollectionRoute = createRoute({
nodeRouter.openapi(
createRoute({
method: "get",
path: "/{user}/{system}.json",
request: {
params: z.object({
user: SingleParam("user"),
system: SingleParam("system").optional(),
user: createParamSchema("user"),
system: createParamSchema("system").optional(),
}),
},
responses: {
200: {
content: {
"application/json": {
schema: z.array(NodeDefinitionSchema),
},
},
description: "Retrieve a single node definition",
},
},
});
nodeRouter.openapi(getNodeCollectionRoute, async (c) => {
responses: createResponseSchema(
"Retrieve nodes for a system",
z.array(NodeDefinitionSchema),
),
}),
async (c) => {
const { user } = c.req.valid("param");
const nodeSystemId = c.req.param("system.json").replace(/\.json$/, "");
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({
id: `${user}/${system}`,
nodes: nodes.map((n) => ({ id: n.id.split("@")[0] })),
});
} catch (error) {
if (error instanceof CustomError) {
throw new HTTPException(error.status, { message: error.message });
}
throw new HTTPException(500, { message: "Internal server error" });
}
},
);
const nodes = await service.getNodesBySystem(user, nodeSystemId);
return c.json(nodes);
});
const getNodeDefinitionRoute = createRoute({
nodeRouter.openapi(
createRoute({
method: "get",
path: "/{user}/{system}/{nodeId}{.+\\.json}",
path: "/{user}/{system}/{nodeId}.json",
request: {
params: ParamsSchema,
params: z.object({
user: createParamSchema("user"),
system: createParamSchema("system"),
nodeId: createParamSchema("nodeId").optional(),
}),
},
responses: {
200: {
content: {
"application/json": {
schema: NodeDefinitionSchema,
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 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}.json",
request: {
params: z.object({
user: createParamSchema("user"),
system: createParamSchema("system"),
nodeId: createParamSchema("nodeId"),
version: createParamSchema("version").optional(),
}),
},
description: "Retrieve a single node definition",
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"),
}),
},
});
nodeRouter.openapi(getNodeDefinitionRoute, async (c) => {
responses: createResponseSchema(
"Retrieve a single node definition",
z.array(NodeDefinitionSchema),
),
}),
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);
}
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" });
}
},
);
const getNodeWasmRoute = createRoute({
nodeRouter.openapi(
createRoute({
method: "get",
path: "/{user}/{system}/{nodeId}{.+\\.wasm}",
path: "/{user}/{system}/{nodeId}.wasm",
request: {
params: ParamsSchema,
params: z.object({
user: createParamSchema("user"),
system: createParamSchema("system"),
nodeId: createParamSchema("nodeId").optional(),
}),
},
responses: {
200: {
content: {
"application/wasm": {
schema: z.any(),
content: { "application/wasm": { schema: z.any() } },
description: "Retrieve a node's WASM file",
},
},
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 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();
const node = await service.createNode(buffer, bytes);
return c.json(node);
});
async (c) => {
const { user, system } = c.req.valid("param");
const nodeId = c.req.param("nodeId.wasm");
console.log("Get NodeWasm by Id", { user, system, nodeId });
try {
const res = await getNodeByVersion(user, system, nodeId);
if (res instanceof ArrayBuffer) {
c.header("Content-Type", "application/wasm");
return c.body(res);
} else {
return c.json(res);
}
} catch (error) {
if (error instanceof CustomError) {
throw new HTTPException(error.status, { message: error.message });
}
throw new HTTPException(500, { message: "Internal server error" });
}
},
);
nodeRouter.openapi(
createRoute({
method: "get",
path: "/{user}/{system}/{nodeId}@{version}.wasm",
request: {
params: z.object({
user: createParamSchema("user"),
system: createParamSchema("system"),
nodeId: createParamSchema("nodeId"),
version: createParamSchema("version").optional(),
}),
},
responses: {
200: {
content: { "application/wasm": { schema: z.any() } },
description: "Retrieve a node's WASM file",
},
},
}),
async (c) => {
const { user, system, nodeId } = c.req.valid("param");
const hash = c.req.param("version.wasm");
try {
const res = await getNodeByVersion(user, system, nodeId, hash);
if (res instanceof ArrayBuffer) {
c.header("Content-Type", "application/wasm");
return c.body(res);
} else {
return c.json(res);
}
} catch (error) {
if (error instanceof CustomError) {
throw new HTTPException(error.status, { message: error.message });
}
throw new HTTPException(500, { message: "Internal server error" });
}
},
);
export { nodeRouter };

View File

@@ -0,0 +1,43 @@
import {
customType,
foreignKey,
index,
json,
pgTable,
serial,
timestamp,
varchar,
} from "drizzle-orm/pg-core";
import { usersTable } from "../user/user.schema.ts";
import { NodeDefinition } from "./validations/types.ts";
const bytea = customType<{
data: ArrayBuffer;
default: false;
}>({
dataType() {
return "bytea";
},
});
export const nodeTable = pgTable("nodes", {
id: serial().primaryKey(),
userId: varchar().notNull().references(() => usersTable.name),
createdAt: timestamp().defaultNow(),
systemId: varchar().notNull(),
nodeId: varchar().notNull(),
content: bytea().notNull(),
definition: json().notNull().$type<NodeDefinition>(),
hash: varchar({ length: 16 }).notNull().unique(),
previous: varchar({ length: 16 }),
}, (table) => [
foreignKey({
columns: [table.previous],
foreignColumns: [table.hash],
name: "node_previous_fk",
}),
index("user_id_idx").on(table.userId),
index("system_id_idx").on(table.systemId),
index("node_id_idx").on(table.nodeId),
index("hash_idx").on(table.hash),
]);

View File

@@ -1,9 +1,10 @@
import { db } from "../../db/db.ts";
import { nodeTable } from "./schemas/node.schema.ts";
import { NodeDefinition, NodeDefinitionSchema } from "./schemas/types.ts";
import { and, eq } from "drizzle-orm";
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/types.ts";
import { extractDefinition } from "./worker/index.ts";
import { InvalidNodeDefinitionError, NodeNotFoundError } from "./errors.ts";
export type CreateNodeDTO = {
id: string;
@@ -15,75 +16,55 @@ export type CreateNodeDTO = {
function getNodeHash(content: Uint8Array) {
const hash = createHash("sha256");
hash.update(content);
return hash.digest("hex").slice(0, 8);
}
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":
console.log("Worker error", e.data.error);
rej(e.data.result);
break;
default:
rej(new Error("Unknown worker response"));
}
};
});
return hash.digest("hex").slice(0, 16);
}
export async function createNode(
wasmBuffer: ArrayBuffer,
content: Uint8Array,
): Promise<NodeDefinition> {
try {
const def = await extractDefinition(wasmBuffer);
const [userId, systemId, nodeId] = def.id.split("/");
const hash = getNodeHash(content);
const node: typeof nodeTable.$inferInsert = {
userId,
systemId,
nodeId,
definition: def,
hash: getNodeHash(content),
hash,
content: content,
};
await db.insert(nodeTable).values(node);
console.log("new node created!");
return def;
} catch (error) {
console.log("Creation Error", { error });
throw error;
const previousNode = await db
.select({ hash: nodeTable.hash })
.from(nodeTable)
.orderBy(asc(nodeTable.createdAt))
.limit(1);
if (previousNode[0]) {
node.previous = previousNode[0].hash;
}
await db.insert(nodeTable).values(node);
return def;
}
export function getNodeDefinitionsByUser(userName: string) {
return db.select({ definition: nodeTable.definition }).from(nodeTable)
.where(
and(
eq(nodeTable.userId, userName),
),
);
export async function getNodeDefinitionsByUser(userName: string) {
const nodes = await db
.select({
definition: nodeTable.definition,
hash: nodeTable.hash,
})
.from(nodeTable)
.where(and(eq(nodeTable.userId, userName)));
return nodes.map((n) => ({
...n.definition,
// id: n.definition.id + "@" + n.hash,
}));
}
export async function getNodesBySystem(
@@ -91,16 +72,26 @@ export async function getNodesBySystem(
systemId: string,
): Promise<NodeDefinition[]> {
const nodes = await db
.select()
.selectDistinctOn(
[nodeTable.userId, nodeTable.systemId, nodeTable.nodeId],
{ definition: nodeTable.definition, hash: nodeTable.hash },
)
.from(nodeTable)
.where(
and(eq(nodeTable.systemId, systemId), eq(nodeTable.userId, username)),
);
)
.orderBy(nodeTable.userId, nodeTable.systemId, nodeTable.nodeId);
const definitions = nodes
.map((node) => NodeDefinitionSchema.safeParse(node.definition))
.filter((v) => v.success)
.map((v) => v.data);
.map(
(node) =>
[NodeDefinitionSchema.safeParse(node.definition), node.hash] as const,
)
.filter(([v]) => v.success)
.map(([v, hash]) => ({
...v.data,
// id: v?.data?.id + "@" + hash,
}));
return definitions;
}
@@ -110,17 +101,21 @@ export async function getNodeWasmById(
systemId: string,
nodeId: string,
) {
const node = await db.select({ content: nodeTable.content }).from(nodeTable)
const node = await db
.select({ content: nodeTable.content })
.from(nodeTable)
.where(
and(
eq(nodeTable.userId, userName),
eq(nodeTable.systemId, systemId),
eq(nodeTable.nodeId, nodeId),
),
).limit(1);
)
.orderBy(asc(nodeTable.createdAt))
.limit(1);
if (!node[0]) {
throw new Error("Node not found");
throw new NodeNotFoundError();
}
return node[0].content;
@@ -131,25 +126,116 @@ export async function getNodeDefinitionById(
systemId: string,
nodeId: string,
) {
const node = await db.select({ definition: nodeTable.definition }).from(
nodeTable,
).where(
const node = await db
.select({
definition: nodeTable.definition,
hash: nodeTable.hash,
})
.from(nodeTable)
.where(
and(
eq(nodeTable.userId, userName),
eq(nodeTable.systemId, systemId),
eq(nodeTable.nodeId, nodeId),
),
).limit(1);
)
.orderBy(asc(nodeTable.createdAt))
.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;
return {
...definition.data,
// id: definition.data.id + "@" + node[0].hash
};
}
export async function getNodeVersions(
user: string,
system: string,
nodeId: string,
) {
const nodes = await db
.select({
definition: nodeTable.definition,
hash: nodeTable.hash,
})
.from(nodeTable)
.where(
and(
eq(nodeTable.userId, user),
eq(nodeTable.systemId, system),
eq(nodeTable.nodeId, nodeId),
),
)
.orderBy(asc(nodeTable.createdAt));
return nodes.map((node) => ({
...node.definition,
// id: node.definition.id + "@" + node.hash,
}));
}
export async function getNodeVersion(
user: string,
system: string,
nodeId: string,
hash: string,
) {
const nodes = await db
.select({
definition: nodeTable.definition,
})
.from(nodeTable)
.where(
and(
eq(nodeTable.userId, user),
eq(nodeTable.systemId, system),
eq(nodeTable.nodeId, nodeId),
eq(nodeTable.hash, hash),
),
)
.limit(1);
if (nodes.length === 0) {
throw new NodeNotFoundError();
}
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;
}

View File

@@ -1,41 +0,0 @@
import {
customType,
integer,
json,
pgTable,
serial,
varchar,
} from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm/relations";
import { usersTable } from "../../user/user.schema.ts";
const bytea = customType<{
data: ArrayBuffer;
default: false;
}>({
dataType() {
return "bytea";
},
});
export const nodeTable = pgTable("nodes", {
id: serial().primaryKey(),
userId: varchar().notNull(),
systemId: varchar().notNull(),
nodeId: varchar().notNull(),
content: bytea().notNull(),
definition: json().notNull(),
hash: varchar({ length: 8 }).notNull(),
previous: integer(),
});
export const nodeRelations = relations(nodeTable, ({ one }) => ({
userId: one(usersTable, {
fields: [nodeTable.userId],
references: [usersTable.id],
}),
previous: one(nodeTable, {
fields: [nodeTable.previous],
references: [nodeTable.id],
}),
}));

View File

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

View File

@@ -1,4 +1,4 @@
import { NodeDefinition } from "../schemas/types.ts";
import { NodeDefinition } from "../validations/types.ts";
type ExtractDefinitionMessage = {
action: "extract-definition";

View File

@@ -1,7 +1,7 @@
/// <reference lib="webworker" />
import { NodeDefinitionSchema } from "../schemas/types.ts";
import { WorkerMessage } from "./types.ts";
import { NodeDefinitionSchema } from "../validations/types.ts";
import { WorkerMessage } from "./messages.ts";
import { createWasmWrapper } from "./utils.ts";
const workerSelf = self as DedicatedWorkerGlobalScope & {

View File

@@ -1,5 +1,5 @@
// @ts-nocheck: Nocheck
import { NodeDefinition } from "../schemas/types.ts";
import { NodeDefinition } from "../validations/types.ts";
const cachedTextDecoder = new TextDecoder("utf-8", {
ignoreBOM: true,

View File

@@ -1,10 +0,0 @@
import { OpenAPIHono } from "@hono/zod-openapi";
import { nodeRouter } from "./node/node.controller.ts";
import { userRouter } from "./user/user.controller.ts";
const router = new OpenAPIHono();
router.route("nodes", nodeRouter);
router.route("users", userRouter);
export { router };

View File

@@ -1,7 +1,8 @@
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
import { UserSchema, usersTable } from "./user.schema.ts";
import { usersTable } from "./user.schema.ts";
import { db } from "../../db/db.ts";
import { findUserByName } from "./user.service.ts";
import { UserSchema } from "./user.validation.ts";
const userRouter = new OpenAPIHono();

View File

@@ -1,14 +1,6 @@
import { pgTable, text, uuid } from "drizzle-orm/pg-core";
import { z } from "@hono/zod-openapi";
export const usersTable = pgTable("users", {
id: uuid().primaryKey().defaultRandom(),
name: text().unique().notNull(),
});
export const UserSchema = z
.object({
id: z.string().uuid(),
name: z.string().min(1), // Non-null text with a unique constraint (enforced at the database level)
})
.openapi("User");

View File

@@ -0,0 +1,8 @@
import { z } from "@hono/zod-openapi";
export const UserSchema = z
.object({
id: z.string().uuid(),
name: z.string().min(1),
})
.openapi("User");

View File

@@ -1,20 +1,18 @@
import { router } from "./routes/router.ts";
import { createUser } from "./routes/user/user.service.ts";
import { swaggerUI } from "@hono/swagger-ui";
import { logger } from "hono/logger";
import { cors } from "hono/cors";
import { OpenAPIHono } from "@hono/zod-openapi";
import { nodeRouter } from "./routes/node/node.controller.ts";
import { userRouter } from "./routes/user/user.controller.ts";
import { migrateDb } from "./db/db.ts";
async function init() {
const openapi = await router.request("/openapi.json");
const json = await openapi.text();
Deno.writeTextFile("openapi.json", json);
await createUser("max");
}
await init();
const router = new OpenAPIHono();
router.use(logger());
router.use(cors());
router.route("nodes", nodeRouter);
router.route("users", userRouter);
router.doc("/openapi.json", {
openapi: "3.0.0",
@@ -27,3 +25,13 @@ router.doc("/openapi.json", {
router.get("/ui", swaggerUI({ url: "/openapi.json" }));
Deno.serve(router.fetch);
async function init() {
await migrateDb();
await createUser("max");
const openapi = await router.request("/openapi.json");
const json = await openapi.text();
Deno.writeTextFile("openapi.json", json);
}
await init();