feat: initial backend store prototype
Some checks failed
Deploy to GitHub Pages / build_site (push) Failing after 13s

This commit is contained in:
2024-12-17 18:15:21 +01:00
parent 5421349c79
commit 9d4d67f086
32 changed files with 2012 additions and 76 deletions

15
store/src/db/db.ts Normal file
View File

@ -0,0 +1,15 @@
import { drizzle } from "drizzle-orm/node-postgres";
import pg from "pg";
import * as schema from "./schema.ts";
// Use pg driver.
const { Pool } = pg;
// Instantiate Drizzle client with pg driver and schema.
export const db = drizzle({
client: new Pool({
connectionString: Deno.env.get("DATABASE_URL"),
}),
schema,
});

2
store/src/db/schema.ts Normal file
View File

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

25
store/src/main.ts Normal file
View File

@ -0,0 +1,25 @@
import { OpenAPIHono } from "@hono/zod-openapi";
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";
await createUser("max");
const app = new OpenAPIHono();
app.use("/v1/*", cors());
app.use(logger());
app.route("v1", router);
app.doc("/doc", {
openapi: "3.0.0",
info: {
version: "1.0.0",
title: "My API",
},
});
app.get("/ui", swaggerUI({ url: "/doc" }));
Deno.serve(app.fetch);

View File

@ -0,0 +1,81 @@
import { z } from "@hono/zod-openapi";
const DefaultOptionsSchema = z.object({
internal: z.boolean().optional(),
external: z.boolean().optional(),
setting: z.string().optional(),
label: z.string().optional(),
description: z.string().optional(),
accepts: z.array(z.string()).optional(),
hidden: z.boolean().optional(),
});
export const NodeInputFloatSchema = z.object({
...DefaultOptionsSchema.shape,
type: z.literal("float"),
element: z.literal("slider").optional(),
value: z.number().optional(),
min: z.number().optional(),
max: z.number().optional(),
step: z.number().optional(),
});
export const NodeInputIntegerSchema = z.object({
...DefaultOptionsSchema.shape,
type: z.literal("integer"),
element: z.literal("slider").optional(),
value: z.number().optional(),
min: z.number().optional(),
max: z.number().optional(),
});
export const NodeInputBooleanSchema = z.object({
...DefaultOptionsSchema.shape,
type: z.literal("boolean"),
value: z.boolean().optional(),
});
export const NodeInputSelectSchema = z.object({
...DefaultOptionsSchema.shape,
type: z.literal("select"),
options: z.array(z.string()).optional(),
value: z.number().optional(),
});
export const NodeInputSeedSchema = z.object({
...DefaultOptionsSchema.shape,
type: z.literal("seed"),
value: z.number().optional(),
});
export const NodeInputVec3Schema = z.object({
...DefaultOptionsSchema.shape,
type: z.literal("vec3"),
value: z.array(z.number()).optional(),
});
export const NodeInputGeometrySchema = z.object({
...DefaultOptionsSchema.shape,
type: z.literal("geometry"),
});
export const NodeInputPathSchema = z.object({
...DefaultOptionsSchema.shape,
type: z.literal("path"),
});
export const NodeInputSchema = z
.union([
NodeInputSeedSchema,
NodeInputBooleanSchema,
NodeInputFloatSchema,
NodeInputIntegerSchema,
NodeInputSelectSchema,
NodeInputSeedSchema,
NodeInputVec3Schema,
NodeInputGeometrySchema,
NodeInputPathSchema,
])
.openapi("NodeInput");
export type NodeInput = z.infer<typeof NodeInputSchema>;

View File

