@electric-sql/client 1.5.11 → 1.5.12
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/bin/intent.mjs +6 -0
- package/package.json +9 -2
- package/skills/electric-debugging/SKILL.md +217 -0
- package/skills/electric-deployment/SKILL.md +196 -0
- package/skills/electric-new-feature/SKILL.md +366 -0
- package/skills/electric-orm/SKILL.md +189 -0
- package/skills/electric-postgres-security/SKILL.md +196 -0
- package/skills/electric-proxy-auth/SKILL.md +269 -0
- package/skills/electric-schema-shapes/SKILL.md +200 -0
- package/skills/electric-shapes/SKILL.md +339 -0
- package/skills/electric-shapes/references/type-parsers.md +64 -0
- package/skills/electric-shapes/references/where-clause.md +64 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: electric-shapes
|
|
3
|
+
description: >
|
|
4
|
+
Configure ShapeStream and Shape to sync a Postgres table to the client.
|
|
5
|
+
Covers ShapeStreamOptions (url, table, where, columns, replica, offset,
|
|
6
|
+
handle), custom type parsers (timestamptz, jsonb, int8), column mappers
|
|
7
|
+
(snakeCamelMapper, createColumnMapper), onError retry semantics, backoff
|
|
8
|
+
options, log modes (full, changes_only), requestSnapshot, fetchSnapshot,
|
|
9
|
+
subscribe/unsubscribe, and Shape materialized view. Load when setting up
|
|
10
|
+
sync, configuring shapes, parsing types, or handling sync errors.
|
|
11
|
+
type: core
|
|
12
|
+
library: electric
|
|
13
|
+
library_version: '1.5.10'
|
|
14
|
+
sources:
|
|
15
|
+
- 'electric-sql/electric:packages/typescript-client/src/client.ts'
|
|
16
|
+
- 'electric-sql/electric:packages/typescript-client/src/shape.ts'
|
|
17
|
+
- 'electric-sql/electric:packages/typescript-client/src/types.ts'
|
|
18
|
+
- 'electric-sql/electric:packages/typescript-client/src/parser.ts'
|
|
19
|
+
- 'electric-sql/electric:packages/typescript-client/src/column-mapper.ts'
|
|
20
|
+
- 'electric-sql/electric:website/docs/guides/shapes.md'
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# Electric — Shape Streaming
|
|
24
|
+
|
|
25
|
+
## Setup
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { ShapeStream, Shape } from '@electric-sql/client'
|
|
29
|
+
|
|
30
|
+
const stream = new ShapeStream({
|
|
31
|
+
url: '/api/todos', // Your proxy route, NOT direct Electric URL
|
|
32
|
+
// Built-in parsers auto-handle: bool, int2, int4, float4, float8, json, jsonb
|
|
33
|
+
// Add custom parsers for other types (see references/type-parsers.md)
|
|
34
|
+
parser: {
|
|
35
|
+
timestamptz: (date: string) => new Date(date),
|
|
36
|
+
},
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
const shape = new Shape(stream)
|
|
40
|
+
|
|
41
|
+
shape.subscribe(({ rows }) => {
|
|
42
|
+
console.log('synced rows:', rows)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// Wait for initial sync
|
|
46
|
+
const rows = await shape.rows
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Core Patterns
|
|
50
|
+
|
|
51
|
+
### Filter rows with WHERE clause and positional params
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const stream = new ShapeStream({
|
|
55
|
+
url: '/api/todos',
|
|
56
|
+
params: {
|
|
57
|
+
table: 'todos',
|
|
58
|
+
where: 'user_id = $1 AND status = $2',
|
|
59
|
+
params: { '1': userId, '2': 'active' },
|
|
60
|
+
},
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Select specific columns (must include primary key)
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
const stream = new ShapeStream({
|
|
68
|
+
url: '/api/todos',
|
|
69
|
+
params: {
|
|
70
|
+
table: 'todos',
|
|
71
|
+
columns: ['id', 'title', 'status'], // PK required
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Map column names between snake_case and camelCase
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { ShapeStream, snakeCamelMapper } from '@electric-sql/client'
|
|
80
|
+
|
|
81
|
+
const stream = new ShapeStream({
|
|
82
|
+
url: '/api/todos',
|
|
83
|
+
columnMapper: snakeCamelMapper(),
|
|
84
|
+
})
|
|
85
|
+
// DB column "created_at" arrives as "createdAt" in client
|
|
86
|
+
// WHERE clauses auto-translate: "createdAt" → "created_at"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Handle errors with retry
|
|
90
|
+
|
|
91
|
+
```ts
|
|
92
|
+
const stream = new ShapeStream({
|
|
93
|
+
url: '/api/todos',
|
|
94
|
+
onError: (error) => {
|
|
95
|
+
console.error('sync error', error)
|
|
96
|
+
return {} // Return {} to retry; returning void stops the stream
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
For auth token refresh on 401 errors, see electric-proxy-auth/SKILL.md.
|
|
102
|
+
|
|
103
|
+
### Resume from stored offset
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
const stream = new ShapeStream({
|
|
107
|
+
url: '/api/todos',
|
|
108
|
+
offset: storedOffset, // Both offset AND handle required
|
|
109
|
+
handle: storedHandle,
|
|
110
|
+
})
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Get replica with old values on update
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const stream = new ShapeStream({
|
|
117
|
+
url: '/api/todos',
|
|
118
|
+
params: {
|
|
119
|
+
table: 'todos',
|
|
120
|
+
replica: 'full', // Sends unchanged columns + old_value on updates
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Common Mistakes
|
|
126
|
+
|
|
127
|
+
### CRITICAL Returning void from onError stops sync permanently
|
|
128
|
+
|
|
129
|
+
Wrong:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
const stream = new ShapeStream({
|
|
133
|
+
url: '/api/todos',
|
|
134
|
+
onError: (error) => {
|
|
135
|
+
console.error('sync error', error)
|
|
136
|
+
// Returning nothing = stream stops forever
|
|
137
|
+
},
|
|
138
|
+
})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Correct:
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
const stream = new ShapeStream({
|
|
145
|
+
url: '/api/todos',
|
|
146
|
+
onError: (error) => {
|
|
147
|
+
console.error('sync error', error)
|
|
148
|
+
return {} // Return {} to retry
|
|
149
|
+
},
|
|
150
|
+
})
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`onError` returning `undefined` signals the stream to permanently stop. Return at least `{}` to retry, or return `{ headers, params }` to retry with updated values.
|
|
154
|
+
|
|
155
|
+
Source: `packages/typescript-client/src/client.ts:409-418`
|
|
156
|
+
|
|
157
|
+
### HIGH Using columns without including primary key
|
|
158
|
+
|
|
159
|
+
Wrong:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
const stream = new ShapeStream({
|
|
163
|
+
url: '/api/todos',
|
|
164
|
+
params: {
|
|
165
|
+
table: 'todos',
|
|
166
|
+
columns: ['title', 'status'],
|
|
167
|
+
},
|
|
168
|
+
})
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Correct:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
const stream = new ShapeStream({
|
|
175
|
+
url: '/api/todos',
|
|
176
|
+
params: {
|
|
177
|
+
table: 'todos',
|
|
178
|
+
columns: ['id', 'title', 'status'],
|
|
179
|
+
},
|
|
180
|
+
})
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Server returns 400 error. The `columns` list must always include the primary key column(s).
|
|
184
|
+
|
|
185
|
+
Source: `website/docs/guides/shapes.md`
|
|
186
|
+
|
|
187
|
+
### HIGH Setting offset without handle for resumption
|
|
188
|
+
|
|
189
|
+
Wrong:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
new ShapeStream({
|
|
193
|
+
url: '/api/todos',
|
|
194
|
+
offset: storedOffset,
|
|
195
|
+
})
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Correct:
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
new ShapeStream({
|
|
202
|
+
url: '/api/todos',
|
|
203
|
+
offset: storedOffset,
|
|
204
|
+
handle: storedHandle,
|
|
205
|
+
})
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Throws `MissingShapeHandleError`. Both `offset` AND `handle` are required to resume a stream from a stored position.
|
|
209
|
+
|
|
210
|
+
Source: `packages/typescript-client/src/client.ts:1997-2003`
|
|
211
|
+
|
|
212
|
+
### HIGH Using non-deterministic functions in WHERE clause
|
|
213
|
+
|
|
214
|
+
Wrong:
|
|
215
|
+
|
|
216
|
+
```ts
|
|
217
|
+
const stream = new ShapeStream({
|
|
218
|
+
url: '/api/events',
|
|
219
|
+
params: {
|
|
220
|
+
table: 'events',
|
|
221
|
+
where: 'start_time > now()',
|
|
222
|
+
},
|
|
223
|
+
})
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Correct:
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
const stream = new ShapeStream({
|
|
230
|
+
url: '/api/events',
|
|
231
|
+
params: {
|
|
232
|
+
table: 'events',
|
|
233
|
+
where: 'start_time > $1',
|
|
234
|
+
params: { '1': new Date().toISOString() },
|
|
235
|
+
},
|
|
236
|
+
})
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Server rejects WHERE clauses with non-deterministic functions like `now()`, `random()`, `count()`. Use static values or positional params.
|
|
240
|
+
|
|
241
|
+
Source: `packages/sync-service/lib/electric/replication/eval/env/known_functions.ex`
|
|
242
|
+
|
|
243
|
+
### HIGH Not parsing custom Postgres types
|
|
244
|
+
|
|
245
|
+
Wrong:
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
const stream = new ShapeStream({
|
|
249
|
+
url: '/api/events',
|
|
250
|
+
})
|
|
251
|
+
// createdAt will be string "2024-01-15T10:30:00.000Z", not a Date
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Correct:
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
const stream = new ShapeStream({
|
|
258
|
+
url: '/api/events',
|
|
259
|
+
parser: {
|
|
260
|
+
timestamptz: (date: string) => new Date(date),
|
|
261
|
+
timestamp: (date: string) => new Date(date),
|
|
262
|
+
},
|
|
263
|
+
})
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Electric auto-parses `bool`, `int2`, `int4`, `float4`, `float8`, `json`, `jsonb`, and `int8` (→ BigInt). All other types arrive as strings — add custom parsers for `timestamptz`, `date`, `numeric`, etc. See [references/type-parsers.md](references/type-parsers.md) for the full list.
|
|
267
|
+
|
|
268
|
+
Source: `AGENTS.md:300-308`
|
|
269
|
+
|
|
270
|
+
### MEDIUM Using reserved parameter names in params
|
|
271
|
+
|
|
272
|
+
Wrong:
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
const stream = new ShapeStream({
|
|
276
|
+
url: '/api/todos',
|
|
277
|
+
params: {
|
|
278
|
+
table: 'todos',
|
|
279
|
+
cursor: 'abc', // Reserved!
|
|
280
|
+
offset: '0', // Reserved!
|
|
281
|
+
},
|
|
282
|
+
})
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Correct:
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
const stream = new ShapeStream({
|
|
289
|
+
url: '/api/todos',
|
|
290
|
+
params: {
|
|
291
|
+
table: 'todos',
|
|
292
|
+
page_cursor: 'abc',
|
|
293
|
+
page_offset: '0',
|
|
294
|
+
},
|
|
295
|
+
})
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Throws `ReservedParamError`. Names `cursor`, `handle`, `live`, `offset`, `cache-buster`, and all `subset__*` prefixed params are reserved by the Electric protocol.
|
|
299
|
+
|
|
300
|
+
Source: `packages/typescript-client/src/client.ts:1984-1985`
|
|
301
|
+
|
|
302
|
+
### MEDIUM Mutating shape options on a running stream
|
|
303
|
+
|
|
304
|
+
Wrong:
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
const stream = new ShapeStream({
|
|
308
|
+
url: '/api/todos',
|
|
309
|
+
params: { table: 'todos', where: "status = 'active'" },
|
|
310
|
+
})
|
|
311
|
+
// Later...
|
|
312
|
+
stream.options.params.where = "status = 'done'" // No effect!
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Correct:
|
|
316
|
+
|
|
317
|
+
```ts
|
|
318
|
+
// Create a new stream with different params
|
|
319
|
+
const newStream = new ShapeStream({
|
|
320
|
+
url: '/api/todos',
|
|
321
|
+
params: { table: 'todos', where: "status = 'done'" },
|
|
322
|
+
})
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Shapes are immutable per subscription. Changing params on a running stream has no effect. Create a new `ShapeStream` instance for different filters.
|
|
326
|
+
|
|
327
|
+
Source: `AGENTS.md:106`
|
|
328
|
+
|
|
329
|
+
## References
|
|
330
|
+
|
|
331
|
+
- [WHERE clause supported types and functions](references/where-clause.md)
|
|
332
|
+
- [Built-in type parsers](references/type-parsers.md)
|
|
333
|
+
|
|
334
|
+
See also: electric-proxy-auth/SKILL.md — Shape URLs must point to proxy routes, not directly to Electric.
|
|
335
|
+
See also: electric-debugging/SKILL.md — onError semantics and backoff are essential for diagnosing sync problems.
|
|
336
|
+
|
|
337
|
+
## Version
|
|
338
|
+
|
|
339
|
+
Targets @electric-sql/client v1.5.10.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Electric Shapes — Type Parser Reference
|
|
2
|
+
|
|
3
|
+
## Built-in Parsers
|
|
4
|
+
|
|
5
|
+
These parsers are applied automatically. All other types arrive as strings.
|
|
6
|
+
|
|
7
|
+
| Postgres Type | Parser | Output Type | Notes |
|
|
8
|
+
| ------------- | ------------- | ----------- | -------------------------- |
|
|
9
|
+
| `int2` | `parseNumber` | `number` | |
|
|
10
|
+
| `int4` | `parseNumber` | `number` | |
|
|
11
|
+
| `int8` | `parseBigInt` | `BigInt` | Returns BigInt, not number |
|
|
12
|
+
| `float4` | `parseNumber` | `number` | |
|
|
13
|
+
| `float8` | `parseNumber` | `number` | |
|
|
14
|
+
| `bool` | `parseBool` | `boolean` | |
|
|
15
|
+
| `json` | `parseJson` | `object` | |
|
|
16
|
+
| `jsonb` | `parseJson` | `object` | |
|
|
17
|
+
|
|
18
|
+
## Common Custom Parsers
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
const stream = new ShapeStream({
|
|
22
|
+
url: '/api/items',
|
|
23
|
+
parser: {
|
|
24
|
+
timestamptz: (date: string) => new Date(date),
|
|
25
|
+
timestamp: (date: string) => new Date(date),
|
|
26
|
+
date: (date: string) => new Date(date),
|
|
27
|
+
numeric: (n: string) => parseFloat(n),
|
|
28
|
+
interval: (i: string) => i, // Keep as string or use a library
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Parser Signature
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
type ParseFunction<Extensions = never> = (
|
|
37
|
+
value: string,
|
|
38
|
+
additionalInfo?: Omit<ColumnInfo, 'type' | 'dims'>
|
|
39
|
+
) => Value<Extensions>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The `additionalInfo` parameter provides column metadata like `precision`, `scale`, `max_length`, `not_null`.
|
|
43
|
+
|
|
44
|
+
## NULL Handling
|
|
45
|
+
|
|
46
|
+
If a column has `not_null: true` in the schema and a `NULL` value is received, the parser throws `ParserNullValueError`. This indicates a schema mismatch.
|
|
47
|
+
|
|
48
|
+
## Transformer vs Parser
|
|
49
|
+
|
|
50
|
+
- **Parser**: converts individual column values by Postgres type name
|
|
51
|
+
- **Transformer**: transforms the entire row object after parsing
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const stream = new ShapeStream({
|
|
55
|
+
url: '/api/items',
|
|
56
|
+
parser: {
|
|
57
|
+
timestamptz: (date: string) => new Date(date),
|
|
58
|
+
},
|
|
59
|
+
transformer: (row) => ({
|
|
60
|
+
...row,
|
|
61
|
+
fullName: `${row.firstName} ${row.lastName}`,
|
|
62
|
+
}),
|
|
63
|
+
})
|
|
64
|
+
```
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Electric Shapes — WHERE Clause Reference
|
|
2
|
+
|
|
3
|
+
## Supported Column Types
|
|
4
|
+
|
|
5
|
+
| Type | Example | Notes |
|
|
6
|
+
| -------------------------- | ------------------------------------- | --------------------- |
|
|
7
|
+
| `text`, `varchar`, `char` | `name = 'Alice'` | String comparison |
|
|
8
|
+
| `int2`, `int4`, `int8` | `age > 21` | Numeric comparison |
|
|
9
|
+
| `float4`, `float8` | `price < 9.99` | Float comparison |
|
|
10
|
+
| `bool` | `active = true` | Boolean |
|
|
11
|
+
| `uuid` | `id = '550e8400-...'` | UUID comparison |
|
|
12
|
+
| `date` | `created > '2024-01-01'` | Date comparison |
|
|
13
|
+
| `timestamp`, `timestamptz` | `updated_at > '2024-01-01T00:00:00Z'` | Timestamp comparison |
|
|
14
|
+
| `interval` | `duration > '1 hour'` | Interval comparison |
|
|
15
|
+
| `numeric` | `amount >= 100.50` | Arbitrary precision |
|
|
16
|
+
| `arrays` | `tags && ARRAY['urgent']` | Array operations |
|
|
17
|
+
| `enum` | `status::text IN ('active', 'done')` | **Must cast to text** |
|
|
18
|
+
|
|
19
|
+
## Unsupported
|
|
20
|
+
|
|
21
|
+
- `timetz` — not supported in WHERE
|
|
22
|
+
- Non-deterministic functions: `now()`, `random()`, `count()`, `current_timestamp`
|
|
23
|
+
- Aggregate functions
|
|
24
|
+
- Subqueries (experimental, requires `ELECTRIC_FEATURE_FLAGS=allow_subqueries`)
|
|
25
|
+
|
|
26
|
+
## Positional Parameters
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
// Array format
|
|
30
|
+
params: { where: 'org_id = $1 AND role = $2', params: ['org-123', 'admin'] }
|
|
31
|
+
|
|
32
|
+
// Object format
|
|
33
|
+
params: { where: 'org_id = $1 AND role = $2', params: { '1': 'org-123', '2': 'admin' } }
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Operators
|
|
37
|
+
|
|
38
|
+
| Operator | Example |
|
|
39
|
+
| ------------------------ | --------------------------------- |
|
|
40
|
+
| `=`, `!=`, `<>` | `status = 'active'` |
|
|
41
|
+
| `<`, `>`, `<=`, `>=` | `age >= 18` |
|
|
42
|
+
| `IN` | `status IN ('active', 'pending')` |
|
|
43
|
+
| `NOT IN` | `status NOT IN ('deleted')` |
|
|
44
|
+
| `LIKE`, `ILIKE` | `name ILIKE '%john%'` |
|
|
45
|
+
| `IS NULL`, `IS NOT NULL` | `deleted_at IS NULL` |
|
|
46
|
+
| `AND`, `OR`, `NOT` | `active = true AND age > 18` |
|
|
47
|
+
| `BETWEEN` | `age BETWEEN 18 AND 65` |
|
|
48
|
+
| `ANY`, `ALL` | Array comparisons |
|
|
49
|
+
|
|
50
|
+
## Enum Gotcha
|
|
51
|
+
|
|
52
|
+
Enum columns require explicit `::text` cast:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
// Wrong — fails silently or errors
|
|
56
|
+
params: {
|
|
57
|
+
where: "status IN ('active', 'done')"
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Correct
|
|
61
|
+
params: {
|
|
62
|
+
where: "status::text IN ('active', 'done')"
|
|
63
|
+
}
|
|
64
|
+
```
|