@electric-sql/client 1.2.0 → 1.2.2

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@electric-sql/client",
3
3
  "description": "Postgres everywhere - your data, in sync, wherever you need it.",
4
- "version": "1.2.0",
4
+ "version": "1.2.2",
5
5
  "author": "ElectricSQL team and contributors.",
6
6
  "bugs": {
7
7
  "url": "https://github.com/electric-sql/electric/issues"
@@ -14,7 +14,7 @@
14
14
  "@types/uuid": "^10.0.0",
15
15
  "@typescript-eslint/eslint-plugin": "^7.14.1",
16
16
  "@typescript-eslint/parser": "^7.14.1",
17
- "@vitest/coverage-istanbul": "2.1.4",
17
+ "@vitest/coverage-istanbul": "4.0.15",
18
18
  "cache-control-parser": "^2.0.6",
19
19
  "concurrently": "^8.2.2",
20
20
  "eslint": "^8.57.0",
@@ -28,7 +28,7 @@
28
28
  "tsup": "^8.0.1",
29
29
  "typescript": "^5.5.2",
30
30
  "uuid": "^10.0.0",
31
- "vitest": "^3.0.0",
31
+ "vitest": "^4.0.15",
32
32
  "vitest-localstorage-mock": "^0.1.2"
33
33
  },
34
34
  "type": "module",
package/src/client.ts CHANGED
@@ -9,7 +9,11 @@ import {
9
9
  SnapshotMetadata,
10
10
  } from './types'
11
11
  import { MessageParser, Parser, TransformFunction } from './parser'
12
- import { ColumnMapper, encodeWhereClause } from './column-mapper'
12
+ import {
13
+ ColumnMapper,
14
+ encodeWhereClause,
15
+ quoteIdentifier,
16
+ } from './column-mapper'
13
17
  import { getOffset, isUpToDateMessage, isChangeMessage } from './helpers'
14
18
  import {
15
19
  FetchError,
@@ -855,8 +859,27 @@ export class ShapeStream<T extends Row<unknown> = Row>
855
859
  )
856
860
  setQueryParam(fetchUrl, WHERE_QUERY_PARAM, encodedWhere)
857
861
  }
858
- if (params.columns)
859
- setQueryParam(fetchUrl, COLUMNS_QUERY_PARAM, params.columns)
862
+ if (params.columns) {
863
+ // Get original columns array from options (before toInternalParams converted to string)
864
+ const originalColumns = await resolveValue(this.options.params?.columns)
865
+ if (Array.isArray(originalColumns)) {
866
+ // Apply columnMapper encoding if present
867
+ let encodedColumns = originalColumns.map(String)
868
+ if (this.options.columnMapper) {
869
+ encodedColumns = encodedColumns.map(
870
+ this.options.columnMapper.encode
871
+ )
872
+ }
873
+ // Quote each column name to handle special characters (commas, etc.)
874
+ const serializedColumns = encodedColumns
875
+ .map(quoteIdentifier)
876
+ .join(`,`)
877
+ setQueryParam(fetchUrl, COLUMNS_QUERY_PARAM, serializedColumns)
878
+ } else {
879
+ // Fallback: columns was already a string
880
+ setQueryParam(fetchUrl, COLUMNS_QUERY_PARAM, params.columns)
881
+ }
882
+ }
860
883
  if (params.replica) setQueryParam(fetchUrl, REPLICA_PARAM, params.replica)
861
884
  if (params.params)
862
885
  setQueryParam(fetchUrl, WHERE_PARAMS_PARAM, params.params)
@@ -1132,10 +1155,16 @@ export class ShapeStream<T extends Row<unknown> = Row>
1132
1155
  // Track when the SSE connection starts
1133
1156
  this.#lastSseConnectionStartTime = Date.now()
1134
1157
 
