Files
veridian/triplit/schema.ts
T

491 lines
18 KiB
TypeScript

import { Schema as S } from '@triplit/client';
import { authSchema } from './auth-schema';
import { Providers, SupportedModalities } from '../app/types/model';
const systemAssistantRecord = S.Record({
enabled: S.Boolean({ default: true }),
prompt: S.String({ nullable: true }),
modelId: S.String({ nullable: true }),
})
export const schema = S.Collections({
...authSchema,
settings: {
schema: S.Schema({
id: S.Id(),
userId: S.String(),
systemAssistants: S.Record({
rename: systemAssistantRecord
}),
appearance: S.Record({
colorScheme: S.String({ nullable: true, default: 'system' }),
accent: S.String({ nullable: true }),
neutral: S.String({ nullable: true }),
hinting: S.Number({ nullable: true }),
fontSize: S.String({ nullable: true }),
}, { nullable: true }),
}),
permissions: {
authenticated: {
read: {
filter: [['userId', '=', '$token.sub']],
},
insert: {
filter: [['userId', '=', '$token.sub']],
},
update: {
filter: [['userId', '=', '$token.sub']],
},
postUpdate: {
filter: [['userId', '=', '$prev.userId']],
},
delete: {
filter: [['userId', '=', '$token.sub']],
},
},
}
},
agents: {
schema: S.Schema({
id: S.Id(),
userId: S.String(),
name: S.String(),
systemPrompt: S.String({ nullable: true }),
imageUrl: S.String({ nullable: true }),
defaultModelId: S.String({ nullable: true }),
createdAt: S.Date({ default: S.Default.now() }),
}),
relationships: {
topics: S.RelationMany('topics', {
where: [['agentId', '=', '$1.id']],
}),
model: S.RelationOne('models', {
where: [['id', '=', '$defaultModelId']],
}),
},
permissions: {
authenticated: {
read: {
filter: [['userId', '=', '$token.sub']],
},
insert: {
filter: [['userId', '=', '$token.sub']],
},
update: {
filter: [['userId', '=', '$token.sub']],
},
postUpdate: {
filter: [['userId', '=', '$prev.userId']],
},
delete: {
filter: [['userId', '=', '$token.sub']],
},
},
},
},
topics: {
schema: S.Schema({
id: S.Id(),
userId: S.String(),
agentId: S.String(),
renaming: S.Boolean({ default: false, nullable: true }),
name: S.String(),
createdAt: S.Date({ default: S.Default.now() }),
}),
relationships: {
agent: S.RelationOne('agents', {
where: [['id', '=', '$agentId']],
}),
messages: S.RelationMany('messages', {
where: [['topicId', '=', '$id']],
}),
generations: S.RelationMany('generations', {
where: [['topicId', '=', '$id']],
}),
},
permissions: {
authenticated: {
read: {
filter: [['userId', '=', '$token.sub']],
},
insert: {
filter: [['userId', '=', '$token.sub']],
},
update: {
filter: [['userId', '=', '$token.sub']],
},
postUpdate: {
filter: [['userId', '=', '$prev.userId']],
},
delete: {
filter: [['userId', '=', '$token.sub']],
},
},
},
},
message_parts: {
schema: S.Schema({
id: S.Id(),
userId: S.String(),
topicId: S.String(),
messageId: S.String(),
toolCallId: S.String({ nullable: true }),
type: S.String({ enum: ['reasoning', 'text', 'tool-call', 'file'] }),
content: S.String(),
providerOptions: S.Json({ nullable: true }),
finished: S.Boolean({ default: false }),
createdAt: S.Date({ default: S.Default.now() }),
lastUpdatedAt: S.Date({ default: S.Default.now() }),
}),
relationships: {
message: S.RelationOne('messages', {
where: [['id', '=', '$messageId']],
}),
toolCall: S.RelationOne('tool_calls', {
where: [['id', '=', '$toolCallId']],
}),
},
permissions: {
authenticated: {
read: {
filter: [['userId', '=', '$token.sub']],
},
insert: {
// blank means it can only be inserted by the service token (ie the server)
},
update: {
filter: [['userId', '=', '$token.sub']],
},
postUpdate: {
filter: [
['userId', '=', '$prev.userId'],
['topicId', '=', '$prev.topicId'],
['messageId', '=', '$prev.messageId'],
['toolCallId', '=', '$prev.toolCallId'],
],
},
delete: {
filter: [['userId', '=', '$token.sub']],
},
},
},
},
tool_calls: {
schema: S.Schema({
id: S.Id(),
userId: S.String(),
toolName: S.String(),
status: S.String({
enum: ['pending', 'completed', 'failed', 'cancelled'],
}),
input: S.Record({ type: S.String({ enum: ['text', 'json'] }), value: S.String() }, { nullable: true }),
output: S.Record({ type: S.String({ enum: ['text', 'json'] }), value: S.String() }, { nullable: true }),
error: S.Record({ type: S.String({ enum: ['text', 'json'] }), value: S.String() }, { nullable: true }),
createdAt: S.Date({ default: S.Default.now() }),
}),
relationships: {
message_part: S.RelationOne('message_parts', {
where: [['id', '=', '$messagePartId']],
}),
},
permissions: {
authenticated: {
read: { filter: [['userId', '=', '$token.sub']] },
insert: {},
update: {},
delete: { filter: [['userId', '=', '$token.sub']] },
},
},
},
messages: {
schema: S.Schema({
id: S.Id(),
topicId: S.String({ nullable: true }),
focusedIndex: S.Number({ nullable: true }),
userId: S.String(),
deleted: S.Boolean({ default: false, nullable: true }),
role: S.String({ enum: ['user', 'assistant'] }),
content: S.String(),
generationId: S.String({ nullable: true, default: null }),
// a message has a "children" relationship that points to these messages.
// For each child, if its topicId matches the parent topicId, it will be a
// regeneration, but if the topicId is different, it will be a branching
// message, basically making a sub-thread of the parent topic.
parentMessageId: S.String({ nullable: true, default: null }),
createdAt: S.Date({ default: S.Default.now() }),
}),
relationships: {
topic: S.RelationOne('topics', {
where: [['id', '=', '$topicId']],
}),
attachments: S.RelationMany('attachments', {
where: [['messageId', '=', '$id']],
}),
parts: S.RelationMany('message_parts', {
where: [['messageId', '=', '$id']],
}),
generation: S.RelationOne('generations', {
where: [['id', '=', '$generationId']],
}),
parentMessage: S.RelationOne('messages', {
where: [['id', '=', '$parentMessageId']],
}),
children: S.RelationMany('messages', {
where: [['parentMessageId', '=', '$id']],
}),
},
permissions: {
authenticated: {
read: {
filter: [['userId', '=', '$token.sub']],
},
insert: {
filter: [
['userId', '=', '$token.sub'],
['topic.userId', '=', '$token.sub'],
],
},
update: {
filter: [['userId', '=', '$token.sub']],
},
postUpdate: {
filter: [
['userId', '=', '$prev.userId'],
['topicId', '=', '$prev.topicId'],
['generationId', '=', '$prev.generationId'],
['parentMessageId', '=', '$prev.parentMessageId'],
],
},
delete: {
filter: [['userId', '=', '$token.sub']],
},
},
},
},
// we split up the files and attachments table because we want to be able to
// store files on their own so they can be used in multiple messages at once
files: {
schema: S.Schema({
id: S.Id(),
userId: S.String(),
name: S.String(),
mimeType: S.String(),
url: S.String(),
createdAt: S.Date({ default: S.Default.now() }),
}),
permissions: {
authenticated: {
read: {
filter: [['userId', '=', '$token.sub']],
},
insert: {
filter: [['userId', '=', '$token.sub']],
},
update: {
filter: [['userId', '=', '$token.sub']],
},
postUpdate: {
filter: [['userId', '=', '$prev.userId']],
},
delete: {
filter: [['userId', '=', '$token.sub']],
},
},
}
},
attachments: {
schema: S.Schema({
id: S.Id(),
userId: S.String(),
topicId: S.String(),
messageId: S.String(),
fileId: S.String(),
// Denormalized for zero-subquery rendering. However, this can be
// dangerous. It is absolutely PARAMOUNT that whenver a file is
// modified, it's children attachments are modified as well.
name: S.String(),
mimeType: S.String(),
url: S.String(),
createdAt: S.Date({ default: S.Default.now() }),
}),
relationships: {
topic: S.RelationOne('topics', {
where: [['id', '=', '$topicId']],
}),
message: S.RelationOne('messages', {
where: [['id', '=', '$messageId']],
}),
},
permissions: {
authenticated: {
read: {
filter: [['userId', '=', '$token.sub']],
},
insert: {
filter: [['userId', '=', '$token.sub']],
},
update: {
filter: [['userId', '=', '$token.sub']],
},
postUpdate: {
filter: [['userId', '=', '$prev.userId']],
},
delete: {
filter: [['userId', '=', '$token.sub']],
},
}
}
},
generations: {
schema: S.Schema({
id: S.Id(),
userId: S.String(),
status: S.String({
enum: ['pending', 'cancelled', 'completed', 'failed'],
}),
topicId: S.String(),
messageId: S.String(),
modelId: S.String(),
tokens: S.Record({
input: S.Number({ nullable: true }),
cache: S.Record({
read: S.Number({ nullable: true }),
write: S.Number({ nullable: true }),
}, { nullable: true }),
output: S.Number({ nullable: true }),
thinking: S.Number({ nullable: true }),
ttft: S.Number({ nullable: true }),
tps: S.Number({ nullable: true }),
}, { nullable: true }),
error: S.String({ nullable: true, default: null }),
}),
relationships: {
message: S.RelationOne('messages', {
where: [['id', '=', '$messageId']],
}),
topic: S.RelationOne('topics', {
where: [['id', '=', '$topicId']],
}),
},
permissions: {
authenticated: {
read: {
filter: [['userId', '=', '$token.sub']],
},
insert: {},
update: {
filter: [['userId', '=', '$token.sub']],
},
postUpdate: {
filter: [
['userId', '=', '$prev.userId'],
['topicId', '=', '$prev.topicId'],
['messageId', '=', '$prev.messageId'],
],
},
delete: {
filter: [['userId', '=', '$token.sub']],
},
},
},
},
providers: {
schema: S.Schema({
id: S.Id(),
userId: S.String(),
type: S.String({ enum: Providers }),
name: S.String(),
enabled: S.Boolean({ default: true }),
config: S.Json(),
createdAt: S.Date({ default: S.Default.now() }),
updatedAt: S.Date({ default: S.Default.now() }),
}),
relationships: {
models: S.RelationMany('models', {
where: [['providerId', '=', '$id']],
}),
},
permissions: {
authenticated: {
read: {
filter: [['userId', '=', '$token.sub']],
},
insert: {
filter: [['userId', '=', '$token.sub']],
},
update: {
filter: [['userId', '=', '$token.sub']],
},
postUpdate: {
filter: [['userId', '=', '$prev.userId']],
},
delete: {
filter: [['userId', '=', '$token.sub']],
},
},
}
},
models: {
schema: S.Schema({
id: S.Id(),
userId: S.String(),
externalId: S.String(),// e.g. "x-ai/grok-4.1-fast"
providerId: S.String(),
name: S.String(),
cost: S.Record({
prompt: S.String({ nullable: true }),
completion: S.String({ nullable: true }),
request: S.String({ nullable: true }),
image: S.String({ nullable: true }),
imageTokens: S.String({ nullable: true }),
imageOutput: S.String({ nullable: true }),
audio: S.String({ nullable: true }),
audioOutput: S.String({ nullable: true }),
inputAudioCache: S.String({ nullable: true }),
webSearch: S.String({ nullable: true }),
internalReasoning: S.String({ nullable: true }),
inputCacheRead: S.String({ nullable: true }),
inputCacheWrite: S.String({ nullable: true }),
discount: S.String({ nullable: true }),
}),
attributes: S.Record({
inputModalities: S.Set(S.String({ enum: SupportedModalities })),
outputModalities: S.Set(S.String({ enum: SupportedModalities })),
capabilities: S.Set(S.String({ enum: ['reasoning', 'tools'] })),
contextWindow: S.Number({ nullable: true }),
supported_parameters: S.Set(S.String(), { nullable: true }),
}), // From models.dev (context window, etc)
isCustom: S.Boolean({ default: false }),
enabled: S.Boolean({ default: true }),
releasedAt: S.Date({ nullable: true }),
}),
relationships: {
provider: S.RelationOne('providers', {
where: [['id', '=', '$providerId']],
}),
},
permissions: {
authenticated: {
read: {
filter: [['userId', '=', '$token.sub'], ['provider.userId', '=', '$token.sub']],
},
insert: {
filter: [['userId', '=', '$token.sub'], ['provider.userId', '=', '$token.sub']],
},
update: {
filter: [['userId', '=', '$token.sub'], ['provider.userId', '=', '$token.sub']],
},
postUpdate: {
filter: [
['userId', '=', '$prev.userId'],
['providerId', '=', '$prev.providerId'],
]
},
delete: {
filter: [['userId', '=', '$token.sub'], ['provider.userId', '=', '$token.sub']],
},
},
},
}
});