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

refactor: proposal to refactor the library with some additional exports #119

Merged
merged 2 commits into from
Oct 19, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"build": "run-s clean format build:*",
"build:main": "tsc -p tsconfig.json",
"build:module": "tsc -p tsconfig.module.json",
"test": "run-s test:db && jest -i",
"test": "run-s test:db && jest --runInBand",
"test:clean": "cd test/db && docker-compose down",
"test:db": "cd test/db && docker-compose down && docker-compose up -d && sleep 5",
"docs": "typedoc --mode file --target ES6 --theme minimal",
"docs:json": "typedoc --json docs/spec.json --mode modules --includeDeclarations --excludeExternals"
Expand Down
57 changes: 57 additions & 0 deletions src/PostgrestClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import PostgrestQueryBuilder from './lib/PostgrestQueryBuilder'
import { PostgrestBuilder } from './lib/types'

export default class PostgrestClient {
url: string
headers: { [key: string]: string }
schema?: string

/**
* Creates a PostgREST client.
*
* @param url URL of the PostgREST endpoint.
* @param headers Custom headers.
* @param schema Postgres schema to switch to.
*/
constructor(
url: string,
{ headers = {}, schema }: { headers?: { [key: string]: string }; schema?: string } = {}
) {
this.url = url
this.headers = headers
this.schema = schema
}

/**
* Authenticates the request with JWT.
*
* @param token The JWT token to use.
*/
auth(token: string): this {
this.headers['Authorization'] = `Bearer ${token}`
return this
}

/**
* Perform a table operation.
*
* @param table The table name to operate on.
*/
from<T = any>(table: string): PostgrestQueryBuilder<T> {
const url = `${this.url}/${table}`
return new PostgrestQueryBuilder(url, { headers: this.headers, schema: this.schema })
}

/**
* Perform a stored procedure call.
*
* @param fn The function name to call.
* @param params The parameters to pass to the function call.
*/
rpc<T = any>(fn: string, params?: object): PostgrestBuilder<T> {
const url = `${this.url}/rpc/${fn}`
return new PostgrestQueryBuilder<T>(url, { headers: this.headers, schema: this.schema }).rpc(
params
)
}
}
60 changes: 5 additions & 55 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,6 @@
import { PostgrestBuilder, PostgrestQueryBuilder } from './builder'
import PostgrestClient from './PostgrestClient'
import PostgrestFilterBuilder from './lib/PostgrestFilterBuilder'
import PostgrestQueryBuilder from './lib/PostgrestQueryBuilder'
import { PostgrestBuilder } from './lib/types'

export class PostgrestClient {
url: string
headers: { [key: string]: string }
schema?: string

/**
* Creates a PostgREST client.
*
* @param url URL of the PostgREST endpoint.
* @param headers Custom headers.
* @param schema Postgres schema to switch to.
*/
constructor(
url: string,
{ headers = {}, schema }: { headers?: { [key: string]: string }; schema?: string } = {}
) {
this.url = url
this.headers = headers
this.schema = schema
}

/**
* Authenticates the request with JWT.
*
* @param token The JWT token to use.
*/
auth(token: string): this {
this.headers['Authorization'] = `Bearer ${token}`
return this
}

/**
* Perform a table operation.
*
* @param table The table name to operate on.
*/
from<T = any>(table: string): PostgrestQueryBuilder<T> {
const url = `${this.url}/${table}`
return new PostgrestQueryBuilder(url, { headers: this.headers, schema: this.schema })
}

/**
* Perform a stored procedure call.
*
* @param fn The function name to call.
* @param params The parameters to pass to the function call.
*/
rpc<T = any>(fn: string, params?: object): PostgrestBuilder<T> {
const url = `${this.url}/rpc/${fn}`
return new PostgrestQueryBuilder<T>(url, { headers: this.headers, schema: this.schema }).rpc(
params
)
}
}
export { PostgrestClient, PostgrestFilterBuilder, PostgrestQueryBuilder, PostgrestBuilder }
247 changes: 2 additions & 245 deletions src/builder.ts → src/lib/PostgrestFilterBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,247 +1,4 @@
import fetch from 'cross-fetch'

/**
* Error format
*
* {@link https://postgrest.org/en/stable/api.html?highlight=options#errors-and-http-status-codes}
*/
interface PostgrestError {
message: string
details: string
hint: string
code: string
}

/**
* Response format
*
* {@link https://github.com/supabase/supabase-js/issues/32}
*/
interface PostgrestResponse<T> {
error: PostgrestError | null
data: T | T[] | null
status: number
statusText: string
// For backward compatibility: body === data
body: T | T[] | null
}

/**
* Base builder
*/

export abstract class PostgrestBuilder<T> implements PromiseLike<any> {
method!: 'GET' | 'HEAD' | 'POST' | 'PATCH' | 'DELETE'
url!: URL
headers!: { [key: string]: string }
schema?: string
body?: Partial<T> | Partial<T>[]

constructor(builder: PostgrestBuilder<T>) {
Object.assign(this, builder)
}

then(onfulfilled?: (value: any) => any, onrejected?: (value: any) => any): Promise<any> {
// https://postgrest.org/en/stable/api.html#switching-schemas
if (typeof this.schema === 'undefined') {
// skip
} else if (['GET', 'HEAD'].includes(this.method)) {
this.headers['Accept-Profile'] = this.schema
} else {
this.headers['Content-Profile'] = this.schema
}
if (this.method !== 'GET' && this.method !== 'HEAD') {
this.headers['Content-Type'] = 'application/json'
}

return fetch(this.url.toString(), {
method: this.method,
headers: this.headers,
body: JSON.stringify(this.body),
})
.then(async (res) => {
let error, data
if (res.ok) {
error = null
data = await res.json()
} else {
error = await res.json()
data = null
}
return {
error,
data,
status: res.status,
statusText: res.statusText,
body: data,
} as PostgrestResponse<T>
})
.then(onfulfilled, onrejected)
}
}

