@electric-sql/client 0.3.2 → 0.3.4

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/src/helpers.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ChangeMessage, ControlMessage, Message, Value } from './types'
1
+ import { ChangeMessage, ControlMessage, Message, Row } from './types'
2
2
 
3
3
  /**
4
4
  * Type guard for checking {@link Message} is {@link ChangeMessage}.
@@ -17,7 +17,7 @@ import { ChangeMessage, ControlMessage, Message, Value } from './types'
17
17
  * }
18
18
  * ```
19
19
  */
20
- export function isChangeMessage<T extends Value = { [key: string]: Value }>(
20
+ export function isChangeMessage<T extends Row = Row>(
21
21
  message: Message<T>
22
22
  ): message is ChangeMessage<T> {
23
23
  return `key` in message
@@ -40,7 +40,7 @@ export function isChangeMessage<T extends Value = { [key: string]: Value }>(
40
40
  * }
41
41
  * ```
42
42
  */
43
- export function isControlMessage<T extends Value = { [key: string]: Value }>(
43
+ export function isControlMessage<T extends Row = Row>(
44
44
  message: Message<T>
45
45
  ): message is ControlMessage {
46
46
  return !isChangeMessage(message)
package/src/parser.ts CHANGED
@@ -1,7 +1,14 @@
1
- import { ColumnInfo, Message, Schema, Value } from './types'
1
+ import { ColumnInfo, Message, Row, Schema, Value } from './types'
2
2
 
3
+ type NullToken = null | `NULL`
4
+ type Token = Exclude<string, NullToken>
5
+ type NullableToken = Token | NullToken
3
6
  export type ParseFunction = (
4
- value: string,
7
+ value: Token,
8
+ additionalInfo?: Omit<ColumnInfo, `type` | `dims`>
9
+ ) => Value
10
+ type NullableParseFunction = (
11
+ value: NullableToken,
5
12
  additionalInfo?: Omit<ColumnInfo, `type` | `dims`>
6
13
  ) => Value
7
14
  export type Parser = { [key: string]: ParseFunction }
@@ -10,6 +17,7 @@ const parseNumber = (value: string) => Number(value)
10
17
  const parseBool = (value: string) => value === `true` || value === `t`
11
18
  const parseBigInt = (value: string) => BigInt(value)
12
19
  const parseJson = (value: string) => JSON.parse(value)
20
+ const identityParser: ParseFunction = (v: string) => v
13
21
 
14
22
  export const defaultParser: Parser = {
15
23
  int2: parseNumber,
@@ -23,10 +31,7 @@ export const defaultParser: Parser = {
23
31
  }
24
32
 
25
33
  // Taken from: https://github.com/electric-sql/pglite/blob/main/packages/pglite/src/types.ts#L233-L279
26
- export function pgArrayParser(
27
- value: string,
28
- parser?: (s: string) => Value
29
- ): Value {
34
+ export function pgArrayParser(value: Token, parser?: ParseFunction): Value {
30
35
  let i = 0
31
36
  let char = null
32
37
  let str = ``
@@ -74,7 +79,7 @@ export function pgArrayParser(
74
79
  return loop(value)[0]
75
80
  }
76
81
 
77
- export class MessageParser {
82
+ export class MessageParser<T extends Row> {
78
83
  private parser: Parser
79
84
  constructor(parser?: Parser) {
80
85
  // Merge the provided parser with the default parser
@@ -83,7 +88,7 @@ export class MessageParser {
83
88
  this.parser = { ...defaultParser, ...parser }
84
89
  }
85
90
 
86
- parse(messages: string, schema: Schema): Message[] {
91
+ parse(messages: string, schema: Schema): Message<T>[] {
87
92
  return JSON.parse(messages, (key, value) => {
88
93
  // typeof value === `object` is needed because
89
94
  // there could be a column named `value`
@@ -92,15 +97,15 @@ export class MessageParser {
92
97
  // Parse the row values
93
98
  const row = value as Record<string, Value>
94
99
  Object.keys(row).forEach((key) => {
95
- row[key] = this.parseRow(key, row[key] as string, schema)
100
+ row[key] = this.parseRow(key, row[key] as NullableToken, schema)
96
101
  })
97
102
  }
98
103
  return value
99
- }) as Message[]
104
+ }) as Message<T>[]
100
105
  }
101
106
 
102
107
  // Parses the message values using the provided parser based on the schema information
103
- private parseRow(key: string, value: string, schema: Schema): Value {
108
+ private parseRow(key: string, value: NullableToken, schema: Schema): Value {
104
109
  const columnInfo = schema[key]
105
110
  if (!columnInfo) {
106
111
  // We don't have information about the value
@@ -114,13 +119,17 @@ export class MessageParser {
114
119
  // Pick the right parser for the type
115
120
  // and support parsing null values if needed
116
121
  // if no parser is provided for the given type, just return the value as is
117
- const identityParser = (v: string) => v
118
- const typParser = this.parser[typ] ?? identityParser
119
- const parser = makeNullableParser(typParser, columnInfo.not_null)
122
+ const typeParser = this.parser[typ] ?? identityParser
123
+ const parser = makeNullableParser(typeParser, columnInfo, key)
120
124
 
121
125
  if (dimensions && dimensions > 0) {
122
126
  // It's an array
123
- return pgArrayParser(value, parser)
127
+ const nullablePgArrayParser = makeNullableParser(
128
+ (value, _) => pgArrayParser(value, parser),
129
+ columnInfo,
130
+ key
131
+ )
132
+ return nullablePgArrayParser(value)
124
133
  }
125
134
 
126
135
  return parser(value, additionalInfo)
@@ -129,15 +138,24 @@ export class MessageParser {
129
138
 
130
139
  function makeNullableParser(
131
140
  parser: ParseFunction,
132
- notNullable?: boolean
133
- ): ParseFunction {
134
- const isNullable = !(notNullable ?? false)
135
- if (isNullable) {
136
- // The sync service contains `null` value for a column whose value is NULL
137
- // but if the column value is an array that contains a NULL value
138
- // then it will be included in the array string as `NULL`, e.g.: `"{1,NULL,3}"`
139
- return (value: string) =>
140
- value === null || value === `NULL` ? null : parser(value)
141
+ columnInfo: ColumnInfo,
142
+ columnName?: string
143
+ ): NullableParseFunction {
144
+ const isNullable = !(columnInfo.not_null ?? false)
145
+ // The sync service contains `null` value for a column whose value is NULL
146
+ // but if the column value is an array that contains a NULL value
147
+ // then it will be included in the array string as `NULL`, e.g.: `"{1,NULL,3}"`
148
+ return (value: NullableToken) => {
149
+ if (isPgNull(value)) {
150
+ if (!isNullable) {
151
+ throw new Error(`Column ${columnName ?? `unknown`} is not nullable`)
152
+ }
153
+ return null
154
+ }
155
+ return parser(value, columnInfo)
141
156
  }
142
- return parser
157
+ }
158
+
159
+ function isPgNull(value: NullableToken): value is NullToken {
160
+ return value === null || value === `NULL`
143
161
  }
package/src/types.ts CHANGED
@@ -7,6 +7,8 @@ export type Value =
7
7
  | Value[]
8
8
  | { [key: string]: Value }
9
9
 
10
+ export type Row = { [key: string]: Value }
11
+
10
12
  export type Offset = `-1` | `${number}_${number}`
11
13
 
12
14
  interface Header {
@@ -17,7 +19,7 @@ export type ControlMessage = {
17
19
  headers: Header & { control: `up-to-date` | `must-refetch` }
18
20
  }
19
21
 
20
- export type ChangeMessage<T> = {
22
+ export type ChangeMessage<T extends Row = Row> = {
21
23
  key: string
22
24
  value: T
23
25
  headers: Header & { operation: `insert` | `update` | `delete` }
@@ -25,9 +27,7 @@ export type ChangeMessage<T> = {
25
27
  }
26
28
 
27
29
  // Define the type for a record
28
- export type Message<T extends Value = { [key: string]: Value }> =
29
- | ControlMessage
30
- | ChangeMessage<T>
30
+ export type Message<T extends Row = Row> = ControlMessage | ChangeMessage<T>
31
31
 
32
32
  /**
33
33
  * Common properties for all columns.
@@ -104,7 +104,7 @@ export type ColumnInfo =
104
104
 
105
105
  export type Schema = { [key: string]: ColumnInfo }
106
106
 
107
- export type TypedMessages<T extends Value = { [key: string]: Value }> = {
107
+ export type TypedMessages<T extends Row = Row> = {
108
108
  messages: Array<Message<T>>
109
109
  schema: ColumnInfo
110
110
  }