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

Update Type Implementation (postgrest-js side) #152

Closed
wants to merge 5 commits 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
19 changes: 12 additions & 7 deletions src/PostgrestClient.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import PostgrestQueryBuilder from './lib/PostgrestQueryBuilder'
import PostgrestTransformBuilder from './lib/PostgrestTransformBuilder'
import { SchemaBase, TableBase } from './lib/types'

export default class PostgrestClient {
export default class PostgrestClient<S extends SchemaBase = SchemaBase> {
url: string
headers: { [key: string]: string }
schema?: string
Expand Down Expand Up @@ -37,9 +38,12 @@ export default class PostgrestClient {
*
* @param table The table name to operate on.
*/
from<T = any>(table: string): PostgrestQueryBuilder<T> {
from<K extends keyof S>(table: K): PostgrestQueryBuilder<S[K]> {
const url = `${this.url}/${table}`
return new PostgrestQueryBuilder<T>(url, { headers: this.headers, schema: this.schema })
return new PostgrestQueryBuilder<S[K]>(url, {
headers: this.headers,
schema: this.schema,
})
}

/**
Expand All @@ -48,10 +52,11 @@ export default class PostgrestClient {
* @param fn The function name to call.
* @param params The parameters to pass to the function call.
*/
rpc<T = any>(fn: string, params?: object): PostgrestTransformBuilder<T> {
rpc<T extends TableBase>(fn: string, params?: object): PostgrestTransformBuilder<T> {
const url = `${this.url}/rpc/${fn}`
return new PostgrestQueryBuilder<T>(url, { headers: this.headers, schema: this.schema }).rpc(
params
)
return new PostgrestQueryBuilder<T>(url, {
headers: this.headers,
schema: this.schema,
}).rpc(params)
}
}
11 changes: 9 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import PostgrestClient from './PostgrestClient'
import PostgrestFilterBuilder from './lib/PostgrestFilterBuilder'
import PostgrestQueryBuilder from './lib/PostgrestQueryBuilder'
import { PostgrestBuilder } from './lib/types'
import { PostgrestBuilder, SchemaBase, TableBase } from './lib/types'

export { PostgrestClient, PostgrestFilterBuilder, PostgrestQueryBuilder, PostgrestBuilder }
export {
PostgrestClient,
PostgrestFilterBuilder,
PostgrestQueryBuilder,
PostgrestBuilder,
SchemaBase,
TableBase,
}
8 changes: 6 additions & 2 deletions src/lib/PostgrestFilterBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import PostgrestTransformBuilder from './PostgrestTransformBuilder'
import { TableBase } from './types'

/**
* Filters
*/

const cleanFilterArray = <T>(filter: T[keyof T][]) => filter.map((s) => `"${s}"`).join(',')
const cleanFilterArray = <T extends TableBase>(filter: T[keyof T][]) =>
filter.map((s) => `"${s}"`).join(',')

type FilterOperator =
| 'eq'
Expand All @@ -30,7 +32,9 @@ type FilterOperator =
| 'phfts'
| 'wfts'

export default class PostgrestFilterBuilder<T> extends PostgrestTransformBuilder<T> {
export default class PostgrestFilterBuilder<
T extends TableBase
> extends PostgrestTransformBuilder<T> {
/**
* Finds all rows which doesn't satisfy the filter.
*
Expand Down
22 changes: 15 additions & 7 deletions src/lib/PostgrestQueryBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { PostgrestBuilder } from './types'
import { PostgrestBuilder, TableBase } from './types'
import PostgrestFilterBuilder from './PostgrestFilterBuilder'
import PostgrestTransformBuilder from './PostgrestTransformBuilder'

export default class PostgrestQueryBuilder<T> extends PostgrestBuilder<T> {
export default class PostgrestQueryBuilder<T extends TableBase> extends PostgrestBuilder<T> {
constructor(
url: string,
{ headers = {}, schema }: { headers?: { [key: string]: string }; schema?: string } = {}
{ headers = {}, schema }: { headers?: Record<string, string>; schema?: string } = {}
) {
super({} as PostgrestBuilder<T>)
this.url = new URL(url)
Expand All @@ -21,7 +21,7 @@ export default class PostgrestQueryBuilder<T> extends PostgrestBuilder<T> {
* @param count Count algorithm to use to count rows in a table.
*/
select(
columns = '*',
columns: '*' | string | keyof T | Array<keyof T> = '*',
Copy link
Author

Choose a reason for hiding this comment

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

This will show '*', and the column names as suggestions, while still allowing a comma-separated string

Copy link
Author

@beeequeue beeequeue May 11, 2021

Choose a reason for hiding this comment

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

If this is going to be a part of a breaking change, I would recommend removing string (and therefore comma separated strings) as an option - IIRC it will then error if you give it an unknown column if a table type is passed in, and will result in string if no custom table type is passed.

columns: '*' | keyof T | Array<keyof T> = '*',

{
head = false,
count = null,
Expand All @@ -31,9 +31,14 @@ export default class PostgrestQueryBuilder<T> extends PostgrestBuilder<T> {
} = {}
): PostgrestFilterBuilder<T> {
this.method = 'GET'

if (Array.isArray(columns)) {
columns = columns.join(',')
}
Comment on lines +35 to +37
Copy link
Author

Choose a reason for hiding this comment

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

This handles the array select input


// Remove whitespaces except when quoted
let quoted = false
const cleanedColumns = columns
const cleanedColumns = (columns as string)
.split('')
.map((c) => {
if (/\s/.test(c) && !quoted) {
Expand All @@ -46,12 +51,15 @@ export default class PostgrestQueryBuilder<T> extends PostgrestBuilder<T> {
})
.join('')
this.url.searchParams.set('select', cleanedColumns)

if (count) {
this.headers['Prefer'] = `count=${count}`
}

if (head) {
this.method = 'HEAD'
}

return new PostgrestFilterBuilder(this)
}

Expand Down Expand Up @@ -88,9 +96,9 @@ export default class PostgrestQueryBuilder<T> extends PostgrestBuilder<T> {
if (count) {
prefersHeaders.push(`count=${count}`)
}

this.headers['Prefer'] = prefersHeaders.join(',')

return new PostgrestFilterBuilder(this)
}

Expand Down
4 changes: 2 additions & 2 deletions src/lib/PostgrestTransformBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { PostgrestBuilder, PostgrestSingleResponse } from './types'
import { PostgrestBuilder, PostgrestSingleResponse, TableBase } from './types'

/**
* Post-filters (transforms)
*/

export default class PostgrestTransformBuilder<T> extends PostgrestBuilder<T> {
export default class PostgrestTransformBuilder<T extends TableBase> extends PostgrestBuilder<T> {
/**
* Performs vertical filtering with SELECT.
*
Expand Down
10 changes: 9 additions & 1 deletion src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import fetch from 'cross-fetch'

export type TableBase = Record<string, any>

export type SchemaBase = Record<string, TableBase>

/**
* Error format
*
Expand Down Expand Up @@ -28,13 +32,15 @@ interface PostgrestResponseSuccess<T> extends PostgrestResponseBase {
body: T[]
count: number | null
}

interface PostgrestResponseFailure extends PostgrestResponseBase {
error: PostgrestError
data: null
// For backward compatibility: body === data
body: null
count: null
}

export type PostgrestResponse<T> = PostgrestResponseSuccess<T> | PostgrestResponseFailure

interface PostgrestSingleResponseSuccess<T> extends PostgrestResponseBase {
Expand All @@ -43,11 +49,13 @@ interface PostgrestSingleResponseSuccess<T> extends PostgrestResponseBase {
// For backward compatibility: body === data
body: T
}

export type PostgrestSingleResponse<T> =
| PostgrestSingleResponseSuccess<T>
| PostgrestResponseFailure

export abstract class PostgrestBuilder<T> implements PromiseLike<PostgrestResponse<T>> {
export abstract class PostgrestBuilder<T extends TableBase>
implements PromiseLike<PostgrestResponse<T>> {
protected method!: 'GET' | 'HEAD' | 'POST' | 'PATCH' | 'DELETE'
protected url!: URL
protected headers!: { [key: string]: string }
Expand Down
45 changes: 45 additions & 0 deletions test/__snapshots__/index.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -2645,6 +2645,51 @@ Object {
}
`;

exports[`select with array columns 1`] = `
Object {
"body": Array [
Object {
"age_range": "[1,2)",
"username": "supabot",
},
Object {
"age_range": "[25,35)",
"username": "kiwicopple",
},
Object {
"age_range": "[25,35)",
"username": "awailas",
},
Object {
"age_range": "[20,30)",
"username": "dragarcia",
},
],
"count": 4,
"data": Array [
Object {
"age_range": "[1,2)",
"username": "supabot",
},
Object {
"age_range": "[25,35)",
"username": "kiwicopple",
},
Object {
"age_range": "[25,35)",
"username": "awailas",
},
Object {
"age_range": "[20,30)",
"username": "dragarcia",
},
],
"error": null,
"status": 200,
"statusText": "OK",
}
`;

exports[`select with count:exact 1`] = `
Object {
"body": Array [
Expand Down
77 changes: 67 additions & 10 deletions test/basic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,23 +102,75 @@ test('connection error', async () => {
expect(isErrorCaught).toBe(true)
})

test('custom type', async () => {
interface User {
describe('types', () => {
type User = {
username: string
data: object | null
age_range: string | null
status: 'ONLINE' | 'OFFLINE'
catchphrase: 'string' | null
}

// TODO: Find a cleaner way to weave a custom type
// eq should show User's properties in LSP/IntelliSense
const { data: users } = <{ data: User[] }>(
await postgrest.from<User>('users').select().eq('username', 'supabot')
)
const user = users[0]
// Autocomplete should show properties of user after '.'
user.username
describe('without schema', () => {
test('everything works without any types passed in', async () => {
// eq should show User's properties in LSP/IntelliSense
const { data: users } = await postgrest.from('users').select().eq('username', 'supabot')

// Should not error on any property
users[0].username
users[0].somethingElse
Comment on lines +120 to +121
Copy link
Author

Choose a reason for hiding this comment

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

These are for some reason nullable, but I can't find what in the code actually does it... They weren't nullable before and they aren't in other tests..


// Should not error when using properties
const username: string = users[0].username
})
})

type Session = {
id: string
expires: string
}

type Schema = {
users: User
sessions: Session
}

describe('with schema', () => {
const typedClient = new PostgrestClient<Schema>(REST_URL)

test('enforces table and column names', async () => {
// Should error on incorrect table name
// @ts-expect-error
typedClient.from('asdasd')

// Should error on incorrect column name
// @ts-expect-error
typedClient.from('sessions').select('expires').eq('asdsdasd')

// Allows array, comma-separated, and * selects
typedClient.from('users').select('username,age_range').eq('username', 'supabot')
typedClient.from('users').select(['username', 'age_range']).eq('username', 'supabot')
typedClient.from('users').select('*').eq('username', 'supabot')

// Reports incorrect columns in array select
// @ts-expect-error
typedClient.from('users').select(['username', 'age_']).eq('username', 'supabot')

// eq and select should show User's properties in LSP/IntelliSense
const { data: users } = await typedClient.from('users').select('*').eq('username', 'supabot')

// Should not error on any property
users[0].username
// Should error on incorrect property
// @ts-expect-error
users[0].somethingElse

// Returns correct types
const username: string = users[0].username
// @ts-expect-error
const notUsername: number = users[0].catchphrase
})
})
})

test("don't mutate PostgrestClient.headers", async () => {
Expand Down Expand Up @@ -170,6 +222,11 @@ test('select with count:exact', async () => {
expect(res).toMatchSnapshot()
})

test('select with array columns', async () => {
const res = await postgrest.from('users').select(['username', 'age_range'], { count: 'exact' })
expect(res).toMatchSnapshot()
})

test("stored procedure with count: 'exact'", async () => {
const res = await postgrest.rpc('get_status', { name_param: 'supabot', count: 'exact' })
expect(res).toMatchSnapshot()
Expand Down