Skip to content

Commit ffff573

Browse files
committed
Update @journeyapps/powersync-attachments README.md
1 parent c20fd44 commit ffff573

File tree

1 file changed

+189
-2
lines changed

1 file changed

+189
-2
lines changed
Lines changed: 189 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,191 @@
1-
# PowerSync library to manage attachments in TypeScript and React Native apps
1+
# @journeyapps/powersync-attachments
22

3-
This package is currently in an alpha release.
3+
A [PowerSync](https://powersync.co) library to manage attachments in TypeScript and React Native apps.
44

5+
Note: This package is currently in a beta release.
6+
7+
# Usage
8+
9+
## Installation
10+
11+
**yarn**
12+
```bash
13+
yarn add @journeyapps/powersync-attachments
14+
```
15+
16+
**pnpm**
17+
```bash
18+
pnpm install @journeyapps/powersync-attachments
19+
```
20+
21+
## API
22+
23+
### Implement an `AttachmentQueue`
24+
25+
The `AttachmentQueue` class is used to manage and sync attachments in your app.
26+
27+
In this example we are capturing photos as part of an inspection workflow. Here is the schema for the `checklist` table:
28+
29+
```typescript
30+
const AppSchema = new Schema([
31+
new Table({
32+
name: 'checklists',
33+
columns: [
34+
new Column({ name: 'photo_id', type: ColumnType.TEXT }),
35+
new Column({ name: 'description', type: ColumnType.TEXT }),
36+
new Column({ name: 'completed', type: ColumnType.INTEGER }),
37+
new Column({ name: 'completed_at', type: ColumnType.TEXT }),
38+
new Column({ name: 'completed_by', type: ColumnType.TEXT })
39+
],
40+
indexes: [
41+
new Index({
42+
name: 'inspections',
43+
columns: [new IndexedColumn({ name: 'checklist_id' })]
44+
})
45+
]
46+
})
47+
]);
48+
```
49+
50+
1. Implement a new class `AttachmentQueue` that extends `AbstractAttachmentQueue` from `@journeyapps/powersync-attachments`.
51+
2. Implement the `attachmentIds` AsyncIterator method to return an array of `string` values of IDs that relate to attachments in your app.
52+
53+
We use `PowerSync`'s watch query to return the all IDs of photos that have been captured as part of an inspection, and map them to an array of `string` values.
54+
55+
```typescript
56+
import { AbstractAttachmentQueue } from '@journeyapps/powersync-attachments';
57+
58+
export class AttachmentQueue extends AbstractAttachmentQueue {
59+
async *attachmentIds(): AsyncIterable<string[]> {
60+
for await (const result of this.powersync.watch(
61+
`SELECT photo_id as id FROM checklists WHERE photo_id IS NOT NULL`,
62+
[]
63+
)) {
64+
yield result.rows?._array.map((r) => r.id) ?? [];
65+
}
66+
}
67+
}
68+
```
69+
70+
3. Implement `newAttachmentRecord` to return an object that represents the attachment record in your app. In this example we always work with JPEG images, but you can use any media type that is supported by your app and storage solution.
71+
72+
```typescript
73+
import { AbstractAttachmentQueue } from '@journeyapps/powersync-attachments';
74+
75+
export class AttachmentQueue extends AbstractAttachmentQueue {
76+
// ...
77+
78+
async newAttachmentRecord(record?: Partial<AttachmentRecord>): Promise<AttachmentRecord> {
79+
const photoId = record?.id ?? uuid();
80+
const filename = record?.filename ?? `${photoId}.jpg`;
81+
return {
82+
id: photoId,
83+
filename,
84+
media_type: 'image/jpeg',
85+
state: AttachmentState.QUEUED_UPLOAD,
86+
...record
87+
};
88+
}
89+
}
90+
```
91+
92+
4. Add an `AttachmentTable` to your app's PowerSync Schema:
93+
94+
```typescript
95+
import { AttachmentTable } from '@journeyapps/powersync-attachments';
96+
97+
const AppSchema = new Schema([
98+
// ... other tables
99+
new AttachmentTable()
100+
]);
101+
```
102+
103+
The `AttachmentTable` can optionally be configured with the following options, in addition to `Table` options:
104+
105+
| Option | Description | Default |
106+
|---------------------|--------------------------------------------------------------------------------------|-------------------------------|
107+
| `name` | The name of the table | `attachments` |
108+
| `additionalColumns` | An array of addition `Column` objects that added to the default columns in the table | See below for default columns |
109+
110+
The default columns in the `AttachmentTable` are:
111+
112+
| Column Name | Type | Description |
113+
|--------------|-----------|-------------------------------------------------------------------|
114+
| `id` | `TEXT` | The ID of the attachment record |
115+
| `filename` | `TEXT` | The filename of the attachment |
116+
| `media_type` | `TEXT` | The media type of the attachment |
117+
| `state` | `INTEGER` | The state of the attachment, one of `AttachmentState` enum values |
118+
| `timestamp` | `INTEGER` | The timestamp of last update to the attachment record |
119+
| `size` | `INTEGER` | The size of the attachment in bytes |
120+
121+
5. To instantiate an `AttachmentQueue`, one needs to provide an instance of `AbstractPowerSyncDatabase` from PowerSync and an instance of `StorageAdapter`. For the specifications about what must be implemented in `StorageAdapter`, please refer to its interface definition.
122+
123+
6. Instantiate you `AttachmentQueue` and call `init()` to start syncing attachments. (Example below uses Supabase Storage)
124+
125+
```typescript
126+
this.storage = this.supabaseConnector.storage;
127+
this.powersync = factory.getInstance();
128+
129+
this.attachmentQueue = new AttachmentQueue({
130+
powersync: this.powersync,
131+
storage: this.storage
132+
});
133+
134+
await this.attachmentQueue.init();
135+
```
136+
137+
# Implementation details
138+
139+
## Attachment States
140+
141+
The `AttachmentQueue` class manages attachments in your app by tracking their state. The state of an attachment can be one of the following:
142+
143+
| State | Description |
144+
|-------------------|-------------------------------------------------------------------------------|
145+
| `QUEUED_SYNC` | Check if the attachment needs to be uploaded or downloaded |
146+
| `QUEUED_UPLOAD` | The attachment has been queued for upload to the cloud storage |
147+
| `QUEUED_DOWNLOAD` | The attachment has been queued for download from the cloud storage |
148+
| `SYNCED` | The attachment has been synced |
149+
| `ARCHIVED` | The attachment has been orphaned, i.e. the associated record has been deleted |
150+
151+
## Initial sync
152+
153+
Upon initializing the `AttachmentQueue`, an initial sync of attachments will take place if the `performInitialSync` is set to true.
154+
Any attachment record with `id` in first set of IDs retrieved from the watch query will be marked as `QUEUED_SYNC`, and these records will be rechecked to see if they need to be uploaded or downloaded.
155+
156+
## Syncing attachments
157+
158+
The `AttachmentQueue` sets up two watch queries on the `attachments` table, one for records in `QUEUED_UPLOAD` state and one for `QUEUED_DOWNLOAD` state.
159+
160+
In addition to watching for changes, the `AttachmentQueue` also trigger a sync every few seconds. This will retry any failed uploads/downloads, in particular after the app was offline
161+
By default, this is every 30 seconds, but can be configured by setting `syncInterval` in the `AttachmentQueue` constructor options, or disabled by setting the interval to `0`.
162+
163+
### Uploading
164+
165+
- An `AttachmentRecord` is created or updated with a state of `QUEUED_UPLOAD`.
166+
- The `AttachmentQueue` picks this up and upon successful upload to Supabase, sets the state to `SYNCED`.
167+
- If the upload is not successful, the record remains in `QUEUED_UPLOAD` state and uploading will be retried when syncing triggers again.
168+
169+
### Downloading
170+
171+
- An `AttachmentRecord` is created or updated with QUEUED_DOWNLOAD state.
172+
- The watch query adds the `id` into a queue of IDs to download and triggers the download process
173+
- This checks whether the photo is already on the device and if so, skips downloading.
174+
- If the photo is not on the device, it is downloaded from cloud storage.
175+
- Writes file to the user's local storage.
176+
- If this is successful, update the AttachmentRecord state to `SYNCED`.
177+
- If any of these fail, the download is retried in the next sync trigger.
178+
179+
### Expire Cache
180+
181+
When PowerSync removes a records (as a result of coming back online or conflict resolution):
182+
- Any associated `AttachmentRecord` is orphaned.
183+
- On the next sync trigger, the `AttachmentQueue` sets all records that are orphaned to `ARCHIVED` state.
184+
- By default, the `AttachmentQueue` only keeps the last `100` attachment records and then expires the rest. (This can be configured by setting `cacheLimit` in the `AttachmentQueue` constructor options).
185+
186+
### Deleting attachments
187+
188+
When a list or to-do item is deleted by a user action or cache expiration:
189+
- Related `AttachmentRecord` is removed from attachments table.
190+
- Local file (if exists) is deleted.
191+
- File on cloud storage is deleted.

0 commit comments

Comments
 (0)