Skip to content

Commit 0094e52

Browse files
Added option to download attachments (#416)
1 parent 3513514 commit 0094e52

File tree

6 files changed

+537
-265
lines changed

6 files changed

+537
-265
lines changed

.changeset/green-waves-rescue.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@powersync/attachments': minor
3+
---
4+
5+
Added option to download attachments

packages/attachments/package.json

+13-1
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,21 @@
2525
"build": "tsc -b",
2626
"build:prod": "tsc -b --sourceMap false",
2727
"clean": "rm -rf lib tsconfig.tsbuildinfo",
28-
"watch": "tsc -b -w"
28+
"watch": "tsc -b -w",
29+
"test": "pnpm build && vitest"
2930
},
3031
"peerDependencies": {
3132
"@powersync/common": "workspace:^1.18.1"
33+
},
34+
"devDependencies": {
35+
"@types/node": "^20.17.6",
36+
"@vitest/browser": "^2.1.4",
37+
"ts-loader": "^9.5.1",
38+
"ts-node": "^10.9.2",
39+
"typescript": "^5.6.3",
40+
"vite": "^5.4.10",
41+
"vite-plugin-top-level-await": "^1.4.4",
42+
"vitest": "^2.1.4",
43+
"webdriverio": "^9.2.8"
3244
}
3345
}

packages/attachments/src/AbstractAttachmentQueue.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ export interface AttachmentQueueOptions {
2121
* Whether to mark the initial watched attachment IDs to be synced
2222
*/
2323
performInitialSync?: boolean;
24+
/**
25+
* Should attachments be downloaded
26+
*/
27+
downloadAttachments?: boolean;
2428
/**
2529
* How to handle download errors, return { retry: false } to ignore the download
2630
*/
@@ -35,7 +39,8 @@ export const DEFAULT_ATTACHMENT_QUEUE_OPTIONS: Partial<AttachmentQueueOptions> =
3539
attachmentDirectoryName: 'attachments',
3640
syncInterval: 30_000,
3741
cacheLimit: 100,
38-
performInitialSync: true
42+
performInitialSync: true,
43+
downloadAttachments: true
3944
};
4045

4146
export abstract class AbstractAttachmentQueue<T extends AttachmentQueueOptions = AttachmentQueueOptions> {
@@ -295,6 +300,9 @@ export abstract class AbstractAttachmentQueue<T extends AttachmentQueueOptions =
295300
}
296301

297302
async downloadRecord(record: AttachmentRecord) {
303+
if (!this.options.downloadAttachments) {
304+
return false;
305+
}
298306
if (!record.local_uri) {
299307
record.local_uri = this.getLocalFilePathSuffix(record.filename);
300308
}
@@ -426,6 +434,9 @@ export abstract class AbstractAttachmentQueue<T extends AttachmentQueueOptions =
426434
}
427435

