flattens create / register, update / authUpdate

This commit is contained in:
James
2020-09-13 12:06:02 -04:00
parent fab4c0ed76
commit bb5f747052
21 changed files with 72 additions and 574 deletions

View File

@@ -97,7 +97,7 @@ describe('Users REST API', () => {
});
it('should allow a user to be created', async () => {
const response = await fetch(`${url}/api/admins/register`, {
const response = await fetch(`${url}/api/admins`, {
body: JSON.stringify({
email: `${faker.name.firstName()}@test.com`,
password,

View File

@@ -1,30 +0,0 @@
/* eslint-disable no-param-reassign */
function register(collection) {
async function resolver(_, args, context) {
const options = {
collection,
data: args.data,
depth: 0,
req: context.req,
};
if (args.locale) {
context.req.locale = args.locale;
options.locale = args.locale;
}
if (args.fallbackLocale) {
context.req.fallbackLocale = args.fallbackLocale;
options.fallbackLocale = args.fallbackLocale;
}
const token = await this.operations.collections.auth.register(options);
return token;
}
const registerResolver = resolver.bind(this);
return registerResolver;
}
module.exports = register;

View File

@@ -1,25 +0,0 @@
/* eslint-disable no-param-reassign */
function update(collection) {
async function resolver(_, args, context) {
if (args.locale) context.req.locale = args.locale;
if (args.fallbackLocale) context.req.fallbackLocale = args.fallbackLocale;
const options = {
collection,
data: args.data,
id: args.id,
depth: 0,
req: context.req,
};
const user = await this.operations.collections.auth.update(options);
return user;
}
const updateResolver = resolver.bind(this);
return updateResolver;
}
module.exports = update;

View File

@@ -1,10 +1,8 @@
const register = require('./register');
const login = require('./login');
const forgotPassword = require('./forgotPassword');
const resetPassword = require('./resetPassword');
module.exports = {
register,
login,
forgotPassword,
resetPassword,

View File

@@ -1,26 +0,0 @@
async function register(options) {
const {
collection: collectionSlug,
depth,
locale,
fallbackLocale,
data,
} = options;
const collection = this.collections[collectionSlug];
return this.operations.collections.auth.register({
depth,
data,
collection,
overrideAccess: true,
req: {
payloadAPI: 'local',
locale,
fallbackLocale,
payload: this,
},
});
}
module.exports = register;

View File

@@ -1,144 +0,0 @@
const passport = require('passport');
const executeAccess = require('../executeAccess');
async function register(args) {
const {
depth,
overrideAccess,
collection: {
Model,
config: collectionConfig,
},
req,
req: {
locale,
fallbackLocale,
},
} = args;
let { data } = args;
// /////////////////////////////////////
// 1. Retrieve and execute access
// /////////////////////////////////////
if (!overrideAccess) {
await executeAccess({ req }, collectionConfig.access.create);
}
// /////////////////////////////////////
// 2. Execute beforeValidate field-level hooks, access, and validation
// /////////////////////////////////////
data = await this.performFieldOperations(collectionConfig, {
data,
req,
hook: 'beforeValidate',
operation: 'create',
overrideAccess,
});
// /////////////////////////////////////
// 3. Execute beforeValidate collection hooks
// /////////////////////////////////////
await collectionConfig.hooks.beforeValidate.reduce(async (priorHook, hook) => {
await priorHook;
data = (await hook({
data,
req,
operation: 'create',
})) || data;
}, Promise.resolve());
// /////////////////////////////////////
// 4. Execute field-level access, beforeChange hooks, and validation
// /////////////////////////////////////
data = await this.performFieldOperations(collectionConfig, {
data,
hook: 'beforeChange',
operation: 'create',
req,
overrideAccess,
});
// /////////////////////////////////////
// 5. Execute beforeChange collection hooks
// /////////////////////////////////////
await collectionConfig.hooks.beforeChange.reduce(async (priorHook, hook) => {
await priorHook;
data = (await hook({
data,
req,
operation: 'create',
})) || data;
}, Promise.resolve());
// /////////////////////////////////////
// 6. Perform register
// /////////////////////////////////////
const modelData = {
...data,
email: data.email ? data.email.toLowerCase() : null,
};
delete modelData.password;
const user = new Model();
if (locale && user.setLocale) {
user.setLocale(locale, fallbackLocale);
}
Object.assign(user, modelData);
let result = await Model.register(user, data.password);
await passport.authenticate('local');
result = result.toJSON({ virtuals: true });
// /////////////////////////////////////
// 7. Execute field-level hooks and access
// /////////////////////////////////////
result = await this.performFieldOperations(collectionConfig, {
data: result,
hook: 'afterRead',
operation: 'read',
req,
depth,
overrideAccess,
});
// /////////////////////////////////////
// 8. Execute afterChange collcetion hooks
// /////////////////////////////////////
await collectionConfig.hooks.afterChange.reduce(async (priorHook, hook) => {
await priorHook;
result = await hook({
doc: result,
req: args.req,
operation: 'create',
}) || result;
}, Promise.resolve());
// /////////////////////////////////////
// 9. Return user
// /////////////////////////////////////
result = JSON.stringify(result);
result = JSON.parse(result);
return result;
}
module.exports = register;

View File

@@ -15,7 +15,7 @@ async function registerFirstUser(args) {
// 2. Perform register first user
// /////////////////////////////////////
let result = await this.operations.collections.auth.register({
let result = await this.operations.collections.create({
...args,
overrideAccess: true,
});

View File

@@ -1,195 +0,0 @@
const httpStatus = require('http-status');
const deepmerge = require('deepmerge');
const overwriteMerge = require('../../utilities/overwriteMerge');
const { NotFound, Forbidden, APIError } = require('../../errors');
const executeAccess = require('../executeAccess');
async function update(args) {
const {
depth,
collection: {
Model,
config: collectionConfig,
},
id,
req,
req: {
locale,
fallbackLocale,
},
overrideAccess,
} = args;
if (!id) {
throw new APIError('Missing ID of document to update.', httpStatus.BAD_REQUEST);
}
// /////////////////////////////////////
// 1. Execute access
// /////////////////////////////////////
const accessResults = !overrideAccess ? await executeAccess({ req, id }, collectionConfig.access.update) : true;
const hasWhereAccess = typeof accessResults === 'object';
// /////////////////////////////////////
// 2. Retrieve document
// /////////////////////////////////////
const queryToBuild = {
where: {
and: [
{
id: {
equals: id,
},
},
],
},
};
if (hasWhereAccess) {
queryToBuild.where.and.push(hasWhereAccess);
}
const query = await Model.buildQuery(queryToBuild, locale);
let user = await Model.findOne(query);
if (!user && !hasWhereAccess) throw new NotFound();
if (!user && hasWhereAccess) throw new Forbidden();
if (locale && user.setLocale) {
user.setLocale(locale, fallbackLocale);
}
let originalDoc = user.toJSON({ virtuals: true });
originalDoc = JSON.stringify(originalDoc);
originalDoc = JSON.parse(originalDoc);
let { data } = args;
// /////////////////////////////////////
// 3. Execute beforeValidate field-level hooks, access, and validation
// /////////////////////////////////////
data = await this.performFieldOperations(collectionConfig, {
data,
req,
id,
originalDoc,
hook: 'beforeValidate',
operation: 'update',
overrideAccess,
});
// /////////////////////////////////////
// 4. Execute beforeValidate collection hooks
// /////////////////////////////////////
await collectionConfig.hooks.beforeValidate.reduce(async (priorHook, hook) => {
await priorHook;
data = (await hook({
data,
req,
operation: 'update',
originalDoc,
})) || data;
}, Promise.resolve());
// /////////////////////////////////////
// 5. Execute field-level hooks, access, and validation
// /////////////////////////////////////
data = await this.performFieldOperations(collectionConfig, {
data,
req,
id,
hook: 'beforeChange',
operation: 'update',
originalDoc,
overrideAccess,
});
// /////////////////////////////////////
// 6. Execute beforeChange collection hooks
// /////////////////////////////////////
await collectionConfig.hooks.beforeChange.reduce(async (priorHook, hook) => {
await priorHook;
data = (await hook({
data,
req,
originalDoc,
operation: 'update',
})) || data;
}, Promise.resolve());
// /////////////////////////////////////
// 7. Merge updates into existing data
// /////////////////////////////////////
data = deepmerge(originalDoc, data, { arrayMerge: overwriteMerge });
// /////////////////////////////////////
// 8. Handle password update
// /////////////////////////////////////
const dataToUpdate = { ...data };
const { password } = dataToUpdate;
if (password) {
await user.setPassword(password);
delete dataToUpdate.password;
}
// /////////////////////////////////////
// 9. Perform database operation
// /////////////////////////////////////
Object.assign(user, dataToUpdate);
await user.save();
user = user.toJSON({ virtuals: true });
// /////////////////////////////////////
// 10. Execute field-level hooks and access
// /////////////////////////////////////
user = await this.performFieldOperations(collectionConfig, {
data: user,
hook: 'afterRead',
operation: 'read',
req,
id,
depth,
overrideAccess,
});
// /////////////////////////////////////
// 11. Execute afterChange collection hooks
// /////////////////////////////////////
await collectionConfig.hooks.afterChange.reduce(async (priorHook, hook) => {
await priorHook;
user = await hook({
doc: user,
req,
operation: 'update',
}) || user;
}, Promise.resolve());
// /////////////////////////////////////
// 12. Return updated user
// /////////////////////////////////////
user = JSON.stringify(user);
user = JSON.parse(user);
return user;
}
module.exports = update;

View File

@@ -1,21 +0,0 @@
const httpStatus = require('http-status');
const formatSuccessResponse = require('../../express/responses/formatSuccess');
async function register(req, res, next) {
try {
const user = await this.operations.collections.auth.register({
collection: req.collection,
req,
data: req.body,
});
return res.status(httpStatus.CREATED).json({
...formatSuccessResponse(`${req.collection.config.labels.singular} successfully created.`, 'message'),
doc: user,
});
} catch (error) {
return next(error);
}
}
module.exports = register;

View File

@@ -1,22 +0,0 @@
const httpStatus = require('http-status');
const formatSuccessResponse = require('../../express/responses/formatSuccess');
async function update(req, res, next) {
try {
const user = await this.operations.collections.auth.update({
req,
data: req.body,
collection: req.collection,
id: req.params.id,
});
return res.status(httpStatus.OK).json({
...formatSuccessResponse('Updated successfully.', 'message'),
doc: user,
});
} catch (error) {
return next(error);
}
}
module.exports = update;

View File

@@ -91,10 +91,6 @@ const EditView = (props) => {
let action = `${serverURL}${api}/${slug}${isEditing ? `/${id}` : ''}`;
const hasSavePermission = (isEditing && collectionPermissions?.update?.permission) || (!isEditing && collectionPermissions?.create?.permission);
if (auth && !isEditing) {
action = `${action}/register`;
}
action += '?depth=0';
return (

View File

@@ -16,7 +16,7 @@ function registerCollections() {
} = this.graphQL.resolvers.collections;
const {
login, logout, me, init, refresh, register, forgotPassword, resetPassword, update: authUpdate,
login, logout, me, init, refresh, forgotPassword, resetPassword,
} = this.graphQL.resolvers.collections.auth;
Object.keys(this.collections).forEach((slug) => {
@@ -37,7 +37,6 @@ function registerCollections() {
const singularLabel = formatName(singular);
let pluralLabel = formatName(plural);
// For collections named 'Media' or similar,
// there is a possibility that the singular name
// will equal the plural name. Append `all` to the beginning
@@ -111,11 +110,9 @@ function registerCollections() {
collection.graphQL.updateMutationInputType = new GraphQLNonNull(this.buildMutationInputType(
`${singularLabel}Update`,
fields.map((field) => ({
...field,
required: false,
})),
fields,
`${singularLabel}Update`,
true,
));
this.Query.fields[singularLabel] = {
@@ -145,6 +142,23 @@ function registerCollections() {
resolve: find(collection),
};
this.Mutation.fields[`create${singularLabel}`] = {
type: collection.graphQL.type,
args: {
data: { type: collection.graphQL.mutationInputType },
},
resolve: create(collection),
};
this.Mutation.fields[`update${singularLabel}`] = {
type: collection.graphQL.type,
args: {
id: { type: new GraphQLNonNull(GraphQLString) },
data: { type: collection.graphQL.updateMutationInputType },
},
resolve: update(collection),
};
this.Mutation.fields[`delete${singularLabel}`] = {
type: collection.graphQL.type,
args: {
@@ -228,14 +242,6 @@ function registerCollections() {
resolve: logout(collection),
};
this.Mutation.fields[`register${singularLabel}`] = {
type: collection.graphQL.type,
args: {
data: { type: collection.graphQL.mutationInputType },
},
resolve: register(collection),
};
this.Mutation.fields[`forgotPassword${singularLabel}`] = {
type: new GraphQLNonNull(GraphQLBoolean),
args: {
@@ -269,32 +275,6 @@ function registerCollections() {
}),
resolve: refresh(collection),
};
this.Mutation.fields[`update${singularLabel}`] = {
type: collection.graphQL.type,
args: {
id: { type: new GraphQLNonNull(GraphQLString) },
data: { type: collection.graphQL.updateMutationInputType },
},
resolve: authUpdate(collection),
};
} else {
this.Mutation.fields[`create${singularLabel}`] = {
type: collection.graphQL.type,
args: {
data: { type: collection.graphQL.mutationInputType },
},
resolve: create(collection),
};
this.Mutation.fields[`update${singularLabel}`] = {
type: collection.graphQL.type,
args: {
id: { type: new GraphQLNonNull(GraphQLString) },
data: { type: collection.graphQL.updateMutationInputType },
},
resolve: update(collection),
};
}
});
}

View File

@@ -59,11 +59,9 @@ function registerCollections() {
logout,
refresh,
me,
register,
registerFirstUser,
forgotPassword,
resetPassword,
update: authUpdate,
} = this.requestHandlers.collections.auth;
router
@@ -97,29 +95,17 @@ function registerCollections() {
router
.route(`/${slug}/reset-password`)
.post(resetPassword);
router
.route(`/${slug}/register`)
.post(register);
router.route(`/${slug}`)
.get(find);
router.route(`/${slug}/:id`)
.get(findByID)
.put(authUpdate)
.delete(deleteHandler);
} else {
router.route(`/${slug}`)
.get(find)
.post(create);
router.route(`/${slug}/:id`)
.put(update)
.get(findByID)
.delete(deleteHandler);
}
router.route(`/${slug}`)
.get(find)
.post(create);
router.route(`/${slug}/:id`)
.put(update)
.get(findByID)
.delete(deleteHandler);
this.router.use(router);
return formattedCollection;

View File

@@ -138,8 +138,17 @@ async function create(args) {
result.setLocale(locale, fallbackLocale);
}
if (collectionConfig.auth && data.email) {
data.email = data.email.toLowerCase();
}
Object.assign(result, data);
await result.save();
if (collectionConfig.auth) {
result = await Model.register(result, data.password);
} else {
await result.save();
}
result = result.toJSON({ virtuals: true });

View File

@@ -182,7 +182,18 @@ async function update(args) {
}
// /////////////////////////////////////
// 9. Perform database operation
// 9. Handle password update
// /////////////////////////////////////
const { password } = data;
if (password) {
await doc.setPassword(password);
delete data.password;
}
// /////////////////////////////////////
// 10. Perform database operation
// /////////////////////////////////////
Object.assign(doc, data);
@@ -192,7 +203,7 @@ async function update(args) {
doc = doc.toJSON({ virtuals: true });
// /////////////////////////////////////
// 10. Execute field-level hooks and access
// 11. Execute field-level hooks and access
// /////////////////////////////////////
doc = await this.performFieldOperations(collectionConfig, {
@@ -206,7 +217,7 @@ async function update(args) {
});
// /////////////////////////////////////
// 11. Execute afterChange collection hooks
// 12. Execute afterChange collection hooks
// /////////////////////////////////////
await collectionConfig.hooks.afterChange.reduce(async (priorHook, hook) => {
@@ -220,7 +231,7 @@ async function update(args) {
}, Promise.resolve());
// /////////////////////////////////////
// 12. Return updated document
// 13. Return updated document
// /////////////////////////////////////
doc = JSON.stringify(doc);

View File

@@ -14,19 +14,19 @@ const withNullableType = require('./withNullableType');
const formatName = require('../utilities/formatName');
const combineParentName = require('../utilities/combineParentName');
function buildMutationInputType(name, fields, parentName) {
function buildMutationInputType(name, fields, parentName, forceNullable = false) {
const fieldToSchemaMap = {
number: (field) => ({ type: withNullableType(field, GraphQLFloat) }),
text: (field) => ({ type: withNullableType(field, GraphQLString) }),
email: (field) => ({ type: withNullableType(field, GraphQLString) }),
textarea: (field) => ({ type: withNullableType(field, GraphQLString) }),
richText: (field) => ({ type: withNullableType(field, GraphQLJSON) }),
code: (field) => ({ type: withNullableType(field, GraphQLString) }),
date: (field) => ({ type: withNullableType(field, GraphQLString) }),
upload: (field) => ({ type: withNullableType(field, GraphQLString) }),
'rich-text': (field) => ({ type: withNullableType(field, GraphQLString) }),
html: (field) => ({ type: withNullableType(field, GraphQLString) }),
radio: (field) => ({ type: withNullableType(field, GraphQLString) }),
number: (field) => ({ type: withNullableType(field, GraphQLFloat, forceNullable) }),
text: (field) => ({ type: withNullableType(field, GraphQLString, forceNullable) }),
email: (field) => ({ type: withNullableType(field, GraphQLString, forceNullable) }),
textarea: (field) => ({ type: withNullableType(field, GraphQLString, forceNullable) }),
richText: (field) => ({ type: withNullableType(field, GraphQLJSON, forceNullable) }),
code: (field) => ({ type: withNullableType(field, GraphQLString, forceNullable) }),
date: (field) => ({ type: withNullableType(field, GraphQLString, forceNullable) }),
upload: (field) => ({ type: withNullableType(field, GraphQLString, forceNullable) }),
'rich-text': (field) => ({ type: withNullableType(field, GraphQLString, forceNullable) }),
html: (field) => ({ type: withNullableType(field, GraphQLString, forceNullable) }),
radio: (field) => ({ type: withNullableType(field, GraphQLString, forceNullable) }),
checkbox: () => ({ type: GraphQLBoolean }),
select: (field) => {
const formattedName = `${combineParentName(parentName, field.name)}_MutationInput`;
@@ -56,7 +56,7 @@ function buildMutationInputType(name, fields, parentName) {
});
type = field.hasMany ? new GraphQLList(type) : type;
type = withNullableType(field, type);
type = withNullableType(field, type, forceNullable);
return { type };
},
@@ -90,7 +90,7 @@ function buildMutationInputType(name, fields, parentName) {
array: (field) => {
const fullName = combineParentName(parentName, field.label);
let type = buildMutationInputType(fullName, field.fields, fullName);
type = new GraphQLList(withNullableType(field, type));
type = new GraphQLList(withNullableType(field, type, forceNullable));
return { type };
},
group: (field) => {

View File

@@ -1,10 +1,10 @@
const { GraphQLNonNull } = require('graphql');
const withNullableType = (field, type) => {
const withNullableType = (field, type, forceNullable) => {
const hasReadAccessControl = field.access && field.access.read;
const condition = field.admin && field.admin.condition;
if (field.required && !field.localized && !condition && !hasReadAccessControl) {
if (!forceNullable && field.required && !field.localized && !condition && !hasReadAccessControl) {
return new GraphQLNonNull(type);
}

View File

@@ -97,7 +97,6 @@ class Payload {
this.find = this.find.bind(this);
this.findByID = this.findByID.bind(this);
this.update = this.update.bind(this);
this.register = this.register.bind(this);
this.login = this.login.bind(this);
this.forgotPassword = this.forgotPassword.bind(this);
this.resetPassword = this.resetPassword.bind(this);
@@ -146,12 +145,6 @@ class Payload {
return deleteOperation(options);
}
async register(options) {
let { register } = localOperations.auth;
register = register.bind(this);
return register(options);
}
async login(options) {
let { login } = localOperations.auth;
login = login.bind(this);

View File

@@ -5,10 +5,8 @@ const login = require('../auth/operations/login');
const logout = require('../auth/operations/logout');
const me = require('../auth/operations/me');
const refresh = require('../auth/operations/refresh');
const register = require('../auth/operations/register');
const registerFirstUser = require('../auth/operations/registerFirstUser');
const resetPassword = require('../auth/operations/resetPassword');
const authUpdate = require('../auth/operations/update');
const create = require('../collections/operations/create');
const find = require('../collections/operations/find');
@@ -37,10 +35,8 @@ function bindOperations(ctx) {
logout: logout.bind(ctx),
me: me.bind(ctx),
refresh: refresh.bind(ctx),
register: register.bind(ctx),
registerFirstUser: registerFirstUser.bind(ctx),
resetPassword: resetPassword.bind(ctx),
update: authUpdate.bind(ctx),
},
},
globals: {

View File

@@ -5,10 +5,8 @@ const login = require('../auth/requestHandlers/login');
const logout = require('../auth/requestHandlers/logout');
const me = require('../auth/requestHandlers/me');
const refresh = require('../auth/requestHandlers/refresh');
const register = require('../auth/requestHandlers/register');
const registerFirstUser = require('../auth/requestHandlers/registerFirstUser');
const resetPassword = require('../auth/requestHandlers/resetPassword');
const authUpdate = require('../auth/requestHandlers/update');
const create = require('../collections/requestHandlers/create');
const find = require('../collections/requestHandlers/find');
@@ -37,10 +35,8 @@ function bindRequestHandlers(ctx) {
logout: logout.bind(ctx),
me: me.bind(ctx),
refresh: refresh.bind(ctx),
register: register.bind(ctx),
registerFirstUser: registerFirstUser.bind(ctx),
resetPassword: resetPassword.bind(ctx),
update: authUpdate.bind(ctx),
},
},
globals: {

View File

@@ -5,9 +5,7 @@ const login = require('../auth/graphql/resolvers/login');
const logout = require('../auth/graphql/resolvers/logout');
const me = require('../auth/graphql/resolvers/me');
const refresh = require('../auth/graphql/resolvers/refresh');
const register = require('../auth/graphql/resolvers/register');
const resetPassword = require('../auth/graphql/resolvers/resetPassword');
const authUpdate = require('../auth/graphql/resolvers/update');
const create = require('../collections/graphql/resolvers/create');
const find = require('../collections/graphql/resolvers/find');
@@ -37,9 +35,7 @@ function bindResolvers(ctx) {
logout: logout.bind(ctx),
me: me.bind(ctx),
refresh: refresh.bind(ctx),
register: register.bind(ctx),
resetPassword: resetPassword.bind(ctx),
update: authUpdate.bind(ctx),
},
},
globals: {