@livestore/sync-electric 0.4.0-dev.2 → 0.4.0-dev.21

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.
@@ -0,0 +1,129 @@
1
+ import { shouldNeverHappen } from '@livestore/utils'
2
+ import { Hash, Schema } from '@livestore/utils/effect'
3
+ import * as ApiSchema from './api-schema.ts'
4
+
5
+ /**
6
+ * This function should be called in a trusted environment (e.g. a proxy server) as it
7
+ * requires access to senstive information (e.g. `apiSecret` / `sourceSecret`).
8
+ */
9
+ export const makeElectricUrl = ({
10
+ electricHost,
11
+ searchParams: providedSearchParams,
12
+ sourceId,
13
+ sourceSecret,
14
+ apiSecret,
15
+ }: {
16
+ electricHost: string
17
+ /**
18
+ * Needed to extract information from the search params which the `@livestore/sync-electric`
19
+ * client implementation automatically adds:
20
+ * - `handle`: the ElectricSQL handle
21
+ * - `storeId`: the Livestore storeId
22
+ */
23
+ searchParams: URLSearchParams
24
+ /** Needed for Electric Cloud */
25
+ sourceId?: string
26
+ /** Needed for Electric Cloud */
27
+ sourceSecret?: string
28
+ /** For self-hosted ElectricSQL */
29
+ apiSecret?: string
30
+ }): {
31
+ /**
32
+ * The URL to the ElectricSQL API endpoint with needed search params.
33
+ */
34
+ url: string
35
+ /** The Livestore storeId */
36
+ storeId: string
37
+ /**
38
+ * Whether the Postgres table needs to be created.
39
+ */
40
+ needsInit: boolean
41
+ /** Sync payload provided by the client */
42
+ payload: Schema.JsonValue | undefined
43
+ } => {
44
+ const endpointUrl = `${electricHost}/v1/shape`
45
+ const UrlParamsSchema = Schema.Struct({ args: ApiSchema.ArgsSchema })
46
+ const argsResult = Schema.decodeUnknownEither(UrlParamsSchema)(Object.fromEntries(providedSearchParams.entries()))
47
+
48
+ if (argsResult._tag === 'Left') {
49
+ return shouldNeverHappen(
50
+ 'Invalid search params provided to makeElectricUrl',
51
+ providedSearchParams,
52
+ Object.fromEntries(providedSearchParams.entries()),
53
+ )
54
+ }
55
+
56
+ const args = argsResult.right.args
57
+ const tableName = toTableName(args.storeId)
58
+ // TODO refactor with Effect URLSearchParams schema
59
+ // https://electric-sql.com/openapi.html
60
+ const searchParams = new URLSearchParams()
61
+ // Electric requires table names with capital letters to be quoted
62
+ // Since our table names include the storeId which may have capitals, we always quote
63
+ searchParams.set('table', `"${tableName}"`)
64
+ if (sourceId !== undefined) {
65
+ searchParams.set('source_id', sourceId)
66
+ }
67
+ if (sourceSecret !== undefined) {
68
+ searchParams.set('source_secret', sourceSecret)
69
+ }
70
+ if (apiSecret !== undefined) {
71
+ searchParams.set('api_secret', apiSecret)
72
+ }
73
+ if (args.handle._tag === 'None') {
74
+ searchParams.set('offset', '-1')
75
+ } else {
76
+ searchParams.set('offset', args.handle.value.offset)
77
+ searchParams.set('handle', args.handle.value.handle)
78
+ searchParams.set('live', args.live ? 'true' : 'false')
79
+ }
80
+
81
+ const payload = args.payload
82
+
83
+ const url = `${endpointUrl}?${searchParams.toString()}`
84
+
85
+ return { url, storeId: args.storeId, needsInit: args.handle._tag === 'None', payload }
86
+ }
87
+
88
+ export const toTableName = (storeId: string) => {
89
+ const escapedStoreId = storeId.replaceAll(/[^a-zA-Z0-9_]/g, '_')
90
+ const tableName = `eventlog_${PERSISTENCE_FORMAT_VERSION}_${escapedStoreId}`
91
+
92
+ if (tableName.length > 63) {
93
+ const hashedStoreId = Hash.string(storeId)
94
+
95
+ console.warn(
96
+ `Table name is too long: "${tableName}". Postgres table names are limited to 63 characters. Using hashed storeId instead: "${hashedStoreId}".`,
97
+ )
98
+
99
+ return `eventlog_${PERSISTENCE_FORMAT_VERSION}_hash_${hashedStoreId}`
100
+ }
101
+
102
+ return tableName
103
+ }
104
+
105
+ /**
106
+ * CRITICAL: Increment this version whenever you modify the Postgres table schema structure.
107
+ *
108
+ * Bump required when:
109
+ * - Adding/removing/renaming columns in the eventlog table (see examples/web-todomvc-sync-electric/src/server/db.ts)
110
+ * - Changing column types or constraints
111
+ * - Modifying primary keys or indexes
112
+ *
113
+ * Bump NOT required when:
114
+ * - Changing query patterns or fetch logic
115
+ * - Adding new tables (as long as existing table schema remains unchanged)
116
+ * - Updating client-side implementation details
117
+ *
118
+ * Impact: Changing this version triggers a "soft reset" - new table names are created
119
+ * and old data becomes inaccessible (but remains in the database).
120
+ *
121
+ * Current schema (PostgreSQL):
122
+ * - seqNum (INTEGER PRIMARY KEY)
123
+ * - parentSeqNum (INTEGER)
124
+ * - name (TEXT NOT NULL)
125
+ * - args (JSONB NOT NULL)
126
+ * - clientId (TEXT NOT NULL)
127
+ * - sessionId (TEXT NOT NULL)
128
+ */
129
+ export const PERSISTENCE_FORMAT_VERSION = 6