@pol-studios/powersync 1.0.22 → 1.0.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/attachments/index.d.ts +51 -17
- package/dist/attachments/index.js +6 -2
- package/dist/{chunk-PG2NPQG3.js → chunk-55DKCJV4.js} +25 -5
- package/dist/chunk-55DKCJV4.js.map +1 -0
- package/dist/{chunk-IMRSLJRV.js → chunk-BGBQYQV3.js} +129 -38
- package/dist/chunk-BGBQYQV3.js.map +1 -0
- package/dist/{chunk-ZM4ENYMF.js → chunk-C5ODS3XH.js} +51 -8
- package/dist/chunk-C5ODS3XH.js.map +1 -0
- package/dist/{chunk-4TXTAEF2.js → chunk-CACKC6XG.js} +3 -2
- package/dist/chunk-CACKC6XG.js.map +1 -0
- package/dist/{chunk-XOCIONAA.js → chunk-TIFL2KWE.js} +3 -3
- package/dist/{chunk-N4K7E53V.js → chunk-VB737IVN.js} +33 -41
- package/dist/{chunk-N4K7E53V.js.map → chunk-VB737IVN.js.map} +1 -1
- package/dist/connector/index.d.ts +1 -1
- package/dist/connector/index.js +1 -1
- package/dist/generator/cli.js +6 -1
- package/dist/generator/index.d.ts +1 -0
- package/dist/generator/index.js +9 -1
- package/dist/generator/index.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +10 -6
- package/dist/index.native.d.ts +3 -3
- package/dist/index.native.js +10 -6
- package/dist/index.web.d.ts +3 -3
- package/dist/index.web.js +10 -6
- package/dist/{pol-attachment-queue-BVAIueoP.d.ts → pol-attachment-queue-BE2HU3Us.d.ts} +71 -7
- package/dist/provider/index.d.ts +2 -2
- package/dist/provider/index.js +4 -4
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.js +3 -3
- package/dist/{supabase-connector-WuiFiBnV.d.ts → supabase-connector-D2oIl2t8.d.ts} +13 -2
- package/dist/sync/index.d.ts +3 -0
- package/dist/sync/index.js +1 -1
- package/package.json +14 -4
- package/dist/chunk-4TXTAEF2.js.map +0 -1
- package/dist/chunk-IMRSLJRV.js.map +0 -1
- package/dist/chunk-PG2NPQG3.js.map +0 -1
- package/dist/chunk-ZM4ENYMF.js.map +0 -1
- /package/dist/{chunk-XOCIONAA.js.map → chunk-TIFL2KWE.js.map} +0 -0
|
@@ -58,9 +58,17 @@ function validateWhereClause(whereClause) {
|
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
+
function resolvePathColumn(config) {
|
|
62
|
+
const column = config.pathColumn ?? config.idColumn;
|
|
63
|
+
if (!column) {
|
|
64
|
+
throw new Error("WatchConfig requires either pathColumn or idColumn. pathColumn is preferred; idColumn is deprecated.");
|
|
65
|
+
}
|
|
66
|
+
return column;
|
|
67
|
+
}
|
|
61
68
|
function buildWatchQuery(config) {
|
|
69
|
+
const pathColumn = resolvePathColumn(config);
|
|
62
70
|
validateSqlIdentifier(config.table, "table");
|
|
63
|
-
validateSqlIdentifier(
|
|
71
|
+
validateSqlIdentifier(pathColumn, "pathColumn");
|
|
64
72
|
if (config.selectColumns) {
|
|
65
73
|
for (const col of config.selectColumns) {
|
|
66
74
|
validateSqlIdentifier(col, "selectColumns");
|
|
@@ -75,13 +83,13 @@ function buildWatchQuery(config) {
|
|
|
75
83
|
if (config.where) {
|
|
76
84
|
validateWhereClause(config.where);
|
|
77
85
|
}
|
|
78
|
-
const selectParts = [`${
|
|
86
|
+
const selectParts = [`${pathColumn} AS id`];
|
|
79
87
|
if (config.selectColumns && config.selectColumns.length > 0) {
|
|
80
88
|
selectParts.push(...config.selectColumns);
|
|
81
89
|
}
|
|
82
90
|
const selectClause = selectParts.join(", ");
|
|
83
91
|
const fromClause = config.table;
|
|
84
|
-
let whereClause = `${
|
|
92
|
+
let whereClause = `${pathColumn} IS NOT NULL AND ${pathColumn} != ''`;
|
|
85
93
|
if (config.where) {
|
|
86
94
|
whereClause = `${whereClause} AND (${config.where})`;
|
|
87
95
|
}
|
|
@@ -102,14 +110,15 @@ function buildIdOnlyWatchQuery(config) {
|
|
|
102
110
|
});
|
|
103
111
|
}
|
|
104
112
|
function buildRecordFetchQuery(config, ids) {
|
|
113
|
+
const pathColumn = resolvePathColumn(config);
|
|
105
114
|
validateSqlIdentifier(config.table, "table");
|
|
106
|
-
validateSqlIdentifier(
|
|
115
|
+
validateSqlIdentifier(pathColumn, "pathColumn");
|
|
107
116
|
if (config.selectColumns) {
|
|
108
117
|
for (const col of config.selectColumns) {
|
|
109
118
|
validateSqlIdentifier(col, "selectColumns");
|
|
110
119
|
}
|
|
111
120
|
}
|
|
112
|
-
const selectParts = [`${
|
|
121
|
+
const selectParts = [`${pathColumn} AS id`];
|
|
113
122
|
if (config.selectColumns && config.selectColumns.length > 0) {
|
|
114
123
|
selectParts.push(...config.selectColumns);
|
|
115
124
|
}
|
|
@@ -118,7 +127,7 @@ function buildRecordFetchQuery(config, ids) {
|
|
|
118
127
|
const params = [];
|
|
119
128
|
if (ids && ids.length > 0) {
|
|
120
129
|
const placeholders = ids.map(() => "?").join(", ");
|
|
121
|
-
query += ` WHERE ${
|
|
130
|
+
query += ` WHERE ${pathColumn} IN (${placeholders})`;
|
|
122
131
|
params.push(...ids);
|
|
123
132
|
}
|
|
124
133
|
return {
|
|
@@ -127,12 +136,44 @@ function buildRecordFetchQuery(config, ids) {
|
|
|
127
136
|
};
|
|
128
137
|
}
|
|
129
138
|
function watchConfigToSourceConfig(watchConfig) {
|
|
139
|
+
const pathColumn = resolvePathColumn(watchConfig);
|
|
130
140
|
return {
|
|
131
141
|
table: watchConfig.table,
|
|
132
|
-
|
|
142
|
+
pathColumn,
|
|
143
|
+
idColumn: pathColumn,
|
|
144
|
+
// For backwards compatibility
|
|
133
145
|
orderByColumn: watchConfig.orderBy?.column ?? null
|
|
134
146
|
};
|
|
135
147
|
}
|
|
148
|
+
function extractIdsFromRows(results) {
|
|
149
|
+
const rows = results.rows;
|
|
150
|
+
if (!rows) return [];
|
|
151
|
+
const rowArray = rows?._array ?? rows;
|
|
152
|
+
if (!Array.isArray(rowArray)) {
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
const ids = [];
|
|
156
|
+
for (let i = 0; i < rowArray.length; i++) {
|
|
157
|
+
const id = rowArray[i]?.id;
|
|
158
|
+
if (id) ids.push(id);
|
|
159
|
+
}
|
|
160
|
+
return ids;
|
|
161
|
+
}
|
|
162
|
+
function createWatchIds(table, pathColumn) {
|
|
163
|
+
const sql = buildIdOnlyWatchQuery({
|
|
164
|
+
table,
|
|
165
|
+
pathColumn
|
|
166
|
+
});
|
|
167
|
+
return (db, onUpdate) => {
|
|
168
|
+
const abortController = new AbortController();
|
|
169
|
+
db.watch(sql, [], {
|
|
170
|
+
onResult: (results) => onUpdate(extractIdsFromRows(results))
|
|
171
|
+
}, {
|
|
172
|
+
signal: abortController.signal
|
|
173
|
+
});
|
|
174
|
+
return () => abortController.abort();
|
|
175
|
+
};
|
|
176
|
+
}
|
|
136
177
|
|
|
137
178
|
// src/attachments/migration.ts
|
|
138
179
|
import { AttachmentState } from "@powersync/attachments";
|
|
@@ -210,6 +251,8 @@ export {
|
|
|
210
251
|
buildIdOnlyWatchQuery,
|
|
211
252
|
buildRecordFetchQuery,
|
|
212
253
|
watchConfigToSourceConfig,
|
|
254
|
+
extractIdsFromRows,
|
|
255
|
+
createWatchIds,
|
|
213
256
|
STATE_MAPPING,
|
|
214
257
|
STATE_NAMES,
|
|
215
258
|
VALID_STATES,
|
|
@@ -227,4 +270,4 @@ export {
|
|
|
227
270
|
recordMigration,
|
|
228
271
|
formatMigrationStats
|
|
229
272
|
};
|
|
230
|
-
//# sourceMappingURL=chunk-
|
|
273
|
+
//# sourceMappingURL=chunk-C5ODS3XH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/attachments/query-builder.ts","../src/attachments/migration.ts"],"sourcesContent":["/**\n * Query Builder for Attachment Watch Queries\n *\n * Generates SQL queries from WatchConfig objects.\n * Provides type-safe query generation without raw SQL strings.\n */\n\nimport type { WatchConfig } from './types';\n\n// ─── SQL Identifier Validation ────────────────────────────────────────────────\n\n/**\n * Valid SQL identifier pattern.\n * Allows alphanumeric characters and underscores, must start with letter or underscore.\n */\nconst VALID_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;\n\n/**\n * SQL reserved words that cannot be used as identifiers (subset of common ones).\n */\nconst SQL_RESERVED_WORDS = new Set(['SELECT', 'FROM', 'WHERE', 'ORDER', 'BY', 'AND', 'OR', 'NOT', 'NULL', 'INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE', 'TABLE', 'INDEX', 'JOIN', 'LEFT', 'RIGHT', 'INNER', 'OUTER', 'ON', 'AS', 'DISTINCT', 'GROUP', 'HAVING', 'LIMIT', 'OFFSET', 'UNION', 'EXCEPT', 'INTERSECT', 'IN', 'BETWEEN', 'LIKE', 'IS', 'TRUE', 'FALSE', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'ASC', 'DESC', 'NULLS', 'FIRST', 'LAST']);\n\n/**\n * Validates that a string is a safe SQL identifier.\n * Throws an error if the identifier is invalid or potentially dangerous.\n *\n * @param identifier - The identifier to validate\n * @param context - Description of where this identifier is used (for error messages)\n * @throws Error if identifier is invalid\n */\nexport function validateSqlIdentifier(identifier: string, context: string): void {\n if (!identifier || typeof identifier !== 'string') {\n throw new Error(`Invalid ${context}: must be a non-empty string`);\n }\n if (!VALID_IDENTIFIER_PATTERN.test(identifier)) {\n throw new Error(`Invalid ${context}: \"${identifier}\" contains invalid characters. ` + `Identifiers must start with a letter or underscore and contain only alphanumeric characters and underscores.`);\n }\n if (SQL_RESERVED_WORDS.has(identifier.toUpperCase())) {\n throw new Error(`Invalid ${context}: \"${identifier}\" is a SQL reserved word. ` + `Use a different name or quote the identifier.`);\n }\n\n // Additional safety: check for SQL injection patterns\n const dangerousPatterns = [/--/,\n // SQL comment\n /;/,\n // Statement terminator\n /'/,\n // String delimiter\n /\"/,\n // Quote\n /\\\\/ // Escape character\n ];\n for (const pattern of dangerousPatterns) {\n if (pattern.test(identifier)) {\n throw new Error(`Invalid ${context}: \"${identifier}\" contains potentially dangerous characters.`);\n }\n }\n}\n\n/**\n * Validates a WHERE clause fragment for basic safety.\n * Note: This is a best-effort validation. Complex WHERE clauses should be reviewed.\n *\n * @param whereClause - The WHERE clause fragment to validate\n * @throws Error if the clause contains dangerous patterns\n */\nexport function validateWhereClause(whereClause: string): void {\n if (!whereClause || typeof whereClause !== 'string') {\n throw new Error('Invalid WHERE clause: must be a non-empty string');\n }\n\n // Check for dangerous patterns\n const dangerousPatterns = [{\n pattern: /;\\s*(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE)/i,\n name: 'SQL injection (statement)'\n }, {\n pattern: /UNION\\s+(ALL\\s+)?SELECT/i,\n name: 'UNION injection'\n }, {\n pattern: /--/,\n name: 'SQL comment'\n }, {\n pattern: /\\/\\*/,\n name: 'block comment'\n }, {\n pattern: /xp_|sp_|exec\\s*\\(/i,\n name: 'stored procedure'\n }];\n for (const {\n pattern,\n name\n } of dangerousPatterns) {\n if (pattern.test(whereClause)) {\n throw new Error(`Invalid WHERE clause: contains ${name}`);\n }\n }\n}\n\n// ─── Query Builder ────────────────────────────────────────────────────────────\n\n/**\n * Resolves the column name to use for the attachment path/ID.\n * Prefers `pathColumn` over `idColumn` when both are provided.\n *\n * @param config - The WatchConfig to resolve the column from\n * @returns The resolved column name\n * @throws Error if neither pathColumn nor idColumn is provided\n */\nfunction resolvePathColumn(config: WatchConfig): string {\n const column = config.pathColumn ?? config.idColumn;\n if (!column) {\n throw new Error('WatchConfig requires either pathColumn or idColumn. ' + 'pathColumn is preferred; idColumn is deprecated.');\n }\n return column;\n}\n\n/**\n * Build a SQL watch query from a WatchConfig.\n *\n * Generates a SELECT statement that:\n * - Selects the path column (aliased as `id`)\n * - Optionally selects additional columns\n * - Applies an optional WHERE clause\n * - Optionally orders by a column\n *\n * @param config - The WatchConfig to build a query from\n * @returns SQL query string\n *\n * @example\n * ```typescript\n * // Using pathColumn (preferred)\n * const query = buildWatchQuery({\n * table: 'EquipmentUnitMediaContent',\n * pathColumn: 'storagePath',\n * selectColumns: ['equipmentUnitId', 'takenOn'],\n * where: 'storagePath IS NOT NULL',\n * orderBy: { column: 'takenOn', direction: 'DESC' },\n * });\n *\n * // Result:\n * // SELECT storagePath AS id, equipmentUnitId, takenOn\n * // FROM EquipmentUnitMediaContent\n * // WHERE storagePath IS NOT NULL\n * // ORDER BY takenOn DESC\n *\n * // Using idColumn (deprecated, still works)\n * const query = buildWatchQuery({\n * table: 'EquipmentUnitMediaContent',\n * idColumn: 'storagePath',\n * });\n * ```\n */\nexport function buildWatchQuery(config: WatchConfig): string {\n // Resolve path column (pathColumn ?? idColumn)\n const pathColumn = resolvePathColumn(config);\n\n // Validate all identifiers\n validateSqlIdentifier(config.table, 'table');\n validateSqlIdentifier(pathColumn, 'pathColumn');\n if (config.selectColumns) {\n for (const col of config.selectColumns) {\n validateSqlIdentifier(col, 'selectColumns');\n }\n }\n if (config.orderBy) {\n validateSqlIdentifier(config.orderBy.column, 'orderBy.column');\n if (config.orderBy.direction !== 'ASC' && config.orderBy.direction !== 'DESC') {\n throw new Error(`Invalid orderBy.direction: must be \"ASC\" or \"DESC\"`);\n }\n }\n if (config.where) {\n validateWhereClause(config.where);\n }\n\n // Build SELECT clause\n const selectParts: string[] = [`${pathColumn} AS id`];\n if (config.selectColumns && config.selectColumns.length > 0) {\n selectParts.push(...config.selectColumns);\n }\n const selectClause = selectParts.join(', ');\n\n // Build FROM clause\n const fromClause = config.table;\n\n // Build WHERE clause\n let whereClause = `${pathColumn} IS NOT NULL AND ${pathColumn} != ''`;\n if (config.where) {\n whereClause = `${whereClause} AND (${config.where})`;\n }\n\n // Build ORDER BY clause\n let orderByClause = '';\n if (config.orderBy) {\n orderByClause = `ORDER BY ${config.orderBy.column} ${config.orderBy.direction}`;\n }\n\n // Assemble query\n const parts = [`SELECT ${selectClause}`, `FROM ${fromClause}`, `WHERE ${whereClause}`];\n if (orderByClause) {\n parts.push(orderByClause);\n }\n return parts.join('\\n');\n}\n\n/**\n * Build a simpler ID-only watch query.\n * Use this when you only need IDs without additional columns.\n *\n * @param config - The WatchConfig to build a query from\n * @returns SQL query string that selects only IDs\n *\n * @example\n * ```typescript\n * const query = buildIdOnlyWatchQuery({\n * table: 'EquipmentUnitMediaContent',\n * idColumn: 'storagePath',\n * where: 'storagePath IS NOT NULL',\n * });\n *\n * // Result:\n * // SELECT storagePath AS id\n * // FROM EquipmentUnitMediaContent\n * // WHERE storagePath IS NOT NULL AND storagePath != ''\n * ```\n */\nexport function buildIdOnlyWatchQuery(config: WatchConfig): string {\n // Use the full builder but ignore selectColumns\n return buildWatchQuery({\n ...config,\n selectColumns: undefined\n });\n}\n\n/**\n * Build a query to fetch records with their IDs and additional columns.\n * Used for populating the BatchFilterContext.records map.\n *\n * @param config - The WatchConfig to build a query from\n * @param ids - Optional list of IDs to filter to (for efficiency)\n * @returns SQL query string and parameters\n *\n * @example\n * ```typescript\n * const { query, params } = buildRecordFetchQuery(\n * {\n * table: 'EquipmentUnitMediaContent',\n * pathColumn: 'storagePath',\n * selectColumns: ['equipmentUnitId'],\n * },\n * ['path/to/file1.jpg', 'path/to/file2.jpg']\n * );\n * ```\n */\nexport function buildRecordFetchQuery(config: WatchConfig, ids?: string[]): {\n query: string;\n params: unknown[];\n} {\n // Resolve path column (pathColumn ?? idColumn)\n const pathColumn = resolvePathColumn(config);\n\n // Validate identifiers\n validateSqlIdentifier(config.table, 'table');\n validateSqlIdentifier(pathColumn, 'pathColumn');\n if (config.selectColumns) {\n for (const col of config.selectColumns) {\n validateSqlIdentifier(col, 'selectColumns');\n }\n }\n\n // Build SELECT clause - always include the path column\n const selectParts: string[] = [`${pathColumn} AS id`];\n if (config.selectColumns && config.selectColumns.length > 0) {\n selectParts.push(...config.selectColumns);\n }\n const selectClause = selectParts.join(', ');\n\n // Build query\n let query = `SELECT ${selectClause} FROM ${config.table}`;\n const params: unknown[] = [];\n\n // Add WHERE clause for IDs if provided\n if (ids && ids.length > 0) {\n const placeholders = ids.map(() => '?').join(', ');\n query += ` WHERE ${pathColumn} IN (${placeholders})`;\n params.push(...ids);\n }\n return {\n query,\n params\n };\n}\n\n/**\n * Convert WatchConfig to a legacy format for backwards compatibility.\n *\n * @param watchConfig - The WatchConfig to convert\n * @returns Object with table, pathColumn, and optional orderByColumn\n */\nexport function watchConfigToSourceConfig(watchConfig: WatchConfig): {\n table: string;\n pathColumn: string;\n /** @deprecated Use pathColumn instead */\n idColumn: string;\n orderByColumn?: string | null;\n} {\n const pathColumn = resolvePathColumn(watchConfig);\n return {\n table: watchConfig.table,\n pathColumn,\n idColumn: pathColumn,\n // For backwards compatibility\n orderByColumn: watchConfig.orderBy?.column ?? null\n };\n}\n\n// ─── Row Extraction Utility ────────────────────────────────────────────────────\n\n/**\n * Extracts an array of string IDs from PowerSync watch results.\n * Handles platform differences between React Native (`rows._array`) and web (`rows` as array).\n * Uses single-pass extraction for efficiency.\n *\n * @param results - The results object from db.watch() onResult callback\n * @returns Array of non-null string IDs\n */\nexport function extractIdsFromRows(results: {\n rows?: unknown;\n}): string[] {\n const rows = results.rows;\n if (!rows) return [];\n // React Native: { _array: Row[] }, Web: Row[]\n const rowArray = (rows as {\n _array?: unknown[];\n })?._array ?? rows;\n if (!Array.isArray(rowArray)) {\n return [];\n }\n\n // Single-pass extraction (more efficient than .map().filter())\n const ids: string[] = [];\n for (let i = 0; i < rowArray.length; i++) {\n const id = (rowArray[i] as {\n id?: string;\n })?.id;\n if (id) ids.push(id);\n }\n return ids;\n}\n\n// ─── Helper for Simple Attachment Configuration ────────────────────────────────\n\n/**\n * Creates a watchIds callback for simple single-table attachment configurations.\n *\n * This is a convenience function that generates a properly typed watchIds callback\n * from a table name and path column. Use this for simple cases where you don't need\n * JOINs or complex watch logic.\n *\n * @param table - The source table name\n * @param pathColumn - The column containing the attachment path/ID\n * @returns A properly typed watchIds callback for use in AttachmentSourceConfig\n *\n * @example\n * ```typescript\n * import { createWatchIds } from '@pol-studios/powersync/attachments';\n *\n * const config: AttachmentSourceConfig = {\n * bucket: 'photos',\n * watchIds: createWatchIds('EquipmentUnitMediaContent', 'storagePath'),\n * };\n * ```\n */\nexport function createWatchIds(table: string, pathColumn: string): (db: import('./types').PowerSyncDBInterface, onUpdate: (ids: string[]) => void) => () => void {\n // Validate identifiers and generate SQL at creation time (not on every callback)\n const sql = buildIdOnlyWatchQuery({\n table,\n pathColumn\n });\n return (db, onUpdate) => {\n const abortController = new AbortController();\n db.watch(sql, [], {\n onResult: results => onUpdate(extractIdsFromRows(results))\n }, {\n signal: abortController.signal\n });\n return () => abortController.abort();\n };\n}","/**\n * Migration Utilities for @pol-studios/powersync Attachments\n *\n * This module provides utilities for migrating from the old attachment API\n * to the new callback-based API. It includes:\n *\n * - State mapping constants for old → new state transitions\n * - `migrateAttachmentState()` for converting state values\n * - Validation helpers for migration safety\n *\n * @example\n * ```typescript\n * import { migrateAttachmentState, isValidAttachmentState } from '@pol-studios/powersync/attachments';\n *\n * // Migrate a single state value\n * const newState = migrateAttachmentState(oldState);\n *\n * // Validate before migration\n * if (isValidAttachmentState(value)) {\n * const migrated = migrateAttachmentState(value);\n * }\n * ```\n */\n\nimport { AttachmentState } from '@powersync/attachments';\nimport { PolAttachmentState } from './types';\n\n// ─── State Mapping Constants ──────────────────────────────────────────────────\n\n/**\n * Maps old state values to new state values.\n *\n * The official @powersync/attachments AttachmentState enum has values 0-4:\n * QUEUED_SYNC=0, QUEUED_UPLOAD=1, QUEUED_DOWNLOAD=2, SYNCED=3, ARCHIVED=4\n *\n * POL extensions add:\n * FAILED_PERMANENT=5, DOWNLOAD_SKIPPED=6\n *\n * For migration purposes, most states map 1:1. The mapping exists to:\n * 1. Document the relationship between old and new states\n * 2. Provide a clear upgrade path for custom state handling code\n * 3. Allow future state reorganization if needed\n */\nexport const STATE_MAPPING: ReadonlyMap<number, number> = new Map<number, number>([\n// Official states (1:1 mapping)\n[AttachmentState.QUEUED_SYNC as number, PolAttachmentState.QUEUED_SYNC], [AttachmentState.QUEUED_UPLOAD as number, PolAttachmentState.QUEUED_UPLOAD], [AttachmentState.QUEUED_DOWNLOAD as number, PolAttachmentState.QUEUED_DOWNLOAD], [AttachmentState.SYNCED as number, PolAttachmentState.SYNCED], [AttachmentState.ARCHIVED as number, PolAttachmentState.ARCHIVED],\n// POL extension states (identity mapping)\n[PolAttachmentState.FAILED_PERMANENT, PolAttachmentState.FAILED_PERMANENT], [PolAttachmentState.DOWNLOAD_SKIPPED, PolAttachmentState.DOWNLOAD_SKIPPED]]);\n\n/**\n * Human-readable names for attachment states.\n * Useful for logging and debugging during migration.\n */\nexport const STATE_NAMES: ReadonlyMap<number, string> = new Map([[PolAttachmentState.QUEUED_SYNC, 'QUEUED_SYNC'], [PolAttachmentState.QUEUED_UPLOAD, 'QUEUED_UPLOAD'], [PolAttachmentState.QUEUED_DOWNLOAD, 'QUEUED_DOWNLOAD'], [PolAttachmentState.SYNCED, 'SYNCED'], [PolAttachmentState.ARCHIVED, 'ARCHIVED'], [PolAttachmentState.FAILED_PERMANENT, 'FAILED_PERMANENT'], [PolAttachmentState.DOWNLOAD_SKIPPED, 'DOWNLOAD_SKIPPED']]);\n\n/**\n * All valid state values (official + POL extensions).\n */\nexport const VALID_STATES: ReadonlySet<number> = new Set([PolAttachmentState.QUEUED_SYNC, PolAttachmentState.QUEUED_UPLOAD, PolAttachmentState.QUEUED_DOWNLOAD, PolAttachmentState.SYNCED, PolAttachmentState.ARCHIVED, PolAttachmentState.FAILED_PERMANENT, PolAttachmentState.DOWNLOAD_SKIPPED]);\n\n/**\n * States that indicate an active upload workflow.\n * Records in these states should not be migrated to download states.\n */\nexport const UPLOAD_WORKFLOW_STATES: ReadonlySet<number> = new Set([PolAttachmentState.QUEUED_UPLOAD, PolAttachmentState.FAILED_PERMANENT]);\n\n/**\n * States that indicate an active download workflow.\n */\nexport const DOWNLOAD_WORKFLOW_STATES: ReadonlySet<number> = new Set([PolAttachmentState.QUEUED_DOWNLOAD, PolAttachmentState.QUEUED_SYNC]);\n\n/**\n * Terminal states (no further processing needed).\n */\nexport const TERMINAL_STATES: ReadonlySet<number> = new Set([PolAttachmentState.SYNCED, PolAttachmentState.ARCHIVED, PolAttachmentState.DOWNLOAD_SKIPPED]);\n\n// ─── Migration Functions ──────────────────────────────────────────────────────\n\n/**\n * Migrates an attachment state from the old API to the new API.\n *\n * Currently, this is a 1:1 mapping since the state values haven't changed.\n * This function exists to:\n * 1. Provide a clear migration path for apps using custom state handling\n * 2. Document the state relationship\n * 3. Allow future state reorganization without breaking existing code\n *\n * @param oldState - The state value from the old API\n * @returns The corresponding state value in the new API\n * @throws Error if the state value is invalid\n *\n * @example\n * ```typescript\n * import { migrateAttachmentState } from '@pol-studios/powersync/attachments';\n *\n * // Migrate a record's state\n * const newState = migrateAttachmentState(record.state);\n *\n * // Migrate with fallback for unknown states\n * const safeState = isValidAttachmentState(record.state)\n * ? migrateAttachmentState(record.state)\n * : PolAttachmentState.QUEUED_SYNC;\n * ```\n */\nexport function migrateAttachmentState(oldState: number): number {\n const newState = STATE_MAPPING.get(oldState);\n if (newState === undefined) {\n throw new Error(`Invalid attachment state: ${oldState}. ` + `Valid states are: ${Array.from(STATE_NAMES.entries()).map(([v, n]) => `${n}(${v})`).join(', ')}`);\n }\n return newState;\n}\n\n/**\n * Safely migrates an attachment state with a fallback.\n *\n * Unlike `migrateAttachmentState`, this function never throws.\n * Invalid states are mapped to the provided fallback.\n *\n * @param oldState - The state value from the old API\n * @param fallback - State to use if oldState is invalid (default: QUEUED_SYNC)\n * @returns The corresponding state value in the new API, or the fallback\n *\n * @example\n * ```typescript\n * // Safely migrate with QUEUED_SYNC as fallback\n * const state = migrateAttachmentStateSafe(unknownValue);\n *\n * // Use custom fallback\n * const state = migrateAttachmentStateSafe(unknownValue, PolAttachmentState.ARCHIVED);\n * ```\n */\nexport function migrateAttachmentStateSafe(oldState: number, fallback: number = PolAttachmentState.QUEUED_SYNC): number {\n const newState = STATE_MAPPING.get(oldState);\n return newState !== undefined ? newState : fallback;\n}\n\n// ─── Validation Helpers ───────────────────────────────────────────────────────\n\n/**\n * Checks if a value is a valid attachment state.\n *\n * @param value - The value to check\n * @returns true if the value is a valid attachment state\n *\n * @example\n * ```typescript\n * if (isValidAttachmentState(record.state)) {\n * // Safe to use record.state\n * } else {\n * console.warn(`Invalid state: ${record.state}`);\n * }\n * ```\n */\nexport function isValidAttachmentState(value: unknown): value is number {\n return typeof value === 'number' && VALID_STATES.has(value);\n}\n\n/**\n * Checks if a state represents an upload workflow.\n *\n * Records in upload workflow states should not be demoted to download states.\n *\n * @param state - The state to check\n * @returns true if the state is part of an upload workflow\n */\nexport function isUploadWorkflowState(state: number): boolean {\n return UPLOAD_WORKFLOW_STATES.has(state);\n}\n\n/**\n * Checks if a state represents a download workflow.\n *\n * @param state - The state to check\n * @returns true if the state is part of a download workflow\n */\nexport function isDownloadWorkflowState(state: number): boolean {\n return DOWNLOAD_WORKFLOW_STATES.has(state);\n}\n\n/**\n * Checks if a state is terminal (no further processing needed).\n *\n * @param state - The state to check\n * @returns true if the state is terminal\n */\nexport function isTerminalState(state: number): boolean {\n return TERMINAL_STATES.has(state);\n}\n\n/**\n * Gets the human-readable name of a state.\n *\n * @param state - The state value\n * @returns The state name, or \"UNKNOWN\" for invalid states\n *\n * @example\n * ```typescript\n * console.log(`State: ${getStateName(record.state)}`); // \"State: SYNCED\"\n * ```\n */\nexport function getStateName(state: number): string {\n return STATE_NAMES.get(state) ?? 'UNKNOWN';\n}\n\n// ─── Migration Report ─────────────────────────────────────────────────────────\n\n/**\n * Statistics about a batch migration.\n */\nexport interface MigrationStats {\n /** Total records processed */\n total: number;\n /** Records successfully migrated */\n migrated: number;\n /** Records with invalid states (used fallback) */\n invalid: number;\n /** Breakdown by state */\n byState: Map<number, number>;\n}\n\n/**\n * Creates empty migration stats.\n */\nexport function createMigrationStats(): MigrationStats {\n return {\n total: 0,\n migrated: 0,\n invalid: 0,\n byState: new Map()\n };\n}\n\n/**\n * Records a migration result in the stats.\n *\n * @param stats - The stats object to update\n * @param oldState - The original state\n * @param newState - The migrated state\n * @param wasValid - Whether the original state was valid\n */\nexport function recordMigration(stats: MigrationStats, oldState: number, newState: number, wasValid: boolean): void {\n stats.total++;\n if (wasValid) {\n stats.migrated++;\n } else {\n stats.invalid++;\n }\n stats.byState.set(newState, (stats.byState.get(newState) ?? 0) + 1);\n}\n\n/**\n * Formats migration stats as a human-readable summary.\n *\n * @param stats - The stats to format\n * @returns A formatted string summary\n *\n * @example\n * ```typescript\n * const stats = createMigrationStats();\n * // ... process records ...\n * console.log(formatMigrationStats(stats));\n * // Output:\n * // Migration Summary:\n * // Total: 100\n * // Migrated: 98\n * // Invalid (used fallback): 2\n * // By State:\n * // SYNCED: 50\n * // QUEUED_DOWNLOAD: 30\n * // QUEUED_UPLOAD: 18\n * // QUEUED_SYNC: 2\n * ```\n */\nexport function formatMigrationStats(stats: MigrationStats): string {\n const lines = ['Migration Summary:', ` Total: ${stats.total}`, ` Migrated: ${stats.migrated}`, ` Invalid (used fallback): ${stats.invalid}`, ' By State:'];\n for (const [state, count] of stats.byState.entries()) {\n lines.push(` ${getStateName(state)}: ${count}`);\n }\n return lines.join('\\n');\n}"],"mappings":";AAeA,IAAM,2BAA2B;AAKjC,IAAM,qBAAqB,oBAAI,IAAI,CAAC,UAAU,QAAQ,SAAS,SAAS,MAAM,OAAO,MAAM,OAAO,QAAQ,UAAU,UAAU,UAAU,QAAQ,UAAU,SAAS,SAAS,QAAQ,QAAQ,SAAS,SAAS,SAAS,MAAM,MAAM,YAAY,SAAS,UAAU,SAAS,UAAU,SAAS,UAAU,aAAa,MAAM,WAAW,QAAQ,MAAM,QAAQ,SAAS,QAAQ,QAAQ,QAAQ,QAAQ,OAAO,OAAO,QAAQ,SAAS,SAAS,MAAM,CAAC;AAU7a,SAAS,sBAAsB,YAAoB,SAAuB;AAC/E,MAAI,CAAC,cAAc,OAAO,eAAe,UAAU;AACjD,UAAM,IAAI,MAAM,WAAW,OAAO,8BAA8B;AAAA,EAClE;AACA,MAAI,CAAC,yBAAyB,KAAK,UAAU,GAAG;AAC9C,UAAM,IAAI,MAAM,WAAW,OAAO,MAAM,UAAU,6IAAkJ;AAAA,EACtM;AACA,MAAI,mBAAmB,IAAI,WAAW,YAAY,CAAC,GAAG;AACpD,UAAM,IAAI,MAAM,WAAW,OAAO,MAAM,UAAU,yEAA8E;AAAA,EAClI;AAGA,QAAM,oBAAoB;AAAA,IAAC;AAAA;AAAA,IAE3B;AAAA;AAAA,IAEA;AAAA;AAAA,IAEA;AAAA;AAAA,IAEA;AAAA;AAAA,EACA;AACA,aAAW,WAAW,mBAAmB;AACvC,QAAI,QAAQ,KAAK,UAAU,GAAG;AAC5B,YAAM,IAAI,MAAM,WAAW,OAAO,MAAM,UAAU,8CAA8C;AAAA,IAClG;AAAA,EACF;AACF;AASO,SAAS,oBAAoB,aAA2B;AAC7D,MAAI,CAAC,eAAe,OAAO,gBAAgB,UAAU;AACnD,UAAM,IAAI,MAAM,kDAAkD;AAAA,EACpE;AAGA,QAAM,oBAAoB,CAAC;AAAA,IACzB,SAAS;AAAA,IACT,MAAM;AAAA,EACR,GAAG;AAAA,IACD,SAAS;AAAA,IACT,MAAM;AAAA,EACR,GAAG;AAAA,IACD,SAAS;AAAA,IACT,MAAM;AAAA,EACR,GAAG;AAAA,IACD,SAAS;AAAA,IACT,MAAM;AAAA,EACR,GAAG;AAAA,IACD,SAAS;AAAA,IACT,MAAM;AAAA,EACR,CAAC;AACD,aAAW;AAAA,IACT;AAAA,IACA;AAAA,EACF,KAAK,mBAAmB;AACtB,QAAI,QAAQ,KAAK,WAAW,GAAG;AAC7B,YAAM,IAAI,MAAM,kCAAkC,IAAI,EAAE;AAAA,IAC1D;AAAA,EACF;AACF;AAYA,SAAS,kBAAkB,QAA6B;AACtD,QAAM,SAAS,OAAO,cAAc,OAAO;AAC3C,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,MAAM,sGAA2G;AAAA,EAC7H;AACA,SAAO;AACT;AAsCO,SAAS,gBAAgB,QAA6B;AAE3D,QAAM,aAAa,kBAAkB,MAAM;AAG3C,wBAAsB,OAAO,OAAO,OAAO;AAC3C,wBAAsB,YAAY,YAAY;AAC9C,MAAI,OAAO,eAAe;AACxB,eAAW,OAAO,OAAO,eAAe;AACtC,4BAAsB,KAAK,eAAe;AAAA,IAC5C;AAAA,EACF;AACA,MAAI,OAAO,SAAS;AAClB,0BAAsB,OAAO,QAAQ,QAAQ,gBAAgB;AAC7D,QAAI,OAAO,QAAQ,cAAc,SAAS,OAAO,QAAQ,cAAc,QAAQ;AAC7E,YAAM,IAAI,MAAM,oDAAoD;AAAA,IACtE;AAAA,EACF;AACA,MAAI,OAAO,OAAO;AAChB,wBAAoB,OAAO,KAAK;AAAA,EAClC;AAGA,QAAM,cAAwB,CAAC,GAAG,UAAU,QAAQ;AACpD,MAAI,OAAO,iBAAiB,OAAO,cAAc,SAAS,GAAG;AAC3D,gBAAY,KAAK,GAAG,OAAO,aAAa;AAAA,EAC1C;AACA,QAAM,eAAe,YAAY,KAAK,IAAI;AAG1C,QAAM,aAAa,OAAO;AAG1B,MAAI,cAAc,GAAG,UAAU,oBAAoB,UAAU;AAC7D,MAAI,OAAO,OAAO;AAChB,kBAAc,GAAG,WAAW,SAAS,OAAO,KAAK;AAAA,EACnD;AAGA,MAAI,gBAAgB;AACpB,MAAI,OAAO,SAAS;AAClB,oBAAgB,YAAY,OAAO,QAAQ,MAAM,IAAI,OAAO,QAAQ,SAAS;AAAA,EAC/E;AAGA,QAAM,QAAQ,CAAC,UAAU,YAAY,IAAI,QAAQ,UAAU,IAAI,SAAS,WAAW,EAAE;AACrF,MAAI,eAAe;AACjB,UAAM,KAAK,aAAa;AAAA,EAC1B;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;AAuBO,SAAS,sBAAsB,QAA6B;AAEjE,SAAO,gBAAgB;AAAA,IACrB,GAAG;AAAA,IACH,eAAe;AAAA,EACjB,CAAC;AACH;AAsBO,SAAS,sBAAsB,QAAqB,KAGzD;AAEA,QAAM,aAAa,kBAAkB,MAAM;AAG3C,wBAAsB,OAAO,OAAO,OAAO;AAC3C,wBAAsB,YAAY,YAAY;AAC9C,MAAI,OAAO,eAAe;AACxB,eAAW,OAAO,OAAO,eAAe;AACtC,4BAAsB,KAAK,eAAe;AAAA,IAC5C;AAAA,EACF;AAGA,QAAM,cAAwB,CAAC,GAAG,UAAU,QAAQ;AACpD,MAAI,OAAO,iBAAiB,OAAO,cAAc,SAAS,GAAG;AAC3D,gBAAY,KAAK,GAAG,OAAO,aAAa;AAAA,EAC1C;AACA,QAAM,eAAe,YAAY,KAAK,IAAI;AAG1C,MAAI,QAAQ,UAAU,YAAY,SAAS,OAAO,KAAK;AACvD,QAAM,SAAoB,CAAC;AAG3B,MAAI,OAAO,IAAI,SAAS,GAAG;AACzB,UAAM,eAAe,IAAI,IAAI,MAAM,GAAG,EAAE,KAAK,IAAI;AACjD,aAAS,UAAU,UAAU,QAAQ,YAAY;AACjD,WAAO,KAAK,GAAG,GAAG;AAAA,EACpB;AACA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,EACF;AACF;AAQO,SAAS,0BAA0B,aAMxC;AACA,QAAM,aAAa,kBAAkB,WAAW;AAChD,SAAO;AAAA,IACL,OAAO,YAAY;AAAA,IACnB;AAAA,IACA,UAAU;AAAA;AAAA,IAEV,eAAe,YAAY,SAAS,UAAU;AAAA,EAChD;AACF;AAYO,SAAS,mBAAmB,SAEtB;AACX,QAAM,OAAO,QAAQ;AACrB,MAAI,CAAC,KAAM,QAAO,CAAC;AAEnB,QAAM,WAAY,MAEd,UAAU;AACd,MAAI,CAAC,MAAM,QAAQ,QAAQ,GAAG;AAC5B,WAAO,CAAC;AAAA,EACV;AAGA,QAAM,MAAgB,CAAC;AACvB,WAAS,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;AACxC,UAAM,KAAM,SAAS,CAAC,GAElB;AACJ,QAAI,GAAI,KAAI,KAAK,EAAE;AAAA,EACrB;AACA,SAAO;AACT;AAyBO,SAAS,eAAe,OAAe,YAAmH;AAE/J,QAAM,MAAM,sBAAsB;AAAA,IAChC;AAAA,IACA;AAAA,EACF,CAAC;AACD,SAAO,CAAC,IAAI,aAAa;AACvB,UAAM,kBAAkB,IAAI,gBAAgB;AAC5C,OAAG,MAAM,KAAK,CAAC,GAAG;AAAA,MAChB,UAAU,aAAW,SAAS,mBAAmB,OAAO,CAAC;AAAA,IAC3D,GAAG;AAAA,MACD,QAAQ,gBAAgB;AAAA,IAC1B,CAAC;AACD,WAAO,MAAM,gBAAgB,MAAM;AAAA,EACrC;AACF;;;AC3WA,SAAS,uBAAuB;AAmBzB,IAAM,gBAA6C,oBAAI,IAAoB;AAAA;AAAA,EAElF,CAAC,gBAAgB,gCAAqD;AAAA,EAAG,CAAC,gBAAgB,oCAAyD;AAAA,EAAG,CAAC,gBAAgB,wCAA6D;AAAA,EAAG,CAAC,gBAAgB,sBAA2C;AAAA,EAAG,CAAC,gBAAgB,0BAA+C;AAAA;AAAA,EAEtW,mDAAyE;AAAA,EAAG,mDAAyE;AAAC,CAAC;AAMhJ,IAAM,cAA2C,oBAAI,IAAI,CAAC,sBAAiC,aAAa,GAAG,wBAAmC,eAAe,GAAG,0BAAqC,iBAAiB,GAAG,iBAA4B,QAAQ,GAAG,mBAA8B,UAAU,GAAG,2BAAsC,kBAAkB,GAAG,2BAAsC,kBAAkB,CAAC,CAAC;AAKha,IAAM,eAAoC,oBAAI,IAAI,0JAAuO,CAAC;AAM1R,IAAM,yBAA8C,oBAAI,IAAI,gDAAsE,CAAC;AAKnI,IAAM,2BAAgD,oBAAI,IAAI,6CAAmE,CAAC;AAKlI,IAAM,kBAAuC,oBAAI,IAAI,2DAA4F,CAAC;AA8BlJ,SAAS,uBAAuB,UAA0B;AAC/D,QAAM,WAAW,cAAc,IAAI,QAAQ;AAC3C,MAAI,aAAa,QAAW;AAC1B,UAAM,IAAI,MAAM,6BAA6B,QAAQ,uBAA4B,MAAM,KAAK,YAAY,QAAQ,CAAC,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,IAAI,CAAC,EAAE;AAAA,EAC/J;AACA,SAAO;AACT;AAqBO,SAAS,2BAA2B,UAAkB,gCAA2D;AACtH,QAAM,WAAW,cAAc,IAAI,QAAQ;AAC3C,SAAO,aAAa,SAAY,WAAW;AAC7C;AAmBO,SAAS,uBAAuB,OAAiC;AACtE,SAAO,OAAO,UAAU,YAAY,aAAa,IAAI,KAAK;AAC5D;AAUO,SAAS,sBAAsB,OAAwB;AAC5D,SAAO,uBAAuB,IAAI,KAAK;AACzC;AAQO,SAAS,wBAAwB,OAAwB;AAC9D,SAAO,yBAAyB,IAAI,KAAK;AAC3C;AAQO,SAAS,gBAAgB,OAAwB;AACtD,SAAO,gBAAgB,IAAI,KAAK;AAClC;AAaO,SAAS,aAAa,OAAuB;AAClD,SAAO,YAAY,IAAI,KAAK,KAAK;AACnC;AAqBO,SAAS,uBAAuC;AACrD,SAAO;AAAA,IACL,OAAO;AAAA,IACP,UAAU;AAAA,IACV,SAAS;AAAA,IACT,SAAS,oBAAI,IAAI;AAAA,EACnB;AACF;AAUO,SAAS,gBAAgB,OAAuB,UAAkB,UAAkB,UAAyB;AAClH,QAAM;AACN,MAAI,UAAU;AACZ,UAAM;AAAA,EACR,OAAO;AACL,UAAM;AAAA,EACR;AACA,QAAM,QAAQ,IAAI,WAAW,MAAM,QAAQ,IAAI,QAAQ,KAAK,KAAK,CAAC;AACpE;AAyBO,SAAS,qBAAqB,OAA+B;AAClE,QAAM,QAAQ,CAAC,sBAAsB,YAAY,MAAM,KAAK,IAAI,eAAe,MAAM,QAAQ,IAAI,8BAA8B,MAAM,OAAO,IAAI,aAAa;AAC7J,aAAW,CAAC,OAAO,KAAK,KAAK,MAAM,QAAQ,QAAQ,GAAG;AACpD,UAAM,KAAK,OAAO,aAAa,KAAK,CAAC,KAAK,KAAK,EAAE;AAAA,EACnD;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;","names":[]}
|
|
@@ -1244,8 +1244,9 @@ var PolAttachmentQueue = class extends AbstractAttachmentQueue2 {
|
|
|
1244
1244
|
}
|
|
1245
1245
|
}
|
|
1246
1246
|
const attachmentsInDatabase = await this.powersync.getAll(`SELECT * FROM ${this.table} WHERE state < ${AttachmentState6.ARCHIVED}`);
|
|
1247
|
+
const attachmentsMap = new Map(attachmentsInDatabase.map((r) => [r.id, r]));
|
|
1247
1248
|
for (const id of filteredIds) {
|
|
1248
|
-
const record =
|
|
1249
|
+
const record = attachmentsMap.get(id);
|
|
1249
1250
|
if (!record) {
|
|
1250
1251
|
const newRecord = await this.newAttachmentRecord({
|
|
1251
1252
|
id,
|
|
@@ -2057,4 +2058,4 @@ export {
|
|
|
2057
2058
|
PolAttachmentQueue,
|
|
2058
2059
|
createPolAttachmentQueue
|
|
2059
2060
|
};
|
|
2060
|
-
//# sourceMappingURL=chunk-
|
|
2061
|
+
//# sourceMappingURL=chunk-CACKC6XG.js.map
|