@ -0,0 +1,133 @@
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
import { NodeDefinitionSchema } from "./types.ts";
import * as service from "./node.service.ts";
import { bodyLimit } from "hono/body-limit";
const nodeRouter = new OpenAPIHono();
const SingleParam = (name: string) =>
z
.string()
.min(3)
.max(20)
.refine(
(value) => /^[a-z_-]+$/i.test(value),
"Name should contain only alphabets",
)
.openapi({ param: { name, in: "path" } });
const ParamsSchema = z.object({
userId: SingleParam("userId"),
nodeSystemId: SingleParam("nodeSystemId"),
nodeId: SingleParam("nodeId"),
});
const getNodeCollectionRoute = createRoute({
method: "get",
path: "/{userId}/{nodeSystemId}.json",
request: {
params: z.object({
userId: SingleParam("userId"),
nodeSystemId: SingleParam("nodeSystemId").optional(),
}),
},
responses: {
200: {
content: {
"application/json": {
schema: z.array(NodeDefinitionSchema),
},
},
description: "Retrieve a single node definition",
},
},
});
nodeRouter.openapi(getNodeCollectionRoute, async (c) => {
const { userId } = c.req.valid("param");
const nodeSystemId = c.req.param("nodeSystemId.json").replace(/\.json$/, "");
const nodes = await service.getNodesBySystem(userId, nodeSystemId);
return c.json(nodes);
});
const getNodeDefinitionRoute = createRoute({
method: "get",
path: "/{userId}/{nodeSystemId}/{nodeId}.json",
request: {
params: ParamsSchema,
},
responses: {
200: {
content: {
"application/json": {
schema: NodeDefinitionSchema,
},
},
description: "Retrieve a single node definition",
},
},
});
nodeRouter.openapi(getNodeDefinitionRoute, (c) => {
return c.json({
id: "",
});
});
const getNodeWasmRoute = createRoute({
method: "get",
path: "/{userId}/{nodeSystemId}/{nodeId}.wasm",
request: {
params: ParamsSchema,
},
responses: {
200: {
content: {
"application/wasm": {
schema: z.any(),
},
},
description: "Retrieve a single node",
},
},
});
nodeRouter.openapi(getNodeWasmRoute, (c) => {
return c.json({
id: "",
});
});
const createNodeRoute = createRoute({
method: "post",
path: "/",
responses: {
200: {
content: {
"application/json": {
schema: NodeDefinitionSchema,
},
},
description: "Create a single node",
},
},
middleware: [
bodyLimit({
maxSize: 50 * 1024, // 50kb
onError: (c) => {
return c.text("overflow :(", 413);
},
}),
],
});
nodeRouter.openapi(createNodeRoute, async (c) => {
const buffer = await c.req.arrayBuffer();
const bytes = await (await c.req.blob()).bytes();
const node = await service.createNode(buffer, bytes);
return c.json(node);
});
export { nodeRouter };

View File

@ -0,0 +1,40 @@
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(),
previous: integer(),
});
export const nodeRelations = relations(nodeTable, ({ one }) => ({
userId: one(usersTable, {
fields: [nodeTable.userId],
references: [usersTable.id],
}),
previous: one(nodeTable, {
fields: [nodeTable.previous],
references: [nodeTable.id],
}),
}));

View File

@ -0,0 +1,87 @@
import { db } from "../../db/db.ts";
import { nodeTable } from "./node.schema.ts";
import { NodeDefinition, NodeDefinitionSchema } from "./types.ts";
import { and, eq } from "drizzle-orm";
export type CreateNodeDTO = {
id: string;
system: string;
user: string;
content: ArrayBuffer;
};
function extractDefinition(content: ArrayBuffer): Promise<NodeDefinition> {
const worker = new Worker(new URL("./node.worker.ts", import.meta.url).href, {
type: "module",
});
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) {
console.log(e.data);
switch (e.data.action) {
case "result":
res(e.data.result);
break;
case "error":
rej(e.data.result);
break;
default:
rej(new Error("Unknown worker response"));
}
};
});
}
export async function createNode(
wasmBuffer: ArrayBuffer,
content: Uint8Array,
): Promise<NodeDefinition> {
try {
const def = await extractDefinition(wasmBuffer);
const [userId, systemId, nodeId] = def.id.split("/");
const node: typeof nodeTable.$inferInsert = {
userId,
systemId,
nodeId,
definition: def,
content: content,
};
await db.insert(nodeTable).values(node);
console.log("New user created!");
// await db.insert(users).values({ name: "Andrew" });
return def;
} catch (error) {
console.log({ error });
throw error;
}
}
export async function getNodesByUser(userName: string) {}
export async function getNodesBySystem(
username: string,
systemId: string,
): Promise<NodeDefinition[]> {
const nodes = await db
.select()
.from(nodeTable)
.where(
and(eq(nodeTable.systemId, systemId), eq(nodeTable.userId, username)),
);
const definitions = nodes
.map((node) => NodeDefinitionSchema.safeParse(node.definition))
.filter((v) => v.success)
.map((v) => v.data);
return definitions;
}
export async function getNodeById(dto: CreateNodeDTO) {}

