mirror of
https://github.com/directus/directus.git
synced 2026-01-28 06:58:02 -05:00
Merge pull request #404 from directus/custom-extensions-api
Custom extensions api
This commit is contained in:
@@ -37,26 +37,31 @@ 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 { InvalidPayloadException } from './exceptions';
|
||||
|
||||
import { registerExtensions } from './extensions';
|
||||
import emitter from './emitter';
|
||||
|
||||
validateEnv(['KEY', 'SECRET']);
|
||||
|
||||
const app = express();
|
||||
|
||||
const customRouter = express.Router();
|
||||
|
||||
app.disable('x-powered-by');
|
||||
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());
|
||||
@@ -111,6 +116,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 +124,11 @@ app.use(errorHandler);
|
||||
const webhooksService = new WebhooksService();
|
||||
webhooksService.register();
|
||||
|
||||
// Register custom hooks / endpoints
|
||||
registerExtensions(customRouter);
|
||||
|
||||
track('serverStarted');
|
||||
|
||||
emitter.emitAsync('server.started').catch((err) => logger.warn(err));
|
||||
|
||||
export default app;
|
||||
|
||||
@@ -27,18 +27,19 @@ function getKevyInstance() {
|
||||
}
|
||||
}
|
||||
|
||||
function getConfig(
|
||||
store: 'memory' | 'redis' | 'memcache' = 'memory'
|
||||
): Options<any> {
|
||||
const config: Options<any> = { namespace: env.CACHE_NAMESPACE, ttl: ms(env.CACHE_TTL as string) };
|
||||
function getConfig(store: 'memory' | 'redis' | 'memcache' = 'memory'): Options<any> {
|
||||
const config: Options<any> = {
|
||||
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') {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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';
|
||||
@@ -25,7 +24,7 @@ router.get(
|
||||
};
|
||||
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
@@ -39,7 +38,7 @@ router.get(
|
||||
};
|
||||
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
@@ -70,7 +69,7 @@ router.post(
|
||||
}
|
||||
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.patch(
|
||||
@@ -94,7 +93,7 @@ router.patch(
|
||||
}
|
||||
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.delete(
|
||||
@@ -104,7 +103,7 @@ router.delete(
|
||||
await service.delete(req.params.pk);
|
||||
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,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();
|
||||
|
||||
@@ -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';
|
||||
@@ -113,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();
|
||||
@@ -128,7 +129,7 @@ router.post(
|
||||
|
||||
const importSchema = Joi.object({
|
||||
url: Joi.string().required(),
|
||||
data: Joi.object()
|
||||
data: Joi.object(),
|
||||
});
|
||||
|
||||
router.post(
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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();
|
||||
@@ -30,7 +29,7 @@ router.post(
|
||||
}
|
||||
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
@@ -51,7 +50,7 @@ router.get(
|
||||
data: records || null,
|
||||
};
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
@@ -70,7 +69,7 @@ router.get(
|
||||
data: result || null,
|
||||
};
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.patch(
|
||||
@@ -101,7 +100,7 @@ router.patch(
|
||||
}
|
||||
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.patch(
|
||||
@@ -129,7 +128,7 @@ router.patch(
|
||||
}
|
||||
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.delete(
|
||||
@@ -140,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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Router } from 'express';
|
||||
import ServerService from '../services/server';
|
||||
import { ServerService } from '../services';
|
||||
|
||||
const router = Router();
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
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 {
|
||||
InvalidPayloadException,
|
||||
InvalidCredentialsException,
|
||||
ForbiddenException,
|
||||
} from '../exceptions';
|
||||
import { UsersService, MetaService, AuthenticationService } from '../services';
|
||||
import useCollection from '../middleware/use-collection';
|
||||
|
||||
const router = express.Router();
|
||||
@@ -200,7 +202,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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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<Migration[]>('*').from('directus_migrations').orderBy('version');
|
||||
const completedMigrations = await database
|
||||
.select<Migration[]>('*')
|
||||
.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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@ 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';
|
||||
|
||||
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;
|
||||
|
||||
@@ -22,14 +22,14 @@ type TableSeed = {
|
||||
column: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
type RowSeed = {
|
||||
table: string;
|
||||
defaults: Record<string, any>;
|
||||
data: Record<string, any>[];
|
||||
}
|
||||
};
|
||||
|
||||
type FieldSeed = {
|
||||
table: string;
|
||||
@@ -50,7 +50,7 @@ type FieldSeed = {
|
||||
translation: Record<string, any> | 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') {
|
||||
|
||||
@@ -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;
|
||||
|
||||
98
api/src/extensions.ts
Normal file
98
api/src/extensions.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
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,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import listFolders from '../utils/list-folders';
|
||||
import path from 'path';
|
||||
import env from '../env';
|
||||
import { ServiceUnavailableException } from '../exceptions';
|
||||
|
||||
export default class ExtensionsService {
|
||||
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 couldn't be opened`, {
|
||||
service: 'extensions',
|
||||
});
|
||||
}
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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> & { 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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
20
api/src/services/index.ts
Normal file
20
api/src/services/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export * from './activity';
|
||||
export * from './assets';
|
||||
export * from './authentication';
|
||||
export * from './collections';
|
||||
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';
|
||||
@@ -15,15 +15,16 @@ 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';
|
||||
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;
|
||||
@@ -51,10 +52,35 @@ export default 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 +96,6 @@ export default 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 +169,18 @@ export default 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;
|
||||
});
|
||||
@@ -168,7 +192,10 @@ export default 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);
|
||||
@@ -201,15 +228,11 @@ export default 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({
|
||||
@@ -221,7 +244,6 @@ export default 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 +263,23 @@ export default 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,
|
||||
@@ -324,12 +363,16 @@ export default class ItemsService implements AbstractService {
|
||||
await cache.clear();
|
||||
}
|
||||
|
||||
emitter.emitAsync(`item.update.${this.collection}`, {
|
||||
collection: this.collection,
|
||||
item: key,
|
||||
action: 'update',
|
||||
payload,
|
||||
});
|
||||
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 +390,13 @@ export default 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 +419,15 @@ export default 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();
|
||||
|
||||
@@ -393,11 +449,16 @@ export default class ItemsService implements AbstractService {
|
||||
await cache.clear();
|
||||
}
|
||||
|
||||
emitter.emitAsync(`item.delete.${this.collection}`, {
|
||||
collection: this.collection,
|
||||
item: key,
|
||||
action: 'delete',
|
||||
});
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<any>;
|
||||
};
|
||||
|
||||
export default class PayloadService {
|
||||
export class PayloadService {
|
||||
accountability: Accountability | null;
|
||||
knex: Knex;
|
||||
collection: string;
|
||||
@@ -176,7 +175,12 @@ export default 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
16
api/src/types/extensions.ts
Normal file
16
api/src/types/extensions.ts
Normal file
@@ -0,0 +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';
|
||||
|
||||
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;
|
||||
@@ -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';
|
||||
|
||||
@@ -19,7 +19,7 @@ type GetASTOptions = {
|
||||
accountability?: Accountability | null;
|
||||
action?: PermissionsAction;
|
||||
knex?: Knex;
|
||||
}
|
||||
};
|
||||
|
||||
export default async function getASTFromQuery(
|
||||
collection: string,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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`];
|
||||
|
||||
@@ -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/<extension-name>`. `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) });
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user