UNPKG

modules-pack

Version:

JavaScript Modules for Modern Frontend & Backend Projects

244 lines (227 loc) 9.25 kB
import { GraphQLScalarType } from 'graphql' import { SevenBoom as Response } from 'graphql-apollo-errors' import gqlFields from 'graphql-fields' import { Kind } from 'graphql/language' import { SERVER } from 'modules-pack/variables' import { definitionByValue, enumFrom, isObject } from 'utils-pack' export { Response } export const queryFields = gqlFields /** * GRAPHQL RESOLVER HELPERS ==================================================== * ============================================================================= */ /** * Wrapper for Resolver Functions to return cached results * * @example: * // Inside GraphQL resolver: * const query = Event.betweenTimeRange.bind(Event, start, end, published) * return ((start || end) ? query() : cachedQuery(`eventsBetween`, query)) * * @param {String} id - query ID to store/retrieve cache * @param {Function} callback - async method that returns the result to be cached * @param {Number} [cacheTime] - duration in milliseconds * @returns {Promise<*>} */ export async function cachedQuery (id, callback, cacheTime = SERVER.QUERY_CACHE_TIME) { cachedQuery.clean(cacheTime) const cached = cachedQuery.by[id] if (cached && cached.time > Date.now() - cacheTime) return cached.result const result = await callback() cachedQuery.by[id] = {result, time: Date.now()} return result } cachedQuery.by = {} // {'queryId': {result, time}} cachedQuery.clean = function (cacheTime = SERVER.QUERY_CACHE_TIME) { const expireTime = Date.now() - cacheTime for (const id in this.by) { const {time} = this.by[id] if (time <= expireTime) delete this.by[id] // free up memory } } /** * Resolve the final return value (Document/Query/Error) from GraphQL Resolvers. * @note: this function is for reference only, do not delete it. * See content of the function for implementation example. * @example: if the resolver returns Promise, Promise.then() or `await` keyword resolves to the final result, * no matter how many promises are nested. * Case 1. * resolver() { * return Model.find() * } * >>> `result`: Query * * Case 2. * async resolver() { * return Model.findById() * } * >>> `result`: Promise<Document> * * Case 3. * async resolver() { * const instance = await Model.findById() * return Object.assign(instance, entry).save() * } * >>> `result`: Promise<Document> * * Usage inside a Decorator that wraps the resolver: * function resolvePromise () { * ----------------------------------------------------------------------------- // descriptor.value = async function (...args) { // // If result is a Promise, resolve it, else use as is without resolving // let instance = func.apply(this, args) // instance = (instance instanceof Promise) ? (await instance) : instance * } * ----------------------------------------------------------------------------- * } */ /** * GRAPHQL TYPE DEFINITIONS ---------------------------------------------------- * ----------------------------------------------------------------------------- */ /** * GraphQL Enum Type - Dynamically defined given Definition Object * @example: * const Language = graphQlEnumType(LANGUAGE, 'Language') * @param {String} name - of GraphQL type, cannot contain space * @param {Object<KEY<_...>>|Array<_...>} DEFINITION - key/value pairs of variable name with its _ value * @param {String} [description] - of GraphQL type * @returns {GraphQLScalarType} enum - type */ export function gqlEnumType (name, DEFINITION, {description} = {}) { const hasEnum = {} const enums = enumFrom(DEFINITION).sort().filter(code => (hasEnum[code] = true)) const Type = new GraphQLScalarType({ name, description: description || `Dynamically defined enumerable: [${enums}]`, serialize: (value) => value, // value sent to the client parseValue (value) {return this._fromClient(value)}, parseLiteral () {return this._fromClient(parseLiteral(...arguments))}, }) Type._fromClient = function (value) { if (hasEnum[value]) return value throw Response.badRequest(`Invalid ${this.name} enum ${value}, must be one of [${enums}]`) } return Type } /** * GraphQL Dynamic Key/Value Pairs Object Type * * @example: * const Phones = graphQlObjType(PHONE, String, 'Phones', {validate: isPhoneNumber}) * @param {String} name - of GraphQL type, cannot contain space * @param {Object<KEY<_...>>|Array<_...>} DEFINITION - key/value pairs of variable name with its _ value * @param {*} ValueType - type of the value i.e String, Number, Array, etc. * @param {String} [description] - of GraphQL type * @param {Function} [validate] - function that returns true/false if value is valid/invalid * @returns {GraphQLScalarType} object - with dynamic keys mapped to values */ export function gqlDynamicObjType (name, DEFINITION, ValueType = String, {validate, description} = {}) { const keyBy = definitionByValue(DEFINITION) const keyEnums = enumFrom(DEFINITION) const example = `\n- {\n- ` + keyEnums.map(key => `&nbsp;&nbsp;${key}: \`${ValueType.name}\``).join(`\n- `) + `\n- }` const Type = new GraphQLScalarType({ name, description: description || `${name} DEFINITION type:${example}`, serialize: (value) => value, // value sent to the client parseValue (value) {return this._fromClient(value)}, parseLiteral () {return this._fromClient(parseLiteral(...arguments))}, }) Type._fromClient = function (value) { if (isObject(value)) { for (const code in value) { const val = value[code] if (!keyBy[code]) throw Response.badRequest(`Invalid ${this.name} code ${code}, must be one of [${keyEnums}]`) if (val.constructor !== ValueType) return Response.badRequest(`Invalid ${this.name}.${code} ${val}, must be ${ValueType.name}`) if (validate && !validate(val)) return Response.badRequest(`Validation failed for ${this.name}.${code} ${val}`) } return value } throw Response.badRequest(`Invalid ${this.name} ${value}, must be key/value pairs`) } return Type } /** * GraphQL Key/Value Pairs Type - Tag (as key) and Level (as value) * @example: * const LanguageLevel = graphQlTagLevelType(LANGUAGE, LANGUAGE_LEVEL, 'LanguageLevel') * @param {String} name - of GraphQL type, cannot contain space * @param {Object<KEY<_...>>|Array<_...>} TAG - key/value pairs of variable name with its _ value * @param {Object<KEY<_...>>|Array<_...>} TAG_LEVEL - key/value pairs of variable name with its _ value * @param {String} [description] - of GraphQL type * @param {Boolean} [range] - whether each level must be a list of values (for sliders) * @returns {GraphQLScalarType} tag/level - type */ export function gqlTagLevelType (name, TAG, TAG_LEVEL, {description, range = false} = {}) { const tagBy = definitionByValue(TAG) const tagLevelBy = definitionByValue(TAG_LEVEL) const tagEnums = enumFrom(TAG) const tagLevelEnums = enumFrom(TAG_LEVEL) const Type = new GraphQLScalarType({ name, description: description || `Object with dynamic key/value pairs:\n- Key \`String\` [${tagEnums}]\n- Value \`Int\` between [${tagLevelEnums}]`, serialize: (value) => value, // value sent to the client parseValue (value) {return this._fromClient(value)}, parseLiteral () {return this._fromClient(parseLiteral(...arguments))}, }) Type._fromClient = function (value) { if (isObject(value)) { for (const code in value) { const level = value[code] if (!tagBy[code]) throw Response.badRequest(`Invalid ${this.name} code ${code}, must be one of [${tagEnums}]`) if (!range && !tagLevelBy[level]) throw Response.badRequest(`Invalid ${this.name} level ${level}, must be one of [${tagLevelEnums}]`) if (range) { if (!isList(level)) throw Response.badRequest(`Invalid ${this.name} value ${level}, must be a list of levels`) level.forEach(lvl => { if (!tagLevelBy[lvl]) throw Response.badRequest(`Invalid ${this.name} value ${level}, must be a list of [${tagLevelEnums}]`) }) } } return value } throw Response.badRequest(`Invalid ${this.name} ${value}, must be key/value pairs`) } return Type } /** * GraphQL Scalar Type function to parse JSON literal */ export function parseLiteral (ast, variables) { switch (ast.kind) { case Kind.STRING: case Kind.BOOLEAN: return ast.value case Kind.INT: case Kind.FLOAT: return parseFloat(ast.value) case Kind.OBJECT: return parseObject(ast, variables) case Kind.LIST: return ast.values.map(function (n) { return parseLiteral(n, variables) }) case Kind.NULL: return null case Kind.VARIABLE: { const name = ast.name.value return variables ? variables[name] : undefined } default: return undefined } } function parseObject (ast, variables) { const value = Object.create(null) ast.fields.forEach(function (field) { // eslint-disable-next-line no-use-before-define value[field.name.value] = parseLiteral(field.value, variables) }) return value }