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): extract sync API and make DAL optional #1355

Merged
merged 31 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
5139292
Create Relations from SatOpMigrate_Tables
kevin-dp Jun 10, 2024
afe5917
Create DB description from array of SatOpMigrate_Table
kevin-dp Jun 10, 2024
e20610e
Add a subscribe method to the sync API.
kevin-dp Jun 11, 2024
d5f322f
Updated lockfile after rebase
kevin-dp Jun 11, 2024
3831657
Change format of dbDescription in Electric client. Fields is now an o…
kevin-dp Jun 11, 2024
daddd6b
Modify CLI to not produce DAL by default and support --with-dal flag …
kevin-dp Jun 11, 2024
cd583e4
Unit test for computeShape
kevin-dp Jun 11, 2024
27ddfa3
Modified unit tests to new dbDescription format for fields.
kevin-dp Jun 12, 2024
1fa0b8a
Unit test that the CLI creates a correct dbDescription from the bundl…
kevin-dp Jun 12, 2024
e489f5b
Custom serialization of dbDescription to create Relation instances
kevin-dp Jun 12, 2024
62ff47f
Update dbDescription format in generated client for e2e tests
kevin-dp Jun 12, 2024
884705f
Modify e2e tests to be able to run without DAL
kevin-dp Jun 13, 2024
12a9004
Extract replication transformer
kevin-dp Jun 13, 2024
71a290e
Updated DB description schemas in tests to use uppercase PG types
kevin-dp Jun 13, 2024
bd343d0
Run formatter on generator
kevin-dp Jun 13, 2024
e04cbe6
Modify CI to also run Satellite e2e tests without DAL.
kevin-dp Jun 13, 2024
1e47213
Check that table exists when syncing a shape
kevin-dp Jun 13, 2024
94a1f6e
Check that provided table name exists in setReplicationTransform.
kevin-dp Jun 13, 2024
06f1475
Modify name of Satellite e2e tests in CI workflow
kevin-dp Jun 13, 2024
1d8ae83
Fix setReplicationTransform after rebase
kevin-dp Jun 18, 2024
e27cfae
Modify format of fields in DbSchema in tests.
kevin-dp Jun 20, 2024
65ab817
Make --with-dal CLI flag default to true.
kevin-dp Jun 20, 2024
1775eda
Add comment explaining custom serialization function for DB description.
kevin-dp Jun 20, 2024
0f1bc3c
Don't delete table name from user input object to sync
kevin-dp Jun 20, 2024
d9922e1
Fixed typo of fkk to fk
kevin-dp Jun 20, 2024
a6d1331
Modify Table to re-use extracted computeShape function.
kevin-dp Jun 20, 2024
c00eefe
Copy docstring from the Table's sync method to the extracted sync method
kevin-dp Jun 20, 2024
7a25b47
Modify unit test to use extracted computeShape function
kevin-dp Jun 20, 2024
d4eeedf
Also bundle type alias for Electric and JsonNull constant.
kevin-dp Jun 20, 2024
8f0cf10
Parse with-dal flag such that everything is true except false
kevin-dp Jun 24, 2024
f2f3bc7
changeset
kevin-dp Jun 24, 2024
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
6 changes: 6 additions & 0 deletions .changeset/six-knives-notice.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"electric-sql": patch
"@electric-sql/prisma-generator": patch
---

Extract the sync API out of the DAL and make the DAL optional.
4 changes: 3 additions & 1 deletion .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,17 +113,19 @@ jobs:
https://analytics-api.buildkite.com/v1/uploads

