Skip to content

Commit

Permalink
feat: refactor the library with additional exports
Browse files Browse the repository at this point in the history
  • Loading branch information
kiwicopple authored and soedirgo committed Oct 20, 2020
1 parent c96ccfb commit 38d604d
Show file tree
Hide file tree
Showing 7 changed files with 309 additions and 301 deletions.
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
Loading

0 comments on commit 38d604d

Please sign in to comment.