Skip to content

Commit

Permalink
Move non-protocol options like table & where to the params sub-key (#…
Browse files Browse the repository at this point in the history
…2081)

Fix #2079

Electric's TypeScript client is currently tightly coupled to
PostgreSQL-specific options in its `ShapeStreamOptions` interface. As
Electric plans to support multiple data sources in the future, we need
to separate protocol-level options from source-specific options.

## Changes

1. Created a new `PostgresParams` type to define PostgreSQL-specific
parameters:
   - `table`: The root table for the shape
   - `where`: Where clauses for the shape
   - `columns`: Columns to include in the shape
   - `replica`: Whether to send full or partial row updates
2. Moved PostgreSQL-specific options from the top-level
`ShapeStreamOptions` interface to the `params` sub-key
3. Updated `ParamsRecord` type to include PostgreSQL parameters
4. Updated the `ShapeStream` class to handle parameters from the
`params` object
5. Updated documentation to reflect the changes

## Migration Example

Before:
```typescript
const stream = new ShapeStream({
  url: 'http://localhost:3000/v1/shape',
  table: 'users',
  where: 'id > 100',
  columns: ['id', 'name'],
  replica: 'full'
})
```

After:
```typescript
const stream = new ShapeStream({
  url: 'http://localhost:3000/v1/shape',
  params: {
    table: 'users',
    where: 'id > 100',
    columns: ['id', 'name'],
    replica: 'full'
  }
})
```
  • Loading branch information
KyleAMathews authored Dec 3, 2024
1 parent 3584f67 commit e96928e
Show file tree
Hide file tree
Showing 21 changed files with 535 additions and 215 deletions.
49 changes: 49 additions & 0 deletions .changeset/tiny-socks-drive.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
---
"@electric-sql/client": minor
"@electric-sql/react": minor
---

[BREAKING]: Move non-protocol options like table & where to the params sub-key

## Context

Electric's TypeScript client is currently tightly coupled to PostgreSQL-specific options in its `ShapeStreamOptions` interface. As Electric plans to support multiple data sources in the future, we need to separate protocol-level options from source-specific options.

## Changes

1. Created a new `PostgresParams` type to define PostgreSQL-specific parameters:
- `table`: The root table for the shape
- `where`: Where clauses for the shape
- `columns`: Columns to include in the shape
- `replica`: Whether to send full or partial row updates

2. Moved PostgreSQL-specific options from the top-level `ShapeStreamOptions` interface to the `params` sub-key
3. Updated `ParamsRecord` type to include PostgreSQL parameters
4. Updated the `ShapeStream` class to handle parameters from the `params` object
5. Updated documentation to reflect the changes

## Migration Example

Before:
```typescript
const stream = new ShapeStream({
url: 'http://localhost:3000/v1/shape',
table: 'users',
where: 'id > 100',
columns: ['id', 'name'],
replica: 'full'
})
```

