Merge pull request #58 from directus/revisions

Revisions
This commit is contained in:
Rijk van Zanten
2020-07-09 13:41:01 -04:00
committed by GitHub
30 changed files with 437 additions and 375 deletions

View File

@@ -31,12 +31,12 @@
"@types/atob": "^2.1.2",
"@types/busboy": "^0.2.3",
"@types/cookie-parser": "^1.4.2",
"@types/express": "^4.17.6",
"@types/express": "^4.17.7",
"@types/express-pino-logger": "^4.0.2",
"@types/express-session": "^1.17.0",
"@types/hapi__joi": "^17.1.2",
"@types/hapi__joi": "^17.1.3",
"@types/jsonwebtoken": "^8.5.0",
"@types/lodash": "^4.14.156",
"@types/lodash": "^4.14.157",
"@types/ms": "^0.7.31",
"@types/nodemailer": "^6.4.0",
"@types/pino": "^6.3.0",
@@ -44,16 +44,16 @@
"@types/uuid": "^8.0.0",
"@types/uuid-validate": "0.0.1",
"copyfiles": "^2.3.0",
"eslint": "^7.3.1",
"eslint": "^7.4.0",
"eslint-plugin-prettier": "^3.1.4",
"husky": "^4.2.5",
"lint-staged": "^10.2.10",
"pino-colada": "^1.6.1",
"lint-staged": "^10.2.11",
"pino-colada": "^2.0.1",
"prettier": "^2.0.5",
"rimraf": "^3.0.2",
"ts-node": "^8.10.2",
"tslint": "^6.1.2",
"typescript": "^3.9.5"
"typescript": "^3.9.6"
},
"husky": {
"hooks": {
@@ -67,9 +67,9 @@
},
"dependencies": {
"@hapi/joi": "^17.1.1",
"@slynova/flydrive": "^1.0.1",
"@slynova/flydrive-gcs": "^1.0.1",
"@slynova/flydrive-s3": "^1.0.1",
"@slynova/flydrive": "^1.0.2",
"@slynova/flydrive-gcs": "^1.0.2",
"@slynova/flydrive-s3": "^1.0.2",
"argon2": "^0.26.2",
"atob": "^2.1.2",
"body-parser": "^1.19.0",
@@ -87,19 +87,19 @@
"icc": "^2.0.0",
"jsonwebtoken": "^8.5.1",
"knex": "^0.21.1",
"liquidjs": "^9.12.0",
"lodash": "^4.17.15",
"liquidjs": "^9.14.1",
"lodash": "^4.17.19",
"ms": "^2.1.2",
"mssql": "^6.2.0",
"mysql": "^2.18.1",
"nanoid": "^3.1.10",
"nodemailer": "^6.4.10",
"oracledb": "^4.2.0",
"pg": "^8.2.1",
"oracledb": "^5.0.0",
"pg": "^8.3.0",
"pino": "^6.3.2",
"sharp": "^0.25.4",
"sqlite3": "^4.2.0",
"ts-node-dev": "^1.0.0-pre.49",
"sqlite3": "^5.0.0",
"ts-node-dev": "^1.0.0-pre.51",
"uuid": "^8.2.0",
"uuid-validate": "0.0.3"
}

View File

@@ -29,7 +29,12 @@ router.post(
const { error } = collectionSchema.validate(req.body);
if (error) throw new InvalidPayloadException(error.message);
const createdCollection = await CollectionsService.create(req.body);
const createdCollection = await CollectionsService.create(req.body, {
ip: req.ip,
userAgent: req.get('user-agent'),
user: req.user,
});
res.json({ data: createdCollection });
})
);
@@ -65,7 +70,12 @@ router.delete(
throw new CollectionNotFoundException(req.params.collection);
}
await CollectionsService.deleteCollection(req.params.collection);
await CollectionsService.deleteCollection(req.params.collection, {
ip: req.ip,
userAgent: req.get('user-agent'),
user: req.user,
});
res.end();
})
);

View File

@@ -61,7 +61,11 @@ router.post(
const field: Partial<Field> = req.body;
const createdField = await FieldsService.createField(req.collection, field);
const createdField = await FieldsService.createField(req.collection, field, {
ip: req.ip,
userAgent: req.get('user-agent'),
user: req.user,
});
res.json({ data: createdField });
})

View File

