From 48fca556510bc39910547b8c1889236e644da499 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Tue, 22 Sep 2020 13:16:14 -0400 Subject: [PATCH 1/4] Restructure exports for services --- api/src/app.ts | 10 +++++- api/src/controllers/activity.ts | 3 +- api/src/controllers/assets.ts | 3 +- api/src/controllers/auth.ts | 3 +- api/src/controllers/collections.ts | 3 +- api/src/controllers/extensions.ts | 2 +- api/src/controllers/fields.ts | 5 ++- api/src/controllers/files.ts | 3 +- api/src/controllers/folders.ts | 3 +- api/src/controllers/items.ts | 3 +- api/src/controllers/permissions.ts | 3 +- api/src/controllers/presets.ts | 3 +- api/src/controllers/relations.ts | 3 +- api/src/controllers/revisions.ts | 3 +- api/src/controllers/roles.ts | 3 +- api/src/controllers/server.ts | 2 +- api/src/controllers/settings.ts | 2 +- api/src/controllers/users.ts | 6 ++-- api/src/controllers/utils.ts | 2 +- api/src/controllers/webhooks.ts | 3 +- api/src/database/run-ast.ts | 2 +- api/src/services/activity.ts | 4 +-- api/src/services/assets.ts | 5 ++- api/src/services/authentication.ts | 4 +-- api/src/services/authorization.ts | 4 +-- api/src/services/collections.ts | 7 ++-- api/src/services/extensions.ts | 55 ++++++++++++++++++++++++++++-- api/src/services/fields.ts | 6 ++-- api/src/services/files.ts | 4 +-- api/src/services/folders.ts | 4 +-- api/src/services/index.ts | 21 ++++++++++++ api/src/services/items.ts | 6 ++-- api/src/services/meta.ts | 2 +- api/src/services/payload.ts | 5 ++- api/src/services/permissions.ts | 6 ++-- api/src/services/presets.ts | 4 +-- api/src/services/relations.ts | 4 +-- api/src/services/revisions.ts | 4 +-- api/src/services/roles.ts | 10 +++--- api/src/services/server.ts | 2 +- api/src/services/settings.ts | 4 +-- api/src/services/users.ts | 8 ++--- api/src/services/utils.ts | 2 +- api/src/services/webhooks.ts | 4 +-- api/src/types/extensions.ts | 3 ++ api/src/types/index.ts | 1 + 46 files changed, 156 insertions(+), 93 deletions(-) create mode 100644 api/src/services/index.ts create mode 100644 api/src/types/extensions.ts diff --git a/api/src/app.ts b/api/src/app.ts index 492adcbc74..60b09ba2ec 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -37,13 +37,16 @@ import webhooksRouter from './controllers/webhooks'; import notFoundHandler from './controllers/not-found'; import sanitizeQuery from './middleware/sanitize-query'; -import WebhooksService from './services/webhooks'; +import { WebhooksService } from './services/webhooks'; +import { ExtensionsService } from './services/extensions'; import { InvalidPayloadException } from './exceptions'; validateEnv(['KEY', 'SECRET']); const app = express(); +const customRouter = express.Router(); + app.disable('x-powered-by'); app.set('trust proxy', true); @@ -111,6 +114,7 @@ app.use('/settings', settingsRouter, respond); app.use('/users', usersRouter, respond); app.use('/utils', utilsRouter, respond); app.use('/webhooks', webhooksRouter, respond); +app.use('/custom', customRouter); app.use(notFoundHandler); app.use(errorHandler); @@ -118,6 +122,10 @@ app.use(errorHandler); const webhooksService = new WebhooksService(); webhooksService.register(); +// Register custom hooks / endpoints +const extensionsService = new ExtensionsService(); +extensionsService.register(customRouter); + track('serverStarted'); export default app; diff --git a/api/src/controllers/activity.ts b/api/src/controllers/activity.ts index f3ec5f06d2..780f183249 100644 --- a/api/src/controllers/activity.ts +++ b/api/src/controllers/activity.ts @@ -1,7 +1,6 @@ import express from 'express'; import asyncHandler from 'express-async-handler'; -import ActivityService from '../services/activity'; -import MetaService from '../services/meta'; +import { ActivityService, MetaService } from '../services'; import { Action } from '../types'; import { ForbiddenException } from '../exceptions'; import useCollection from '../middleware/use-collection'; diff --git a/api/src/controllers/assets.ts b/api/src/controllers/assets.ts index f10fbde8c7..838ecc7626 100644 --- a/api/src/controllers/assets.ts +++ b/api/src/controllers/assets.ts @@ -3,12 +3,11 @@ import asyncHandler from 'express-async-handler'; import database from '../database'; import { SYSTEM_ASSET_ALLOW_LIST, ASSET_TRANSFORM_QUERY_KEYS } from '../constants'; import { InvalidQueryException, ForbiddenException } from '../exceptions'; -import AssetsService from '../services/assets'; import validate from 'uuid-validate'; import { pick } from 'lodash'; import { Transformation } from '../types/assets'; import storage from '../storage'; -import PayloadService from '../services/payload'; +import { PayloadService, AssetsService } from '../services'; import useCollection from '../middleware/use-collection'; const router = Router(); diff --git a/api/src/controllers/auth.ts b/api/src/controllers/auth.ts index 3825962ef7..efedc40fb8 100644 --- a/api/src/controllers/auth.ts +++ b/api/src/controllers/auth.ts @@ -2,7 +2,6 @@ import { Router } from 'express'; import session from 'express-session'; import asyncHandler from 'express-async-handler'; import Joi from 'joi'; -import AuthenticationService from '../services/authentication'; import grant from 'grant'; import getGrantConfig from '../utils/get-grant-config'; import getEmailFromProfile from '../utils/get-email-from-profile'; @@ -10,7 +9,7 @@ import { InvalidPayloadException } from '../exceptions/invalid-payload'; import ms from 'ms'; import cookieParser from 'cookie-parser'; import env from '../env'; -import UsersService from '../services/users'; +import { UsersService, AuthenticationService } from '../services'; const router = Router(); diff --git a/api/src/controllers/collections.ts b/api/src/controllers/collections.ts index d7e2867760..92ebe80869 100644 --- a/api/src/controllers/collections.ts +++ b/api/src/controllers/collections.ts @@ -1,7 +1,6 @@ import { Router } from 'express'; import asyncHandler from 'express-async-handler'; -import CollectionsService from '../services/collections' -import MetaService from '../services/meta'; +import { CollectionsService, MetaService } from '../services'; import { ForbiddenException } from '../exceptions'; const router = Router(); diff --git a/api/src/controllers/extensions.ts b/api/src/controllers/extensions.ts index 4f1133856e..5347b53d11 100644 --- a/api/src/controllers/extensions.ts +++ b/api/src/controllers/extensions.ts @@ -1,7 +1,7 @@ import express, { Router } from 'express'; import asyncHandler from 'express-async-handler'; import { RouteNotFoundException } from '../exceptions'; -import ExtensionsService from '../services/extensions'; +import { ExtensionsService } from '../services/extensions'; import env from '../env'; const router = Router(); diff --git a/api/src/controllers/fields.ts b/api/src/controllers/fields.ts index 6530f8c0ad..e57cd75f5d 100644 --- a/api/src/controllers/fields.ts +++ b/api/src/controllers/fields.ts @@ -1,12 +1,11 @@ import { Router } from 'express'; import asyncHandler from 'express-async-handler'; -import FieldsService from '../services/fields'; +import { FieldsService } from '../services/fields'; import validateCollection from '../middleware/collection-exists'; import { schemaInspector } from '../database'; import { InvalidPayloadException, ForbiddenException } from '../exceptions'; import Joi from 'joi'; -import { Field } from '../types/field'; -import { types } from '../types'; +import { types, Field } from '../types'; import useCollection from '../middleware/use-collection'; const router = Router(); diff --git a/api/src/controllers/files.ts b/api/src/controllers/files.ts index 7d29e4ce61..86cae96ec1 100644 --- a/api/src/controllers/files.ts +++ b/api/src/controllers/files.ts @@ -1,8 +1,7 @@ import express from 'express'; import asyncHandler from 'express-async-handler'; import Busboy from 'busboy'; -import FilesService from '../services/files'; -import MetaService from '../services/meta'; +import { MetaService, FilesService } from '../services'; import { File, PrimaryKey } from '../types'; import formatTitle from '@directus/format-title'; import env from '../env'; diff --git a/api/src/controllers/folders.ts b/api/src/controllers/folders.ts index ba630c6132..84e2a14ba8 100644 --- a/api/src/controllers/folders.ts +++ b/api/src/controllers/folders.ts @@ -1,7 +1,6 @@ import express from 'express'; import asyncHandler from 'express-async-handler'; -import FoldersService from '../services/folders'; -import MetaService from '../services/meta'; +import {FoldersService, MetaService} from '../services'; import { ForbiddenException } from '../exceptions'; import useCollection from '../middleware/use-collection'; diff --git a/api/src/controllers/items.ts b/api/src/controllers/items.ts index cdc767a2d5..2af0809fb5 100644 --- a/api/src/controllers/items.ts +++ b/api/src/controllers/items.ts @@ -1,8 +1,7 @@ import express from 'express'; import asyncHandler from 'express-async-handler'; -import ItemsService from '../services/items'; import collectionExists from '../middleware/collection-exists'; -import MetaService from '../services/meta'; +import { ItemsService, MetaService} from '../services'; import { RouteNotFoundException, ForbiddenException } from '../exceptions'; const router = express.Router(); diff --git a/api/src/controllers/permissions.ts b/api/src/controllers/permissions.ts index 6a7c5dcb37..1bb269973f 100644 --- a/api/src/controllers/permissions.ts +++ b/api/src/controllers/permissions.ts @@ -1,7 +1,6 @@ import express from 'express'; import asyncHandler from 'express-async-handler'; -import PermissionsService from '../services/permissions'; -import MetaService from '../services/meta'; +import { PermissionsService, MetaService } from '../services'; import { clone } from 'lodash'; import { InvalidCredentialsException, ForbiddenException } from '../exceptions'; import useCollection from '../middleware/use-collection'; diff --git a/api/src/controllers/presets.ts b/api/src/controllers/presets.ts index 333751a367..73db48f448 100644 --- a/api/src/controllers/presets.ts +++ b/api/src/controllers/presets.ts @@ -1,7 +1,6 @@ import express from 'express'; import asyncHandler from 'express-async-handler'; -import PresetsService from '../services/presets'; -import MetaService from '../services/meta'; +import { PresetsService, MetaService } from '../services'; import { ForbiddenException } from '../exceptions'; import useCollection from '../middleware/use-collection'; diff --git a/api/src/controllers/relations.ts b/api/src/controllers/relations.ts index 2d5bf71eb0..49f3c8f11a 100644 --- a/api/src/controllers/relations.ts +++ b/api/src/controllers/relations.ts @@ -1,7 +1,6 @@ import express from 'express'; import asyncHandler from 'express-async-handler'; -import RelationsService from '../services/relations'; -import MetaService from '../services/meta'; +import { RelationsService, MetaService } from '../services'; import { ForbiddenException } from '../exceptions'; import useCollection from '../middleware/use-collection'; diff --git a/api/src/controllers/revisions.ts b/api/src/controllers/revisions.ts index 2c69fe2e4b..a583b5fb79 100644 --- a/api/src/controllers/revisions.ts +++ b/api/src/controllers/revisions.ts @@ -1,7 +1,6 @@ import express from 'express'; import asyncHandler from 'express-async-handler'; -import RevisionsService from '../services/revisions'; -import MetaService from '../services/meta'; +import { RevisionsService, MetaService} from '../services'; import useCollection from '../middleware/use-collection'; const router = express.Router(); diff --git a/api/src/controllers/roles.ts b/api/src/controllers/roles.ts index 9af7583ff3..45e049a88b 100644 --- a/api/src/controllers/roles.ts +++ b/api/src/controllers/roles.ts @@ -1,7 +1,6 @@ import express from 'express'; import asyncHandler from 'express-async-handler'; -import RolesService from '../services/roles'; -import MetaService from '../services/meta'; +import { RolesService, MetaService} from '../services'; import { ForbiddenException } from '../exceptions'; import useCollection from '../middleware/use-collection'; diff --git a/api/src/controllers/server.ts b/api/src/controllers/server.ts index cd0a5a6244..d3cb13394d 100644 --- a/api/src/controllers/server.ts +++ b/api/src/controllers/server.ts @@ -1,5 +1,5 @@ import { Router } from 'express'; -import ServerService from '../services/server'; +import { ServerService } from '../services'; const router = Router(); diff --git a/api/src/controllers/settings.ts b/api/src/controllers/settings.ts index cefd137392..2aa79e2f67 100644 --- a/api/src/controllers/settings.ts +++ b/api/src/controllers/settings.ts @@ -1,6 +1,6 @@ import express from 'express'; import asyncHandler from 'express-async-handler'; -import SettingsService from '../services/settings'; +import { SettingsService } from '../services'; import { ForbiddenException } from '../exceptions'; import useCollection from '../middleware/use-collection'; diff --git a/api/src/controllers/users.ts b/api/src/controllers/users.ts index dca81815fd..dc1f8237d5 100644 --- a/api/src/controllers/users.ts +++ b/api/src/controllers/users.ts @@ -2,9 +2,7 @@ import express from 'express'; import asyncHandler from 'express-async-handler'; import Joi from 'joi'; import { InvalidPayloadException, InvalidCredentialsException, ForbiddenException } from '../exceptions'; -import UsersService from '../services/users'; -import MetaService from '../services/meta'; -import AuthService from '../services/authentication'; +import { UsersService, MetaService, AuthenticationService } from '../services'; import useCollection from '../middleware/use-collection'; const router = express.Router(); @@ -200,7 +198,7 @@ router.post( } const service = new UsersService({ accountability: req.accountability }); - const authService = new AuthService({ accountability: req.accountability }); + const authService = new AuthenticationService({ accountability: req.accountability }); const otpValid = await authService.verifyOTP(req.accountability.user, req.body.otp); diff --git a/api/src/controllers/utils.ts b/api/src/controllers/utils.ts index bb2dd250b9..c4a67c103f 100644 --- a/api/src/controllers/utils.ts +++ b/api/src/controllers/utils.ts @@ -4,7 +4,7 @@ import { nanoid } from 'nanoid'; import { InvalidQueryException, InvalidPayloadException } from '../exceptions'; import argon2 from 'argon2'; import collectionExists from '../middleware/collection-exists'; -import UtilsService from '../services/utils'; +import { UtilsService } from '../services'; import Joi from 'joi'; const router = Router(); diff --git a/api/src/controllers/webhooks.ts b/api/src/controllers/webhooks.ts index dbd05c7b00..0e05f051c9 100644 --- a/api/src/controllers/webhooks.ts +++ b/api/src/controllers/webhooks.ts @@ -1,7 +1,6 @@ import express from 'express'; import asyncHandler from 'express-async-handler'; -import WebhooksService from '../services/webhooks'; -import MetaService from '../services/meta'; +import { WebhooksService, MetaService} from '../services'; import { ForbiddenException } from '../exceptions'; import useCollection from '../middleware/use-collection'; diff --git a/api/src/database/run-ast.ts b/api/src/database/run-ast.ts index 2e3d9c6367..af5f244c89 100644 --- a/api/src/database/run-ast.ts +++ b/api/src/database/run-ast.ts @@ -3,7 +3,7 @@ import { clone, uniq, pick } from 'lodash'; import database from './index'; import SchemaInspector from 'knex-schema-inspector'; import { Query, Item } from '../types'; -import PayloadService from '../services/payload'; +import { PayloadService } from '../services/payload'; import applyQuery from '../utils/apply-query'; import Knex from 'knex'; diff --git a/api/src/services/activity.ts b/api/src/services/activity.ts index 68b95e2458..8e364d4d5d 100644 --- a/api/src/services/activity.ts +++ b/api/src/services/activity.ts @@ -1,11 +1,11 @@ -import ItemsService from './items'; +import { ItemsService } from './items'; import { AbstractServiceOptions } from '../types'; /** * @TODO only return activity of the collections you have access to */ -export default class ActivityService extends ItemsService { +export class ActivityService extends ItemsService { constructor(options?: AbstractServiceOptions) { super('directus_activity', options); } diff --git a/api/src/services/assets.ts b/api/src/services/assets.ts index d82ed06c52..bfbf93f550 100644 --- a/api/src/services/assets.ts +++ b/api/src/services/assets.ts @@ -1,12 +1,11 @@ -import { Transformation } from '../types/assets'; import storage from '../storage'; import sharp, { ResizeOptions } from 'sharp'; import database from '../database'; import path from 'path'; import Knex from 'knex'; -import { Accountability, AbstractServiceOptions } from '../types'; +import { Accountability, AbstractServiceOptions, Transformation } from '../types'; -export default class AssetsService { +export class AssetsService { knex: Knex; accountability: Accountability | null; diff --git a/api/src/services/authentication.ts b/api/src/services/authentication.ts index 3f9b4b310a..6c61d69a3b 100644 --- a/api/src/services/authentication.ts +++ b/api/src/services/authentication.ts @@ -10,7 +10,7 @@ import { } from '../exceptions'; import { Session, Accountability, AbstractServiceOptions, Action } from '../types'; import Knex from 'knex'; -import ActivityService from '../services/activity'; +import { ActivityService } from '../services/activity'; import env from '../env'; import { authenticator } from 'otplib'; @@ -22,7 +22,7 @@ type AuthenticateOptions = { otp?: string; }; -export default class AuthenticationService { +export class AuthenticationService { knex: Knex; accountability: Accountability | null; activityService: ActivityService; diff --git a/api/src/services/authorization.ts b/api/src/services/authorization.ts index 3dc6170202..df71a155ea 100644 --- a/api/src/services/authorization.ts +++ b/api/src/services/authorization.ts @@ -15,10 +15,10 @@ import Knex from 'knex'; import { ForbiddenException, FailedValidationException } from '../exceptions'; import { uniq, merge } from 'lodash'; import generateJoi from '../utils/generate-joi'; -import ItemsService from './items'; +import { ItemsService } from './items'; import { parseFilter } from '../utils/parse-filter'; -export default class AuthorizationService { +export class AuthorizationService { knex: Knex; accountability: Accountability | null; diff --git a/api/src/services/collections.ts b/api/src/services/collections.ts index bece1e741b..5df73d0d32 100644 --- a/api/src/services/collections.ts +++ b/api/src/services/collections.ts @@ -3,12 +3,11 @@ import { AbstractServiceOptions, Accountability, Collection, Relation } from '.. import Knex from 'knex'; import { ForbiddenException, InvalidPayloadException } from '../exceptions'; import SchemaInspector from 'knex-schema-inspector'; -import FieldsService from '../services/fields'; -import { omit } from 'lodash'; -import ItemsService from '../services/items'; +import { FieldsService } from '../services/fields'; +import { ItemsService } from '../services/items'; import cache from '../cache'; -export default class CollectionsService { +export class CollectionsService { knex: Knex; accountability: Accountability | null; diff --git a/api/src/services/extensions.ts b/api/src/services/extensions.ts index 048380b49a..f253bad00c 100644 --- a/api/src/services/extensions.ts +++ b/api/src/services/extensions.ts @@ -2,8 +2,14 @@ import listFolders from '../utils/list-folders'; import path from 'path'; import env from '../env'; import { ServiceUnavailableException } from '../exceptions'; +import { Router } from 'express'; +import emitter from '../emitter'; +import logger from '../logger'; +import { HookRegisterFunction } from '../types'; + +export class ExtensionsService { + registeredHooks: Record = {}; -export default class ExtensionsService { async listExtensions(type: string) { const extensionsPath = env.EXTENSIONS_PATH as string; const location = path.join(extensionsPath, type); @@ -12,11 +18,54 @@ export default class ExtensionsService { return await listFolders(location); } catch (err) { if (err.code === 'ENOENT') { - throw new ServiceUnavailableException(`Extension folder couldn't be opened`, { + throw new ServiceUnavailableException(`Extension folder "extensions/${type}" couldn't be opened`, { service: 'extensions', }); } - console.log(err); + throw err; + } + } + + async register(router: Router) { + let hooks: string[] = []; + let endpoints: string[] = []; + + try { + hooks = await this.listExtensions('hooks'); + this.registerHooks(hooks); + } catch (err) { + logger.warn(err); + } + + try { + endpoints = await this.listExtensions('endpoints'); + } catch (err) { + logger.warn(err); + } + + console.log(hooks, endpoints); + } + + registerHooks(hooks: string[]) { + const extensionsPath = env.EXTENSIONS_PATH as string; + + for (const hook of hooks) { + try { + registerHook(hook); + } catch (error) { + logger.warn(`Couldn't register hook "${hook}"`); + logger.info(error); + } + } + + function registerHook(hook: string) { + const hookPath = path.resolve(extensionsPath, 'hooks', hook, 'index.js'); + const register: HookRegisterFunction = require(hookPath); + const events = register(null); + + for (const [event, handler] of Object.entries(events)) { + emitter.on(event, handler); + } } } } diff --git a/api/src/services/fields.ts b/api/src/services/fields.ts index e0d7937cea..aeac1057a0 100644 --- a/api/src/services/fields.ts +++ b/api/src/services/fields.ts @@ -1,13 +1,13 @@ import database, { schemaInspector } from '../database'; import { Field } from '../types/field'; import { Accountability, AbstractServiceOptions, FieldMeta, Relation } from '../types'; -import ItemsService from '../services/items'; +import { ItemsService } from '../services/items'; import { ColumnBuilder } from 'knex'; import getLocalType from '../utils/get-local-type'; import { types } from '../types'; import { ForbiddenException } from '../exceptions'; import Knex, { CreateTableBuilder } from 'knex'; -import PayloadService from '../services/payload'; +import { PayloadService } from '../services/payload'; import getDefaultValue from '../utils/get-default-value'; import cache from '../cache'; @@ -21,7 +21,7 @@ type RawField = Partial & { field: string; type: typeof types[number] }; * - Don't use items service, as this is a different case than regular collections */ -export default class FieldsService { +export class FieldsService { knex: Knex; accountability: Accountability | null; itemsService: ItemsService; diff --git a/api/src/services/files.ts b/api/src/services/files.ts index 6d226eebdb..7651aef04d 100644 --- a/api/src/services/files.ts +++ b/api/src/services/files.ts @@ -1,4 +1,4 @@ -import ItemsService from './items'; +import { ItemsService } from './items'; import storage from '../storage'; import sharp from 'sharp'; import { parse as parseICC } from 'icc'; @@ -9,7 +9,7 @@ import { AbstractServiceOptions, File, PrimaryKey } from '../types'; import { clone } from 'lodash'; import cache from '../cache'; -export default class FilesService extends ItemsService { +export class FilesService extends ItemsService { constructor(options?: AbstractServiceOptions) { super('directus_files', options); } diff --git a/api/src/services/folders.ts b/api/src/services/folders.ts index b30d56d9b8..50ce7e6a47 100644 --- a/api/src/services/folders.ts +++ b/api/src/services/folders.ts @@ -1,7 +1,7 @@ -import ItemsService from './items'; +import { ItemsService } from './items'; import { AbstractServiceOptions } from '../types'; -export default class FoldersService extends ItemsService { +export class FoldersService extends ItemsService { constructor(options?: AbstractServiceOptions) { super('directus_folders', options); } diff --git a/api/src/services/index.ts b/api/src/services/index.ts new file mode 100644 index 0000000000..b21d94fd8e --- /dev/null +++ b/api/src/services/index.ts @@ -0,0 +1,21 @@ +export * from './activity'; +export * from './assets'; +export * from './authentication'; +export * from './collections'; +export * from './extensions'; +export * from './fields'; +export * from './files'; +export * from './folders'; +export * from './items'; +export * from './meta'; +export * from './payload'; +export * from './permissions'; +export * from './presets'; +export * from './relations'; +export * from './revisions'; +export * from './roles'; +export * from './server'; +export * from './settings'; +export * from './users'; +export * from './utils'; +export * from './webhooks'; diff --git a/api/src/services/items.ts b/api/src/services/items.ts index 782d8377e8..f620c2c584 100644 --- a/api/src/services/items.ts +++ b/api/src/services/items.ts @@ -16,14 +16,14 @@ import Knex from 'knex'; import cache from '../cache'; import emitter from '../emitter'; -import PayloadService from './payload'; -import AuthorizationService from './authorization'; +import { PayloadService } from './payload'; +import { AuthorizationService } from './authorization'; import { pick, clone } from 'lodash'; import getDefaultValue from '../utils/get-default-value'; import { InvalidPayloadException } from '../exceptions'; -export default class ItemsService implements AbstractService { +export class ItemsService implements AbstractService { collection: string; knex: Knex; accountability: Accountability | null; diff --git a/api/src/services/meta.ts b/api/src/services/meta.ts index 09f1ad8b71..28afd4df6f 100644 --- a/api/src/services/meta.ts +++ b/api/src/services/meta.ts @@ -4,7 +4,7 @@ import { AbstractServiceOptions, Accountability } from '../types'; import Knex from 'knex'; import { applyFilter } from '../utils/apply-query'; -export default class MetaService { +export class MetaService { knex: Knex; accountability: Accountability | null; diff --git a/api/src/services/payload.ts b/api/src/services/payload.ts index a639d5adf5..76d8acbad1 100644 --- a/api/src/services/payload.ts +++ b/api/src/services/payload.ts @@ -3,13 +3,12 @@ * handled correctly. */ -import { FieldMeta } from '../types/field'; import argon2 from 'argon2'; import { v4 as uuidv4 } from 'uuid'; import database from '../database'; import { clone, isObject } from 'lodash'; import { Relation, Item, AbstractServiceOptions, Accountability, PrimaryKey } from '../types'; -import ItemsService from './items'; +import { ItemsService } from './items'; import { URL } from 'url'; import Knex from 'knex'; import env from '../env'; @@ -25,7 +24,7 @@ type Transformers = { ) => Promise; }; -export default class PayloadService { +export class PayloadService { accountability: Accountability | null; knex: Knex; collection: string; diff --git a/api/src/services/permissions.ts b/api/src/services/permissions.ts index 66b1d9ff49..9cd939520d 100644 --- a/api/src/services/permissions.ts +++ b/api/src/services/permissions.ts @@ -1,7 +1,7 @@ -import { AbstractServiceOptions, PermissionsAction } from '../types'; -import ItemsService from '../services/items'; +import { AbstractServiceOptions } from '../types'; +import { ItemsService } from '../services/items'; -export default class PermissionsService extends ItemsService { +export class PermissionsService extends ItemsService { constructor(options?: AbstractServiceOptions) { super('directus_permissions', options); } diff --git a/api/src/services/presets.ts b/api/src/services/presets.ts index 6902d6ee6e..d6397ad006 100644 --- a/api/src/services/presets.ts +++ b/api/src/services/presets.ts @@ -1,7 +1,7 @@ -import ItemsService from './items'; +import { ItemsService } from './items'; import { AbstractServiceOptions } from '../types'; -export default class PresetsService extends ItemsService { +export class PresetsService extends ItemsService { constructor(options?: AbstractServiceOptions) { super('directus_presets', options); } diff --git a/api/src/services/relations.ts b/api/src/services/relations.ts index 6813968da1..830f223326 100644 --- a/api/src/services/relations.ts +++ b/api/src/services/relations.ts @@ -1,11 +1,11 @@ -import ItemsService from './items'; +import { ItemsService } from './items'; import { AbstractServiceOptions } from '../types'; /** * @TODO update foreign key constraints when relations are updated */ -export default class RelationsService extends ItemsService { +export class RelationsService extends ItemsService { constructor(options?: AbstractServiceOptions) { super('directus_relations', options); } diff --git a/api/src/services/revisions.ts b/api/src/services/revisions.ts index e0a2aa2a4d..0922caad5b 100644 --- a/api/src/services/revisions.ts +++ b/api/src/services/revisions.ts @@ -1,11 +1,11 @@ -import ItemsService from './items'; +import { ItemsService } from './items'; import { AbstractServiceOptions } from '../types'; /** * @TODO only return data / delta based on permissions you have for the requested collection */ -export default class RevisionsService extends ItemsService { +export class RevisionsService extends ItemsService { constructor(options?: AbstractServiceOptions) { super('directus_revisions', options); } diff --git a/api/src/services/roles.ts b/api/src/services/roles.ts index 6fb0262a84..8a8388c5aa 100644 --- a/api/src/services/roles.ts +++ b/api/src/services/roles.ts @@ -1,10 +1,10 @@ -import ItemsService from './items'; +import { ItemsService } from './items'; import { AbstractServiceOptions, PrimaryKey } from '../types'; -import PermissionsService from './permissions'; -import UsersService from './users'; -import PresetsService from './presets'; +import { PermissionsService } from './permissions'; +import { UsersService } from './users'; +import { PresetsService } from './presets'; -export default class RolesService extends ItemsService { +export class RolesService extends ItemsService { constructor(options?: AbstractServiceOptions) { super('directus_roles', options); } diff --git a/api/src/services/server.ts b/api/src/services/server.ts index 215575f272..8c24228ae9 100644 --- a/api/src/services/server.ts +++ b/api/src/services/server.ts @@ -7,7 +7,7 @@ import { ForbiddenException } from '../exceptions'; import { version } from '../../package.json'; import macosRelease from 'macos-release'; -export default class ServerService { +export class ServerService { knex: Knex; accountability: Accountability | null; diff --git a/api/src/services/settings.ts b/api/src/services/settings.ts index 02f538ac99..2a7b069d08 100644 --- a/api/src/services/settings.ts +++ b/api/src/services/settings.ts @@ -1,7 +1,7 @@ -import ItemsService from './items'; +import { ItemsService } from './items'; import { AbstractServiceOptions } from '../types'; -export default class SettingsService extends ItemsService { +export class SettingsService extends ItemsService { constructor(options?: AbstractServiceOptions) { super('directus_settings', options); } diff --git a/api/src/services/users.ts b/api/src/services/users.ts index 4594f55a3b..56f600049d 100644 --- a/api/src/services/users.ts +++ b/api/src/services/users.ts @@ -1,5 +1,5 @@ -import AuthService from './authentication'; -import ItemsService from './items'; +import { AuthenticationService } from './authentication'; +import { ItemsService } from './items'; import jwt from 'jsonwebtoken'; import { sendInviteMail, sendPasswordResetMail } from '../mail'; import database from '../database'; @@ -10,7 +10,7 @@ import Knex from 'knex'; import env from '../env'; import cache from '../cache'; -export default class UsersService extends ItemsService { +export class UsersService extends ItemsService { knex: Knex; accountability: Accountability | null; service: ItemsService; @@ -140,7 +140,7 @@ export default class UsersService extends ItemsService { throw new InvalidPayloadException('TFA Secret is already set for this user'); } - const authService = new AuthService(); + const authService = new AuthenticationService(); const secret = authService.generateTFASecret(); await this.knex('directus_users').update({ tfa_secret: secret }).where({ id: pk }); diff --git a/api/src/services/utils.ts b/api/src/services/utils.ts index 7fc0c964dd..d7d8c2734b 100644 --- a/api/src/services/utils.ts +++ b/api/src/services/utils.ts @@ -4,7 +4,7 @@ import Knex from 'knex'; import { InvalidPayloadException, ForbiddenException } from '../exceptions'; import SchemaInspector from 'knex-schema-inspector'; -export default class UtilsService { +export class UtilsService { knex: Knex; accountability: Accountability | null; diff --git a/api/src/services/webhooks.ts b/api/src/services/webhooks.ts index bd0c29388d..eac6f23f83 100644 --- a/api/src/services/webhooks.ts +++ b/api/src/services/webhooks.ts @@ -1,4 +1,4 @@ -import ItemsService from './items'; +import { ItemsService } from './items'; import { Item, PrimaryKey, AbstractServiceOptions } from '../types'; import emitter from '../emitter'; import { ListenerFn } from 'eventemitter2'; @@ -8,7 +8,7 @@ import logger from '../logger'; let registered: { event: string; handler: ListenerFn }[] = []; -export default class WebhooksService extends ItemsService { +export class WebhooksService extends ItemsService { constructor(options?: AbstractServiceOptions) { super('directus_webhooks', options); } diff --git a/api/src/types/extensions.ts b/api/src/types/extensions.ts new file mode 100644 index 0000000000..9ae81504c3 --- /dev/null +++ b/api/src/types/extensions.ts @@ -0,0 +1,3 @@ +import { ListenerFn } from 'eventemitter2'; + +export type HookRegisterFunction = (context: any) => Record; diff --git a/api/src/types/index.ts b/api/src/types/index.ts index 52ca062a11..4af07e6c1d 100644 --- a/api/src/types/index.ts +++ b/api/src/types/index.ts @@ -3,6 +3,7 @@ export * from './activity'; export * from './assets'; export * from './ast'; export * from './collection'; +export * from './extensions'; export * from './field'; export * from './files'; export * from './items'; From 7caf429d1d23e8a826d4896859b7e0f66f45c525 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Tue, 22 Sep 2020 15:55:22 -0400 Subject: [PATCH 2/4] Add custom endpoints support --- api/src/app.ts | 10 +++- api/src/controllers/extensions.ts | 5 +- api/src/emitter.ts | 5 +- api/src/extensions.ts | 95 +++++++++++++++++++++++++++++++ api/src/services/extensions.ts | 71 ----------------------- api/src/services/index.ts | 1 - api/src/services/items.ts | 94 ++++++++++++++++++++++-------- api/src/types/extensions.ts | 15 ++++- 8 files changed, 193 insertions(+), 103 deletions(-) create mode 100644 api/src/extensions.ts delete mode 100644 api/src/services/extensions.ts diff --git a/api/src/app.ts b/api/src/app.ts index 60b09ba2ec..ab3f86acfd 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -38,9 +38,11 @@ import webhooksRouter from './controllers/webhooks'; import notFoundHandler from './controllers/not-found'; import sanitizeQuery from './middleware/sanitize-query'; import { WebhooksService } from './services/webhooks'; -import { ExtensionsService } from './services/extensions'; import { InvalidPayloadException } from './exceptions'; +import { registerExtensions } from './extensions'; +import emitter from './emitter'; + validateEnv(['KEY', 'SECRET']); const app = express(); @@ -123,9 +125,11 @@ const webhooksService = new WebhooksService(); webhooksService.register(); // Register custom hooks / endpoints -const extensionsService = new ExtensionsService(); -extensionsService.register(customRouter); +registerExtensions(customRouter); track('serverStarted'); +emitter.emitAsync('server.started') + .catch((err) => logger.warn(err)); + export default app; diff --git a/api/src/controllers/extensions.ts b/api/src/controllers/extensions.ts index 5347b53d11..a1d3eac32a 100644 --- a/api/src/controllers/extensions.ts +++ b/api/src/controllers/extensions.ts @@ -1,7 +1,7 @@ import express, { Router } from 'express'; import asyncHandler from 'express-async-handler'; import { RouteNotFoundException } from '../exceptions'; -import { ExtensionsService } from '../services/extensions'; +import { listExtensions } from '../extensions'; import env from '../env'; const router = Router(); @@ -12,14 +12,13 @@ router.use(express.static(extensionsPath)); router.get( '/:type', asyncHandler(async (req, res, next) => { - const service = new ExtensionsService(); const typeAllowList = ['interfaces', 'layouts', 'displays', 'modules']; if (typeAllowList.includes(req.params.type) === false) { throw new RouteNotFoundException(req.path); } - const extensions = await service.listExtensions(req.params.type); + const extensions = await listExtensions(req.params.type); res.locals.payload = { data: extensions, diff --git a/api/src/emitter.ts b/api/src/emitter.ts index b6a984e212..323dbb178a 100644 --- a/api/src/emitter.ts +++ b/api/src/emitter.ts @@ -1,5 +1,8 @@ import { EventEmitter2 } from 'eventemitter2'; -const emitter = new EventEmitter2({ wildcard: true, verboseMemoryLeak: true }); +const emitter = new EventEmitter2({ wildcard: true, verboseMemoryLeak: true, delimiter: '.' }); + +// No-op function to ensure we never end up with no data +emitter.on('item.*.*.before', input => input); export default emitter; diff --git a/api/src/extensions.ts b/api/src/extensions.ts new file mode 100644 index 0000000000..ac6cabb607 --- /dev/null +++ b/api/src/extensions.ts @@ -0,0 +1,95 @@ +import listFolders from './utils/list-folders'; +import path from 'path'; +import env from './env'; +import { ServiceUnavailableException } from './exceptions'; +import express, { Router } from 'express'; +import emitter from './emitter'; +import logger from './logger'; +import { HookRegisterFunction, EndpointRegisterFunction } from './types'; + +import * as exceptions from './exceptions'; +import * as services from './services'; +import database from './database'; + +export async function listExtensions(type: string) { + const extensionsPath = env.EXTENSIONS_PATH as string; + const location = path.join(extensionsPath, type); + + try { + return await listFolders(location); + } catch (err) { + if (err.code === 'ENOENT') { + throw new ServiceUnavailableException(`Extension folder "extensions/${type}" couldn't be opened`, { + service: 'extensions', + }); + } + throw err; + } +} + +export async function registerExtensions(router: Router) { + let hooks: string[] = []; + let endpoints: string[] = []; + + try { + hooks = await listExtensions('hooks'); + registerHooks(hooks); + } catch (err) { + logger.warn(err); + } + + try { + endpoints = await listExtensions('endpoints'); + registerEndpoints(endpoints, router); + } catch (err) { + logger.warn(err); + } + + console.log(hooks, endpoints); +} + +function registerHooks(hooks: string[]) { + const extensionsPath = env.EXTENSIONS_PATH as string; + + for (const hook of hooks) { + try { + registerHook(hook); + } catch (error) { + logger.warn(`Couldn't register hook "${hook}"`); + logger.info(error); + } + } + + function registerHook(hook: string) { + const hookPath = path.resolve(extensionsPath, 'hooks', hook, 'index.js'); + const register: HookRegisterFunction = require(hookPath); + const events = register({ services, exceptions, env, database }); + + for (const [event, handler] of Object.entries(events)) { + emitter.on(event, handler); + } + } +} + +function registerEndpoints(endpoints: string[], router: Router) { + const extensionsPath = env.EXTENSIONS_PATH as string; + + for (const endpoint of endpoints) { + try { + registerEndpoint(endpoint); + } catch (error) { + logger.warn(`Couldn't register endpoint "${endpoint}"`); + logger.info(error); + } + } + + function registerEndpoint(endpoint: string) { + const endpointPath = path.resolve(extensionsPath, 'endpoints', endpoint, 'index.js'); + const register: EndpointRegisterFunction = require(endpointPath); + + const scopedRouter = express.Router(); + router.use(`/${endpoint}/`, scopedRouter); + + register(scopedRouter, { services, exceptions, env, database }); + } +} diff --git a/api/src/services/extensions.ts b/api/src/services/extensions.ts deleted file mode 100644 index f253bad00c..0000000000 --- a/api/src/services/extensions.ts +++ /dev/null @@ -1,71 +0,0 @@ -import listFolders from '../utils/list-folders'; -import path from 'path'; -import env from '../env'; -import { ServiceUnavailableException } from '../exceptions'; -import { Router } from 'express'; -import emitter from '../emitter'; -import logger from '../logger'; -import { HookRegisterFunction } from '../types'; - -export class ExtensionsService { - registeredHooks: Record = {}; - - async listExtensions(type: string) { - const extensionsPath = env.EXTENSIONS_PATH as string; - const location = path.join(extensionsPath, type); - - try { - return await listFolders(location); - } catch (err) { - if (err.code === 'ENOENT') { - throw new ServiceUnavailableException(`Extension folder "extensions/${type}" couldn't be opened`, { - service: 'extensions', - }); - } - throw err; - } - } - - async register(router: Router) { - let hooks: string[] = []; - let endpoints: string[] = []; - - try { - hooks = await this.listExtensions('hooks'); - this.registerHooks(hooks); - } catch (err) { - logger.warn(err); - } - - try { - endpoints = await this.listExtensions('endpoints'); - } catch (err) { - logger.warn(err); - } - - console.log(hooks, endpoints); - } - - registerHooks(hooks: string[]) { - const extensionsPath = env.EXTENSIONS_PATH as string; - - for (const hook of hooks) { - try { - registerHook(hook); - } catch (error) { - logger.warn(`Couldn't register hook "${hook}"`); - logger.info(error); - } - } - - function registerHook(hook: string) { - const hookPath = path.resolve(extensionsPath, 'hooks', hook, 'index.js'); - const register: HookRegisterFunction = require(hookPath); - const events = register(null); - - for (const [event, handler] of Object.entries(events)) { - emitter.on(event, handler); - } - } - } -} diff --git a/api/src/services/index.ts b/api/src/services/index.ts index b21d94fd8e..f9f5e3b502 100644 --- a/api/src/services/index.ts +++ b/api/src/services/index.ts @@ -2,7 +2,6 @@ export * from './activity'; export * from './assets'; export * from './authentication'; export * from './collections'; -export * from './extensions'; export * from './fields'; export * from './files'; export * from './folders'; diff --git a/api/src/services/items.ts b/api/src/services/items.ts index f620c2c584..ad4da99f37 100644 --- a/api/src/services/items.ts +++ b/api/src/services/items.ts @@ -15,6 +15,7 @@ import { import Knex from 'knex'; import cache from '../cache'; import emitter from '../emitter'; +import logger from '../logger'; import { PayloadService } from './payload'; import { AuthorizationService } from './authorization'; @@ -51,10 +52,31 @@ export class ItemsService implements AbstractService { knex: trx, }); - const authorizationService = new AuthorizationService({ - accountability: this.accountability, - knex: trx, - }); + if (this.collection.startsWith('directus_') === false) { + const customProcessed = await emitter.emitAsync(`item.create.${this.collection}.before`, payloads, { + event: `item.create.${this.collection}.before`, + accountability: this.accountability, + collection: this.collection, + item: null, + action: 'create', + payload: payloads, + }); + + payloads = customProcessed[customProcessed.length - 1]; + } + + if (this.accountability && this.accountability.admin !== true) { + const authorizationService = new AuthorizationService({ + accountability: this.accountability, + knex: trx, + }); + + payloads = await authorizationService.validatePayload( + 'create', + this.collection, + payloads + ); + } payloads = await payloadService.processM2O(payloads); @@ -70,14 +92,6 @@ export class ItemsService implements AbstractService { payloadsWithoutAliases ); - if (this.accountability && this.accountability.admin !== true) { - payloads = await authorizationService.validatePayload( - 'create', - this.collection, - payloads - ); - } - const primaryKeys: PrimaryKey[] = []; for (const payloadWithoutAlias of payloadsWithoutAliases) { @@ -151,12 +165,16 @@ export class ItemsService implements AbstractService { await cache.clear(); } - emitter.emitAsync(`item.create.${this.collection}`, { - collection: this.collection, - item: primaryKeys, - action: 'create', - payload: payloads, - }); + if (this.collection.startsWith('directus_') === false) { + emitter.emitAsync(`item.create.${this.collection}`, { + event: `item.create.${this.collection}`, + accountability: this.accountability, + collection: this.collection, + item: primaryKeys, + action: 'create', + payload: payloads, + }).catch(err => logger.warn(err)); + } return primaryKeys; }); @@ -221,7 +239,6 @@ export class ItemsService implements AbstractService { const records = await runAST(ast, { knex: this.knex }); return Array.isArray(key) ? records : records[0]; - return [] as Item; } update(data: Partial, keys: PrimaryKey[]): Promise; @@ -241,6 +258,19 @@ export class ItemsService implements AbstractService { let payload = clone(data); + if (this.collection.startsWith('directus_') === false) { + const customProcessed = await emitter.emitAsync(`item.update.${this.collection}.before`, payload, { + event: `item.update.${this.collection}.before`, + accountability: this.accountability, + collection: this.collection, + item: null, + action: 'update', + payload, + }); + + payload = customProcessed[customProcessed.length - 1]; + } + if (this.accountability && this.accountability.admin !== true) { const authorizationService = new AuthorizationService({ accountability: this.accountability, @@ -325,11 +355,13 @@ export class ItemsService implements AbstractService { } emitter.emitAsync(`item.update.${this.collection}`, { + event: `item.update.${this.collection}`, + accountability: this.accountability, collection: this.collection, item: key, action: 'update', payload, - }); + }).catch(err => logger.warn(err)); return key; } @@ -347,9 +379,13 @@ export class ItemsService implements AbstractService { for (const single of payloads as Partial[]) { let payload = clone(single); const key = payload[primaryKeyField]; - if (!key) + + if (!key) { throw new InvalidPayloadException('Primary key is missing in update payload.'); + } + keys.push(key); + await itemsService.update(payload, key); } }); @@ -372,6 +408,15 @@ export class ItemsService implements AbstractService { await authorizationService.checkAccess('delete', this.collection, key); } + await emitter.emitAsync(`item.delete.${this.collection}.before`, { + event: `item.update.${this.collection}`, + accountability: this.accountability, + collection: this.collection, + item: keys, + action: 'delete', + payload: null, + }); + await this.knex.transaction(async (trx) => { await trx(this.collection).whereIn(primaryKeyField, keys).delete(); @@ -394,10 +439,13 @@ export class ItemsService implements AbstractService { } emitter.emitAsync(`item.delete.${this.collection}`, { + event: `item.delete.${this.collection}`, + accountability: this.accountability, collection: this.collection, - item: key, + item: keys, action: 'delete', - }); + payload: null, + }).catch(err => logger.warn(err)); return key; } diff --git a/api/src/types/extensions.ts b/api/src/types/extensions.ts index 9ae81504c3..0107e1c2af 100644 --- a/api/src/types/extensions.ts +++ b/api/src/types/extensions.ts @@ -1,3 +1,16 @@ import { ListenerFn } from 'eventemitter2'; +import * as services from '../services'; +import * as exceptions from '../exceptions'; +import env from '../env'; +import Knex from 'knex'; +import { Router } from 'express'; -export type HookRegisterFunction = (context: any) => Record; +type ExtensionContext = { + services: typeof services, + exceptions: typeof exceptions, + database: Knex, + env: typeof env, +}; + +export type HookRegisterFunction = (context: ExtensionContext) => Record; +export type EndpointRegisterFunction = (router: Router, context: ExtensionContext) => void; From e703d1e92806f8317df5c8b8ff923febc7a9eab7 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Tue, 22 Sep 2020 16:09:02 -0400 Subject: [PATCH 3/4] Add docs --- .../creating-a-custom-api-endpoint.md | 42 +++++++++++- .../extensions/creating-a-custom-api-hook.md | 67 ++++++++++++++++++- 2 files changed, 107 insertions(+), 2 deletions(-) diff --git a/docs/guides/extensions/creating-a-custom-api-endpoint.md b/docs/guides/extensions/creating-a-custom-api-endpoint.md index 59a7984bf0..fc1c8cf2ff 100644 --- a/docs/guides/extensions/creating-a-custom-api-endpoint.md +++ b/docs/guides/extensions/creating-a-custom-api-endpoint.md @@ -1,3 +1,43 @@ # Creating a Custom API Endpoint -> TK +Custom endpoints are dynamically loaded from your configured extensions folder. + +Custom endpoints are registered using a registration function: + +```js +// extensions/endpoints/my-endpoint/index.js + +module.exports = function registerEndpoint(router) { + router.get('/', (req, res) => res.send('Hello, World!')); +} +``` + +The `registerEndpoint` function receives two parameters: `router` and `context`. Router is an express Router +instance that's scoped to `/custom/`. `context` holds the following properties: + +* `services` — All API interal services +* `exceptions` — API exception objects that can be used to throw "proper" errors +* `database` — Knex instance that's connected to the current DB +* `env` — Parsed environment variables + +--- + +## Full example: + +```js +// extensions/endpoints/recipes/index.js + +module.exports = function registerEndpoint(router, { services, exceptions }) { + const { ItemsService } = services; + const { ServiceUnavailableException } = exceptions; + + const recipeService = new ItemsService('recipes'); + + router.get('/', (req, res) => { + recipeService + .readByQuery({ sort: 'name', fields: '*' }) + .then(results => res.json(results)) + .catch(error => { throw new ServiceUnavailableException(error.message) }); + }); +} +``` diff --git a/docs/guides/extensions/creating-a-custom-api-hook.md b/docs/guides/extensions/creating-a-custom-api-hook.md index 126800be87..28962e7049 100644 --- a/docs/guides/extensions/creating-a-custom-api-hook.md +++ b/docs/guides/extensions/creating-a-custom-api-hook.md @@ -1,3 +1,68 @@ # Creating a Custom API Hook -> TK +Custom hooks are dynamically loaded from your configured extensions folder. + +Custom hooks are registered using a registration function: + +```js +// extensions/hooks/my-hook/index.js + +module.exports = function registerHook() { + return { + 'item.create.articles': function() { + axios.post('http://example.com/webhook'); + } + } +} +``` + +Register function return an object with key = event, value = handler function. + +The `registerHook` function receives one parameter: `context`. `context` holds the following properties: + +* `services` — All API interal services +* `exceptions` — API exception objects that can be used to throw "proper" errors +* `database` — Knex instance that's connected to the current DB +* `env` — Parsed environment variables + +Each handler function gets a `context` parameter with the following properties: + +* `event` — Full event string +* `accountability` — Information about the current user +* `collection` — Collection that's being modified +* `item` — Primary key(s) of the item(s) that's being modified +* `action` — Action that's performed +* `payload` — Payload of the request + +Events that are prefixed with `.before` run before the event is completed, and are blocking. These allow you to check / modify the payload before it's processed. + +--- + +## Full example: + +```js +// extensions/hooks/sync-with-external/index.js + +module.exports = function registerHook({ services, exceptions }) { + const { ServiceUnavailableException, ForbiddenException } = exceptions; + + return { + // Force everything to be admin only at all times + 'item.*.*': async function({ item, accountability }) { + if (accountability.admin !== true) throw new ForbiddenException(); + }, + // Sync with external recipes service, cancel creation on failure + 'item.recipes.create.before': async function(input) { + try { + await axios.post('https://example.com/recipes', input); + } catch (error) { + throw new ServiceUnavailableException(error); + } + + input[0].syncedWithExample = true; + + return input; + } + } +} +``` From 85ca7b5d176aa83bc3908e26e3445d4c87f40028 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Tue, 22 Sep 2020 16:11:28 -0400 Subject: [PATCH 4/4] Run prettier --- api/src/app.ts | 13 ++- api/src/cache.ts | 15 +-- api/src/cli/commands/database/migrate.ts | 2 +- api/src/cli/index.ts | 17 +++- api/src/controllers/activity.ts | 10 +- api/src/controllers/files.ts | 6 +- api/src/controllers/folders.ts | 2 +- api/src/controllers/items.ts | 14 +-- api/src/controllers/revisions.ts | 2 +- api/src/controllers/roles.ts | 2 +- api/src/controllers/users.ts | 6 +- api/src/controllers/webhooks.ts | 2 +- api/src/database/migrations/run.ts | 21 +++-- api/src/database/run-ast.ts | 6 +- api/src/database/seeds/run.ts | 30 ++++-- api/src/emitter.ts | 2 +- api/src/extensions.ts | 9 +- api/src/middleware/respond.ts | 19 ++-- api/src/middleware/sanitize-query.ts | 6 +- api/src/services/items.ts | 111 +++++++++++++---------- api/src/services/payload.ts | 7 +- api/src/types/extensions.ts | 8 +- api/src/utils/get-ast-from-query.ts | 2 +- api/src/utils/get-cache-key.ts | 6 +- api/src/utils/track.ts | 14 ++- 25 files changed, 203 insertions(+), 129 deletions(-) diff --git a/api/src/app.ts b/api/src/app.ts index ab3f86acfd..c925dfa324 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -55,13 +55,13 @@ app.set('trust proxy', true); app.use(expressLogger({ logger })); app.use((req, res, next) => { - bodyParser.json()(req, res, err => { - if (err) { + bodyParser.json()(req, res, (err) => { + if (err) { return next(new InvalidPayloadException(err.message)); - } + } - return next(); - }); + return next(); + }); }); app.use(bodyParser.json()); @@ -129,7 +129,6 @@ registerExtensions(customRouter); track('serverStarted'); -emitter.emitAsync('server.started') - .catch((err) => logger.warn(err)); +emitter.emitAsync('server.started').catch((err) => logger.warn(err)); export default app; diff --git a/api/src/cache.ts b/api/src/cache.ts index 2a1a230e66..3bcc494dc2 100644 --- a/api/src/cache.ts +++ b/api/src/cache.ts @@ -27,18 +27,19 @@ function getKevyInstance() { } } -function getConfig( - store: 'memory' | 'redis' | 'memcache' = 'memory' -): Options { - const config: Options = { namespace: env.CACHE_NAMESPACE, ttl: ms(env.CACHE_TTL as string) }; +function getConfig(store: 'memory' | 'redis' | 'memcache' = 'memory'): Options { + const config: Options = { + namespace: env.CACHE_NAMESPACE, + ttl: ms(env.CACHE_TTL as string), + }; if (store === 'redis') { const Redis = require('ioredis'); const KeyvRedis = require('@keyv/redis'); - config.store = new KeyvRedis(new Redis( - env.CACHE_REDIS || getConfigFromEnv('CACHE_REDIS_') - )); + config.store = new KeyvRedis( + new Redis(env.CACHE_REDIS || getConfigFromEnv('CACHE_REDIS_')) + ); } if (store === 'memcache') { diff --git a/api/src/cli/commands/database/migrate.ts b/api/src/cli/commands/database/migrate.ts index 2bbaa49c71..0629b253ce 100644 --- a/api/src/cli/commands/database/migrate.ts +++ b/api/src/cli/commands/database/migrate.ts @@ -5,7 +5,7 @@ export default async function migrate(direction: 'latest' | 'up' | 'down') { try { await run(database, direction); - } catch(err) { + } catch (err) { console.log(err); process.exit(1); } finally { diff --git a/api/src/cli/index.ts b/api/src/cli/index.ts index a7c20c52f6..3331dc3415 100644 --- a/api/src/cli/index.ts +++ b/api/src/cli/index.ts @@ -19,9 +19,18 @@ program.command('init').description('Create a new Directus Project').action(init const dbCommand = program.command('database'); dbCommand.command('install').description('Install the database').action(dbInstall); -dbCommand.command('migrate:latest').description('Upgrade the database').action(() => dbMigrate('latest')); -dbCommand.command('migrate:up').description('Upgrade the database').action(() => dbMigrate('up')); -dbCommand.command('migrate:down').description('Downgrade the database').action(() => dbMigrate('down')); +dbCommand + .command('migrate:latest') + .description('Upgrade the database') + .action(() => dbMigrate('latest')); +dbCommand + .command('migrate:up') + .description('Upgrade the database') + .action(() => dbMigrate('up')); +dbCommand + .command('migrate:down') + .description('Downgrade the database') + .action(() => dbMigrate('down')); const usersCommand = program.command('users'); usersCommand @@ -34,7 +43,7 @@ usersCommand const rolesCommand = program.command('roles'); rolesCommand -.command('create') + .command('create') .storeOptionsAsProperties(false) .passCommandToAction(false) .description('Create a new role') diff --git a/api/src/controllers/activity.ts b/api/src/controllers/activity.ts index 780f183249..774b0293ef 100644 --- a/api/src/controllers/activity.ts +++ b/api/src/controllers/activity.ts @@ -24,7 +24,7 @@ router.get( }; return next(); - }), + }) ); router.get( @@ -38,7 +38,7 @@ router.get( }; return next(); - }), + }) ); router.post( @@ -69,7 +69,7 @@ router.post( } return next(); - }), + }) ); router.patch( @@ -93,7 +93,7 @@ router.patch( } return next(); - }), + }) ); router.delete( @@ -103,7 +103,7 @@ router.delete( await service.delete(req.params.pk); return next(); - }), + }) ); export default router; diff --git a/api/src/controllers/files.ts b/api/src/controllers/files.ts index 86cae96ec1..673f3daf91 100644 --- a/api/src/controllers/files.ts +++ b/api/src/controllers/files.ts @@ -112,7 +112,9 @@ router.post( try { const record = await service.readByKey(keys as any, req.sanitizedQuery); - res.locals.payload = { data: res.locals.savedFiles.length === 1 ? record[0] : record || null }; + res.locals.payload = { + data: res.locals.savedFiles.length === 1 ? record[0] : record || null, + }; } catch (error) { if (error instanceof ForbiddenException) { return next(); @@ -127,7 +129,7 @@ router.post( const importSchema = Joi.object({ url: Joi.string().required(), - data: Joi.object() + data: Joi.object(), }); router.post( diff --git a/api/src/controllers/folders.ts b/api/src/controllers/folders.ts index 84e2a14ba8..660611cc55 100644 --- a/api/src/controllers/folders.ts +++ b/api/src/controllers/folders.ts @@ -1,6 +1,6 @@ import express from 'express'; import asyncHandler from 'express-async-handler'; -import {FoldersService, MetaService} from '../services'; +import { FoldersService, MetaService } from '../services'; import { ForbiddenException } from '../exceptions'; import useCollection from '../middleware/use-collection'; diff --git a/api/src/controllers/items.ts b/api/src/controllers/items.ts index 2af0809fb5..5d31d60eb9 100644 --- a/api/src/controllers/items.ts +++ b/api/src/controllers/items.ts @@ -1,7 +1,7 @@ import express from 'express'; import asyncHandler from 'express-async-handler'; import collectionExists from '../middleware/collection-exists'; -import { ItemsService, MetaService} from '../services'; +import { ItemsService, MetaService } from '../services'; import { RouteNotFoundException, ForbiddenException } from '../exceptions'; const router = express.Router(); @@ -29,7 +29,7 @@ router.post( } return next(); - }), + }) ); router.get( @@ -50,7 +50,7 @@ router.get( data: records || null, }; return next(); - }), + }) ); router.get( @@ -69,7 +69,7 @@ router.get( data: result || null, }; return next(); - }), + }) ); router.patch( @@ -100,7 +100,7 @@ router.patch( } return next(); - }), + }) ); router.patch( @@ -128,7 +128,7 @@ router.patch( } return next(); - }), + }) ); router.delete( @@ -139,7 +139,7 @@ router.delete( const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk; await service.delete(pk as any); return next(); - }), + }) ); export default router; diff --git a/api/src/controllers/revisions.ts b/api/src/controllers/revisions.ts index a583b5fb79..310315ba9e 100644 --- a/api/src/controllers/revisions.ts +++ b/api/src/controllers/revisions.ts @@ -1,6 +1,6 @@ import express from 'express'; import asyncHandler from 'express-async-handler'; -import { RevisionsService, MetaService} from '../services'; +import { RevisionsService, MetaService } from '../services'; import useCollection from '../middleware/use-collection'; const router = express.Router(); diff --git a/api/src/controllers/roles.ts b/api/src/controllers/roles.ts index 45e049a88b..8413681c02 100644 --- a/api/src/controllers/roles.ts +++ b/api/src/controllers/roles.ts @@ -1,6 +1,6 @@ import express from 'express'; import asyncHandler from 'express-async-handler'; -import { RolesService, MetaService} from '../services'; +import { RolesService, MetaService } from '../services'; import { ForbiddenException } from '../exceptions'; import useCollection from '../middleware/use-collection'; diff --git a/api/src/controllers/users.ts b/api/src/controllers/users.ts index dc1f8237d5..502bda6253 100644 --- a/api/src/controllers/users.ts +++ b/api/src/controllers/users.ts @@ -1,7 +1,11 @@ import express from 'express'; import asyncHandler from 'express-async-handler'; import Joi from 'joi'; -import { InvalidPayloadException, InvalidCredentialsException, ForbiddenException } from '../exceptions'; +import { + InvalidPayloadException, + InvalidCredentialsException, + ForbiddenException, +} from '../exceptions'; import { UsersService, MetaService, AuthenticationService } from '../services'; import useCollection from '../middleware/use-collection'; diff --git a/api/src/controllers/webhooks.ts b/api/src/controllers/webhooks.ts index 0e05f051c9..de8a6a76a8 100644 --- a/api/src/controllers/webhooks.ts +++ b/api/src/controllers/webhooks.ts @@ -1,6 +1,6 @@ import express from 'express'; import asyncHandler from 'express-async-handler'; -import { WebhooksService, MetaService} from '../services'; +import { WebhooksService, MetaService } from '../services'; import { ForbiddenException } from '../exceptions'; import useCollection from '../middleware/use-collection'; diff --git a/api/src/database/migrations/run.ts b/api/src/database/migrations/run.ts index 7f5c77df24..6c3fece8f2 100644 --- a/api/src/database/migrations/run.ts +++ b/api/src/database/migrations/run.ts @@ -7,13 +7,16 @@ type Migration = { version: string; name: string; timestamp: Date; -} +}; export default async function run(database: Knex, direction: 'up' | 'down' | 'latest') { let migrationFiles = await fse.readdir(__dirname); migrationFiles = migrationFiles.filter((file: string) => file !== 'run.ts'); - const completedMigrations = await database.select('*').from('directus_migrations').orderBy('version'); + const completedMigrations = await database + .select('*') + .from('directus_migrations') + .orderBy('version'); const migrations = migrationFiles.map((migrationFile) => { const version = migrationFile.split('-')[0]; @@ -24,7 +27,7 @@ export default async function run(database: Knex, direction: 'up' | 'down' | 'la file: migrationFile, version, name, - completed + completed, }; }); @@ -51,7 +54,9 @@ export default async function run(database: Knex, direction: 'up' | 'down' | 'la const { up } = require(path.join(__dirname, nextVersion.file)); await up(database); - await database.insert({ version: nextVersion.version, name: nextVersion.name }).into('directus_migrations'); + await database + .insert({ version: nextVersion.version, name: nextVersion.name }) + .into('directus_migrations'); } async function down() { @@ -61,7 +66,9 @@ export default async function run(database: Knex, direction: 'up' | 'down' | 'la throw Error('Nothing to downgrade'); } - const migration = migrations.find((migration) => migration.version === currentVersion.version); + const migration = migrations.find( + (migration) => migration.version === currentVersion.version + ); if (!migration) { throw new Error('Couldnt find migration'); @@ -77,7 +84,9 @@ export default async function run(database: Knex, direction: 'up' | 'down' | 'la if (migration.completed === false) { const { up } = require(path.join(__dirname, migration.file)); await up(database); - await database.insert({ version: migration.version, name: migration.name }).into('directus_migrations'); + await database + .insert({ version: migration.version, name: migration.name }) + .into('directus_migrations'); } } } diff --git a/api/src/database/run-ast.ts b/api/src/database/run-ast.ts index af5f244c89..a35a9ee65b 100644 --- a/api/src/database/run-ast.ts +++ b/api/src/database/run-ast.ts @@ -8,9 +8,9 @@ import applyQuery from '../utils/apply-query'; import Knex from 'knex'; type RunASTOptions = { - query?: AST['query'], - knex?: Knex -} + query?: AST['query']; + knex?: Knex; +}; export default async function runAST(ast: AST, options?: RunASTOptions) { const query = options?.query || ast.query; diff --git a/api/src/database/seeds/run.ts b/api/src/database/seeds/run.ts index 108bb72427..3544183e6c 100644 --- a/api/src/database/seeds/run.ts +++ b/api/src/database/seeds/run.ts @@ -22,14 +22,14 @@ type TableSeed = { column: string; }; }; - } -} + }; +}; type RowSeed = { table: string; defaults: Record; data: Record[]; -} +}; type FieldSeed = { table: string; @@ -50,7 +50,7 @@ type FieldSeed = { translation: Record | null; note: string | null; }[]; -} +}; export default async function runSeed(database: Knex) { const exists = await database.schema.hasTable('directus_collections'); @@ -68,10 +68,13 @@ async function createTables(database: Knex) { const tableSeeds = await fse.readdir(path.resolve(__dirname, './01-tables/')); for (const tableSeedFile of tableSeeds) { - const yamlRaw = await fse.readFile(path.resolve(__dirname, './01-tables', tableSeedFile), 'utf8'); + const yamlRaw = await fse.readFile( + path.resolve(__dirname, './01-tables', tableSeedFile), + 'utf8' + ); const seedData = yaml.safeLoad(yamlRaw) as TableSeed; - await database.schema.createTable(seedData.table, tableBuilder => { + await database.schema.createTable(seedData.table, (tableBuilder) => { for (const [columnName, columnInfo] of Object.entries(seedData.columns)) { let column: ColumnBuilder; @@ -129,7 +132,10 @@ async function insertRows(database: Knex) { const rowSeeds = await fse.readdir(path.resolve(__dirname, './02-rows/')); for (const rowSeedFile of rowSeeds) { - const yamlRaw = await fse.readFile(path.resolve(__dirname, './02-rows', rowSeedFile), 'utf8'); + const yamlRaw = await fse.readFile( + path.resolve(__dirname, './02-rows', rowSeedFile), + 'utf8' + ); const seedData = yaml.safeLoad(yamlRaw) as RowSeed; const dataWithDefaults = seedData.data.map((row) => { @@ -149,11 +155,17 @@ async function insertRows(database: Knex) { async function insertFields(database: Knex) { const fieldSeeds = await fse.readdir(path.resolve(__dirname, './03-fields/')); - const defaultsYaml = await fse.readFile(path.resolve(__dirname, './03-fields/_defaults.yaml'), 'utf8'); + const defaultsYaml = await fse.readFile( + path.resolve(__dirname, './03-fields/_defaults.yaml'), + 'utf8' + ); const defaults = yaml.safeLoad(defaultsYaml) as FieldSeed; for (const fieldSeedFile of fieldSeeds) { - const yamlRaw = await fse.readFile(path.resolve(__dirname, './03-fields', fieldSeedFile), 'utf8'); + const yamlRaw = await fse.readFile( + path.resolve(__dirname, './03-fields', fieldSeedFile), + 'utf8' + ); const seedData = yaml.safeLoad(yamlRaw) as FieldSeed; if (fieldSeedFile === '_defaults.yaml') { diff --git a/api/src/emitter.ts b/api/src/emitter.ts index 323dbb178a..ac126496da 100644 --- a/api/src/emitter.ts +++ b/api/src/emitter.ts @@ -3,6 +3,6 @@ import { EventEmitter2 } from 'eventemitter2'; const emitter = new EventEmitter2({ wildcard: true, verboseMemoryLeak: true, delimiter: '.' }); // No-op function to ensure we never end up with no data -emitter.on('item.*.*.before', input => input); +emitter.on('item.*.*.before', (input) => input); export default emitter; diff --git a/api/src/extensions.ts b/api/src/extensions.ts index ac6cabb607..30131bac28 100644 --- a/api/src/extensions.ts +++ b/api/src/extensions.ts @@ -19,9 +19,12 @@ export async function listExtensions(type: string) { return await listFolders(location); } catch (err) { if (err.code === 'ENOENT') { - throw new ServiceUnavailableException(`Extension folder "extensions/${type}" couldn't be opened`, { - service: 'extensions', - }); + throw new ServiceUnavailableException( + `Extension folder "extensions/${type}" couldn't be opened`, + { + service: 'extensions', + } + ); } throw err; } diff --git a/api/src/middleware/respond.ts b/api/src/middleware/respond.ts index a49c25ad95..2ea4162913 100644 --- a/api/src/middleware/respond.ts +++ b/api/src/middleware/respond.ts @@ -1,13 +1,18 @@ -import { RequestHandler } from "express"; -import asyncHandler from "express-async-handler"; -import env from "../env"; -import { getCacheKey } from "../utils/get-cache-key"; +import { RequestHandler } from 'express'; +import asyncHandler from 'express-async-handler'; +import env from '../env'; +import { getCacheKey } from '../utils/get-cache-key'; import cache from '../cache'; import { Transform, transforms } from 'json2csv'; import { PassThrough } from 'stream'; export const respond: RequestHandler = asyncHandler(async (req, res) => { - if (req.method.toLowerCase() === 'get' && env.CACHE_ENABLED === true && cache && !req.sanitizedQuery.export) { + if ( + req.method.toLowerCase() === 'get' && + env.CACHE_ENABLED === true && + cache && + !req.sanitizedQuery.export + ) { const key = getCacheKey(req); await cache.set(key, res.locals.payload); } @@ -34,7 +39,9 @@ export const respond: RequestHandler = asyncHandler(async (req, res) => { res.set('Content-Type', 'text/csv'); const stream = new PassThrough(); stream.end(Buffer.from(JSON.stringify(res.locals.payload.data), 'utf-8')); - const json2csv = new Transform({ transforms: [transforms.flatten({ separator: '.' })] }); + const json2csv = new Transform({ + transforms: [transforms.flatten({ separator: '.' })], + }); return stream.pipe(json2csv).pipe(res); } } diff --git a/api/src/middleware/sanitize-query.ts b/api/src/middleware/sanitize-query.ts index 58031f6523..ee2858c5bd 100644 --- a/api/src/middleware/sanitize-query.ts +++ b/api/src/middleware/sanitize-query.ts @@ -56,7 +56,11 @@ const sanitizeQuery: RequestHandler = (req, res, next) => { query.search = req.query.search; } - if (req.query.export && typeof req.query.export === 'string' && ['json', 'csv'].includes(req.query.export)) { + if ( + req.query.export && + typeof req.query.export === 'string' && + ['json', 'csv'].includes(req.query.export) + ) { query.export = req.query.export as 'json' | 'csv'; } diff --git a/api/src/services/items.ts b/api/src/services/items.ts index ad4da99f37..bbac3a52bb 100644 --- a/api/src/services/items.ts +++ b/api/src/services/items.ts @@ -53,14 +53,18 @@ export class ItemsService implements AbstractService { }); if (this.collection.startsWith('directus_') === false) { - const customProcessed = await emitter.emitAsync(`item.create.${this.collection}.before`, payloads, { - event: `item.create.${this.collection}.before`, - accountability: this.accountability, - collection: this.collection, - item: null, - action: 'create', - payload: payloads, - }); + const customProcessed = await emitter.emitAsync( + `item.create.${this.collection}.before`, + payloads, + { + event: `item.create.${this.collection}.before`, + accountability: this.accountability, + collection: this.collection, + item: null, + action: 'create', + payload: payloads, + } + ); payloads = customProcessed[customProcessed.length - 1]; } @@ -166,14 +170,16 @@ export class ItemsService implements AbstractService { } if (this.collection.startsWith('directus_') === false) { - emitter.emitAsync(`item.create.${this.collection}`, { - event: `item.create.${this.collection}`, - accountability: this.accountability, - collection: this.collection, - item: primaryKeys, - action: 'create', - payload: payloads, - }).catch(err => logger.warn(err)); + emitter + .emitAsync(`item.create.${this.collection}`, { + event: `item.create.${this.collection}`, + accountability: this.accountability, + collection: this.collection, + item: primaryKeys, + action: 'create', + payload: payloads, + }) + .catch((err) => logger.warn(err)); } return primaryKeys; @@ -186,7 +192,10 @@ export class ItemsService implements AbstractService { const authorizationService = new AuthorizationService({ accountability: this.accountability, }); - let ast = await getASTFromQuery(this.collection, query, { accountability: this.accountability, knex: this.knex }); + let ast = await getASTFromQuery(this.collection, query, { + accountability: this.accountability, + knex: this.knex, + }); if (this.accountability && this.accountability.admin !== true) { ast = await authorizationService.processAST(ast); @@ -219,15 +228,11 @@ export class ItemsService implements AbstractService { }, }; - let ast = await getASTFromQuery( - this.collection, - queryWithFilter, - { - accountability: this.accountability, - action, - knex: this.knex, - } - ); + let ast = await getASTFromQuery(this.collection, queryWithFilter, { + accountability: this.accountability, + action, + knex: this.knex, + }); if (this.accountability && this.accountability.admin !== true) { const authorizationService = new AuthorizationService({ @@ -259,14 +264,18 @@ export class ItemsService implements AbstractService { let payload = clone(data); if (this.collection.startsWith('directus_') === false) { - const customProcessed = await emitter.emitAsync(`item.update.${this.collection}.before`, payload, { - event: `item.update.${this.collection}.before`, - accountability: this.accountability, - collection: this.collection, - item: null, - action: 'update', + const customProcessed = await emitter.emitAsync( + `item.update.${this.collection}.before`, payload, - }); + { + event: `item.update.${this.collection}.before`, + accountability: this.accountability, + collection: this.collection, + item: null, + action: 'update', + payload, + } + ); payload = customProcessed[customProcessed.length - 1]; } @@ -354,14 +363,16 @@ export class ItemsService implements AbstractService { await cache.clear(); } - emitter.emitAsync(`item.update.${this.collection}`, { - event: `item.update.${this.collection}`, - accountability: this.accountability, - collection: this.collection, - item: key, - action: 'update', - payload, - }).catch(err => logger.warn(err)); + emitter + .emitAsync(`item.update.${this.collection}`, { + event: `item.update.${this.collection}`, + accountability: this.accountability, + collection: this.collection, + item: key, + action: 'update', + payload, + }) + .catch((err) => logger.warn(err)); return key; } @@ -438,14 +449,16 @@ export class ItemsService implements AbstractService { await cache.clear(); } - emitter.emitAsync(`item.delete.${this.collection}`, { - event: `item.delete.${this.collection}`, - accountability: this.accountability, - collection: this.collection, - item: keys, - action: 'delete', - payload: null, - }).catch(err => logger.warn(err)); + emitter + .emitAsync(`item.delete.${this.collection}`, { + event: `item.delete.${this.collection}`, + accountability: this.accountability, + collection: this.collection, + item: keys, + action: 'delete', + payload: null, + }) + .catch((err) => logger.warn(err)); return key; } diff --git a/api/src/services/payload.ts b/api/src/services/payload.ts index 76d8acbad1..e54c86623e 100644 --- a/api/src/services/payload.ts +++ b/api/src/services/payload.ts @@ -175,7 +175,12 @@ export class PayloadService { if (['create', 'update'].includes(action)) { processedPayload.forEach((record) => { for (const [key, value] of Object.entries(record)) { - if (Array.isArray(value) || (typeof value === 'object' && (value instanceof Date) !== true && value !== null)) { + if ( + Array.isArray(value) || + (typeof value === 'object' && + value instanceof Date !== true && + value !== null) + ) { record[key] = JSON.stringify(value); } } diff --git a/api/src/types/extensions.ts b/api/src/types/extensions.ts index 0107e1c2af..1d60d25bfa 100644 --- a/api/src/types/extensions.ts +++ b/api/src/types/extensions.ts @@ -6,10 +6,10 @@ import Knex from 'knex'; import { Router } from 'express'; type ExtensionContext = { - services: typeof services, - exceptions: typeof exceptions, - database: Knex, - env: typeof env, + services: typeof services; + exceptions: typeof exceptions; + database: Knex; + env: typeof env; }; export type HookRegisterFunction = (context: ExtensionContext) => Record; diff --git a/api/src/utils/get-ast-from-query.ts b/api/src/utils/get-ast-from-query.ts index f5c05a7d36..d349d3d3a9 100644 --- a/api/src/utils/get-ast-from-query.ts +++ b/api/src/utils/get-ast-from-query.ts @@ -19,7 +19,7 @@ type GetASTOptions = { accountability?: Accountability | null; action?: PermissionsAction; knex?: Knex; -} +}; export default async function getASTFromQuery( collection: string, diff --git a/api/src/utils/get-cache-key.ts b/api/src/utils/get-cache-key.ts index 4c39e0a003..47e554a9d1 100644 --- a/api/src/utils/get-cache-key.ts +++ b/api/src/utils/get-cache-key.ts @@ -1,8 +1,10 @@ -import { Request } from "express"; +import { Request } from 'express'; import url from 'url'; export function getCacheKey(req: Request) { const path = url.parse(req.originalUrl).pathname; - const key = `${req.accountability?.user || 'null'}-${path}-${JSON.stringify(req.sanitizedQuery)}`; + const key = `${req.accountability?.user || 'null'}-${path}-${JSON.stringify( + req.sanitizedQuery + )}`; return key; } diff --git a/api/src/utils/track.ts b/api/src/utils/track.ts index 09c3ebe214..5a86cf68ae 100644 --- a/api/src/utils/track.ts +++ b/api/src/utils/track.ts @@ -48,7 +48,7 @@ async function getEnvInfo(event: string) { store: env.CACHE_STORE, }, storage: { - drivers: getStorageDrivers() + drivers: getStorageDrivers(), }, cors: { enabled: env.CORS_ENABLED, @@ -57,15 +57,19 @@ async function getEnvInfo(event: string) { transport: env.EMAIL_TRANSPORT, }, oauth: { - providers: env.OAUTH_PROVIDERS.split(',').filter((p?: string) => p).map((p: string) => p.trim()), + providers: env.OAUTH_PROVIDERS.split(',') + .filter((p?: string) => p) + .map((p: string) => p.trim()), }, - db_client: env.DB_CLIENT - } + db_client: env.DB_CLIENT, + }; } function getStorageDrivers() { const drivers: string[] = []; - const locations = env.STORAGE_LOCATIONS.split(',').filter((l?: string) => l).map((l: string) => l.trim()); + const locations = env.STORAGE_LOCATIONS.split(',') + .filter((l?: string) => l) + .map((l: string) => l.trim()); for (const location of locations) { const driver = env[`STORAGE_${location.toUpperCase()}_DRIVER`];