diff --git a/libraries/supabase-js/src/Base.js b/libraries/supabase-js/src/Base.js new file mode 100644 index 00000000000..296705ad3dc --- /dev/null +++ b/libraries/supabase-js/src/Base.js @@ -0,0 +1,144 @@ +import { Socket } from 'phoenix-channels' +import BaseRequest from './BaseRequest' + +class Base { + constructor(tableName, restUrl, apiSocket, uuid) { + this.tableName = tableName + this.restUrl = restUrl + this.apiSocket = apiSocket + this.uuid = uuid + + this.socket = null + this.channel = null + this.listeners = {} + + this.queryFilters = [] + } + + createListener() { + this.socket = new Socket(apiSocket) + this.channel = this.socket.channel(this.tableName) + } + + on(eventType, callbackFunction) { + if (this.socket == null) this.createListener() + + var ref = this.channel.on(eventType, callbackFunction) + this.listeners[eventType] = ref + } + + subscribe() { + if (this.socket == null) this.createListener() + + this.socket.connect() + + if (this.channel.state !== 'joined') { + this.channel + .join() + .receive('ok', resp => console.log('Joined Realtime successfully ', resp)) + .receive('error', resp => console.log('Unable to join ', resp)) + .receive('timeout', () => console.log('Networking issue. Still waiting...')) + } + } + + unsubscribe() { + for (var ref in this.listeners) { + let eventName = this.listeners[ref] + this.off(eventName, ref) + } + + this.socket.disconnect() + } + + request(method) { + let path = `${this.restUrl}/${this.tableName}` + return new BaseRequest(method, path) + } + + addFilters(request, options) { + if (Object.keys(options).length != 0) { + Object.keys(options).forEach(option => { + let setting = options[option] + request.set(option, setting) + }) + } + + // loop through this.queryFilters + this.queryFilters.forEach(queryFilter => { + + let columnName = queryFilter.columnName + let operator = queryFilter.operator + let criteria = queryFilter.criteria + + request.filter(columnName, operator, criteria) + }) + } + + select(columnQuery = '*', options = {}) { + let method = 'get' + let request = this.request(method) + + request.select(columnQuery) + this.addFilters(request, options) + + return request + } + + insert(data, options = {}) { + let method = 'post' + let request = this.request(method) + + if (!Array.isArray(data)) { + return { + body: null, + status: 400, + statusCode: 400, + statusText: 'Data type should be an array.', + } + } + + data.forEach(datum => { + request.send(datum) + }) + + this.addFilters(request, options) + + return request + } + + update(data, options = {}) { + let method = 'patch' + let request = this.request(method) + + request.send(data) + this.addFilters(request, options) + + return request + } + + delete(options = {}) { + let method = 'delete' + let request = this.request(method) + + this.addFilters(request, options) + + return request + } +} + +// pre-empts if any of the filters are used before select +const filters = ['eq', 'gt', 'lt', 'gte', 'lte', 'like', 'ilike', 'is', 'in', 'not'] + +filters.forEach( + operator => + (Base.prototype[operator] = function filterValue(columnName, criteria) { + this.queryFilters.push({ + columnName, + operator, + criteria, + }) + return this + }) +) + +export default Base \ No newline at end of file diff --git a/libraries/supabase-js/src/BaseRequest.js b/libraries/supabase-js/src/BaseRequest.js index 3988f0f32fe..68d516a818a 100644 --- a/libraries/supabase-js/src/BaseRequest.js +++ b/libraries/supabase-js/src/BaseRequest.js @@ -4,6 +4,7 @@ */ import { Request } from 'superagent' +import * as Filters from './utils/Filters' const contentRangeStructure = /^(\d+)-(\d+)\/(\d+)$/ @@ -21,6 +22,19 @@ class BaseRequest extends Request { super(method, path) this.set('Accept', 'application/json') + this.filterMap = { + eq: (columnName, criteria) => Filters.Eq(columnName, criteria), + gt: (columnName, criteria) => Filters.Gt(columnName, criteria), + lt: (columnName, criteria) => Filters.Lt(columnName, criteria), + gte: (columnName, criteria) => Filters.Gte(columnName, criteria), + lte: (columnName, criteria) => Filters.Lte(columnName, criteria), + like: (columnName, criteria) => Filters.Like(columnName, criteria), + ilike: (columnName, criteria) => Filters.Ilike(columnName, criteria), + is: (columnName, criteria) => Filters.Is(columnName, criteria), + in: (columnName, criteria) => Filters.In(columnName, criteria), + not: (columnName, criteria) => Filters.Not(columnName, criteria), + } + // Fix for superagent disconnect on client & server. if (!this.get) { this.get = this.getHeader @@ -28,7 +42,7 @@ class BaseRequest extends Request { } /** - * Set auth using special formats. If only one string paramter is passed, it + * Set auth using special formats. If only one string parameter is passed, it * is interpreted as a bearer token. If an object and nothing else is passed, * `user` and `pass` keys are extracted from it and used for basic auth. * @@ -51,6 +65,20 @@ class BaseRequest extends Request { return super.auth(user, pass) } + /** + * All in one conveniencd function for all filters available. + * + * @param {string} Name of the database column. + * @param {string} Name of filter operator be utilised. + * @param { object | array | string | integer | boolean | null } Value to compare to. Exact data type of criteria would depend on the operator used. + * @returns {BaseRequest} The API request object. + */ + + filter(columnName, operator, criteria){ + let newQuery = this.filterMap[operator.toLowerCase()](columnName, criteria) + return this.query(newQuery) + } + /** * Takes a query object and translates it to a PostgREST filter query string. * All values are prefixed with `eq.`. @@ -179,15 +207,15 @@ class BaseRequest extends Request { * For all of the PostgREST filters add a shortcut method to use it. * * @param {string} The name of the column. - * @param {any} The value of the column to be filtered. + * @param { object | array | string | integer | boolean | null } The value of the column to be filtered. * @returns {BaseRequest} The API request object. */ const filters = ['eq', 'gt', 'lt', 'gte', 'lte', 'like', 'ilike', 'is', 'in', 'not'] filters.forEach(filter => - BaseRequest.prototype[filter] = function filterValue (name, value) { - return this.query(`${name}=${filter}.${Array.isArray(value) ? value.join(',') : value}`) + BaseRequest.prototype[filter] = function filterValue (columnName, criteria) { + return this.filter(columnName, filter, criteria) } ) diff --git a/libraries/supabase-js/src/Subscription.js b/libraries/supabase-js/src/Subscription.js deleted file mode 100644 index 94f232056fe..00000000000 --- a/libraries/supabase-js/src/Subscription.js +++ /dev/null @@ -1,44 +0,0 @@ -import { Socket } from 'phoenix-channels' - -export default class BaseChannel { - constructor(tableName, apiSocket, uuid){ - this.socket = new Socket(apiSocket) - this.uuid = uuid - this.tableName = tableName - this.channel = this.socket.channel(tableName) - - this.start() - - this.socket.onOpen(() => { - console.log('Socket connected') - }) - this.socket.onClose(() => { - console.log('Socket disconnected') - }) - } - - on(eventName, callbackFunction){ - this.channel.on(eventName, callbackFunction) - } - - start(){ - this.socket.connect() - - if(this.channel.state !== 'joined'){ - this.channel - .join() - .receive('ok', resp => console.log('Joined Realtime successfully ', resp)) - .receive('error', resp => console.log('Unable to join ', resp)) - .receive('timeout', () => console.log('Networking issue. Still waiting...')) - } - } - - stop(){ - for (var ref in this.listeners){ - let eventName = this.listeners[ref] - this.off(eventName, ref) - } - - this.socket.disconnect() - } -} \ No newline at end of file diff --git a/libraries/supabase-js/src/index.js b/libraries/supabase-js/src/index.js index b41eb0e45a1..c23e6251ed3 100644 --- a/libraries/supabase-js/src/index.js +++ b/libraries/supabase-js/src/index.js @@ -1,82 +1,47 @@ import BaseRequest from './BaseRequest' import Subscription from './Subscription' import { uuid } from './utils/Helpers' +import Base from './Base' class SupabaseClient { - constructor(supabaseUrl, supabaseKey, options) { + constructor(supabaseUrl, supabaseKey, options = {}) { this.supabaseUrl = supabaseUrl this.supabaseKey = supabaseKey - // this will be the case for now - this.restUrl = supabaseUrl - this.apiSocket = '' - + this.restUrl = null + this.apiSocket = null this.subscriptions = {} + + this.authenticate(supabaseUrl, supabaseKey) } - /** - * @todo - */ - subscribe(tableName) { - let uuid = uuid() + authenticate(supabaseUrl, supabaseKey){ + // do something to utilise parameters and receive + // the restUrl & apiSocket + + var restUrl = supabaseUrl + var apiSocket = '' - this.subscriptions[uuid] = new Subscription(tableName, this.apiSocket, uuid) - return this.subscriptions[uuid] + this.restUrl = restUrl + this.apiSocket = apiSocket } - unsubscribe(subscription){ - subscription.stop() - delete this.subscriptions[subscription.uuid] + from(tableName){ + let identifier = uuid() + + this.subscriptions[identifier] = new Base(tableName, this.restUrl, this.apiSocket, identifier) + return this.subscriptions[identifier] } - /** - * Convenience wrapper for starting a PostgREST request builder. Adds the - * API URL to the provided path. - * - * @param {string} The HTTP method of the request. - * @param {string} The path of the request. - * @returns {BaseRequest} The API request object. - */ - - request(method, path) { - return new BaseRequest(method, this.restUrl + path) + removeSubscription(mySubscription){ + mySubscription.unsubscribe() + delete this.subscriptions[mySubscription.uuid] } } -/** - * Basic HTTP method functions for quick chaining. - * - * @param {string} The path of the request. - * @returns {BaseRequest} The API request object. - */ - -const methods = ['POST', 'GET', 'PATCH', 'DELETE'] - -methods.forEach(method => { - SupabaseClient.prototype[method.toLowerCase()] = function requestMethod(tableName) { - let path = `/${tableName}` - return this.request(method, path) - } -}) - const createClient = (supabaseUrl, supabaseKey, options = {}) => { return new SupabaseClient(supabaseUrl, supabaseKey, options) } export { createClient } - -/** - * TO BE REMOVED SOON - */ - -// const defaultAwesomeFunction = (name) => { -// const returnStr = `I am the Default Awesome Function, fellow comrade! - ${name}`; -// return returnStr; -// }; - -// const awesomeFunction = () => 'I am just an Awesome Function'; - -// export default defaultAwesomeFunction; - -// export { awesomeFunction }; diff --git a/libraries/supabase-js/src/utils/Filters.js b/libraries/supabase-js/src/utils/Filters.js new file mode 100644 index 00000000000..92cc784afae --- /dev/null +++ b/libraries/supabase-js/src/utils/Filters.js @@ -0,0 +1,39 @@ +export function Eq(columnName, filterValue){ + return `${columnName}=eq.${filterValue}` +} + +export function Gt(columnName, filterValue){ + return `${columnName}=gt.${filterValue}` +} + +export function Lt(columnName, filterValue){ + return `${columnName}=lt.${filterValue}` +} + +export function Gte(columnName, filterValue){ + return `${columnName}=gte.${filterValue}` +} + +export function Lte(columnName, filterValue){ + return `${columnName}=lte.${filterValue}` +} + +export function Like(columnName, stringPattern){ + return `${columnName}=like.${stringPattern}` +} + +export function Ilike(columnName, stringPattern){ + return `${columnName}=ilike.${stringPattern}` +} + +export function Is(columnName, filterValue){ + return `${columnName}=is.${filterValue}` +} + +export function In(columnName, filterArray){ + return `${columnName}=in.${filterArray.join(',')}` +} + +export function Not(columnName, filterValue){ + return `${columnName}=not.${filterValue}` +} \ No newline at end of file