Vulcan schema: ```ts import { SchemaType } from './SchemaType.js' import { schema } from '@vlcn.io/typed-sql' import { nanoid } from 'nanoid'; import {ID_of} from '@vlcn.io/id'; export const SchemaName = 'schema.sql' // DB name does not need to be static by any means. See other example apps and the vite-start for dynamic db names. export const DBName = 'linear'; export function newID(): ID_of { return nanoid() as ID_of; } export const Schema = schema` CREATE TABLE IF NOT EXISTS issue ( "id" 'ID_of' PRIMARY KEY NOT NULL, "title" TEXT DEFAULT '' NOT NULL, "creator" TEXT DEFAULT '' NOT NULL, "priority" '"none" | "urgent" | "high" | "low" | "medium"' DEFAULT 'none' NOT NULL, "status" '"backlog" | "todo" | "in_progress" | "done" | "canceled"' DEFAULT 'todo' NOT NULL, "created" INTEGER NOT NULL, "modified" INTEGER NOT NULL, "kanbanorder" NOT NULL ); SELECT crsql_fract_as_ordered('issue', 'kanbanorder'); CREATE TABLE IF NOT EXISTS "description" ( "id" 'ID_of' PRIMARY KEY NOT NULL, "body" TEXT DEFAULT '' NOT NULL ); CREATE TABLE IF NOT EXISTS "comment" ( "id" 'ID_of' PRIMARY KEY NOT NULL, "body" TEXT DEFAULT '' NOT NULL, "creator" TEXT DEFAULT '' NOT NULL, "issueId" 'ID_of' NOT NULL, "created" INTEGER NOT NULL ); CREATE TABLE IF NOT EXISTS "filter_state" ( "id" '"singleton"' PRIMARY KEY NOT NULL, "orderBy" TEXT DEFAULT 'created' NOT NULL, "orderDirection" TEXT DEFAULT 'asc' NOT NULL, "status" 'String_of', "priority" 'String_of', "query" TEXT ); ` ``` LiveStore schema: ```ts import { DbSchema, makeSchema, sql } from '@livestore/livestore' import { Priority, PriorityType, Status, StatusType } from '../types/issue' const issue = DbSchema.table('issue', { id: DbSchema.text({ primaryKey: true }), title: DbSchema.text({ default: '' }), creator: DbSchema.text({ default: '' }), priority: DbSchema.text({ default: Priority.NONE }), status: DbSchema.text({ default: Status.TODO }), created: DbSchema.integer(), modified: DbSchema.integer(), kanbanorder: DbSchema.text({ nullable: false }), }) export interface FilterState { orderBy: string orderDirection: 'asc' | 'desc' status?: StatusType[] priority?: PriorityType[] query?: string } const description = DbSchema.table('description', { // TODO: id is also a foreign key to issue id: DbSchema.text({ primaryKey: true }), body: DbSchema.text({ default: '' }), }) const comment = DbSchema.table( 'comment', { id: DbSchema.text({ primaryKey: true }), body: DbSchema.text({ default: '' }), creator: DbSchema.text({ default: '' }), // TODO: issueId is a foreign key to issue issueId: DbSchema.text(), created: DbSchema.integer(), author: DbSchema.text({ nullable: false }), }, [ { name: 'issue_id', columns: ['issueId'], }, ], ) // TODO: move filter state into its own table? const appState = DbSchema.table('app_state', { key: DbSchema.text({ primaryKey: true }), value: DbSchema.text(), }) export type AppState = DbSchema.FromTable.RowDecoded export type Issue = DbSchema.FromTable.RowDecoded export type Description = DbSchema.FromTable.RowDecoded export type Comment = DbSchema.FromTable.RowDecoded export const schema = makeSchema({ // TODO get rid of `app_state` alias once fixed https://github.com/livestorejs/livestore/issues/25 tables: { issue, description, comment, app_state: appState }, actions: { createIssue: { statement: { sql: sql`INSERT INTO issue ("id", "title", "priority", "status", "created", "modified", "kanbanorder") VALUES ($id, $title, $priority, $status, $created, $modified, $kanbanorder)`, writeTables: ['issue'], }, }, createDescription: { statement: { sql: sql`INSERT INTO description ("id", "body") VALUES ($id, $body)`, writeTables: ['description'], }, }, createComment: { statement: { sql: sql`INSERT INTO comment ("id", "body", "issueId", "created", "author") VALUES ($id, $body, $issueId, $created, $author)`, writeTables: ['comment'], }, }, deleteIssue: { statement: { sql: sql`DELETE FROM issue WHERE id = $id`, writeTables: ['issue'], }, }, deleteDescriptin: { statement: { sql: sql`DELETE FROM description WHERE id = $id`, writeTables: ['description'], }, }, deleteComment: { statement: { sql: sql`DELETE FROM comment WHERE id = $id`, writeTables: ['comment'], }, }, deleteCommentsByIssueId: { statement: { sql: sql`DELETE FROM comment WHERE issueId = $issueId`, writeTables: ['comment'], }, }, updateIssue: { statement: { sql: sql`UPDATE issue SET title = $title, priority = $priority, status = $status, modified = $modified WHERE id = $id`, writeTables: ['issue'], }, }, updateIssueStatus: { statement: { sql: sql`UPDATE issue SET status = $status, modified = unixepoch() * 1000 WHERE id = $id`, writeTables: ['issue'], }, }, updateIssueKanbanOrder: { statement: { sql: sql`UPDATE issue SET kanbanorder = $kanbanorder, modified = unixepoch() * 1000 WHERE id = $id`, writeTables: ['issue'], }, }, updateIssueTitle: { statement: { sql: sql`UPDATE issue SET title = $title, modified = unixepoch() * 1000 WHERE id = $id`, writeTables: ['issue'], }, }, moveIssue: { statement: { sql: sql`UPDATE issue SET kanbanorder = $kanbanorder, status = $status, modified = unixepoch() * 1000 WHERE id = $id`, writeTables: ['issue'], }, }, updateIssuePriority: { statement: { sql: sql`UPDATE issue SET priority = $priority, modified = unixepoch() * 1000 WHERE id = $id`, writeTables: ['issue'], }, }, updateDescription: { statement: { sql: sql`UPDATE description SET body = $body WHERE id = $id`, writeTables: ['description'], }, }, upsertAppAtom: { statement: { sql: sql`INSERT INTO app_state (key, value) VALUES ($key, $value) ON CONFLICT (key) DO UPDATE SET value = $value`, writeTables: ['app_state'], }, }, }, }) ``` Vulcan Mutations (LiveStore includes these in Schema): ```ts import { TXAsync } from "@vlcn.io/xplat-api" import { Issue, Description, Comment, DecodedFilterState, encodeFilterState, StatusType } from "./SchemaType" import { ID_of } from "@vlcn.io/id" function colNames(obj: { [key: string]: unknown }) { return Object.keys(obj).map(key => `"${key}"`).join(', '); } function placeholders(obj: { [key: string]: unknown }) { return Object.keys(obj).map(() => '?').join(', '); } function values(obj: { [key: string]: unknown }) { return Object.values(obj); } function set(obj: { [key: string]: unknown }) { return Object.keys(obj).map(key => `"${key}" = ?`).join(', '); } export const mutations = { createIssue(tx: TXAsync, issue: Issue) { return tx.exec( `INSERT INTO issue (${colNames(issue)}) VALUES (${placeholders(issue)})`, values(issue) ); }, createDescription(tx: TXAsync, desc: Description) { return tx.exec( `INSERT INTO description (${colNames(desc)}) VALUES (${placeholders(desc)})`, values(desc) ); }, createIssueWithDescription(tx: TXAsync, issue: Issue, desc: Description) { return tx.exec( `INSERT INTO issue (${colNames(issue)}) VALUES (${placeholders(issue)})`, values(issue) ).then(() => { return tx.exec( `INSERT INTO description (${colNames(desc)}) VALUES (${placeholders(desc)})`, values(desc) ); }); }, createComment(tx: TXAsync, comment: Comment) { return tx.exec( `INSERT INTO comment (${colNames(comment)}) VALUES (${placeholders(comment)})`, values(comment) ); }, putFilterState(tx: TXAsync, filterState: DecodedFilterState) { const encoded = encodeFilterState(filterState) return tx.exec( `INSERT INTO filter_state (${colNames(encoded)}) VALUES (${placeholders(encoded)}) ON CONFLICT DO UPDATE SET ${set(encoded)}`, [...values(encoded), ...values(encoded)] ); }, updateIssue(tx: TXAsync, issue: Partial) { if (!issue.modified) { issue = { ...issue, modified: Date.now() } } return tx.exec( `UPDATE issue SET ${set(issue)} WHERE id = ?`, [...values(issue), issue.id] ); }, updateDescription(tx: TXAsync, desc: Description) { return tx.exec( `UPDATE description SET ${set(desc)} WHERE id = ?`, [...values(desc), desc.id] ); }, moveIssue(tx: TXAsync, id: ID_of, afterId: ID_of | null, newStatus: StatusType) { return tx.exec( `UPDATE issue_fractindex SET after_id = ?, status = ? WHERE id = ?`, [afterId, newStatus, id] ) }, async deleteIssue(tx: TXAsync, id: ID_of) { await tx.exec( `DELETE FROM issue WHERE id = ?`, [id] ); await tx.exec( `DELETE FROM description WHERE id = ?`, [id] ); await tx.exec( `DELETE FROM comment WHERE issueId = ?`, [id] ); } }; ```