/**
* CRUD
*/

export class PostgrestQueryBuilder<T> extends PostgrestBuilder<T> {
constructor(
url: string,
{ headers = {}, schema }: { headers?: { [key: string]: string }; schema?: string } = {}
) {
super({} as PostgrestBuilder<T>)
this.url = new URL(url)
this.headers = { ...headers }
this.schema = schema
}

/**
* Performs horizontal filtering with SELECT.
*
* @param columns The columns to retrieve, separated by commas.
*/
select(columns = '*'): PostgrestFilterBuilder<T> {
this.method = 'GET'
// Remove whitespaces except when quoted
let quoted = false
const cleanedColumns = columns
.split('')
.map((c) => {
if (/\s/.test(c) && !quoted) {
return ''
}
if (c === '"') {
quoted = !quoted
}
return c
})
.join('')
this.url.searchParams.set('select', cleanedColumns)
return new PostgrestFilterBuilder(this)
}

/**
* Performs an INSERT into the table.
*
* @param values The values to insert.
* @param upsert If `true`, performs an UPSERT.
* @param onConflict By specifying the `on_conflict` query parameter, you can make UPSERT work on a column(s) that has a UNIQUE constraint.
*/
insert(
values: Partial<T> | Partial<T>[],
{ upsert = false, onConflict }: { upsert?: boolean; onConflict?: string } = {}
): PostgrestFilterBuilder<T> {
this.method = 'POST'
this.headers['Prefer'] = upsert
? 'return=representation,resolution=merge-duplicates'
: 'return=representation'
if (upsert && onConflict !== undefined) this.url.searchParams.set('on_conflict', onConflict)
this.body = values
return new PostgrestFilterBuilder(this)
}

/**
* Performs an UPDATE on the table.
*
* @param values The values to update.
*/
update(values: Partial<T>): PostgrestFilterBuilder<T> {
this.method = 'PATCH'
this.headers['Prefer'] = 'return=representation'
this.body = values
return new PostgrestFilterBuilder(this)
}

/**
* Performs a DELETE on the table.
*/
delete(): PostgrestFilterBuilder<T> {
this.method = 'DELETE'
this.headers['Prefer'] = 'return=representation'
return new PostgrestFilterBuilder(this)
}

/** @internal */
rpc(params?: object): PostgrestBuilder<T> {
this.method = 'POST'
this.body = params
return this
}
}

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

class PostgrestTransformBuilder<T> extends PostgrestBuilder<T> {
/**
* Orders the result with the specified `column`.
*
* @param column The column to order on.
* @param ascending If `true`, the result will be in ascending order.
* @param nullsFirst If `true`, `null`s appear first.
* @param foreignTable The foreign table to use (if `column` is a foreign column).
*/
order(
column: keyof T,
{
ascending = true,
nullsFirst = false,
foreignTable,
}: { ascending?: boolean; nullsFirst?: boolean; foreignTable?: string } = {}
): PostgrestTransformBuilder<T> {
const key = typeof foreignTable === 'undefined' ? 'order' : `"${foreignTable}".order`
this.url.searchParams.set(
key,
`"${column}".${ascending ? 'asc' : 'desc'}.${nullsFirst ? 'nullsfirst' : 'nullslast'}`
)
return this
}

/**
* Limits the result with the specified `count`.
*
* @param count The maximum no. of rows to limit to.
* @param foreignTable The foreign table to use (for foreign columns).
*/
limit(
count: number,
{ foreignTable }: { foreignTable?: string } = {}
): PostgrestTransformBuilder<T> {
const key = typeof foreignTable === 'undefined' ? 'limit' : `"${foreignTable}".limit`
this.url.searchParams.set(key, `${count}`)
return this
}

/**
* Limits the result to rows within the specified range, inclusive.
*
* @param from The starting index from which to limit the result, inclusive.
* @param to The last index to which to limit the result, inclusive.
* @param foreignTable The foreign table to use (for foreign columns).
*/
range(
from: number,
to: number,
{ foreignTable }: { foreignTable?: string } = {}
): PostgrestTransformBuilder<T> {
const keyOffset = typeof foreignTable === 'undefined' ? 'offset' : `"${foreignTable}".offset`
const keyLimit = typeof foreignTable === 'undefined' ? 'limit' : `"${foreignTable}".limit`
this.url.searchParams.set(keyOffset, `${from}`)
// Range is inclusive, so add 1
this.url.searchParams.set(keyLimit, `${to - from + 1}`)
return this
}

/**
* Retrieves only one row from the result. Result must be one row (e.g. using
* `limit`), otherwise this will result in an error.
*/
single(): PostgrestTransformBuilder<T> {
this.headers['Accept'] = 'application/vnd.pgrst.object+json'
return this
}
}
import PostgrestTransformBuilder from './PostgrestTransformBuilder'

/**
* Filters
Expand Down Expand Up @@ -273,7 +30,7 @@ type FilterOperator =
| 'phfts'
| 'wfts'

class PostgrestFilterBuilder<T> extends PostgrestTransformBuilder<T> {
export default class PostgrestFilterBuilder<T> extends PostgrestTransformBuilder<T> {
/**
* Finds all rows which doesn't satisfy the filter.
*
Expand Down