@pol-studios/powersync 1.0.25 → 1.0.32
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/README.md +0 -1
- package/dist/{CacheSettingsManager-uz-kbnRH.d.ts → CacheSettingsManager-0H_7thHW.d.ts} +21 -3
- package/dist/attachments/index.d.ts +30 -30
- package/dist/attachments/index.js +13 -4
- package/dist/{background-sync-ChCXW-EV.d.ts → background-sync-BujnI3IR.d.ts} +1 -1
- package/dist/{chunk-55DKCJV4.js → chunk-2RDWLXJW.js} +408 -78
- package/dist/chunk-2RDWLXJW.js.map +1 -0
- package/dist/{chunk-P4HZA6ZT.js → chunk-4665ZSE5.js} +2 -2
- package/dist/chunk-4665ZSE5.js.map +1 -0
- package/dist/{chunk-XOY2CJ67.js → chunk-4F5B5CZ7.js} +3 -3
- package/dist/chunk-5WRI5ZAA.js +31 -0
- package/dist/{chunk-BGBQYQV3.js → chunk-65A3SYJZ.js} +193 -299
- package/dist/chunk-65A3SYJZ.js.map +1 -0
- package/dist/chunk-6SZ64KCZ.js +755 -0
- package/dist/chunk-6SZ64KCZ.js.map +1 -0
- package/dist/{chunk-YSTEESEG.js → chunk-74TBHWJ4.js} +122 -11
- package/dist/chunk-74TBHWJ4.js.map +1 -0
- package/dist/chunk-ANXWYQEJ.js +1 -0
- package/dist/chunk-ANXWYQEJ.js.map +1 -0
- package/dist/{chunk-CAB26E6F.js → chunk-C4J4MLER.js} +29 -24
- package/dist/chunk-C4J4MLER.js.map +1 -0
- package/dist/{chunk-C5ODS3XH.js → chunk-EOW7JK7Q.js} +9 -16
- package/dist/chunk-EOW7JK7Q.js.map +1 -0
- package/dist/chunk-HRAVPIAZ.js +220 -0
- package/dist/chunk-HRAVPIAZ.js.map +1 -0
- package/dist/{chunk-XAEII4ZX.js → chunk-NUGQOTEM.js} +32 -4
- package/dist/chunk-NUGQOTEM.js.map +1 -0
- package/dist/chunk-OGUFUZSY.js +5415 -0
- package/dist/chunk-OGUFUZSY.js.map +1 -0
- package/dist/{chunk-VB737IVN.js → chunk-P4D6BQ4X.js} +328 -706
- package/dist/chunk-P4D6BQ4X.js.map +1 -0
- package/dist/{chunk-CACKC6XG.js → chunk-PGEDE6IM.js} +136 -89
- package/dist/chunk-PGEDE6IM.js.map +1 -0
- package/dist/{chunk-A4IBBWGO.js → chunk-RALHHPTU.js} +1 -1
- package/dist/chunk-RIDSPLE5.js +42 -0
- package/dist/chunk-RIDSPLE5.js.map +1 -0
- package/dist/{chunk-Z6VOBGTU.js → chunk-UOMHWUHV.js} +2 -12
- package/dist/chunk-UOMHWUHV.js.map +1 -0
- package/dist/{chunk-WGHNIAF7.js → chunk-YONQYTVH.js} +2 -2
- package/dist/chunk-ZAN22NGL.js +13 -0
- package/dist/chunk-ZAN22NGL.js.map +1 -0
- package/dist/config/index.d.ts +200 -0
- package/dist/config/index.js +23 -0
- package/dist/config/index.js.map +1 -0
- package/dist/connector/index.d.ts +23 -5
- package/dist/connector/index.js +4 -2
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +1 -0
- package/dist/error/index.js +1 -0
- package/dist/generator/index.js +2 -0
- package/dist/generator/index.js.map +1 -1
- package/dist/index.d.ts +19 -16
- package/dist/index.js +88 -46
- package/dist/index.native.d.ts +18 -14
- package/dist/index.native.js +93 -44
- package/dist/index.web.d.ts +17 -14
- package/dist/index.web.js +88 -46
- package/dist/maintenance/index.d.ts +2 -2
- package/dist/maintenance/index.js +3 -2
- package/dist/platform/index.d.ts +1 -1
- package/dist/platform/index.js +2 -0
- package/dist/platform/index.js.map +1 -1
- package/dist/platform/index.native.d.ts +1 -1
- package/dist/platform/index.native.js +1 -0
- package/dist/platform/index.web.d.ts +1 -1
- package/dist/platform/index.web.js +1 -0
- package/dist/pol-attachment-queue-DqBvLAEY.d.ts +255 -0
- package/dist/provider/index.d.ts +319 -124
- package/dist/provider/index.js +21 -16
- package/dist/provider/index.native.d.ts +108 -0
- package/dist/provider/index.native.js +121 -0
- package/dist/provider/index.native.js.map +1 -0
- package/dist/provider/index.web.d.ts +16 -0
- package/dist/provider/index.web.js +112 -0
- package/dist/provider/index.web.js.map +1 -0
- package/dist/react/index.d.ts +16 -65
- package/dist/react/index.js +2 -9
- package/dist/storage/index.d.ts +5 -4
- package/dist/storage/index.js +12 -9
- package/dist/storage/index.native.d.ts +5 -4
- package/dist/storage/index.native.js +8 -5
- package/dist/storage/index.web.d.ts +5 -4
- package/dist/storage/index.web.js +11 -8
- package/dist/storage/upload/index.d.ts +4 -3
- package/dist/storage/upload/index.js +4 -2
- package/dist/storage/upload/index.native.d.ts +4 -3
- package/dist/storage/upload/index.native.js +4 -2
- package/dist/storage/upload/index.web.d.ts +2 -1
- package/dist/storage/upload/index.web.js +4 -2
- package/dist/{supabase-connector-D2oIl2t8.d.ts → supabase-connector-HMxBA9Kg.d.ts} +23 -25
- package/dist/sync/index.d.ts +183 -11
- package/dist/sync/index.js +13 -3
- package/dist/{types-CyvBaAl8.d.ts → types-6QHGELuY.d.ts} +4 -1
- package/dist/{types-CDqWh56B.d.ts → types-B9MptP7E.d.ts} +13 -1
- package/dist/types-BhAEsJj-.d.ts +330 -0
- package/dist/{types-D0WcHrq6.d.ts → types-CGMibJKD.d.ts} +8 -0
- package/dist/{types-DiBvmGEi.d.ts → types-DqJnP50o.d.ts} +22 -24
- package/dist/{pol-attachment-queue-BE2HU3Us.d.ts → types-JCEhw2Lf.d.ts} +139 -346
- package/package.json +18 -4
- package/dist/chunk-24RDMMCL.js +0 -44
- package/dist/chunk-24RDMMCL.js.map +0 -1
- package/dist/chunk-55DKCJV4.js.map +0 -1
- package/dist/chunk-654ERHA7.js +0 -1
- package/dist/chunk-BGBQYQV3.js.map +0 -1
- package/dist/chunk-C5ODS3XH.js.map +0 -1
- package/dist/chunk-CAB26E6F.js.map +0 -1
- package/dist/chunk-CACKC6XG.js.map +0 -1
- package/dist/chunk-P4HZA6ZT.js.map +0 -1
- package/dist/chunk-TIFL2KWE.js +0 -358
- package/dist/chunk-TIFL2KWE.js.map +0 -1
- package/dist/chunk-VB737IVN.js.map +0 -1
- package/dist/chunk-XAEII4ZX.js.map +0 -1
- package/dist/chunk-YSTEESEG.js.map +0 -1
- package/dist/chunk-Z6VOBGTU.js.map +0 -1
- /package/dist/{chunk-XOY2CJ67.js.map → chunk-4F5B5CZ7.js.map} +0 -0
- /package/dist/{chunk-654ERHA7.js.map → chunk-5WRI5ZAA.js.map} +0 -0
- /package/dist/{chunk-A4IBBWGO.js.map → chunk-RALHHPTU.js.map} +0 -0
- /package/dist/{chunk-WGHNIAF7.js.map → chunk-YONQYTVH.js.map} +0 -0
|
@@ -59,11 +59,7 @@ function validateWhereClause(whereClause) {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
function resolvePathColumn(config) {
|
|
62
|
-
|
|
63
|
-
if (!column) {
|
|
64
|
-
throw new Error("WatchConfig requires either pathColumn or idColumn. pathColumn is preferred; idColumn is deprecated.");
|
|
65
|
-
}
|
|
66
|
-
return column;
|
|
62
|
+
return config.pathColumn;
|
|
67
63
|
}
|
|
68
64
|
function buildWatchQuery(config) {
|
|
69
65
|
const pathColumn = resolvePathColumn(config);
|
|
@@ -140,8 +136,6 @@ function watchConfigToSourceConfig(watchConfig) {
|
|
|
140
136
|
return {
|
|
141
137
|
table: watchConfig.table,
|
|
142
138
|
pathColumn,
|
|
143
|
-
idColumn: pathColumn,
|
|
144
|
-
// For backwards compatibility
|
|
145
139
|
orderByColumn: watchConfig.orderBy?.column ?? null
|
|
146
140
|
};
|
|
147
141
|
}
|
|
@@ -159,12 +153,12 @@ function extractIdsFromRows(results) {
|
|
|
159
153
|
}
|
|
160
154
|
return ids;
|
|
161
155
|
}
|
|
162
|
-
function
|
|
156
|
+
function createWatchPaths(table, pathColumn) {
|
|
163
157
|
const sql = buildIdOnlyWatchQuery({
|
|
164
158
|
table,
|
|
165
159
|
pathColumn
|
|
166
160
|
});
|
|
167
|
-
return (db, onUpdate) => {
|
|
161
|
+
return (db, _supabase, onUpdate) => {
|
|
168
162
|
const abortController = new AbortController();
|
|
169
163
|
db.watch(sql, [], {
|
|
170
164
|
onResult: (results) => onUpdate(extractIdsFromRows(results))
|
|
@@ -185,14 +179,13 @@ var STATE_MAPPING = /* @__PURE__ */ new Map([
|
|
|
185
179
|
[AttachmentState.SYNCED, 3 /* SYNCED */],
|
|
186
180
|
[AttachmentState.ARCHIVED, 4 /* ARCHIVED */],
|
|
187
181
|
// POL extension states (identity mapping)
|
|
188
|
-
[5 /* FAILED_PERMANENT */, 5 /* FAILED_PERMANENT */]
|
|
189
|
-
[6 /* DOWNLOAD_SKIPPED */, 6 /* DOWNLOAD_SKIPPED */]
|
|
182
|
+
[5 /* FAILED_PERMANENT */, 5 /* FAILED_PERMANENT */]
|
|
190
183
|
]);
|
|
191
|
-
var STATE_NAMES = /* @__PURE__ */ new Map([[0 /* QUEUED_SYNC */, "QUEUED_SYNC"], [1 /* QUEUED_UPLOAD */, "QUEUED_UPLOAD"], [2 /* QUEUED_DOWNLOAD */, "QUEUED_DOWNLOAD"], [3 /* SYNCED */, "SYNCED"], [4 /* ARCHIVED */, "ARCHIVED"], [5 /* FAILED_PERMANENT */, "FAILED_PERMANENT"]
|
|
192
|
-
var VALID_STATES = /* @__PURE__ */ new Set([0 /* QUEUED_SYNC */, 1 /* QUEUED_UPLOAD */, 2 /* QUEUED_DOWNLOAD */, 3 /* SYNCED */, 4 /* ARCHIVED */, 5 /* FAILED_PERMANENT
|
|
184
|
+
var STATE_NAMES = /* @__PURE__ */ new Map([[0 /* QUEUED_SYNC */, "QUEUED_SYNC"], [1 /* QUEUED_UPLOAD */, "QUEUED_UPLOAD"], [2 /* QUEUED_DOWNLOAD */, "QUEUED_DOWNLOAD"], [3 /* SYNCED */, "SYNCED"], [4 /* ARCHIVED */, "ARCHIVED"], [5 /* FAILED_PERMANENT */, "FAILED_PERMANENT"]]);
|
|
185
|
+
var VALID_STATES = /* @__PURE__ */ new Set([0 /* QUEUED_SYNC */, 1 /* QUEUED_UPLOAD */, 2 /* QUEUED_DOWNLOAD */, 3 /* SYNCED */, 4 /* ARCHIVED */, 5 /* FAILED_PERMANENT */]);
|
|
193
186
|
var UPLOAD_WORKFLOW_STATES = /* @__PURE__ */ new Set([1 /* QUEUED_UPLOAD */, 5 /* FAILED_PERMANENT */]);
|
|
194
187
|
var DOWNLOAD_WORKFLOW_STATES = /* @__PURE__ */ new Set([2 /* QUEUED_DOWNLOAD */, 0 /* QUEUED_SYNC */]);
|
|
195
|
-
var TERMINAL_STATES = /* @__PURE__ */ new Set([3 /* SYNCED */, 4 /* ARCHIVED
|
|
188
|
+
var TERMINAL_STATES = /* @__PURE__ */ new Set([3 /* SYNCED */, 4 /* ARCHIVED */]);
|
|
196
189
|
function migrateAttachmentState(oldState) {
|
|
197
190
|
const newState = STATE_MAPPING.get(oldState);
|
|
198
191
|
if (newState === void 0) {
|
|
@@ -252,7 +245,7 @@ export {
|
|
|
252
245
|
buildRecordFetchQuery,
|
|
253
246
|
watchConfigToSourceConfig,
|
|
254
247
|
extractIdsFromRows,
|
|
255
|
-
|
|
248
|
+
createWatchPaths,
|
|
256
249
|
STATE_MAPPING,
|
|
257
250
|
STATE_NAMES,
|
|
258
251
|
VALID_STATES,
|
|
@@ -270,4 +263,4 @@ export {
|
|
|
270
263
|
recordMigration,
|
|
271
264
|
formatMigrationStats
|
|
272
265
|
};
|
|
273
|
-
//# sourceMappingURL=chunk-
|
|
266
|
+
//# sourceMappingURL=chunk-EOW7JK7Q.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 * Gets the path column from a WatchConfig.\n *\n * @param config - The WatchConfig to get the column from\n * @returns The path column name\n */\nfunction resolvePathColumn(config: WatchConfig): string {\n return config.pathColumn;\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 */\nexport function buildWatchQuery(config: WatchConfig): string {\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 path-only watch query.\n * Use this when you only need paths without additional columns.\n *\n * @param config - The WatchConfig to build a query from\n * @returns SQL query string that selects only paths\n *\n * @example\n * ```typescript\n * const query = buildIdOnlyWatchQuery({\n * table: 'EquipmentUnitMediaContent',\n * pathColumn: '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 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 source config format.\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 orderByColumn?: string | null;\n} {\n const pathColumn = resolvePathColumn(watchConfig);\n return {\n table: watchConfig.table,\n pathColumn,\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 watchPaths callback for simple single-table attachment configurations.\n *\n * This is a convenience function that generates a properly typed watchPaths 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\n * @returns A properly typed watchPaths callback for use in AttachmentSourceConfig\n *\n * @example\n * ```typescript\n * import { createWatchPaths } from '@pol-studios/powersync/attachments';\n *\n * const config: AttachmentSourceConfig = {\n * source: { type: 'supabase-bucket', bucket: 'photos' },\n * watchPaths: createWatchPaths('EquipmentUnitMediaContent', 'storagePath'),\n * };\n * ```\n */\nexport function createWatchPaths(table: string, pathColumn: string): (db: import('./types').PowerSyncDBInterface, supabase: unknown, onUpdate: (paths: 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, _supabase, 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\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]]);\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']]);\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]);\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]);\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":"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","names":[]}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LOCAL_ONLY_TABLES
|
|
3
|
+
} from "./chunk-OGUFUZSY.js";
|
|
4
|
+
|
|
5
|
+
// src/config/schema-generator.ts
|
|
6
|
+
import { column, Schema, Table } from "@powersync/react-native";
|
|
7
|
+
function normalizeTableSpec(spec) {
|
|
8
|
+
if (typeof spec === "string") {
|
|
9
|
+
if (spec.includes(".")) {
|
|
10
|
+
const [schemaName2, tableName] = spec.split(".");
|
|
11
|
+
return {
|
|
12
|
+
tableName,
|
|
13
|
+
schemaName: schemaName2,
|
|
14
|
+
trackMetadata: false,
|
|
15
|
+
alias: toPascalCase(schemaName2) + tableName
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
tableName: spec,
|
|
20
|
+
schemaName: "public",
|
|
21
|
+
trackMetadata: false,
|
|
22
|
+
alias: spec
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const schemaName = spec.schema || "public";
|
|
26
|
+
const alias = schemaName === "public" ? spec.table : toPascalCase(schemaName) + spec.table;
|
|
27
|
+
return {
|
|
28
|
+
tableName: spec.table,
|
|
29
|
+
schemaName,
|
|
30
|
+
trackMetadata: spec.trackMetadata ?? false,
|
|
31
|
+
alias
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function toPascalCase(str) {
|
|
35
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
36
|
+
}
|
|
37
|
+
function mapColumnType(col) {
|
|
38
|
+
const type = col.type;
|
|
39
|
+
if (type === "json") {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
switch (type) {
|
|
43
|
+
case "number":
|
|
44
|
+
return column.real;
|
|
45
|
+
case "boolean":
|
|
46
|
+
return column.integer;
|
|
47
|
+
case "string":
|
|
48
|
+
case "date":
|
|
49
|
+
case "enum":
|
|
50
|
+
return column.text;
|
|
51
|
+
default:
|
|
52
|
+
return column.text;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function shouldSkipColumn(col) {
|
|
56
|
+
if (col.name === "id") {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
if (col.type === "json") {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
function getTableFromSchema(databaseSchema, schemaName, tableName) {
|
|
65
|
+
const schema = databaseSchema.schemas[schemaName];
|
|
66
|
+
if (!schema) {
|
|
67
|
+
console.warn(`[PolConfig] Schema "${schemaName}" not found in databaseSchema`);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const table = schema.tables[tableName];
|
|
71
|
+
if (!table) {
|
|
72
|
+
const view = schema.views?.[tableName];
|
|
73
|
+
if (view) {
|
|
74
|
+
return view;
|
|
75
|
+
}
|
|
76
|
+
console.warn(`[PolConfig] Table "${schemaName}.${tableName}" not found in databaseSchema`);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
return table;
|
|
80
|
+
}
|
|
81
|
+
function generatePowerSyncSchema(databaseSchema, tables) {
|
|
82
|
+
const warnings = [];
|
|
83
|
+
const tableDefinitions = {};
|
|
84
|
+
const tableMap = /* @__PURE__ */ new Map();
|
|
85
|
+
for (const spec of tables) {
|
|
86
|
+
const normalized = normalizeTableSpec(spec);
|
|
87
|
+
const {
|
|
88
|
+
tableName,
|
|
89
|
+
schemaName,
|
|
90
|
+
alias
|
|
91
|
+
} = normalized;
|
|
92
|
+
const tableDef = getTableFromSchema(databaseSchema, schemaName, tableName);
|
|
93
|
+
if (!tableDef) {
|
|
94
|
+
warnings.push(`Table "${schemaName}.${tableName}" not found, skipping`);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const columns = {};
|
|
98
|
+
let columnCount = 0;
|
|
99
|
+
for (const col of tableDef.columns) {
|
|
100
|
+
if (shouldSkipColumn(col)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const psType = mapColumnType(col);
|
|
104
|
+
if (psType === null) {
|
|
105
|
+
warnings.push(`Column "${tableName}.${col.name}" has unsupported type "${col.type}", skipping`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
columns[col.name] = psType;
|
|
109
|
+
columnCount++;
|
|
110
|
+
}
|
|
111
|
+
if (columnCount > 0) {
|
|
112
|
+
if (tableDefinitions[alias]) {
|
|
113
|
+
warnings.push(`Duplicate table "${alias}" - later definition wins`);
|
|
114
|
+
}
|
|
115
|
+
tableDefinitions[alias] = normalized.trackMetadata ? new Table(columns, {
|
|
116
|
+
trackMetadata: true
|
|
117
|
+
}) : new Table(columns);
|
|
118
|
+
tableMap.set(alias, normalized);
|
|
119
|
+
} else {
|
|
120
|
+
warnings.push(`Table "${schemaName}.${tableName}" has no valid columns after filtering, skipping`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const allTableDefinitions = {
|
|
124
|
+
...tableDefinitions,
|
|
125
|
+
...LOCAL_ONLY_TABLES
|
|
126
|
+
};
|
|
127
|
+
const schema = new Schema(allTableDefinitions);
|
|
128
|
+
return {
|
|
129
|
+
schema,
|
|
130
|
+
warnings,
|
|
131
|
+
tableMap
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function createSchemaRouter(tableMap) {
|
|
135
|
+
return (table) => {
|
|
136
|
+
const spec = tableMap.get(table);
|
|
137
|
+
if (spec) {
|
|
138
|
+
return spec.schemaName;
|
|
139
|
+
}
|
|
140
|
+
if (table.includes(".")) {
|
|
141
|
+
const [schema] = table.split(".");
|
|
142
|
+
return schema;
|
|
143
|
+
}
|
|
144
|
+
return "public";
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/config/index.ts
|
|
149
|
+
function definePolConfig(config) {
|
|
150
|
+
if (!config.databaseSchema) {
|
|
151
|
+
throw new Error("[definePolConfig] databaseSchema is required");
|
|
152
|
+
}
|
|
153
|
+
if (!config.powersync || config.powersync.length === 0) {
|
|
154
|
+
throw new Error("[definePolConfig] powersync array must contain at least one table");
|
|
155
|
+
}
|
|
156
|
+
const {
|
|
157
|
+
schema,
|
|
158
|
+
warnings,
|
|
159
|
+
tableMap
|
|
160
|
+
} = generatePowerSyncSchema(config.databaseSchema, config.powersync);
|
|
161
|
+
if (warnings.length > 0 && typeof __DEV__ !== "undefined" && __DEV__) {
|
|
162
|
+
console.warn("[definePolConfig] Schema generation warnings:", warnings);
|
|
163
|
+
}
|
|
164
|
+
if (tableMap.size === 0) {
|
|
165
|
+
throw new Error(`[definePolConfig] No valid tables found in powersync array. Check that the tables exist in databaseSchema. Warnings: ${warnings.join(", ")}`);
|
|
166
|
+
}
|
|
167
|
+
const tableStrategies = {};
|
|
168
|
+
const powersyncTables = [];
|
|
169
|
+
for (const [alias, spec] of tableMap.entries()) {
|
|
170
|
+
tableStrategies[alias] = {
|
|
171
|
+
strategy: "powersync",
|
|
172
|
+
schema: spec.schemaName,
|
|
173
|
+
table: spec.tableName,
|
|
174
|
+
alias
|
|
175
|
+
// PowerSync SQLite table name
|
|
176
|
+
};
|
|
177
|
+
const qualifiedName = spec.schemaName === "public" ? spec.tableName : `${spec.schemaName}.${spec.tableName}`;
|
|
178
|
+
if (qualifiedName !== alias) {
|
|
179
|
+
tableStrategies[qualifiedName] = {
|
|
180
|
+
strategy: "powersync",
|
|
181
|
+
schema: spec.schemaName,
|
|
182
|
+
table: spec.tableName,
|
|
183
|
+
alias
|
|
184
|
+
// PowerSync SQLite table name (same alias for both keys)
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
powersyncTables.push(qualifiedName);
|
|
188
|
+
}
|
|
189
|
+
const schemaRouter = createSchemaRouter(tableMap);
|
|
190
|
+
const processedConfig = {
|
|
191
|
+
...config,
|
|
192
|
+
__generatedSchema: schema,
|
|
193
|
+
__tableStrategies: tableStrategies,
|
|
194
|
+
__schemaRouter: schemaRouter,
|
|
195
|
+
__powersyncTables: powersyncTables
|
|
196
|
+
};
|
|
197
|
+
return processedConfig;
|
|
198
|
+
}
|
|
199
|
+
function isProcessedPolConfig(config) {
|
|
200
|
+
return typeof config === "object" && config !== null && "__generatedSchema" in config && "__tableStrategies" in config;
|
|
201
|
+
}
|
|
202
|
+
function getTableStrategy(config, tableName) {
|
|
203
|
+
return config.__tableStrategies[tableName];
|
|
204
|
+
}
|
|
205
|
+
function isTablePowerSync(config, tableName) {
|
|
206
|
+
const strategy = getTableStrategy(config, tableName);
|
|
207
|
+
return strategy?.strategy === "powersync";
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export {
|
|
211
|
+
normalizeTableSpec,
|
|
212
|
+
getTableFromSchema,
|
|
213
|
+
generatePowerSyncSchema,
|
|
214
|
+
createSchemaRouter,
|
|
215
|
+
definePolConfig,
|
|
216
|
+
isProcessedPolConfig,
|
|
217
|
+
getTableStrategy,
|
|
218
|
+
isTablePowerSync
|
|
219
|
+
};
|
|
220
|
+
//# sourceMappingURL=chunk-HRAVPIAZ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config/schema-generator.ts","../src/config/index.ts"],"sourcesContent":["/**\n * Schema Generator for @pol-studios/powersync\n *\n * Generates PowerSync Schema at runtime from the Supabase database schema object.\n * This eliminates the need to manually maintain a separate PowerSync schema file.\n *\n * Type Mapping:\n * - number -> column.real (ALL numbers use real, no integer/real distinction)\n * - boolean -> column.integer (SQLite stores booleans as 0/1)\n * - string -> column.text\n * - date -> column.text (ISO strings)\n * - enum -> column.text\n * - json -> SKIPPED (not supported in PowerSync)\n * - arrays -> SKIPPED (not supported in PowerSync)\n *\n * Skipped columns:\n * - `id` (PowerSync handles this automatically)\n * - JSON types (complex objects not supported)\n * - Array types (not supported)\n */\n\nimport { column, Schema, Table } from '@powersync/react-native';\nimport type { DatabaseSchemaObject, DatabaseTableDef, DatabaseColumnDef, TableSpec } from './types';\nimport { LOCAL_ONLY_TABLES } from '../sync/local-tables';\n\n// Type alias for PowerSync column types (same as what column.real/integer/text return)\ntype PowerSyncColumn = typeof column.real | typeof column.integer | typeof column.text;\n\n// ─── Table Spec Normalization ─────────────────────────────────────────────────\n\n/**\n * Normalized table specification.\n */\ninterface NormalizedTableSpec {\n /** Table name (without schema prefix) */\n tableName: string;\n /** Schema name */\n schemaName: string;\n /** Whether to track sync metadata */\n trackMetadata: boolean;\n /**\n * Alias for PowerSync table name.\n * For cross-schema tables, this is SchemaTable (e.g., \"CoreProfile\").\n * For public schema, this is just the table name.\n */\n alias: string;\n}\n\n/**\n * Normalize a TableSpec to a consistent format.\n *\n * @param spec - The table specification (string or object)\n * @returns Normalized specification with all fields filled\n */\nexport function normalizeTableSpec(spec: TableSpec): NormalizedTableSpec {\n if (typeof spec === 'string') {\n // Check for schema prefix (e.g., \"core.Profile\")\n if (spec.includes('.')) {\n const [schemaName, tableName] = spec.split('.');\n return {\n tableName,\n schemaName,\n trackMetadata: false,\n alias: toPascalCase(schemaName) + tableName\n };\n }\n // Simple table name - public schema\n return {\n tableName: spec,\n schemaName: 'public',\n trackMetadata: false,\n alias: spec\n };\n }\n\n // Object format\n const schemaName = spec.schema || 'public';\n const alias = schemaName === 'public' ? spec.table : toPascalCase(schemaName) + spec.table;\n return {\n tableName: spec.table,\n schemaName,\n trackMetadata: spec.trackMetadata ?? false,\n alias\n };\n}\n\n/**\n * Convert a string to PascalCase.\n */\nfunction toPascalCase(str: string): string {\n return str.charAt(0).toUpperCase() + str.slice(1);\n}\n\n// ─── Type Mapping ─────────────────────────────────────────────────────────────\n\n/**\n * Map a database column type to a PowerSync column type.\n *\n * @param col - The column definition\n * @returns PowerSync column type or null if should be skipped\n */\nfunction mapColumnType(col: DatabaseColumnDef): PowerSyncColumn | null {\n const type = col.type;\n\n // Skip JSON types - not supported in PowerSync\n if (type === 'json') {\n return null;\n }\n\n // Map based on type\n switch (type) {\n case 'number':\n // ALL numbers use column.real - no heuristics\n return column.real;\n case 'boolean':\n // SQLite stores booleans as integers (0/1)\n return column.integer;\n case 'string':\n case 'date':\n case 'enum':\n return column.text;\n default:\n // Unknown types default to text\n return column.text;\n }\n}\n\n/**\n * Check if a column should be skipped.\n *\n * @param col - The column definition\n * @returns true if the column should be skipped\n */\nfunction shouldSkipColumn(col: DatabaseColumnDef): boolean {\n // Skip `id` - PowerSync handles this automatically\n if (col.name === 'id') {\n return true;\n }\n\n // Skip JSON types\n if (col.type === 'json') {\n return true;\n }\n\n // Skip array-like columns (columns ending with 's' that are JSON)\n // This is a heuristic - actual array detection would need type info\n // For now, we rely on the type being 'json' which is already skipped\n\n return false;\n}\n\n// ─── Table Lookup ─────────────────────────────────────────────────────────────\n\n/**\n * Get a table definition from the database schema.\n *\n * @param databaseSchema - The full database schema object\n * @param schemaName - The schema name (e.g., \"public\", \"core\")\n * @param tableName - The table name\n * @returns The table definition or null if not found\n */\nexport function getTableFromSchema(databaseSchema: DatabaseSchemaObject, schemaName: string, tableName: string): DatabaseTableDef | null {\n const schema = databaseSchema.schemas[schemaName];\n if (!schema) {\n console.warn(`[PolConfig] Schema \"${schemaName}\" not found in databaseSchema`);\n return null;\n }\n const table = schema.tables[tableName];\n if (!table) {\n // Also check views\n const view = schema.views?.[tableName];\n if (view) {\n return view;\n }\n console.warn(`[PolConfig] Table \"${schemaName}.${tableName}\" not found in databaseSchema`);\n return null;\n }\n return table;\n}\n\n// ─── Schema Generation ────────────────────────────────────────────────────────\n\n/**\n * Result of schema generation.\n */\nexport interface SchemaGenerationResult {\n /** The generated PowerSync Schema */\n schema: Schema;\n /** Warnings encountered during generation */\n warnings: string[];\n /** Map of table alias to normalized spec */\n tableMap: Map<string, NormalizedTableSpec>;\n}\n\n/**\n * Generate a PowerSync Schema from the database schema and table specs.\n *\n * @param databaseSchema - The database schema object (from generated types)\n * @param tables - Array of table specifications to sync\n * @returns The generated PowerSync Schema and metadata\n *\n * @example\n * ```typescript\n * const { schema, warnings } = generatePowerSyncSchema(databaseSchema, [\n * \"Project\",\n * \"Task\",\n * { table: \"Profile\", schema: \"core\" },\n * ]);\n *\n * // Use the schema with PowerSyncProvider\n * <PowerSyncProvider config={{ schema, ... }} />\n * ```\n */\nexport function generatePowerSyncSchema(databaseSchema: DatabaseSchemaObject, tables: TableSpec[]): SchemaGenerationResult {\n const warnings: string[] = [];\n const tableDefinitions: Record<string, Table> = {};\n const tableMap = new Map<string, NormalizedTableSpec>();\n for (const spec of tables) {\n const normalized = normalizeTableSpec(spec);\n const {\n tableName,\n schemaName,\n alias\n } = normalized;\n\n // Get table definition from schema\n const tableDef = getTableFromSchema(databaseSchema, schemaName, tableName);\n if (!tableDef) {\n warnings.push(`Table \"${schemaName}.${tableName}\" not found, skipping`);\n continue;\n }\n\n // Build PowerSync columns\n const columns: Record<string, PowerSyncColumn> = {};\n let columnCount = 0;\n for (const col of tableDef.columns) {\n // Skip certain columns\n if (shouldSkipColumn(col)) {\n continue;\n }\n\n // Map the column type\n const psType = mapColumnType(col);\n if (psType === null) {\n // Column type not supported, skip with warning\n warnings.push(`Column \"${tableName}.${col.name}\" has unsupported type \"${col.type}\", skipping`);\n continue;\n }\n columns[col.name] = psType;\n columnCount++;\n }\n\n // Create PowerSync Table\n if (columnCount > 0) {\n // Check for duplicate tables\n if (tableDefinitions[alias]) {\n warnings.push(`Duplicate table \"${alias}\" - later definition wins`);\n }\n\n // Pass trackMetadata option if specified\n tableDefinitions[alias] = normalized.trackMetadata ? new Table(columns, {\n trackMetadata: true\n }) : new Table(columns);\n tableMap.set(alias, normalized);\n } else {\n warnings.push(`Table \"${schemaName}.${tableName}\" has no valid columns after filtering, skipping`);\n }\n }\n\n // Add local-only tables for sync status persistence\n const allTableDefinitions = {\n ...tableDefinitions,\n ...LOCAL_ONLY_TABLES\n };\n\n // Create the Schema\n const schema = new Schema(allTableDefinitions);\n return {\n schema,\n warnings,\n tableMap\n };\n}\n\n// ─── Schema Router ────────────────────────────────────────────────────────────\n\n/**\n * Create a schema router function from the table map.\n *\n * @param tableMap - Map of table alias to normalized spec\n * @returns A function that returns the schema for a given table\n */\nexport function createSchemaRouter(tableMap: Map<string, NormalizedTableSpec>): (table: string) => string {\n return (table: string): string => {\n // Check if it's a known PowerSync table\n const spec = tableMap.get(table);\n if (spec) {\n return spec.schemaName;\n }\n\n // Check for schema-prefixed format (e.g., \"core.Profile\")\n if (table.includes('.')) {\n const [schema] = table.split('.');\n return schema;\n }\n\n // Default to public\n return 'public';\n };\n}","/**\n * Unified Configuration for @pol-studios/powersync\n *\n * This module provides the `definePolConfig` function for creating\n * a unified configuration that automatically generates:\n * - PowerSync Schema from the database schema\n * - Table routing strategies for DataLayer\n * - Attachment queue configuration\n *\n * @example\n * ```typescript\n * import { definePolConfig } from '@pol-studios/powersync/config';\n * import { databaseSchema } from './databaseSchema';\n *\n * export const polConfig = definePolConfig({\n * powerSyncUrl: process.env.EXPO_PUBLIC_POWERSYNC_URL,\n * databaseSchema,\n * powersync: [\"Project\", \"Task\", { table: \"Profile\", schema: \"core\" }],\n * attachments: {\n * source: { type: 'supabase-bucket', bucket: 'photos' },\n * watchPaths: (db, supabase, onUpdate) => {\n * const abort = new AbortController();\n * db.watch(\n * `SELECT storagePath FROM photos WHERE storagePath IS NOT NULL`,\n * [],\n * { onResult: (r) => onUpdate(r.rows._array.map(x => x.storagePath)) },\n * { signal: abort.signal }\n * );\n * return () => abort.abort();\n * },\n * },\n * });\n *\n * // Use with OfflineDataProvider\n * <OfflineDataProvider polConfig={polConfig}>\n * <App />\n * </OfflineDataProvider>\n * ```\n */\n\nimport type { PolConfig, ProcessedPolConfig, ProcessedTableStrategy, TableSpec } from './types';\nimport { generatePowerSyncSchema, normalizeTableSpec, createSchemaRouter } from './schema-generator';\n\n// Re-export types\nexport type { PolConfig, ProcessedPolConfig, TableSpec, ProcessedTableStrategy, PolConfigAttachments, PolConfigConnector, PolConfigSync, DatabaseSchemaObject, DatabaseTableDef, DatabaseColumnDef,\n// Alias for attachment source type from config perspective\nPolAttachmentSource } from './types';\n\n// Re-export attachment source types from attachments module\n// (these are the canonical definitions)\nexport type { AttachmentSource, SupabaseBucketSource, CustomAttachmentSource } from '../attachments/types';\n\n// Re-export schema generator utilities\nexport { generatePowerSyncSchema, normalizeTableSpec, createSchemaRouter, getTableFromSchema } from './schema-generator';\n\n// ─── Configuration Processing ─────────────────────────────────────────────────\n\n/**\n * Create a unified PowerSync configuration.\n *\n * This function processes the configuration to generate:\n * - `__generatedSchema`: PowerSync Schema ready for use\n * - `__tableStrategies`: Lookup map for routing queries\n * - `__schemaRouter`: Function to resolve table schemas\n *\n * @param config - The unified configuration\n * @returns Processed configuration with generated artifacts\n *\n * @example\n * ```typescript\n * const polConfig = definePolConfig({\n * powerSyncUrl: 'https://your-instance.powersync.journeyapps.com',\n * databaseSchema,\n * powersync: [\n * \"Project\",\n * \"Task\",\n * { table: \"Profile\", schema: \"core\" },\n * ],\n * });\n *\n * // Access generated schema\n * console.log(polConfig.__generatedSchema);\n *\n * // Check if a table uses PowerSync\n * const strategy = polConfig.__tableStrategies[\"Project\"];\n * console.log(strategy.strategy); // \"powersync\"\n * ```\n */\nexport function definePolConfig(config: PolConfig): ProcessedPolConfig {\n // Validate required fields\n if (!config.databaseSchema) {\n throw new Error('[definePolConfig] databaseSchema is required');\n }\n if (!config.powersync || config.powersync.length === 0) {\n throw new Error('[definePolConfig] powersync array must contain at least one table');\n }\n\n // Generate PowerSync schema\n const {\n schema,\n warnings,\n tableMap\n } = generatePowerSyncSchema(config.databaseSchema, config.powersync);\n\n // Log warnings in development (safe check for non-React-Native environments)\n if (warnings.length > 0 && typeof __DEV__ !== 'undefined' && __DEV__) {\n console.warn('[definePolConfig] Schema generation warnings:', warnings);\n }\n\n // Validate that at least one table was successfully generated\n if (tableMap.size === 0) {\n throw new Error('[definePolConfig] No valid tables found in powersync array. ' + `Check that the tables exist in databaseSchema. Warnings: ${warnings.join(', ')}`);\n }\n\n // Build table strategies lookup\n const tableStrategies: Record<string, ProcessedTableStrategy> = {};\n const powersyncTables: string[] = [];\n for (const [alias, spec] of tableMap.entries()) {\n // Add to strategies map with alias as key\n tableStrategies[alias] = {\n strategy: 'powersync',\n schema: spec.schemaName,\n table: spec.tableName,\n alias: alias // PowerSync SQLite table name\n };\n\n // Also add with full qualified name for lookup\n const qualifiedName = spec.schemaName === 'public' ? spec.tableName : `${spec.schemaName}.${spec.tableName}`;\n if (qualifiedName !== alias) {\n tableStrategies[qualifiedName] = {\n strategy: 'powersync',\n schema: spec.schemaName,\n table: spec.tableName,\n alias: alias // PowerSync SQLite table name (same alias for both keys)\n };\n }\n powersyncTables.push(qualifiedName);\n }\n\n // Create schema router\n const schemaRouter = createSchemaRouter(tableMap);\n\n // Return processed config\n const processedConfig: ProcessedPolConfig = {\n ...config,\n __generatedSchema: schema,\n __tableStrategies: tableStrategies,\n __schemaRouter: schemaRouter,\n __powersyncTables: powersyncTables\n };\n return processedConfig;\n}\n\n// ─── Helper Functions ─────────────────────────────────────────────────────────\n\n/**\n * Check if a configuration is a processed PolConfig.\n *\n * @param config - The configuration to check\n * @returns true if the config has been processed by definePolConfig\n */\nexport function isProcessedPolConfig(config: unknown): config is ProcessedPolConfig {\n return typeof config === 'object' && config !== null && '__generatedSchema' in config && '__tableStrategies' in config;\n}\n\n/**\n * Get the strategy for a table from a processed config.\n *\n * @param config - The processed configuration\n * @param tableName - Table name (can be \"Table\" or \"schema.Table\")\n * @returns The strategy or undefined if not found\n */\nexport function getTableStrategy(config: ProcessedPolConfig, tableName: string): ProcessedTableStrategy | undefined {\n return config.__tableStrategies[tableName];\n}\n\n/**\n * Check if a table uses PowerSync for sync.\n *\n * @param config - The processed configuration\n * @param tableName - Table name\n * @returns true if the table is synced via PowerSync\n */\nexport function isTablePowerSync(config: ProcessedPolConfig, tableName: string): boolean {\n const strategy = getTableStrategy(config, tableName);\n return strategy?.strategy === 'powersync';\n}\n\n// ─── Global Declaration ───────────────────────────────────────────────────────\n\n// Declare __DEV__ global for React Native\ndeclare const __DEV__: boolean;"],"mappings":"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","names":["schemaName"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
resolveBucket
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-UOMHWUHV.js";
|
|
4
4
|
import {
|
|
5
5
|
AbortError
|
|
6
6
|
} from "./chunk-FV2HXEIY.js";
|
|
@@ -93,6 +93,8 @@ var SupabaseStorageAdapter = class {
|
|
|
93
93
|
fileSystem;
|
|
94
94
|
logger;
|
|
95
95
|
imageTransform;
|
|
96
|
+
useSignedUrls;
|
|
97
|
+
customUrlResolver;
|
|
96
98
|
constructor(options, fileSystem) {
|
|
97
99
|
this.client = options.client;
|
|
98
100
|
this.defaultBucket = options.defaultBucket;
|
|
@@ -102,6 +104,8 @@ var SupabaseStorageAdapter = class {
|
|
|
102
104
|
this.fileSystem = fileSystem;
|
|
103
105
|
this.logger = options.logger;
|
|
104
106
|
this.imageTransform = options.imageTransform;
|
|
107
|
+
this.useSignedUrls = options.useSignedUrls ?? true;
|
|
108
|
+
this.customUrlResolver = options.customUrlResolver;
|
|
105
109
|
}
|
|
106
110
|
/**
|
|
107
111
|
* Update image transform options (can be set after construction).
|
|
@@ -250,15 +254,39 @@ var SupabaseStorageAdapter = class {
|
|
|
250
254
|
});
|
|
251
255
|
}
|
|
252
256
|
/**
|
|
253
|
-
* Get a
|
|
257
|
+
* Get a download URL for a remote file.
|
|
254
258
|
*
|
|
255
|
-
*
|
|
259
|
+
* Supports three modes:
|
|
260
|
+
* 1. Custom URL resolver (for non-Supabase backends)
|
|
261
|
+
* 2. Public URLs (for public Supabase buckets, when useSignedUrls=false)
|
|
262
|
+
* 3. Signed URLs (default, for private Supabase buckets)
|
|
263
|
+
*
|
|
264
|
+
* Includes session refresh logic for signed URLs to handle stale tokens during background downloads.
|
|
256
265
|
* If the initial request fails with an auth error (400/401/403), refreshes the session
|
|
257
266
|
* and retries once.
|
|
258
267
|
*/
|
|
259
268
|
async getDownloadUrl(remotePath) {
|
|
260
269
|
const normalizedPath = normalizeStoragePath(remotePath);
|
|
270
|
+
if (this.customUrlResolver) {
|
|
271
|
+
this.logger?.debug?.(`[SupabaseStorageAdapter] Using custom URL resolver`, {
|
|
272
|
+
path: normalizedPath
|
|
273
|
+
});
|
|
274
|
+
return this.customUrlResolver(normalizedPath);
|
|
275
|
+
}
|
|
261
276
|
const bucket = this.resolveBucket(normalizedPath);
|
|
277
|
+
if (!this.useSignedUrls) {
|
|
278
|
+
this.logger?.debug?.(`[SupabaseStorageAdapter] Creating public URL`, {
|
|
279
|
+
bucket,
|
|
280
|
+
path: normalizedPath
|
|
281
|
+
});
|
|
282
|
+
const {
|
|
283
|
+
data: data2
|
|
284
|
+
} = this.client.storage.from(bucket).getPublicUrl(normalizedPath);
|
|
285
|
+
if (!data2?.publicUrl) {
|
|
286
|
+
throw new Error(`Failed to create public URL for '${normalizedPath}' in bucket '${bucket}': No URL returned`);
|
|
287
|
+
}
|
|
288
|
+
return data2.publicUrl;
|
|
289
|
+
}
|
|
262
290
|
const useTransform = this.imageTransform?.enabled !== false && this.isTransformableImage(normalizedPath);
|
|
263
291
|
const signedUrlOptions = useTransform && this.imageTransform ? {
|
|
264
292
|
transform: {
|
|
@@ -453,4 +481,4 @@ export {
|
|
|
453
481
|
SupabaseStorageAdapter,
|
|
454
482
|
createSupabaseStorageAdapter
|
|
455
483
|
};
|
|
456
|
-
//# sourceMappingURL=chunk-
|
|
484
|
+
//# sourceMappingURL=chunk-NUGQOTEM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/storage/auth-error-utils.ts","../src/storage/SupabaseStorageAdapter.ts"],"sourcesContent":["/**\n * Utilities for detecting storage authentication errors.\n *\n * These utilities help identify when a storage operation failed due to\n * authentication issues (expired tokens, invalid sessions, etc.) that\n * can often be resolved by refreshing the user's session.\n *\n * NOTE: This is a copy of the utility from @pol-studios/db/storage.\n * We maintain a separate copy here to avoid circular dependencies between\n * @pol-studios/powersync and @pol-studios/db.\n */\n\n/**\n * Patterns that indicate an error is authentication-related.\n * These are checked against error messages when the status code alone\n * isn't sufficient to determine if it's an auth error.\n */\nconst AUTH_ERROR_PATTERNS = [/\\btoken\\b/i, /\\bauthoriz/i, /\\bauthenticat/i, /\\bsession\\b/i, /\\bexpired?\\b/i, /\\binvalid.*credential/i, /\\bno.*session\\b/i, /\\bJWT\\b/i] as const;\n\n/**\n * Check if a storage error is an authentication error.\n *\n * These errors can often be fixed by refreshing the session.\n *\n * @param error - The error to check (typically from Supabase Storage operations)\n * @returns true if the error appears to be authentication-related\n *\n * @example\n * ```typescript\n * const { data, error } = await storage.createSignedUrl(path, 3600);\n * if (error && isStorageAuthError(error)) {\n * await supabase.auth.refreshSession();\n * // Retry the request...\n * }\n * ```\n */\nexport function isStorageAuthError(error: unknown): boolean {\n if (!error) return false;\n const errorObj = error as {\n status?: number;\n statusCode?: number;\n message?: string;\n };\n const status = errorObj.status ?? errorObj.statusCode;\n\n // 401 (Unauthorized) and 403 (Forbidden) are always auth errors\n if (status === 401 || status === 403) {\n return true;\n }\n\n // 400 (Bad Request) is only an auth error when the message contains auth-related keywords\n // This is because custom gateways may return 400 with messages like\n // \"querystring must have required property 'token'\" for auth failures,\n // but 400 can also mean malformed parameters unrelated to auth\n if (status === 400) {\n const msg = errorObj.message ?? String(error);\n return /\\btoken\\b/i.test(msg) || /\\bauthoriz/i.test(msg) || /\\bauthenticat/i.test(msg);\n }\n\n // For other status codes (or no status), check the error message for auth-related keywords\n const message = errorObj.message ?? String(error);\n return AUTH_ERROR_PATTERNS.some(pattern => pattern.test(message));\n}\n\n/**\n * Extract a meaningful error message from a storage error.\n *\n * Supabase Storage errors may have different structures:\n * - Standard Error with `.message` property\n * - StorageError with `.message` and optional `.error` property\n * - Response-like object with `status`, `statusText`, `url` properties\n * - Plain string\n *\n * This function handles all these cases and returns a human-readable message.\n *\n * @param error - The error to extract a message from\n * @returns A human-readable error message\n *\n * @example\n * ```typescript\n * const { data, error } = await storage.createSignedUrl(path, 3600);\n * if (error) {\n * console.error(getStorageErrorMessage(error));\n * // \"Invalid path format\" or \"Object not found\" etc.\n * }\n * ```\n */\nexport function getStorageErrorMessage(error: unknown): string {\n if (!error) return 'Unknown error';\n\n // Handle plain strings\n if (typeof error === 'string') {\n return error;\n }\n\n // Handle Error objects and Supabase StorageError\n const errorObj = error as {\n message?: string;\n error?: string;\n status?: number;\n statusCode?: number;\n statusText?: string;\n name?: string;\n cause?: unknown;\n };\n\n // Prefer .message if it exists and is a non-empty string\n if (typeof errorObj.message === 'string' && errorObj.message.length > 0) {\n return errorObj.message;\n }\n\n // Check for .error property (some Supabase errors use this)\n if (typeof errorObj.error === 'string' && errorObj.error.length > 0) {\n return errorObj.error;\n }\n\n // Construct message from status code if available\n const status = errorObj.status ?? errorObj.statusCode;\n if (status) {\n const statusText = errorObj.statusText || getStatusText(status);\n return `HTTP ${status}: ${statusText}`;\n }\n\n // Fall back to JSON stringification, but handle circular references\n try {\n return JSON.stringify(error);\n } catch {\n return String(error);\n }\n}\n\n/**\n * Get a human-readable status text for common HTTP status codes.\n */\nfunction getStatusText(status: number): string {\n switch (status) {\n case 400:\n return 'Bad Request';\n case 401:\n return 'Unauthorized';\n case 403:\n return 'Forbidden';\n case 404:\n return 'Not Found';\n case 409:\n return 'Conflict';\n case 413:\n return 'Payload Too Large';\n case 422:\n return 'Unprocessable Entity';\n case 429:\n return 'Too Many Requests';\n case 500:\n return 'Internal Server Error';\n case 502:\n return 'Bad Gateway';\n case 503:\n return 'Service Unavailable';\n case 504:\n return 'Gateway Timeout';\n default:\n return 'Error';\n }\n}\n\n/**\n * Normalize a storage path by removing leading slashes.\n *\n * Supabase Storage expects paths without leading slashes.\n * This function ensures the path is properly formatted.\n *\n * @param path - The storage path to normalize\n * @returns The normalized path without leading slashes\n *\n * @example\n * ```typescript\n * normalizeStoragePath('/foo/bar.jpg'); // 'foo/bar.jpg'\n * normalizeStoragePath('foo/bar.jpg'); // 'foo/bar.jpg'\n * normalizeStoragePath('//foo/bar.jpg'); // 'foo/bar.jpg'\n * ```\n */\nexport function normalizeStoragePath(path: string): string {\n // Remove leading slashes\n let normalized = path;\n while (normalized.startsWith('/')) {\n normalized = normalized.slice(1);\n }\n return normalized;\n}","/**\n * Supabase Storage Adapter for @pol-studios/powersync\n *\n * Implements the unified SupabaseStorage interface for attachment operations.\n * Uses Supabase Storage with platform-agnostic file system operations.\n *\n * Key features:\n * - Memory-efficient downloads using signed URLs + native file system\n * - Multi-bucket support via BucketConfig/BucketResolver\n * - Platform-agnostic via FileSystemAdapter interface\n * - Implements unified SupabaseStorage interface\n */\n\nimport type { FileSystemAdapter, LoggerAdapter } from '../platform/types';\nimport type { SupabaseStorage, SupabaseClient } from './types';\nimport { resolveBucket } from './types';\nimport { isStorageAuthError, getStorageErrorMessage, normalizeStoragePath } from './auth-error-utils';\nimport { AbortError } from '../utils/retry';\n\n/**\n * Supabase Storage adapter implementing the unified SupabaseStorage interface.\n *\n * Uses PlatformAdapter's fileSystem for I/O operations to remain platform-agnostic.\n *\n * IMPORTANT: This adapter downloads files using signed URLs + platform file system\n * operations to avoid loading entire files into memory. This prevents OOM crashes\n * on large files that occurred with the previous blob-based approach.\n *\n * @example\n * ```typescript\n * const adapter = new SupabaseStorageAdapter(\n * {\n * client: supabaseClient,\n * defaultBucket: 'attachments',\n * bucketMap: new Map([['avatars/', 'user-avatars']]),\n * },\n * platform.fileSystem\n * );\n *\n * // Download returns file:// URI\n * const localUri = await adapter.download('photos/image.jpg');\n *\n * // Upload from local file\n * await adapter.upload('photos/new.jpg', localUri, 'image/jpeg');\n *\n * // Delete from remote storage\n * await adapter.delete('photos/old.jpg');\n * ```\n */\n/**\n * Image transform options for Supabase Storage.\n * Used to compress/resize images on download via Supabase's transform feature.\n */\nexport interface ImageTransformOptions {\n /** Enable image transformation (default: true) */\n enabled?: boolean;\n /** Image width in pixels (default: 2048) */\n width?: number;\n /** Image height in pixels (optional, maintains aspect ratio if omitted) */\n height?: number;\n /** Compression quality 1-100 (default: 70) */\n quality?: number;\n /** Output format (default: 'origin' to keep original format) */\n format?: 'origin' | 'avif' | 'webp';\n /** Resize mode (default: 'contain') */\n resize?: 'cover' | 'contain' | 'fill';\n}\n\n/**\n * Options for SupabaseStorageAdapter constructor.\n */\nexport interface SupabaseStorageAdapterOptions {\n client: SupabaseClient;\n defaultBucket: string;\n bucketMap?: Map<string, string>;\n bucketResolver?: (storagePath: string) => string | undefined;\n signedUrlExpiry?: number;\n logger?: LoggerAdapter;\n /** Image transform options for compression on download */\n imageTransform?: ImageTransformOptions;\n /**\n * Whether to use signed URLs for downloads (default: true).\n * Set to false for public buckets to use getPublicUrl instead of createSignedUrl.\n * This avoids rate limits and simplifies access for public assets.\n */\n useSignedUrls?: boolean;\n /**\n * Custom URL resolver for non-Supabase backends.\n * When provided, getDownloadUrl will use this instead of Supabase Storage API.\n */\n customUrlResolver?: (path: string) => Promise<string> | string;\n}\n\n// Image extensions that support Supabase transform\nconst IMAGE_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.webp', '.avif', '.gif', '.heic', '.heif']);\nexport class SupabaseStorageAdapter implements SupabaseStorage {\n private client: SupabaseClient;\n private defaultBucket: string;\n private bucketMap?: Map<string, string>;\n private bucketResolver?: (storagePath: string) => string | undefined;\n private signedUrlExpiry: number;\n private fileSystem?: FileSystemAdapter;\n private logger?: LoggerAdapter;\n private imageTransform?: ImageTransformOptions;\n private useSignedUrls: boolean;\n private customUrlResolver?: (path: string) => Promise<string> | string;\n constructor(options: SupabaseStorageAdapterOptions, fileSystem?: FileSystemAdapter) {\n this.client = options.client;\n this.defaultBucket = options.defaultBucket;\n this.bucketMap = options.bucketMap;\n this.bucketResolver = options.bucketResolver;\n this.signedUrlExpiry = options.signedUrlExpiry ?? 60;\n this.fileSystem = fileSystem;\n this.logger = options.logger;\n this.imageTransform = options.imageTransform;\n this.useSignedUrls = options.useSignedUrls ?? true; // Default to signed URLs\n this.customUrlResolver = options.customUrlResolver;\n }\n\n /**\n * Update image transform options (can be set after construction).\n */\n setImageTransform(options: ImageTransformOptions): void {\n this.imageTransform = options;\n }\n\n /**\n * Check if a file path is an image that supports transformation.\n */\n private isTransformableImage(path: string): boolean {\n const ext = path.slice(path.lastIndexOf('.')).toLowerCase();\n return IMAGE_EXTENSIONS.has(ext);\n }\n\n /**\n * Set the file system adapter (can be set after construction).\n */\n setFileSystem(fileSystem: FileSystemAdapter): void {\n this.fileSystem = fileSystem;\n }\n\n /**\n * Resolve the storage bucket for a given file path.\n */\n resolveBucket(filePath: string): string {\n return resolveBucket({\n defaultBucket: this.defaultBucket,\n bucketMap: this.bucketMap,\n bucketResolver: this.bucketResolver\n }, filePath);\n }\n\n // ─── SupabaseStorage Interface ──────────────────────────────────────────────\n\n /**\n * Download a file from Supabase Storage and return as a file:// URI.\n *\n * Uses streaming download to a temp file to avoid loading the entire file\n * into memory. This prevents OOM crashes and FileReader hangs that occur\n * with blob-based downloads in React Native after ~115 downloads.\n *\n * Implements SupabaseStorage.download\n *\n * @param path - Remote storage path\n * @returns file:// URI pointing to the downloaded temp file\n */\n async download(path: string): Promise<string> {\n if (!this.fileSystem) {\n throw new Error('FileSystem adapter not configured - cannot download file');\n }\n\n // Use streaming download to temp file (memory-efficient)\n const cacheDir = this.fileSystem.getCacheDirectory();\n const tempFileName = `download_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`;\n const tempFilePath = `${cacheDir}${tempFileName}`;\n await this.downloadToPath(path, tempFilePath);\n\n // Return file:// URI for efficient file copy path\n return tempFilePath.startsWith('file://') ? tempFilePath : `file://${tempFilePath}`;\n }\n\n /**\n * Download a file from Supabase Storage to a specific local path.\n *\n * Uses the platform's streaming downloadFile method to avoid loading\n * the entire file into memory (prevents OOM on large files).\n *\n * @param remotePath - Remote storage path\n * @param localPath - Local file path to write to\n */\n async downloadToPath(remotePath: string, localPath: string): Promise<void> {\n if (!this.fileSystem) {\n throw new Error('FileSystem adapter not configured');\n }\n const signedUrl = await this.getDownloadUrl(remotePath);\n\n // Use the platform's streaming download method\n // This writes directly to disk without loading the entire file into memory\n await this.fileSystem.downloadFile(signedUrl, localPath);\n }\n\n /**\n * Upload a local file to Supabase Storage.\n *\n * Implements SupabaseStorage.upload\n *\n * @param path - Remote storage path\n * @param localUri - Local file:// URI to upload from\n * @param mediaType - MIME type of the file\n * @param signal - Optional AbortSignal for cancellation\n */\n async upload(path: string, localUri: string, mediaType: string, signal?: AbortSignal): Promise<void> {\n // Check if already aborted\n if (signal?.aborted) {\n throw new AbortError('Upload aborted');\n }\n if (!this.fileSystem) {\n throw new Error('FileSystem adapter not configured - cannot upload file');\n }\n const normalizedPath = normalizeStoragePath(path);\n const bucket = this.resolveBucket(normalizedPath);\n this.logger?.debug?.(`[SupabaseStorageAdapter] Uploading file`, {\n bucket,\n path: normalizedPath,\n localUri,\n mediaType\n });\n\n // Read file content as base64 and convert to blob\n const base64Content = await this.fileSystem.readFile(localUri, 'base64');\n\n // Check abort after file read\n if (signal?.aborted) {\n throw new AbortError('Upload aborted');\n }\n const binaryString = atob(base64Content);\n const bytes = new Uint8Array(binaryString.length);\n for (let i = 0; i < binaryString.length; i++) {\n bytes[i] = binaryString.charCodeAt(i);\n }\n const fileData = new Blob([bytes], {\n type: mediaType\n });\n\n // Upload to Supabase\n const {\n error\n } = await this.client.storage.from(bucket).upload(normalizedPath, fileData, {\n contentType: mediaType,\n upsert: true\n });\n if (error) {\n const errorMessage = getStorageErrorMessage(error);\n throw new Error(`Upload failed for '${normalizedPath}' in bucket '${bucket}': ${errorMessage}`);\n }\n this.logger?.info?.(`[SupabaseStorageAdapter] Upload completed`, {\n path: normalizedPath,\n bucket\n });\n }\n\n /**\n * Delete a file from Supabase Storage.\n *\n * Implements SupabaseStorage.delete\n *\n * @param path - Remote storage path to delete\n */\n async delete(path: string): Promise<void> {\n const normalizedPath = normalizeStoragePath(path);\n const bucket = this.resolveBucket(normalizedPath);\n this.logger?.debug?.(`[SupabaseStorageAdapter] Deleting file`, {\n bucket,\n path: normalizedPath\n });\n const {\n error\n } = await this.client.storage.from(bucket).remove([normalizedPath]);\n if (error) {\n const errorMessage = getStorageErrorMessage(error);\n throw new Error(`Delete failed for '${normalizedPath}' in bucket '${bucket}': ${errorMessage}`);\n }\n this.logger?.info?.(`[SupabaseStorageAdapter] Delete completed`, {\n path: normalizedPath,\n bucket\n });\n }\n\n /**\n * Get a download URL for a remote file.\n *\n * Supports three modes:\n * 1. Custom URL resolver (for non-Supabase backends)\n * 2. Public URLs (for public Supabase buckets, when useSignedUrls=false)\n * 3. Signed URLs (default, for private Supabase buckets)\n *\n * Includes session refresh logic for signed URLs to handle stale tokens during background downloads.\n * If the initial request fails with an auth error (400/401/403), refreshes the session\n * and retries once.\n */\n async getDownloadUrl(remotePath: string): Promise<string> {\n // Normalize the path to remove any leading slashes\n // Supabase Storage expects paths without leading slashes\n const normalizedPath = normalizeStoragePath(remotePath);\n\n // Mode 1: Custom URL resolver (non-Supabase backends)\n if (this.customUrlResolver) {\n this.logger?.debug?.(`[SupabaseStorageAdapter] Using custom URL resolver`, {\n path: normalizedPath\n });\n return this.customUrlResolver(normalizedPath);\n }\n const bucket = this.resolveBucket(normalizedPath);\n\n // Mode 2: Public URLs (for public buckets)\n if (!this.useSignedUrls) {\n this.logger?.debug?.(`[SupabaseStorageAdapter] Creating public URL`, {\n bucket,\n path: normalizedPath\n });\n const {\n data\n } = this.client.storage.from(bucket).getPublicUrl(normalizedPath);\n if (!data?.publicUrl) {\n throw new Error(`Failed to create public URL for '${normalizedPath}' in bucket '${bucket}': No URL returned`);\n }\n return data.publicUrl;\n }\n\n // Mode 3: Signed URLs (default, for private buckets)\n // Build transform options for images if enabled\n const useTransform = this.imageTransform?.enabled !== false && this.isTransformableImage(normalizedPath);\n const signedUrlOptions = useTransform && this.imageTransform ? {\n transform: {\n width: this.imageTransform.width ?? 2048,\n height: this.imageTransform.height,\n quality: this.imageTransform.quality ?? 70,\n format: this.imageTransform.format ?? 'origin' as const,\n resize: this.imageTransform.resize ?? 'contain' as const\n }\n } : undefined;\n this.logger?.debug?.(`[SupabaseStorageAdapter] Creating signed URL`, {\n bucket,\n path: normalizedPath,\n originalPath: remotePath !== normalizedPath ? remotePath : undefined,\n transform: useTransform ? signedUrlOptions?.transform : undefined\n });\n\n // First attempt\n const {\n data,\n error\n } = await this.client.storage.from(bucket).createSignedUrl(normalizedPath, this.signedUrlExpiry, signedUrlOptions);\n if (error) {\n const errorMessage = getStorageErrorMessage(error);\n\n // Check if this is an auth error that might be fixed by refreshing the session\n const isAuthError = isStorageAuthError(error);\n if (isAuthError) {\n this.logger?.warn?.(`[SupabaseStorageAdapter] Auth error creating signed URL, refreshing session and retrying:`, {\n path: normalizedPath,\n bucket,\n error: errorMessage\n });\n try {\n // Refresh the session\n await this.client.auth.refreshSession();\n\n // Retry the request with same transform options\n const {\n data: retryData,\n error: retryError\n } = await this.client.storage.from(bucket).createSignedUrl(normalizedPath, this.signedUrlExpiry, signedUrlOptions);\n if (retryError) {\n const retryErrorMessage = getStorageErrorMessage(retryError);\n throw new Error(`Failed to create signed URL after session refresh: ${retryErrorMessage}`);\n }\n if (!retryData?.signedUrl) {\n throw new Error('Failed to create signed URL after session refresh: No URL returned');\n }\n this.logger?.info?.(`[SupabaseStorageAdapter] Successfully created signed URL after session refresh`);\n return retryData.signedUrl;\n } catch (refreshError) {\n // If refresh also fails, throw the original error with context\n throw new Error(`Failed to create signed URL: ${errorMessage} (session refresh also failed: ${refreshError instanceof Error ? refreshError.message : String(refreshError)})`);\n }\n }\n\n // Non-auth error, throw with normalized error message\n throw new Error(`Failed to create signed URL for '${normalizedPath}' in bucket '${bucket}': ${errorMessage}`);\n }\n if (!data?.signedUrl) {\n throw new Error(`Failed to create signed URL for '${normalizedPath}' in bucket '${bucket}': No URL returned`);\n }\n return data.signedUrl;\n }\n\n /**\n * Check if a file exists in remote storage.\n */\n async exists(remotePath: string): Promise<boolean> {\n const bucket = this.resolveBucket(remotePath);\n try {\n // Use list with a specific path prefix to check existence\n const pathParts = remotePath.split('/');\n const fileName = pathParts.pop();\n const directory = pathParts.join('/');\n const {\n data,\n error\n } = await this.client.storage.from(bucket).list(directory, {\n search: fileName,\n limit: 1\n });\n if (error) {\n return false;\n }\n return data?.some((file: {\n name: string;\n }) => file.name === fileName) ?? false;\n } catch {\n return false;\n }\n }\n\n // ─── Backward Compatibility (AttachmentStorageAdapter) ─────────────────────\n\n /**\n * Download a file from remote storage.\n * Alias for download() - for backward compatibility with AttachmentStorageAdapter interface.\n *\n * @param filePath - Remote storage path\n * @returns file:// URI pointing to the downloaded temp file\n */\n async downloadFile(filePath: string): Promise<string> {\n return this.download(filePath);\n }\n\n // ─── Local File Operations ─────────────────────────────────────────────────\n\n /**\n * Write data to a local file.\n */\n async writeFile(fileURI: string, data: string, options?: {\n encoding?: 'utf8' | 'base64';\n }): Promise<void> {\n if (!this.fileSystem) {\n throw new Error('FileSystem adapter not configured');\n }\n const encoding = options?.encoding ?? 'utf8';\n\n // Ensure parent directory exists\n const parentDir = fileURI.substring(0, fileURI.lastIndexOf('/'));\n if (parentDir) {\n await this.fileSystem.makeDirectory(parentDir, {\n intermediates: true\n });\n }\n await this.fileSystem.writeFile(fileURI, data, encoding);\n }\n\n /**\n * Read data from a local file.\n */\n async readFile(fileURI: string, options?: {\n encoding?: 'utf8' | 'base64';\n mediaType?: string;\n }): Promise<ArrayBuffer> {\n if (!this.fileSystem) {\n throw new Error('FileSystem adapter not configured');\n }\n const encoding = options?.encoding ?? 'utf8';\n const content = await this.fileSystem.readFile(fileURI, encoding);\n if (encoding === 'base64') {\n // Decode base64 to ArrayBuffer\n const binaryString = atob(content);\n const bytes = new Uint8Array(binaryString.length);\n for (let i = 0; i < binaryString.length; i++) {\n bytes[i] = binaryString.charCodeAt(i);\n }\n return bytes.buffer;\n }\n\n // For utf8, encode to ArrayBuffer\n const encoder = new TextEncoder();\n return encoder.encode(content).buffer;\n }\n\n /**\n * Delete a local file.\n */\n async deleteFile(uri: string, _options?: {\n filename?: string;\n }): Promise<void> {\n if (!this.fileSystem) {\n throw new Error('FileSystem adapter not configured');\n }\n await this.fileSystem.deleteFile(uri);\n }\n\n /**\n * Check if a local file exists.\n */\n async fileExists(fileURI: string): Promise<boolean> {\n if (!this.fileSystem) {\n throw new Error('FileSystem adapter not configured');\n }\n const info = await this.fileSystem.getFileInfo(fileURI);\n return info?.exists ?? false;\n }\n\n /**\n * Create a directory.\n */\n async makeDir(uri: string): Promise<void> {\n if (!this.fileSystem) {\n throw new Error('FileSystem adapter not configured');\n }\n await this.fileSystem.makeDirectory(uri, {\n intermediates: true\n });\n }\n\n /**\n * Copy a file.\n */\n async copyFile(sourceUri: string, targetUri: string): Promise<void> {\n if (!this.fileSystem) {\n throw new Error('FileSystem adapter not configured');\n }\n await this.fileSystem.copyFile(sourceUri, targetUri);\n }\n\n /**\n * Get the user storage directory.\n */\n getUserStorageDirectory(): string {\n if (!this.fileSystem) {\n throw new Error('FileSystem adapter not configured');\n }\n return this.fileSystem.getDocumentsDirectory();\n }\n}\n\n/**\n * Create a SupabaseStorageAdapter with simplified configuration.\n *\n * @example\n * ```typescript\n * const adapter = createSupabaseStorageAdapter(\n * supabaseClient,\n * 'attachments',\n * platform.fileSystem\n * );\n * ```\n */\nexport function createSupabaseStorageAdapter(client: SupabaseClient, defaultBucket: string, fileSystem?: FileSystemAdapter, options?: {\n bucketMap?: Map<string, string>;\n bucketResolver?: (storagePath: string) => string | undefined;\n signedUrlExpiry?: number;\n logger?: LoggerAdapter;\n}): SupabaseStorageAdapter {\n return new SupabaseStorageAdapter({\n client,\n defaultBucket,\n bucketMap: options?.bucketMap,\n bucketResolver: options?.bucketResolver,\n signedUrlExpiry: options?.signedUrlExpiry,\n logger: options?.logger\n }, fileSystem);\n}"],"mappings":"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","names":["data"]}
|