View File

@ -0,0 +1,12 @@
import { expect } from "jsr:@std/expect";
import { router } from "../router.ts";
Deno.test("simple test", async () => {
const res = await router.request("/max/plants/test.json");
const json = await res.text();
console.log({ json });
expect(true).toEqual(true);
expect(json).toEqual({ hello: "world" });
});

View File

@ -0,0 +1,30 @@
/// <reference lib="webworker" />
import { NodeDefinitionSchema } from "./types.ts";
import { createWasmWrapper } from "./utils.ts";
function extractDefinition(wasmCode: ArrayBuffer) {
const wasm = createWasmWrapper(wasmCode);
const definition = wasm.get_definition();
const p = NodeDefinitionSchema.safeParse(definition);
if (!p.success) {
self.postMessage({ action: "error", error: p.error });
return;
}
self.postMessage({ action: "result", result: p.data });
}
self.onmessage = (e) => {
switch (e.data.action) {
case "extract-definition":
extractDefinition(e.data.content);
self.close();
break;
default:
throw new Error("Unknwon action", e.data.action);
}
};

View File

@ -0,0 +1,69 @@
import { z } from "zod";
import { NodeInputSchema } from "./inputs.ts";
export type NodeId = `${string}/${string}/${string}`;
export const NodeSchema = z.object({
id: z.number(),
type: z.string(),
props: z.record(z.union([z.number(), z.array(z.number())])).optional(),
meta: z
.object({
title: z.string().optional(),
lastModified: z.string().optional(),
})
.optional(),
position: z.tuple([z.number(), z.number()]),
});
export type Node = z.infer<typeof NodeSchema>;
const partPattern = /[a-z-_]{3,32}/;
const idSchema = z
.string()
.regex(
new RegExp(
`^(${partPattern.source})/(${partPattern.source})/(${partPattern.source})$`,
),
"Invalid id format",
);
export const NodeDefinitionSchema = z
.object({
id: idSchema,
inputs: z.record(NodeInputSchema).optional(),
outputs: z.array(z.string()).optional(),
meta: z
.object({
description: z.string().optional(),
title: z.string().optional(),
})
.optional(),
})
.openapi("NodeDefinition");
export type NodeDefinition = z.infer<typeof NodeDefinitionSchema>;
export type Socket = {
node: Node;
index: number | string;
position: [number, number];
};
export type Edge = [Node, number, Node, string];
export const GraphSchema = z.object({
id: z.number().optional(),
meta: z
.object({
title: z.string().optional(),
lastModified: z.string().optional(),
})
.optional(),
settings: z.record(z.any()).optional(),
nodes: z.array(NodeSchema),
edges: z.array(z.tuple([z.number(), z.number(), z.number(), z.string()])),
});
export type Graph = z.infer<typeof GraphSchema> & { nodes: Node[] };

View File

