@pol-studios/powersync 1.0.6 → 1.0.10
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 +933 -0
- package/dist/CacheSettingsManager-uz-kbnRH.d.ts +461 -0
- package/dist/attachments/index.d.ts +745 -332
- package/dist/attachments/index.js +152 -6
- package/dist/{types-Cd7RhNqf.d.ts → background-sync-ChCXW-EV.d.ts} +53 -2
- package/dist/chunk-24RDMMCL.js +44 -0
- package/dist/chunk-24RDMMCL.js.map +1 -0
- package/dist/chunk-4TXTAEF2.js +2060 -0
- package/dist/chunk-4TXTAEF2.js.map +1 -0
- package/dist/chunk-63PXSPIN.js +358 -0
- package/dist/chunk-63PXSPIN.js.map +1 -0
- package/dist/chunk-654ERHA7.js +1 -0
- package/dist/chunk-A4IBBWGO.js +377 -0
- package/dist/chunk-A4IBBWGO.js.map +1 -0
- package/dist/chunk-BRXQNASY.js +1720 -0
- package/dist/chunk-BRXQNASY.js.map +1 -0
- package/dist/chunk-CAB26E6F.js +142 -0
- package/dist/chunk-CAB26E6F.js.map +1 -0
- package/dist/{chunk-EJ23MXPQ.js → chunk-CGL33PL4.js} +3 -1
- package/dist/chunk-CGL33PL4.js.map +1 -0
- package/dist/{chunk-R4YFWQ3Q.js → chunk-CUCAYK7Z.js} +309 -92
- package/dist/chunk-CUCAYK7Z.js.map +1 -0
- package/dist/chunk-FV2HXEIY.js +124 -0
- package/dist/chunk-FV2HXEIY.js.map +1 -0
- package/dist/chunk-HWSNV45P.js +279 -0
- package/dist/chunk-HWSNV45P.js.map +1 -0
- package/dist/{chunk-62J2DPKX.js → chunk-KN2IZERF.js} +530 -413
- package/dist/chunk-KN2IZERF.js.map +1 -0
- package/dist/{chunk-7EMDVIZX.js → chunk-N75DEF5J.js} +19 -1
- package/dist/chunk-N75DEF5J.js.map +1 -0
- package/dist/chunk-P4HZA6ZT.js +83 -0
- package/dist/chunk-P4HZA6ZT.js.map +1 -0
- package/dist/chunk-P6WOZO7H.js +49 -0
- package/dist/chunk-P6WOZO7H.js.map +1 -0
- package/dist/chunk-T4AO7JIG.js +1 -0
- package/dist/chunk-TGBT5XBE.js +1 -0
- package/dist/{chunk-FPTDATY5.js → chunk-VACPAAQZ.js} +54 -12
- package/dist/chunk-VACPAAQZ.js.map +1 -0
- package/dist/chunk-WGHNIAF7.js +329 -0
- package/dist/chunk-WGHNIAF7.js.map +1 -0
- package/dist/{chunk-3AYXHQ4W.js → chunk-WN5ZJ3E2.js} +108 -47
- package/dist/chunk-WN5ZJ3E2.js.map +1 -0
- package/dist/chunk-XAEII4ZX.js +456 -0
- package/dist/chunk-XAEII4ZX.js.map +1 -0
- package/dist/chunk-XOY2CJ67.js +289 -0
- package/dist/chunk-XOY2CJ67.js.map +1 -0
- package/dist/chunk-YHTZ7VMV.js +1 -0
- package/dist/chunk-YSTEESEG.js +676 -0
- package/dist/chunk-YSTEESEG.js.map +1 -0
- package/dist/chunk-Z6VOBGTU.js +32 -0
- package/dist/chunk-Z6VOBGTU.js.map +1 -0
- package/dist/chunk-ZM4ENYMF.js +230 -0
- package/dist/chunk-ZM4ENYMF.js.map +1 -0
- package/dist/connector/index.d.ts +236 -4
- package/dist/connector/index.js +15 -4
- package/dist/core/index.d.ts +16 -3
- package/dist/core/index.js +6 -2
- package/dist/error/index.d.ts +54 -0
- package/dist/error/index.js +7 -0
- package/dist/error/index.js.map +1 -0
- package/dist/index.d.ts +102 -12
- package/dist/index.js +309 -37
- package/dist/index.native.d.ts +22 -10
- package/dist/index.native.js +309 -38
- package/dist/index.web.d.ts +22 -10
- package/dist/index.web.js +310 -38
- package/dist/maintenance/index.d.ts +118 -0
- package/dist/maintenance/index.js +16 -0
- package/dist/maintenance/index.js.map +1 -0
- package/dist/platform/index.d.ts +16 -1
- package/dist/platform/index.js.map +1 -1
- package/dist/platform/index.native.d.ts +2 -2
- package/dist/platform/index.native.js +1 -1
- package/dist/platform/index.web.d.ts +1 -1
- package/dist/platform/index.web.js +1 -1
- package/dist/pol-attachment-queue-BVAIueoP.d.ts +817 -0
- package/dist/provider/index.d.ts +451 -21
- package/dist/provider/index.js +32 -13
- package/dist/react/index.d.ts +372 -0
- package/dist/react/index.js +25 -0
- package/dist/react/index.js.map +1 -0
- package/dist/storage/index.d.ts +6 -0
- package/dist/storage/index.js +42 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/index.native.d.ts +6 -0
- package/dist/storage/index.native.js +40 -0
- package/dist/storage/index.native.js.map +1 -0
- package/dist/storage/index.web.d.ts +6 -0
- package/dist/storage/index.web.js +40 -0
- package/dist/storage/index.web.js.map +1 -0
- package/dist/storage/upload/index.d.ts +54 -0
- package/dist/storage/upload/index.js +15 -0
- package/dist/storage/upload/index.js.map +1 -0
- package/dist/storage/upload/index.native.d.ts +56 -0
- package/dist/storage/upload/index.native.js +15 -0
- package/dist/storage/upload/index.native.js.map +1 -0
- package/dist/storage/upload/index.web.d.ts +2 -0
- package/dist/storage/upload/index.web.js +14 -0
- package/dist/storage/upload/index.web.js.map +1 -0
- package/dist/supabase-connector-T9vHq_3i.d.ts +202 -0
- package/dist/sync/index.d.ts +288 -23
- package/dist/sync/index.js +22 -10
- package/dist/{index-l3iL9Jte.d.ts → types-B212hgfA.d.ts} +101 -158
- package/dist/{types-afHtE1U_.d.ts → types-CDqWh56B.d.ts} +2 -0
- package/dist/types-CyvBaAl8.d.ts +60 -0
- package/dist/types-D0WcHrq6.d.ts +234 -0
- package/package.json +89 -5
- package/dist/chunk-32OLICZO.js +0 -1
- package/dist/chunk-3AYXHQ4W.js.map +0 -1
- package/dist/chunk-5FIMA26D.js +0 -1
- package/dist/chunk-62J2DPKX.js.map +0 -1
- package/dist/chunk-7EMDVIZX.js.map +0 -1
- package/dist/chunk-EJ23MXPQ.js.map +0 -1
- package/dist/chunk-FPTDATY5.js.map +0 -1
- package/dist/chunk-KCDG2MNP.js +0 -1431
- package/dist/chunk-KCDG2MNP.js.map +0 -1
- package/dist/chunk-OLHGI472.js +0 -1
- package/dist/chunk-PAFBKNL3.js +0 -99
- package/dist/chunk-PAFBKNL3.js.map +0 -1
- package/dist/chunk-R4YFWQ3Q.js.map +0 -1
- package/dist/chunk-V6LJ6MR2.js +0 -740
- package/dist/chunk-V6LJ6MR2.js.map +0 -1
- package/dist/chunk-VJCL2SWD.js +0 -1
- package/dist/failed-upload-store-C0cLxxPz.d.ts +0 -33
- /package/dist/{chunk-32OLICZO.js.map → chunk-654ERHA7.js.map} +0 -0
- /package/dist/{chunk-5FIMA26D.js.map → chunk-T4AO7JIG.js.map} +0 -0
- /package/dist/{chunk-OLHGI472.js.map → chunk-TGBT5XBE.js.map} +0 -0
- /package/dist/{chunk-VJCL2SWD.js.map → chunk-YHTZ7VMV.js.map} +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/provider/context.ts","../src/provider/hooks.ts"],"sourcesContent":["/**\n * React Contexts for @pol-studios/powersync\n *\n * This module creates the React contexts used by the PowerSyncProvider.\n */\n\nimport { createContext } from 'react';\nimport type { PowerSyncContextValue, SyncStatusContextValue, ConnectionHealthContextValue, SyncMetricsContextValue, ConnectionStatusContextValue, SyncActivityContextValue, PendingMutationsContextValue, FailedTransactionsContextValue, CompletedTransactionsContextValue, SyncModeContextValue } from './types';\nimport type { PolAttachmentQueue } from '../attachments/pol-attachment-queue';\n\n// ─── Main PowerSync Context ──────────────────────────────────────────────────\n\n/**\n * Main context for PowerSync database instance and related state.\n *\n * Provides access to:\n * - PowerSync database instance\n * - SupabaseConnector instance\n * - AttachmentQueue instance (if configured)\n * - Initialization state\n *\n * @example\n * ```typescript\n * const { db, isReady, error } = useContext(PowerSyncContext);\n * ```\n */\nexport const PowerSyncContext = createContext<PowerSyncContextValue | null>(null);\nPowerSyncContext.displayName = 'PowerSyncContext';\n\n// ─── Sync Status Context ─────────────────────────────────────────────────────\n\n/**\n * Context for sync status updates.\n *\n * Provides access to:\n * - Current sync status (connected, syncing, etc.)\n * - Pending mutations count\n * - Paused state\n * - Last synced timestamp\n *\n * @example\n * ```typescript\n * const { status, pendingCount, isPaused } = useContext(SyncStatusContext);\n * ```\n */\nexport const SyncStatusContext = createContext<SyncStatusContextValue | null>(null);\nSyncStatusContext.displayName = 'SyncStatusContext';\n\n// ─── Split Contexts (Performance Optimization) ───────────────────────────────\n// These focused contexts allow components to subscribe to only the data they need.\n\n/**\n * Context for connection status only.\n * Subscribe to this instead of SyncStatusContext when you only need connection state.\n */\nexport const ConnectionStatusContext = createContext<ConnectionStatusContextValue | null>(null);\nConnectionStatusContext.displayName = 'ConnectionStatusContext';\n\n/**\n * Context for sync activity (uploading/downloading).\n * Subscribe to this instead of SyncStatusContext when you only need sync progress.\n */\nexport const SyncActivityContext = createContext<SyncActivityContextValue | null>(null);\nSyncActivityContext.displayName = 'SyncActivityContext';\n\n/**\n * Context for pending mutations.\n * Subscribe to this instead of SyncStatusContext when you only need pending upload info.\n */\nexport const PendingMutationsContext = createContext<PendingMutationsContextValue | null>(null);\nPendingMutationsContext.displayName = 'PendingMutationsContext';\n\n/**\n * Context for failed transactions.\n * Subscribe to this instead of SyncStatusContext when you only need error info.\n */\nexport const FailedTransactionsContext = createContext<FailedTransactionsContextValue | null>(null);\nFailedTransactionsContext.displayName = 'FailedTransactionsContext';\n\n/**\n * Context for completed transactions.\n * Subscribe to this instead of SyncStatusContext when you only need sync history.\n */\nexport const CompletedTransactionsContext = createContext<CompletedTransactionsContextValue | null>(null);\nCompletedTransactionsContext.displayName = 'CompletedTransactionsContext';\n\n/**\n * Context for sync mode control.\n * Subscribe to this instead of SyncStatusContext when you only need sync mode.\n */\nexport const SyncModeContext = createContext<SyncModeContextValue | null>(null);\nSyncModeContext.displayName = 'SyncModeContext';\n\n// ─── Connection Health Context ───────────────────────────────────────────────\n\n/**\n * Context for connection health monitoring.\n *\n * Provides access to:\n * - Health status (healthy, degraded, disconnected)\n * - Latency measurements\n * - Health check timestamps\n * - Failure counts\n *\n * @example\n * ```typescript\n * const { health } = useContext(ConnectionHealthContext);\n * if (health.status === 'degraded') {\n * showWarning('Connection is slow');\n * }\n * ```\n */\nexport const ConnectionHealthContext = createContext<ConnectionHealthContextValue | null>(null);\nConnectionHealthContext.displayName = 'ConnectionHealthContext';\n\n// ─── Sync Metrics Context ────────────────────────────────────────────────────\n\n/**\n * Context for sync metrics and statistics.\n *\n * Provides access to:\n * - Sync operation counts\n * - Sync durations\n * - Data transfer amounts\n * - Error tracking\n *\n * @example\n * ```typescript\n * const { metrics } = useContext(SyncMetricsContext);\n * console.log(`Total syncs: ${metrics.totalSyncs}`);\n * ```\n */\nexport const SyncMetricsContext = createContext<SyncMetricsContextValue | null>(null);\nSyncMetricsContext.displayName = 'SyncMetricsContext';\n\n// ─── Attachment Queue Context ────────────────────────────────────────────────\n\n/**\n * Context for the attachment queue (if configured).\n *\n * Provides direct access to the AttachmentQueue instance for:\n * - Checking attachment sync stats\n * - Pausing/resuming downloads\n * - Getting local URIs for attachments\n *\n * @example\n * ```typescript\n * const attachmentQueue = useContext(AttachmentQueueContext);\n * if (attachmentQueue) {\n * const stats = await attachmentQueue.getStats();\n * console.log(`Downloaded: ${stats.syncedCount}/${stats.totalExpected}`);\n * }\n * ```\n */\nexport const AttachmentQueueContext = createContext<PolAttachmentQueue | null>(null);\nAttachmentQueueContext.displayName = 'AttachmentQueueContext';","import { c as _c } from \"react/compiler-runtime\";\n/**\n * React Hooks for @pol-studios/powersync\n *\n * This module provides React hooks for accessing PowerSync functionality\n * within the provider's context.\n */\n\nimport { useContext, useCallback, useMemo, useRef, useState, useEffect } from 'react';\nimport type { AbstractPowerSyncDatabase, SyncStatus, ConnectionHealth, SyncMetrics, CrudEntry, EntitySyncState, FailedTransaction, CompletedTransaction, SyncError } from '../core/types';\nimport type { PlatformAdapter } from '../platform/types';\nimport type { PowerSyncContextValue, SyncStatusContextValue, ConnectionHealthContextValue, SyncMetricsContextValue, ConnectionStatusContextValue, SyncActivityContextValue, PendingMutationsContextValue, FailedTransactionsContextValue, CompletedTransactionsContextValue, SyncModeContextValue } from './types';\nimport type { SyncScope, SyncControlActions } from '../sync/types';\nimport type { PolAttachmentQueue } from '../attachments/pol-attachment-queue';\nimport type { SupabaseConnector } from '../connector/supabase-connector';\nimport { PowerSyncContext, SyncStatusContext, ConnectionHealthContext, SyncMetricsContext, AttachmentQueueContext, ConnectionStatusContext, SyncActivityContext, PendingMutationsContext, FailedTransactionsContext, CompletedTransactionsContext, SyncModeContext } from './context';\nimport type { SyncMode } from '../core/types';\n// Note: STORAGE_KEY_PAUSED and STORAGE_KEY_SYNC_MODE are handled by the status tracker\n\n// Track warned call sites to avoid console spam during re-renders.\n// Intentionally module-scoped: This is a dev-only debugging feature that tracks\n// which call sites have already been warned about using deprecated hooks.\n// The Set persists across React lifecycles by design to prevent duplicate warnings.\nconst warnedSyncStatusCallSites = __DEV__ ? new Set<string>() : null;\n\n// Duration to show 'synced' state after entity finishes syncing\nconst SYNCED_DISPLAY_DURATION_MS = 3000;\n\n// ─── Main Hook ───────────────────────────────────────────────────────────────\n\n/**\n * Hook to access the PowerSync database and related services.\n *\n * @returns PowerSync context value with database, connector, and state\n * @throws Error if used outside of PowerSyncProvider\n *\n * @example\n * ```typescript\n * function MyComponent() {\n * const { db, isReady, error } = usePowerSync();\n *\n * if (!isReady) return <LoadingSpinner />;\n * if (error) return <Error message={error.message} />;\n *\n * // Use db for queries\n * const users = await db.getAll('SELECT * FROM users');\n * }\n * ```\n */\nexport function usePowerSync() {\n const context = useContext(PowerSyncContext);\n if (!context) {\n throw new Error(\"usePowerSync must be used within a PowerSyncProvider\");\n }\n return context as PowerSyncContextValue<TSchema>;\n}\n\n// ─── Sync Status Hook ────────────────────────────────────────────────────────\n\n/**\n * @deprecated This hook causes re-renders on any status change.\n * Use focused hooks instead:\n *\n * | Old (useSyncStatus) | New hook |\n * |--------------------------------|-----------------------------|\n * | connected, hasSynced | useConnectionStatus() |\n * | uploading, downloading | useSyncActivityContext() |\n * | pendingCount, pendingMutations | usePendingMutationsContext()|\n * | failedTransactions | useFailedTransactions() |\n * | syncMode, setSyncMode | useSyncMode() |\n * | triggerSync, pause, resume | useSyncControl() |\n *\n * @returns Sync status with connection state, pending uploads, and progress\n * @throws Error if used outside of PowerSyncProvider\n *\n * @example\n * ```typescript\n * function SyncIndicator() {\n * const { status, pendingCount, isPaused } = useSyncStatus();\n *\n * if (status.downloading) {\n * const { current, target, percentage } = status.downloadProgress ?? {};\n * return <Progress value={percentage} label={`${current}/${target}`} />;\n * }\n *\n * if (pendingCount > 0) {\n * return <Badge>{pendingCount} pending uploads</Badge>;\n * }\n *\n * return <Text>Synced</Text>;\n * }\n * ```\n */\nexport function useSyncStatus() {\n if (__DEV__ && warnedSyncStatusCallSites) {\n const stack = new Error().stack?.split(\"\\n\")[2] ?? \"unknown\";\n if (!warnedSyncStatusCallSites.has(stack)) {\n warnedSyncStatusCallSites.add(stack);\n console.warn(\"[useSyncStatus] Deprecated: This hook causes re-renders on any status change. Use focused hooks: useConnectionStatus, useSyncActivityContext, usePendingMutationsContext, etc. See migration guide in JSDoc.\");\n }\n }\n const context = useContext(SyncStatusContext);\n if (!context) {\n throw new Error(\"useSyncStatus must be used within a PowerSyncProvider\");\n }\n return context;\n}\n\n// ─── Focused Context Hooks (Performance Optimization) ────────────────────────\n// These hooks subscribe to only specific data, preventing unnecessary re-renders.\n\n/**\n * Hook to access connection status only.\n * Use this instead of useSyncStatus when you only need connection state.\n *\n * @returns Connection status with connected, connecting, hasSynced states\n * @throws Error if used outside of PowerSyncProvider\n *\n * @example\n * ```typescript\n * function ConnectionBadge() {\n * const { connected, connecting } = useConnectionStatus();\n *\n * if (connecting) return <Badge>Connecting...</Badge>;\n * return <Badge color={connected ? 'green' : 'red'}>{connected ? 'Online' : 'Offline'}</Badge>;\n * }\n * ```\n */\nexport function useConnectionStatus() {\n const context = useContext(ConnectionStatusContext);\n if (!context) {\n throw new Error(\"useConnectionStatus must be used within a PowerSyncProvider\");\n }\n return context;\n}\n\n/**\n * Hook to access sync activity (uploading/downloading).\n * Use this instead of useSyncStatus when you only need sync progress.\n *\n * @returns Sync activity with uploading, downloading, and downloadProgress\n * @throws Error if used outside of PowerSyncProvider\n *\n * @example\n * ```typescript\n * function SyncProgressBar() {\n * const { downloading, downloadProgress } = useSyncActivityContext();\n *\n * if (!downloading) return null;\n * return <ProgressBar value={downloadProgress?.percentage ?? 0} />;\n * }\n * ```\n */\nexport function useSyncActivityContext() {\n const context = useContext(SyncActivityContext);\n if (!context) {\n throw new Error(\"useSyncActivityContext must be used within a PowerSyncProvider\");\n }\n return context;\n}\n\n/**\n * Hook to access pending mutations.\n * Use this instead of useSyncStatus when you only need pending upload info.\n *\n * @returns Pending mutations with count and discard functions\n * @throws Error if used outside of PowerSyncProvider\n *\n * @example\n * ```typescript\n * function PendingChangesIndicator() {\n * const { pendingCount } = usePendingMutationsContext();\n *\n * if (pendingCount === 0) return null;\n * return <Badge>{pendingCount} pending</Badge>;\n * }\n * ```\n */\nexport function usePendingMutationsContext() {\n const context = useContext(PendingMutationsContext);\n if (!context) {\n throw new Error(\"usePendingMutationsContext must be used within a PowerSyncProvider\");\n }\n return context;\n}\n\n/**\n * Hook to access failed transactions.\n * Use this instead of useSyncStatus when you only need error info.\n *\n * @returns Failed transactions with error counts and clear/retry functions\n * @throws Error if used outside of PowerSyncProvider\n *\n * @example\n * ```typescript\n * function SyncErrorBanner() {\n * const { hasUploadErrors, permanentErrorCount, retryFailure, failedTransactions } = useFailedTransactionsContext();\n *\n * if (!hasUploadErrors) return null;\n * return (\n * <Banner onRetry={() => failedTransactions.forEach(f => retryFailure(f.id))}>\n * {permanentErrorCount} changes failed to sync\n * </Banner>\n * );\n * }\n * ```\n */\nexport function useFailedTransactionsContext() {\n const context = useContext(FailedTransactionsContext);\n if (!context) {\n throw new Error(\"useFailedTransactionsContext must be used within a PowerSyncProvider\");\n }\n return context;\n}\n\n/**\n * Hook to access completed transactions history.\n * Use this instead of useSyncStatus when you only need sync history.\n *\n * @returns Completed transactions with clear function\n * @throws Error if used outside of PowerSyncProvider\n *\n * @example\n * ```typescript\n * function RecentSyncs() {\n * const { completedTransactions, clearCompletedHistory } = useCompletedTransactionsContext();\n *\n * return (\n * <View>\n * {completedTransactions.map(t => (\n * <Text key={t.id}>Synced {t.affectedEntityIds.length} items</Text>\n * ))}\n * <Button onPress={clearCompletedHistory}>Clear</Button>\n * </View>\n * );\n * }\n * ```\n */\nexport function useCompletedTransactionsContext() {\n const context = useContext(CompletedTransactionsContext);\n if (!context) {\n throw new Error(\"useCompletedTransactionsContext must be used within a PowerSyncProvider\");\n }\n return context;\n}\n\n/**\n * Hook to access sync mode control.\n * Use this instead of useSyncStatus when you only need sync mode.\n *\n * @returns Sync mode with isPaused state and control functions\n * @throws Error if used outside of PowerSyncProvider\n *\n * @example\n * ```typescript\n * function SyncModeToggle() {\n * const { syncMode, isPaused, setSyncMode } = useSyncModeContext();\n *\n * return (\n * <Toggle\n * value={!isPaused}\n * onValueChange={(on) => setSyncMode(on ? 'push-pull' : 'offline')}\n * label={isPaused ? 'Offline' : syncMode}\n * />\n * );\n * }\n * ```\n */\nexport function useSyncModeContext() {\n const context = useContext(SyncModeContext);\n if (!context) {\n throw new Error(\"useSyncModeContext must be used within a PowerSyncProvider\");\n }\n return context;\n}\n\n// ─── Sync Control Hook ───────────────────────────────────────────────────────\n\n/**\n * Hook to control sync operations.\n *\n * @returns Actions for triggering, pausing, and resuming sync\n *\n * @example\n * ```typescript\n * function SyncControls() {\n * const { triggerSync, syncNow, pause, resume, disconnect, setSyncMode } = useSyncControl();\n * const { isPaused, syncMode } = useSyncStatus();\n *\n * return (\n * <View>\n * <Button onPress={syncNow}>Sync Now</Button>\n * {isPaused ? (\n * <Button onPress={resume}>Resume</Button>\n * ) : (\n * <Button onPress={pause}>Pause</Button>\n * )}\n * </View>\n * );\n * }\n * ```\n */\nexport function useSyncControl() {\n const $ = _c(35);\n const {\n db,\n connector,\n platform\n } = usePowerSync();\n const {\n setSyncMode: setContextSyncMode,\n setForceNextUpload\n } = useSyncModeContext();\n const scopeRef = useRef(null);\n let t0;\n if ($[0] !== connector || $[1] !== db || $[2] !== platform || $[3] !== setContextSyncMode) {\n t0 = async mode => {\n await setContextSyncMode(mode);\n if (mode === \"offline\") {\n if (db?.connected) {\n platform.logger.info(\"[useSyncControl] Mode changed to offline - disconnecting\");\n await db.disconnect();\n }\n } else {\n if (db && connector && !db.connected) {\n platform.logger.info(\"[useSyncControl] Mode changed to\", mode, \"- reconnecting\");\n await db.connect(connector);\n }\n }\n };\n $[0] = connector;\n $[1] = db;\n $[2] = platform;\n $[3] = setContextSyncMode;\n $[4] = t0;\n } else {\n t0 = $[4];\n }\n const setSyncMode = t0;\n let t1;\n if ($[5] !== connector || $[6] !== db || $[7] !== platform || $[8] !== setForceNextUpload) {\n t1 = async () => {\n if (!db || !connector) {\n platform.logger.warn(\"[useSyncControl] Cannot sync - database not initialized\");\n return;\n }\n setForceNextUpload(true);\n platform.logger.info(\"[useSyncControl] Sync Now triggered - forcing full sync\");\n if (db.connected) {\n await db.disconnect();\n }\n await db.connect(connector);\n platform.logger.info(\"[useSyncControl] Connected, sync should start automatically\");\n };\n $[5] = connector;\n $[6] = db;\n $[7] = platform;\n $[8] = setForceNextUpload;\n $[9] = t1;\n } else {\n t1 = $[9];\n }\n const syncNow = t1;\n let t2;\n if ($[10] !== connector || $[11] !== db || $[12] !== platform || $[13] !== setContextSyncMode) {\n t2 = async () => {\n if (!db || !connector) {\n platform.logger.warn(\"[useSyncControl] Cannot trigger sync - not initialized\");\n return;\n }\n await setContextSyncMode(\"push-pull\");\n if (db.connected) {\n platform.logger.info(\"[useSyncControl] Disconnecting to force fresh sync...\");\n await db.disconnect();\n }\n platform.logger.info(\"[useSyncControl] Connecting...\");\n await db.connect(connector);\n platform.logger.info(\"[useSyncControl] Connected, sync should start automatically\");\n };\n $[10] = connector;\n $[11] = db;\n $[12] = platform;\n $[13] = setContextSyncMode;\n $[14] = t2;\n } else {\n t2 = $[14];\n }\n const triggerSync = t2;\n let t3;\n if ($[15] !== platform || $[16] !== setSyncMode) {\n t3 = async () => {\n await setSyncMode(\"offline\");\n platform.logger.info(\"[useSyncControl] Sync paused\");\n };\n $[15] = platform;\n $[16] = setSyncMode;\n $[17] = t3;\n } else {\n t3 = $[17];\n }\n const pause = t3;\n let t4;\n if ($[18] !== platform || $[19] !== setSyncMode) {\n t4 = async () => {\n await setSyncMode(\"push-pull\");\n platform.logger.info(\"[useSyncControl] Sync resumed\");\n };\n $[18] = platform;\n $[19] = setSyncMode;\n $[20] = t4;\n } else {\n t4 = $[20];\n }\n const resume = t4;\n let t5;\n if ($[21] !== db || $[22] !== platform) {\n t5 = async () => {\n if (!db) {\n platform.logger.warn(\"[useSyncControl] Cannot disconnect - not initialized\");\n return;\n }\n platform.logger.info(\"[useSyncControl] Disconnecting...\");\n await db.disconnect();\n platform.logger.info(\"[useSyncControl] Disconnected\");\n };\n $[21] = db;\n $[22] = platform;\n $[23] = t5;\n } else {\n t5 = $[23];\n }\n const disconnect = t5;\n let t6;\n if ($[24] !== connector || $[25] !== platform) {\n t6 = scope => {\n scopeRef.current = scope;\n if (connector && scope) {\n connector.setActiveProjectIds(scope.ids);\n platform.logger.info(\"[useSyncControl] Scope set:\", scope);\n }\n };\n $[24] = connector;\n $[25] = platform;\n $[26] = t6;\n } else {\n t6 = $[26];\n }\n const setScope = t6;\n let t7;\n if ($[27] !== disconnect || $[28] !== pause || $[29] !== resume || $[30] !== setScope || $[31] !== setSyncMode || $[32] !== syncNow || $[33] !== triggerSync) {\n t7 = {\n triggerSync,\n syncNow,\n pause,\n resume,\n disconnect,\n setScope,\n setSyncMode\n };\n $[27] = disconnect;\n $[28] = pause;\n $[29] = resume;\n $[30] = setScope;\n $[31] = setSyncMode;\n $[32] = syncNow;\n $[33] = triggerSync;\n $[34] = t7;\n } else {\n t7 = $[34];\n }\n return t7;\n}\n\n// ─── Sync Mode Hook ──────────────────────────────────────────────────────────\n\n/**\n * Hook to get and set the current sync mode.\n *\n * @returns Object with current mode, setter, and capability flags\n *\n * @example\n * ```typescript\n * function SyncModeSelector() {\n * const { mode, setMode, canUpload, canDownload } = useSyncMode();\n *\n * return (\n * <View>\n * <Text>Current mode: {mode}</Text>\n * <Text>Can upload: {canUpload ? 'Yes' : 'No'}</Text>\n * <Button onPress={() => setMode('pull-only')}>Download Only</Button>\n * </View>\n * );\n * }\n * ```\n */\nexport function useSyncMode() {\n const $ = _c(6);\n const {\n syncMode,\n setSyncMode,\n networkReachable\n } = useSyncModeContext();\n const t0 = networkReachable && syncMode === \"push-pull\";\n const t1 = syncMode !== \"offline\";\n let t2;\n if ($[0] !== networkReachable || $[1] !== setSyncMode || $[2] !== syncMode || $[3] !== t0 || $[4] !== t1) {\n t2 = {\n mode: syncMode,\n setMode: setSyncMode,\n canUpload: t0,\n canDownload: t1,\n networkReachable\n };\n $[0] = networkReachable;\n $[1] = setSyncMode;\n $[2] = syncMode;\n $[3] = t0;\n $[4] = t1;\n $[5] = t2;\n } else {\n t2 = $[5];\n }\n return t2;\n}\n\n// ─── Connection Health Hook ──────────────────────────────────────────────────\n\n/**\n * Hook to access connection health status.\n *\n * @returns Current connection health with latency and failure tracking\n * @throws Error if used outside of PowerSyncProvider\n *\n * @example\n * ```typescript\n * function ConnectionIndicator() {\n * const health = useConnectionHealth();\n *\n * const statusColor = {\n * healthy: 'green',\n * degraded: 'yellow',\n * disconnected: 'red',\n * }[health.status];\n *\n * return (\n * <View>\n * <StatusDot color={statusColor} />\n * {health.latency && <Text>{health.latency}ms</Text>}\n * </View>\n * );\n * }\n * ```\n */\nexport function useConnectionHealth() {\n const context = useContext(ConnectionHealthContext);\n if (!context) {\n throw new Error(\"useConnectionHealth must be used within a PowerSyncProvider\");\n }\n return context.health;\n}\n\n// ─── Sync Metrics Hook ───────────────────────────────────────────────────────\n\n/**\n * Hook to access sync metrics.\n *\n * @returns Sync metrics including success rates, timing, and data transfer\n * @throws Error if used outside of PowerSyncProvider\n *\n * @example\n * ```typescript\n * function SyncStats() {\n * const metrics = useSyncMetrics();\n *\n * const successRate = metrics.totalSyncs > 0\n * ? (metrics.successfulSyncs / metrics.totalSyncs * 100).toFixed(1)\n * : 100;\n *\n * return (\n * <View>\n * <Text>Total syncs: {metrics.totalSyncs}</Text>\n * <Text>Success rate: {successRate}%</Text>\n * <Text>Avg duration: {metrics.averageSyncDuration ?? 'N/A'}ms</Text>\n * </View>\n * );\n * }\n * ```\n */\nexport function useSyncMetrics() {\n const context = useContext(SyncMetricsContext);\n if (!context) {\n throw new Error(\"useSyncMetrics must be used within a PowerSyncProvider\");\n }\n return context.metrics;\n}\n\n// ─── Attachment Queue Hook ───────────────────────────────────────────────────\n\n/**\n * Hook to access the attachment queue (if configured).\n *\n * @returns AttachmentQueue instance or null if not configured\n *\n * @example\n * ```typescript\n * function PhotoStats() {\n * const attachmentQueue = useAttachmentQueue();\n * const [stats, setStats] = useState(null);\n *\n * useEffect(() => {\n * if (!attachmentQueue) return;\n *\n * return attachmentQueue.onProgress((newStats) => {\n * setStats(newStats);\n * });\n * }, [attachmentQueue]);\n *\n * if (!stats) return null;\n *\n * return (\n * <View>\n * <Text>Photos: {stats.syncedCount}/{stats.totalExpected}</Text>\n * <Text>Cache used: {formatBytes(stats.syncedSize)}</Text>\n * </View>\n * );\n * }\n * ```\n */\nexport function useAttachmentQueue() {\n return useContext(AttachmentQueueContext);\n}\n\n// ─── Attachment Queue Ready Hook ─────────────────────────────────────────────\n\n/**\n * Hook to check if the attachment queue has finished initializing.\n *\n * This is useful when you need to wait for the attachment queue to be ready\n * before performing upload operations. The attachment queue initializes\n * asynchronously after the database is ready, so there's a brief period\n * where `isReady` is true but `attachmentQueueReady` is false.\n *\n * @returns Whether the attachment queue is ready (true if initialized or if attachments not configured)\n *\n * @example\n * ```typescript\n * function UploadButton() {\n * const attachmentQueue = useAttachmentQueue();\n * const attachmentQueueReady = useAttachmentQueueReady();\n *\n * if (!attachmentQueueReady) {\n * return <Button disabled>Initializing...</Button>;\n * }\n *\n * if (!attachmentQueue) {\n * return <Text>Uploads not configured</Text>;\n * }\n *\n * return <Button onPress={handleUpload}>Upload</Button>;\n * }\n * ```\n */\nexport function useAttachmentQueueReady() {\n const {\n attachmentQueueReady\n } = usePowerSync();\n return attachmentQueueReady;\n}\n\n// ─── Database Query Hook ─────────────────────────────────────────────────────\n\n/**\n * Hook to get the PowerSync database instance.\n * Throws if not ready.\n *\n * @returns The PowerSync database instance\n * @throws Error if not initialized or used outside of PowerSyncProvider\n *\n * @example\n * ```typescript\n * function UserList() {\n * const db = useDatabase();\n * const [users, setUsers] = useState([]);\n *\n * useEffect(() => {\n * db.getAll('SELECT * FROM users').then(setUsers);\n * }, [db]);\n *\n * return <FlatList data={users} />;\n * }\n * ```\n */\nexport function useDatabase() {\n const {\n db,\n isReady,\n error\n } = usePowerSync();\n if (error) {\n throw error;\n }\n if (!isReady || !db) {\n throw new Error(\"PowerSync database is not ready\");\n }\n return db;\n}\n\n// ─── Platform Adapter Hook ───────────────────────────────────────────────────\n\n/**\n * Hook to access the platform adapter.\n *\n * @returns The platform adapter instance\n *\n * @example\n * ```typescript\n * function FileViewer({ filePath }) {\n * const { platform } = usePowerSync();\n *\n * const handleOpen = async () => {\n * const content = await platform.fileSystem.readFile(filePath);\n * // Process content...\n * };\n *\n * return <Button onPress={handleOpen}>Open File</Button>;\n * }\n * ```\n */\nexport function usePlatform() {\n const {\n platform\n } = usePowerSync();\n return platform;\n}\n\n// ─── Online Status Hook ──────────────────────────────────────────────────────\n\n/**\n * Hook to track online/offline status using the platform's network adapter.\n *\n * @returns Whether the device is currently connected to the internet\n *\n * @example\n * ```typescript\n * function OfflineBanner() {\n * const isOnline = useOnlineStatus();\n *\n * if (isOnline) return null;\n *\n * return <Banner type=\"warning\">You are offline</Banner>;\n * }\n * ```\n */\nexport function useOnlineStatus() {\n const $ = _c(4);\n const {\n platform\n } = usePowerSync();\n const [isOnline, setIsOnline] = useState(true);\n let t0;\n if ($[0] !== platform.network) {\n t0 = () => {\n platform.network.isConnected().then(setIsOnline);\n const unsubscribe = platform.network.addConnectionListener(setIsOnline);\n return unsubscribe;\n };\n $[0] = platform.network;\n $[1] = t0;\n } else {\n t0 = $[1];\n }\n let t1;\n if ($[2] !== platform) {\n t1 = [platform];\n $[2] = platform;\n $[3] = t1;\n } else {\n t1 = $[3];\n }\n useEffect(t0, t1);\n return isOnline;\n}\n\n// ─── Pending Mutations Hook ──────────────────────────────────────────────────\n\n/**\n * Hook to get pending mutations that need to be uploaded.\n *\n * @returns Array of pending CRUD entries and count\n *\n * @example\n * ```typescript\n * function PendingChanges() {\n * const { mutations, count } = usePendingMutations();\n *\n * if (count === 0) return null;\n *\n * return (\n * <View>\n * <Text>{count} changes pending upload</Text>\n * <FlatList\n * data={mutations}\n * renderItem={({ item }) => (\n * <Text>{item.op} on {item.table}</Text>\n * )}\n * />\n * </View>\n * );\n * }\n * ```\n */\nexport function usePendingMutations() {\n const $ = _c(3);\n const {\n pendingMutations,\n pendingCount\n } = usePendingMutationsContext();\n let t0;\n if ($[0] !== pendingCount || $[1] !== pendingMutations) {\n t0 = {\n mutations: pendingMutations,\n count: pendingCount\n };\n $[0] = pendingCount;\n $[1] = pendingMutations;\n $[2] = t0;\n } else {\n t0 = $[2];\n }\n return t0;\n}\n\n// ─── Is Syncing Hook ─────────────────────────────────────────────────────────\n\n/**\n * Hook to check if sync is currently active.\n *\n * @returns Whether sync is currently in progress (uploading or downloading)\n *\n * @example\n * ```typescript\n * function SyncButton() {\n * const isSyncing = useIsSyncing();\n * const { triggerSync } = useSyncControl();\n *\n * return (\n * <Button\n * onPress={triggerSync}\n * disabled={isSyncing}\n * >\n * {isSyncing ? 'Syncing...' : 'Sync Now'}\n * </Button>\n * );\n * }\n * ```\n */\nexport function useIsSyncing() {\n const {\n uploading,\n downloading\n } = useSyncActivityContext();\n return uploading || downloading;\n}\n\n// ─── Download Progress Hook ──────────────────────────────────────────────────\n\n/**\n * Hook to get download progress during sync.\n *\n * @returns Download progress or null if not downloading\n *\n * @example\n * ```typescript\n * function DownloadProgress() {\n * const progress = useDownloadProgress();\n *\n * if (!progress) return null;\n *\n * return (\n * <ProgressBar\n * value={progress.percentage}\n * label={`${progress.current}/${progress.target} operations`}\n * />\n * );\n * }\n * ```\n */\nexport function useDownloadProgress() {\n const {\n downloadProgress\n } = useSyncActivityContext();\n return downloadProgress;\n}\n\n// ─── Entity Sync Status Hook ─────────────────────────────────────────────────\n\n/**\n * Return type for useEntitySyncStatus hook.\n */\nexport interface EntitySyncStatusResult {\n /** Current sync state for this entity */\n state: EntitySyncState;\n /** Error details if state is 'error' */\n error: SyncError | null;\n /** Number of pending operations for this entity */\n pendingOperations: number;\n /** The failed transaction if any */\n failedTransaction: FailedTransaction | null;\n /** Dismiss the failure (remove from tracking) */\n dismiss: () => void;\n}\n\n/**\n * Hook to get sync status for a specific entity.\n *\n * Combines local mutation state (from pending mutations) with\n * failure state to provide a unified status for UI.\n *\n * @param entityId - The entity ID to check status for\n * @returns Unified sync state and actions\n *\n * @example\n * ```typescript\n * function EquipmentHeader({ unitId }) {\n * const { state, error, dismiss } = useEntitySyncStatus(unitId);\n *\n * const borderColor = {\n * idle: 'transparent',\n * saving: 'orange',\n * syncing: 'amber',\n * synced: 'green',\n * error: 'red',\n * }[state];\n * }\n * ```\n */\nexport function useEntitySyncStatus(entityId: string | undefined): EntitySyncStatusResult {\n // Use focused context hooks instead of deprecated useSyncStatus\n const {\n pendingMutations\n } = usePendingMutationsContext();\n const {\n failedTransactions,\n clearFailure\n } = useFailedTransactionsContext();\n const [, forceUpdate] = useState(0);\n\n // Track recently synced entities with timestamps using a ref (avoids module-level mutable state)\n const recentlySyncedRef = useRef<Map<string, number>>(new Map());\n\n // Find if entity is in pending mutations\n // Check both entry.id (PowerSync's internal CRUD entry ID) and entry.opData?.id (the actual record ID)\n const entityPendingMutations = useMemo(() => {\n if (!entityId) return [];\n return pendingMutations.filter(entry => entry.id === entityId || String(entry.opData?.id) === entityId);\n }, [entityId, pendingMutations]);\n\n // Find if entity has a failed transaction\n const failedTransaction = useMemo(() => {\n if (!entityId) return null;\n return failedTransactions.find(ft => ft.affectedEntityIds.includes(entityId)) ?? null;\n }, [entityId, failedTransactions]);\n\n // Track transition from syncing to synced\n const wasSyncingRef = useRef(false);\n const isCurrentlySyncing = entityPendingMutations.length > 0;\n\n // When entity transitions from syncing to not syncing (and no error),\n // mark it as recently synced\n useEffect(() => {\n if (!entityId) return;\n if (wasSyncingRef.current && !isCurrentlySyncing && !failedTransaction) {\n recentlySyncedRef.current.set(entityId, Date.now());\n\n // Schedule cleanup after the display duration\n const timer = setTimeout(() => {\n const syncedAt = recentlySyncedRef.current.get(entityId);\n if (syncedAt && Date.now() - syncedAt >= SYNCED_DISPLAY_DURATION_MS) {\n recentlySyncedRef.current.delete(entityId);\n forceUpdate(n => n + 1);\n }\n }, SYNCED_DISPLAY_DURATION_MS);\n return () => {\n clearTimeout(timer);\n recentlySyncedRef.current.delete(entityId);\n };\n }\n wasSyncingRef.current = isCurrentlySyncing;\n }, [entityId, isCurrentlySyncing, failedTransaction]);\n\n // Determine current state\n const state = useMemo((): EntitySyncState => {\n if (!entityId) return 'idle';\n\n // Check for ANY failure (highest priority)\n // Both permanent and transient failures should show error state\n // The UI can use failedTransaction.isPermanent to distinguish if needed\n if (failedTransaction) {\n return 'error';\n }\n\n // Check if currently syncing\n if (entityPendingMutations.length > 0) {\n return 'syncing';\n }\n\n // Check if recently synced\n const syncedAt_0 = recentlySyncedRef.current.get(entityId);\n if (syncedAt_0 && Date.now() - syncedAt_0 < SYNCED_DISPLAY_DURATION_MS) {\n return 'synced';\n }\n return 'idle';\n }, [entityId, failedTransaction, entityPendingMutations.length]);\n\n // Get error from failed transaction\n const error = failedTransaction?.error ?? null;\n\n // Dismiss function - clears the failure from tracking\n const dismiss = useCallback(() => {\n if (failedTransaction) {\n clearFailure(failedTransaction.id);\n }\n }, [failedTransaction, clearFailure]);\n return {\n state,\n error,\n pendingOperations: entityPendingMutations.length,\n failedTransaction,\n dismiss\n };\n}\n\n// ─── Upload Status Hook ──────────────────────────────────────────────────────\n\n/**\n * Return type for useUploadStatus hook.\n */\nexport interface UploadStatusResult {\n /** Number of operations waiting to upload */\n pendingCount: number;\n /** Number of failed transactions */\n failedCount: number;\n /** Number of permanent failures needing user action */\n permanentFailureCount: number;\n /** Whether there are any errors */\n hasErrors: boolean;\n /** Whether there are permanent errors needing attention */\n hasPermanentErrors: boolean;\n /** All failed transactions */\n failedTransactions: FailedTransaction[];\n /** Trigger sync retry (reconnect) */\n retryAll: () => Promise<void>;\n /** Dismiss all failures */\n dismissAll: () => void;\n}\n\n/**\n * Hook to get overall upload status across all entities.\n *\n * @returns Upload status with counts and actions\n *\n * @example\n * ```typescript\n * function SyncStatusBar() {\n * const { pendingCount, failedCount, hasPermanentErrors, retryAll } = useUploadStatus();\n *\n * if (hasPermanentErrors) {\n * return <Banner onRetry={retryAll}>\n * {failedCount} changes failed to sync\n * </Banner>;\n * }\n * }\n * ```\n */\nexport function useUploadStatus() {\n const $ = _c(12);\n const {\n pendingCount\n } = usePendingMutationsContext();\n const {\n failedTransactions,\n hasUploadErrors,\n permanentErrorCount,\n clearAllFailures\n } = useFailedTransactionsContext();\n const {\n triggerSync\n } = useSyncControl();\n let t0;\n if ($[0] !== triggerSync) {\n t0 = async () => {\n await triggerSync();\n };\n $[0] = triggerSync;\n $[1] = t0;\n } else {\n t0 = $[1];\n }\n const retryAll = t0;\n let t1;\n if ($[2] !== clearAllFailures) {\n t1 = () => {\n clearAllFailures();\n };\n $[2] = clearAllFailures;\n $[3] = t1;\n } else {\n t1 = $[3];\n }\n const dismissAll = t1;\n const t2 = permanentErrorCount > 0;\n let t3;\n if ($[4] !== dismissAll || $[5] !== failedTransactions || $[6] !== hasUploadErrors || $[7] !== pendingCount || $[8] !== permanentErrorCount || $[9] !== retryAll || $[10] !== t2) {\n t3 = {\n pendingCount,\n failedCount: failedTransactions.length,\n permanentFailureCount: permanentErrorCount,\n hasErrors: hasUploadErrors,\n hasPermanentErrors: t2,\n failedTransactions,\n retryAll,\n dismissAll\n };\n $[4] = dismissAll;\n $[5] = failedTransactions;\n $[6] = hasUploadErrors;\n $[7] = pendingCount;\n $[8] = permanentErrorCount;\n $[9] = retryAll;\n $[10] = t2;\n $[11] = t3;\n } else {\n t3 = $[11];\n }\n return t3;\n}\n\n// ─── Sync Activity Hook ──────────────────────────────────────────────────────\n\n/**\n * Return type for useSyncActivity hook.\n */\nexport interface SyncActivityResult {\n /** Pending CRUD entries waiting to be synced */\n pending: CrudEntry[];\n /** Failed transactions that need attention */\n failed: FailedTransaction[];\n /** Recently completed transactions */\n completed: CompletedTransaction[];\n /** New completed transactions since last notification display (for toasts/banners) */\n newCompleted: CompletedTransaction[];\n /** Counts summary */\n counts: {\n pending: number;\n failed: number;\n completed: number;\n newCompleted: number;\n };\n /** Whether there is any sync activity to show (pending or failed) */\n hasActivity: boolean;\n /** Retry all failed transactions */\n retryAll: () => Promise<void>;\n /** Retry a specific failed transaction */\n retryFailure: (failureId: string) => Promise<void>;\n /** Dismiss a specific failure */\n dismissFailure: (failureId: string) => void;\n /** Clear all completed transactions from the list */\n clearCompleted: () => void;\n /** Clear a specific completed transaction from the list */\n clearCompletedItem: (completedId: string) => void;\n /** Mark notifications as seen (for auto-dismiss functionality) */\n markNotificationsAsSeen: () => void;\n}\n\n/**\n * Hook to get comprehensive sync activity including pending, failed, and completed transactions.\n *\n * Uses the provider's completed transaction tracking via the status tracker.\n *\n * @returns Sync activity with all transaction states and actions\n *\n * @example\n * ```typescript\n * function SyncActivityBanner() {\n * const { pending, failed, completed, counts, hasActivity, retryAll, clearCompleted } = useSyncActivity();\n *\n * if (!hasActivity && completed.length === 0) return null;\n *\n * return (\n * <View>\n * <Text>{counts.pending} syncing, {counts.failed} failed</Text>\n * {failed.length > 0 && <Button onPress={retryAll}>Retry All</Button>}\n * </View>\n * );\n * }\n * ```\n */\nexport function useSyncActivity() {\n const $ = _c(24);\n const {\n pendingMutations\n } = usePendingMutationsContext();\n const {\n failedTransactions,\n clearFailure,\n retryFailure\n } = useFailedTransactionsContext();\n const {\n completedTransactions,\n clearCompletedHistory,\n clearCompletedItem,\n newCompletedTransactions,\n markNotificationsAsSeen\n } = useCompletedTransactionsContext();\n const {\n uploading: isUploading,\n downloading: isDownloading\n } = useSyncActivityContext();\n const {\n triggerSync\n } = useSyncControl();\n let t0;\n if ($[0] !== triggerSync) {\n t0 = async () => {\n await triggerSync();\n };\n $[0] = triggerSync;\n $[1] = t0;\n } else {\n t0 = $[1];\n }\n const retryAll = t0;\n let t1;\n if ($[2] !== clearFailure) {\n t1 = failureId => {\n clearFailure(failureId);\n };\n $[2] = clearFailure;\n $[3] = t1;\n } else {\n t1 = $[3];\n }\n const dismissFailure = t1;\n let t2;\n if ($[4] !== clearCompletedHistory) {\n t2 = () => {\n clearCompletedHistory();\n };\n $[4] = clearCompletedHistory;\n $[5] = t2;\n } else {\n t2 = $[5];\n }\n const clearCompleted = t2;\n let t3;\n if ($[6] !== completedTransactions.length || $[7] !== failedTransactions.length || $[8] !== newCompletedTransactions.length || $[9] !== pendingMutations.length) {\n t3 = {\n pending: pendingMutations.length,\n failed: failedTransactions.length,\n completed: completedTransactions.length,\n newCompleted: newCompletedTransactions.length\n };\n $[6] = completedTransactions.length;\n $[7] = failedTransactions.length;\n $[8] = newCompletedTransactions.length;\n $[9] = pendingMutations.length;\n $[10] = t3;\n } else {\n t3 = $[10];\n }\n const counts = t3;\n const hasActivity = isUploading || isDownloading || failedTransactions.length > 0;\n let t4;\n if ($[11] !== clearCompleted || $[12] !== clearCompletedItem || $[13] !== completedTransactions || $[14] !== counts || $[15] !== dismissFailure || $[16] !== failedTransactions || $[17] !== hasActivity || $[18] !== markNotificationsAsSeen || $[19] !== newCompletedTransactions || $[20] !== pendingMutations || $[21] !== retryAll || $[22] !== retryFailure) {\n t4 = {\n pending: pendingMutations,\n failed: failedTransactions,\n completed: completedTransactions,\n newCompleted: newCompletedTransactions,\n counts,\n hasActivity,\n retryAll,\n retryFailure,\n dismissFailure,\n clearCompleted,\n clearCompletedItem,\n markNotificationsAsSeen\n };\n $[11] = clearCompleted;\n $[12] = clearCompletedItem;\n $[13] = completedTransactions;\n $[14] = counts;\n $[15] = dismissFailure;\n $[16] = failedTransactions;\n $[17] = hasActivity;\n $[18] = markNotificationsAsSeen;\n $[19] = newCompletedTransactions;\n $[20] = pendingMutations;\n $[21] = retryAll;\n $[22] = retryFailure;\n $[23] = t4;\n } else {\n t4 = $[23];\n }\n return t4;\n}\n\n// ============================================================================\n// Renamed Hooks (cleaner API)\n// ============================================================================\n\n// Note: useSyncActivity and usePendingMutations already exist as higher-level hooks\n// with different return types (lines 707 and 1049). The focused context hooks\n// (useSyncActivityContext, usePendingMutationsContext) retain the \"Context\" suffix\n// to distinguish them from the convenience hooks.\n\n/**\n * Get failed transactions state and retry/dismiss functions.\n * Alias for useFailedTransactionsContext.\n */\nexport const useFailedTransactions = useFailedTransactionsContext;\n\n/**\n * Get completed transactions state and clear function.\n * Alias for useCompletedTransactionsContext.\n */\nexport const useCompletedTransactions = useCompletedTransactionsContext;"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// src/storage/types.ts
|
|
2
|
+
function resolveBucket(options, storagePath) {
|
|
3
|
+
if (options.bucketResolver) {
|
|
4
|
+
const resolved = options.bucketResolver(storagePath);
|
|
5
|
+
if (resolved !== void 0) {
|
|
6
|
+
return resolved;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
if (options.bucketMap) {
|
|
10
|
+
for (const [prefix, bucket] of options.bucketMap) {
|
|
11
|
+
if (storagePath.startsWith(prefix)) {
|
|
12
|
+
return bucket;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return options.defaultBucket;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/storage/upload/types.ts
|
|
20
|
+
var DEFAULT_UPLOAD_NOTIFICATION = {
|
|
21
|
+
enabled: true,
|
|
22
|
+
autoClear: true,
|
|
23
|
+
onProgressTitle: "Uploading...",
|
|
24
|
+
onCompleteTitle: "Upload complete",
|
|
25
|
+
onErrorTitle: "Upload failed"
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
resolveBucket,
|
|
30
|
+
DEFAULT_UPLOAD_NOTIFICATION
|
|
31
|
+
};
|
|
32
|
+
//# sourceMappingURL=chunk-Z6VOBGTU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/storage/types.ts","../src/storage/upload/types.ts"],"sourcesContent":["/**\n * Unified Storage Types for @pol-studios/powersync\n *\n * Single storage interface for all Supabase storage operations.\n * Consolidates the previous 6 fragmented interfaces into one:\n * - AttachmentStorageAdapter (attachments/types.ts)\n * - RemoteStorageAdapter (storage/types.ts - removed)\n * - StorageAdapter (@powersync/attachments)\n * - PowerSyncStorageAdapter (storage/types.ts - removed)\n * - StorageUploadHandler (storage/types.ts - removed)\n * - UploadHandler (attachments/types.ts)\n */\n\n// ─── Unified Storage Interface ──────────────────────────────────────────────\n\n/**\n * Unified storage interface for Supabase storage operations.\n * Handles both upload and download for attachments.\n *\n * This is the ONLY storage interface consumers need to implement.\n * It replaces all previous storage adapter interfaces.\n *\n * @example\n * ```typescript\n * const storage: SupabaseStorage = {\n * async download(path) {\n * const { data } = await supabase.storage\n * .from('attachments')\n * .createSignedUrl(path, 60);\n * // Download to temp file and return URI\n * return tempFileUri;\n * },\n *\n * async upload(path, localUri, mediaType) {\n * const file = await readFile(localUri);\n * await supabase.storage\n * .from('attachments')\n * .upload(path, file, { contentType: mediaType });\n * },\n * };\n * ```\n */\nexport interface SupabaseStorage {\n /**\n * Download a file and return the local file:// URI.\n *\n * @param path - Remote storage path\n * @returns Local file:// URI pointing to the downloaded file\n */\n download(path: string): Promise<string>;\n\n /**\n * Upload a local file to remote storage.\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 upload(path: string, localUri: string, mediaType: string, signal?: AbortSignal): Promise<void>;\n\n /**\n * Delete a file from remote storage.\n * Optional - not all implementations need deletion.\n *\n * @param path - Remote storage path to delete\n */\n delete?(path: string): Promise<void>;\n\n /**\n * Resolve which bucket a path belongs to.\n * Optional - useful for multi-bucket configurations.\n *\n * @param path - Remote storage path\n * @returns The bucket name\n */\n resolveBucket?(path: string): string;\n}\n\n// ─── Storage Backend (Factory API) ──────────────────────────────────────────\n\n/**\n * Download result discriminated union.\n * Native platforms return file URIs, web returns blobs.\n */\nexport type DownloadResult = {\n type: 'file';\n uri: string;\n} | {\n type: 'blob';\n data: Blob;\n};\n\n/**\n * Storage backend returned by createSupabaseStorage factory.\n * This is a more flexible interface that returns discriminated unions\n * to handle both native (file URI) and web (blob) download results.\n *\n * For simpler use cases, use SupabaseStorage interface directly.\n */\nexport interface StorageBackend {\n /** Download a file, returns discriminated union based on platform */\n download(path: string): Promise<DownloadResult>;\n\n /** Upload a local file to remote storage */\n upload(path: string, localUri: string, contentType: string, options?: StorageUploadOptions): Promise<void>;\n\n /** Delete a file from remote storage */\n delete(path: string): Promise<void>;\n\n /** Resolve which bucket a path belongs to */\n resolveBucket(path: string): string;\n}\n\n// ─── Upload Progress ────────────────────────────────────────────────────────\n\n/**\n * Progress information for upload operations.\n */\nexport interface UploadProgress {\n /** Bytes uploaded so far */\n loaded: number;\n /** Total bytes to upload */\n total: number;\n}\n\n// ─── Upload Options ─────────────────────────────────────────────────────────\n\n/**\n * Options for upload operations.\n */\nexport interface StorageUploadOptions {\n /** AbortSignal for cancellation support */\n signal?: AbortSignal;\n /** Progress callback */\n onProgress?: (progress: UploadProgress) => void;\n}\n\n// ─── Supabase Storage Options ───────────────────────────────────────────────\n\nimport type { LoggerAdapter, FileSystemAdapter } from '../platform/types';\n\n/**\n * Options for creating a Supabase storage backend.\n */\nexport interface SupabaseStorageOptions {\n /** Supabase client instance */\n client: SupabaseClient;\n\n /** Default storage bucket */\n defaultBucket: string;\n\n /**\n * Map of path prefixes to bucket names.\n * Checked in iteration order (use Map to preserve order).\n *\n * @example\n * ```typescript\n * bucketMap: new Map([\n * ['avatars/', 'user-avatars'],\n * ['docs/', 'documents'],\n * ])\n * ```\n */\n bucketMap?: Map<string, string>;\n\n /**\n * Custom bucket resolver function.\n * Takes precedence over bucketMap.\n * Return undefined to fall through to bucketMap/defaultBucket.\n */\n bucketResolver?: (storagePath: string) => string | undefined;\n\n /**\n * Signed URL expiry in seconds.\n * @default 60 (short-lived for security)\n */\n signedUrlExpiry?: number;\n\n /**\n * File system adapter for native file operations.\n * Required for native platforms to enable file-based downloads.\n */\n fileSystem?: FileSystemAdapter;\n\n /**\n * Optional logger for diagnostic messages.\n */\n logger?: LoggerAdapter;\n}\n\n// ─── Supabase Client Type ───────────────────────────────────────────────────\n\n/**\n * Transform options for Supabase image transformation.\n * See: https://supabase.com/docs/guides/storage/serving/image-transformations\n */\nexport interface SupabaseTransformOptions {\n width?: number;\n height?: number;\n resize?: 'cover' | 'contain' | 'fill';\n format?: 'origin' | 'avif' | 'webp';\n quality?: number; // 1-100\n}\n\n/**\n * Minimal Supabase client interface for storage operations.\n * Using a minimal interface to avoid version conflicts with @supabase/supabase-js.\n */\nexport interface SupabaseClient {\n storage: {\n from(bucket: string): {\n createSignedUrl(path: string, expiresIn: number, options?: {\n download?: string | boolean;\n transform?: SupabaseTransformOptions;\n }): Promise<{\n data: {\n signedUrl: string;\n } | null;\n error: Error | null;\n }>;\n upload(path: string, file: Blob | ArrayBuffer | FormData, options?: {\n contentType?: string;\n upsert?: boolean;\n }): Promise<{\n data: {\n path: string;\n } | null;\n error: Error | null;\n }>;\n remove(paths: string[]): Promise<{\n data: {\n name: string;\n }[] | null;\n error: Error | null;\n }>;\n list(path?: string, options?: {\n search?: string;\n limit?: number;\n }): Promise<{\n data: {\n name: string;\n }[] | null;\n error: Error | null;\n }>;\n };\n };\n auth: {\n refreshSession(): Promise<{\n data: unknown;\n error: Error | null;\n }>;\n };\n}\n\n// ─── Bucket Resolution Helpers ──────────────────────────────────────────────\n\n/**\n * Resolve bucket from storage path using SupabaseStorageOptions.\n *\n * Resolution order:\n * 1. Custom bucketResolver (if provided and returns a value)\n * 2. bucketMap prefix matching (if provided)\n * 3. defaultBucket\n */\nexport function resolveBucket(options: Pick<SupabaseStorageOptions, 'defaultBucket' | 'bucketMap' | 'bucketResolver'>, storagePath: string): string {\n // Try custom resolver first\n if (options.bucketResolver) {\n const resolved = options.bucketResolver(storagePath);\n if (resolved !== undefined) {\n return resolved;\n }\n }\n\n // Try bucket map prefix matching\n if (options.bucketMap) {\n for (const [prefix, bucket] of options.bucketMap) {\n if (storagePath.startsWith(prefix)) {\n return bucket;\n }\n }\n }\n\n // Fall back to default\n return options.defaultBucket;\n}","/**\n * Upload Handler Types for @pol-studios/powersync\n *\n * Defines types for platform-specific upload handlers.\n */\n\n/**\n * Bucket configuration for multi-bucket routing.\n */\nexport interface BucketConfig {\n /** Default bucket for storage operations */\n defaultBucket: string;\n\n /** Map of path prefixes to bucket names */\n bucketMap?: Map<string, string>;\n\n /** Custom bucket resolver function */\n resolver?: (storagePath: string) => string | undefined;\n}\n\n/**\n * Options for creating a SupabaseUploadHandler.\n */\nexport interface SupabaseUploadHandlerOptions {\n /** Supabase client instance */\n supabaseClient: any; // SupabaseClient - using any to avoid version conflicts\n\n /** Bucket configuration for multi-bucket routing */\n bucketConfig: BucketConfig;\n}\n\n/**\n * Event handlers for upload progress tracking.\n */\nexport interface UploadEventHandlers {\n /** Called with progress updates during upload */\n onProgress?: (progress: {\n loaded: number;\n total: number;\n }) => void;\n\n /** Called when upload completes successfully */\n onComplete?: () => void;\n\n /** Called when upload fails */\n onError?: (error: Error) => void;\n}\n\n/**\n * Configuration for native upload notifications (Android).\n */\nexport interface UploadNotificationConfig {\n /** Whether to show upload notifications */\n enabled: boolean;\n\n /** Whether to auto-clear notification on completion */\n autoClear: boolean;\n\n /** Title shown during upload progress */\n onProgressTitle: string;\n\n /** Title shown when upload completes */\n onCompleteTitle: string;\n\n /** Title shown when upload fails */\n onErrorTitle: string;\n}\n\n/**\n * Default notification configuration for Android uploads.\n */\nexport const DEFAULT_UPLOAD_NOTIFICATION: UploadNotificationConfig = {\n enabled: true,\n autoClear: true,\n onProgressTitle: 'Uploading...',\n onCompleteTitle: 'Upload complete',\n onErrorTitle: 'Upload failed'\n};"],"mappings":";AAyQO,SAAS,cAAc,SAAyF,aAA6B;AAElJ,MAAI,QAAQ,gBAAgB;AAC1B,UAAM,WAAW,QAAQ,eAAe,WAAW;AACnD,QAAI,aAAa,QAAW;AAC1B,aAAO;AAAA,IACT;AAAA,EACF;AAGA,MAAI,QAAQ,WAAW;AACrB,eAAW,CAAC,QAAQ,MAAM,KAAK,QAAQ,WAAW;AAChD,UAAI,YAAY,WAAW,MAAM,GAAG;AAClC,eAAO;AAAA,MACT;AAAA,IACF;AAAA,EACF;AAGA,SAAO,QAAQ;AACjB;;;ACtNO,IAAM,8BAAwD;AAAA,EACnE,SAAS;AAAA,EACT,WAAW;AAAA,EACX,iBAAiB;AAAA,EACjB,iBAAiB;AAAA,EACjB,cAAc;AAChB;","names":[]}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// src/attachments/query-builder.ts
|
|
2
|
+
var VALID_IDENTIFIER_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
3
|
+
var SQL_RESERVED_WORDS = /* @__PURE__ */ 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"]);
|
|
4
|
+
function validateSqlIdentifier(identifier, context) {
|
|
5
|
+
if (!identifier || typeof identifier !== "string") {
|
|
6
|
+
throw new Error(`Invalid ${context}: must be a non-empty string`);
|
|
7
|
+
}
|
|
8
|
+
if (!VALID_IDENTIFIER_PATTERN.test(identifier)) {
|
|
9
|
+
throw new Error(`Invalid ${context}: "${identifier}" contains invalid characters. Identifiers must start with a letter or underscore and contain only alphanumeric characters and underscores.`);
|
|
10
|
+
}
|
|
11
|
+
if (SQL_RESERVED_WORDS.has(identifier.toUpperCase())) {
|
|
12
|
+
throw new Error(`Invalid ${context}: "${identifier}" is a SQL reserved word. Use a different name or quote the identifier.`);
|
|
13
|
+
}
|
|
14
|
+
const dangerousPatterns = [
|
|
15
|
+
/--/,
|
|
16
|
+
// SQL comment
|
|
17
|
+
/;/,
|
|
18
|
+
// Statement terminator
|
|
19
|
+
/'/,
|
|
20
|
+
// String delimiter
|
|
21
|
+
/"/,
|
|
22
|
+
// Quote
|
|
23
|
+
/\\/
|
|
24
|
+
// Escape character
|
|
25
|
+
];
|
|
26
|
+
for (const pattern of dangerousPatterns) {
|
|
27
|
+
if (pattern.test(identifier)) {
|
|
28
|
+
throw new Error(`Invalid ${context}: "${identifier}" contains potentially dangerous characters.`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function validateWhereClause(whereClause) {
|
|
33
|
+
if (!whereClause || typeof whereClause !== "string") {
|
|
34
|
+
throw new Error("Invalid WHERE clause: must be a non-empty string");
|
|
35
|
+
}
|
|
36
|
+
const dangerousPatterns = [{
|
|
37
|
+
pattern: /;\s*(SELECT|INSERT|UPDATE|DELETE|DROP|CREATE)/i,
|
|
38
|
+
name: "SQL injection (statement)"
|
|
39
|
+
}, {
|
|
40
|
+
pattern: /UNION\s+(ALL\s+)?SELECT/i,
|
|
41
|
+
name: "UNION injection"
|
|
42
|
+
}, {
|
|
43
|
+
pattern: /--/,
|
|
44
|
+
name: "SQL comment"
|
|
45
|
+
}, {
|
|
46
|
+
pattern: /\/\*/,
|
|
47
|
+
name: "block comment"
|
|
48
|
+
}, {
|
|
49
|
+
pattern: /xp_|sp_|exec\s*\(/i,
|
|
50
|
+
name: "stored procedure"
|
|
51
|
+
}];
|
|
52
|
+
for (const {
|
|
53
|
+
pattern,
|
|
54
|
+
name
|
|
55
|
+
} of dangerousPatterns) {
|
|
56
|
+
if (pattern.test(whereClause)) {
|
|
57
|
+
throw new Error(`Invalid WHERE clause: contains ${name}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function buildWatchQuery(config) {
|
|
62
|
+
validateSqlIdentifier(config.table, "table");
|
|
63
|
+
validateSqlIdentifier(config.idColumn, "idColumn");
|
|
64
|
+
if (config.selectColumns) {
|
|
65
|
+
for (const col of config.selectColumns) {
|
|
66
|
+
validateSqlIdentifier(col, "selectColumns");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (config.orderBy) {
|
|
70
|
+
validateSqlIdentifier(config.orderBy.column, "orderBy.column");
|
|
71
|
+
if (config.orderBy.direction !== "ASC" && config.orderBy.direction !== "DESC") {
|
|
72
|
+
throw new Error(`Invalid orderBy.direction: must be "ASC" or "DESC"`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (config.where) {
|
|
76
|
+
validateWhereClause(config.where);
|
|
77
|
+
}
|
|
78
|
+
const selectParts = [`${config.idColumn} AS id`];
|
|
79
|
+
if (config.selectColumns && config.selectColumns.length > 0) {
|
|
80
|
+
selectParts.push(...config.selectColumns);
|
|
81
|
+
}
|
|
82
|
+
const selectClause = selectParts.join(", ");
|
|
83
|
+
const fromClause = config.table;
|
|
84
|
+
let whereClause = `${config.idColumn} IS NOT NULL AND ${config.idColumn} != ''`;
|
|
85
|
+
if (config.where) {
|
|
86
|
+
whereClause = `${whereClause} AND (${config.where})`;
|
|
87
|
+
}
|
|
88
|
+
let orderByClause = "";
|
|
89
|
+
if (config.orderBy) {
|
|
90
|
+
orderByClause = `ORDER BY ${config.orderBy.column} ${config.orderBy.direction}`;
|
|
91
|
+
}
|
|
92
|
+
const parts = [`SELECT ${selectClause}`, `FROM ${fromClause}`, `WHERE ${whereClause}`];
|
|
93
|
+
if (orderByClause) {
|
|
94
|
+
parts.push(orderByClause);
|
|
95
|
+
}
|
|
96
|
+
return parts.join("\n");
|
|
97
|
+
}
|
|
98
|
+
function buildIdOnlyWatchQuery(config) {
|
|
99
|
+
return buildWatchQuery({
|
|
100
|
+
...config,
|
|
101
|
+
selectColumns: void 0
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function buildRecordFetchQuery(config, ids) {
|
|
105
|
+
validateSqlIdentifier(config.table, "table");
|
|
106
|
+
validateSqlIdentifier(config.idColumn, "idColumn");
|
|
107
|
+
if (config.selectColumns) {
|
|
108
|
+
for (const col of config.selectColumns) {
|
|
109
|
+
validateSqlIdentifier(col, "selectColumns");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const selectParts = [`${config.idColumn} AS id`];
|
|
113
|
+
if (config.selectColumns && config.selectColumns.length > 0) {
|
|
114
|
+
selectParts.push(...config.selectColumns);
|
|
115
|
+
}
|
|
116
|
+
const selectClause = selectParts.join(", ");
|
|
117
|
+
let query = `SELECT ${selectClause} FROM ${config.table}`;
|
|
118
|
+
const params = [];
|
|
119
|
+
if (ids && ids.length > 0) {
|
|
120
|
+
const placeholders = ids.map(() => "?").join(", ");
|
|
121
|
+
query += ` WHERE ${config.idColumn} IN (${placeholders})`;
|
|
122
|
+
params.push(...ids);
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
query,
|
|
126
|
+
params
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function watchConfigToSourceConfig(watchConfig) {
|
|
130
|
+
return {
|
|
131
|
+
table: watchConfig.table,
|
|
132
|
+
idColumn: watchConfig.idColumn,
|
|
133
|
+
orderByColumn: watchConfig.orderBy?.column ?? null
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/attachments/migration.ts
|
|
138
|
+
import { AttachmentState } from "@powersync/attachments";
|
|
139
|
+
var STATE_MAPPING = /* @__PURE__ */ new Map([
|
|
140
|
+
// Official states (1:1 mapping)
|
|
141
|
+
[AttachmentState.QUEUED_SYNC, 0 /* QUEUED_SYNC */],
|
|
142
|
+
[AttachmentState.QUEUED_UPLOAD, 1 /* QUEUED_UPLOAD */],
|
|
143
|
+
[AttachmentState.QUEUED_DOWNLOAD, 2 /* QUEUED_DOWNLOAD */],
|
|
144
|
+
[AttachmentState.SYNCED, 3 /* SYNCED */],
|
|
145
|
+
[AttachmentState.ARCHIVED, 4 /* ARCHIVED */],
|
|
146
|
+
// POL extension states (identity mapping)
|
|
147
|
+
[5 /* FAILED_PERMANENT */, 5 /* FAILED_PERMANENT */],
|
|
148
|
+
[6 /* DOWNLOAD_SKIPPED */, 6 /* DOWNLOAD_SKIPPED */]
|
|
149
|
+
]);
|
|
150
|
+
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"], [6 /* DOWNLOAD_SKIPPED */, "DOWNLOAD_SKIPPED"]]);
|
|
151
|
+
var VALID_STATES = /* @__PURE__ */ new Set([0 /* QUEUED_SYNC */, 1 /* QUEUED_UPLOAD */, 2 /* QUEUED_DOWNLOAD */, 3 /* SYNCED */, 4 /* ARCHIVED */, 5 /* FAILED_PERMANENT */, 6 /* DOWNLOAD_SKIPPED */]);
|
|
152
|
+
var UPLOAD_WORKFLOW_STATES = /* @__PURE__ */ new Set([1 /* QUEUED_UPLOAD */, 5 /* FAILED_PERMANENT */]);
|
|
153
|
+
var DOWNLOAD_WORKFLOW_STATES = /* @__PURE__ */ new Set([2 /* QUEUED_DOWNLOAD */, 0 /* QUEUED_SYNC */]);
|
|
154
|
+
var TERMINAL_STATES = /* @__PURE__ */ new Set([3 /* SYNCED */, 4 /* ARCHIVED */, 6 /* DOWNLOAD_SKIPPED */]);
|
|
155
|
+
function migrateAttachmentState(oldState) {
|
|
156
|
+
const newState = STATE_MAPPING.get(oldState);
|
|
157
|
+
if (newState === void 0) {
|
|
158
|
+
throw new Error(`Invalid attachment state: ${oldState}. Valid states are: ${Array.from(STATE_NAMES.entries()).map(([v, n]) => `${n}(${v})`).join(", ")}`);
|
|
159
|
+
}
|
|
160
|
+
return newState;
|
|
161
|
+
}
|
|
162
|
+
function migrateAttachmentStateSafe(oldState, fallback = 0 /* QUEUED_SYNC */) {
|
|
163
|
+
const newState = STATE_MAPPING.get(oldState);
|
|
164
|
+
return newState !== void 0 ? newState : fallback;
|
|
165
|
+
}
|
|
166
|
+
function isValidAttachmentState(value) {
|
|
167
|
+
return typeof value === "number" && VALID_STATES.has(value);
|
|
168
|
+
}
|
|
169
|
+
function isUploadWorkflowState(state) {
|
|
170
|
+
return UPLOAD_WORKFLOW_STATES.has(state);
|
|
171
|
+
}
|
|
172
|
+
function isDownloadWorkflowState(state) {
|
|
173
|
+
return DOWNLOAD_WORKFLOW_STATES.has(state);
|
|
174
|
+
}
|
|
175
|
+
function isTerminalState(state) {
|
|
176
|
+
return TERMINAL_STATES.has(state);
|
|
177
|
+
}
|
|
178
|
+
function getStateName(state) {
|
|
179
|
+
return STATE_NAMES.get(state) ?? "UNKNOWN";
|
|
180
|
+
}
|
|
181
|
+
function createMigrationStats() {
|
|
182
|
+
return {
|
|
183
|
+
total: 0,
|
|
184
|
+
migrated: 0,
|
|
185
|
+
invalid: 0,
|
|
186
|
+
byState: /* @__PURE__ */ new Map()
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function recordMigration(stats, oldState, newState, wasValid) {
|
|
190
|
+
stats.total++;
|
|
191
|
+
if (wasValid) {
|
|
192
|
+
stats.migrated++;
|
|
193
|
+
} else {
|
|
194
|
+
stats.invalid++;
|
|
195
|
+
}
|
|
196
|
+
stats.byState.set(newState, (stats.byState.get(newState) ?? 0) + 1);
|
|
197
|
+
}
|
|
198
|
+
function formatMigrationStats(stats) {
|
|
199
|
+
const lines = ["Migration Summary:", ` Total: ${stats.total}`, ` Migrated: ${stats.migrated}`, ` Invalid (used fallback): ${stats.invalid}`, " By State:"];
|
|
200
|
+
for (const [state, count] of stats.byState.entries()) {
|
|
201
|
+
lines.push(` ${getStateName(state)}: ${count}`);
|
|
202
|
+
}
|
|
203
|
+
return lines.join("\n");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export {
|
|
207
|
+
validateSqlIdentifier,
|
|
208
|
+
validateWhereClause,
|
|
209
|
+
buildWatchQuery,
|
|
210
|
+
buildIdOnlyWatchQuery,
|
|
211
|
+
buildRecordFetchQuery,
|
|
212
|
+
watchConfigToSourceConfig,
|
|
213
|
+
STATE_MAPPING,
|
|
214
|
+
STATE_NAMES,
|
|
215
|
+
VALID_STATES,
|
|
216
|
+
UPLOAD_WORKFLOW_STATES,
|
|
217
|
+
DOWNLOAD_WORKFLOW_STATES,
|
|
218
|
+
TERMINAL_STATES,
|
|
219
|
+
migrateAttachmentState,
|
|
220
|
+
migrateAttachmentStateSafe,
|
|
221
|
+
isValidAttachmentState,
|
|
222
|
+
isUploadWorkflowState,
|
|
223
|
+
isDownloadWorkflowState,
|
|
224
|
+
isTerminalState,
|
|
225
|
+
getStateName,
|
|
226
|
+
createMigrationStats,
|
|
227
|
+
recordMigration,
|
|
228
|
+
formatMigrationStats
|
|
229
|
+
};
|
|
230
|
+
//# sourceMappingURL=chunk-ZM4ENYMF.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 * Build a SQL watch query from a WatchConfig.\n *\n * Generates a SELECT statement that:\n * - Selects the ID 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 * const query = buildWatchQuery({\n * table: 'EquipmentUnitMediaContent',\n * idColumn: '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 // Validate all identifiers\n validateSqlIdentifier(config.table, 'table');\n validateSqlIdentifier(config.idColumn, 'idColumn');\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[] = [`${config.idColumn} 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 = `${config.idColumn} IS NOT NULL AND ${config.idColumn} != ''`;\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 * idColumn: '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 // Validate identifiers\n validateSqlIdentifier(config.table, 'table');\n validateSqlIdentifier(config.idColumn, 'idColumn');\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 ID column\n const selectParts: string[] = [`${config.idColumn} 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 ${config.idColumn} IN (${placeholders})`;\n params.push(...ids);\n }\n return {\n query,\n params\n };\n}\n\n/**\n * Convert WatchConfig to the legacy AttachmentSourceConfig format.\n * Useful during migration to maintain backwards compatibility.\n *\n * @param watchConfig - The new WatchConfig\n * @returns Legacy AttachmentSourceConfig\n */\nexport function watchConfigToSourceConfig(watchConfig: WatchConfig): {\n table: string;\n idColumn: string;\n orderByColumn?: string | null;\n} {\n return {\n table: watchConfig.table,\n idColumn: watchConfig.idColumn,\n orderByColumn: watchConfig.orderBy?.column ?? null\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":"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","names":[]}
|