Skip to content

Commit f6abe9b

Browse files
authored
If a schema is passed, use that for the collection type (#186)
1 parent e15ecd4 commit f6abe9b

File tree

16 files changed

+702
-238
lines changed

16 files changed

+702
-238
lines changed

.changeset/tiny-adults-shave.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@tanstack/db-collections": patch
3+
"@tanstack/db": patch
4+
---
5+
6+
If a schema is passed, use that for the collection type.
7+
8+
You now must either pass an explicit type or schema - passing both will conflict.

.github/workflows/pr.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ jobs:
4343
uses: tanstack/config/.github/setup@main
4444
- name: Build Packages
4545
run: pnpm run build
46+
- name: Publish Previews
47+
run: pnpx pkg-pr-new publish --pnpm --compact './packages/*' --template './examples/*/*'
4648
- name: Compressed Size Action - DB Package
4749
uses: preactjs/compressed-size-action@v2
4850
with:
@@ -55,5 +57,3 @@ jobs:
5557
repo-token: "${{ secrets.GITHUB_TOKEN }}"
5658
pattern: "./packages/react-db/dist/**/*.{js,mjs}"
5759
comment-key: "react-db-package-size"
58-
- name: Publish Previews
59-
run: pnpx pkg-pr-new publish --pnpm --compact './packages/*' --template './examples/*/*'

docs/overview.md

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,14 +163,18 @@ You can also use:
163163

164164
All collections optionally support a `schema`.
165165

166-
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.
166+
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.
167+
168+
The collection will use the schema for its type so if you provide a schema, you can't also pass in an explicit
169+
type (e.g. `createCollection<Todo>()`).
170+
167171

168172
#### `QueryCollection`
169173

170174
[TanStack Query](https://tanstack.com/query) fetches data using managed queries. Use `queryCollectionOptions` to fetch data into a collection using TanStack Query:
171175

172176
```ts
173-
const todoCollection = createCollection<Todo>(queryCollectionOptions({
177+
const todoCollection = createCollection(queryCollectionOptions({
174178
queryKey: ['todoItems'],
175179
queryFn: async () => fetch('/api/todos'),
176180
getKey: (item) => item.id,
@@ -190,7 +194,7 @@ Electric's main primitive for sync is a [Shape](https://electric-sql.com/docs/gu
190194
import { createCollection } from '@tanstack/react-db'
191195
import { electricCollectionOptions } from '@tanstack/db-collections'
192196

193-
export const todoCollection = createCollection<Todo>(electricCollectionOptions({
197+
export const todoCollection = createCollection(electricCollectionOptions({
194198
id: 'todos',
195199
shapeOptions: {
196200
url: 'https://example.com/v1/shape',
@@ -215,7 +219,7 @@ When you create the collection, sync starts automatically.
215219
Electric shapes allow you to filter data using where clauses:
216220

217221
```ts
218-
export const myPendingTodos = createCollection<Todo>(electricCollectionOptions({
222+
export const myPendingTodos = createCollection(electricCollectionOptions({
219223
id: 'todos',
220224
shapeOptions: {
221225
url: 'https://example.com/v1/shape',

examples/react/todo/src/App.tsx

Lines changed: 60 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import {
66
} from "@tanstack/db-collections"
77
// import { DevTools } from "./DevTools"
88
import { QueryClient } from "@tanstack/query-core"
9-
import { updateConfigSchema, updateTodoSchema } from "./db/validation"
9+
import { selectConfigSchema, selectTodoSchema } from "./db/validation"
1010
import type { Collection } from "@tanstack/react-db"
11-
import type { UpdateConfig, UpdateTodo } from "./db/validation"
11+
import type { SelectConfig, SelectTodo } from "./db/validation"
1212
import type { FormEvent } from "react"
1313

1414
// API helper for todos and config
@@ -17,21 +17,21 @@ const API_BASE_URL = `http://localhost:3001/api`
1717
const api = {
1818
// Todo API methods
1919
todos: {
20-
getAll: async (): Promise<Array<UpdateTodo>> => {
20+
getAll: async (): Promise<Array<SelectTodo>> => {
2121
const response = await fetch(`${API_BASE_URL}/todos`)
2222
if (!response.ok)
2323
throw new Error(`HTTP error! Status: ${response.status}`)
2424
return response.json()
2525
},
26-
getById: async (id: number): Promise<UpdateTodo> => {
26+
getById: async (id: number): Promise<SelectTodo> => {
2727
const response = await fetch(`${API_BASE_URL}/todos/${id}`)
2828
if (!response.ok)
2929
throw new Error(`HTTP error! Status: ${response.status}`)
3030
return response.json()
3131
},
3232
create: async (
33-
todo: Partial<UpdateTodo>
34-
): Promise<{ todo: UpdateTodo; txid: number }> => {
33+
todo: Partial<SelectTodo>
34+
): Promise<{ todo: SelectTodo; txid: number }> => {
3535
const response = await fetch(`${API_BASE_URL}/todos`, {
3636
method: `POST`,
3737
headers: { "Content-Type": `application/json` },
@@ -43,8 +43,8 @@ const api = {
4343
},
4444
update: async (
4545
id: unknown,
46-
changes: Partial<UpdateTodo>
47-
): Promise<{ todo: UpdateTodo; txid: number }> => {
46+
changes: Partial<SelectTodo>
47+
): Promise<{ todo: SelectTodo; txid: number }> => {
4848
const response = await fetch(`${API_BASE_URL}/todos/${id}`, {
4949
method: `PUT`,
5050
headers: { "Content-Type": `application/json` },
@@ -68,21 +68,21 @@ const api = {
6868

6969
// Config API methods
7070
config: {
71-
getAll: async (): Promise<Array<UpdateConfig>> => {
71+
getAll: async (): Promise<Array<SelectConfig>> => {
7272
const response = await fetch(`${API_BASE_URL}/config`)
7373
if (!response.ok)
7474
throw new Error(`HTTP error! Status: ${response.status}`)
7575
return response.json()
7676
},
77-
getById: async (id: number): Promise<UpdateConfig> => {
77+
getById: async (id: number): Promise<SelectConfig> => {
7878
const response = await fetch(`${API_BASE_URL}/config/${id}`)
7979
if (!response.ok)
8080
throw new Error(`HTTP error! Status: ${response.status}`)
8181
return response.json()
8282
},
8383
create: async (
84-
config: Partial<UpdateConfig>
85-
): Promise<{ config: UpdateConfig; txid: number }> => {
84+
config: Partial<SelectConfig>
85+
): Promise<{ config: SelectConfig; txid: number }> => {
8686
const response = await fetch(`${API_BASE_URL}/config`, {
8787
method: `POST`,
8888
headers: { "Content-Type": `application/json` },
@@ -94,8 +94,8 @@ const api = {
9494
},
9595
update: async (
9696
id: number,
97-
changes: Partial<UpdateConfig>
98-
): Promise<{ config: UpdateConfig; txid: number }> => {
97+
changes: Partial<SelectConfig>
98+
): Promise<{ config: SelectConfig; txid: number }> => {
9999
const response = await fetch(`${API_BASE_URL}/config/${id}`, {
100100
method: `PUT`,
101101
headers: { "Content-Type": `application/json` },
@@ -130,12 +130,12 @@ const collectionsCache = new Map()
130130
// Function to create the appropriate todo collection based on type
131131
const createTodoCollection = (type: CollectionType) => {
132132
if (collectionsCache.has(`todo`)) {
133-
return collectionsCache.get(`todo`) as Collection<UpdateTodo>
133+
return collectionsCache.get(`todo`) as Collection<SelectTodo>
134134
} else {
135-
let newCollection: Collection<UpdateTodo>
135+
let newCollection: Collection<SelectTodo>
136136
if (type === CollectionType.Electric) {
137137
newCollection = createCollection(
138-
electricCollectionOptions<UpdateTodo>({
138+
electricCollectionOptions({
139139
id: `todos`,
140140
shapeOptions: {
141141
url: `http://localhost:3003/v1/shape`,
@@ -147,10 +147,15 @@ const createTodoCollection = (type: CollectionType) => {
147147
timestamptz: (date: string) => new Date(date),
148148
},
149149
},
150-
getKey: (item) => item.id!,
151-
schema: updateTodoSchema,
150+
getKey: (item) => item.id,
151+
schema: selectTodoSchema,
152152
onInsert: async ({ transaction }) => {
153-
const modified = transaction.mutations[0].modified
153+
const {
154+
id: _id,
155+
created_at: _f,
156+
updated_at: _ff,
157+
...modified
158+
} = transaction.mutations[0].modified
154159
const response = await api.todos.create(modified)
155160

156161
return { txid: String(response.txid) }
@@ -165,7 +170,7 @@ const createTodoCollection = (type: CollectionType) => {
165170
})
166171
)
167172

168-
return { txid: String(txids[0].txid) }
173+
return { txid: String(txids[0]!.txid) }
169174
},
170175
onDelete: async ({ transaction }) => {
171176
const txids = await Promise.all(
@@ -177,7 +182,7 @@ const createTodoCollection = (type: CollectionType) => {
177182
})
178183
)
179184

180-
return { txid: String(txids[0].txid) }
185+
return { txid: String(txids[0]!.txid) }
181186
},
182187
})
183188
)
@@ -190,22 +195,23 @@ const createTodoCollection = (type: CollectionType) => {
190195
refetchInterval: 3000,
191196
queryFn: async () => {
192197
const todos = await api.todos.getAll()
193-
// Turn date strings into Dates if needed
198+
// Turn date strings into Dates
194199
return todos.map((todo) => ({
195200
...todo,
196-
created_at: todo.created_at
197-
? new Date(todo.created_at)
198-
: undefined,
199-
updated_at: todo.updated_at
200-
? new Date(todo.updated_at)
201-
: undefined,
201+
created_at: new Date(todo.created_at),
202+
updated_at: new Date(todo.updated_at),
202203
}))
203204
},
204-
getKey: (item: UpdateTodo) => item.id!,
205-
schema: updateTodoSchema,
205+
getKey: (item: SelectTodo) => item.id,
206+
schema: selectTodoSchema,
206207
queryClient,
207208
onInsert: async ({ transaction }) => {
208-
const modified = transaction.mutations[0].modified
209+
const {
210+
id: _id,
211+
created_at: _crea,
212+
updated_at: _up,
213+
...modified
214+
} = transaction.mutations[0].modified
209215
return await api.todos.create(modified)
210216
},
211217
onUpdate: async ({ transaction }) => {
@@ -235,9 +241,9 @@ const createTodoCollection = (type: CollectionType) => {
235241
// Function to create the appropriate config collection based on type
236242
const createConfigCollection = (type: CollectionType) => {
237243
if (collectionsCache.has(`config`)) {
238-
return collectionsCache.get(`config`)
244+
return collectionsCache.get(`config`) as Collection<SelectConfig>
239245
} else {
240-
let newCollection: Collection<UpdateConfig>
246+
let newCollection: Collection<SelectConfig>
241247
if (type === CollectionType.Electric) {
242248
newCollection = createCollection(
243249
electricCollectionOptions({
@@ -254,8 +260,8 @@ const createConfigCollection = (type: CollectionType) => {
254260
},
255261
},
256262
},
257-
getKey: (item: UpdateConfig) => item.id!,
258-
schema: updateConfigSchema,
263+
getKey: (item: SelectConfig) => item.id,
264+
schema: selectConfigSchema,
259265
onInsert: async ({ transaction }) => {
260266
const modified = transaction.mutations[0].modified
261267
const response = await api.config.create(modified)
@@ -286,19 +292,15 @@ const createConfigCollection = (type: CollectionType) => {
286292
refetchInterval: 3000,
287293
queryFn: async () => {
288294
const configs = await api.config.getAll()
289-
// Turn date strings into Dates if needed
295+
// Turn date strings into Dates
290296
return configs.map((config) => ({
291297
...config,
292-
created_at: config.created_at
293-
? new Date(config.created_at)
294-
: undefined,
295-
updated_at: config.updated_at
296-
? new Date(config.updated_at)
297-
: undefined,
298+
created_at: new Date(config.created_at),
299+
updated_at: new Date(config.updated_at),
298300
}))
299301
},
300-
getKey: (item: UpdateConfig) => item.id,
301-
schema: updateConfigSchema,
302+
getKey: (item: SelectConfig) => item.id,
303+
schema: selectConfigSchema,
302304
queryClient,
303305
onInsert: async ({ transaction }) => {
304306
const modified = transaction.mutations[0].modified
@@ -348,12 +350,12 @@ export default function App() {
348350
q
349351
.from({ todoCollection: todoCollection })
350352
.orderBy(`@created_at`)
351-
.select(`@id`, `@created_at`, `@text`, `@completed`)
353+
.select(`@*`)
352354
)
353355

354356
const { data: configData } = useLiveQuery((q) =>
355357
q
356-
.from({ configCollection: configCollection as Collection<UpdateConfig> })
358+
.from({ configCollection: configCollection })
357359
.select(`@id`, `@key`, `@value`)
358360
)
359361

@@ -371,7 +373,7 @@ export default function App() {
371373
const getConfigValue = (key: string): string => {
372374
for (const config of configData) {
373375
if (config.key === key) {
374-
return config.value!
376+
return config.value
375377
}
376378
}
377379
return ``
@@ -391,8 +393,11 @@ export default function App() {
391393

392394
// If the config doesn't exist yet, create it
393395
configCollection.insert({
396+
id: Math.random(),
394397
key,
395398
value,
399+
created_at: new Date(),
400+
updated_at: new Date(),
396401
})
397402
}
398403

@@ -457,17 +462,12 @@ export default function App() {
457462
text: newTodo,
458463
completed: false,
459464
id: Math.round(Math.random() * 1000000),
465+
created_at: new Date(),
466+
updated_at: new Date(),
460467
})
461468
setNewTodo(``)
462469
}
463470

464-
const toggleTodo = (todo: UpdateTodo) => {
465-
console.log(todoCollection)
466-
todoCollection.update(todo.id, (draft) => {
467-
draft.completed = !draft.completed
468-
})
469-
}
470-
471471
const activeTodos = todos.filter((todo) => !todo.completed)
472472
const completedTodos = todos.filter((todo) => todo.completed)
473473

@@ -597,7 +597,11 @@ export default function App() {
597597
<input
598598
type="checkbox"
599599
checked={todo.completed}
600-
onChange={() => toggleTodo(todo)}
600+
onChange={() =>
601+
todoCollection.update(todo.id, (draft) => {
602+
draft.completed = !draft.completed
603+
})
604+
}
601605
className="absolute left-[12px] top-0 bottom-0 my-auto h-[40px] w-[40px] cursor-pointer"
602606
/>
603607
<label

packages/db-collections/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"dependencies": {
66
"@tanstack/db": "workspace:*",
77
"@tanstack/query-core": "^5.75.7",
8+
"@standard-schema/spec": "^1.0.0",
89
"@tanstack/store": "^0.7.0"
910
},
1011
"devDependencies": {

0 commit comments

Comments
 (0)