@ -0,0 +1,255 @@
// @ts-nocheck: Nocheck
import { NodeDefinition } from "./types.ts";
const cachedTextDecoder = new TextDecoder("utf-8", {
ignoreBOM: true,
fatal: true,
});
const cachedTextEncoder = new TextEncoder();
const encodeString = typeof cachedTextEncoder.encodeInto === "function"
? function (arg: string, view: Uint8Array) {
return cachedTextEncoder.encodeInto(arg, view);
}
: function (arg: string, view: Uint8Array) {
const buf = cachedTextEncoder.encode(arg);
view.set(buf);
return {
read: arg.length,
written: buf.length,
};
};
function createWrapper() {
let wasm: WebAssembly.Exports & { memory: { buffer: Iterable<number> } };
let cachedUint8Memory0: Uint8Array | null = null;
let cachedInt32Memory0: Int32Array | null = null;
let cachedUint32Memory0: Uint32Array | null = null;
const heap = new Array(128).fill(undefined);
heap.push(undefined, null, true, false);
let heap_next = heap.length;
function getUint8Memory0() {
if (cachedUint8Memory0 === null || cachedUint8Memory0.byteLength === 0) {
cachedUint8Memory0 = new Uint8Array(wasm.memory.buffer);
}
return cachedUint8Memory0;
}
function getInt32Memory0() {
if (cachedInt32Memory0 === null || cachedInt32Memory0.byteLength === 0) {
cachedInt32Memory0 = new Int32Array(wasm.memory.buffer);
}
return cachedInt32Memory0;
}
function getUint32Memory0() {
if (cachedUint32Memory0 === null || cachedUint32Memory0.byteLength === 0) {
cachedUint32Memory0 = new Uint32Array(wasm.memory.buffer);
}
return cachedUint32Memory0;
}
function getStringFromWasm0(ptr: number, len: number) {
return cachedTextDecoder.decode(getUint8Memory0().subarray(ptr, ptr + len));
}
function getObject(idx: number) {
return heap[idx];
}
function addHeapObject(obj: unknown) {
if (heap_next === heap.length) heap.push(heap.length + 1);
const idx = heap_next;
heap_next = heap[idx];
heap[idx] = obj;
return idx;
}
let WASM_VECTOR_LEN = 0;
function passArray32ToWasm0(
arg: ArrayLike<number>,
malloc: (arg0: number, arg1: number) => number,
) {
const ptr = malloc(arg.length * 4, 4) >>> 0;
getUint32Memory0().set(arg, ptr / 4);
WASM_VECTOR_LEN = arg.length;
return ptr;
}
function getArrayI32FromWasm0(ptr: number, len: number) {
ptr = ptr >>> 0;
return getInt32Memory0().subarray(ptr / 4, ptr / 4 + len);
}
function dropObject(idx: number) {
if (idx < 132) return;
heap[idx] = heap_next;
heap_next = idx;
}
function takeObject(idx: number) {
const ret = getObject(idx);
dropObject(idx);
return ret;
}
function __wbindgen_string_new(arg0: number, arg1: number) {
const ret = getStringFromWasm0(arg0, arg1);
return addHeapObject(ret);
}
// Additional methods and their internal helpers can also be refactored in a similar manner.
function get_definition() {
let deferred1_0: number;
let deferred1_1: number;
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
wasm.get_definition(retptr);
const r0 = getInt32Memory0()[retptr / 4 + 0];
const r1 = getInt32Memory0()[retptr / 4 + 1];
deferred1_0 = r0;
deferred1_1 = r1;
const rawDefinition = getStringFromWasm0(r0, r1);
return JSON.parse(rawDefinition) as NodeDefinition;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
wasm.__wbindgen_free(deferred1_0, deferred1_1, 1);
}
}
function execute(args: Int32Array) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passArray32ToWasm0(args, wasm.__wbindgen_malloc);
const len0 = WASM_VECTOR_LEN;
wasm.execute(retptr, ptr0, len0);
const r0 = getInt32Memory0()[retptr / 4 + 0];
const r1 = getInt32Memory0()[retptr / 4 + 1];
const v2 = getArrayI32FromWasm0(r0, r1).slice();
wasm.__wbindgen_free(r0, r1 * 4, 4);
return v2;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
function passStringToWasm0(
arg: string,
malloc: (arg0: number, arg1: number) => number,
realloc:
| ((arg0: number, arg1: number, arg2: number, arg3: number) => number)
| undefined,
) {
if (realloc === undefined) {
const buf = cachedTextEncoder.encode(arg);
const ptr = malloc(buf.length, 1) >>> 0;
getUint8Memory0()
.subarray(ptr, ptr + buf.length)
.set(buf);
WASM_VECTOR_LEN = buf.length;
return ptr;
}
let len = arg.length;
let ptr = malloc(len, 1) >>> 0;
const mem = getUint8Memory0();
let offset = 0;
for (; offset < len; offset++) {
const code = arg.charCodeAt(offset);
if (code > 0x7f) break;
mem[ptr + offset] = code;
}
if (offset !== len) {
if (offset !== 0) {
arg = arg.slice(offset);
}
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
const view = getUint8Memory0().subarray(ptr + offset, ptr + len);
const ret = encodeString(arg, view);
offset += ret.written;
ptr = realloc(ptr, len, offset, 1) >>> 0;
}
WASM_VECTOR_LEN = offset;
return ptr;
}
function __wbg_new_abda76e883ba8a5f() {
const ret = new Error();
return addHeapObject(ret);
}
function __wbg_stack_658279fe44541cf6(arg0: number, arg1: number) {
const ret = getObject(arg1).stack;
const ptr1 = passStringToWasm0(
ret,
wasm.__wbindgen_malloc,
wasm.__wbindgen_realloc,
);
const len1 = WASM_VECTOR_LEN;
getInt32Memory0()[arg0 / 4 + 1] = len1;
getInt32Memory0()[arg0 / 4 + 0] = ptr1;
}
function __wbg_error_f851667af71bcfc6(arg0: number, arg1: number) {
let deferred0_0;
let deferred0_1;
try {
deferred0_0 = arg0;
deferred0_1 = arg1;
console.error(getStringFromWasm0(arg0, arg1));
} finally {
wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
}
}
function __wbindgen_object_drop_ref(arg0: number) {
takeObject(arg0);
}
function __wbg_log_5bb5f88f245d7762(arg0: number) {
console.log(getObject(arg0));
}
function __wbindgen_throw(arg0: number, arg1: number) {
throw new Error(getStringFromWasm0(arg0, arg1));
}
return {
setInstance(instance: WebAssembly.Instance) {
wasm = instance.exports;
},
exports: {
// Expose other methods that interact with the wasm instance
execute,
get_definition,
},
__wbindgen_string_new,
__wbindgen_object_drop_ref,
__wbg_new_abda76e883ba8a5f,
__wbg_error_f851667af71bcfc6,
__wbg_stack_658279fe44541cf6,
__wbg_log_5bb5f88f245d7762,
__wbindgen_throw,
};
}
export function createWasmWrapper(wasmBuffer: ArrayBuffer) {
const wrapper = createWrapper();
const module = new WebAssembly.Module(wasmBuffer);
const instance = new WebAssembly.Instance(module, {
["./index_bg.js"]: wrapper,
});
wrapper.setInstance(instance);
return wrapper.exports;
}

