@electric-sql/client 0.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/src/parser.ts ADDED
@@ -0,0 +1,130 @@
1
+ import { ColumnInfo, Message, Schema, Value } from './types'
2
+
3
+ export type ParseFunction = (
4
+ value: string,
5
+ additionalInfo?: Omit<ColumnInfo, `type` | `dims`>
6
+ ) => Value
7
+ export type Parser = { [key: string]: ParseFunction }
8
+
9
+ const parseNumber = (value: string) => Number(value)
10
+ const parseBool = (value: string) => value === `true` || value === `t`
11
+ const parseBigInt = (value: string) => BigInt(value)
12
+ const parseJson = (value: string) => JSON.parse(value)
13
+
14
+ export const defaultParser: Parser = {
15
+ int2: parseNumber,
16
+ int4: parseNumber,
17
+ int8: parseBigInt,
18
+ bool: parseBool,
19
+ float4: parseNumber,
20
+ float8: parseNumber,
21
+ json: parseJson,
22
+ jsonb: parseJson,
23
+ }
24
+
25
+ // 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 {
30
+ let i = 0
31
+ let char = null
32
+ let str = ``
33
+ let quoted = false
34
+ let last = 0
35
+ let p: string | undefined = undefined
36
+
37
+ function loop(x: string): Value[] {
38
+ const xs = []
39
+ for (; i < x.length; i++) {
40
+ char = x[i]
41
+ if (quoted) {
42
+ if (char === `\\`) {
43
+ str += x[++i]
44
+ } else if (char === `"`) {
45
+ xs.push(parser ? parser(str) : str)
46
+ str = ``
47
+ quoted = x[i + 1] === `"`
48
+ last = i + 2
49
+ } else {
50
+ str += char
51
+ }
52
+ } else if (char === `"`) {
53
+ quoted = true
54
+ } else if (char === `{`) {
55
+ last = ++i
56
+ xs.push(loop(x))
57
+ } else if (char === `}`) {
58
+ quoted = false
59
+ last < i &&
60
+ xs.push(parser ? parser(x.slice(last, i)) : x.slice(last, i))
61
+ last = i + 1
62
+ break
63
+ } else if (char === `,` && p !== `}` && p !== `"`) {
64
+ xs.push(parser ? parser(x.slice(last, i)) : x.slice(last, i))
65
+ last = i + 1
66
+ }
67
+ p = char
68
+ }
69
+ last < i &&
70
+ xs.push(parser ? parser(x.slice(last, i + 1)) : x.slice(last, i + 1))
71
+ return xs
72
+ }
73
+
74
+ return loop(value)[0]
75
+ }
76
+
77
+ export class MessageParser {
78
+ private parser: Parser
79
+ constructor(parser?: Parser) {
80
+ // Merge the provided parser with the default parser
81
+ // to use the provided parser whenever defined
82
+ // and otherwise fall back to the default parser
83
+ this.parser = { ...defaultParser, ...parser }
84
+ }
85
+
86
+ parse(messages: string, schema: Schema): Message[] {
87
+ return JSON.parse(messages, (key, value) => {
88
+ // typeof value === `object` is needed because
89
+ // there could be a column named `value`
90
+ // and the value associated to that column will be a string
91
+ if (key === `value` && typeof value === `object`) {
92
+ // Parse the row values
93
+ const row = value as Record<string, Value>
94
+ Object.keys(row).forEach((key) => {
95
+ row[key] = this.parseRow(key, row[key] as string, schema)
96
+ })
97
+ }
98
+ return value
99
+ }) as Message[]
100
+ }
101
+
102
+ // Parses the message values using the provided parser based on the schema information
103
+ private parseRow(key: string, value: string, schema: Schema): Value {
104
+ const columnInfo = schema[key]
105
+ if (!columnInfo) {
106
+ // We don't have information about the value
107
+ // so we just return it
108
+ return value
109
+ }
110
+
111
+ // Pick the right parser for the type
112
+ const parser = this.parser[columnInfo.type]
113
+
114
+ // Copy the object but don't include `dimensions` and `type`
115
+ const { type: _typ, dims: dimensions, ...additionalInfo } = columnInfo
116
+
117
+ if (dimensions > 0) {
118
+ // It's an array
119
+ const identityParser = (v: string) => v
120
+ return pgArrayParser(value, parser ?? identityParser)
121
+ }
122
+
123
+ if (!parser) {
124
+ // No parser was provided for this type of values
125
+ return value
126
+ }
127
+
128
+ return parser(value, additionalInfo)
129
+ }
130
+ }
package/src/types.ts ADDED
@@ -0,0 +1,108 @@
1
+ export type Value =
2
+ | string
3
+ | number
4
+ | boolean
5
+ | bigint
6
+ | null
7
+ | Value[]
8
+ | { [key: string]: Value }
9
+
10
+ export type Offset = `-1` | `${number}_${number}`
11
+
12
+ interface Header {
13
+ [key: string]: Value
14
+ }
15
+
16
+ export type ControlMessage = {
17
+ headers: Header
18
+ }
19
+
20
+ export type ChangeMessage<T> = {
21
+ key: string
22
+ value: T
23
+ headers: Header & { action: `insert` | `update` | `delete` }
24
+ offset: Offset
25
+ }
26
+
27
+ // Define the type for a record
28
+ export type Message<T extends Value = { [key: string]: Value }> =
29
+ | ControlMessage
30
+ | ChangeMessage<T>
31
+
32
+ export type RegularColumn = {
33
+ type: string
34
+ dims: number
35
+ }
36
+
37
+ export type VarcharColumn = {
38
+ type: `varchar`
39
+ dims: number
40
+ max_length?: number
41
+ }
42
+
43
+ export type BpcharColumn = {
44
+ type: `bpchar`
45
+ dims: number
46
+ length?: number
47
+ }
48
+
49
+ export type TimeColumn = {
50
+ type: `time` | `timetz` | `timestamp` | `timestamptz`
51
+ dims: number
52
+ precision?: number
53
+ }
54
+
55
+ export type IntervalColumn = {
56
+ type: `interval`
57
+ dims: number
58
+ fields?:
59
+ | `YEAR`
60
+ | `MONTH`
61
+ | `DAY`
62
+ | `HOUR`
63
+ | `MINUTE`
64
+ | `YEAR TO MONTH`
65
+ | `DAY TO HOUR`
66
+ | `DAY TO MINUTE`
67
+ | `DAY TO SECOND`
68
+ | `HOUR TO MINUTE`
69
+ | `HOUR TO SECOND`
70
+ | `MINUTE TO SECOND`
71
+ }
72
+
73
+ export type IntervalColumnWithPrecision = {
74
+ type: `interval`
75
+ dims: number
76
+ precision?: 0 | 1 | 2 | 3 | 4 | 5 | 6
77
+ fields?: `SECOND`
78
+ }
79
+
80
+ export type BitColumn = {
81
+ type: `bit`
82
+ dims: number
83
+ length: number
84
+ }
85
+
86
+ export type NumericColumn = {
87
+ type: `numeric`
88
+ dims: number
89
+ precision?: number
90
+ scale?: number
91
+ }
92
+
93
+ export type ColumnInfo =
94
+ | RegularColumn
95
+ | VarcharColumn
96
+ | BpcharColumn
97
+ | TimeColumn
98
+ | IntervalColumn
99
+ | IntervalColumnWithPrecision
100
+ | BitColumn
101
+ | NumericColumn
102
+
103
+ export type Schema = { [key: string]: ColumnInfo }
104
+
105
+ export type TypedMessages<T extends Value = { [key: string]: Value }> = {
106
+ messages: Array<Message<T>>
107
+ schema: ColumnInfo
108
+ }