After:
```typescript
const stream = new ShapeStream({
url: 'http://localhost:3000/v1/shape',
params: {
table: 'users',
where: 'id > 100',
columns: ['id', 'name'],
replica: 'full'
}
})
```
4 changes: 3 additions & 1 deletion examples/basic-example/src/Example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ const baseUrl = import.meta.env.ELECTRIC_URL ?? `http://localhost:3000`
export const Example = () => {
const { data: items } = useShape<Item>({
url: `${baseUrl}/v1/shape`,
table: `items`,
params: {
table: `items`,
},
})

return (
Expand Down
4 changes: 2 additions & 2 deletions examples/linearlite/src/pages/Issue/Comments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ function Comments(commentProps: CommentsProps) {
const [newCommentBody, setNewCommentBody] = useState<string>(``)
const allComments = useShape({
url: `${baseUrl}/v1/shape`,
table: `comment`,
databaseId,
params: {
token,
database_id: databaseId,
table: `comment`,
},
})! as Comment[]

Expand Down
2 changes: 1 addition & 1 deletion examples/linearlite/src/shapes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import { baseUrl, databaseId, token } from './electric'

export const issueShape: ShapeStreamOptions = {
url: `${baseUrl}/v1/shape/`,
table: `issue`,
params: {
table: `issue`,
token,
database_id: databaseId,
},
Expand Down
11 changes: 8 additions & 3 deletions examples/nextjs-example/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,22 @@ import { useOptimistic } from "react"
import { useShape, getShapeStream } from "@electric-sql/react"
import "./Example.css"
import { matchStream } from "./match-stream"
import { ShapeStreamOptions } from "@electric-sql/client/*"

const itemShape = () => {
const itemShape = (): ShapeStreamOptions => {
if (typeof window !== `undefined`) {
return {
url: new URL(`/shape-proxy`, window?.location.origin).href,
table: `items`,
params: {
table: `items`,
},
}
} else {
return {
url: new URL(`https://not-sure-how-this-works.com/shape-proxy`).href,
table: `items`,
params: {
table: `items`,
},
}
}
}
Expand Down
8 changes: 6 additions & 2 deletions examples/proxy-auth/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,19 @@ const usersShape = (): ShapeStreamOptions => {
return {
url: new URL(`/shape-proxy?org_id=${org_id}`, window.location.origin)
.href,
table: `users`,
params: {
table: `users`,
},
headers: {
Authorization: org_id || ``,
},
}
} else {
return {
url: new URL(`https://not-sure-how-this-works.com/shape-proxy`).href,
table: `items`,
params: {
table: `items`,
},
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion examples/redis-sync/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ client.connect().then(async () => {

const itemsStream = new ShapeStream({
url: `http://localhost:3000/v1/shape`,
table: `items`,
params: {
table: `items`,
},
})
itemsStream.subscribe(async (messages: Message[]) => {
// Begin a Redis transaction
Expand Down
4 changes: 3 additions & 1 deletion examples/todo-app/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ type ToDo = {
export default function Index() {
const { data: todos } = useShape<ToDo>({
url: `http://localhost:3000/v1/shape`,
table: `todos`,
params: {
table: `todos`,
}
})
todos.sort((a, b) => a.created_at - b.created_at)
console.log({ todos })
Expand Down
4 changes: 3 additions & 1 deletion packages/react-hooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import { useShape } from "@electric-sql/react"
export default function MyComponent () {
const { isLoading, data } = useShape({
url: "http://my-api.com/shape",
table: `foo`,
params: {
table: `foo`
}
})

if (isLoading) {
Expand Down
21 changes: 20 additions & 1 deletion packages/react-hooks/src/react-hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,27 @@ export async function preloadShape<T extends Row<unknown> = Row>(
return shape
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function sortObjectKeys(obj: any): any {
if (typeof obj !== `object` || obj === null) return obj

if (Array.isArray(obj)) {
return obj.map(sortObjectKeys)
}

return (
Object.keys(obj)
.sort()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.reduce<Record<string, any>>((sorted, key) => {
sorted[key] = sortObjectKeys(obj[key])
return sorted
}, {})
)
}

export function sortedOptionsHash<T>(options: ShapeStreamOptions<T>): string {
return JSON.stringify(options, Object.keys(options).sort())
return JSON.stringify(sortObjectKeys(options))
}

export function getShapeStream<T extends Row<unknown>>(
Expand Down
12 changes: 9 additions & 3 deletions packages/react-hooks/test/react-hooks.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { Row } from 'packages/typescript-client/dist'
describe(`useShape`, () => {
it(`should infer correct return type when no selector is provided`, () => {
const shape = useShape({
table: ``,
params: {
table: ``,
},
url: ``,
})

Expand All @@ -21,7 +23,9 @@ describe(`useShape`, () => {

it(`should infer correct return type when a selector is provided`, () => {
const shape = useShape({
table: ``,
params: {
table: ``,
},
url: ``,
selector: (_value: UseShapeResult) => {
return {
Expand All @@ -38,7 +42,9 @@ describe(`useShape`, () => {

it(`should raise a type error if type argument does not equal inferred return type`, () => {
const shape = useShape<Row, number>({
table: ``,
params: {
table: ``,
},
url: ``,
// @ts-expect-error - should have type mismatch, because doesn't match the declared `Number` type
selector: (_value: UseShapeResult) => {
Expand Down
Loading

0 comments on commit e96928e

Please sign in to comment.