src/generator/getContext.js
// @flow
/* eslint-disable max-len */
/* eslint-disable no-unused-vars */
import { getRoles } from './authorize/getRoles';
import { isAuthorizeDirectiveDefined } from './authorize/isAuthorizeDirectiveDefined';
import { lcFirst, ucFirst } from './util/capitalization';
import { prep } from '../utilities';
import generatePerField from './util/generatePerField';
import type { templateContextType } from '../constants';
import {
CREATE,
USER_LITERAL,
ROLE_FIELD_DEFAULT,
SINGULAR,
PAGINATED,
MODEL,
RESOLVER,
SCHEMA
} from '../constants';
/**
* gets context for later template compilation
* reads schema and determines data context for code replacements
* @public
* @param {Object} inputSchema - schema of the type
* @param {string} User - name of the user model for User model context
* @param {string} codeType - to distinguish MODEL/RESOLVER runs
* @return {Object} templateContext - data context for template compilation
*
* @property {boolean} authorize - if authorization logic is there
* @property {boolean} isUserType- if it is the User type
* @property {string} typeName - name of the type with starting lower case
* @property {string} TypeName - name of the type with starting upper case
* @property {string} User - name of the user model
* @property {Object} userRoles - authorizations matrix for userRole
* @property {Object} docRoles - authorization matrix for docRole
* @property {string} firstUserRole - the role for protectFields
* @property {string} roleField - field name where the userRole is stored
* @property {array} singularFields - fields array
* @property {array} paginatedFields - fields array
* @property {object} schema - schema definition
*/
export function getContext(
inputSchema: any = {},
User: string = USER_LITERAL,
codeType: string = MODEL
): templateContextType {
// for field generation
// prepare template context for later compilation
const authorize = isAuthorizeDirectiveDefined(inputSchema);
// read TypeName out of inputSchema
const TypeName = inputSchema.definitions[0].name.value;
const typeName = lcFirst(TypeName);
const isUserType = TypeName === User;
// getting the role definitions out of the @authorize directive
const { userRoles, docRoles, roleFieldName } = getRoles(
authorize,
inputSchema
);
// get generated fields for resolvers and models
const { singularFields, paginatedFields } = getFields(inputSchema, codeType);
// prepare protectFields only on for the "User" type as an example
// roleField shouldn't be empty
// it checks, if the field is there
// it takes the first found userRole into the protectFields as a suggestion
// to the programmer, assuming this is the most important role,
// with higher authorization (see in README.md)
const firstUserRole = userRoles[CREATE][0] ? userRoles[CREATE][0] : ``;
const roleField = roleFieldName
? `${roleFieldName}`
: `${ROLE_FIELD_DEFAULT}`;
// create proper strings for the roles, for template injection
Object.keys(userRoles).forEach(mode => {
userRoles[mode] = prepareString(userRoles[mode]);
});
Object.keys(userRoles).forEach(mode => {
docRoles[mode] = prepareString(docRoles[mode]);
});
// this is the returned data context for the later template processing
return {
authorize,
isUserType,
typeName,
TypeName,
User,
userRoles,
docRoles,
firstUserRole,
roleField,
singularFields,
paginatedFields
};
}
/**
* prepares contexts for singular and paginated field associations
* @param {Object} inputSchema - schema of the type
* @param {string} codeType - to distinguish between MODEL/RESOLVER
* @return {Object} fields - fields for associations
* @property singularFields - fields for singular associations
* @property paginatedFields - fields for paginated associations
*/
function getFields(
inputSchema: any,
codeType: string
): {
singularFields: any,
paginatedFields: any
} {
const type = inputSchema.definitions[0];
// prepare singular and paginated field arrays for the field templates
const singularFields = [];
const paginatedFields = [];
// generators for the different field association types:
const generators = {
// singular association @belongsTo
belongsTo(replacements) {
const field = buildFieldContext(SINGULAR, replacements, codeType);
singularFields.push(field);
return field;
},
// paginated association @belongsToMany
belongsToMany(replacements) {
const { typeName, fieldName } = replacements;
const field = buildFieldContext(
PAGINATED,
{
...replacements,
query: `_id: { $in: ${typeName}.${fieldName}Ids || [] }`
},
codeType
);
paginatedFields.push(field);
return field;
},
// paginated association @hasMany
hasMany(replacements, { as }) {
const { typeName } = replacements;
const field = buildFieldContext(
PAGINATED,
{
...replacements,
query: `${as || typeName}Id: ${typeName}._id`
},
codeType
);
paginatedFields.push(field);
return field;
},
// paginated association @hasAndBelongsToMany
hasAndBelongsToMany(replacements, { as }) {
const { typeName } = replacements;
const field = buildFieldContext(
PAGINATED,
{
...replacements,
query: `${as || typeName}Ids: ${typeName}._id`
},
codeType
);
paginatedFields.push(field);
return field;
}
};
generatePerField(type, generators);
return { singularFields, paginatedFields };
}
/**
* builds a field's context
*
* @param {string} fieldType - SINGULAR or PAGINATED field type
* @param {object} context - context for the template partial
* @property {string} typeName - name of the type
* @property {string} fieldName - name of the field
* @property {string} argsStr - arguments of the field
* @property {string} ReturnTypeName - type to build association with
* @property {string} query - query for the data access to the referenced type
* @param {string} codeType - to distinguish between MODEL/RESOLVER
* @return {Object} field - field for an association
* @property {string} fieldType - SINGULAR or PAGINATED
* @property {string} fieldName - name of the field
* @property {string} typeName - name of the type with first lower character
* @property {string} TypeName - name of the type with first upper character
* @property {string} ReturnTypeName - name of the type to associate
* @property {string} argsString - argument string for parameters
* @property {string} argsFields - fields to pass on to the association
* @property {string} query - which fields to query during association
*/
function buildFieldContext(
fieldType: string,
{
typeName,
fieldName,
argsStr,
ReturnTypeName,
query
}: {
typeName: string,
fieldName: string,
argsStr: string,
ReturnTypeName: string,
query: string
},
codeType: string
): any {
// clone the string
let argFields = (' ' + argsStr).slice(1);
// populate some arguments with defaults
const argsWithDefaultsStr = argsStr
.replace('lastCreatedAt', 'lastCreatedAt = 0')
.replace('limit', 'limit = 10');
// prepares a fields string to pass on to the referenced type
if (fieldType === PAGINATED && argFields !== '') {
argFields = argFields.replace('{ ', '{ baseQuery, ');
}
// returns the build field context
return {
fieldType: fieldType || '',
fieldName: fieldName || '',
typeName: typeName || '',
TypeName: ucFirst(typeName) || '',
ReturnTypeName: ReturnTypeName || '',
argsString: argsWithDefaultsStr || '',
argsFields: argFields || '',
query: query || ''
};
}
/**
* prepare roles for code generator
* convert array to String value
* replace " by '
* @private
* @param {array} role - name of role
* @return {string} roleString - role string
*/
function prepareString(role: string): string {
return JSON.stringify(role).replace(/"/g, "'").replace(/,/g, ', ');
}