This commit is contained in:
parent
d82993f2a7
commit
97774d5852
6 changed files with 278 additions and 39 deletions
|
@ -0,0 +1,18 @@
|
|||
-- RedefineTables
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_NodeMeta" (
|
||||
"nodeId" TEXT NOT NULL,
|
||||
"appId" TEXT NOT NULL,
|
||||
"appKey" TEXT NOT NULL,
|
||||
"value" BLOB NOT NULL,
|
||||
|
||||
PRIMARY KEY ("nodeId", "appId", "appKey"),
|
||||
CONSTRAINT "NodeMeta_nodeId_fkey" FOREIGN KEY ("nodeId") REFERENCES "Node" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
CONSTRAINT "NodeMeta_appId_fkey" FOREIGN KEY ("appId") REFERENCES "App" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_NodeMeta" ("appId", "appKey", "nodeId", "value") SELECT "appId", "appKey", "nodeId", "value" FROM "NodeMeta";
|
||||
DROP TABLE "NodeMeta";
|
||||
ALTER TABLE "new_NodeMeta" RENAME TO "NodeMeta";
|
||||
CREATE INDEX "NodeMeta_appId_appKey_idx" ON "NodeMeta"("appId", "appKey");
|
||||
PRAGMA foreign_key_check;
|
||||
PRAGMA foreign_keys=ON;
|
|
@ -46,7 +46,7 @@ model Edge {
|
|||
|
||||
model NodeMeta {
|
||||
nodeId String
|
||||
node Node @relation(fields: [nodeId], references: [id])
|
||||
node Node @relation(fields: [nodeId], references: [id], onDelete: Cascade)
|
||||
|
||||
/// The app that created this metadata field
|
||||
app App @relation(fields: [appId], references: [id])
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
import { Node, App, PrismaClient } from "@prisma/client";
|
||||
import {
|
||||
FetchedNode,
|
||||
ICreateNodeRequest,
|
||||
IFindManyNodesRequest,
|
||||
IGetNodeRequest,
|
||||
IRegisterAppRequest,
|
||||
IRemoveNodeRequest,
|
||||
IUpdateNodeRequest,
|
||||
} from "./types";
|
||||
import { inspect } from "util";
|
||||
|
||||
export class Database {
|
||||
private prisma: PrismaClient;
|
||||
|
@ -12,6 +17,33 @@ export class Database {
|
|||
this.prisma = new PrismaClient();
|
||||
}
|
||||
|
||||
/**
|
||||
* Query for a node. If you are querying by ID, use {@link getNode} instead.
|
||||
*/
|
||||
public async findManyNodes(
|
||||
request: IFindManyNodesRequest
|
||||
): Promise<FetchedNode[] | null> {
|
||||
const query = {
|
||||
take: request.take,
|
||||
select: { id: true, label: true, metadata: true },
|
||||
where: { metadata: { some: { OR: [] } } },
|
||||
};
|
||||
|
||||
if (request.select_keys) {
|
||||
for (const key of request.select_keys) {
|
||||
query.where.metadata.some.OR.push({
|
||||
appKey: key.appKey,
|
||||
appId: key.appId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log("query", inspect(query, false, 10));
|
||||
|
||||
const nodes = await this.prisma.node.findMany(query);
|
||||
return nodes;
|
||||
}
|
||||
|
||||
public async registerApp(request: IRegisterAppRequest): Promise<App> {
|
||||
// TODO: Some kind of authentication maybe?
|
||||
|
||||
|
@ -69,6 +101,45 @@ export class Database {
|
|||
return createdNode;
|
||||
});
|
||||
}
|
||||
|
||||
public async updateNode(request: IUpdateNodeRequest): Promise<Node | null> {
|
||||
const node = await this.prisma.$transaction(async (client) => {
|
||||
await client.node.update({ where: { id: request.id }, data: {} });
|
||||
|
||||
if (request.metadata_keys) {
|
||||
for (const [key, value] of request.metadata_keys.entries()) {
|
||||
await client.nodeMeta.update({
|
||||
where: {
|
||||
nodeId_appId_appKey: {
|
||||
nodeId: request.id,
|
||||
appId: key.appId,
|
||||
appKey: key.appKey,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
value: Buffer.from(value),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return await this.prisma.node.findFirst({
|
||||
where: { id: request.id },
|
||||
select: { id: true, label: true, metadata: true },
|
||||
});
|
||||
});
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
public async removeNode(request: IRemoveNodeRequest): Promise<void> {
|
||||
// TODO: Check conditions? Delete edge nodes?
|
||||
|
||||
await this.prisma.$transaction(async (client) => {
|
||||
await this.prisma.node.delete({ where: { id: request.id } });
|
||||
});
|
||||
console.log("Removed", request);
|
||||
}
|
||||
}
|
||||
|
||||
export class NodeWrapper {
|
||||
|
|
|
@ -1,3 +1,14 @@
|
|||
import { Node, NodeMeta } from "@prisma/client";
|
||||
|
||||
export interface FetchedNode extends Node {
|
||||
metadata: NodeMeta[];
|
||||
}
|
||||
|
||||
export interface IFindManyNodesRequest {
|
||||
select_keys?: IMetaKey[];
|
||||
take?: number;
|
||||
}
|
||||
|
||||
export interface IRegisterAppRequest {
|
||||
name: string;
|
||||
}
|
||||
|
@ -34,6 +45,17 @@ export interface ICreateNodeRequest {
|
|||
metadata_keys?: Map<IMetaKey, string>;
|
||||
}
|
||||
|
||||
export interface IUpdateNodeRequest {
|
||||
id: string;
|
||||
|
||||
/** A set of metadata keys to values */
|
||||
metadata_keys?: Map<IMetaKey, string>;
|
||||
}
|
||||
|
||||
export interface IRemoveNodeRequest {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface ICreateEdgeRequest {
|
||||
label: string;
|
||||
from_node: string;
|
||||
|
|
|
@ -1,24 +1,69 @@
|
|||
import { For, batch, createEffect, createSignal } from "solid-js";
|
||||
import { For, batch, createEffect, createSignal, JSX } from "solid-js";
|
||||
import { SetStoreFunction, Store, createStore } from "solid-js/store";
|
||||
import { createServerAction$ } from "solid-start/server";
|
||||
import { useRouteData } from "solid-start";
|
||||
import { createServerAction$, createServerData$ } from "solid-start/server";
|
||||
|
||||
import { db, todosApp } from "~/db";
|
||||
|
||||
const retrieveMeta = (node, key) => {
|
||||
const meta = node.metadata.find(
|
||||
(meta) => meta.appId == todosApp.id && meta.appKey == key
|
||||
);
|
||||
if (!meta) return null;
|
||||
const metaValue = meta.value.toString();
|
||||
console.log("s", key, metaValue);
|
||||
return JSON.parse(metaValue);
|
||||
};
|
||||
|
||||
const nodeToTodoItem = (node) => ({
|
||||
id: node.id,
|
||||
title: retrieveMeta(node, "title"),
|
||||
completed: retrieveMeta(node, "completed"),
|
||||
committed: true,
|
||||
deleted: false,
|
||||
});
|
||||
|
||||
interface TodoItem {
|
||||
id: string;
|
||||
title: string;
|
||||
done: boolean;
|
||||
completed: boolean;
|
||||
committed: boolean;
|
||||
deleted: boolean;
|
||||
}
|
||||
|
||||
export function routeData() {
|
||||
return createServerData$(async () => {
|
||||
const nodes = await db.findManyNodes({
|
||||
select_keys: [
|
||||
{ appId: todosApp.id, appKey: "title" },
|
||||
{ appId: todosApp.id, appKey: "completed" },
|
||||
],
|
||||
});
|
||||
if (!nodes) return null;
|
||||
|
||||
const initTodos = nodes.map(nodeToTodoItem);
|
||||
return { initTodos };
|
||||
});
|
||||
}
|
||||
|
||||
export default function Todos() {
|
||||
const [newTitle, setTitle] = createSignal("");
|
||||
const [todos, setTodos] = createTodoStore<TodoItem[]>("todos", []);
|
||||
const ouais = useRouteData<typeof routeData>();
|
||||
if (!ouais()) return <>Loading...</>;
|
||||
const { initTodos } = ouais();
|
||||
|
||||
const [adding, add] = createServerAction$(
|
||||
const [newTitle, setTitle] = createSignal("");
|
||||
const [todos, setTodos] = createTodoStore<TodoItem[]>("todos", initTodos);
|
||||
|
||||
const [adding, addTodoAction] = createServerAction$(
|
||||
async ({ title }: { title: string }) => {
|
||||
const metadata_keys = new Map();
|
||||
const keyOf = (appKey: string) => ({ appId: todosApp.id, appKey });
|
||||
metadata_keys.set(keyOf("title"), title);
|
||||
const setMeta = (appKey: string, value: any) =>
|
||||
metadata_keys.set(
|
||||
{ appId: todosApp.id, appKey },
|
||||
JSON.stringify(value)
|
||||
);
|
||||
setMeta("title", title);
|
||||
setMeta("completed", false);
|
||||
|
||||
const node = await db.createNode({
|
||||
label: `todo-${title}`,
|
||||
|
@ -27,10 +72,11 @@ export default function Todos() {
|
|||
console.log("Created node", node);
|
||||
|
||||
return node;
|
||||
}
|
||||
},
|
||||
{ invalidate: ["todos"] }
|
||||
);
|
||||
|
||||
const addTodo = (e: SubmitEvent) => {
|
||||
const addTodo = async (e: SubmitEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
|
@ -40,15 +86,89 @@ export default function Todos() {
|
|||
batch(() => {
|
||||
setTodos(idx, {
|
||||
title,
|
||||
done: false,
|
||||
completed: false,
|
||||
committed: false,
|
||||
deleted: false,
|
||||
});
|
||||
|
||||
setTitle("");
|
||||
});
|
||||
|
||||
add({ title });
|
||||
console.log("Adding", adding);
|
||||
const node = await addTodoAction({ title });
|
||||
batch(() => {
|
||||
setTodos(idx, "id", node.id);
|
||||
setTodos(idx, "committed", true);
|
||||
});
|
||||
};
|
||||
|
||||
const [updating, updateTodoAction] = createServerAction$(
|
||||
async ({
|
||||
id,
|
||||
title,
|
||||
completed,
|
||||
}: {
|
||||
id: string;
|
||||
title: string;
|
||||
completed: boolean;
|
||||
}) => {
|
||||
const metadata_keys = new Map();
|
||||
const setMeta = (appKey: string, value: any) =>
|
||||
metadata_keys.set(
|
||||
{ appId: todosApp.id, appKey },
|
||||
JSON.stringify(value)
|
||||
);
|
||||
setMeta("title", title);
|
||||
setMeta("completed", completed);
|
||||
|
||||
const node = await db.updateNode({ id, metadata_keys });
|
||||
console.log("Updated node", node);
|
||||
|
||||
return nodeToTodoItem(node);
|
||||
}
|
||||
);
|
||||
|
||||
const updateNode = async (
|
||||
idx: number,
|
||||
prop: keyof TodoItem,
|
||||
newValue: any
|
||||
) => {
|
||||
batch(() => {
|
||||
setTodos(idx, "committed", false);
|
||||
setTodos(idx, prop, newValue);
|
||||
});
|
||||
|
||||
const todoItem = todos[idx];
|
||||
const node = await updateTodoAction({
|
||||
id: todoItem.id,
|
||||
title: todoItem.title,
|
||||
completed: todoItem.completed,
|
||||
});
|
||||
if (node)
|
||||
batch(() => {
|
||||
setTodos(idx, "completed", node.completed);
|
||||
setTodos(idx, "committed", true);
|
||||
});
|
||||
};
|
||||
|
||||
const [removing, removeTodoAction] = createServerAction$(
|
||||
async ({ id }: { id: string }) => {
|
||||
await db.removeNode({ id });
|
||||
},
|
||||
{ invalidate: ["todos"] }
|
||||
);
|
||||
|
||||
const removeTodo = async (idx: number) => {
|
||||
const todo = todos[idx];
|
||||
|
||||
batch(() => {
|
||||
setTodos(idx, "deleted", true);
|
||||
setTodos(idx, "committed", false);
|
||||
});
|
||||
|
||||
await removeTodoAction({ id: todo.id });
|
||||
console.log("Removed", todo);
|
||||
// TODO: Is there a race condition here with the index?
|
||||
setTodos((t) => removeIndex(t, idx));
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -60,28 +180,42 @@ export default function Todos() {
|
|||
required
|
||||
value={newTitle()}
|
||||
onInput={(e) => setTitle(e.currentTarget.value)}
|
||||
disabled={adding.pending}
|
||||
/>
|
||||
<button>+</button>
|
||||
</form>
|
||||
Todos:
|
||||
|
||||
<p>Todos:</p>
|
||||
<For each={todos}>
|
||||
{(todo, i) => (
|
||||
<div>
|
||||
{(todo, i) => {
|
||||
const style: JSX.CSSProperties = {};
|
||||
if (todo.deleted) style["text-decoration"] = "line-through";
|
||||
return (
|
||||
<div style={style}>
|
||||
<p>{JSON.stringify(todo)}</p>
|
||||
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={todo.done}
|
||||
onChange={(e) => setTodos(i(), "done", e.currentTarget.checked)}
|
||||
checked={todo.completed}
|
||||
onChange={(e) =>
|
||||
updateNode(i(), "completed", e.currentTarget.checked)
|
||||
}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={todo.title}
|
||||
onChange={(e) => setTodos(i(), "title", e.currentTarget.value)}
|
||||
onChange={(e) =>
|
||||
updateNode(i(), "title", e.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
<button onClick={() => setTodos((t) => removeIndex(t, i()))}>
|
||||
x
|
||||
</button>
|
||||
<button onClick={() => removeTodo(i())}>x</button>
|
||||
|
||||
{updating?.input?.id == todo.id &&
|
||||
updating.pending &&
|
||||
"saving..."}
|
||||
</div>
|
||||
)}
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</>
|
||||
);
|
||||
|
@ -94,7 +228,7 @@ function createTodoStore<T extends object>(
|
|||
// const localState = localStorage.getItem(name);
|
||||
|
||||
const [state, setState] = createStore<T>(
|
||||
init
|
||||
init || []
|
||||
// localState ? JSON.parse(localState) : init
|
||||
);
|
||||
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
import solid from "solid-start/vite";
|
||||
import { defineConfig } from "vite";
|
||||
export default defineConfig({
|
||||
plugins: [solid()],
|
||||
ssr: { external: ["@prisma/client"] },
|
||||
});
|
Loading…
Reference in a new issue