View File

@ -0,0 +1,10 @@
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("nodes", userRouter);
export { router };

View File

@ -0,0 +1,54 @@
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";
import { UserSchema, usersTable } from "./user.schema.ts";
import { db } from "../../db/db.ts";
import { findUserByName } from "./user.service.ts";
const userRouter = new OpenAPIHono();
const getAllUsersRoute = createRoute({
method: "get",
path: "/users.json",
responses: {
200: {
content: {
"application/json": {
schema: z.array(UserSchema),
},
},
description: "Retrieve a single node definition",
},
},
});
userRouter.openapi(getAllUsersRoute, async (c) => {
const users = await db.select().from(usersTable);
return c.json(users);
});
const getSingleUserRoute = createRoute({
method: "get",
path: "/{userId}.json",
request: {
params: z.object({ userId: z.string().optional() }),
},
responses: {
200: {
content: {
"application/json": {
schema: UserSchema,
},
},
description: "Retrieve a single node definition",
},
},
});
userRouter.openapi(getSingleUserRoute, async (c) => {
const userId = c.req.param("userId.json");
const user = await findUserByName(userId.replace(/\.json$/, ""));
return c.json(user);
});
export { userRouter };

View File

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

View File

@ -0,0 +1,28 @@
import { eq } from "drizzle-orm";
import { db } from "../../db/db.ts";
import { usersTable } from "./user.schema.ts";
import * as uuid from "jsr:@std/uuid";
export async function createUser(userName: string) {
const user = await db
.select()
.from(usersTable)
.where(eq(usersTable.name, userName));
if (user.length) {
return;
}
return await db
.insert(usersTable)
.values({ id: uuid.v1.generate(), name: userName });
}
export async function findUserByName(userName: string) {
const users = await db
.select()
.from(usersTable)
.where(eq(usersTable.name, userName));
return users[0];
}