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 {
|
model NodeMeta {
|
||||||
nodeId String
|
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
|
/// The app that created this metadata field
|
||||||
app App @relation(fields: [appId], references: [id])
|
app App @relation(fields: [appId], references: [id])
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
import { Node, App, PrismaClient } from "@prisma/client";
|
import { Node, App, PrismaClient } from "@prisma/client";
|
||||||
import {
|
import {
|
||||||
|
FetchedNode,
|
||||||
ICreateNodeRequest,
|
ICreateNodeRequest,
|
||||||
|
IFindManyNodesRequest,
|
||||||
IGetNodeRequest,
|
IGetNodeRequest,
|
||||||
IRegisterAppRequest,
|
IRegisterAppRequest,
|
||||||
|
IRemoveNodeRequest,
|
||||||
|
IUpdateNodeRequest,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
import { inspect } from "util";
|
||||||
|
|
||||||
export class Database {
|
export class Database {
|
||||||
private prisma: PrismaClient;
|
private prisma: PrismaClient;
|
||||||
|
@ -12,6 +17,33 @@ export class Database {
|
||||||
this.prisma = new PrismaClient();
|
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> {
|
public async registerApp(request: IRegisterAppRequest): Promise<App> {
|
||||||
// TODO: Some kind of authentication maybe?
|
// TODO: Some kind of authentication maybe?
|
||||||
|
|
||||||
|
@ -69,6 +101,45 @@ export class Database {
|
||||||
return createdNode;
|
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 {
|
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 {
|
export interface IRegisterAppRequest {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
@ -34,6 +45,17 @@ export interface ICreateNodeRequest {
|
||||||
metadata_keys?: Map<IMetaKey, string>;
|
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 {
|
export interface ICreateEdgeRequest {
|
||||||
label: string;
|
label: string;
|
||||||
from_node: 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 { 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";
|
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 {
|
interface TodoItem {
|
||||||
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
done: boolean;
|
completed: boolean;
|
||||||
committed: 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() {
|
export default function Todos() {
|
||||||
const [newTitle, setTitle] = createSignal("");
|
const ouais = useRouteData<typeof routeData>();
|
||||||
const [todos, setTodos] = createTodoStore<TodoItem[]>("todos", []);
|
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 }) => {
|
async ({ title }: { title: string }) => {
|
||||||
const metadata_keys = new Map();
|
const metadata_keys = new Map();
|
||||||
const keyOf = (appKey: string) => ({ appId: todosApp.id, appKey });
|
const setMeta = (appKey: string, value: any) =>
|
||||||
metadata_keys.set(keyOf("title"), title);
|
metadata_keys.set(
|
||||||
|
{ appId: todosApp.id, appKey },
|
||||||
|
JSON.stringify(value)
|
||||||
|
);
|
||||||
|
setMeta("title", title);
|
||||||
|
setMeta("completed", false);
|
||||||
|
|
||||||
const node = await db.createNode({
|
const node = await db.createNode({
|
||||||
label: `todo-${title}`,
|
label: `todo-${title}`,
|
||||||
|
@ -27,10 +72,11 @@ export default function Todos() {
|
||||||
console.log("Created node", node);
|
console.log("Created node", node);
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
}
|
},
|
||||||
|
{ invalidate: ["todos"] }
|
||||||
);
|
);
|
||||||
|
|
||||||
const addTodo = (e: SubmitEvent) => {
|
const addTodo = async (e: SubmitEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
@ -40,15 +86,89 @@ export default function Todos() {
|
||||||
batch(() => {
|
batch(() => {
|
||||||
setTodos(idx, {
|
setTodos(idx, {
|
||||||
title,
|
title,
|
||||||
done: false,
|
completed: false,
|
||||||
committed: false,
|
committed: false,
|
||||||
|
deleted: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
setTitle("");
|
setTitle("");
|
||||||
});
|
});
|
||||||
|
|
||||||
add({ title });
|
const node = await addTodoAction({ title });
|
||||||
console.log("Adding", adding);
|
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 (
|
return (
|
||||||
|
@ -60,28 +180,42 @@ export default function Todos() {
|
||||||
required
|
required
|
||||||
value={newTitle()}
|
value={newTitle()}
|
||||||
onInput={(e) => setTitle(e.currentTarget.value)}
|
onInput={(e) => setTitle(e.currentTarget.value)}
|
||||||
|
disabled={adding.pending}
|
||||||
/>
|
/>
|
||||||
<button>+</button>
|
<button>+</button>
|
||||||
</form>
|
</form>
|
||||||
Todos:
|
|
||||||
|
<p>Todos:</p>
|
||||||
<For each={todos}>
|
<For each={todos}>
|
||||||
{(todo, i) => (
|
{(todo, i) => {
|
||||||
<div>
|
const style: JSX.CSSProperties = {};
|
||||||
|
if (todo.deleted) style["text-decoration"] = "line-through";
|
||||||
|
return (
|
||||||
|
<div style={style}>
|
||||||
|
<p>{JSON.stringify(todo)}</p>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={todo.done}
|
checked={todo.completed}
|
||||||
onChange={(e) => setTodos(i(), "done", e.currentTarget.checked)}
|
onChange={(e) =>
|
||||||
|
updateNode(i(), "completed", e.currentTarget.checked)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={todo.title}
|
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()))}>
|
<button onClick={() => removeTodo(i())}>x</button>
|
||||||
x
|
|
||||||
</button>
|
{updating?.input?.id == todo.id &&
|
||||||
|
updating.pending &&
|
||||||
|
"saving..."}
|
||||||
</div>
|
</div>
|
||||||
)}
|
);
|
||||||
|
}}
|
||||||
</For>
|
</For>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -94,7 +228,7 @@ function createTodoStore<T extends object>(
|
||||||
// const localState = localStorage.getItem(name);
|
// const localState = localStorage.getItem(name);
|
||||||
|
|
||||||
const [state, setState] = createStore<T>(
|
const [state, setState] = createStore<T>(
|
||||||
init
|
init || []
|
||||||
// localState ? JSON.parse(localState) : 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