diff --git a/docs/sql-helpers.md b/docs/sql-helpers.md new file mode 100644 index 0000000000..edabf3de4e --- /dev/null +++ b/docs/sql-helpers.md @@ -0,0 +1,93 @@ +# SQL helpers + +Files can be found in [src/server/sql](/src/server/sql). + +These are functions useful in ETL and reshaping operations +for database -> resolver data shapes. + +### orderedFor + +In `helpers.js` + +``` +orderedFor( rows, collection, field, singleObject ) +``` + +__rows__ is the rows returned from the database, +there are multiple rows for each element of the collection. + +__collection__ is the set of items you are getting multiple rows for from the database. + +__field__ is the object filed in the rows to group the results by +and then associate the elements of the collection. + +__singleObject__ is a boolean which determines if each collection +entry is a single object (1-1 relationship) or not (1-N) relationship. + +It returns an array of objects in the first case, and +an array of arrays of objects in the second case. + +--- + +The adapters are a work in progress and +require some changes to how arguments +to the graphql query are passed around in the server. +The query also has more parameters, though maybe they should +be combined into a single object with subfields +for the different capabilities...? + +### paging + +In `paging.js`, adds limit and offset + +``` +paging(queryBuilder, args) +``` + +__queryBuilder__ is a knex instance. + +__args__ is an object with fields `{limit, offset}` + +The function returns the queryBuilder object after checking for and adding +limit and/or offset knex calls. + +### cursor + +TBD... cursor based paging + + +### currentOrdering + +In `ordering.js` +uses the current code an orderBy argument + + +### ordering + +In `ordering.js`, looks for an array of OrderBy GraphQL type + +An orderby object looks like `{column, table, order}`, only column is required. + + +``` +ordering(queryBuilder, args) +``` + +__queryBuilder__ is a knex instance. + +__args__ is an array named `orderBys` containing objects with the fields above. + +The function returns the queryBuilder object after checking for +and adding `orderBy` knex calls. + +### currentFilter + +In `filters.js` +uses the current code and filter argument + + +### filterBuilder + +uses a more advanced filtering input and processing. + +See https://github.com/sysgears/apollo-universal-starter-kit/issues/569 diff --git a/src/server/modules/user/resolvers.js b/src/server/modules/user/resolvers.js index 9ad23e982c..0c6440fd29 100644 --- a/src/server/modules/user/resolvers.js +++ b/src/server/modules/user/resolvers.js @@ -8,8 +8,8 @@ import settings from '../../../../settings'; export default pubsub => ({ Query: { - users: withAuth(['user:view:all'], (obj, { orderBy, filter }, context) => { - return context.User.getUsers(orderBy, filter); + users: withAuth(['user:view:all'], (obj, { orderBy, filter, limit, offset }, context) => { + return context.User.getUsers(orderBy, filter, limit, offset); }), user: withAuth( (obj, args, context) => { diff --git a/src/server/modules/user/schema.graphql b/src/server/modules/user/schema.graphql index 6234de1a78..6f6d5815ed 100644 --- a/src/server/modules/user/schema.graphql +++ b/src/server/modules/user/schema.graphql @@ -55,7 +55,12 @@ type Tokens { extend type Query { # Get all users ordered by: OrderByUserInput add filtered by: FilterUserInput - users(orderBy: OrderByUserInput, filter: FilterUserInput): [User] + users( + orderBy: OrderByUserInput + filter: FilterUserInput + limit: Int + offset: Int + ): [User] # Get user by id user(id: Int!): User # Get current user diff --git a/src/server/modules/user/sql.js b/src/server/modules/user/sql.js index 1a71431fc7..6aeb439eba 100644 --- a/src/server/modules/user/sql.js +++ b/src/server/modules/user/sql.js @@ -1,13 +1,16 @@ // Helpers -import { camelizeKeys, decamelizeKeys, decamelize } from 'humps'; -import { has } from 'lodash'; +import { camelizeKeys, decamelizeKeys } from 'humps'; import bcrypt from 'bcryptjs'; import knex from '../../../server/sql/connector'; +import { ordering } from '../../../server/sql/ordering'; +import paging from '../../../server/sql/paging'; +import { currentFilter } from '../../../server/sql/filters'; + // Actual query fetching and transformation in DB export default class User { - async getUsers(orderBy, filter) { - const queryBuilder = knex + async getUsers(orderBy, filter, limit, offset) { + let queryBuilder = knex .select( 'u.id as id', 'u.username', @@ -28,40 +31,14 @@ export default class User { .leftJoin('auth_facebook AS fa', 'fa.user_id', 'u.id') .leftJoin('auth_google AS ga', 'ga.user_id', 'u.id'); - // add order by - if (orderBy && orderBy.column) { - let column = orderBy.column; - let order = 'asc'; - if (orderBy.order) { - order = orderBy.order; - } - - queryBuilder.orderBy(decamelize(column), order); - } + // Add filters + queryBuilder = currentFilter(queryBuilder, filter); - // add filter conditions - if (filter) { - if (has(filter, 'role') && filter.role !== '') { - queryBuilder.where(function() { - this.where('u.role', filter.role); - }); - } + // Add limit / offset + queryBuilder = paging(queryBuilder, { limit, offset }); - if (has(filter, 'isActive') && filter.isActive !== null) { - queryBuilder.where(function() { - this.where('u.is_active', filter.isActive); - }); - } - - if (has(filter, 'searchText') && filter.searchText !== '') { - queryBuilder.where(function() { - this.where('u.username', 'like', `%${filter.searchText}%`) - .orWhere('u.email', 'like', `%${filter.searchText}%`) - .orWhere('up.first_name', 'like', `%${filter.searchText}%`) - .orWhere('up.last_name', 'like', `%${filter.searchText}%`); - }); - } - } + // add result ordering + queryBuilder = ordering(queryBuilder, [orderBy]); return camelizeKeys(await queryBuilder); } diff --git a/src/server/sql/filters.js b/src/server/sql/filters.js new file mode 100644 index 0000000000..11232f4d75 --- /dev/null +++ b/src/server/sql/filters.js @@ -0,0 +1,135 @@ +import { decamelize } from 'humps'; +import { has } from 'lodash'; + +export function currentFilter(queryBuilder, filter) { + if (filter) { + if (has(filter, 'role') && filter.role !== '') { + queryBuilder.where(function() { + this.where('u.role', filter.role); + }); + } + + if (has(filter, 'isActive') && filter.isActive !== null) { + queryBuilder.where(function() { + this.where('u.is_active', filter.isActive); + }); + } + + if (has(filter, 'searchText') && filter.searchText !== '') { + queryBuilder.where(function() { + this.where('u.username', 'like', `%${filter.searchText}%`) + .orWhere('u.email', 'like', `%${filter.searchText}%`) + .orWhere('up.first_name', 'like', `%${filter.searchText}%`) + .orWhere('up.last_name', 'like', `%${filter.searchText}%`); + }); + } + } + + return queryBuilder; +} + +export function filterBuilder(queryBuilder, args) { + let { filters } = args; + + console.log('FILTERS', filters); + + // add filter conditions + if (filters) { + let first = true; + for (let filter of filters) { + // Pre Filters Recursion + if (filter.prefilters) { + if (first) { + first = false; + queryBuilder.where(function() { + filterBuilder(this, { filters: filter.prefilters }); + }); + } else { + if (filter.filterBool === 'and') { + queryBuilder.andWhere(function() { + filterBuilder(this, { filters: filter.prefilters }); + }); + } else if (filter.filterBool === 'or') { + queryBuilder.orWhere(function() { + filterBuilder(this, { filters: filter.prefilters }); + }); + } else { + // Default to OR + queryBuilder.orWhere(function() { + filterBuilder(this, { filters: filter.prefilters }); + }); + } + } + } + + // This Filter Visitation + if (filter.field) { + let column = filter.field; + if (filter.table) { + column = filter.table + '.' + column; + } + column = decamelize(column); + + let compare = '='; + if (filter.compare) { + compare = filter.compare; + } + + let value = filter.value ? filter.value : filter.values; + if (!value) { + value = filter.timeValue ? filter.timeValue : filter.timeValues; + if (!value) { + value = filter.intValue ? filter.intValue : filter.intValues; + } + if (!value) { + value = filter.floatValue ? filter.floatValue : filter.floatValues; + } + if (!value) { + value = filter.boolValue ? filter.boolValue : filter.boolValues; + } + } + + if (first) { + first = false; + queryBuilder.where(column, compare, value); + } else { + if (filter.bool === 'and') { + queryBuilder.andWhere(column, compare, value); + } else if (filter.bool === 'or') { + queryBuilder.orWhere(column, compare, value); + } else { + // Default to OR + queryBuilder.orWhere(column, compare, value); + } + } + } + + // Post Filters Recursion + if (filter.postfilters) { + if (first) { + first = false; + queryBuilder.where(function() { + filterBuilder(this, { filters: filter.postfilters }); + }); + } else { + if (filter.filterBool === 'and') { + queryBuilder.andWhere(function() { + filterBuilder(this, { filters: filter.postfilters }); + }); + } else if (filter.filterBool === 'or') { + queryBuilder.orWhere(function() { + filterBuilder(this, { filters: filter.postfilters }); + }); + } else { + // Default to OR + queryBuilder.orWhere(function() { + filterBuilder(this, { filters: filter.postfilters }); + }); + } + } + } + } + } + + return queryBuilder; +} diff --git a/src/server/sql/ordering.js b/src/server/sql/ordering.js new file mode 100644 index 0000000000..43fe50910b --- /dev/null +++ b/src/server/sql/ordering.js @@ -0,0 +1,40 @@ +import { decamelize } from 'humps'; + +export function currentOrdering(queryBuilder, orderBy) { + if (orderBy && orderBy.column) { + let column = orderBy.column; + let order = 'asc'; + if (orderBy.order) { + order = orderBy.order; + } + + queryBuilder.orderBy(decamelize(column), order); + } + + return queryBuilder; +} + +export function ordering(queryBuilder, args) { + let { orderBys } = args; + + // add order by + if (orderBys) { + for (let orderBy of orderBys) { + if (orderBy && orderBy.column) { + let column = orderBy.column; + if (orderBy.table) { + column = orderBy.table + '.' + column; + } + column = decamelize(column); + + let order = 'asc'; + if (orderBy.order) { + order = orderBy.order; + } + queryBuilder.orderBy(column, order); + } + } + } + + return queryBuilder; +} diff --git a/src/server/sql/paging.js b/src/server/sql/paging.js new file mode 100644 index 0000000000..bc01404dbc --- /dev/null +++ b/src/server/sql/paging.js @@ -0,0 +1,13 @@ +export default function paging(queryBuilder, args) { + const { offset, limit } = args; + + if (offset) { + queryBuilder.offset(offset); + } + + if (limit) { + queryBuilder.limit(limit); + } + + return queryBuilder; +}