modules-pack
Version:
JavaScript Modules for Modern Frontend & Backend Projects
245 lines (228 loc) • 8.35 kB
JavaScript
import { __PROD__, Active, classInstanceMethodNames, get, isList, l, localiseTranslation, toJSON } from 'utils-pack'
import { _ } from 'utils-pack/translations'
import { Response } from './resolver'
/**
* GRAPHQL RESOLVER DECORATORS =================================================
* =============================================================================
*/
localiseTranslation({
PLEASE_ADD_decorator_BEFORE_USING_func: {
[l.ENGLISH]: 'Please add @{decorator} before using @{func}',
},
YOU_CANNOT_ACCESS_PROTECTED_RESOURCE: {
[l.ENGLISH]: 'You cannot access protected resource',
}
})
/**
* GraphQL Decorator to Export Resolvers Object using Class-style Syntax
* @example:
* const resolvers = {}
* *@exportTo.Query(resolvers)
* class Query { // note that class Query.name will not be 'Query' when bundled by webpack in backend
* user (parent, args, context, info) {
* // ...resolver logic
* }
* }
* export default resolvers
*
* @param {String} Name - resolver to extend, one of ['Query', 'Mutation', 'Subscription']
* @param {Object} resolvers - plain js object to be used as GQL resolvers for Query, Mutation, Subscription...
* @returns {(function(*=): void)|*} decorator - that converts Class-style methods to plain object resolvers for GQL
*/
export function exportTo (Name, resolvers) {
return function Decorator (Class) {
const instance = new Class()
const nestedResolvers = {}
classInstanceMethodNames(Class).forEach(resolver => nestedResolvers[resolver] = instance[resolver])
if (Name) {
resolvers[Name] = nestedResolvers
} else {
Object.assign(resolvers, nestedResolvers)
}
}
}
// @see: exportTo() docs
exportTo.Query = function (...args) {
return exportTo('Query', ...args)
}
// @see: exportTo() docs
exportTo.Mutation = function (...args) {
return exportTo('Mutation', ...args)
}
// @see: exportTo() docs
exportTo.Subscription = function (...args) {
return exportTo('Subscription', ...args)
}
/**
* Decorator to Enforce Existence of Logged in User ID for GraphQL Resolvers
* @example:
* class Query {
* *@authenticated
* user (parent, args, context, info) {
* // ...resolver logic
* }
* }
*/
export function authenticated (target, key, descriptor) {
const func = descriptor.value
descriptor.value = function (...args) {
const [_1, _2, {user: {id} = {}}] = args
if (!id) return Response.unauthorized(id)
return func.apply(this, args)
}
return descriptor
}
/**
* Decorator to Enforce Minimum Authorization Level of Logged in User for GraphQL Resolvers
* @example: see `authenticated` decorator
* @param {Number} userRole - one of ENUM.USER_ROLE, the minimum authorization level required
*/
export function authLevel (userRole) {
return function (target, key, descriptor) {
const func = descriptor.value
descriptor.value = function (...args) {
const [_1, _2, {user: {auth} = {}}] = args
if (!(auth >= userRole)) return Response.forbidden(_.YOU_CANNOT_ACCESS_PROTECTED_RESOURCE)
return func.apply(this, args)
}
return descriptor
}
}
/**
* Decorator to Delay GraphQL Resolvers for Testing Purpose
* @example: see `authenticated` decorator
* @param {Number} milliseconds - to delay
*/
export function delayed (milliseconds) {
return function (target, key, descriptor) {
if (__PROD__) return descriptor
const func = descriptor.value
descriptor.value = function (...args) {
return new Promise((resolve, reject) => {
setTimeout(() => {
try {
resolve(func.apply(this, args))
} catch (error) {
reject(error)
}
}, milliseconds)
})
}
return descriptor
}
}
/**
* Decorator to Standardize Query `filter` and `match` variables for GraphQL Resolvers
* (mutates payload `filter` if `match` Operator provided)
* @example: see `authenticated` decorator
* @param {Function} [validator] - validation function, receives (resolverArgs) as arguments,
* should return error Response in case of invalid, else void if validation passed
* @param {Function<Function>} [filtersToApply] - filter mutations to apply, for example: defaultFilter(resolverArgs)
*/
export function filtered (validator, ...filtersToApply) {
return function (target, key, descriptor) {
const func = descriptor.value
descriptor.value = async function (...args) {
for (const filterTo of filtersToApply) {
filterTo.apply(this, args)
}
if (validator) {
const error = await validator.apply(this, args)
if (error) return error
}
const [_1, payload] = args
const {filter, match} = payload
if (filter && match) {
const conditions = []
for (const field in filter) {
conditions.push({[field]: filter[field]})
}
// Mutate filter so resolver can insert it directly to Model.find(filter)
payload.filter = {[`$${match}`]: conditions}
}
return func.apply(this, args)
}
return descriptor
}
}
/**
* Decorator to Process Localised Strings (by mutation) for GraphQL Resolvers
* - Injects `user.lang` or `lang` from Context to the `entry.lang` Payload argument
* - Injects language code to returned doc/s for resolving entry virtual getters/setters
* @default: fallbacks to active language code used by the application
* @example: see `authenticated` decorator
*/
export function localised (target, key, descriptor) {
const func = descriptor.value
descriptor.value = async function (...args) {
const [_1, payload, {user = {}, lang}] = args
const langCode = user.lang || lang || Active.LANG._
// .lang prop must be the first in entry object for virtuals to work, because of Object.assign order
if (payload.entry && payload.entry.lang == null) payload.entry = {lang: langCode, ...payload.entry}
const result = await func.apply(this, args)
if (isList(result)) {
result.forEach(entry => {entry.lang = langCode})
} else if (result) {
result.lang = langCode
}
return result
}
return descriptor
}
/**
* Validate that Localised field with `required: true` must have at least one translated value
* @example:
* @verified(requiredLocalised(tagSchema))
*
* @param {Object} schema - Mongoose schema definition plain object
* @returns {Function} validator - that takes GQL resolverArgs aa arguments
*/
export function requiredLocalised (schema) {
if (!schema._.required) throw new Error(`${requiredLocalised.name} not needed for schema \n${toJSON(schema, null, 2)}`)
return function validation (_1, {entry}) {
if (!entry._) {
// Skip check for existing entry if LocalString is not updated directly
if (entry.id) return
// Enforce LocalString for new entries (i.e. without Id)
const fields = schema._.type
let requiredFields = []
for (const name in fields) {
if (fields[name].required) requiredFields.push(name)
}
return Response.badRequest(`[${requiredFields.join(', ')}] ${_.REQUIRED}`)
}
// If Localised _ object given, assume it must contain all required LocalString values
const localised = entry._
for (const field in localised) {
if (!get(schema, `_.type[${field}].required`)) continue
let isValid = false
const localString = localised[field]
for (const lang in localString) {
if (localString[lang]) {
isValid = true
break
}
}
if (!isValid) return Response.badRequest(`[${field}] ${_.REQUIRED}`)
}
}
}
/**
* Decorator to Run Validations for GraphQL Resolvers
* @example: see `authenticated` decorator
* @param {Function<Function>} [validators] - functions to run, each receives (resolverArgs) as arguments,
* should return error Response in case of invalid, else void if validation passed
*/
export function verified (...validators) {
return function (target, key, descriptor) {
const func = descriptor.value
descriptor.value = async function (...args) {
for (const validator of validators) {
const error = await validator.apply(this, args)
if (error) return error
}
return func.apply(this, args)
}
return descriptor
}
}