1158
+ // Add Accept header for SSE requests
1159
+ const sseHeaders = {
1160
+ ...headers,
1161
+ Accept: `text/event-stream`,
1162
+ }
1163
+
1135
1164
  try {
1136
1165
  let buffer: Array<Message<T>> = []
1137
1166
  await fetchEventSource(fetchUrl.toString(), {
1138
- headers,
1167
+ headers: sseHeaders,
1139
1168
  fetch,
1140
1169
  onopen: async (response: Response) => {
1141
1170
  this.#connected = true
@@ -3,6 +3,31 @@ import { Schema } from './types'
3
3
  type DbColumnName = string
4
4
  type AppColumnName = string
5
5
 
6
+ /**
7
+ * Quote a PostgreSQL identifier for safe use in query parameters.
8
+ *
9
+ * Wraps the identifier in double quotes and escapes any internal
10
+ * double quotes by doubling them. This ensures identifiers with
11
+ * special characters (commas, spaces, etc.) are handled correctly.
12
+ *
13
+ * @param identifier - The identifier to quote
14
+ * @returns The quoted identifier
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * quoteIdentifier('user_id') // '"user_id"'
19
+ * quoteIdentifier('foo,bar') // '"foo,bar"'
20
+ * quoteIdentifier('has"quote') // '"has""quote"'
21
+ * ```
22
+ *
23
+ * @internal
24
+ */
25
+ export function quoteIdentifier(identifier: string): string {
26
+ // Escape internal double quotes by doubling them
27
+ const escaped = identifier.replace(/"/g, `""`)
28
+ return `"${escaped}"`
29
+ }
30
+
6
31
  /**
7
32
  * A bidirectional column mapper that handles transforming column **names**
8
33
  * between database format (e.g., snake_case) and application format (e.g., camelCase).
package/src/types.ts CHANGED
@@ -43,10 +43,30 @@ export type NormalizedPgSnapshot = {
43
43
  }
44
44
 
45
45
  interface Header {
46
- [key: Exclude<string, `operation` | `control`>]: Value
46
+ [key: Exclude<string, `operation` | `control` | `event`>]: Value
47
47
  }
48
48
 
49
49
  export type Operation = `insert` | `update` | `delete`
50
+ /**
51
+ * A tag is a string identifying a reason for this row to be part of the shape.
52
+ *
53
+ * Tags can be composite, but they are always sent as a single string. Compound tags
54
+ * are separated by `|`. It's up to the client to split the tag into its components
55
+ * in order to react to move-outs correctly. Tag parts are guaranteed to not contain an
56
+ * unescaped `|` character (escaped as `\\|`) or be a literal `*`.
57
+ *
58
+ * Composite tag width is guaranteed to be fixed for a given shape.
59
+ */
60
+ export type MoveTag = string
61
+
62
+ /**
63
+ * A move-out pattern is a position and a value. The position is the index of the column
64
+ * that is being moved out. The value is the value of the column that is being moved out.
65
+ *
66
+ * Tag width and value order is fixed for a given shape, so the client can determine
67
+ * which tags match this pattern.
68
+ */
69
+ export type MoveOutPattern = { pos: number; value: string }
50
70
 
51
71
  export type ControlMessage = {
52
72
  headers:
@@ -57,16 +77,27 @@ export type ControlMessage = {
57
77
  | (Header & { control: `snapshot-end` } & PostgresSnapshot)
58
78
  }
59
79
 
80
+ export type EventMessage = {
81
+ headers: Header & { event: `move-out`; patterns: MoveOutPattern[] }
82
+ }
83
+
60
84
  export type ChangeMessage<T extends Row<unknown> = Row> = {
61
85
  key: string
62
86
  value: T
63
87
  old_value?: Partial<T> // Only provided for updates if `replica` is `full`
64
- headers: Header & { operation: Operation; txids?: number[] }
88
+ headers: Header & {
89
+ operation: Operation
90
+ txids?: number[]
91
+ /** Tags will always be present for changes if the shape has a subquery in its where clause, and are omitted otherwise.*/
92
+ tags?: MoveTag[]
93
+ removed_tags?: MoveTag[]
94
+ }
65
95
  }
66
96
 
67
97
  // Define the type for a record
68
98
  export type Message<T extends Row<unknown> = Row> =
69
99
  | ControlMessage
100
+ | EventMessage
70
101
  | ChangeMessage<T>
71
102
 
72
103
  /**