diff --git a/.changeset/tiny-adults-shave.md b/.changeset/tiny-adults-shave.md new file mode 100644 index 000000000..38bbfed71 --- /dev/null +++ b/.changeset/tiny-adults-shave.md @@ -0,0 +1,8 @@ +--- +"@tanstack/db-collections": patch +"@tanstack/db": patch +--- + +If a schema is passed, use that for the collection type. + +You now must either pass an explicit type or schema - passing both will conflict. diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 651b41597..9440df60c 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -43,6 +43,8 @@ jobs: uses: tanstack/config/.github/setup@main - name: Build Packages run: pnpm run build + - name: Publish Previews + run: pnpx pkg-pr-new publish --pnpm --compact './packages/*' --template './examples/*/*' - name: Compressed Size Action - DB Package uses: preactjs/compressed-size-action@v2 with: @@ -55,5 +57,3 @@ jobs: repo-token: "${{ secrets.GITHUB_TOKEN }}" pattern: "./packages/react-db/dist/**/*.{js,mjs}" comment-key: "react-db-package-size" - - name: Publish Previews - run: pnpx pkg-pr-new publish --pnpm --compact './packages/*' --template './examples/*/*' diff --git a/docs/overview.md b/docs/overview.md index 4711a90b7..ac5e7b9e4 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -163,14 +163,18 @@ You can also use: All collections optionally support a `schema`. -If provided, this should be a [Standard Schema](https://standardschema.dev) compatible schema instance, such as a [Zod](https://zod.dev) or [Effect](https://effect.website/docs/schema/introduction/) schema. +If provided, this must be a [Standard Schema](https://standardschema.dev) compatible schema instance, such as a [Zod](https://zod.dev) or [Effect](https://effect.website/docs/schema/introduction/) schema. + +The collection will use the schema for its type so if you provide a schema, you can't also pass in an explicit +type (e.g. `createCollection()`). + #### `QueryCollection` [TanStack Query](https://tanstack.com/query) fetches data using managed queries. Use `queryCollectionOptions` to fetch data into a collection using TanStack Query: ```ts -const todoCollection = createCollection(queryCollectionOptions({ +const todoCollection = createCollection(queryCollectionOptions({ queryKey: ['todoItems'], queryFn: async () => fetch('/api/todos'), getKey: (item) => item.id, @@ -190,7 +194,7 @@ Electric's main primitive for sync is a [Shape](https://electric-sql.com/docs/gu import { createCollection } from '@tanstack/react-db' import { electricCollectionOptions } from '@tanstack/db-collections' -export const todoCollection = createCollection(electricCollectionOptions({ +export const todoCollection = createCollection(electricCollectionOptions({ id: 'todos', shapeOptions: { url: 'https://example.com/v1/shape', @@ -215,7 +219,7 @@ When you create the collection, sync starts automatically. Electric shapes allow you to filter data using where clauses: ```ts -export const myPendingTodos = createCollection(electricCollectionOptions({ +export const myPendingTodos = createCollection(electricCollectionOptions({ id: 'todos', shapeOptions: { url: 'https://example.com/v1/shape', diff --git a/examples/react/todo/src/App.tsx b/examples/react/todo/src/App.tsx index 9c1a85b1c..f84527ae8 100644 --- a/examples/react/todo/src/App.tsx +++ b/examples/react/todo/src/App.tsx @@ -6,9 +6,9 @@ import { } from "@tanstack/db-collections" // import { DevTools } from "./DevTools" import { QueryClient } from "@tanstack/query-core" -import { updateConfigSchema, updateTodoSchema } from "./db/validation" +import { selectConfigSchema, selectTodoSchema } from "./db/validation" import type { Collection } from "@tanstack/react-db" -import type { UpdateConfig, UpdateTodo } from "./db/validation" +import type { SelectConfig, SelectTodo } from "./db/validation" import type { FormEvent } from "react" // API helper for todos and config @@ -17,21 +17,21 @@ const API_BASE_URL = `http://localhost:3001/api` const api = { // Todo API methods todos: { - getAll: async (): Promise> => { + getAll: async (): Promise> => { const response = await fetch(`${API_BASE_URL}/todos`) if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`) return response.json() }, - getById: async (id: number): Promise => { + getById: async (id: number): Promise => { const response = await fetch(`${API_BASE_URL}/todos/${id}`) if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`) return response.json() }, create: async ( - todo: Partial - ): Promise<{ todo: UpdateTodo; txid: number }> => { + todo: Partial + ): Promise<{ todo: SelectTodo; txid: number }> => { const response = await fetch(`${API_BASE_URL}/todos`, { method: `POST`, headers: { "Content-Type": `application/json` }, @@ -43,8 +43,8 @@ const api = { }, update: async ( id: unknown, - changes: Partial - ): Promise<{ todo: UpdateTodo; txid: number }> => { + changes: Partial + ): Promise<{ todo: SelectTodo; txid: number }> => { const response = await fetch(`${API_BASE_URL}/todos/${id}`, { method: `PUT`, headers: { "Content-Type": `application/json` }, @@ -68,21 +68,21 @@ const api = { // Config API methods config: { - getAll: async (): Promise> => { + getAll: async (): Promise> => { const response = await fetch(`${API_BASE_URL}/config`) if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`) return response.json() }, - getById: async (id: number): Promise => { + getById: async (id: number): Promise => { const response = await fetch(`${API_BASE_URL}/config/${id}`) if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`) return response.json() }, create: async ( - config: Partial - ): Promise<{ config: UpdateConfig; txid: number }> => { + config: Partial + ): Promise<{ config: SelectConfig; txid: number }> => { const response = await fetch(`${API_BASE_URL}/config`, { method: `POST`, headers: { "Content-Type": `application/json` }, @@ -94,8 +94,8 @@ const api = { }, update: async ( id: number, - changes: Partial - ): Promise<{ config: UpdateConfig; txid: number }> => { + changes: Partial + ): Promise<{ config: SelectConfig; txid: number }> => { const response = await fetch(`${API_BASE_URL}/config/${id}`, { method: `PUT`, headers: { "Content-Type": `application/json` }, @@ -130,12 +130,12 @@ const collectionsCache = new Map() // Function to create the appropriate todo collection based on type const createTodoCollection = (type: CollectionType) => { if (collectionsCache.has(`todo`)) { - return collectionsCache.get(`todo`) as Collection + return collectionsCache.get(`todo`) as Collection } else { - let newCollection: Collection + let newCollection: Collection if (type === CollectionType.Electric) { newCollection = createCollection( - electricCollectionOptions({ + electricCollectionOptions({ id: `todos`, shapeOptions: { url: `http://localhost:3003/v1/shape`, @@ -147,10 +147,15 @@ const createTodoCollection = (type: CollectionType) => { timestamptz: (date: string) => new Date(date), }, }, - getKey: (item) => item.id!, - schema: updateTodoSchema, + getKey: (item) => item.id, + schema: selectTodoSchema, onInsert: async ({ transaction }) => { - const modified = transaction.mutations[0].modified + const { + id: _id, + created_at: _f, + updated_at: _ff, + ...modified + } = transaction.mutations[0].modified const response = await api.todos.create(modified) return { txid: String(response.txid) } @@ -165,7 +170,7 @@ const createTodoCollection = (type: CollectionType) => { }) ) - return { txid: String(txids[0].txid) } + return { txid: String(txids[0]!.txid) } }, onDelete: async ({ transaction }) => { const txids = await Promise.all( @@ -177,7 +182,7 @@ const createTodoCollection = (type: CollectionType) => { }) ) - return { txid: String(txids[0].txid) } + return { txid: String(txids[0]!.txid) } }, }) ) @@ -190,22 +195,23 @@ const createTodoCollection = (type: CollectionType) => { refetchInterval: 3000, queryFn: async () => { const todos = await api.todos.getAll() - // Turn date strings into Dates if needed + // Turn date strings into Dates return todos.map((todo) => ({ ...todo, - created_at: todo.created_at - ? new Date(todo.created_at) - : undefined, - updated_at: todo.updated_at - ? new Date(todo.updated_at) - : undefined, + created_at: new Date(todo.created_at), + updated_at: new Date(todo.updated_at), })) }, - getKey: (item: UpdateTodo) => item.id!, - schema: updateTodoSchema, + getKey: (item: SelectTodo) => item.id, + schema: selectTodoSchema, queryClient, onInsert: async ({ transaction }) => { - const modified = transaction.mutations[0].modified + const { + id: _id, + created_at: _crea, + updated_at: _up, + ...modified + } = transaction.mutations[0].modified return await api.todos.create(modified) }, onUpdate: async ({ transaction }) => { @@ -235,9 +241,9 @@ const createTodoCollection = (type: CollectionType) => { // Function to create the appropriate config collection based on type const createConfigCollection = (type: CollectionType) => { if (collectionsCache.has(`config`)) { - return collectionsCache.get(`config`) + return collectionsCache.get(`config`) as Collection } else { - let newCollection: Collection + let newCollection: Collection if (type === CollectionType.Electric) { newCollection = createCollection( electricCollectionOptions({ @@ -254,8 +260,8 @@ const createConfigCollection = (type: CollectionType) => { }, }, }, - getKey: (item: UpdateConfig) => item.id!, - schema: updateConfigSchema, + getKey: (item: SelectConfig) => item.id, + schema: selectConfigSchema, onInsert: async ({ transaction }) => { const modified = transaction.mutations[0].modified const response = await api.config.create(modified) @@ -286,19 +292,15 @@ const createConfigCollection = (type: CollectionType) => { refetchInterval: 3000, queryFn: async () => { const configs = await api.config.getAll() - // Turn date strings into Dates if needed + // Turn date strings into Dates return configs.map((config) => ({ ...config, - created_at: config.created_at - ? new Date(config.created_at) - : undefined, - updated_at: config.updated_at - ? new Date(config.updated_at) - : undefined, + created_at: new Date(config.created_at), + updated_at: new Date(config.updated_at), })) }, - getKey: (item: UpdateConfig) => item.id, - schema: updateConfigSchema, + getKey: (item: SelectConfig) => item.id, + schema: selectConfigSchema, queryClient, onInsert: async ({ transaction }) => { const modified = transaction.mutations[0].modified @@ -348,12 +350,12 @@ export default function App() { q .from({ todoCollection: todoCollection }) .orderBy(`@created_at`) - .select(`@id`, `@created_at`, `@text`, `@completed`) + .select(`@*`) ) const { data: configData } = useLiveQuery((q) => q - .from({ configCollection: configCollection as Collection }) + .from({ configCollection: configCollection }) .select(`@id`, `@key`, `@value`) ) @@ -371,7 +373,7 @@ export default function App() { const getConfigValue = (key: string): string => { for (const config of configData) { if (config.key === key) { - return config.value! + return config.value } } return `` @@ -391,8 +393,11 @@ export default function App() { // If the config doesn't exist yet, create it configCollection.insert({ + id: Math.random(), key, value, + created_at: new Date(), + updated_at: new Date(), }) } @@ -457,17 +462,12 @@ export default function App() { text: newTodo, completed: false, id: Math.round(Math.random() * 1000000), + created_at: new Date(), + updated_at: new Date(), }) setNewTodo(``) } - const toggleTodo = (todo: UpdateTodo) => { - console.log(todoCollection) - todoCollection.update(todo.id, (draft) => { - draft.completed = !draft.completed - }) - } - const activeTodos = todos.filter((todo) => !todo.completed) const completedTodos = todos.filter((todo) => todo.completed) @@ -597,7 +597,11 @@ export default function App() { toggleTodo(todo)} + onChange={() => + todoCollection.update(todo.id, (draft) => { + draft.completed = !draft.completed + }) + } className="absolute left-[12px] top-0 bottom-0 my-auto h-[40px] w-[40px] cursor-pointer" />