428436
watchDownloads() {
437+
if (!this.options.downloadAttachments) {
438+
return;
439+
}
429440
this.idsToDownload(async (ids) => {
430441
ids.map((id) => this.downloadQueue.add(id));
431442
// No need to await this, the lock will ensure only one loop is running at a time
@@ -434,6 +445,9 @@ export abstract class AbstractAttachmentQueue<T extends AttachmentQueueOptions =
434445
}
435446

436447
private async downloadRecords() {
448+
if (!this.options.downloadAttachments) {
449+
return;
450+
}
437451
if (this.downloading) {
438452
return;
439453
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import * as commonSdk from '@powersync/common';
2+
import { beforeEach, describe, expect, it, vi } from 'vitest';
3+
import { AbstractAttachmentQueue } from '../../src/AbstractAttachmentQueue';
4+
import { AttachmentRecord, AttachmentState } from '../../src/Schema';
5+
import { AbstractPowerSyncDatabase } from '@powersync/common';
6+
import { StorageAdapter } from '../../src/StorageAdapter';
7+
8+
const record = {
9+
id: 'test-1',
10+
filename: 'test.jpg',
11+
state: AttachmentState.QUEUED_DOWNLOAD
12+
}
13+
14+
const mockPowerSync = {
15+
currentStatus: { status: 'initial' },
16+
registerListener: vi.fn(() => {}),
17+
resolveTables: vi.fn(() => ['table1', 'table2']),
18+
onChangeWithCallback: vi.fn(),
19+
getAll: vi.fn(() => Promise.resolve([{id: 'test-1'}, {id: 'test-2'}])),
20+
execute: vi.fn(() => Promise.resolve()),
21+
getOptional: vi.fn((_query, params) => Promise.resolve(record)),
22+
watch: vi.fn((query, params, callbacks) => {
23+
callbacks?.onResult?.({ rows: { _array: [{id: 'test-1'}, {id: 'test-2'}] } });
24+
}),
25+
writeTransaction: vi.fn(async (callback) => {
26+
await callback({
27+
execute: vi.fn(() => Promise.resolve())
28+
});
29+
})
30+
};
31+
32+
const mockStorage: StorageAdapter = {
33+
downloadFile: vi.fn(),
34+
uploadFile: vi.fn(),
35+
deleteFile: vi.fn(),
36+
writeFile: vi.fn(),
37+
readFile: vi.fn(),
38+
fileExists: vi.fn(),
39+
makeDir: vi.fn(),
40+
copyFile: vi.fn(),
41+
getUserStorageDirectory: vi.fn()
42+
};
43+
44+
class TestAttachmentQueue extends AbstractAttachmentQueue {
45+
onAttachmentIdsChange(onUpdate: (ids: string[]) => void): void {
46+
throw new Error('Method not implemented.');
47+
}
48+
newAttachmentRecord(record?: Partial<AttachmentRecord>): Promise<AttachmentRecord> {
49+
throw new Error('Method not implemented.');
50+
}
51+
}
52+
53+
describe('attachments', () => {
54+
beforeEach(() => {
55+
vi.clearAllMocks();
56+
});
57+
58+
it('should not download attachments when downloadRecord is called with downloadAttachments false', async () => {
59+
const queue = new TestAttachmentQueue({
60+
powersync: mockPowerSync as any,
61+
storage: mockStorage,
62+
downloadAttachments: false
63+
});
64+
65+
await queue.downloadRecord(record);
66+
67+
expect(mockStorage.downloadFile).not.toHaveBeenCalled();
68+
});
69+
70+
it('should download attachments when downloadRecord is called with downloadAttachments true', async () => {
71+
const queue = new TestAttachmentQueue({
72+
powersync: mockPowerSync as any,
73+
storage: mockStorage,
74+
downloadAttachments: true
75+
});
76+
77+
await queue.downloadRecord(record);
78+
79+
expect(mockStorage.downloadFile).toHaveBeenCalled();
80+
});
81+
82+
// Testing the inverse of this test, i.e. when downloadAttachments is false, is not required as you can't wait for something that does not happen
83+
it('should not download attachments with watchDownloads is called with downloadAttachments false', async () => {
84+
const queue = new TestAttachmentQueue({
85+
powersync: mockPowerSync as any,
86+
storage: mockStorage,
87+
downloadAttachments: true
88+
});
89+
90+
queue.watchDownloads();
91+
await vi.waitFor(() => {
92+
expect(mockStorage.downloadFile).toBeCalledTimes(2);
93+
});
94+
});
95+
});

packages/attachments/vitest.config.ts

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import topLevelAwait from 'vite-plugin-top-level-await';
2+
import { defineConfig, UserConfigExport } from 'vitest/config';
3+
4+
const config: UserConfigExport = {
5+
plugins: [topLevelAwait()],
6+
test: {
7+
isolate: false,
8+
globals: true,
9+
include: ['tests/**/*.test.ts'],
10+
browser: {
11+
enabled: true,
12+
headless: true,
13+
provider: 'webdriverio',
14+
name: 'chrome' // browser name is required
15+
}
16+
}
17+
};
18+
19+
export default defineConfig(config);

0 commit comments

Comments
 (0)