Compare commits
19 Commits
a740da1099
...
main
Author | SHA1 | Date | |
---|---|---|---|
05b192e7ab | |||
edcaab4bd4
|
|||
a99040f42e
|
|||
fca59e87e5
|
|||
05e8970475
|
|||
385d1dd831
|
|||
dc46c4b64c
|
|||
15ff1cc52d | |||
a70e8195a2 | |||
4ca36b324b | |||
221817fc16 | |||
7060b37df5 | |||
ec037a3bbd | |||
2814165ee6 | |||
c6badff1ee | |||
a0d420517c | |||
eadd37bfa4 | |||
9d698be86f | |||
540d0549d7 |
@@ -23,7 +23,7 @@
|
|||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
{#if !activeUser}
|
{#if !activeUser}
|
||||||
{#await registry.fetchUsers()}
|
{#await registry.fetchUsers()}
|
||||||
<div>Loading...</div>
|
<div>Loading Users...</div>
|
||||||
{:then users}
|
{:then users}
|
||||||
{#each users as user}
|
{#each users as user}
|
||||||
<button
|
<button
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
{/await}
|
{/await}
|
||||||
{:else if !activeCollection}
|
{:else if !activeCollection}
|
||||||
{#await registry.fetchUser(activeUser)}
|
{#await registry.fetchUser(activeUser)}
|
||||||
<div>Loading...</div>
|
<div>Loading User...</div>
|
||||||
{:then user}
|
{:then user}
|
||||||
{#each user.collections as collection}
|
{#each user.collections as collection}
|
||||||
<button
|
<button
|
||||||
@@ -53,11 +53,11 @@
|
|||||||
{/await}
|
{/await}
|
||||||
{:else if !activeNode}
|
{:else if !activeNode}
|
||||||
{#await registry.fetchCollection(`${activeUser}/${activeCollection}`)}
|
{#await registry.fetchCollection(`${activeUser}/${activeCollection}`)}
|
||||||
<div>Loading...</div>
|
<div>Loading Collection...</div>
|
||||||
{:then collection}
|
{:then collection}
|
||||||
{#each collection.nodes as node}
|
{#each collection.nodes as node}
|
||||||
{#await registry.fetchNodeDefinition(node.id)}
|
{#await registry.fetchNodeDefinition(node.id)}
|
||||||
<div>Loading... {node.id}</div>
|
<div>Loading Node... {node.id}</div>
|
||||||
{:then node}
|
{:then node}
|
||||||
{#if node}
|
{#if node}
|
||||||
<DraggableNode {node} />
|
<DraggableNode {node} />
|
||||||
|
@@ -32,8 +32,10 @@
|
|||||||
let performanceStore = createPerformanceStore();
|
let performanceStore = createPerformanceStore();
|
||||||
|
|
||||||
const registryCache = new IndexDBCache("node-registry");
|
const registryCache = new IndexDBCache("node-registry");
|
||||||
const nodeRegistry = new RemoteNodeRegistry("");
|
const nodeRegistry = new RemoteNodeRegistry(
|
||||||
nodeRegistry.cache = registryCache;
|
"https://node-store.app.max-richter.dev",
|
||||||
|
registryCache,
|
||||||
|
);
|
||||||
const workerRuntime = new WorkerRuntimeExecutor();
|
const workerRuntime = new WorkerRuntimeExecutor();
|
||||||
const runtimeCache = new MemoryRuntimeCache();
|
const runtimeCache = new MemoryRuntimeCache();
|
||||||
const memoryRuntime = new MemoryRuntimeExecutor(nodeRegistry, runtimeCache);
|
const memoryRuntime = new MemoryRuntimeExecutor(nodeRegistry, runtimeCache);
|
||||||
|
@@ -18,14 +18,14 @@
|
|||||||
"hidden": true,
|
"hidden": true,
|
||||||
"min": 0,
|
"min": 0,
|
||||||
"max": 1,
|
"max": 1,
|
||||||
"value": 0.5,
|
"value": 0.5
|
||||||
},
|
},
|
||||||
"depth": {
|
"depth": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"min": 1,
|
"min": 1,
|
||||||
"max": 10,
|
"max": 10,
|
||||||
"hidden": true,
|
"hidden": true,
|
||||||
"value": 1,
|
"value": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -13,7 +13,7 @@ export class RemoteNodeRegistry implements NodeRegistry {
|
|||||||
|
|
||||||
fetch: typeof fetch = globalThis.fetch.bind(globalThis);
|
fetch: typeof fetch = globalThis.fetch.bind(globalThis);
|
||||||
|
|
||||||
constructor(private url: string) { }
|
constructor(private url: string, private cache?: AsyncCache<ArrayBuffer>) { }
|
||||||
|
|
||||||
async fetchUsers() {
|
async fetchUsers() {
|
||||||
const response = await this.fetch(`${this.url}/nodes/users.json`);
|
const response = await this.fetch(`${this.url}/nodes/users.json`);
|
||||||
@@ -24,7 +24,7 @@ export class RemoteNodeRegistry implements NodeRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fetchUser(userId: `${string}`) {
|
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) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to load user ${userId}`);
|
throw new Error(`Failed to load user ${userId}`);
|
||||||
}
|
}
|
||||||
|
13
store/Dockerfile
Normal file
13
store/Dockerfile
Normal 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"]
|
@@ -44,7 +44,8 @@ for await (const dir of dirs) {
|
|||||||
async function postNode(node: Node) {
|
async function postNode(node: Node) {
|
||||||
const wasmContent = await Deno.readFile(node.path);
|
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, {
|
const res = await fetch(url, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -55,7 +56,7 @@ async function postNode(node: Node) {
|
|||||||
console.log(`Uploaded ${node.id}`);
|
console.log(`Uploaded ${node.id}`);
|
||||||
} else {
|
} else {
|
||||||
const text = await res.text();
|
const text = await res.text();
|
||||||
console.log(`Failed to upload ${node.id}: ${text}`);
|
console.log(`Failed to upload ${node.id}: ${res.status} ${text}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"tasks": {
|
"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",
|
"test": "deno run vitest",
|
||||||
"drizzle": "podman-compose exec app deno --env -A --node-modules-dir npm:drizzle-kit",
|
"drizzle": "podman-compose exec app deno --env -A --node-modules-dir npm:drizzle-kit",
|
||||||
"upload": "deno run --allow-read --allow-net bin/upload.ts"
|
"upload": "deno run --allow-read --allow-net bin/upload.ts"
|
||||||
@@ -11,6 +12,7 @@
|
|||||||
"@hono/zod-openapi": "npm:@hono/zod-openapi@^0.18.3",
|
"@hono/zod-openapi": "npm:@hono/zod-openapi@^0.18.3",
|
||||||
"@std/assert": "jsr:@std/assert@1",
|
"@std/assert": "jsr:@std/assert@1",
|
||||||
"@types/pg": "npm:@types/pg@^8.11.10",
|
"@types/pg": "npm:@types/pg@^8.11.10",
|
||||||
|
"drizzle-kit": "npm:drizzle-kit@^0.30.1",
|
||||||
"drizzle-orm": "npm:drizzle-orm@^0.38.2",
|
"drizzle-orm": "npm:drizzle-orm@^0.38.2",
|
||||||
"hono": "npm:hono@^4.6.14",
|
"hono": "npm:hono@^4.6.14",
|
||||||
"pg": "npm:pg@^8.13.1",
|
"pg": "npm:pg@^8.13.1",
|
||||||
|
2
store/deno.lock
generated
2
store/deno.lock
generated
@@ -15,6 +15,7 @@
|
|||||||
"npm:@types/node@*": "22.5.4",
|
"npm:@types/node@*": "22.5.4",
|
||||||
"npm:@types/pg@^8.11.10": "8.11.10",
|
"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_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: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:hono@^4.6.14": "4.6.14",
|
||||||
"npm:pg@^8.13.1": "8.13.1",
|
"npm:pg@^8.13.1": "8.13.1",
|
||||||
@@ -871,6 +872,7 @@
|
|||||||
"npm:@hono/swagger-ui@0.5",
|
"npm:@hono/swagger-ui@0.5",
|
||||||
"npm:@hono/zod-openapi@~0.18.3",
|
"npm:@hono/zod-openapi@~0.18.3",
|
||||||
"npm:@types/pg@^8.11.10",
|
"npm:@types/pg@^8.11.10",
|
||||||
|
"npm:drizzle-kit@~0.30.1",
|
||||||
"npm:drizzle-orm@~0.38.2",
|
"npm:drizzle-orm@~0.38.2",
|
||||||
"npm:hono@^4.6.14",
|
"npm:hono@^4.6.14",
|
||||||
"npm:pg@^8.13.1",
|
"npm:pg@^8.13.1",
|
||||||
|
@@ -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
|
|
||||||
);
|
|
25
store/drizzle/0000_known_kid_colt.sql
Normal file
25
store/drizzle/0000_known_kid_colt.sql
Normal 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");
|
@@ -1,9 +1,43 @@
|
|||||||
{
|
{
|
||||||
"id": "53dea8d7-01be-4983-ac75-9de9c9a7f592",
|
"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",
|
||||||
"tables": {
|
"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": {
|
"public.nodes": {
|
||||||
"name": "nodes",
|
"name": "nodes",
|
||||||
"schema": "",
|
"schema": "",
|
||||||
@@ -11,6 +45,31 @@
|
|||||||
"id": {
|
"id": {
|
||||||
"name": "id",
|
"name": "id",
|
||||||
"type": "serial",
|
"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,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
@@ -25,37 +84,120 @@
|
|||||||
"type": "json",
|
"type": "json",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": true
|
"notNull": true
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"indexes": {},
|
"hash": {
|
||||||
"foreignKeys": {},
|
"name": "hash",
|
||||||
"compositePrimaryKeys": {},
|
"type": "varchar(16)",
|
||||||
"uniqueConstraints": {},
|
"primaryKey": false,
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": false
|
|
||||||
},
|
|
||||||
"public.users": {
|
|
||||||
"name": "users",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "serial",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
"notNull": true
|
||||||
},
|
},
|
||||||
"name": {
|
"previous": {
|
||||||
"name": "name",
|
"name": "previous",
|
||||||
"type": "text",
|
"type": "varchar(16)",
|
||||||
"primaryKey": false,
|
"primaryKey": false,
|
||||||
"notNull": false
|
"notNull": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"indexes": {},
|
"indexes": {
|
||||||
"foreignKeys": {},
|
"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": {},
|
"compositePrimaryKeys": {},
|
||||||
"uniqueConstraints": {},
|
"uniqueConstraints": {
|
||||||
|
"nodes_hash_unique": {
|
||||||
|
"name": "nodes_hash_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"hash"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"policies": {},
|
"policies": {},
|
||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
|
@@ -5,8 +5,8 @@
|
|||||||
{
|
{
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1734446124519,
|
"when": 1734703963242,
|
||||||
"tag": "0000_dark_squirrel_girl",
|
"tag": "0000_known_kid_colt",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
File diff suppressed because one or more lines are too long
@@ -2,14 +2,21 @@ import { drizzle } from "drizzle-orm/node-postgres";
|
|||||||
import pg from "pg";
|
import pg from "pg";
|
||||||
import * as schema from "./schema.ts";
|
import * as schema from "./schema.ts";
|
||||||
|
|
||||||
|
import { migrate } from "drizzle-orm/node-postgres/migrator";
|
||||||
|
|
||||||
// Use pg driver.
|
// Use pg driver.
|
||||||
const { Pool } = pg;
|
const { Pool } = pg;
|
||||||
|
|
||||||
// Instantiate Drizzle client with pg driver and schema.
|
// Instantiate Drizzle client with pg driver and schema.
|
||||||
export const db = drizzle({
|
export const db = drizzle({
|
||||||
client: new Pool({
|
client: new Pool({
|
||||||
|
max: 20,
|
||||||
connectionString: Deno.env.get("DATABASE_URL"),
|
connectionString: Deno.env.get("DATABASE_URL"),
|
||||||
}),
|
}),
|
||||||
schema,
|
schema,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export async function migrateDb() {
|
||||||
|
await migrate(db, { migrationsFolder: "drizzle" });
|
||||||
|
console.log("Database migrated");
|
||||||
|
}
|
||||||
|
@@ -1,2 +1,2 @@
|
|||||||
export * from "../routes/user/user.schema.ts";
|
export * from "../routes/user/user.schema.ts";
|
||||||
export * from "../routes/node/schemas/node.schema.ts";
|
export * from "../routes/node/node.schema.ts";
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
@@ -1,172 +1,352 @@
|
|||||||
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
|
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
|
||||||
import { HTTPException } from "hono/http-exception";
|
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 * 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 alphabets",
|
`${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 } },
|
||||||
|
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",
|
method: "get",
|
||||||
path: "/{user}.json",
|
path: "/{user}.json",
|
||||||
request: {
|
request: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
user: SingleParam("user"),
|
user: createParamSchema("user").optional(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: createResponseSchema(
|
||||||
200: {
|
"Retrieve nodes for a user",
|
||||||
content: {
|
z.array(NodeDefinitionSchema),
|
||||||
"application/json": {
|
),
|
||||||
schema: z.array(NodeDefinitionSchema),
|
}),
|
||||||
},
|
async (c) => {
|
||||||
},
|
const user = c.req.param("user.json").replace(/\.json$/, "");
|
||||||
description: "Retrieve a single node definition",
|
try {
|
||||||
},
|
const nodes = await service.getNodeDefinitionsByUser(user);
|
||||||
},
|
|
||||||
});
|
|
||||||
nodeRouter.openapi(getUserNodesRoute, async (c) => {
|
|
||||||
const userId = c.req.param("user.json").replace(/\.json$/, "");
|
|
||||||
const nodes = await service.getNodeDefinitionsByUser(userId);
|
|
||||||
return c.json(nodes);
|
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",
|
method: "get",
|
||||||
path: "/{user}/{system}.json",
|
path: "/{user}/{system}.json",
|
||||||
request: {
|
request: {
|
||||||
params: z.object({
|
params: z.object({
|
||||||
user: SingleParam("user"),
|
user: createParamSchema("user"),
|
||||||
system: SingleParam("system").optional(),
|
system: createParamSchema("system").optional(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: createResponseSchema(
|
||||||
200: {
|
"Retrieve nodes for a system",
|
||||||
content: {
|
z.array(NodeDefinitionSchema),
|
||||||
"application/json": {
|
),
|
||||||
schema: z.array(NodeDefinitionSchema),
|
}),
|
||||||
},
|
async (c) => {
|
||||||
},
|
|
||||||
description: "Retrieve a single node definition",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
nodeRouter.openapi(getNodeCollectionRoute, async (c) => {
|
|
||||||
const { user } = c.req.valid("param");
|
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);
|
nodeRouter.openapi(
|
||||||
return c.json(nodes);
|
createRoute({
|
||||||
});
|
|
||||||
|
|
||||||
const getNodeDefinitionRoute = createRoute({
|
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/{user}/{system}/{nodeId}{.+\\.json}",
|
path: "/{user}/{system}/{nodeId}.json",
|
||||||
request: {
|
request: {
|
||||||
params: ParamsSchema,
|
params: z.object({
|
||||||
|
user: createParamSchema("user"),
|
||||||
|
system: createParamSchema("system"),
|
||||||
|
nodeId: createParamSchema("nodeId").optional(),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: createResponseSchema(
|
||||||
200: {
|
"Retrieve a single node definition",
|
||||||
content: {
|
NodeDefinitionSchema,
|
||||||
"application/json": {
|
),
|
||||||
schema: 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"),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
});
|
responses: createResponseSchema(
|
||||||
nodeRouter.openapi(getNodeDefinitionRoute, async (c) => {
|
"Retrieve a single node definition",
|
||||||
|
z.array(NodeDefinitionSchema),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
async (c) => {
|
||||||
const { user, system, nodeId } = c.req.valid("param");
|
const { user, system, nodeId } = c.req.valid("param");
|
||||||
|
|
||||||
const node = await service.getNodeDefinitionById(
|
try {
|
||||||
user,
|
const node = await service.getNodeVersions(user, system, nodeId);
|
||||||
system,
|
|
||||||
nodeId.replace(/\.json$/, ""),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!node) {
|
|
||||||
throw new HTTPException(404);
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.json(node);
|
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",
|
method: "get",
|
||||||
path: "/{user}/{system}/{nodeId}{.+\\.wasm}",
|
path: "/{user}/{system}/{nodeId}.wasm",
|
||||||
request: {
|
request: {
|
||||||
params: ParamsSchema,
|
params: z.object({
|
||||||
|
user: createParamSchema("user"),
|
||||||
|
system: createParamSchema("system"),
|
||||||
|
nodeId: createParamSchema("nodeId").optional(),
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
responses: {
|
responses: {
|
||||||
200: {
|
200: {
|
||||||
content: {
|
content: { "application/wasm": { schema: z.any() } },
|
||||||
"application/wasm": {
|
description: "Retrieve a node's WASM file",
|
||||||
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 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);
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
],
|
async (c) => {
|
||||||
});
|
const { user, system } = c.req.valid("param");
|
||||||
nodeRouter.openapi(createNodeRoute, async (c) => {
|
const nodeId = c.req.param("nodeId.wasm");
|
||||||
const buffer = await c.req.arrayBuffer();
|
console.log("Get NodeWasm by Id", { user, system, nodeId });
|
||||||
const bytes = await (await c.req.blob()).bytes();
|
try {
|
||||||
const node = await service.createNode(buffer, bytes);
|
const res = await getNodeByVersion(user, system, nodeId);
|
||||||
return c.json(node);
|
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 };
|
||||||
|
43
store/src/routes/node/node.schema.ts
Normal file
43
store/src/routes/node/node.schema.ts
Normal 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),
|
||||||
|
]);
|
@@ -1,9 +1,10 @@
|
|||||||
import { db } from "../../db/db.ts";
|
import { db } from "../../db/db.ts";
|
||||||
import { nodeTable } from "./schemas/node.schema.ts";
|
import { nodeTable } from "./node.schema.ts";
|
||||||
import { NodeDefinition, NodeDefinitionSchema } from "./schemas/types.ts";
|
import { NodeDefinition, NodeDefinitionSchema } from "./validations/types.ts";
|
||||||
import { and, 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/types.ts";
|
import { extractDefinition } from "./worker/index.ts";
|
||||||
|
import { InvalidNodeDefinitionError, NodeNotFoundError } from "./errors.ts";
|
||||||
|
|
||||||
export type CreateNodeDTO = {
|
export type CreateNodeDTO = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -15,75 +16,55 @@ export type CreateNodeDTO = {
|
|||||||
function getNodeHash(content: Uint8Array) {
|
function getNodeHash(content: Uint8Array) {
|
||||||
const hash = createHash("sha256");
|
const hash = createHash("sha256");
|
||||||
hash.update(content);
|
hash.update(content);
|
||||||
return hash.digest("hex").slice(0, 8);
|
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":
|
|
||||||
console.log("Worker error", e.data.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,
|
||||||
): Promise<NodeDefinition> {
|
): Promise<NodeDefinition> {
|
||||||
try {
|
|
||||||
const def = await extractDefinition(wasmBuffer);
|
const def = await extractDefinition(wasmBuffer);
|
||||||
|
|
||||||
const [userId, systemId, nodeId] = def.id.split("/");
|
const [userId, systemId, nodeId] = def.id.split("/");
|
||||||
|
|
||||||
|
const hash = getNodeHash(content);
|
||||||
|
|
||||||
const node: typeof nodeTable.$inferInsert = {
|
const node: typeof nodeTable.$inferInsert = {
|
||||||
userId,
|
userId,
|
||||||
systemId,
|
systemId,
|
||||||
nodeId,
|
nodeId,
|
||||||
definition: def,
|
definition: def,
|
||||||
hash: getNodeHash(content),
|
hash,
|
||||||
content: content,
|
content: content,
|
||||||
};
|
};
|
||||||
|
|
||||||
await db.insert(nodeTable).values(node);
|
const previousNode = await db
|
||||||
console.log("new node created!");
|
.select({ hash: nodeTable.hash })
|
||||||
return def;
|
.from(nodeTable)
|
||||||
} catch (error) {
|
.orderBy(asc(nodeTable.createdAt))
|
||||||
console.log("Creation Error", { error });
|
.limit(1);
|
||||||
throw error;
|
|
||||||
|
if (previousNode[0]) {
|
||||||
|
node.previous = previousNode[0].hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await db.insert(nodeTable).values(node);
|
||||||
|
return def;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getNodeDefinitionsByUser(userName: string) {
|
export async function getNodeDefinitionsByUser(userName: string) {
|
||||||
return db.select({ definition: nodeTable.definition }).from(nodeTable)
|
const nodes = await db
|
||||||
.where(
|
.select({
|
||||||
and(
|
definition: nodeTable.definition,
|
||||||
eq(nodeTable.userId, userName),
|
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(
|
export async function getNodesBySystem(
|
||||||
@@ -91,16 +72,26 @@ export async function getNodesBySystem(
|
|||||||
systemId: string,
|
systemId: string,
|
||||||
): Promise<NodeDefinition[]> {
|
): Promise<NodeDefinition[]> {
|
||||||
const nodes = await db
|
const nodes = await db
|
||||||
.select()
|
.selectDistinctOn(
|
||||||
|
[nodeTable.userId, nodeTable.systemId, nodeTable.nodeId],
|
||||||
|
{ definition: nodeTable.definition, hash: nodeTable.hash },
|
||||||
|
)
|
||||||
.from(nodeTable)
|
.from(nodeTable)
|
||||||
.where(
|
.where(
|
||||||
and(eq(nodeTable.systemId, systemId), eq(nodeTable.userId, username)),
|
and(eq(nodeTable.systemId, systemId), eq(nodeTable.userId, username)),
|
||||||
);
|
)
|
||||||
|
.orderBy(nodeTable.userId, nodeTable.systemId, nodeTable.nodeId);
|
||||||
|
|
||||||
const definitions = nodes
|
const definitions = nodes
|
||||||
.map((node) => NodeDefinitionSchema.safeParse(node.definition))
|
.map(
|
||||||
.filter((v) => v.success)
|
(node) =>
|
||||||
.map((v) => v.data);
|
[NodeDefinitionSchema.safeParse(node.definition), node.hash] as const,
|
||||||
|
)
|
||||||
|
.filter(([v]) => v.success)
|
||||||
|
.map(([v, hash]) => ({
|
||||||
|
...v.data,
|
||||||
|
// id: v?.data?.id + "@" + hash,
|
||||||
|
}));
|
||||||
|
|
||||||
return definitions;
|
return definitions;
|
||||||
}
|
}
|
||||||
@@ -110,17 +101,21 @@ export async function getNodeWasmById(
|
|||||||
systemId: string,
|
systemId: string,
|
||||||
nodeId: string,
|
nodeId: string,
|
||||||
) {
|
) {
|
||||||
const node = await db.select({ content: nodeTable.content }).from(nodeTable)
|
const node = await db
|
||||||
|
.select({ content: nodeTable.content })
|
||||||
|
.from(nodeTable)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(nodeTable.userId, userName),
|
eq(nodeTable.userId, userName),
|
||||||
eq(nodeTable.systemId, systemId),
|
eq(nodeTable.systemId, systemId),
|
||||||
eq(nodeTable.nodeId, nodeId),
|
eq(nodeTable.nodeId, nodeId),
|
||||||
),
|
),
|
||||||
).limit(1);
|
)
|
||||||
|
.orderBy(asc(nodeTable.createdAt))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
if (!node[0]) {
|
if (!node[0]) {
|
||||||
throw new Error("Node not found");
|
throw new NodeNotFoundError();
|
||||||
}
|
}
|
||||||
|
|
||||||
return node[0].content;
|
return node[0].content;
|
||||||
@@ -131,25 +126,116 @@ export async function getNodeDefinitionById(
|
|||||||
systemId: string,
|
systemId: string,
|
||||||
nodeId: string,
|
nodeId: string,
|
||||||
) {
|
) {
|
||||||
const node = await db.select({ definition: nodeTable.definition }).from(
|
const node = await db
|
||||||
nodeTable,
|
.select({
|
||||||
).where(
|
definition: nodeTable.definition,
|
||||||
|
hash: nodeTable.hash,
|
||||||
|
})
|
||||||
|
.from(nodeTable)
|
||||||
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(nodeTable.userId, userName),
|
eq(nodeTable.userId, userName),
|
||||||
eq(nodeTable.systemId, systemId),
|
eq(nodeTable.systemId, systemId),
|
||||||
eq(nodeTable.nodeId, nodeId),
|
eq(nodeTable.nodeId, nodeId),
|
||||||
),
|
),
|
||||||
).limit(1);
|
)
|
||||||
|
.orderBy(asc(nodeTable.createdAt))
|
||||||
|
.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;
|
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;
|
||||||
}
|
}
|
||||||
|
@@ -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],
|
|
||||||
}),
|
|
||||||
}));
|
|
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());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
import { NodeDefinition } from "../schemas/types.ts";
|
import { NodeDefinition } from "../validations/types.ts";
|
||||||
|
|
||||||
type ExtractDefinitionMessage = {
|
type ExtractDefinitionMessage = {
|
||||||
action: "extract-definition";
|
action: "extract-definition";
|
@@ -1,7 +1,7 @@
|
|||||||
/// <reference lib="webworker" />
|
/// <reference lib="webworker" />
|
||||||
|
|
||||||
import { NodeDefinitionSchema } from "../schemas/types.ts";
|
import { NodeDefinitionSchema } from "../validations/types.ts";
|
||||||
import { WorkerMessage } from "./types.ts";
|
import { WorkerMessage } from "./messages.ts";
|
||||||
import { createWasmWrapper } from "./utils.ts";
|
import { createWasmWrapper } from "./utils.ts";
|
||||||
|
|
||||||
const workerSelf = self as DedicatedWorkerGlobalScope & {
|
const workerSelf = self as DedicatedWorkerGlobalScope & {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
// @ts-nocheck: Nocheck
|
// @ts-nocheck: Nocheck
|
||||||
import { NodeDefinition } from "../schemas/types.ts";
|
import { NodeDefinition } from "../validations/types.ts";
|
||||||
|
|
||||||
const cachedTextDecoder = new TextDecoder("utf-8", {
|
const cachedTextDecoder = new TextDecoder("utf-8", {
|
||||||
ignoreBOM: true,
|
ignoreBOM: true,
|
||||||
|
@@ -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 };
|
|
@@ -1,7 +1,8 @@
|
|||||||
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
|
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 { db } from "../../db/db.ts";
|
||||||
import { findUserByName } from "./user.service.ts";
|
import { findUserByName } from "./user.service.ts";
|
||||||
|
import { UserSchema } from "./user.validation.ts";
|
||||||
|
|
||||||
const userRouter = new OpenAPIHono();
|
const userRouter = new OpenAPIHono();
|
||||||
|
|
||||||
|
@@ -1,14 +1,6 @@
|
|||||||
import { pgTable, text, uuid } from "drizzle-orm/pg-core";
|
import { pgTable, text, uuid } from "drizzle-orm/pg-core";
|
||||||
import { z } from "@hono/zod-openapi";
|
|
||||||
|
|
||||||
export const usersTable = pgTable("users", {
|
export const usersTable = pgTable("users", {
|
||||||
id: uuid().primaryKey().defaultRandom(),
|
id: uuid().primaryKey().defaultRandom(),
|
||||||
name: text().unique().notNull(),
|
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");
|
|
||||||
|
8
store/src/routes/user/user.validation.ts
Normal file
8
store/src/routes/user/user.validation.ts
Normal 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");
|
@@ -1,20 +1,18 @@
|
|||||||
import { router } from "./routes/router.ts";
|
|
||||||
import { createUser } from "./routes/user/user.service.ts";
|
import { createUser } from "./routes/user/user.service.ts";
|
||||||
import { swaggerUI } from "@hono/swagger-ui";
|
import { swaggerUI } from "@hono/swagger-ui";
|
||||||
import { logger } from "hono/logger";
|
import { logger } from "hono/logger";
|
||||||
import { cors } from "hono/cors";
|
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 router = new OpenAPIHono();
|
||||||
const openapi = await router.request("/openapi.json");
|
|
||||||
const json = await openapi.text();
|
|
||||||
Deno.writeTextFile("openapi.json", json);
|
|
||||||
|
|
||||||
await createUser("max");
|
|
||||||
}
|
|
||||||
await init();
|
|
||||||
|
|
||||||
router.use(logger());
|
router.use(logger());
|
||||||
router.use(cors());
|
router.use(cors());
|
||||||
|
router.route("nodes", nodeRouter);
|
||||||
|
router.route("users", userRouter);
|
||||||
|
|
||||||
router.doc("/openapi.json", {
|
router.doc("/openapi.json", {
|
||||||
openapi: "3.0.0",
|
openapi: "3.0.0",
|
||||||
@@ -27,3 +25,13 @@ router.doc("/openapi.json", {
|
|||||||
router.get("/ui", swaggerUI({ url: "/openapi.json" }));
|
router.get("/ui", swaggerUI({ url: "/openapi.json" }));
|
||||||
|
|
||||||
Deno.serve(router.fetch);
|
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();
|
Reference in New Issue
Block a user