Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/green-waves-rescue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@powersync/attachments': minor
---

Added option to download attachments
17 changes: 16 additions & 1 deletion packages/attachments/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,24 @@
"build": "tsc -b",
"build:prod": "tsc -b --sourceMap false",
"clean": "rm -rf lib tsconfig.tsbuildinfo",
"watch": "tsc -b -w"
"watch": "tsc -b -w",
"test": "pnpm build && vitest"
},
"peerDependencies": {
"@powersync/common": "workspace:^1.18.1"
},
"devDependencies": {
"@powersync/web": "workspace:*",
"@journeyapps/wa-sqlite": "^1.0.0",
"@types/node": "^20.17.6",
"@vitest/browser": "^2.1.4",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"typescript": "^5.6.3",
"vite": "^5.4.10",
"vite-plugin-top-level-await": "^1.4.4",
"vite-plugin-wasm": "^3.3.0",
"vitest": "^2.1.4",
"webdriverio": "^9.2.8"
}
}
16 changes: 15 additions & 1 deletion packages/attachments/src/AbstractAttachmentQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export interface AttachmentQueueOptions {
* Whether to mark the initial watched attachment IDs to be synced
*/
performInitialSync?: boolean;
/**
* Should attachments be downloaded
*/
downloadAttachments?: boolean;
/**
* How to handle download errors, return { retry: false } to ignore the download
*/
Expand All @@ -35,7 +39,8 @@ export const DEFAULT_ATTACHMENT_QUEUE_OPTIONS: Partial<AttachmentQueueOptions> =
attachmentDirectoryName: 'attachments',
syncInterval: 30_000,
cacheLimit: 100,
performInitialSync: true
performInitialSync: true,
downloadAttachments: true
};

export abstract class AbstractAttachmentQueue<T extends AttachmentQueueOptions = AttachmentQueueOptions> {
Expand Down Expand Up @@ -295,6 +300,9 @@ export abstract class AbstractAttachmentQueue<T extends AttachmentQueueOptions =
}

async downloadRecord(record: AttachmentRecord) {
if (!this.options.downloadAttachments) {
return false;
}
if (!record.local_uri) {
record.local_uri = this.getLocalFilePathSuffix(record.filename);
}
Expand Down Expand Up @@ -426,6 +434,9 @@ export abstract class AbstractAttachmentQueue<T extends AttachmentQueueOptions =
}

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

private async downloadRecords() {
if (!this.options.downloadAttachments) {
return;
}
if (this.downloading) {
return;
}
Expand Down
95 changes: 95 additions & 0 deletions packages/attachments/tests/attachments/AttachmentQueue.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import * as commonSdk from '@powersync/common';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { AbstractAttachmentQueue } from '../../src/AbstractAttachmentQueue';
import { AttachmentRecord, AttachmentState } from '../../src/Schema';
import { AbstractPowerSyncDatabase } from '@powersync/common';
import { StorageAdapter } from '../../src/StorageAdapter';

const record = {
id: 'test-1',
filename: 'test.jpg',
state: AttachmentState.QUEUED_DOWNLOAD
}

const mockPowerSync = {
currentStatus: { status: 'initial' },
registerListener: vi.fn(() => {}),
resolveTables: vi.fn(() => ['table1', 'table2']),
onChangeWithCallback: vi.fn(),
getAll: vi.fn(() => Promise.resolve([{id: 'test-1'}, {id: 'test-2'}])),
execute: vi.fn(() => Promise.resolve()),
getOptional: vi.fn((_query, params) => Promise.resolve(record)),
watch: vi.fn((query, params, callbacks) => {
callbacks?.onResult?.({ rows: { _array: [{id: 'test-1'}, {id: 'test-2'}] } });
}),
writeTransaction: vi.fn(async (callback) => {
await callback({
execute: vi.fn(() => Promise.resolve())
});
})
};

const mockStorage: StorageAdapter = {
downloadFile: vi.fn(),
uploadFile: vi.fn(),
deleteFile: vi.fn(),
writeFile: vi.fn(),
readFile: vi.fn(),
fileExists: vi.fn(),
makeDir: vi.fn(),
copyFile: vi.fn(),
getUserStorageDirectory: vi.fn()
};

class TestAttachmentQueue extends AbstractAttachmentQueue {
onAttachmentIdsChange(onUpdate: (ids: string[]) => void): void {
throw new Error('Method not implemented.');
}
newAttachmentRecord(record?: Partial<AttachmentRecord>): Promise<AttachmentRecord> {
throw new Error('Method not implemented.');
}
}

describe('attachments', () => {
beforeEach(() => {
vi.clearAllMocks();
});

it('should not download attachments when downloadRecord is called with downloadAttachments false', async () => {
const queue = new TestAttachmentQueue({
powersync: mockPowerSync as any,
storage: mockStorage,
downloadAttachments: false
});

await queue.downloadRecord(record);

expect(mockStorage.downloadFile).not.toHaveBeenCalled();
});

it('should download attachments when downloadRecord is called with downloadAttachments true', async () => {
const queue = new TestAttachmentQueue({
powersync: mockPowerSync as any,
storage: mockStorage,
downloadAttachments: true
});

await queue.downloadRecord(record);

expect(mockStorage.downloadFile).toHaveBeenCalled();
});

// 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
it('should not download attachments with watchDownloads is called with downloadAttachments false', async () => {
const queue = new TestAttachmentQueue({
powersync: mockPowerSync as any,
storage: mockStorage,
downloadAttachments: true
});

queue.watchDownloads();
await vi.waitFor(() => {
expect(mockStorage.downloadFile).toBeCalledTimes(2);
});
});
});
29 changes: 29 additions & 0 deletions packages/attachments/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import wasm from 'vite-plugin-wasm';
import topLevelAwait from 'vite-plugin-top-level-await';
import { defineConfig, UserConfigExport } from 'vitest/config';

const config: UserConfigExport = {
worker: {
format: 'es',
plugins: () => [wasm(), topLevelAwait()]
},
optimizeDeps: {
// Don't optimise these packages as they contain web workers and WASM files.
// https://github.com/vitejs/vite/issues/11672#issuecomment-1415820673
exclude: ['@journeyapps/wa-sqlite', '@powersync/web']
},
plugins: [wasm(), topLevelAwait()],
test: {
isolate: false,
globals: true,
include: ['tests/**/*.test.ts'],
browser: {
enabled: true,
headless: true,
provider: 'webdriverio',
name: 'chrome' // browser name is required
}
}
};

export default defineConfig(config);
65 changes: 51 additions & 14 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading