feat: ditch triplit, move to postgresql + drizzle orm

This commit is contained in:
Zoe
2026-04-08 17:03:07 -05:00
parent c341c96798
commit e2e3ac6e86
121 changed files with 6680 additions and 4373 deletions
+196 -58
View File
@@ -1,75 +1,213 @@
import type { Entity } from "@triplit/client";
import type schema from "#triplit/schema";
import { nanoid } from "nanoid";
import { assert } from "~~/utils/assert";
import { attempt } from "~~/types/result";
import * as schema from '~~/drizzle/schema';
export type Agent = Readonly<Entity<typeof schema, 'agents'> & { topics: Readonly<Entity<typeof schema, 'topics'>>[] }>;
export type Topic = typeof schema.topics.$inferSelect;
export type Agent = typeof schema.agents.$inferSelect;
export type AgentWithTopics = Agent & { topics: Topic[] };
export const useAgents = () => {
const nuxtApp = useNuxtApp();
const triplit = useTriplitClient();
export const useAgents = async () => {
const agents = useState<AgentWithTopics[]>('agents_state', () => []);
const loaded = useState('agents_loaded', () => false);
// dont leaking between different users/requests
if (!nuxtApp._agentsState) {
nuxtApp._agentsState = {
list: ref<Agent[]>([]),
initPromise: null as Promise<void> | null,
};
}
const { refresh } = await useFetch<AgentWithTopics[]>('/api/agents', {
key: 'agents_request',
immediate: !loaded.value,
onRequest() {
loaded.value = true;
},
onResponse({ response }) {
if (response.ok) {
agents.value = response._data ?? [];
}
}
});
const state = nuxtApp._agentsState as {
list: Ref<Agent[]>;
initPromise: Promise<void> | null;
};
const init = (): Promise<void> => {
if (state.initPromise) return state.initPromise;
state.initPromise = (async () => {
const query = triplit.query('agents')
.Include('topics', (rel) => rel('topics').Order('createdAt', 'DESC'))
.Order('createdAt', 'ASC');
const { results } = await useQuery('agents', triplit, query)
watch(results, (newAgents) => {
if (newAgents && newAgents.length > 0) {
state.list.value = newAgents as unknown as Agent[];
}
}, { immediate: true, flush: 'sync' });
})();
return state.initPromise;
};
const getAgent = (id: MaybeRef<string>) => {
return computed(() => state.list.value.find((agent) => agent.id === toRef(id).value) || null);
}
const createAgent = async () => {
const triplit = useTriplitClient();
const createAgent = async (navigate: boolean = true) => {
const { user } = useAuth();
if (!user.value) throw new Error('No user');
if (!user.value) {
console.error('No user');
return null;
}
const id = nanoid();
await triplit.insert('agents', {
id,
name: 'New Agent',
const agentId = nanoid();
const agent = {
id: agentId,
userId: user.value.id,
name: 'New Agent',
systemPrompt: 'You are a helpful assistant.',
defaultModelId: null,
imageUrl: null,
createdAt: new Date().toISOString(),
} as AgentWithTopics;
const router = useRouter()
// @ts-ignore - stack depth
const res = await attempt($fetch('/api/agent', {
method: 'POST',
body: agent,
onRequest() {
agents.value = [...(agents.value), { ...agent, topics: [] as Topic[], createdAt: new Date() }];
if (navigate) {
router.push(`/agent/${agentId}`);
}
},
onRequestError() {
if (navigate) {
const route = useRoute();
if (route.params.id === agentId) {
router.push('/');
}
}
agents.value = agents.value.filter(a => a.id !== agentId);
},
onResponseError() {
if (navigate) {
const route = useRoute();
if (route.params.id === agentId) {
router.push('/');
}
}
agents.value = agents.value.filter(a => a.id !== agentId);
},
async onResponse() {
await refresh();
}
}));
if (!res.ok) return null;
return agent;
};
const patchAgentLocally = (id: string, updates: Partial<Agent>) => {
if (!agents.value) return null;
agents.value = agents.value.map(a =>
a.id === id ? { ...a, ...updates } : a
);
};
const patchTopicLocally = (id: string, updates: Partial<Topic>) => {
if (!agents.value) return null;
agents.value = agents.value.map(a =>
a.topics.find(t => t.id === id) ? {
...a, topics: a.topics.map(t =>
t.id === id ? { ...t, ...updates } : t
)
} : a
);
};
const updateAgent = async (id: string, updates: Partial<Agent>) => {
const agent = agents.value.find(a => a.id === id);
if (!agent) return;
await $fetch(`/api/agent/${id}`, {
method: 'PATCH',
body: updates,
onRequest() {
agents.value = agents.value.map(a =>
a.id === id ? { ...a, ...updates } : a
);
},
onRequestError() {
agents.value = agents.value.map(a =>
a.id === id ? agent : a
);
},
onResponseError() {
agents.value = agents.value.map(a =>
a.id === id ? agent : a
);
},
async onResponse() {
await refresh();
}
});
}
const getAgent = (id: MaybeRef<string>) => {
return computed(() => agents.value?.find((agent) => agent.id === unref(id)) || null);
}
const deleteAgent = async (id: string) => {
let agent = agents.value.find(a => a.id === id);
if (!agent) return;
await $fetch(`/api/agent/${id}`, {
method: 'DELETE',
onRequest() {
agents.value = agents.value.filter(a => a.id !== id);
},
onResponseError() {
agents.value = [...agents.value.filter(a => a.id !== id), agent];
},
async onResponse() {
await refresh();
}
});
}
// TODO: since topics are contained within each agent struct, all topic
// actions must be done through the agents composable
const createTopic = async (agentId: string) => {
const topicId = nanoid();
const topic = {
id: topicId,
name: 'New Topic',
agentId,
};
await $fetch(`/api/topic`, {
method: 'POST',
body: topic,
onRequest() {
agents.value = agents.value.map(a =>
a.id === agentId ? { ...a, topics: [{ ...topic, createdAt: new Date() }, ...a.topics] } : a
);
},
onResponseError() {
agents.value = agents.value.map(a =>
a.id === agentId ? { ...a, topics: a.topics.filter(t => t.id !== topicId) } : a
);
},
});
assert('flush' in triplit);
await triplit.flush();
return topic;
}
const deleteTopic = async (agentId: string, topicId: string) => {
const targetTopic = agents.value.flatMap(agent => agent.topics).find(topic => topic.id === topicId);
if (!targetTopic) return;
await $fetch(`/api/topic/${topicId}`, {
method: 'DELETE',
onRequest() {
agents.value = agents.value.map(a =>
a.id === agentId ? { ...a, topics: a.topics.filter(t => t.id !== topicId) } : a
);
},
onResponseError() {
agents.value = agents.value.map(a =>
a.id === agentId ? { ...a, topics: [...a.topics.filter(t => t.id !== topicId), targetTopic] } : a
);
},
});
}
return state.list.value.find((agent) => agent.id === id)!;
};
return {
init,
agents: state.list,
agents,
refresh,
createAgent,
createTopic,
getAgent,
createAgent
patchAgentLocally,
patchTopicLocally,
updateAgent,
deleteAgent,
deleteTopic
};
};
}