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">
|
||||
{#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} />
|
||||
|
@@ -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);
|
||||
|
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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
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) {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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
2
store/deno.lock
generated
@@ -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",
|
||||
|
@@ -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",
|
||||
"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
|
||||
|
@@ -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
@@ -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");
|
||||
}
|
||||
|
@@ -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";
|
||||
|
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 { 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 getUserNodesRoute = createRoute({
|
||||
method: "get",
|
||||
path: "/{user}.json",
|
||||
request: {
|
||||
params: z.object({
|
||||
user: SingleParam("user"),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(NodeDefinitionSchema),
|
||||
},
|
||||
},
|
||||
description: "Retrieve a single node definition",
|
||||
},
|
||||
const createResponseSchema = <T extends ZodSchema>(
|
||||
description: string,
|
||||
schema: T,
|
||||
) => ({
|
||||
200: {
|
||||
content: { "application/json": { schema } },
|
||||
description,
|
||||
},
|
||||
});
|
||||
nodeRouter.openapi(getUserNodesRoute, async (c) => {
|
||||
const userId = c.req.param("user.json").replace(/\.json$/, "");
|
||||
const nodes = await service.getNodeDefinitionsByUser(userId);
|
||||
return c.json(nodes);
|
||||
});
|
||||
|
||||
const getNodeCollectionRoute = createRoute({
|
||||
method: "get",
|
||||
path: "/{user}/{system}.json",
|
||||
request: {
|
||||
params: z.object({
|
||||
user: SingleParam("user"),
|
||||
system: SingleParam("system").optional(),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: z.array(NodeDefinitionSchema),
|
||||
},
|
||||
},
|
||||
description: "Retrieve a single node definition",
|
||||
},
|
||||
},
|
||||
});
|
||||
nodeRouter.openapi(getNodeCollectionRoute, async (c) => {
|
||||
const { user } = c.req.valid("param");
|
||||
const nodeSystemId = c.req.param("system.json").replace(/\.json$/, "");
|
||||
|
||||
const nodes = await service.getNodesBySystem(user, nodeSystemId);
|
||||
return c.json(nodes);
|
||||
});
|
||||
|
||||
const getNodeDefinitionRoute = createRoute({
|
||||
method: "get",
|
||||
path: "/{user}/{system}/{nodeId}{.+\\.json}",
|
||||
request: {
|
||||
params: ParamsSchema,
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: NodeDefinitionSchema,
|
||||
},
|
||||
},
|
||||
description: "Retrieve a single node definition",
|
||||
},
|
||||
},
|
||||
});
|
||||
nodeRouter.openapi(getNodeDefinitionRoute, async (c) => {
|
||||
const { user, system, nodeId } = c.req.valid("param");
|
||||
|
||||
const node = await service.getNodeDefinitionById(
|
||||
user,
|
||||
system,
|
||||
nodeId.replace(/\.json$/, ""),
|
||||
);
|
||||
|
||||
if (!node) {
|
||||
throw new HTTPException(404);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return c.json(node);
|
||||
});
|
||||
|
||||
const getNodeWasmRoute = createRoute({
|
||||
method: "get",
|
||||
path: "/{user}/{system}/{nodeId}{.+\\.wasm}",
|
||||
request: {
|
||||
params: ParamsSchema,
|
||||
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" });
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
"application/wasm": {
|
||||
schema: z.any(),
|
||||
},
|
||||
},
|
||||
description: "Retrieve a single node",
|
||||
);
|
||||
|
||||
nodeRouter.openapi(
|
||||
createRoute({
|
||||
method: "get",
|
||||
path: "/{user}.json",
|
||||
request: {
|
||||
params: z.object({
|
||||
user: createParamSchema("user").optional(),
|
||||
}),
|
||||
},
|
||||
responses: createResponseSchema(
|
||||
"Retrieve nodes for a user",
|
||||
z.array(NodeDefinitionSchema),
|
||||
),
|
||||
}),
|
||||
async (c) => {
|
||||
const user = c.req.param("user.json").replace(/\.json$/, "");
|
||||
try {
|
||||
const nodes = await service.getNodeDefinitionsByUser(user);
|
||||
return c.json(nodes);
|
||||
} catch (error) {
|
||||
if (error instanceof CustomError) {
|
||||
throw new HTTPException(error.status, { message: error.message });
|
||||
}
|
||||
throw new HTTPException(500, { message: "Internal server error" });
|
||||
}
|
||||
},
|
||||
});
|
||||
nodeRouter.openapi(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",
|
||||
nodeRouter.openapi(
|
||||
createRoute({
|
||||
method: "get",
|
||||
path: "/{user}/{system}.json",
|
||||
request: {
|
||||
params: z.object({
|
||||
user: createParamSchema("user"),
|
||||
system: createParamSchema("system").optional(),
|
||||
}),
|
||||
},
|
||||
responses: createResponseSchema(
|
||||
"Retrieve nodes for a system",
|
||||
z.array(NodeDefinitionSchema),
|
||||
),
|
||||
}),
|
||||
async (c) => {
|
||||
const { user } = c.req.valid("param");
|
||||
const system = c.req.param("system.json").replace(/\.json$/, "");
|
||||
console.log("Get Nodes by System", { user, system });
|
||||
try {
|
||||
const nodes = await service.getNodesBySystem(user, system);
|
||||
return c.json({
|
||||
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" });
|
||||
}
|
||||
},
|
||||
middleware: [
|
||||
bodyLimit({
|
||||
maxSize: 128 * 1024, // 128kb
|
||||
onError: (c) => {
|
||||
return c.text("Node content too large", 413);
|
||||
);
|
||||
|
||||
nodeRouter.openapi(
|
||||
createRoute({
|
||||
method: "get",
|
||||
path: "/{user}/{system}/{nodeId}.json",
|
||||
request: {
|
||||
params: z.object({
|
||||
user: createParamSchema("user"),
|
||||
system: createParamSchema("system"),
|
||||
nodeId: createParamSchema("nodeId").optional(),
|
||||
}),
|
||||
},
|
||||
responses: createResponseSchema(
|
||||
"Retrieve a single node definition",
|
||||
NodeDefinitionSchema,
|
||||
),
|
||||
}),
|
||||
async (c) => {
|
||||
const { user, system } = c.req.valid("param");
|
||||
const nodeId = c.req.param("nodeId.json").replace(/\.json$/, "");
|
||||
console.log("Get Node by Id", { user, system, nodeId });
|
||||
try {
|
||||
const 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(),
|
||||
}),
|
||||
},
|
||||
responses: createResponseSchema(
|
||||
"Retrieve a single node definition",
|
||||
NodeDefinitionSchema,
|
||||
),
|
||||
}),
|
||||
async (c) => {
|
||||
const { user, system, nodeId } = c.req.valid("param");
|
||||
const hash = c.req.param("version.json");
|
||||
try {
|
||||
const res = await getNodeByVersion(user, system, nodeId, hash);
|
||||
if (res instanceof ArrayBuffer) {
|
||||
c.header("Content-Type", "application/wasm");
|
||||
return c.body(res);
|
||||
} else {
|
||||
return c.json(res);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof CustomError) {
|
||||
throw new HTTPException(error.status, { message: error.message });
|
||||
}
|
||||
throw new HTTPException(500, { message: "Internal server error" });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
nodeRouter.openapi(
|
||||
createRoute({
|
||||
method: "get",
|
||||
path: "/{user}/{system}/{nodeId}/versions.json",
|
||||
request: {
|
||||
params: z.object({
|
||||
user: createParamSchema("user"),
|
||||
system: createParamSchema("system"),
|
||||
nodeId: createParamSchema("nodeId"),
|
||||
}),
|
||||
},
|
||||
responses: createResponseSchema(
|
||||
"Retrieve a single node definition",
|
||||
z.array(NodeDefinitionSchema),
|
||||
),
|
||||
}),
|
||||
async (c) => {
|
||||
const { user, system, nodeId } = c.req.valid("param");
|
||||
|
||||
try {
|
||||
const node = await service.getNodeVersions(user, system, nodeId);
|
||||
|
||||
return c.json(node);
|
||||
} catch (error) {
|
||||
if (error instanceof CustomError) {
|
||||
throw new HTTPException(error.status, { message: error.message });
|
||||
}
|
||||
throw new HTTPException(500, { message: "Internal server error" });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
nodeRouter.openapi(
|
||||
createRoute({
|
||||
method: "get",
|
||||
path: "/{user}/{system}/{nodeId}.wasm",
|
||||
request: {
|
||||
params: z.object({
|
||||
user: createParamSchema("user"),
|
||||
system: createParamSchema("system"),
|
||||
nodeId: createParamSchema("nodeId").optional(),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
content: { "application/wasm": { schema: z.any() } },
|
||||
description: "Retrieve a node's WASM file",
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
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 };
|
||||
|
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 { 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 def = await extractDefinition(wasmBuffer);
|
||||
|
||||
const [userId, systemId, nodeId] = def.id.split("/");
|
||||
const [userId, systemId, nodeId] = def.id.split("/");
|
||||
|
||||
const node: typeof nodeTable.$inferInsert = {
|
||||
userId,
|
||||
systemId,
|
||||
nodeId,
|
||||
definition: def,
|
||||
hash: getNodeHash(content),
|
||||
content: content,
|
||||
};
|
||||
const hash = getNodeHash(content);
|
||||
|
||||
await db.insert(nodeTable).values(node);
|
||||
console.log("new node created!");
|
||||
return def;
|
||||
} catch (error) {
|
||||
console.log("Creation Error", { error });
|
||||
throw error;
|
||||
const node: typeof nodeTable.$inferInsert = {
|
||||
userId,
|
||||
systemId,
|
||||
nodeId,
|
||||
definition: def,
|
||||
hash,
|
||||
content: content,
|
||||
};
|
||||
|
||||
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(
|
||||
and(
|
||||
eq(nodeTable.userId, userName),
|
||||
eq(nodeTable.systemId, systemId),
|
||||
eq(nodeTable.nodeId, nodeId),
|
||||
),
|
||||
).limit(1);
|
||||
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),
|
||||
),
|
||||
)
|
||||
.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;
|
||||
}
|
||||
|
@@ -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 = {
|
||||
action: "extract-definition";
|
@@ -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 & {
|
||||
|
@@ -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,
|
||||
|
@@ -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 { 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();
|
||||
|
||||
|
@@ -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");
|
||||
|
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 { 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();
|
Reference in New Issue
Block a user