mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Add custom endpoints support
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
95
api/src/extensions.ts
Normal file
95
api/src/extensions.ts
Normal file
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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<string, Function> = {};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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<Item>, keys: PrimaryKey[]): Promise<PrimaryKey[]>;
|
||||
@@ -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<Item>[]) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<string, ListenerFn>;
|
||||
type ExtensionContext = {
|
||||
services: typeof services,
|
||||
exceptions: typeof exceptions,
|
||||
database: Knex,
|
||||
env: typeof env,
|
||||
};
|
||||
|
||||
export type HookRegisterFunction = (context: ExtensionContext) => Record<string, ListenerFn>;
|
||||
export type EndpointRegisterFunction = (router: Router, context: ExtensionContext) => void;
|
||||
|
||||
Reference in New Issue
Block a user