-
Notifications
You must be signed in to change notification settings - Fork 78
Expand file tree
/
Copy pathAttachmentContext.ts
More file actions
280 lines (256 loc) · 7.43 KB
/
Copy pathAttachmentContext.ts
File metadata and controls
280 lines (256 loc) · 7.43 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
import { AbstractPowerSyncDatabase } from '../client/AbstractPowerSyncDatabase.js';
import { Transaction } from '../db/DBAdapter.js';
import { ILogger } from '../utils/Logger.js';
import { AttachmentRecord, AttachmentState, attachmentFromSql } from './Schema.js';
/**
* AttachmentContext provides database operations for managing attachment records.
*
* Provides methods to query, insert, update, and delete attachment records with
* proper transaction management through PowerSync.
*
* @experimental
* @alpha
*/
export class AttachmentContext {
/** PowerSync database instance for executing queries */
readonly db: AbstractPowerSyncDatabase;
/** Name of the database table storing attachment records */
readonly tableName: string;
/** Logger instance for diagnostic information */
readonly logger: ILogger;
/** Maximum number of archived attachments to keep before cleanup */
readonly archivedCacheLimit: number = 100;
/**
* Creates a new AttachmentContext instance.
*
* @param db - PowerSync database instance
* @param tableName - Name of the table storing attachment records. Default: 'attachments'
* @param logger - Logger instance for diagnostic output
*/
constructor(
db: AbstractPowerSyncDatabase,
tableName: string = 'attachments',
logger: ILogger,
archivedCacheLimit: number
) {
this.db = db;
this.tableName = tableName;
this.logger = logger;
this.archivedCacheLimit = archivedCacheLimit;
}
/**
* Retrieves all active attachments that require synchronization.
* Active attachments include those queued for upload, download, or delete.
* Results are ordered by timestamp in ascending order.
*
* @returns Promise resolving to an array of active attachment records
*/
async getActiveAttachments(): Promise<AttachmentRecord[]> {
const attachments = await this.db.getAll(
/* sql */
`
SELECT
*
FROM
${this.tableName}
WHERE
state = ?
OR state = ?
OR state = ?
ORDER BY
timestamp ASC
`,
[AttachmentState.QUEUED_UPLOAD, AttachmentState.QUEUED_DOWNLOAD, AttachmentState.QUEUED_DELETE]
);
return attachments.map(attachmentFromSql);
}
/**
* Retrieves all archived attachments.
*
* Archived attachments are no longer referenced but haven't been permanently deleted.
* These are candidates for cleanup operations to free up storage space.
*
* @returns Promise resolving to an array of archived attachment records
*/
async getArchivedAttachments(): Promise<AttachmentRecord[]> {
const attachments = await this.db.getAll(
/* sql */
`
SELECT
*
FROM
${this.tableName}
WHERE
state = ?
ORDER BY
timestamp ASC
`,
[AttachmentState.ARCHIVED]
);
return attachments.map(attachmentFromSql);
}
/**
* Retrieves all attachment records regardless of state.
* Results are ordered by timestamp in ascending order.
*
* @returns Promise resolving to an array of all attachment records
*/
async getAttachments(): Promise<AttachmentRecord[]> {
const attachments = await this.db.getAll(
/* sql */
`
SELECT
*
FROM
${this.tableName}
ORDER BY
timestamp ASC
`,
[]
);
return attachments.map(attachmentFromSql);
}
/**
* Inserts or updates an attachment record within an existing transaction.
*
* Performs an upsert operation (INSERT OR REPLACE). Must be called within
* an active database transaction context.
*
* @param attachment - The attachment record to upsert
* @param context - Active database transaction context
*/
async upsertAttachment(attachment: AttachmentRecord, context: Transaction): Promise<void> {
await context.execute(
/* sql */
`
INSERT
OR REPLACE INTO ${this.tableName} (
id,
filename,
local_uri,
size,
media_type,
timestamp,
state,
has_synced,
meta_data
)
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?)
`,
[
attachment.id,
attachment.filename,
attachment.localUri || null,
attachment.size || null,
attachment.mediaType || null,
attachment.timestamp,
attachment.state,
attachment.hasSynced ? 1 : 0,
attachment.metaData || null
]
);
}
async getAttachment(id: string): Promise<AttachmentRecord | undefined> {
const attachment = await this.db.get(
/* sql */
`
SELECT
*
FROM
${this.tableName}
WHERE
id = ?
`,
[id]
);
return attachment ? attachmentFromSql(attachment) : undefined;
}
/**
* Permanently deletes an attachment record from the database.
*
* This operation removes the attachment record but does not delete
* the associated local or remote files. File deletion should be handled
* separately through the appropriate storage adapters.
*
* @param attachmentId - Unique identifier of the attachment to delete
*/
async deleteAttachment(attachmentId: string): Promise<void> {
await this.db.writeTransaction((tx) =>
tx.execute(
/* sql */
`
DELETE FROM ${this.tableName}
WHERE
id = ?
`,
[attachmentId]
)
);
}
async clearQueue(): Promise<void> {
await this.db.writeTransaction((tx) => tx.execute(/* sql */ ` DELETE FROM ${this.tableName} `));
}
async deleteArchivedAttachments(callback?: (attachments: AttachmentRecord[]) => Promise<void>): Promise<boolean> {
const limit = 1000;
const results = await this.db.getAll(
/* sql */
`
SELECT
*
FROM
${this.tableName}
WHERE
state = ?
ORDER BY
timestamp DESC
LIMIT
?
OFFSET
?
`,
[AttachmentState.ARCHIVED, limit, this.archivedCacheLimit]
);
const archivedAttachments = results.map(attachmentFromSql);
if (archivedAttachments.length === 0) return false;
await callback?.(archivedAttachments);
this.logger.info(
`Deleting ${archivedAttachments.length} archived attachments. Archived attachment exceeds cache archiveCacheLimit of ${this.archivedCacheLimit}.`
);
const ids = archivedAttachments.map((attachment) => attachment.id);
await this.db.execute(
/* sql */
`
DELETE FROM ${this.tableName}
WHERE
id IN (
SELECT
json_each.value
FROM
json_each (?)
);
`,
[JSON.stringify(ids)]
);
this.logger.info(`Deleted ${archivedAttachments.length} archived attachments`);
return archivedAttachments.length < limit;
}
/**
* Saves multiple attachment records in a single transaction.
*
* All updates are saved in a single batch after processing.
* If the attachments array is empty, no database operations are performed.
*
* @param attachments - Array of attachment records to save
*/
async saveAttachments(attachments: AttachmentRecord[]): Promise<void> {
if (attachments.length === 0) {
return;
}
await this.db.writeTransaction(async (tx) => {
for (const attachment of attachments) {
await this.upsertAttachment(attachment, tx);
}
});
}
}