Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat (client): make converter API public #1397

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
elixir 1.17.0-otp-27
erlang 27.0
nodejs 20.2.0
nodejs 21.4.0
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

might need to update the CI workflows to use this version as well in the test matrix, build, etc

pnpm 9.4.0
# keep synchronised with version pinned in .github/workflows/satellite_proto.yml
protoc 26.1
69 changes: 69 additions & 0 deletions clients/typescript/src/client/conversions/converter.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Row } from '../../util'
import { AnyTableSchema } from '../model'
import { PgType } from './types'

export interface Converter {
Expand All @@ -7,12 +9,48 @@ export interface Converter {
* @param pgType The Postgres type of the column in which to store the value.
*/
encode(v: any, pgType: PgType): any
/**
* Encodes the provided row for storing in the database.
* @param row The row to encode
* @param tableSchema The schema of the table for this row.
*/
encodeRow(
row: Record<string, unknown>,
tableSchema: Pick<AnyTableSchema, 'fields'>
): Row
/**
* Encodes the provided rows for storing in the database.
* @param rows The rows to encode
* @param tableSchema The schema of the table for these rows.
*/
encodeRows(
rows: Array<Record<string, unknown>>,
tableSchema: Pick<AnyTableSchema, 'fields'>
): Array<Row>
/**
* Decodes the provided value from the database.
* @param v The value to decode.
* @param pgType The Postgres type of the column from which to decode the value.
*/
decode(v: any, pgType: PgType): any
/**
* Decodes the provided row from the database.
* @param row The row to decode
* @param tableSchema The schema of the table for this row.
*/
decodeRow<T extends Record<string, any> = Record<string, any>>(
row: Record<string, unknown>,
tableSchema: Pick<AnyTableSchema, 'fields'>
): T
/**
* Decodes the provided rows from the database.
* @param rows The rows to decode
* @param tableSchema The schema of the table for these rows.
*/
decodeRows<T extends Record<string, any> = Record<string, any>>(
rows: Array<Record<string, unknown>>,
tableSchema: Pick<AnyTableSchema, 'fields'>
): Array<T>
}

/**
Expand All @@ -26,3 +64,34 @@ export interface Converter {
export function isDataObject(v: unknown): boolean {
return v instanceof Date || typeof v === 'bigint' || ArrayBuffer.isView(v)
}

export function mapRow<
T extends Record<string, unknown> = Record<string, unknown>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: might be worth extracting Record<string, unknown> to a type alias like RowRecord or something as it's so common

>(
row: Record<string, unknown>,
tableSchema: Pick<AnyTableSchema, 'fields'>,
f: (v: any, pgType: PgType) => any
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: more explicit name like convert? f feels a little bit too terse haha

): T {
const mappedRow = {} as T

for (const [key, value] of Object.entries(row)) {
const pgType = tableSchema.fields[key]
const mappedValue =
pgType === undefined
? value // it's an unknown column, leave it as is
: f(value, pgType)
mappedRow[key as keyof T] = mappedValue
}

return mappedRow
}

export function mapRows<
T extends Record<string, unknown> = Record<string, unknown>
>(
rows: Array<Record<string, unknown>>,
tableSchema: Pick<AnyTableSchema, 'fields'>,
f: (v: any, pgType: PgType) => any
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: same as above

): T[] {
return rows.map((row) => mapRow<T>(row, tableSchema, f))
}
3 changes: 2 additions & 1 deletion clients/typescript/src/client/conversions/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { postgresConverter } from './postgres'
export type { Converter } from './converter'
export { sqliteConverter } from './sqlite'
export { postgresConverter } from './postgres'
export { PgBasicType } from './types'
23 changes: 20 additions & 3 deletions clients/typescript/src/client/conversions/postgres.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { InvalidArgumentError } from '../validation/errors/invalidArgumentError'
import { Converter } from './converter'
import { Converter, mapRow, mapRows } from './converter'
import { deserialiseDate, serialiseDate } from './datatypes/date'
import { isJsonNull } from './datatypes/json'
import { PgBasicType, PgDateType, PgType } from './types'
import { AnyTableSchema } from '../model/schema'

/**
* This module takes care of converting TypeScript values to a Postgres storeable value and back.
Expand All @@ -11,7 +12,7 @@ import { PgBasicType, PgDateType, PgType } from './types'
* Currently, no conversions are needed for the data types we support.
*/

function toPostgres(v: any, pgType: PgType): any {
export function toPostgres(v: any, pgType: PgType): any {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: why are these exported, they are exported via the converter interface no? (same for SQLite)

if (v === null) {
// don't transform null values
return v
Expand Down Expand Up @@ -52,7 +53,7 @@ function toPostgres(v: any, pgType: PgType): any {
}
}

function fromPostgres(v: any, pgType: PgType): any {
export function fromPostgres(v: any, pgType: PgType): any {
if (v === null) {
// don't transform null values
return v
Expand Down Expand Up @@ -97,5 +98,21 @@ function fromPostgres(v: any, pgType: PgType): any {

export const postgresConverter: Converter = {
encode: toPostgres,
encodeRow: <T extends Record<string, unknown> = Record<string, unknown>>(
row: Record<string, unknown>,
tableSchema: AnyTableSchema
) => mapRow<T>(row, tableSchema, toPostgres),
encodeRows: <T extends Record<string, unknown> = Record<string, unknown>>(
rows: Array<Record<string, unknown>>,
tableSchema: AnyTableSchema
) => mapRows<T>(rows, tableSchema, toPostgres),
decode: fromPostgres,
decodeRow: <T extends Record<string, any> = Record<string, any>>(
row: Record<string, unknown>,
tableSchema: AnyTableSchema
) => mapRow<T>(row, tableSchema, fromPostgres),
decodeRows: <T extends Record<string, any> = Record<string, any>>(
rows: Array<Record<string, unknown>>,
tableSchema: AnyTableSchema
) => mapRows<T>(rows, tableSchema, fromPostgres),
}
23 changes: 20 additions & 3 deletions clients/typescript/src/client/conversions/sqlite.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { InvalidArgumentError } from '../validation/errors/invalidArgumentError'
import { Converter } from './converter'
import { Converter, mapRow, mapRows } from './converter'
import { deserialiseBoolean, serialiseBoolean } from './datatypes/boolean'
import { deserialiseBlob, serialiseBlob } from './datatypes/blob'
import { deserialiseDate, serialiseDate } from './datatypes/date'
import { deserialiseJSON, serialiseJSON } from './datatypes/json'
import { PgBasicType, PgDateType, PgType, isPgDateType } from './types'
import { AnyTableSchema } from '../model/schema'

/**
* This module takes care of converting TypeScript values for Postgres-specific types to a SQLite storeable value and back.
Expand All @@ -14,7 +15,7 @@ import { PgBasicType, PgDateType, PgType, isPgDateType } from './types'
* When reading from the SQLite database, the string can be parsed back into a `Date` object.
*/

function toSqlite(v: any, pgType: PgType): any {
export function toSqlite(v: any, pgType: PgType): any {
if (v === null) {
// don't transform null values
return v
Expand Down Expand Up @@ -49,7 +50,7 @@ function toSqlite(v: any, pgType: PgType): any {
}
}

function fromSqlite(v: any, pgType: PgType): any {
export function fromSqlite(v: any, pgType: PgType): any {
if (v === null) {
// don't transform null values
return v
Expand Down Expand Up @@ -93,5 +94,21 @@ function fromSqlite(v: any, pgType: PgType): any {

export const sqliteConverter: Converter = {
encode: toSqlite,
encodeRow: <T extends Record<string, unknown> = Record<string, unknown>>(
row: Record<string, unknown>,
tableSchema: AnyTableSchema
) => mapRow<T>(row, tableSchema, toSqlite),
encodeRows: <T extends Record<string, unknown> = Record<string, unknown>>(
rows: Array<Record<string, unknown>>,
tableSchema: AnyTableSchema
) => mapRows<T>(rows, tableSchema, toSqlite),
decode: fromSqlite,
decodeRow: <T extends Record<string, any> = Record<string, any>>(
row: Record<string, unknown>,
tableSchema: AnyTableSchema
) => mapRow<T>(row, tableSchema, fromSqlite),
decodeRows: <T extends Record<string, unknown> = Record<string, unknown>>(
rows: Array<Record<string, unknown>>,
tableSchema: AnyTableSchema
) => mapRows<T>(rows, tableSchema, fromSqlite),
}
10 changes: 3 additions & 7 deletions clients/typescript/src/client/execution/nonTransactionalDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { QueryBuilder } from 'squel'
import { DB } from './db'
import * as z from 'zod'
import { Row, Statement } from '../../util'
import { Transformation, transformFields } from '../conversions/input'
import { Fields } from '../model/schema'
import { Converter } from '../conversions/converter'

Expand Down Expand Up @@ -59,12 +58,9 @@ export class NonTransactionalDB implements DB {
// convert SQLite/PG values back to JS values
// and then parse the transformed object
// with the Zod schema to validate it
const transformedRow = transformFields(
row,
this._fields,
this._converter,
Transformation.Decode
)
const transformedRow = this._converter.decodeRow(row, {
fields: this._fields,
})
return schema.parse(transformedRow)
})
successCallback(this, objects)
Expand Down
10 changes: 3 additions & 7 deletions clients/typescript/src/client/execution/transactionalDB.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { DB } from './db'
import * as z from 'zod'
import { Row, Statement } from '../../util'
import { Fields } from '../model/schema'
import { Transformation, transformFields } from '../conversions/input'
import { Converter } from '../conversions/converter'

export class TransactionalDB implements DB {
Expand Down Expand Up @@ -52,12 +51,9 @@ export class TransactionalDB implements DB {
// convert SQLite/PG values back to JS values
// and then parse the transformed object
// with the Zod schema to validate it
const transformedRow = transformFields(
row,
this._fields,
this._converter,
Transformation.Decode
)
const transformedRow = this._converter.decodeRow(row, {
fields: this._fields,
})
return schema.parse(transformedRow)
})
successCallback(
Expand Down
4 changes: 1 addition & 3 deletions clients/typescript/src/client/model/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,7 @@ export class ElectricClient<
}
}

setReplicationTransform<
T extends Record<string, unknown> = Record<string, unknown>
>(
setReplicationTransform<T extends Row = Row>(
qualifiedTableName: QualifiedTablename,
i: ReplicatedRowTransformer<T>
): void {
Expand Down
Loading