diff --git a/store/bin/upload.ts b/store/bin/upload.ts index fb2da96..9d6f246 100644 --- a/store/bin/upload.ts +++ b/store/bin/upload.ts @@ -44,7 +44,7 @@ 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 res = await fetch(url, { method: "POST", @@ -55,7 +55,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}`); } } diff --git a/store/drizzle/0001_amazing_weapon_omega.sql b/store/drizzle/0001_amazing_weapon_omega.sql new file mode 100644 index 0000000..a2230a1 --- /dev/null +++ b/store/drizzle/0001_amazing_weapon_omega.sql @@ -0,0 +1 @@ +ALTER TABLE "nodes" ADD COLUMN "createdAt" timestamp DEFAULT now(); \ No newline at end of file diff --git a/store/drizzle/meta/0001_snapshot.json b/store/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..adbd0a9 --- /dev/null +++ b/store/drizzle/meta/0001_snapshot.json @@ -0,0 +1,217 @@ +{ + "id": "080ee514-5516-4400-9286-295826df6f8a", + "prevId": "b5fc8bcf-82d4-4d2e-bcd1-89d5a238f5e2", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_name_unique": { + "name": "users_name_unique", + "nullsNotDistinct": false, + "columns": [ + "name" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.nodes": { + "name": "nodes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "systemId": { + "name": "systemId", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "nodeId": { + "name": "nodeId", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "bytea", + "primaryKey": false, + "notNull": true + }, + "definition": { + "name": "definition", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "hash": { + "name": "hash", + "type": "varchar(8)", + "primaryKey": false, + "notNull": true + }, + "previous": { + "name": "previous", + "type": "varchar(8)", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "user_id_idx": { + "name": "user_id_idx", + "columns": [ + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "system_id_idx": { + "name": "system_id_idx", + "columns": [ + { + "expression": "systemId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "node_id_idx": { + "name": "node_id_idx", + "columns": [ + { + "expression": "nodeId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "hash_idx": { + "name": "hash_idx", + "columns": [ + { + "expression": "hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "nodes_userId_users_name_fk": { + "name": "nodes_userId_users_name_fk", + "tableFrom": "nodes", + "tableTo": "users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "name" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "node_previous_fk": { + "name": "node_previous_fk", + "tableFrom": "nodes", + "tableTo": "nodes", + "columnsFrom": [ + "previous" + ], + "columnsTo": [ + "hash" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "nodes_hash_unique": { + "name": "nodes_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "hash" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/store/drizzle/meta/_journal.json b/store/drizzle/meta/_journal.json index 9f2f80a..7a2bfe6 100644 --- a/store/drizzle/meta/_journal.json +++ b/store/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1734695353420, "tag": "0000_steep_bromley", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1734696211359, + "tag": "0001_amazing_weapon_omega", + "breakpoints": true } ] } \ No newline at end of file diff --git a/store/openapi.json b/store/openapi.json index 082c588..2f7d493 100644 --- a/store/openapi.json +++ b/store/openapi.json @@ -1 +1 @@ -{"openapi":"3.0.0","info":{"version":"1.0.0","title":"Nodarium API"},"components":{"schemas":{"NodeInput":{"anyOf":[{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["seed"]},"value":{"type":"number"}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["boolean"]},"value":{"type":"boolean"}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["float"]},"element":{"type":"string","enum":["slider"]},"value":{"type":"number"},"min":{"type":"number"},"max":{"type":"number"},"step":{"type":"number"}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["integer"]},"element":{"type":"string","enum":["slider"]},"value":{"type":"number"},"min":{"type":"number"},"max":{"type":"number"}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["select"]},"options":{"type":"array","items":{"type":"string"}},"value":{"type":"number"}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["seed"]},"value":{"type":"number"}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["vec3"]},"value":{"type":"array","items":{"type":"number"}}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["geometry"]}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["path"]}},"required":["type"]}]},"NodeDefinition":{"type":"object","properties":{"id":{"type":"string","pattern":"^([a-z0-9-]+)\\/([a-z0-9-]+)\\/([a-z0-9-]+)$"},"inputs":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/NodeInput"}},"outputs":{"type":"array","items":{"type":"string"}},"meta":{"type":"object","properties":{"description":{"type":"string"},"title":{"type":"string"}}}},"required":["id"]},"User":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","minLength":1}},"required":["id","name"]}},"parameters":{}},"paths":{"/nodes/{user}.json":{"get":{"parameters":[{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"user","in":"path"}],"responses":{"200":{"description":"Retrieve a single node definition","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/NodeDefinition"}}}}}}}},"/nodes/{user}/{system}.json":{"get":{"parameters":[{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"user","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":false,"name":"system","in":"path"}],"responses":{"200":{"description":"Retrieve a single node definition","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/NodeDefinition"}}}}}}}},"/nodes/{user}/{system}/{nodeId}{.+\\.json}":{"get":{"parameters":[{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"user","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"system","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"nodeId","in":"path"}],"responses":{"200":{"description":"Retrieve a single node definition","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NodeDefinition"}}}}}}},"/nodes/{user}/{system}/{nodeId}{.+\\.wasm}":{"get":{"parameters":[{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"user","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"system","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"nodeId","in":"path"}],"responses":{"200":{"description":"Retrieve a single node","content":{"application/wasm":{"schema":{"nullable":true}}}}}}},"/nodes":{"post":{"responses":{"200":{"description":"Create a single node","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NodeDefinition"}}}}}}},"/users/users.json":{"get":{"responses":{"200":{"description":"Retrieve a single node definition","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/User"}}}}}}}},"/users/{userId}.json":{"get":{"parameters":[{"schema":{"type":"string"},"required":false,"name":"userId","in":"path"}],"responses":{"200":{"description":"Retrieve a single node definition","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}}}}}}} \ No newline at end of file +{"openapi":"3.0.0","info":{"version":"1.0.0","title":"Nodarium API"},"components":{"schemas":{"NodeInput":{"anyOf":[{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["seed"]},"value":{"type":"number"}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["boolean"]},"value":{"type":"boolean"}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["float"]},"element":{"type":"string","enum":["slider"]},"value":{"type":"number"},"min":{"type":"number"},"max":{"type":"number"},"step":{"type":"number"}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["integer"]},"element":{"type":"string","enum":["slider"]},"value":{"type":"number"},"min":{"type":"number"},"max":{"type":"number"}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["select"]},"options":{"type":"array","items":{"type":"string"}},"value":{"type":"number"}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["seed"]},"value":{"type":"number"}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["vec3"]},"value":{"type":"array","items":{"type":"number"}}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["geometry"]}},"required":["type"]},{"type":"object","properties":{"internal":{"type":"boolean"},"external":{"type":"boolean"},"setting":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"accepts":{"type":"array","items":{"type":"string"}},"hidden":{"type":"boolean"},"type":{"type":"string","enum":["path"]}},"required":["type"]}]},"NodeDefinition":{"type":"object","properties":{"id":{"type":"string","pattern":"^([a-z0-9-]+)\\/([a-z0-9-]+)\\/([a-z0-9-]+)$"},"inputs":{"type":"object","additionalProperties":{"$ref":"#/components/schemas/NodeInput"}},"outputs":{"type":"array","items":{"type":"string"}},"meta":{"type":"object","properties":{"description":{"type":"string"},"title":{"type":"string"}}}},"required":["id"]},"User":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string","minLength":1}},"required":["id","name"]}},"parameters":{}},"paths":{"/nodes/{user}.json":{"get":{"parameters":[{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"user","in":"path"}],"responses":{"200":{"description":"Retrieve a single node definition","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/NodeDefinition"}}}}}}}},"/nodes/{user}/{system}.json":{"get":{"parameters":[{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"user","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":false,"name":"system","in":"path"}],"responses":{"200":{"description":"Retrieve a single node definition","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/NodeDefinition"}}}}}}}},"/nodes/{user}/{system}/{nodeId}.json":{"get":{"parameters":[{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"user","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"system","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"nodeId","in":"path"}],"responses":{"200":{"description":"Retrieve a single node definition","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NodeDefinition"}}}}}}},"/nodes/{user}/{system}/{nodeId}.wasm":{"get":{"parameters":[{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"user","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"system","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"nodeId","in":"path"}],"responses":{"200":{"description":"Retrieve a single node","content":{"application/wasm":{"schema":{"nullable":true}}}}}}},"/nodes/{user}/{system}/{nodeId}@{hash}.json":{"get":{"parameters":[{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"user","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"system","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"nodeId","in":"path"}],"responses":{"200":{"description":"Create a single node","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NodeDefinition"}}}}}}},"/nodes/{user}/{system}/{nodeId}/versions.json":{"get":{"parameters":[{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"user","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"system","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"nodeId","in":"path"},{"schema":{"type":"string","minLength":3,"maxLength":20},"required":true,"name":"hash","in":"path"}],"responses":{"200":{"description":"Create a single node","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NodeDefinition"}}}}}}},"/nodes":{"post":{"responses":{"200":{"description":"Create a single node","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NodeDefinition"}}}}}}},"/users/users.json":{"get":{"responses":{"200":{"description":"Retrieve a single node definition","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/User"}}}}}}}},"/users/{userId}.json":{"get":{"parameters":[{"schema":{"type":"string"},"required":false,"name":"userId","in":"path"}],"responses":{"200":{"description":"Retrieve a single node definition","content":{"application/json":{"schema":{"$ref":"#/components/schemas/User"}}}}}}}}} \ No newline at end of file diff --git a/store/src/routes/node/node.controller.ts b/store/src/routes/node/node.controller.ts index 33eb20e..8ab1d3a 100644 --- a/store/src/routes/node/node.controller.ts +++ b/store/src/routes/node/node.controller.ts @@ -78,7 +78,7 @@ nodeRouter.openapi(getNodeCollectionRoute, async (c) => { const getNodeDefinitionRoute = createRoute({ method: "get", - path: "/{user}/{system}/{nodeId}{.+\\.json}", + path: "/{user}/{system}/{nodeId}.json", request: { params: ParamsSchema, }, @@ -111,7 +111,7 @@ nodeRouter.openapi(getNodeDefinitionRoute, async (c) => { const getNodeWasmRoute = createRoute({ method: "get", - path: "/{user}/{system}/{nodeId}{.+\\.wasm}", + path: "/{user}/{system}/{nodeId}.wasm", request: { params: ParamsSchema, }, @@ -140,6 +140,63 @@ nodeRouter.openapi(getNodeWasmRoute, async (c) => { return c.body(wasmContent); }); +const getNodeVersionRoute = createRoute({ + method: "get", + path: "/{user}/{system}/{nodeId}@{hash}.json", + request: { + params: ParamsSchema, + }, + responses: { + 200: { + content: { + "application/json": { + schema: NodeDefinitionSchema, + }, + }, + description: "Create a single node", + }, + }, +}); + +nodeRouter.openapi(getNodeVersionRoute, async (c) => { + const { user, system, nodeId } = c.req.valid("param"); + + const nodes = await service.getNodeVersions(user, system, nodeId); + + return c.json(nodes); +}); + +const getNodeVersionsRoute = createRoute({ + method: "get", + path: "/{user}/{system}/{nodeId}/versions.json", + request: { + params: z.object({ + user: SingleParam("user"), + system: SingleParam("system"), + nodeId: SingleParam("nodeId"), + hash: SingleParam("hash"), + }), + }, + responses: { + 200: { + content: { + "application/json": { + schema: NodeDefinitionSchema, + }, + }, + description: "Create a single node", + }, + }, +}); + +nodeRouter.openapi(getNodeVersionsRoute, async (c) => { + const { user, system, nodeId, hash } = c.req.valid("param"); + + const node = await service.getNodeVersion(user, system, nodeId, hash); + + return c.json(node); +}); + const createNodeRoute = createRoute({ method: "post", path: "/", @@ -165,8 +222,19 @@ const createNodeRoute = createRoute({ 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); + + try { + const node = await service.createNode(buffer, bytes); + return c.json(node); + } catch (error) { + if (error instanceof Error && "code" in error) { + switch (error.code) { + case "23505": + throw new HTTPException(409, { message: "node already exists" }); + } + } + } + throw new HTTPException(500); }); export { nodeRouter }; diff --git a/store/src/routes/node/node.schema.ts b/store/src/routes/node/node.schema.ts index 02ee6ea..4d8dac6 100644 --- a/store/src/routes/node/node.schema.ts +++ b/store/src/routes/node/node.schema.ts @@ -2,14 +2,14 @@ import { customType, foreignKey, index, - integer, json, pgTable, serial, + timestamp, varchar, } from "drizzle-orm/pg-core"; -import { relations } from "drizzle-orm/relations"; import { usersTable } from "../user/user.schema.ts"; +import { NodeDefinition } from "./validations/types.ts"; const bytea = customType<{ data: ArrayBuffer; @@ -23,12 +23,13 @@ const bytea = customType<{ 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(), - hash: varchar({ length: 8 }).notNull().unique(), - previous: varchar({ length: 8 }), + definition: json().notNull().$type(), + hash: varchar({ length: 16 }).notNull().unique(), + previous: varchar({ length: 16 }), }, (table) => [ foreignKey({ columns: [table.previous], diff --git a/store/src/routes/node/node.service.ts b/store/src/routes/node/node.service.ts index 41987b2..623c7da 100644 --- a/store/src/routes/node/node.service.ts +++ b/store/src/routes/node/node.service.ts @@ -1,7 +1,7 @@ import { db } from "../../db/db.ts"; import { nodeTable } from "./node.schema.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 { WorkerMessage } from "./worker/messages.ts"; @@ -15,7 +15,7 @@ export type CreateNodeDTO = { function getNodeHash(content: Uint8Array) { const hash = createHash("sha256"); hash.update(content); - return hash.digest("hex").slice(0, 8); + return hash.digest("hex").slice(0, 16); } function extractDefinition(content: ArrayBuffer): Promise { @@ -40,7 +40,6 @@ function extractDefinition(content: ArrayBuffer): Promise { res(e.data.result); break; case "error": - console.log("Worker error", e.data.error); rej(e.data.result); break; default: @@ -54,36 +53,46 @@ export async function createNode( wasmBuffer: ArrayBuffer, content: Uint8Array, ): Promise { - 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) +export async function getNodeDefinitionsByUser(userName: string) { + const nodes = await db.select({ definition: nodeTable.definition }).from( + nodeTable, + ) .where( and( eq(nodeTable.userId, userName), ), ); + + return nodes.map((n) => n.definition); } export async function getNodesBySystem( @@ -91,11 +100,14 @@ export async function getNodesBySystem( systemId: string, ): Promise { const nodes = await db - .select() + .selectDistinctOn( + [nodeTable.userId, nodeTable.systemId, nodeTable.nodeId], + { definition: nodeTable.definition }, + ) .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)) @@ -118,7 +130,9 @@ export async function getNodeWasmById( eq(nodeTable.systemId, systemId), eq(nodeTable.nodeId, nodeId), ), - ).limit(1).execute(); + ) + .orderBy(asc(nodeTable.createdAt)) + .limit(1); console.log("Time to load wasm", performance.now() - a); if (!node[0]) { @@ -141,7 +155,9 @@ export async function getNodeDefinitionById( eq(nodeTable.systemId, systemId), eq(nodeTable.nodeId, nodeId), ), - ).limit(1); + ) + .orderBy(asc(nodeTable.createdAt)) + .limit(1); if (!node[0]) { return; @@ -155,3 +171,55 @@ export async function getNodeDefinitionById( return definition.data; } + +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, + hash: nodeTable.hash, + }).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 Error("Node not found"); + } + + return nodes[0].definition; +}