Change payload middleware to service

This commit is contained in:
rijkvanzanten
2020-06-25 17:59:11 -04:00
parent b1790a666f
commit db6d43988d
9 changed files with 74 additions and 123 deletions

5
package-lock.json generated
View File

@@ -1107,11 +1107,6 @@
"assert-plus": "^1.0.0"
}
},
"dataloader": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.0.0.tgz",
"integrity": "sha512-YzhyDAwA4TaQIhM5go+vCLmU0UikghC/t9DTQYZR2M/UvZ1MdOhPezSDZcjj9uqQJOMqjLcpWtyW2iNINdlatQ=="
},
"date-utils": {
"version": "1.2.21",
"resolved": "https://registry.npmjs.org/date-utils/-/date-utils-1.2.21.tgz",

View File

@@ -66,7 +66,6 @@
"atob": "^2.1.2",
"bcrypt": "^5.0.0",
"body-parser": "^1.19.0",
"dataloader": "^2.0.0",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"express-async-handler": "^1.1.4",

View File

@@ -7,7 +7,6 @@ import logger from 'express-pino-logger';
import { errorHandler } from './error';
import initLoaders from './middleware/init-loaders';
import extractToken from './middleware/extract-token';
import authenticate from './middleware/authenticate';
@@ -33,7 +32,6 @@ const app = express()
.disable('x-powered-by')
.use(logger())
.use(bodyParser.json())
.use(initLoaders)
.use(extractToken)
.use(authenticate)
.use('/activity', activityRouter)

View File

@@ -1,33 +0,0 @@
/**
* Loaders are individual DataLoader instances that can be used to query often used information more
* efficiently. This is relied on for fetching field information for example, seeing that's an
* operation we'll do often in various middleware checks
*/
import DataLoader from 'dataloader';
import { FieldInfo } from './types/field';
import database from './database';
async function getCollections(keys: string[]) {
const records = await database
.select('*')
.from('directus_collections')
.whereIn('collection', keys);
return keys.map((key) => records.find((collection) => collection.collection === key));
}
export default function createSystemLoaders() {
return {
collections: new DataLoader(getCollections),
fieldsByCollection: new DataLoader<string, FieldInfo[]>((collections: string[]) =>
database
.select('*')
.from('directus_fields')
.whereIn('collection', collections)
.then((rows) =>
collections.map((collection) => rows.filter((x) => x.collection === collection))
)
),
};
}

View File

@@ -1,14 +0,0 @@
/**
* Sets up the data loaders for this individual request. This allows us to reuse the same system
* loaders in the other middleware and route handlers
*/
import { RequestHandler } from 'express';
import createSystemLoaders from '../loaders';
const initLoaders: RequestHandler = (req, res, next) => {
req.loaders = createSystemLoaders();
next();
};
export default initLoaders;

View File

@@ -1,63 +0,0 @@
/**
* Will check the fields system table for any special operations that need to be done on the field
* value, this can include hashing the value or generating a UUID
*/
import { RequestHandler } from 'express';
import asyncHandler from 'express-async-handler';
import { FieldInfo } from '../types/field';
import bcrypt from 'bcrypt';
import { v4 as uuidv4 } from 'uuid';
type Operation = 'create' | 'update';
/**
* @TODO
*
* Move this out of the middleware into a service
*/
const processPayload = (operation: Operation) => {
const middleware: RequestHandler = asyncHandler(async (req, res, next) => {
const fieldsInCollection = await req.loaders.fieldsByCollection.load(req.collection);
const specialFields = fieldsInCollection.filter((field) => {
if (field instanceof Error) return false;
return field.special !== null;
});
for (const field of specialFields) {
req.body[field.field] = await processField(req.collection, field, req.body, operation);
}
next();
});
return middleware;
};
async function processField(
collection: string,
field: FieldInfo,
payload: Record<string, any>,
operation: Operation
) {
switch (field.special) {
case 'hash':
return await genHash(payload[field.field]);
case 'uuid':
return await genUUID(operation);
}
}
async function genHash(value: string | number) {
return await bcrypt.hash(value, Number(process.env.SALT_ROUNDS));
}
async function genUUID(operation: Operation) {
if (operation === 'create') {
return uuidv4();
}
}
export default processPayload;

View File

@@ -6,7 +6,7 @@ import validateCollection from '../middleware/validate-collection';
import validateSingleton from '../middleware/validate-singleton';
import validateQuery from '../middleware/validate-query';
import * as MetaService from '../services/meta';
import processPayload from '../middleware/process-payload';
import * as PayloadService from '../services/payload';
const router = express.Router();
@@ -14,9 +14,9 @@ router.post(
'/:collection',
validateCollection,
validateSingleton,
processPayload('create'),
asyncHandler(async (req, res) => {
await createItem(req.params.collection, req.body);
const payload = await PayloadService.processValues('create', req.collection, req.body);
await createItem(req.params.collection, payload);
res.status(200).end();
})
);
@@ -55,7 +55,8 @@ router.patch(
'/:collection/:pk',
validateCollection,
asyncHandler(async (req, res) => {
await updateItem(req.params.collection, req.params.pk, req.body);
const payload = await PayloadService.processValues('update', req.collection, req.body);
await updateItem(req.params.collection, req.params.pk, payload);
return res.status(200).end();
})
);

62
src/services/payload.ts Normal file
View File

@@ -0,0 +1,62 @@
/**
* # PayloadService
*
* Process a given payload for a collection to ensure the special fields (hash, uuid, date etc) are
* handled correctly.
*/
import { FieldInfo } from '../types/field';
import bcrypt from 'bcrypt';
import { v4 as uuidv4 } from 'uuid';
import database from '../database';
import { clone } from 'lodash';
/**
* Process and update all the special fields in the given payload
*
* @param collection Collection the payload goes in
* @param operation If this is on create or on update
* @param payload The actual payload itself
* @returns The updated payload
*/
export const processValues = async (
operation: 'create' | 'update',
collection: string,
payload: Record<string, any>
) => {
const processedPayload = clone(payload);
const specialFieldsInCollection = await database
.select('field', 'special')
.from('directus_fields')
.where({ collection: collection })
.whereNotNull('special');
for (const field of specialFieldsInCollection) {
processedPayload[field.field] = await processField(field, processedPayload, operation);
}
return processedPayload;
};
async function processField(
field: FieldInfo,
payload: Record<string, any>,
operation: 'create' | 'update'
) {
switch (field.special) {
case 'hash':
return await genHash(payload[field.field]);
case 'uuid':
return await genUUID(operation);
}
}
async function genHash(value: string | number) {
return await bcrypt.hash(value, Number(process.env.SALT_ROUNDS));
}
async function genUUID(operation: 'create' | 'update') {
if (operation === 'create') {
return uuidv4();
}
}

View File

@@ -5,6 +5,7 @@ import { sendInviteMail } from '../mail';
import database from '../database';
import APIError, { ErrorCode } from '../error';
import bcrypt from 'bcrypt';
import * as PayloadService from '../services/payload';
export const createUser = async (data: Record<string, any>, query?: Query) => {
return await ItemsService.createItem('directus_users', data, query);
@@ -27,7 +28,12 @@ export const deleteUser = async (pk: string | number) => {
};
export const inviteUser = async (email: string, role: string) => {
await createUser({ email, role, status: 'invited' });
const userPayload = await PayloadService.processValues('create', 'directus_users', {
email,
role,
status: 'invited',
});
await createUser(userPayload);
const payload = { email };
const token = jwt.sign(payload, process.env.SECRET, { expiresIn: '7d' });