e2e_satellite_tests:
name: E2E Satellite tests
runs-on: electric-e2e-8-32
strategy:
matrix:
dialect: [SQLite, Postgres]
dal: [true, false]
name: E2E Satellite tests (Dialect ${{ matrix.dialect }} - uses DAL? ${{ matrix.dal }})
defaults:
run:
working-directory: e2e
env:
BUILDKITE_ANALYTICS_TOKEN: ${{ secrets.BUILDKITE_TEST_ANALYTICS_E2E }}
DIALECT: ${{ matrix.dialect }}
DAL: ${{ matrix.dal }}
steps:
- uses: actions/checkout@v3
with:
Expand Down
9 changes: 9 additions & 0 deletions clients/typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"./node": "./dist/drivers/better-sqlite3/index.js",
"./node-postgres": "./dist/drivers/node-postgres/index.js",
"./pglite": "./dist/drivers/pglite/index.js",
"./protocol": "./dist/_generated/protocol/satellite.js",
"./react": "./dist/frameworks/react/index.js",
"./tauri-postgres": "./dist/drivers/tauri-postgres/index.js",
"./vuejs": "./dist/frameworks/vuejs/index.js",
Expand All @@ -78,6 +79,9 @@
"capacitor": [
"./dist/drivers/capacitor-sqlite/index.d.ts"
],
"client": [
"./dist/client/index.d.ts"
],
"expo": [
"./dist/drivers/expo-sqlite/index.d.ts"
],
Expand All @@ -96,6 +100,9 @@
"pglite": [
"./dist/drivers/pglite/index.d.ts"
],
"protocol": [
"./dist/_generated/protocol/satellite.d.ts"
],
"react": [
"./dist/frameworks/react/index.d.ts"
],
Expand Down Expand Up @@ -181,6 +188,7 @@
"lodash.flow": "^3.5.0",
"lodash.groupby": "^4.6.0",
"lodash.isequal": "^4.5.0",
"lodash.keyby": "^4.6.0",
"lodash.mapvalues": "^4.6.0",
"lodash.omitby": "^4.6.0",
"lodash.partition": "^4.6.0",
Expand Down Expand Up @@ -209,6 +217,7 @@
"@types/lodash.flow": "^3.5.7",
"@types/lodash.groupby": "^4.6.7",
"@types/lodash.isequal": "^4.5.6",
"@types/lodash.keyby": "^4.6.9",
"@types/lodash.mapvalues": "^4.6.7",
"@types/lodash.omitby": "^4.6.7",
"@types/lodash.partition": "^4.6.7",
Expand Down
3 changes: 3 additions & 0 deletions clients/typescript/src/client/conversions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { postgresConverter } from './postgres'
export { sqliteConverter } from './sqlite'
export { PgBasicType } from './types'
9 changes: 6 additions & 3 deletions clients/typescript/src/client/conversions/input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@ export class InputTransformer {
value: any,
fields: Fields
): any {
const pgType = fields.get(field)
const pgType = fields[field]

if (!pgType) throw new InvalidArgumentError(`Unknown field ${field}`)

Expand Down Expand Up @@ -335,7 +335,7 @@ export function transformFields(
// as those will be transformed later when the query on the related field is processed.
const copied: Record<string, any> = { ...o }
Object.entries(o).forEach(([field, value]) => {
const pgType = fields.get(field)
const pgType = fields[field]
// Skip anything that's not an actual column on the table
if (pgType === undefined) return

Expand Down Expand Up @@ -363,7 +363,10 @@ export function isFilterObject(value: any): boolean {
* @returns A filtered object.
*/
function keepTableFieldsOnly(o: object, fields: Fields) {
return filterKeys(o, fields)
return filterKeys(o, {
...fields,
has: (x) => Object.hasOwn(fields, x),
})
}

/**
Expand Down
3 changes: 3 additions & 0 deletions clients/typescript/src/client/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type { TableName, AnyTable, AnyTableSchema } from './model'
export { type DbSchema, createDbDescription } from './util/relations'
export * from './conversions'
4 changes: 2 additions & 2 deletions clients/typescript/src/client/model/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ export class Builder {
* The DAL will convert the string into a BigInt in the `fromSqlite` function from `../conversions/sqlite.ts`.
*/
private castBigIntToText(field: string) {
const pgType = this._tableDescription.fields.get(field)
const pgType = this._tableDescription.fields[field]
if (pgType === PgBasicType.PG_INT8 && this.dialect === 'SQLite') {
const quotedField = quoteIdentifier(field)
return `cast(${quotedField} as TEXT) AS ${quotedField}`
Expand Down Expand Up @@ -308,7 +308,7 @@ export class Builder {
// if field is of type BigInt cast the result to TEXT
// because not all adapters deal well with BigInts
// the DAL will convert the string into a BigInt in the `fromSqlite` function from `../conversions/sqlite.ts`.
const pgType = this._tableDescription.fields.get(field)
const pgType = this._tableDescription.fields[field]
if (pgType === PgBasicType.PG_INT8 && this.dialect === 'SQLite') {
// make a raw string and quote the field name ourselves
// because otherwise Squel would add quotes around the entire cast
Expand Down
131 changes: 101 additions & 30 deletions clients/typescript/src/client/model/client.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,32 @@
import { ElectricNamespace } from '../../electric/namespace'
import { DbSchema, TableSchema } from './schema'
import { DbSchema, TableSchema, TableSchemas } from './schema'
import { rawQuery, liveRawQuery, unsafeExec, Table } from './table'
import { Row, Statement } from '../../util'
import {
QualifiedTablename,
ReplicatedRowTransformer,
Row,
Statement,
} from '../../util'
import { LiveResultContext } from './model'
import { Notifier } from '../../notifiers'
import { DatabaseAdapter } from '../../electric/adapter'
import { GlobalRegistry, Registry, Satellite } from '../../satellite'
import { ReplicationTransformManager } from './transforms'
import {
GlobalRegistry,
Registry,
Satellite,
ShapeSubscription,
} from '../../satellite'
import {
IReplicationTransformManager,
ReplicationTransformManager,
setReplicationTransform,
} from './transforms'
import { Dialect } from '../../migrators/query-builder/builder'
import { InputTransformer } from '../conversions/input'
import { sqliteConverter } from '../conversions/sqlite'
import { postgresConverter } from '../conversions/postgres'
import { IShapeManager } from './shapes'
import { ShapeInputWithTable, sync } from './sync'

export type ClientTables<DB extends DbSchema<any>> = {
[Tbl in keyof DB['tables']]: DB['tables'][Tbl] extends TableSchema<
Expand Down Expand Up @@ -96,25 +111,62 @@ interface RawQueries {
export class ElectricClient<
DB extends DbSchema<any>
> extends ElectricNamespace {
public sync: Omit<IShapeManager, 'subscribe'>
public sync: Omit<IShapeManager, 'subscribe'> & {
/**
* Subscribes to the given shape, returnig a {@link ShapeSubscription} object which
* can be used to wait for the shape to sync initial data.
*
* NOTE: If you establish a shape subscription that has already synced its initial data,
* awaiting `shape.synced` will always resolve immediately as shape subscriptions are persisted.
* i.e.: imagine that you re-sync the same shape during subsequent application loads.
* Awaiting `shape.synced` a second time will only ensure that the initial
* shape load is complete. It does not ensure that the replication stream
* has caught up to the central DB's more recent state.
*
* @param i - The shape to subscribe to
* @param key - An optional unique key that identifies the subscription
* @returns A shape subscription
*/
subscribe: (
i: ShapeInputWithTable,
key?: string
) => Promise<ShapeSubscription>
}

private constructor(
public db: ClientTables<DB> & RawQueries,
dbName: string,
private _dbDescription: DB,
adapter: DatabaseAdapter,
notifier: Notifier,
public readonly satellite: Satellite,
registry: Registry | GlobalRegistry
registry: Registry | GlobalRegistry,
private _replicationTransformManager: IReplicationTransformManager
) {
super(dbName, adapter, notifier, registry)
this.satellite = satellite
// Expose the Shape Sync API without additional properties
this.sync = {
syncStatus: this.satellite.syncStatus.bind(this.satellite),
subscribe: sync.bind(null, this.satellite, this._dbDescription),
unsubscribe: this.satellite.unsubscribe.bind(this.satellite),
}
}

setReplicationTransform<
T extends Record<string, unknown> = Record<string, unknown>
>(
qualifiedTableName: QualifiedTablename,
i: ReplicatedRowTransformer<T>
): void {
setReplicationTransform<T>(
this._dbDescription,
this._replicationTransformManager,
qualifiedTableName,
i
)
}

/**
* Connects to the Electric sync service.
* This method is idempotent, it is safe to call it multiple times.
Expand All @@ -136,7 +188,10 @@ export class ElectricClient<
this.satellite.clientDisconnect()
}

// Builds the DAL namespace from a `dbDescription` object
/**
* Builds the DAL namespace from a `dbDescription` object
* @param minimalDbDescription - A minimal description of the database schema can be provided in order to use Electric without the DAL.
*/
static create<DB extends DbSchema<any>>(
dbName: string,
dbDescription: DB,
Expand All @@ -154,30 +209,44 @@ export class ElectricClient<
)
const inputTransformer = new InputTransformer(converter)

const createTable = (tableName: string) => {
return new Table(
tableName,
adapter,
notifier,
satellite,
replicationTransformManager,
dbDescription,
inputTransformer,
dialect
)
}
// Check if we need to create the DAL
// If the schemas are missing from the `dbDescription``
// it means that the user did not generate the Electric client
// and thus we don't create the DAL.
// This is needed because we piggyback the minimal DB description (that is used without the DAL)
// on the same DB description argument as the one that is used with the DAL.
const ts: Array<[string, TableSchemas]> = Object.entries(
dbDescription.tables
)
const withDal = ts.length > 0 && ts[0][1].modelSchema !== undefined
let dal = {} as ClientTables<DB>

// Create all tables
const dal = Object.fromEntries(
Object.keys(tables).map((tableName) => {
return [tableName, createTable(tableName)]
})
) as ClientTables<DB>
if (withDal) {
const createTable = (tableName: string) => {
return new Table(
tableName,
adapter,
notifier,
satellite,
replicationTransformManager,
dbDescription,
inputTransformer,
dialect
)
}

// Now inform each table about all tables
Object.keys(dal).forEach((tableName) => {
dal[tableName].setTables(new Map(Object.entries(dal)))
})
// Create all tables
dal = Object.fromEntries(
kevin-dp marked this conversation as resolved.
Show resolved Hide resolved
Object.keys(tables).map((tableName) => {
return [tableName, createTable(tableName)]
})
) as ClientTables<DB>

// Now inform each table about all tables
Object.keys(dal).forEach((tableName) => {
dal[tableName].setTables(new Map(Object.entries(dal)))
kevin-dp marked this conversation as resolved.
Show resolved Hide resolved
})
}

const db: ClientTables<DB> & RawQueries = {
...dal,
Expand All @@ -191,10 +260,12 @@ export class ElectricClient<
return new ElectricClient(
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: not for this PR but I think we should really move to object arguments rather than positional haha

db,
dbName,
dbDescription,
adapter,
notifier,
satellite,
registry
registry,
replicationTransformManager
)
}
}
8 changes: 7 additions & 1 deletion clients/typescript/src/client/model/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
export { ElectricClient } from './client'
export type { ClientTables } from './client'
export type { TableSchema } from './schema'
export type {
TableSchema,
TableSchemas,
TableName,
AnyTableSchema,
} from './schema'
export { DbSchema, Relation } from './schema'
export { Table } from './table'
export type { AnyTable } from './table'
export type { HKT } from '../util/hkt'
export type { SyncStatus } from './shapes'
20 changes: 15 additions & 5 deletions clients/typescript/src/client/model/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export type TableName = string
export type FieldName = string
export type RelationName = string

export type Fields = Map<FieldName, PgType>
export type Fields = Record<FieldName, PgType>

export type TableSchema<
T extends Record<string, any>,
Expand Down Expand Up @@ -76,11 +76,21 @@ export type ExtendedTableSchema<
incomingRelations: Relation[]
}

export type TableSchemas = Record<
TableName,
TableSchema<any, any, any, any, any, any, any, any, any, HKT>
export type AnyTableSchema = TableSchema<
any,
any,
any,
any,
any,
any,
any,
any,
any,
HKT
>

export type TableSchemas = Record<TableName, AnyTableSchema>

export type ExtendedTableSchemas = Record<
TableName,
ExtendedTableSchema<any, any, any, any, any, any, any, any, any, HKT>
Expand Down Expand Up @@ -190,7 +200,7 @@ export class DbSchema<T extends TableSchemas> {
}

getFieldNames(table: TableName): FieldName[] {
return Array.from(this.getFields(table).keys())
return Array.from(Object.keys(this.getFields(table)))
}

hasRelationForField(table: TableName, field: FieldName): boolean {
Expand Down
Loading
Loading