Skip to content

Commit 91c24c6

Browse files
committed
Docs.
1 parent d18ab19 commit 91c24c6

2 files changed

Lines changed: 171 additions & 0 deletions

File tree

docs/collections/powersync-collection.md

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1099,4 +1099,171 @@ const liveQuery = createLiveQueryCollection({
10991099
completed: todo.completed,
11001100
})),
11011101
})
1102+
```
1103+
1104+
## Attachments
1105+
1106+
`@tanstack/powersync-db-collection` ships `TanStackDBAttachmentQueue`, an [`AttachmentQueue`](https://docs.powersync.com/usage/use-case-examples/attachments-files) whose file operations commit inside a TanStack DB collection transaction. This lets you create (or delete) an attachment and mutate a related collection row (for example, setting `lists.photo_id`) atomically in a single transaction, instead of issuing two independent writes.
1107+
1108+
The queue extends PowerSync's `AttachmentQueue`, so the generic concepts are unchanged and documented once in the SDK.
1109+
1110+
> This section only covers what is specific to the TanStack DB integration. For storage adapters (local and remote), the `AttachmentTable` schema primitive, error-handling/retry semantics, and the `startSync()` / `stopSync()` lifecycle, see the [PowerSync attachments documentation](https://docs.powersync.com/usage/use-case-examples/attachments-files).
1111+
1112+
### Prerequisites
1113+
1114+
These are standard PowerSync attachment requirements. See the SDK attachments docs for details.
1115+
1116+
- An `AttachmentTable` in your schema:
1117+
1118+
```ts
1119+
import { AttachmentTable, Schema } from "@powersync/web"
1120+
1121+
const APP_SCHEMA = new Schema({
1122+
// ...your tables
1123+
attachments: new AttachmentTable(),
1124+
})
1125+
```
1126+
1127+
- A local storage adapter (such as `IndexDBFileSystemStorageAdapter` on web) and a remote storage adapter (an implementation of the SDK's `RemoteStorageAdapter`, for example backed by Supabase Storage). Both are generic to all attachment users. See the SDK docs for the available adapters and the remote-adapter contract.
1128+
1129+
### 1. Create the attachments collection
1130+
1131+
This is the piece that makes the integration TanStack-aware: a normal PowerSync collection over the attachments table. The queue reads and writes attachment records through it.
1132+
1133+
```ts
1134+
import { createCollection } from "@tanstack/react-db"
1135+
import { powerSyncCollectionOptions } from "@tanstack/powersync-db-collection"
1136+
1137+
const attachmentsCollection = createCollection(
1138+
powerSyncCollectionOptions({
1139+
database: db,
1140+
table: APP_SCHEMA.props.attachments,
1141+
})
1142+
)
1143+
```
1144+
1145+
### 2. Construct the queue
1146+
1147+
Pass your collection as `attachmentsCollection` alongside the standard `AttachmentQueue` options. Only `attachmentsCollection` and `watchAttachments` (below) are specific to this package; `db`, `localStorage`, `remoteStorage`, and `errorHandler` are the usual SDK options.
1148+
1149+
```ts
1150+
import { TanStackDBAttachmentQueue } from "@tanstack/powersync-db-collection"
1151+
1152+
const attachmentQueue = new TanStackDBAttachmentQueue({
1153+
db,
1154+
attachmentsCollection, // TanStack DB collection over your AttachmentTable
1155+
localStorage, // SDK local storage adapter
1156+
remoteStorage, // your RemoteStorageAdapter (see SDK docs)
1157+
watchAttachments, // see step 3
1158+
errorHandler, // standard AttachmentQueue error handler (see SDK docs)
1159+
})
1160+
```
1161+
1162+
Start and stop syncing with the standard `attachmentQueue.startSync()` / `attachmentQueue.stopSync()` lifecycle (see SDK docs), typically inside a React effect or provider.
1163+
1164+
### 3. Tell the queue which attachments exist (`watchAttachments`)
1165+
1166+
`watchAttachments` reports the set of attachment IDs your data currently references, so the queue knows what to download and what to archive. With TanStack DB you drive it from a live query: emit the initial state, then re-emit the complete set on every change, and clean up on abort.
1167+
1168+
```ts
1169+
import {
1170+
createCollection,
1171+
isNull,
1172+
liveQueryCollectionOptions,
1173+
not,
1174+
} from "@tanstack/db"
1175+
import { WatchedAttachmentItem } from "@powersync/web"
1176+
1177+
const watchAttachments = async (onUpdate, abortSignal) => {
1178+
// Every row in your data model that references an attachment.
1179+
const livePhotoIds = createCollection(
1180+
liveQueryCollectionOptions({
1181+
query: (q) =>
1182+
q
1183+
.from({ document: listsCollection })
1184+
.where(({ document }) => not(isNull(document.photo_id)))
1185+
.select(({ document }) => ({ photo_id: document.photo_id })),
1186+
})
1187+
)
1188+
1189+
const mapper = (item) =>
1190+
({
1191+
id: item.photo_id,
1192+
fileExtension: "jpg",
1193+
}) satisfies WatchedAttachmentItem
1194+
1195+
// 1. Report the initial set of referenced attachment IDs.
1196+
const initialState = await livePhotoIds.stateWhenReady()
1197+
onUpdate(Array.from(initialState.values()).map(mapper))
1198+
1199+
// 2. Re-emit the whole set on every change (the queue expects the holistic state).
1200+
livePhotoIds.subscribeChanges(() => {
1201+
onUpdate(livePhotoIds.map(mapper))
1202+
})
1203+
1204+
// 3. Clean up when sync stops.
1205+
abortSignal.addEventListener("abort", () => livePhotoIds.cleanup(), {
1206+
once: true,
1207+
})
1208+
}
1209+
```
1210+
1211+
> A `watchAttachmentsFromQuery(...)` convenience helper that collapses this boilerplate into a single call is planned. Until then, use the pattern above.
1212+
1213+
### 4. Save an attachment atomically with related data
1214+
1215+
`saveFileTanStack` writes the file, inserts the attachment record into your collection, and runs your `updateHook` mutations in the same transaction. Use the hook to insert or update the row that references the new attachment, so both land together or not at all.
1216+
1217+
```ts
1218+
await attachmentQueue.saveFileTanStack({
1219+
data, // file bytes (ArrayBuffer / base64, per your local adapter)
1220+
fileExtension: "jpg",
1221+
updateHook: async (attachmentRecord) => {
1222+
// Runs in the same transaction as the attachment insert.
1223+
listsCollection.insert({
1224+
id: crypto.randomUUID(),
1225+
name,
1226+
created_at: new Date(),
1227+
owner_id: userID,
1228+
photo_id: attachmentRecord.id, // associate the row with the attachment
1229+
})
1230+
},
1231+
})
1232+
```
1233+
1234+
### 5. Delete an attachment and detach it from the row
1235+
1236+
`deleteFileTanStack` queues the file for deletion and runs your `updateHook` in the same transaction. Clear the foreign key so the row and the attachment stay consistent.
1237+
1238+
```ts
1239+
await attachmentQueue.deleteFileTanStack({
1240+
id: photo_id,
1241+
updateHook: async () => {
1242+
listsCollection.update(listId, (draft) => {
1243+
draft.photo_id = null
1244+
})
1245+
},
1246+
})
1247+
```
1248+
1249+
### 6. Display attachments via a live-query join
1250+
1251+
Join your attachments collection into a live query to read the local URI (the locally cached file path) alongside your domain rows:
1252+
1253+
```ts
1254+
import { eq } from "@tanstack/db"
1255+
1256+
const { data } = useLiveQuery((q) =>
1257+
q
1258+
.from({ lists: listsCollection })
1259+
.leftJoin({ attachment: attachmentsCollection }, ({ lists, attachment }) =>
1260+
eq(lists.photo_id, attachment.id)
1261+
)
1262+
.select(({ lists, attachment }) => ({
1263+
id: lists.id,
1264+
name: lists.name,
1265+
photo_id: lists.photo_id,
1266+
attachment_local_uri: attachment?.local_uri,
1267+
}))
1268+
)
11021269
```

packages/powersync-db-collection/src/attachments.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ export interface SaveFileTanStackOptions {
4040

4141
export interface DeleteFileTanStackOptions {
4242
id: string
43+
/** *
44+
* Note that this is called inside a synchronous TanStackDB transaction,
45+
* any mutations made to other collections will be in the same transaction.
46+
*/
4347
updateHook?: (attachment: AttachmentQueueRow) => Promise<void>
4448
}
4549

0 commit comments

Comments
 (0)