@@ -55,29 +55,26 @@ const multipartHandler = (operation: 'create' | 'update') =>
try {
if (operation === 'create') {
const file = await FilesService.createFile(payload, fileStream);
ActivityService.createActivity({
action: ActivityService.Action.UPLOAD,
collection: 'directus_files',
item: file.id,
const pk = await FilesService.createFile(payload, fileStream, {
ip: req.ip,
user_agent: req.get('user-agent'),
action_by: req.user,
userAgent: req.get('user-agent'),
user: req.user,
});
const file = await FilesService.readFile(pk, req.sanitizedQuery);
savedFiles.push(file);
} else {
const file = await FilesService.updateFile(req.params.pk, payload, fileStream);
ActivityService.createActivity({
action: ActivityService.Action.UPDATE,
collection: 'directus_files',
item: file.id,
ip: req.ip,
user_agent: req.get('user-agent'),
action_by: req.user,
});
const pk = await FilesService.updateFile(
req.params.pk,
payload,
{
ip: req.ip,
userAgent: req.get('user-agent'),
user: req.user,
},
fileStream
);
const file = await FilesService.readFile(pk, req.sanitizedQuery);
savedFiles.push(file);
}
@@ -130,16 +127,12 @@ router.patch(
if (req.is('multipart/form-data')) {
file = await multipartHandler('update')(req, res, next);
} else {
file = await FilesService.updateFile(req.params.pk, req.body);
ActivityService.createActivity({
action: ActivityService.Action.UPDATE,
collection: 'directus_files',
item: file.id,
const pk = await FilesService.updateFile(req.params.pk, req.body, {
ip: req.ip,
user_agent: req.get('user-agent'),
action_by: req.user,
userAgent: req.get('user-agent'),
user: req.user,
});
file = await FilesService.readFile(pk, req.sanitizedQuery);
}
return res.status(200).json({ data: file });
@@ -150,7 +143,11 @@ router.delete(
'/:pk',
useCollection('directus_files'),
asyncHandler(async (req, res) => {
await FilesService.deleteFile(req.params.pk);
await FilesService.deleteFile(req.params.pk, {
ip: req.ip,
userAgent: req.get('user-agent'),
user: req.user,
});
return res.status(200).end();
})
);

View File

@@ -12,17 +12,13 @@ router.post(
'/',
useCollection('directus_folders'),
asyncHandler(async (req, res) => {
const record = await FoldersService.createFolder(req.body, req.sanitizedQuery);
ActivityService.createActivity({
action: ActivityService.Action.CREATE,
collection: req.collection,
item: record.id,
const primaryKey = await FoldersService.createFolder(req.body, {
ip: req.ip,
user_agent: req.get('user-agent'),
action_by: req.user,
userAgent: req.get('user-agent'),
user: req.user,
});
const record = await FoldersService.readFolder(primaryKey, req.sanitizedQuery);
return res.json({ data: record });
})
);
@@ -53,21 +49,14 @@ router.patch(
'/:pk',
useCollection('directus_folders'),
asyncHandler(async (req, res) => {
const record = await FoldersService.updateFolder(
req.params.pk,
req.body,
req.sanitizedQuery
);
ActivityService.createActivity({
action: ActivityService.Action.UPDATE,
collection: req.collection,
item: record.id,
const primaryKey = await FoldersService.updateFolder(req.params.pk, req.body, {
ip: req.ip,
user_agent: req.get('user-agent'),
action_by: req.user,
userAgent: req.get('user-agent'),
user: req.user,
});
const record = await FoldersService.readFolder(primaryKey, req.sanitizedQuery);
return res.json({ data: record });
})
);
@@ -76,7 +65,11 @@ router.delete(
'/:pk',
useCollection('directus_folders'),
asyncHandler(async (req, res) => {
await FoldersService.deleteFolder(req.params.pk);
await FoldersService.deleteFolder(req.params.pk, {
ip: req.ip,
userAgent: req.get('user-agent'),
user: req.user,
});
ActivityService.createActivity({
action: ActivityService.Action.DELETE,

View File

@@ -6,7 +6,6 @@ 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 * as ActivityService from '../services/activity';
const router = express.Router();
@@ -17,19 +16,14 @@ router.post(
sanitizeQuery,
validateQuery,
asyncHandler(async (req, res) => {
const primaryKey = await ItemsService.createItem(req.collection, req.body);
const item = await ItemsService.readItem(req.collection, primaryKey, req.sanitizedQuery);
ActivityService.createActivity({
action: ActivityService.Action.CREATE,
collection: req.collection,
/** @TODO don't forget to use real primary key here */
item: item.id,
const primaryKey = await ItemsService.createItem(req.collection, req.body, {
ip: req.ip,
user_agent: req.get('user-agent'),
action_by: req.user,
userAgent: req.get('user-agent'),
user: req.user,
});
const item = await ItemsService.readItem(req.collection, primaryKey, req.sanitizedQuery);
res.json({ data: item });
})
);
@@ -76,18 +70,14 @@ router.patch(
sanitizeQuery,
validateQuery,
asyncHandler(async (req, res) => {
const primaryKey = await ItemsService.updateItem(req.collection, req.params.pk, req.body);
const item = await ItemsService.readItem(req.collection, primaryKey, req.sanitizedQuery);
ActivityService.createActivity({
action: ActivityService.Action.UPDATE,
collection: req.collection,
item: item.id,
const primaryKey = await ItemsService.updateItem(req.collection, req.params.pk, req.body, {
ip: req.ip,
user_agent: req.get('user-agent'),
action_by: req.user,
userAgent: req.get('user-agent'),
user: req.user,
});
const item = await ItemsService.readItem(req.collection, primaryKey, req.sanitizedQuery);
return res.json({ data: item });
})
);
@@ -96,15 +86,10 @@ router.delete(
'/:collection/:pk',
validateCollection,
asyncHandler(async (req, res) => {
await ItemsService.deleteItem(req.collection, req.params.pk);
ActivityService.createActivity({
action: ActivityService.Action.DELETE,
collection: req.collection,
item: req.params.pk,
await ItemsService.deleteItem(req.collection, req.params.pk, {
ip: req.ip,
user_agent: req.get('user-agent'),
action_by: req.user,
userAgent: req.get('user-agent'),
user: req.user,
});
return res.status(200).end();

View File

@@ -3,7 +3,6 @@ import asyncHandler from 'express-async-handler';
import sanitizeQuery from '../middleware/sanitize-query';
import validateQuery from '../middleware/validate-query';
import * as PermissionsService from '../services/permissions';
import * as ActivityService from '../services/activity';
import useCollection from '../middleware/use-collection';
const router = express.Router();
@@ -12,17 +11,14 @@ router.post(
'/',
useCollection('directus_permissions'),
asyncHandler(async (req, res) => {
const item = await PermissionsService.createPermission(req.body, req.sanitizedQuery);
ActivityService.createActivity({
action: ActivityService.Action.CREATE,
collection: req.collection,
item: item.id,
const primaryKey = await PermissionsService.createPermission(req.body, {
ip: req.ip,
user_agent: req.get('user-agent'),
action_by: req.user,
userAgent: req.get('user-agent'),
user: req.user,
});
const item = await PermissionsService.readPermission(primaryKey, req.sanitizedQuery);
return res.json({ data: item });
})
);
@@ -44,7 +40,10 @@ router.get(
sanitizeQuery,
validateQuery,
asyncHandler(async (req, res) => {
const record = await PermissionsService.readPermission(req.params.pk, req.sanitizedQuery);
const record = await PermissionsService.readPermission(
Number(req.params.pk),
req.sanitizedQuery
);
return res.json({ data: record });
})
);
@@ -53,20 +52,17 @@ router.patch(
'/:pk',
useCollection('directus_permissions'),
asyncHandler(async (req, res) => {
const item = await PermissionsService.updatePermission(
req.params.pk,
const primaryKey = await PermissionsService.updatePermission(
Number(req.params.pk),
req.body,
req.sanitizedQuery
{
ip: req.ip,
userAgent: req.get('user-agent'),
user: req.user,
}
);
ActivityService.createActivity({
action: ActivityService.Action.UPDATE,
collection: req.collection,
item: item.id,
ip: req.ip,
user_agent: req.get('user-agent'),
action_by: req.user,
});
const item = await PermissionsService.readPermission(primaryKey, req.sanitizedQuery);
return res.json({ data: item });
})
@@ -76,15 +72,10 @@ router.delete(
'/:pk',
useCollection('directus_permissions'),
asyncHandler(async (req, res) => {
await PermissionsService.deletePermission(req.params.pk);
ActivityService.createActivity({
action: ActivityService.Action.DELETE,
collection: req.collection,
item: req.params.pk,
await PermissionsService.deletePermission(Number(req.params.pk), {
ip: req.ip,
user_agent: req.get('user-agent'),
action_by: req.user,
userAgent: req.get('user-agent'),
user: req.user,
});
return res.status(200).end();

View File

@@ -4,25 +4,21 @@ import sanitizeQuery from '../middleware/sanitize-query';
import validateQuery from '../middleware/validate-query';
import * as RelationsService from '../services/relations';
import useCollection from '../middleware/use-collection';
import * as ActivityService from '../services/activity';
const router = express.Router();
router.post(
'/',
useCollection('directus_relations'),
sanitizeQuery,
validateQuery,
asyncHandler(async (req, res) => {
const item = await RelationsService.createRelation(req.body, req.sanitizedQuery);
ActivityService.createActivity({
action: ActivityService.Action.CREATE,
collection: req.collection,
item: item.id,
const primaryKey = await RelationsService.createRelation(req.body, {
ip: req.ip,
user_agent: req.get('user-agent'),
action_by: req.user,
userAgent: req.get('user-agent'),
user: req.user,
});
const item = await RelationsService.readRelation(primaryKey, req.sanitizedQuery);
return res.json({ data: item });
})
);
@@ -52,22 +48,15 @@ router.get(
router.patch(
'/:pk',
useCollection('directus_relations'),
sanitizeQuery,
validateQuery,
asyncHandler(async (req, res) => {
const item = await RelationsService.updateRelation(
req.params.pk,
req.body,
req.sanitizedQuery
);
ActivityService.createActivity({
action: ActivityService.Action.UPDATE,
collection: req.collection,
item: item.id,
const primaryKey = await RelationsService.updateRelation(req.params.pk, req.body, {
ip: req.ip,
user_agent: req.get('user-agent'),
action_by: req.user,
userAgent: req.get('user-agent'),
user: req.user,
});
const item = await RelationsService.readRelation(primaryKey, req.sanitizedQuery);
return res.json({ data: item });
})
);
@@ -76,15 +65,10 @@ router.delete(
'/:pk',
useCollection('directus_relations'),
asyncHandler(async (req, res) => {
await RelationsService.deleteRelation(req.params.pk);
ActivityService.createActivity({
action: ActivityService.Action.DELETE,
collection: req.collection,
item: req.params.pk,
await RelationsService.deleteRelation(Number(req.params.pk), {
ip: req.ip,
user_agent: req.get('user-agent'),
action_by: req.user,
userAgent: req.get('user-agent'),
user: req.user,
});
return res.status(200).end();

View File

@@ -11,18 +11,15 @@ const router = express.Router();
router.post(
'/',
useCollection('directus_roles'),
sanitizeQuery,
validateQuery,
asyncHandler(async (req, res) => {
const item = await RolesService.createRole(req.body, req.sanitizedQuery);
ActivityService.createActivity({
action: ActivityService.Action.CREATE,
collection: req.collection,
item: item.id,
const primaryKey = await RolesService.createRole(req.body, {
ip: req.ip,
user_agent: req.get('user-agent'),
action_by: req.user,
userAgent: req.get('user-agent'),
user: req.user,
});
const item = await RolesService.readRole(primaryKey, req.sanitizedQuery);
return res.json({ data: item });
})
);
@@ -52,18 +49,15 @@ router.get(
router.patch(
'/:pk',
useCollection('directus_roles'),
sanitizeQuery,
validateQuery,
asyncHandler(async (req, res) => {
const item = await RolesService.updateRole(req.params.pk, req.body, req.sanitizedQuery);
ActivityService.createActivity({
action: ActivityService.Action.UPDATE,
collection: req.collection,
item: item.id,
const primaryKey = await RolesService.updateRole(req.params.pk, req.body, {
ip: req.ip,
user_agent: req.get('user-agent'),
action_by: req.user,
userAgent: req.get('user-agent'),
user: req.user,
});
const item = await RolesService.readRole(primaryKey, req.sanitizedQuery);
return res.json({ data: item });
})
);
@@ -72,15 +66,10 @@ router.delete(
'/:pk',
useCollection('directus_roles'),
asyncHandler(async (req, res) => {
await RolesService.deleteRole(req.params.pk);
ActivityService.createActivity({
action: ActivityService.Action.DELETE,
collection: req.collection,
item: req.params.pk,
await RolesService.deleteRole(req.params.pk, {
ip: req.ip,
user_agent: req.get('user-agent'),
action_by: req.user,
userAgent: req.get('user-agent'),
user: req.user,
});
return res.status(200).end();

View File

@@ -13,7 +13,7 @@ router.get(
sanitizeQuery,
validateQuery,
asyncHandler(async (req, res) => {
const records = await SettingsService.readSettings(1, req.sanitizedQuery);
const records = await SettingsService.readSettings(req.sanitizedQuery);
return res.json({ data: records });
})
);
@@ -21,13 +21,22 @@ router.get(
router.patch(
'/',
useCollection('directus_settings'),
sanitizeQuery,
validateQuery,
asyncHandler(async (req, res) => {
const records = await SettingsService.updateSettings(
const primaryKey = await SettingsService.updateSettings(
req.params.pk /** @TODO Singleton */,
req.body,
req.sanitizedQuery
{
ip: req.ip,
userAgent: req.get('user-agent'),
user: req.user,
}
);
return res.json({ data: records });
const record = await SettingsService.readSettings(req.sanitizedQuery);
return res.json({ data: record });
})
);

View File

@@ -6,25 +6,21 @@ import * as UsersService from '../services/users';
import Joi from '@hapi/joi';
import { InvalidPayloadException, InvalidCredentialsException } from '../exceptions';
import useCollection from '../middleware/use-collection';
import * as ActivityService from '../services/activity';
const router = express.Router();
router.post(
'/',
useCollection('directus_users'),
sanitizeQuery,
validateQuery,
asyncHandler(async (req, res) => {
const item = await UsersService.createUser(req.body, req.sanitizedQuery);
ActivityService.createActivity({
action: ActivityService.Action.CREATE,
collection: req.collection,
item: item.id,
const primaryKey = await UsersService.createUser(req.body, {
ip: req.ip,
user_agent: req.get('user-agent'),
action_by: req.user,
userAgent: req.get('user-agent'),
user: req.user,
});
const item = await UsersService.readUser(primaryKey, req.sanitizedQuery);
return res.json({ data: item });
})
);
@@ -36,7 +32,6 @@ router.get(
validateQuery,
asyncHandler(async (req, res) => {
const item = await UsersService.readUsers(req.sanitizedQuery);
return res.json({ data: item });
})
);
@@ -77,17 +72,13 @@ router.patch(
throw new InvalidCredentialsException();
}
const item = await UsersService.updateUser(req.user, req.body, req.sanitizedQuery);
ActivityService.createActivity({
action: ActivityService.Action.UPDATE,
collection: req.collection,
item: item.id,
const primaryKey = await UsersService.updateUser(req.user, req.body, {
ip: req.ip,
user_agent: req.get('user-agent'),
action_by: req.user,
userAgent: req.get('user-agent'),
user: req.user,
});
const item = await UsersService.readUser(primaryKey, req.sanitizedQuery);
return res.json({ data: item });
})
);
@@ -95,18 +86,15 @@ router.patch(
router.patch(
'/:pk',
useCollection('directus_users'),
sanitizeQuery,
validateQuery,
asyncHandler(async (req, res) => {
const item = await UsersService.updateUser(req.params.pk, req.body, req.sanitizedQuery);
ActivityService.createActivity({
action: ActivityService.Action.UPDATE,
collection: req.collection,
item: item.id,
const primaryKey = await UsersService.updateUser(req.params.pk, req.body, {
ip: req.ip,
user_agent: req.get('user-agent'),
action_by: req.user,
userAgent: req.get('user-agent'),
user: req.user,
});
const item = await UsersService.readUser(primaryKey, req.sanitizedQuery);
return res.json({ data: item });
})
);
@@ -115,15 +103,10 @@ router.delete(
'/:pk',
useCollection('directus_users'),
asyncHandler(async (req, res) => {
await UsersService.deleteUser(req.params.pk);
ActivityService.createActivity({
action: ActivityService.Action.DELETE,
collection: req.collection,
item: req.params.pk,
await UsersService.deleteUser(req.params.pk, {
ip: req.ip,
user_agent: req.get('user-agent'),
action_by: req.user,
userAgent: req.get('user-agent'),
user: req.user,
});
return res.status(200).end();
@@ -141,7 +124,11 @@ router.post(
asyncHandler(async (req, res) => {
const { error } = inviteSchema.validate(req.body);
if (error) throw new InvalidPayloadException(error.message);
await UsersService.inviteUser(req.body.email, req.body.role);
await UsersService.inviteUser(req.body.email, req.body.role, {
ip: req.ip,
userAgent: req.get('user-agent'),
user: req.user,
});
res.end();
})
);

View File

@@ -12,17 +12,14 @@ router.post(
'/',
useCollection('directus_webhooks'),
asyncHandler(async (req, res) => {
const item = await WebhooksService.createWebhook(req.body, req.sanitizedQuery);
ActivityService.createActivity({
action: ActivityService.Action.CREATE,
collection: req.collection,
item: item.id,
const primaryKey = await WebhooksService.createWebhook(req.body, {
ip: req.ip,
user_agent: req.get('user-agent'),
action_by: req.user,
userAgent: req.get('user-agent'),
user: req.user,
});
const item = await WebhooksService.readWebhook(primaryKey, req.sanitizedQuery);
return res.json({ data: item });
})
);
@@ -53,21 +50,12 @@ router.patch(
'/:pk',
useCollection('directus_webhooks'),
asyncHandler(async (req, res) => {
const item = await WebhooksService.updateWebhook(
req.params.pk,
req.body,
req.sanitizedQuery
);
ActivityService.createActivity({
action: ActivityService.Action.UPDATE,
collection: req.collection,
item: item.id,
const primaryKey = await WebhooksService.updateWebhook(req.params.pk, req.body, {
ip: req.ip,
user_agent: req.get('user-agent'),
action_by: req.user,
userAgent: req.get('user-agent'),
user: req.user,
});
const item = await WebhooksService.readWebhook(primaryKey, req.sanitizedQuery);
return res.json({ data: item });
})
);
@@ -76,15 +64,10 @@ router.delete(
'/:pk',
useCollection('directus_webhooks'),
asyncHandler(async (req, res) => {
await WebhooksService.deleteWebhook(req.params.pk);
ActivityService.createActivity({
action: ActivityService.Action.DELETE,
collection: req.collection,
item: req.params.pk,
await WebhooksService.deleteWebhook(req.params.pk, {
ip: req.ip,
user_agent: req.get('user-agent'),
action_by: req.user,
userAgent: req.get('user-agent'),
user: req.user,
});
return res.status(200).end();

View File

@@ -11,9 +11,8 @@ export enum Action {
AUTHENTICATE = 'authenticate',
}
export const createActivity = async (data: Record<string, any>, query?: Query) => {
const primaryKey = await ItemsService.createItem('directus_activity', data);
return await ItemsService.readItem('directus_activity', primaryKey, query);
export const createActivity = async (data: Record<string, any>) => {
return await ItemsService.createItem('directus_activity', data);
};
export const readActivities = async (query?: Query) => {

View File

@@ -1,6 +1,8 @@
import { Query } from '../types/query';
import * as ItemsService from './items';
/** @todo check if we want to save activity for collection presets */
export const createCollectionPreset = async (data: Record<string, any>, query: Query) => {
const primaryKey = await ItemsService.createItem('directus_collection_presets', data);
return await ItemsService.readItem('directus_collection_presets', primaryKey, query);

View File

@@ -3,9 +3,10 @@ import * as ItemsService from '../services/items';
import { Collection } from '../types/collection';
import { Query } from '../types/query';
import { ColumnBuilder } from 'knex';
import { Accountability } from '../types/accountability';
/** @Todo properly type this */
export const create = async (payload: any) => {
export const create = async (payload: any, accountability: Accountability) => {
await database.schema.createTable(payload.collection, (table) => {
if (payload.note) {
table.comment(payload.note);
@@ -13,6 +14,8 @@ export const create = async (payload: any) => {
/** @todo move this into fields service */
/** @todo adhere to new nested structure system vs database */
payload.fields?.forEach((field: any) => {
let column: ColumnBuilder;
@@ -36,16 +39,18 @@ export const create = async (payload: any) => {
});
});
const primaryKey = await ItemsService.createItem('directus_collections', {
collection: payload.collection,
hidden: payload.hidden || false,
single: payload.single || false,
icon: payload.icon || null,
note: payload.note || null,
translation: payload.translation || null,
});
const collection = await ItemsService.createItem('directus_collections', primaryKey);
const primaryKey = await ItemsService.createItem(
'directus_collections',
{
collection: payload.collection,
hidden: payload.hidden || false,
single: payload.single || false,
icon: payload.icon || null,
note: payload.note || null,
translation: payload.translation || null,
},
accountability
);
/**
* @TODO make this flexible and based on payload
@@ -62,7 +67,7 @@ export const create = async (payload: any) => {
}))
);
return collection;
return await ItemsService.readItem('directus_collections', primaryKey);
};
export const readAll = async (query?: Query) => {
@@ -105,10 +110,10 @@ export const readOne = async (collection: string, query?: Query) => {
};
};
export const deleteCollection = async (collection: string) => {
export const deleteCollection = async (collection: string, accountability: Accountability) => {
await Promise.all([
database.schema.dropTable(collection),
ItemsService.deleteItem('directus_collections', collection),
ItemsService.deleteItem('directus_collections', collection, accountability),
database.delete().from('directus_fields').where({ collection }),
database
.delete()

View File

@@ -1,6 +1,8 @@
import database, { schemaInspector } from '../database';
import { Field } from '../types/field';
import { uniq } from 'lodash';
import { Accountability } from '../types';
import * as ItemsService from '../services/items';
export const fieldsInCollection = async (collection: string) => {
const [fields, columns] = await Promise.all([
@@ -55,20 +57,30 @@ export const readOne = async (collection: string, field: string) => {
return data;
};
export const createField = async (collection: string, field: Partial<Field>) => {
await database.schema.alterTable('articles', (table) => {
export const createField = async (
collection: string,
field: Partial<Field>,
accountability: Accountability
) => {
await database.schema.alterTable(collection, (table) => {
table.specificType(field.field, field.database.type);
/** @todo add support for other database info (length etc) */
});
if (field.system) {
await database('directus_fields').insert({
...field.system,
collection: collection,
field: field.field,
});
await ItemsService.createItem(
'directus_fields',
{
...field.system,
collection: collection,
field: field.field,
},
accountability
);
}
const createdField = await readOne(collection, field.field);
return createdField;
};
/** @todo add update / delete field */

View File

@@ -9,11 +9,13 @@ import parseEXIF from 'exif-reader';
import parseIPTC from '../utils/parse-iptc';
import path from 'path';
import { v4 as uuidv4 } from 'uuid';
import { Accountability } from '../types';
import { Readable } from 'stream';
export const createFile = async (
data: Record<string, any>,
stream: NodeJS.ReadableStream,
query?: Query
accountability: Accountability
) => {
const id = uuidv4();
@@ -51,8 +53,7 @@ export const createFile = async (
}
await storage.disk(data.storage).put(payload.filename_disk, stream.pipe(pipeline));
const primaryKey = await ItemsService.createItem('directus_files', payload);
return await ItemsService.readItem('directus_files', primaryKey, query);
return await ItemsService.createItem('directus_files', payload, accountability);
};
export const readFiles = async (query: Query) => {
@@ -63,12 +64,11 @@ export const readFile = async (pk: string | number, query: Query) => {
return await ItemsService.readItem('directus_files', pk, query);
};
// @todo Add query support
export const updateFile = async (
pk: string | number,
data: Record<string, any>,
stream?: NodeJS.ReadableStream,
query?: Query
accountability: Accountability,
stream?: NodeJS.ReadableStream
) => {
/**
* @TODO
@@ -87,15 +87,13 @@ export const updateFile = async (
.where({ id: pk })
.first();
// @todo type of stream in flydrive is wrong: https://github.com/Slynova-Org/flydrive/issues/145
await storage.disk(file.storage).put(file.filename_disk, stream as any);
await storage.disk(file.storage).put(file.filename_disk, stream as Readable);
}
const primaryKey = await ItemsService.updateItem('directus_files', pk, data);
return await ItemsService.readItem('directus_files', primaryKey, query);
return await ItemsService.updateItem('directus_files', pk, data, accountability);
};
export const deleteFile = async (pk: string | number) => {
export const deleteFile = async (pk: string, accountability: Accountability) => {
const file = await database
.select('storage', 'filename_disk')
.from('directus_files')

View File

@@ -1,28 +1,29 @@
import { Query } from '../types/query';
import * as ItemsService from './items';
import { Accountability, Query } from '../types';
export const createFolder = async (data: Record<string, any>, query: Query) => {
const primaryKey = await ItemsService.createItem('directus_folders', data);
return await ItemsService.readItem('directus_folders', primaryKey, query);
export const createFolder = async (
data: Record<string, any>,
accountability: Accountability
): Promise<string> => {
return (await ItemsService.createItem('directus_folders', data, accountability)) as string;
};
export const readFolders = async (query: Query) => {
return await ItemsService.readItems('directus_folders', query);
};
export const readFolder = async (pk: string | number, query: Query) => {
export const readFolder = async (pk: string, query: Query) => {
return await ItemsService.readItem('directus_folders', pk, query);
};
export const updateFolder = async (
pk: string | number,
pk: string,
data: Record<string, any>,
query: Query
) => {
const primaryKey = await ItemsService.updateItem('directus_folders', pk, data);
return await ItemsService.readItem('directus_folders', primaryKey, query);
accountability: Accountability
): Promise<string> => {
return (await ItemsService.updateItem('directus_folders', pk, data, accountability)) as string;
};
export const deleteFolder = async (pk: string | number) => {
await ItemsService.deleteItem('directus_folders', pk);
export const deleteFolder = async (pk: string, accountability: Accountability) => {
await ItemsService.deleteItem('directus_folders', pk, accountability);
};

View File

@@ -3,9 +3,46 @@ import { Query } from '../types/query';
import runAST from '../database/run-ast';
import getAST from '../utils/get-ast';
import * as PayloadService from './payload';
import { pick } from 'lodash';
import { Accountability } from '../types/accountability';
export const createItem = async (collection: string, data: Record<string, any>) => {
import * as ActivityService from './activity';
import * as RevisionsService from './revisions';
import { pick } from 'lodash';
import logger from '../logger';
async function saveActivityAndRevision(
action: ActivityService.Action,
collection: string,
item: string,
payload: Record<string, any>,
accountability: Accountability
) {
const activityID = await ActivityService.createActivity({
action,
collection,
item,
ip: accountability.ip,
user_agent: accountability.userAgent,
action_by: accountability.user,
});
await RevisionsService.createRevision({
activity: activityID,
collection,
item,
delta: payload,
/** @todo make this configurable */
data: await readItem(collection, item, { fields: ['*'] }),
parent: accountability.parent,
});
}
export const createItem = async (
collection: string,
data: Record<string, any>,
accountability?: Accountability
): Promise<string | number> => {
let payload = await PayloadService.processValues('create', collection, data);
payload = await PayloadService.processM2O(collection, payload);
@@ -14,13 +51,14 @@ export const createItem = async (collection: string, data: Record<string, any>)
// Only insert the values that actually save to an existing column. This ensures we ignore aliases etc
const columns = await schemaInspector.columns(collection);
const payloadWithoutAlias = pick(
payload,
columns.map(({ column }) => column)
);
const primaryKeys = await database(collection)
.insert(
pick(
payload,
columns.map(({ column }) => column)
)
)
.insert(payloadWithoutAlias)
.returning(primaryKeyField);
// This allows the o2m values to be populated correctly
@@ -28,6 +66,17 @@ export const createItem = async (collection: string, data: Record<string, any>)
await PayloadService.processO2M(collection, payload);
if (accountability) {
// Don't await this. It can run async in the background
saveActivityAndRevision(
ActivityService.Action.CREATE,
collection,
primaryKeys[0],
payloadWithoutAlias,
accountability
).catch((err) => logger.error(err));
}
return primaryKeys[0];
};
@@ -67,9 +116,10 @@ export const readItem = async <T = any>(
export const updateItem = async (
collection: string,
pk: number | string,
data: Record<string, any>
) => {
let payload = await PayloadService.processValues('create', collection, data);
data: Record<string, any>,
accountability?: Accountability
): Promise<string | number> => {
let payload = await PayloadService.processValues('update', collection, data);
payload = await PayloadService.processM2O(collection, payload);
@@ -77,21 +127,49 @@ export const updateItem = async (
// Only insert the values that actually save to an existing column. This ensures we ignore aliases etc
const columns = await schemaInspector.columns(collection);
const payloadWithoutAlias = pick(
payload,
columns.map(({ column }) => column)
);
const primaryKeys = await database(collection)
.update(
pick(
payload,
columns.map(({ column }) => column)
)
)
.update(payloadWithoutAlias)
.where({ [primaryKeyField]: pk })
.returning(primaryKeyField);
if (accountability) {
// Don't await this. It can run async in the background
saveActivityAndRevision(
ActivityService.Action.UPDATE,
collection,
primaryKeys[0],
payloadWithoutAlias,
accountability
).catch((err) => logger.error(err));
}
return primaryKeys[0];
};
export const deleteItem = async (collection: string, pk: number | string) => {
export const deleteItem = async (
collection: string,
pk: number | string,
accountability?: Accountability
) => {
const primaryKeyField = await schemaInspector.primary(collection);
if (accountability) {
// Don't await this. It can run async in the background
saveActivityAndRevision(
ActivityService.Action.DELETE,
collection,
String(pk),
{},
accountability
).catch((err) => logger.error(err));
}
return await database(collection)
.delete()
.where({ [primaryKeyField]: pk });

View File

@@ -7,7 +7,7 @@ import { System } from '../types/field';
import argon2 from 'argon2';
import { v4 as uuidv4 } from 'uuid';
import database from '../database';
import { clone } from 'lodash';
import { clone, isObject } from 'lodash';
import { File } from '../types/files';
import { Relation } from '../types/relation';
import * as ItemsService from './items';
@@ -24,6 +24,10 @@ type Transformers = {
/**
* @todo allow this to be extended
*
* @todo allow these extended special types to have "field dependencies"?
* f.e. the file-links transformer needs the id and filename_download to be fetched from the DB
* in order to work
*/
const transformers: Transformers = {
async hash(operation, value) {
@@ -42,10 +46,13 @@ const transformers: Transformers = {
return value;
},
async 'file-info'(operation, value, payload: File) {
if (operation === 'read' && payload) {
async 'file-links'(operation, value, payload: File) {
if (operation === 'read' && payload && payload.storage && payload.filename_disk) {
const publicKey = `STORAGE_${payload.storage.toUpperCase()}_PUBLIC_URL`;
return {
asset_url: new URL(`/assets/${payload.id}`, process.env.PUBLIC_URL),
public_url: new URL(payload.filename_disk, process.env[publicKey]),
};
}
@@ -131,7 +138,7 @@ export const processM2O = async (collection: string, payload: Record<string, any
const relationsToProcess = relations.filter((relation) => {
return (
payloadClone.hasOwnProperty(relation.field_many) &&
typeof payloadClone[relation.field_many] === 'object'
isObject(payloadClone[relation.field_many])
);
});

View File

@@ -1,28 +1,34 @@
import { Query } from '../types/query';
import { Accountability, Query } from '../types';
import * as ItemsService from './items';
export const createPermission = async (data: Record<string, any>, query: Query) => {
const primaryKey = await ItemsService.createItem('directus_permissions', data);
return await ItemsService.readItem('directus_permissions', primaryKey, query);
export const createPermission = async (
data: Record<string, any>,
accountability: Accountability
): Promise<number> => {
return (await ItemsService.createItem('directus_permissions', data, accountability)) as number;
};
export const readPermissions = async (query: Query) => {
return await ItemsService.readItems('directus_permissions', query);
};
export const readPermission = async (pk: string | number, query: Query) => {
export const readPermission = async (pk: number, query: Query) => {
return await ItemsService.readItem('directus_permissions', pk, query);
};
export const updatePermission = async (
pk: string | number,
pk: number,
data: Record<string, any>,
query: Query
) => {
const primaryKey = await ItemsService.updateItem('directus_permissions', pk, data);
return await ItemsService.readItem('directus_permissions', primaryKey, query);
accountability: Accountability
): Promise<number> => {
return (await ItemsService.updateItem(
'directus_permissions',
pk,
data,
accountability
)) as number;
};
export const deletePermission = async (pk: string | number) => {
await ItemsService.deleteItem('directus_permissions', pk);
export const deletePermission = async (pk: number, accountability: Accountability) => {
await ItemsService.deleteItem('directus_permissions', pk, accountability);
};

View File

@@ -1,9 +1,8 @@
import { Query } from '../types/query';
import { Accountability, Query } from '../types';
import * as ItemsService from './items';
export const createRelation = async (data: Record<string, any>, query: Query) => {
const primaryKey = await ItemsService.createItem('directus_relations', data);
return ItemsService.readItem('directus_relations', primaryKey, query);
export const createRelation = async (data: Record<string, any>, accountability: Accountability) => {
return await ItemsService.createItem('directus_relations', data, accountability);
};
export const readRelations = async (query: Query) => {
@@ -17,12 +16,11 @@ export const readRelation = async (pk: string | number, query: Query) => {
export const updateRelation = async (
pk: string | number,
data: Record<string, any>,
query: Query
accountability: Accountability
) => {
const primaryKey = await ItemsService.updateItem('directus_relations', pk, data);
return ItemsService.readItem('directus_relations', primaryKey, query);
return await ItemsService.updateItem('directus_relations', pk, data, accountability);
};
export const deleteRelation = async (pk: string | number) => {
await ItemsService.deleteItem('directus_relations', pk);
export const deleteRelation = async (pk: number, accountability: Accountability) => {
await ItemsService.deleteItem('directus_relations', pk, accountability);
};

View File

@@ -1,6 +1,10 @@
import { Query } from '../types/query';
import * as ItemsService from './items';
export const createRevision = async (data: Record<string, any>) => {
return await ItemsService.createItem('directus_revisions', data);
};
export const readRevisions = async (query: Query) => {
return await ItemsService.readItems('directus_revisions', query);
};

View File

@@ -1,9 +1,8 @@
import { Query } from '../types/query';
import { Accountability, Query } from '../types';
import * as ItemsService from './items';
export const createRole = async (data: Record<string, any>, query: Query) => {
const primaryKey = await ItemsService.createItem('directus_roles', data);
return await ItemsService.readItem('directus_roles', primaryKey, query);
export const createRole = async (data: Record<string, any>, accountability: Accountability) => {
return await ItemsService.createItem('directus_roles', data, accountability);
};
export const readRoles = async (query: Query) => {
@@ -14,11 +13,14 @@ export const readRole = async (pk: string | number, query: Query) => {
return await ItemsService.readItem('directus_roles', pk, query);
};
export const updateRole = async (pk: string | number, data: Record<string, any>, query: Query) => {
const primaryKey = await ItemsService.updateItem('directus_roles', pk, data);
return await ItemsService.readItem('directus_roles', primaryKey, query);
export const updateRole = async (
pk: string | number,
data: Record<string, any>,
accountability: Accountability
) => {
return await ItemsService.updateItem('directus_roles', pk, data, accountability);
};
export const deleteRole = async (pk: string | number) => {
await ItemsService.deleteItem('directus_roles', pk);
export const deleteRole = async (pk: string | number, accountability: Accountability) => {
await ItemsService.deleteItem('directus_roles', pk, accountability);
};

View File

@@ -1,7 +1,8 @@
import { Query } from '../types/query';
import * as ItemsService from './items';
import { Accountability } from '../types';
export const readSettings = async (pk: string | number, query: Query) => {
export const readSettings = async (query: Query) => {
const settings = await ItemsService.readItems('directus_settings', {
...query,
limit: 1,
@@ -13,9 +14,8 @@ export const readSettings = async (pk: string | number, query: Query) => {
export const updateSettings = async (
pk: string | number,
data: Record<string, any>,
query: Query
accountability: Accountability
) => {
/** @NOTE I guess this can technically update _all_ items, as we expect there to only be one */
const primaryKey = await ItemsService.updateItem('directus_settings', pk, data);
return await ItemsService.readItem('directus_settings', primaryKey, query);
return await ItemsService.updateItem('directus_settings', pk, data, accountability);
};

View File

@@ -1,14 +1,13 @@
import { Query } from '../types/query';
import * as ItemsService from './items';
import jwt from 'jsonwebtoken';
import { sendInviteMail } from '../mail';
import database from '../database';
import argon2 from 'argon2';
import { InvalidPayloadException } from '../exceptions';
import { Accountability, Query } from '../types';
export const createUser = async (data: Record<string, any>, query?: Query) => {
const primaryKey = await ItemsService.createItem('directus_users', data);
return await ItemsService.readItem('directus_user', primaryKey, query);
export const createUser = async (data: Record<string, any>, accountability: Accountability) => {
return await ItemsService.createItem('directus_users', data, accountability);
};
export const readUsers = async (query?: Query) => {
@@ -19,23 +18,26 @@ export const readUser = async (pk: string | number, query?: Query) => {
return await ItemsService.readItem('directus_users', pk, query);
};
export const updateUser = async (pk: string | number, data: Record<string, any>, query?: Query) => {
export const updateUser = async (
pk: string | number,
data: Record<string, any>,
accountability: Accountability
) => {
/**
* @todo
* Remove "other" refresh token sessions when changing password to enforce "logout everywhere" on password change
*
* Maybe make this an option?
*/
const primaryKey = await ItemsService.updateItem('directus_users', pk, data);
return await ItemsService.readItem('directus_user', primaryKey, query);
return await ItemsService.updateItem('directus_users', pk, data, accountability);
};
export const deleteUser = async (pk: string | number) => {
await ItemsService.deleteItem('directus_users', pk);
export const deleteUser = async (pk: string | number, accountability: Accountability) => {
await ItemsService.deleteItem('directus_users', pk, accountability);
};
export const inviteUser = async (email: string, role: string) => {
await createUser({ email, role, status: 'invited' });
export const inviteUser = async (email: string, role: string, accountability: Accountability) => {
await createUser({ email, role, status: 'invited' }, accountability);
const payload = { email };
const token = jwt.sign(payload, process.env.SECRET, { expiresIn: '7d' });
@@ -45,7 +47,7 @@ export const inviteUser = async (email: string, role: string) => {
};
export const acceptInvite = async (token: string, password: string) => {
const { email } = jwt.verify(token, process.env.SECRET) as Record<string, any>;
const { email } = jwt.verify(token, process.env.SECRET) as { email: string };
const user = await database
.select('id', 'status')
.from('directus_users')

View File

@@ -1,9 +1,8 @@
import { Query } from '../types/query';
import { Accountability, Query } from '../types';
import * as ItemsService from './items';
export const createWebhook = async (data: Record<string, any>, query: Query) => {
const primaryKey = await ItemsService.createItem('directus_webhooks', data);
return await ItemsService.readItem('directus_webhooks', primaryKey, query);
export const createWebhook = async (data: Record<string, any>, accountability: Accountability) => {
return await ItemsService.createItem('directus_webhooks', data, accountability);
};
export const readWebhooks = async (query: Query) => {
@@ -17,12 +16,11 @@ export const readWebhook = async (pk: string | number, query: Query) => {
export const updateWebhook = async (
pk: string | number,
data: Record<string, any>,
query: Query
accountability: Accountability
) => {
const primaryKey = await ItemsService.updateItem('directus_webhooks', pk, data);
return await ItemsService.readItem('directus_webhooks', primaryKey, query);
return await ItemsService.updateItem('directus_webhooks', pk, data, accountability);
};
export const deleteWebhook = async (pk: string | number) => {
await ItemsService.deleteItem('directus_webhooks', pk);
export const deleteWebhook = async (pk: string | number, accountability: Accountability) => {
await ItemsService.deleteItem('directus_webhooks', pk, accountability);
};

View File

@@ -0,0 +1,7 @@
export type Accountability = {
ip?: string;
userAgent?: string;
user?: string;
parent?: number;
};

View File

@@ -2,4 +2,5 @@
export type File = {
id: string; // uuid
filename_disk: string;
storage: string;
};

10
src/types/index.ts Normal file
View File

@@ -0,0 +1,10 @@
export * from './accountability';
export * from './assets';
export * from './ast';
export * from './collection';
export * from './field';
export * from './files';
export * from './meta';
export * from './query';
export * from './relation';
export * from './sessions';