mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Merge branch 'main' into sdk
This commit is contained in:
@@ -4,6 +4,8 @@ import logger from './logger';
|
||||
import expressLogger from 'express-pino-logger';
|
||||
import path from 'path';
|
||||
|
||||
import { validateDBConnection, isInstalled } from './database';
|
||||
|
||||
import { validateEnv } from './utils/validate-env';
|
||||
import env from './env';
|
||||
import { track } from './utils/track';
|
||||
@@ -34,19 +36,29 @@ import usersRouter from './controllers/users';
|
||||
import utilsRouter from './controllers/utils';
|
||||
import webhooksRouter from './controllers/webhooks';
|
||||
import graphqlRouter from './controllers/graphql';
|
||||
import schema from './middleware/schema';
|
||||
|
||||
import notFoundHandler from './controllers/not-found';
|
||||
import sanitizeQuery from './middleware/sanitize-query';
|
||||
import { checkIP } from './middleware/check-ip';
|
||||
import { WebhooksService } from './services/webhooks';
|
||||
import { InvalidPayloadException } from './exceptions';
|
||||
|
||||
import { registerExtensions } from './extensions';
|
||||
import { register as registerWebhooks } from './webhooks';
|
||||
import emitter from './emitter';
|
||||
|
||||
import fse from 'fs-extra';
|
||||
|
||||
export default async function createApp() {
|
||||
validateEnv(['KEY', 'SECRET']);
|
||||
|
||||
await validateDBConnection();
|
||||
|
||||
if ((await isInstalled()) === false) {
|
||||
logger.fatal(`Database doesn't have Directus tables installed.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const app = express();
|
||||
|
||||
const customRouter = express.Router();
|
||||
@@ -80,11 +92,18 @@ export default async function createApp() {
|
||||
|
||||
if (env.NODE_ENV !== 'development') {
|
||||
const adminPath = require.resolve('@directus/app/dist/index.html');
|
||||
const publicUrl = env.PUBLIC_URL.endsWith('/') ? env.PUBLIC_URL : env.PUBLIC_URL + '/';
|
||||
|
||||
app.get('/', (req, res) => res.redirect('/admin/'));
|
||||
// Prefix all href/src in the index html with the APIs public path
|
||||
let html = fse.readFileSync(adminPath, 'utf-8');
|
||||
html = html.replace(/href="\//g, `href="${publicUrl}`);
|
||||
html = html.replace(/src="\//g, `src="${publicUrl}`);
|
||||
|
||||
app.get('/', (req, res) => res.redirect(`./admin/`));
|
||||
app.get('/admin', (req, res) => res.send(html));
|
||||
app.use('/admin', express.static(path.join(adminPath, '..')));
|
||||
app.use('/admin/*', (req, res) => {
|
||||
res.sendFile(adminPath);
|
||||
res.send(html);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -93,16 +112,18 @@ export default async function createApp() {
|
||||
app.use(rateLimiter);
|
||||
}
|
||||
|
||||
app.use(sanitizeQuery);
|
||||
|
||||
app.use('/auth', authRouter);
|
||||
|
||||
app.use(authenticate);
|
||||
|
||||
app.use(checkIP);
|
||||
|
||||
app.use(sanitizeQuery);
|
||||
|
||||
app.use(cache);
|
||||
|
||||
app.use(schema);
|
||||
|
||||
app.use('/auth', authRouter);
|
||||
|
||||
app.use('/graphql', graphqlRouter);
|
||||
|
||||
app.use('/activity', activityRouter);
|
||||
@@ -128,8 +149,7 @@ export default async function createApp() {
|
||||
app.use(errorHandler);
|
||||
|
||||
// Register all webhooks
|
||||
const webhooksService = new WebhooksService();
|
||||
await webhooksService.register();
|
||||
await registerWebhooks();
|
||||
|
||||
// Register custom hooks / endpoints
|
||||
await registerExtensions(customRouter);
|
||||
|
||||
@@ -10,7 +10,7 @@ let cache: Keyv | null = null;
|
||||
if (env.CACHE_ENABLED === true) {
|
||||
validateEnv(['CACHE_NAMESPACE', 'CACHE_TTL', 'CACHE_STORE']);
|
||||
cache = getKevyInstance();
|
||||
cache.on('error', logger.error);
|
||||
cache.on('error', (err) => logger.error(err));
|
||||
}
|
||||
|
||||
export default cache;
|
||||
@@ -34,12 +34,8 @@ function getConfig(store: 'memory' | 'redis' | 'memcache' = 'memory'): Options<a
|
||||
};
|
||||
|
||||
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(env.CACHE_REDIS || getConfigFromEnv('CACHE_REDIS_'));
|
||||
}
|
||||
|
||||
if (store === 'memcache') {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export default async function rolesCreate(collection: string) {
|
||||
export default async function count(collection: string) {
|
||||
const database = require('../../../database/index').default;
|
||||
|
||||
if (!collection) {
|
||||
@@ -6,10 +6,14 @@ export default async function rolesCreate(collection: string) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const records = await database(collection).count('*', { as: 'count' });
|
||||
const count = Number(records[0].count);
|
||||
try {
|
||||
const records = await database(collection).count('*', { as: 'count' });
|
||||
const count = Number(records[0].count);
|
||||
|
||||
console.log(count);
|
||||
|
||||
database.destroy();
|
||||
console.log(count);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
database.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import Knex from 'knex';
|
||||
import run from '../../../database/seeds/run';
|
||||
import installSeeds from '../../../database/seeds/run';
|
||||
import runMigrations from '../../../database/migrations/run';
|
||||
|
||||
export default async function start() {
|
||||
const database = require('../../../database/index').default as Knex;
|
||||
|
||||
try {
|
||||
await run(database);
|
||||
await installSeeds(database);
|
||||
await runMigrations(database, 'latest');
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
process.exit(1);
|
||||
|
||||
@@ -1,10 +1,20 @@
|
||||
import run from '../../../database/migrations/run';
|
||||
|
||||
import ora from 'ora';
|
||||
|
||||
export default async function migrate(direction: 'latest' | 'up' | 'down') {
|
||||
const database = require('../../../database').default;
|
||||
|
||||
try {
|
||||
const spinnerDriver = ora('Running migrations...').start();
|
||||
await run(database, direction);
|
||||
spinnerDriver.stop();
|
||||
|
||||
if (direction === 'down') {
|
||||
console.log('✨ Downgrade successful');
|
||||
} else {
|
||||
console.log('✨ Database up to date');
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
process.exit(1);
|
||||
|
||||
@@ -10,6 +10,7 @@ import ora from 'ora';
|
||||
import argon2 from 'argon2';
|
||||
|
||||
import runSeed from '../../../database/seeds/run';
|
||||
import runMigrations from '../../../database/migrations/run';
|
||||
|
||||
import createDBConnection, { Credentials } from '../../utils/create-db-connection';
|
||||
import Knex from 'knex';
|
||||
@@ -28,13 +29,9 @@ export default async function init(options: Record<string, any>) {
|
||||
|
||||
const dbClient = getDriverForClient(client)!;
|
||||
|
||||
try {
|
||||
require.resolve(dbClient);
|
||||
} catch {
|
||||
const spinnerDriver = ora('Installing Database Driver...').start();
|
||||
await execa('npm', ['install', dbClient, '--production']);
|
||||
spinnerDriver.stop();
|
||||
}
|
||||
const spinnerDriver = ora('Installing Database Driver...').start();
|
||||
await execa('npm', ['install', dbClient, '--production']);
|
||||
spinnerDriver.stop();
|
||||
|
||||
let attemptsRemaining = 5;
|
||||
|
||||
@@ -51,6 +48,7 @@ export default async function init(options: Record<string, any>) {
|
||||
|
||||
try {
|
||||
await runSeed(db);
|
||||
await runMigrations(db, 'latest');
|
||||
} catch (err) {
|
||||
console.log();
|
||||
console.log('Something went wrong while seeding the database:');
|
||||
@@ -102,7 +100,7 @@ export default async function init(options: Record<string, any>) {
|
||||
await db('directus_roles').insert({
|
||||
id: roleID,
|
||||
name: 'Administrator',
|
||||
icon: 'verified_user',
|
||||
icon: 'verified',
|
||||
admin_access: true,
|
||||
description: 'Initial administrative role with unrestricted App/API access',
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default async function rolesCreate({ name, admin }: any) {
|
||||
const database = require('../../../database/index').default;
|
||||
const { default: database, schemaInspector } = require('../../../database/index');
|
||||
const { RolesService } = require('../../../services/roles');
|
||||
|
||||
if (!name) {
|
||||
@@ -7,8 +7,15 @@ export default async function rolesCreate({ name, admin }: any) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const service = new RolesService();
|
||||
const id = await service.create({ name, admin_access: admin });
|
||||
console.log(id);
|
||||
database.destroy();
|
||||
try {
|
||||
const schema = await schemaInspector.overview();
|
||||
const service = new RolesService({ schema: schema, knex: database });
|
||||
|
||||
const id = await service.create({ name, admin_access: admin });
|
||||
console.log(id);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
database.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default async function usersCreate({ email, password, role }: any) {
|
||||
const database = require('../../../database/index').default;
|
||||
const { default: database, schemaInspector } = require('../../../database/index');
|
||||
const { UsersService } = require('../../../services/users');
|
||||
|
||||
if (!email || !password || !role) {
|
||||
@@ -7,8 +7,15 @@ export default async function usersCreate({ email, password, role }: any) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const service = new UsersService();
|
||||
const id = await service.create({ email, password, role, status: 'active' });
|
||||
console.log(id);
|
||||
database.destroy();
|
||||
try {
|
||||
const schema = await schemaInspector.overview();
|
||||
const service = new UsersService({ schema, knex: database });
|
||||
|
||||
const id = await service.create({ email, password, role, status: 'active' });
|
||||
console.log(id);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
database.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,4 +57,7 @@ program
|
||||
.description('Count the amount of items in a given collection')
|
||||
.action(count);
|
||||
|
||||
program.parse(process.argv);
|
||||
program.parseAsync(process.argv).catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
@@ -3,37 +3,37 @@ import { Transformation } from './types/assets';
|
||||
export const SYSTEM_ASSET_ALLOW_LIST: Transformation[] = [
|
||||
{
|
||||
key: 'system-small-cover',
|
||||
w: 64,
|
||||
h: 64,
|
||||
f: 'cover',
|
||||
width: 64,
|
||||
height: 64,
|
||||
fit: 'cover',
|
||||
},
|
||||
{
|
||||
key: 'system-small-contain',
|
||||
w: 64,
|
||||
f: 'contain',
|
||||
width: 64,
|
||||
fit: 'contain',
|
||||
},
|
||||
{
|
||||
key: 'system-medium-cover',
|
||||
w: 300,
|
||||
h: 300,
|
||||
f: 'cover',
|
||||
width: 300,
|
||||
height: 300,
|
||||
fit: 'cover',
|
||||
},
|
||||
{
|
||||
key: 'system-medium-contain',
|
||||
w: 300,
|
||||
f: 'contain',
|
||||
width: 300,
|
||||
fit: 'contain',
|
||||
},
|
||||
{
|
||||
key: 'system-large-cover',
|
||||
w: 800,
|
||||
h: 600,
|
||||
f: 'cover',
|
||||
width: 800,
|
||||
height: 600,
|
||||
fit: 'cover',
|
||||
},
|
||||
{
|
||||
key: 'system-large-contain',
|
||||
w: 800,
|
||||
f: 'contain',
|
||||
width: 800,
|
||||
fit: 'contain',
|
||||
},
|
||||
];
|
||||
|
||||
export const ASSET_TRANSFORM_QUERY_KEYS = ['key', 'w', 'h', 'f'];
|
||||
export const ASSET_TRANSFORM_QUERY_KEYS = ['key', 'width', 'height', 'fit', 'withoutEnlargement'];
|
||||
|
||||
@@ -13,8 +13,14 @@ router.use(useCollection('directus_activity'));
|
||||
router.get(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new ActivityService({ accountability: req.accountability });
|
||||
const metaService = new MetaService({ accountability: req.accountability });
|
||||
const service = new ActivityService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const metaService = new MetaService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const records = await service.readByQuery(req.sanitizedQuery);
|
||||
const meta = await metaService.getMetaForQuery('directus_activity', req.sanitizedQuery);
|
||||
@@ -32,7 +38,10 @@ router.get(
|
||||
router.get(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new ActivityService({ accountability: req.accountability });
|
||||
const service = new ActivityService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const record = await service.readByKey(req.params.pk, req.sanitizedQuery);
|
||||
|
||||
res.locals.payload = {
|
||||
@@ -47,7 +56,10 @@ router.get(
|
||||
router.post(
|
||||
'/comment',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new ActivityService({ accountability: req.accountability });
|
||||
const service = new ActivityService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const primaryKey = await service.create({
|
||||
...req.body,
|
||||
@@ -79,7 +91,10 @@ router.post(
|
||||
router.patch(
|
||||
'/comment/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new ActivityService({ accountability: req.accountability });
|
||||
const service = new ActivityService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const primaryKey = await service.update(req.body, req.params.pk);
|
||||
|
||||
try {
|
||||
@@ -104,7 +119,10 @@ router.patch(
|
||||
router.delete(
|
||||
'/comment/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new ActivityService({ accountability: req.accountability });
|
||||
const service = new ActivityService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
await service.delete(req.params.pk);
|
||||
|
||||
return next();
|
||||
|
||||
@@ -47,7 +47,7 @@ router.get(
|
||||
|
||||
// Validate query params
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const payloadService = new PayloadService('directus_settings');
|
||||
const payloadService = new PayloadService('directus_settings', { schema: req.schema });
|
||||
const defaults = { storage_asset_presets: [], storage_asset_transform: 'all' };
|
||||
|
||||
let savedAssetSettings = await database
|
||||
@@ -78,18 +78,20 @@ router.get(
|
||||
];
|
||||
|
||||
// For use in the next request handler
|
||||
res.locals.shortcuts = [...SYSTEM_ASSET_ALLOW_LIST, assetSettings.storage_asset_presets];
|
||||
res.locals.shortcuts = [
|
||||
...SYSTEM_ASSET_ALLOW_LIST,
|
||||
...(assetSettings.storage_asset_presets || []),
|
||||
];
|
||||
res.locals.transformation = transformation;
|
||||
|
||||
if (Object.keys(transformation).length === 0) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (assetSettings.asset_generation === 'all') {
|
||||
if (assetSettings.storage_asset_transform === 'all') {
|
||||
if (transformation.key && allKeys.includes(transformation.key as string) === false)
|
||||
throw new InvalidQueryException(`Key "${transformation.key}" isn't configured.`);
|
||||
return next();
|
||||
} else if (assetSettings.asset_generation === 'shortcut') {
|
||||
} else if (assetSettings.storage_asset_transform === 'shortcut') {
|
||||
if (allKeys.includes(transformation.key as string)) return next();
|
||||
throw new InvalidQueryException(
|
||||
`Only configured shortcuts can be used in asset generation.`
|
||||
@@ -105,7 +107,11 @@ router.get(
|
||||
|
||||
// Return file
|
||||
asyncHandler(async (req, res) => {
|
||||
const service = new AssetsService({ accountability: req.accountability });
|
||||
const service = new AssetsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const transformation: Transformation = res.locals.transformation.key
|
||||
? res.locals.shortcuts.find(
|
||||
(transformation: Transformation) =>
|
||||
|
||||
@@ -34,6 +34,7 @@ router.post(
|
||||
|
||||
const authenticationService = new AuthenticationService({
|
||||
accountability: accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const { error } = loginSchema.validate(req.body);
|
||||
@@ -46,15 +47,13 @@ router.post(
|
||||
const ip = req.ip;
|
||||
const userAgent = req.get('user-agent');
|
||||
|
||||
const { accessToken, refreshToken, expires, id } = await authenticationService.authenticate(
|
||||
{
|
||||
ip,
|
||||
userAgent,
|
||||
email,
|
||||
password,
|
||||
otp,
|
||||
}
|
||||
);
|
||||
const { accessToken, refreshToken, expires } = await authenticationService.authenticate({
|
||||
ip,
|
||||
userAgent,
|
||||
email,
|
||||
password,
|
||||
otp,
|
||||
});
|
||||
|
||||
const payload = {
|
||||
data: { access_token: accessToken, expires },
|
||||
@@ -92,6 +91,7 @@ router.post(
|
||||
|
||||
const authenticationService = new AuthenticationService({
|
||||
accountability: accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const currentRefreshToken = req.body.refresh_token || req.cookies.directus_refresh_token;
|
||||
@@ -144,6 +144,7 @@ router.post(
|
||||
|
||||
const authenticationService = new AuthenticationService({
|
||||
accountability: accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const currentRefreshToken = req.body.refresh_token || req.cookies.directus_refresh_token;
|
||||
@@ -173,7 +174,7 @@ router.post(
|
||||
role: null,
|
||||
};
|
||||
|
||||
const service = new UsersService({ accountability });
|
||||
const service = new UsersService({ accountability, schema: req.schema });
|
||||
|
||||
try {
|
||||
await service.requestPasswordReset(req.body.email);
|
||||
@@ -204,7 +205,7 @@ router.post(
|
||||
role: null,
|
||||
};
|
||||
|
||||
const service = new UsersService({ accountability });
|
||||
const service = new UsersService({ accountability, schema: req.schema });
|
||||
await service.resetPassword(req.body.token, req.body.password);
|
||||
return next();
|
||||
}),
|
||||
@@ -239,7 +240,7 @@ router.get(
|
||||
}
|
||||
|
||||
if (req.query?.redirect && req.session) {
|
||||
req.session.redirect = req.query.redirect;
|
||||
req.session.redirect = req.query.redirect as string;
|
||||
}
|
||||
|
||||
next();
|
||||
@@ -252,7 +253,7 @@ router.use(grant.express()(grantConfig));
|
||||
router.get(
|
||||
'/oauth/:provider/callback',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const redirect = req.session?.redirect;
|
||||
const redirect = req.session.redirect;
|
||||
|
||||
const accountability = {
|
||||
ip: req.ip,
|
||||
@@ -262,12 +263,10 @@ router.get(
|
||||
|
||||
const authenticationService = new AuthenticationService({
|
||||
accountability: accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const email = getEmailFromProfile(
|
||||
req.params.provider,
|
||||
req.session!.grant.response?.profile
|
||||
);
|
||||
const email = getEmailFromProfile(req.params.provider, req.session.grant.response?.profile);
|
||||
|
||||
req.session?.destroy(() => {});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Router } from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import { CollectionsService, MetaService } from '../services';
|
||||
import { ForbiddenException } from '../exceptions';
|
||||
import { ForbiddenException, InvalidPayloadException } from '../exceptions';
|
||||
import { respond } from '../middleware/respond';
|
||||
|
||||
const router = Router();
|
||||
@@ -9,7 +9,10 @@ const router = Router();
|
||||
router.post(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const collectionsService = new CollectionsService({ accountability: req.accountability });
|
||||
const collectionsService = new CollectionsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const collectionKey = await collectionsService.create(req.body);
|
||||
const record = await collectionsService.readByKey(collectionKey);
|
||||
@@ -23,8 +26,14 @@ router.post(
|
||||
router.get(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const collectionsService = new CollectionsService({ accountability: req.accountability });
|
||||
const metaService = new MetaService({ accountability: req.accountability });
|
||||
const collectionsService = new CollectionsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const metaService = new MetaService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const collections = await collectionsService.readByQuery();
|
||||
const meta = await metaService.getMetaForQuery('directus_collections', {});
|
||||
@@ -38,7 +47,10 @@ router.get(
|
||||
router.get(
|
||||
'/:collection',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const collectionsService = new CollectionsService({ accountability: req.accountability });
|
||||
const collectionsService = new CollectionsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const collectionKey = req.params.collection.includes(',')
|
||||
? req.params.collection.split(',')
|
||||
: req.params.collection;
|
||||
@@ -62,7 +74,10 @@ router.get(
|
||||
router.patch(
|
||||
'/:collection',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const collectionsService = new CollectionsService({ accountability: req.accountability });
|
||||
const collectionsService = new CollectionsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const collectionKey = req.params.collection.includes(',')
|
||||
? req.params.collection.split(',')
|
||||
: req.params.collection;
|
||||
@@ -84,10 +99,31 @@ router.patch(
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
if (!req.body || Array.isArray(req.body) === false) {
|
||||
throw new InvalidPayloadException(`Body has to be an array of primary keys`);
|
||||
}
|
||||
|
||||
const collectionsService = new CollectionsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
await collectionsService.delete(req.body as string[]);
|
||||
|
||||
return next();
|
||||
}),
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:collection',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const collectionsService = new CollectionsService({ accountability: req.accountability });
|
||||
const collectionsService = new CollectionsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const collectionKey = req.params.collection.includes(',')
|
||||
? req.params.collection.split(',')
|
||||
: req.params.collection;
|
||||
|
||||
@@ -2,7 +2,6 @@ import { Router } from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
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 { types, Field } from '../types';
|
||||
@@ -16,7 +15,10 @@ router.use(useCollection('directus_fields'));
|
||||
router.get(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new FieldsService({ accountability: req.accountability });
|
||||
const service = new FieldsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const fields = await service.readAll();
|
||||
|
||||
res.locals.payload = { data: fields || null };
|
||||
@@ -29,7 +31,10 @@ router.get(
|
||||
'/:collection',
|
||||
validateCollection,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new FieldsService({ accountability: req.accountability });
|
||||
const service = new FieldsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const fields = await service.readAll(req.params.collection);
|
||||
|
||||
res.locals.payload = { data: fields || null };
|
||||
@@ -42,10 +47,13 @@ router.get(
|
||||
'/:collection/:field',
|
||||
validateCollection,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new FieldsService({ accountability: req.accountability });
|
||||
const service = new FieldsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const exists = await schemaInspector.hasColumn(req.params.collection, req.params.field);
|
||||
if (exists === false) throw new ForbiddenException();
|
||||
if (req.params.field in req.schema[req.params.collection].columns === false)
|
||||
throw new ForbiddenException();
|
||||
|
||||
const field = await service.readOne(req.params.collection, req.params.field);
|
||||
|
||||
@@ -75,7 +83,10 @@ router.post(
|
||||
if (!req.body.schema && !req.body.meta)
|
||||
throw new InvalidPayloadException(`"schema" or "meta" is required`);
|
||||
|
||||
const service = new FieldsService({ accountability: req.accountability });
|
||||
const service = new FieldsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const { error } = newFieldSchema.validate(req.body);
|
||||
|
||||
@@ -107,7 +118,10 @@ router.patch(
|
||||
'/:collection',
|
||||
validateCollection,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new FieldsService({ accountability: req.accountability });
|
||||
const service = new FieldsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
if (Array.isArray(req.body) === false) {
|
||||
throw new InvalidPayloadException('Submitted body has to be an array.');
|
||||
@@ -142,7 +156,10 @@ router.patch(
|
||||
validateCollection,
|
||||
// @todo: validate field
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new FieldsService({ accountability: req.accountability });
|
||||
const service = new FieldsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const fieldData: Partial<Field> & { field: string; type: typeof types[number] } = req.body;
|
||||
|
||||
if (!fieldData.field) fieldData.field = req.params.field;
|
||||
@@ -169,7 +186,10 @@ router.delete(
|
||||
'/:collection/:field',
|
||||
validateCollection,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new FieldsService({ accountability: req.accountability });
|
||||
const service = new FieldsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
await service.deleteField(req.params.collection, req.params.field);
|
||||
return next();
|
||||
}),
|
||||
|
||||
@@ -23,7 +23,7 @@ const multipartHandler = asyncHandler(async (req, res, next) => {
|
||||
|
||||
const busboy = new Busboy({ headers: req.headers });
|
||||
const savedFiles: PrimaryKey[] = [];
|
||||
const service = new FilesService({ accountability: req.accountability });
|
||||
const service = new FilesService({ accountability: req.accountability, schema: req.schema });
|
||||
|
||||
const existingPrimaryKey = req.params.pk || undefined;
|
||||
|
||||
@@ -102,7 +102,10 @@ router.post(
|
||||
'/',
|
||||
multipartHandler,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new FilesService({ accountability: req.accountability });
|
||||
const service = new FilesService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
let keys: PrimaryKey | PrimaryKey[] = [];
|
||||
|
||||
if (req.is('multipart/form-data')) {
|
||||
@@ -145,7 +148,10 @@ router.post(
|
||||
throw new InvalidPayloadException(error.message);
|
||||
}
|
||||
|
||||
const service = new FilesService({ accountability: req.accountability });
|
||||
const service = new FilesService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const fileResponse = await axios.get<NodeJS.ReadableStream>(req.body.url, {
|
||||
responseType: 'stream',
|
||||
@@ -183,8 +189,14 @@ router.post(
|
||||
router.get(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new FilesService({ accountability: req.accountability });
|
||||
const metaService = new MetaService({ accountability: req.accountability });
|
||||
const service = new FilesService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const metaService = new MetaService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const records = await service.readByQuery(req.sanitizedQuery);
|
||||
const meta = await metaService.getMetaForQuery('directus_files', req.sanitizedQuery);
|
||||
@@ -199,7 +211,10 @@ router.get(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const keys = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const service = new FilesService({ accountability: req.accountability });
|
||||
const service = new FilesService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const record = await service.readByKey(keys as any, req.sanitizedQuery);
|
||||
res.locals.payload = { data: record || null };
|
||||
return next();
|
||||
@@ -211,7 +226,10 @@ router.patch(
|
||||
'/:pk',
|
||||
multipartHandler,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new FilesService({ accountability: req.accountability });
|
||||
const service = new FilesService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
let keys: PrimaryKey | PrimaryKey[] = [];
|
||||
|
||||
if (req.is('multipart/form-data')) {
|
||||
@@ -237,11 +255,31 @@ router.patch(
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
if (!req.body || Array.isArray(req.body) === false) {
|
||||
throw new InvalidPayloadException(`Body has to be an array of primary keys`);
|
||||
}
|
||||
|
||||
const service = new FilesService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
await service.delete(req.body as PrimaryKey[]);
|
||||
return next();
|
||||
}),
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const keys = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const service = new FilesService({ accountability: req.accountability });
|
||||
const service = new FilesService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
await service.delete(keys as any);
|
||||
return next();
|
||||
}),
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import { FoldersService, MetaService } from '../services';
|
||||
import { ForbiddenException } from '../exceptions';
|
||||
import { ForbiddenException, InvalidPayloadException } from '../exceptions';
|
||||
import useCollection from '../middleware/use-collection';
|
||||
import { respond } from '../middleware/respond';
|
||||
import { PrimaryKey } from '../types';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -12,7 +13,10 @@ router.use(useCollection('directus_folders'));
|
||||
router.post(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new FoldersService({ accountability: req.accountability });
|
||||
const service = new FoldersService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const primaryKey = await service.create(req.body);
|
||||
|
||||
try {
|
||||
@@ -34,8 +38,14 @@ router.post(
|
||||
router.get(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new FoldersService({ accountability: req.accountability });
|
||||
const metaService = new MetaService({ accountability: req.accountability });
|
||||
const service = new FoldersService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const metaService = new MetaService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const records = await service.readByQuery(req.sanitizedQuery);
|
||||
const meta = await metaService.getMetaForQuery('directus_files', req.sanitizedQuery);
|
||||
@@ -49,7 +59,10 @@ router.get(
|
||||
router.get(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new FoldersService({ accountability: req.accountability });
|
||||
const service = new FoldersService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const primaryKey = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const record = await service.readByKey(primaryKey as any, req.sanitizedQuery);
|
||||
|
||||
@@ -62,7 +75,10 @@ router.get(
|
||||
router.patch(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new FoldersService({ accountability: req.accountability });
|
||||
const service = new FoldersService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const primaryKey = await service.update(req.body, pk as any);
|
||||
|
||||
@@ -82,10 +98,30 @@ router.patch(
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
if (!req.body || Array.isArray(req.body) === false) {
|
||||
throw new InvalidPayloadException(`Body has to be an array of primary keys`);
|
||||
}
|
||||
|
||||
const service = new FoldersService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
await service.delete(req.body as PrimaryKey[]);
|
||||
return next();
|
||||
}),
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new FoldersService({ accountability: req.accountability });
|
||||
const service = new FoldersService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const primaryKey = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
await service.delete(primaryKey as any);
|
||||
return next();
|
||||
|
||||
@@ -5,12 +5,16 @@ import asyncHandler from 'express-async-handler';
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(asyncHandler(async (req, res) => {
|
||||
const service = new GraphQLService({ accountability: req.accountability });
|
||||
const schema = await service.getSchema();
|
||||
router.use(
|
||||
asyncHandler(async (req, res) => {
|
||||
const service = new GraphQLService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const schema = await service.getSchema();
|
||||
|
||||
graphqlHTTP({ schema, graphiql: true })(req, res);
|
||||
}));
|
||||
graphqlHTTP({ schema, graphiql: true })(req, res);
|
||||
})
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
|
||||
@@ -2,8 +2,15 @@ import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import collectionExists from '../middleware/collection-exists';
|
||||
import { ItemsService, MetaService } from '../services';
|
||||
import { RouteNotFoundException, ForbiddenException } from '../exceptions';
|
||||
import {
|
||||
RouteNotFoundException,
|
||||
ForbiddenException,
|
||||
FailedValidationException,
|
||||
} from '../exceptions';
|
||||
import { respond } from '../middleware/respond';
|
||||
import { InvalidPayloadException } from '../exceptions';
|
||||
import { PrimaryKey } from '../types';
|
||||
import Joi from 'joi';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -15,7 +22,10 @@ router.post(
|
||||
throw new RouteNotFoundException(req.path);
|
||||
}
|
||||
|
||||
const service = new ItemsService(req.collection, { accountability: req.accountability });
|
||||
const service = new ItemsService(req.collection, {
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const primaryKey = await service.create(req.body);
|
||||
|
||||
try {
|
||||
@@ -38,8 +48,14 @@ router.get(
|
||||
'/:collection',
|
||||
collectionExists,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new ItemsService(req.collection, { accountability: req.accountability });
|
||||
const metaService = new MetaService({ accountability: req.accountability });
|
||||
const service = new ItemsService(req.collection, {
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const metaService = new MetaService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const records = req.singleton
|
||||
? await service.readSingleton(req.sanitizedQuery)
|
||||
@@ -64,7 +80,10 @@ router.get(
|
||||
throw new RouteNotFoundException(req.path);
|
||||
}
|
||||
|
||||
const service = new ItemsService(req.collection, { accountability: req.accountability });
|
||||
const service = new ItemsService(req.collection, {
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const primaryKey = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const result = await service.readByKey(primaryKey as any, req.sanitizedQuery);
|
||||
|
||||
@@ -80,7 +99,10 @@ router.patch(
|
||||
'/:collection',
|
||||
collectionExists,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new ItemsService(req.collection, { accountability: req.accountability });
|
||||
const service = new ItemsService(req.collection, {
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
if (req.singleton === true) {
|
||||
await service.upsertSingleton(req.body);
|
||||
@@ -90,7 +112,35 @@ router.patch(
|
||||
return next();
|
||||
}
|
||||
|
||||
const primaryKeys = await service.update(req.body);
|
||||
if (Array.isArray(req.body)) {
|
||||
const primaryKeys = await service.update(req.body);
|
||||
|
||||
try {
|
||||
const result = await service.readByKey(primaryKeys, req.sanitizedQuery);
|
||||
res.locals.payload = { data: result || null };
|
||||
} catch (error) {
|
||||
if (error instanceof ForbiddenException) {
|
||||
return next();
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
const updateSchema = Joi.object({
|
||||
keys: Joi.array().items(Joi.alternatives(Joi.string(), Joi.number())).required(),
|
||||
data: Joi.object().required().unknown(),
|
||||
});
|
||||
|
||||
const { error } = updateSchema.validate(req.body);
|
||||
|
||||
if (error) {
|
||||
throw new FailedValidationException(error.details[0]);
|
||||
}
|
||||
|
||||
const primaryKeys = await service.update(req.body.data, req.body.keys);
|
||||
|
||||
try {
|
||||
const result = await service.readByKey(primaryKeys, req.sanitizedQuery);
|
||||
@@ -116,7 +166,10 @@ router.patch(
|
||||
throw new RouteNotFoundException(req.path);
|
||||
}
|
||||
|
||||
const service = new ItemsService(req.collection, { accountability: req.accountability });
|
||||
const service = new ItemsService(req.collection, {
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const primaryKey = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
|
||||
const updatedPrimaryKey = await service.update(req.body, primaryKey as any);
|
||||
@@ -137,11 +190,32 @@ router.patch(
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:collection',
|
||||
collectionExists,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
if (!req.body || Array.isArray(req.body) === false) {
|
||||
throw new InvalidPayloadException(`Body has to be an array of primary keys`);
|
||||
}
|
||||
|
||||
const service = new ItemsService(req.collection, {
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
await service.delete(req.body as PrimaryKey[]);
|
||||
return next();
|
||||
}),
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:collection/:pk',
|
||||
collectionExists,
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new ItemsService(req.collection, { accountability: req.accountability });
|
||||
const service = new ItemsService(req.collection, {
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
await service.delete(pk as any);
|
||||
return next();
|
||||
|
||||
@@ -2,9 +2,14 @@ import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import { PermissionsService, MetaService } from '../services';
|
||||
import { clone } from 'lodash';
|
||||
import { InvalidCredentialsException, ForbiddenException } from '../exceptions';
|
||||
import {
|
||||
InvalidCredentialsException,
|
||||
ForbiddenException,
|
||||
InvalidPayloadException,
|
||||
} from '../exceptions';
|
||||
import useCollection from '../middleware/use-collection';
|
||||
import { respond } from '../middleware/respond';
|
||||
import { PrimaryKey } from '../types';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -13,7 +18,10 @@ router.use(useCollection('directus_permissions'));
|
||||
router.post(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new PermissionsService({ accountability: req.accountability });
|
||||
const service = new PermissionsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const primaryKey = await service.create(req.body);
|
||||
|
||||
try {
|
||||
@@ -34,8 +42,14 @@ router.post(
|
||||
router.get(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new PermissionsService({ accountability: req.accountability });
|
||||
const metaService = new MetaService({ accountability: req.accountability });
|
||||
const service = new PermissionsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const metaService = new MetaService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const item = await service.readByQuery(req.sanitizedQuery);
|
||||
const meta = await metaService.getMetaForQuery('directus_permissions', req.sanitizedQuery);
|
||||
@@ -53,7 +67,7 @@ router.get(
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
|
||||
const service = new PermissionsService();
|
||||
const service = new PermissionsService({ schema: req.schema });
|
||||
const query = clone(req.sanitizedQuery || {});
|
||||
|
||||
query.filter = {
|
||||
@@ -75,7 +89,10 @@ router.get(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
if (req.path.endsWith('me')) return next();
|
||||
const service = new PermissionsService({ accountability: req.accountability });
|
||||
const service = new PermissionsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const primaryKey = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const record = await service.readByKey(primaryKey as any, req.sanitizedQuery);
|
||||
|
||||
@@ -88,7 +105,10 @@ router.get(
|
||||
router.patch(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new PermissionsService({ accountability: req.accountability });
|
||||
const service = new PermissionsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const primaryKey = await service.update(req.body, pk as any);
|
||||
|
||||
@@ -108,10 +128,30 @@ router.patch(
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
if (!req.body || Array.isArray(req.body) === false) {
|
||||
throw new InvalidPayloadException(`Body has to be an array of primary keys`);
|
||||
}
|
||||
|
||||
const service = new PermissionsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
await service.delete(req.body as PrimaryKey[]);
|
||||
return next();
|
||||
}),
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new PermissionsService({ accountability: req.accountability });
|
||||
const service = new PermissionsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
await service.delete(pk as any);
|
||||
return next();
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import { PresetsService, MetaService } from '../services';
|
||||
import { ForbiddenException } from '../exceptions';
|
||||
import { ForbiddenException, InvalidPayloadException } from '../exceptions';
|
||||
import useCollection from '../middleware/use-collection';
|
||||
import { respond } from '../middleware/respond';
|
||||
import { PrimaryKey } from '../types';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -12,7 +13,10 @@ router.use(useCollection('directus_presets'));
|
||||
router.post(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new PresetsService({ accountability: req.accountability });
|
||||
const service = new PresetsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const primaryKey = await service.create(req.body);
|
||||
|
||||
try {
|
||||
@@ -34,8 +38,14 @@ router.post(
|
||||
router.get(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new PresetsService({ accountability: req.accountability });
|
||||
const metaService = new MetaService({ accountability: req.accountability });
|
||||
const service = new PresetsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const metaService = new MetaService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const records = await service.readByQuery(req.sanitizedQuery);
|
||||
const meta = await metaService.getMetaForQuery('directus_presets', req.sanitizedQuery);
|
||||
@@ -49,7 +59,10 @@ router.get(
|
||||
router.get(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new PresetsService({ accountability: req.accountability });
|
||||
const service = new PresetsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const record = await service.readByKey(pk as any, req.sanitizedQuery);
|
||||
|
||||
@@ -62,7 +75,10 @@ router.get(
|
||||
router.patch(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new PresetsService({ accountability: req.accountability });
|
||||
const service = new PresetsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const primaryKey = await service.update(req.body, pk as any);
|
||||
|
||||
@@ -82,10 +98,30 @@ router.patch(
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
if (!req.body || Array.isArray(req.body) === false) {
|
||||
throw new InvalidPayloadException(`Body has to be an array of primary keys`);
|
||||
}
|
||||
|
||||
const service = new PresetsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
await service.delete(req.body as PrimaryKey[]);
|
||||
return next();
|
||||
}),
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new PresetsService({ accountability: req.accountability });
|
||||
const service = new PresetsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
await service.delete(pk as any);
|
||||
return next();
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import { RelationsService, MetaService } from '../services';
|
||||
import { ForbiddenException } from '../exceptions';
|
||||
import { ForbiddenException, InvalidPayloadException } from '../exceptions';
|
||||
import useCollection from '../middleware/use-collection';
|
||||
import { respond } from '../middleware/respond';
|
||||
import { PrimaryKey } from '../types';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -12,7 +13,10 @@ router.use(useCollection('directus_relations'));
|
||||
router.post(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new RelationsService({ accountability: req.accountability });
|
||||
const service = new RelationsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const primaryKey = await service.create(req.body);
|
||||
|
||||
try {
|
||||
@@ -34,8 +38,14 @@ router.post(
|
||||
router.get(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new RelationsService({ accountability: req.accountability });
|
||||
const metaService = new MetaService({ accountability: req.accountability });
|
||||
const service = new RelationsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const metaService = new MetaService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const records = await service.readByQuery(req.sanitizedQuery);
|
||||
const meta = await metaService.getMetaForQuery(req.collection, req.sanitizedQuery);
|
||||
@@ -49,7 +59,10 @@ router.get(
|
||||
router.get(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new RelationsService({ accountability: req.accountability });
|
||||
const service = new RelationsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const record = await service.readByKey(pk as any, req.sanitizedQuery);
|
||||
res.locals.payload = { data: record || null };
|
||||
@@ -61,7 +74,10 @@ router.get(
|
||||
router.patch(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new RelationsService({ accountability: req.accountability });
|
||||
const service = new RelationsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const primaryKey = await service.update(req.body, pk as any);
|
||||
|
||||
@@ -81,10 +97,30 @@ router.patch(
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
if (!req.body || Array.isArray(req.body) === false) {
|
||||
throw new InvalidPayloadException(`Body has to be an array of primary keys`);
|
||||
}
|
||||
|
||||
const service = new RelationsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
await service.delete(req.body as PrimaryKey[]);
|
||||
return next();
|
||||
}),
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new RelationsService({ accountability: req.accountability });
|
||||
const service = new RelationsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
await service.delete(pk as any);
|
||||
return next();
|
||||
|
||||
@@ -11,8 +11,14 @@ router.use(useCollection('directus_revisions'));
|
||||
router.get(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new RevisionsService({ accountability: req.accountability });
|
||||
const metaService = new MetaService({ accountability: req.accountability });
|
||||
const service = new RevisionsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const metaService = new MetaService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const records = await service.readByQuery(req.sanitizedQuery);
|
||||
const meta = await metaService.getMetaForQuery('directus_revisions', req.sanitizedQuery);
|
||||
@@ -26,7 +32,10 @@ router.get(
|
||||
router.get(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new RevisionsService({ accountability: req.accountability });
|
||||
const service = new RevisionsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const record = await service.readByKey(pk as any, req.sanitizedQuery);
|
||||
res.locals.payload = { data: record || null };
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import { RolesService, MetaService } from '../services';
|
||||
import { ForbiddenException } from '../exceptions';
|
||||
import { ForbiddenException, InvalidPayloadException } from '../exceptions';
|
||||
import useCollection from '../middleware/use-collection';
|
||||
import { respond } from '../middleware/respond';
|
||||
import { PrimaryKey } from '../types';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -12,7 +13,10 @@ router.use(useCollection('directus_roles'));
|
||||
router.post(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new RolesService({ accountability: req.accountability });
|
||||
const service = new RolesService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const primaryKey = await service.create(req.body);
|
||||
|
||||
try {
|
||||
@@ -34,8 +38,14 @@ router.post(
|
||||
router.get(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new RolesService({ accountability: req.accountability });
|
||||
const metaService = new MetaService({ accountability: req.accountability });
|
||||
const service = new RolesService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const metaService = new MetaService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const records = await service.readByQuery(req.sanitizedQuery);
|
||||
const meta = await metaService.getMetaForQuery('directus_roles', req.sanitizedQuery);
|
||||
@@ -49,7 +59,10 @@ router.get(
|
||||
router.get(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new RolesService({ accountability: req.accountability });
|
||||
const service = new RolesService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const record = await service.readByKey(pk as any, req.sanitizedQuery);
|
||||
res.locals.payload = { data: record || null };
|
||||
@@ -61,7 +74,10 @@ router.get(
|
||||
router.patch(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new RolesService({ accountability: req.accountability });
|
||||
const service = new RolesService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const primaryKey = await service.update(req.body, pk as any);
|
||||
|
||||
@@ -81,10 +97,30 @@ router.patch(
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
if (!req.body || Array.isArray(req.body) === false) {
|
||||
throw new InvalidPayloadException(`Body has to be an array of primary keys`);
|
||||
}
|
||||
|
||||
const service = new RolesService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
await service.delete(req.body as PrimaryKey[]);
|
||||
return next();
|
||||
}),
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new RolesService({ accountability: req.accountability });
|
||||
const service = new RolesService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
await service.delete(pk as any);
|
||||
return next();
|
||||
|
||||
@@ -9,7 +9,10 @@ const router = Router();
|
||||
router.get(
|
||||
'/specs/oas',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new SpecificationService({ accountability: req.accountability });
|
||||
const service = new SpecificationService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
res.locals.payload = await service.oas.generate();
|
||||
return next();
|
||||
}),
|
||||
@@ -21,7 +24,10 @@ router.get('/ping', (req, res) => res.send('pong'));
|
||||
router.get(
|
||||
'/info',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new ServerService({ accountability: req.accountability });
|
||||
const service = new ServerService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const data = await service.serverInfo();
|
||||
res.locals.payload = { data };
|
||||
return next();
|
||||
|
||||
@@ -12,7 +12,10 @@ router.use(useCollection('directus_settings'));
|
||||
router.get(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new SettingsService({ accountability: req.accountability });
|
||||
const service = new SettingsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const records = await service.readSingleton(req.sanitizedQuery);
|
||||
res.locals.payload = { data: records || null };
|
||||
return next();
|
||||
@@ -23,7 +26,10 @@ router.get(
|
||||
router.patch(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new SettingsService({ accountability: req.accountability });
|
||||
const service = new SettingsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
await service.upsertSingleton(req.body);
|
||||
|
||||
try {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
import { UsersService, MetaService, AuthenticationService } from '../services';
|
||||
import useCollection from '../middleware/use-collection';
|
||||
import { respond } from '../middleware/respond';
|
||||
import { PrimaryKey } from '../types';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -17,7 +18,10 @@ router.use(useCollection('directus_users'));
|
||||
router.post(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new UsersService({ accountability: req.accountability });
|
||||
const service = new UsersService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const primaryKey = await service.create(req.body);
|
||||
|
||||
try {
|
||||
@@ -39,8 +43,14 @@ router.post(
|
||||
router.get(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new UsersService({ accountability: req.accountability });
|
||||
const metaService = new MetaService({ accountability: req.accountability });
|
||||
const service = new UsersService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const metaService = new MetaService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const item = await service.readByQuery(req.sanitizedQuery);
|
||||
const meta = await metaService.getMetaForQuery('directus_users', req.sanitizedQuery);
|
||||
@@ -58,7 +68,10 @@ router.get(
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
|
||||
const service = new UsersService({ accountability: req.accountability });
|
||||
const service = new UsersService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
try {
|
||||
const item = await service.readByKey(req.accountability.user, req.sanitizedQuery);
|
||||
@@ -81,7 +94,10 @@ router.get(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
if (req.path.endsWith('me')) return next();
|
||||
const service = new UsersService({ accountability: req.accountability });
|
||||
const service = new UsersService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const items = await service.readByKey(pk as any, req.sanitizedQuery);
|
||||
res.locals.payload = { data: items || null };
|
||||
@@ -97,7 +113,10 @@ router.patch(
|
||||
throw new InvalidCredentialsException();
|
||||
}
|
||||
|
||||
const service = new UsersService({ accountability: req.accountability });
|
||||
const service = new UsersService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const primaryKey = await service.update(req.body, req.accountability.user);
|
||||
const item = await service.readByKey(primaryKey, req.sanitizedQuery);
|
||||
|
||||
@@ -118,7 +137,7 @@ router.patch(
|
||||
throw new InvalidPayloadException(`"last_page" key is required.`);
|
||||
}
|
||||
|
||||
const service = new UsersService();
|
||||
const service = new UsersService({ schema: req.schema });
|
||||
await service.update({ last_page: req.body.last_page }, req.accountability.user);
|
||||
|
||||
return next();
|
||||
@@ -129,7 +148,10 @@ router.patch(
|
||||
router.patch(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new UsersService({ accountability: req.accountability });
|
||||
const service = new UsersService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const primaryKey = await service.update(req.body, pk as any);
|
||||
|
||||
@@ -149,10 +171,31 @@ router.patch(
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
if (!req.body || Array.isArray(req.body) === false) {
|
||||
throw new InvalidPayloadException(`Body has to be an array of primary keys`);
|
||||
}
|
||||
|
||||
const service = new UsersService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
await service.delete(req.body as PrimaryKey[]);
|
||||
|
||||
return next();
|
||||
}),
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new UsersService({ accountability: req.accountability });
|
||||
const service = new UsersService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
await service.delete(pk as any);
|
||||
|
||||
@@ -162,7 +205,10 @@ router.delete(
|
||||
);
|
||||
|
||||
const inviteSchema = Joi.object({
|
||||
email: Joi.string().email().required(),
|
||||
email: Joi.alternatives(
|
||||
Joi.string().email(),
|
||||
Joi.array().items(Joi.string().email())
|
||||
).required(),
|
||||
role: Joi.string().uuid({ version: 'uuidv4' }).required(),
|
||||
});
|
||||
|
||||
@@ -172,7 +218,10 @@ router.post(
|
||||
const { error } = inviteSchema.validate(req.body);
|
||||
if (error) throw new InvalidPayloadException(error.message);
|
||||
|
||||
const service = new UsersService({ accountability: req.accountability });
|
||||
const service = new UsersService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
await service.inviteUser(req.body.email, req.body.role);
|
||||
return next();
|
||||
}),
|
||||
@@ -189,7 +238,10 @@ router.post(
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const { error } = acceptInviteSchema.validate(req.body);
|
||||
if (error) throw new InvalidPayloadException(error.message);
|
||||
const service = new UsersService({ accountability: req.accountability });
|
||||
const service = new UsersService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
await service.acceptInvite(req.body.token, req.body.password);
|
||||
return next();
|
||||
}),
|
||||
@@ -207,9 +259,15 @@ router.post(
|
||||
throw new InvalidPayloadException(`"password" is required`);
|
||||
}
|
||||
|
||||
const service = new UsersService({ accountability: req.accountability });
|
||||
const service = new UsersService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const authService = new AuthenticationService({ accountability: req.accountability });
|
||||
const authService = new AuthenticationService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
await authService.verifyPassword(req.accountability.user, req.body.password);
|
||||
|
||||
const { url, secret } = await service.enableTFA(req.accountability.user);
|
||||
@@ -231,8 +289,14 @@ router.post(
|
||||
throw new InvalidPayloadException(`"otp" is required`);
|
||||
}
|
||||
|
||||
const service = new UsersService({ accountability: req.accountability });
|
||||
const authService = new AuthenticationService({ accountability: req.accountability });
|
||||
const service = new UsersService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const authService = new AuthenticationService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const otpValid = await authService.verifyOTP(req.accountability.user, req.body.otp);
|
||||
|
||||
|
||||
@@ -67,7 +67,10 @@ router.post(
|
||||
const { error } = SortSchema.validate(req.body);
|
||||
if (error) throw new InvalidPayloadException(error.message);
|
||||
|
||||
const service = new UtilsService({ accountability: req.accountability });
|
||||
const service = new UtilsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
await service.sort(req.collection, req.body);
|
||||
|
||||
return res.status(200).end();
|
||||
@@ -78,7 +81,10 @@ router.post(
|
||||
router.post(
|
||||
'/revert/:revision',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new RevisionsService({ accountability: req.accountability });
|
||||
const service = new RevisionsService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
await service.revert(req.params.revision);
|
||||
next();
|
||||
}),
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import express from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import { WebhooksService, MetaService } from '../services';
|
||||
import { ForbiddenException } from '../exceptions';
|
||||
import { ForbiddenException, InvalidPayloadException } from '../exceptions';
|
||||
import useCollection from '../middleware/use-collection';
|
||||
import { respond } from '../middleware/respond';
|
||||
import { PrimaryKey } from '../types';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -12,7 +13,10 @@ router.use(useCollection('directus_webhooks'));
|
||||
router.post(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new WebhooksService({ accountability: req.accountability });
|
||||
const service = new WebhooksService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const primaryKey = await service.create(req.body);
|
||||
|
||||
try {
|
||||
@@ -34,8 +38,14 @@ router.post(
|
||||
router.get(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new WebhooksService({ accountability: req.accountability });
|
||||
const metaService = new MetaService({ accountability: req.accountability });
|
||||
const service = new WebhooksService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const metaService = new MetaService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
|
||||
const records = await service.readByQuery(req.sanitizedQuery);
|
||||
const meta = await metaService.getMetaForQuery(req.collection, req.sanitizedQuery);
|
||||
@@ -49,7 +59,10 @@ router.get(
|
||||
router.get(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new WebhooksService({ accountability: req.accountability });
|
||||
const service = new WebhooksService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const record = await service.readByKey(pk as any, req.sanitizedQuery);
|
||||
|
||||
@@ -62,7 +75,10 @@ router.get(
|
||||
router.patch(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new WebhooksService({ accountability: req.accountability });
|
||||
const service = new WebhooksService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const primaryKey = await service.update(req.body, pk as any);
|
||||
|
||||
@@ -82,10 +98,31 @@ router.patch(
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
if (!req.body || Array.isArray(req.body) === false) {
|
||||
throw new InvalidPayloadException(`Body has to be an array of primary keys`);
|
||||
}
|
||||
|
||||
const service = new WebhooksService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
await service.delete(req.body as PrimaryKey[]);
|
||||
|
||||
return next();
|
||||
}),
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new WebhooksService({ accountability: req.accountability });
|
||||
const service = new WebhooksService({
|
||||
accountability: req.accountability,
|
||||
schema: req.schema,
|
||||
});
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
await service.delete(pk as any);
|
||||
|
||||
|
||||
@@ -4,8 +4,9 @@ import camelCase from 'camelcase';
|
||||
import path from 'path';
|
||||
import logger from '../logger';
|
||||
import env from '../env';
|
||||
import { performance } from 'perf_hooks';
|
||||
|
||||
import SchemaInspector from 'knex-schema-inspector';
|
||||
import SchemaInspector from '@directus/schema';
|
||||
|
||||
dotenv.config({ path: path.resolve(__dirname, '../../', '.env') });
|
||||
|
||||
@@ -46,6 +47,17 @@ if (env.DB_CLIENT === 'sqlite3') {
|
||||
|
||||
const database = knex(knexConfig);
|
||||
|
||||
const times: Record<string, number> = {};
|
||||
|
||||
database
|
||||
.on('query', (queryInfo) => {
|
||||
times[queryInfo.__knexUid] = performance.now();
|
||||
})
|
||||
.on('query-response', (response, queryInfo) => {
|
||||
const delta = performance.now() - times[queryInfo.__knexUid];
|
||||
logger.trace(`[${delta.toFixed(3)}ms] ${queryInfo.sql} [${queryInfo.bindings.join(', ')}]`);
|
||||
});
|
||||
|
||||
export async function validateDBConnection() {
|
||||
try {
|
||||
await database.raw('select 1+1 as result');
|
||||
@@ -57,4 +69,12 @@ export async function validateDBConnection() {
|
||||
}
|
||||
|
||||
export const schemaInspector = SchemaInspector(database);
|
||||
|
||||
export async function isInstalled() {
|
||||
// The existence of a directus_collections table alone isn't a "proper" check to see if everything
|
||||
// is installed correctly of course, but it's safe enough to assume that this collection only
|
||||
// exists when using the installer CLI.
|
||||
return await schemaInspector.hasTable('directus_collections');
|
||||
}
|
||||
|
||||
export default database;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import Knex from 'knex';
|
||||
|
||||
export async function up(knex: Knex) {
|
||||
await knex.schema.alterTable('directus_fields', (table) => {
|
||||
table.dropForeign(['collection']);
|
||||
});
|
||||
|
||||
await knex.schema.alterTable('directus_activity', (table) => {
|
||||
table.dropForeign(['collection']);
|
||||
});
|
||||
|
||||
await knex.schema.alterTable('directus_permissions', (table) => {
|
||||
table.dropForeign(['collection']);
|
||||
});
|
||||
|
||||
await knex.schema.alterTable('directus_presets', (table) => {
|
||||
table.dropForeign(['collection']);
|
||||
});
|
||||
|
||||
await knex.schema.alterTable('directus_relations', (table) => {
|
||||
table.dropForeign(['one_collection']);
|
||||
table.dropForeign(['many_collection']);
|
||||
});
|
||||
|
||||
await knex.schema.alterTable('directus_revisions', (table) => {
|
||||
table.dropForeign(['collection']);
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex) {
|
||||
await knex.schema.alterTable('directus_fields', (table) => {
|
||||
table.foreign('collection').references('directus_collections.collection');
|
||||
});
|
||||
|
||||
await knex.schema.alterTable('directus_activity', (table) => {
|
||||
table.foreign('collection').references('directus_collections.collection');
|
||||
});
|
||||
|
||||
await knex.schema.alterTable('directus_permissions', (table) => {
|
||||
table.foreign('collection').references('directus_collections.collection');
|
||||
});
|
||||
|
||||
await knex.schema.alterTable('directus_presets', (table) => {
|
||||
table.foreign('collection').references('directus_collections.collection');
|
||||
});
|
||||
|
||||
await knex.schema.alterTable('directus_relations', (table) => {
|
||||
table.foreign('one_collection').references('directus_collections.collection');
|
||||
table.foreign('many_collection').references('directus_collections.collection');
|
||||
});
|
||||
|
||||
await knex.schema.alterTable('directus_revisions', (table) => {
|
||||
table.foreign('collection').references('directus_collections.collection');
|
||||
});
|
||||
}
|
||||
128
api/src/database/migrations/20201029A-remove-system-relations.ts
Normal file
128
api/src/database/migrations/20201029A-remove-system-relations.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import Knex from 'knex';
|
||||
import { merge } from 'lodash';
|
||||
|
||||
export async function up(knex: Knex) {
|
||||
await knex('directus_relations')
|
||||
.delete()
|
||||
.where('many_collection', 'like', 'directus_%')
|
||||
.andWhere('one_collection', 'like', 'directus_%');
|
||||
}
|
||||
|
||||
export async function down(knex: Knex) {
|
||||
const defaults = {
|
||||
many_collection: 'directus_users',
|
||||
many_field: null,
|
||||
many_primary: null,
|
||||
one_collection: null,
|
||||
one_field: null,
|
||||
one_primary: null,
|
||||
junction_field: null,
|
||||
};
|
||||
|
||||
const systemRelations = [
|
||||
{
|
||||
many_collection: 'directus_users',
|
||||
many_field: 'role',
|
||||
many_primary: 'id',
|
||||
one_collection: 'directus_roles',
|
||||
one_field: 'users',
|
||||
one_primary: 'id',
|
||||
},
|
||||
{
|
||||
many_collection: 'directus_users',
|
||||
many_field: 'avatar',
|
||||
many_primary: 'id',
|
||||
one_collection: 'directus_files',
|
||||
one_primary: 'id',
|
||||
},
|
||||
{
|
||||
many_collection: 'directus_revisions',
|
||||
many_field: 'activity',
|
||||
many_primary: 'id',
|
||||
one_collection: 'directus_activity',
|
||||
one_field: 'revisions',
|
||||
one_primary: 'id',
|
||||
},
|
||||
{
|
||||
many_collection: 'directus_presets',
|
||||
many_field: 'user',
|
||||
many_primary: 'id',
|
||||
one_collection: 'directus_users',
|
||||
one_primary: 'id',
|
||||
},
|
||||
{
|
||||
many_collection: 'directus_presets',
|
||||
many_field: 'role',
|
||||
many_primary: 'id',
|
||||
one_collection: 'directus_roles',
|
||||
one_primary: 'id',
|
||||
},
|
||||
{
|
||||
many_collection: 'directus_folders',
|
||||
many_field: 'parent',
|
||||
many_primary: 'id',
|
||||
one_collection: 'directus_folders',
|
||||
one_primary: 'id',
|
||||
},
|
||||
{
|
||||
many_collection: 'directus_files',
|
||||
many_field: 'folder',
|
||||
many_primary: 'id',
|
||||
one_collection: 'directus_folders',
|
||||
one_primary: 'id',
|
||||
},
|
||||
{
|
||||
many_collection: 'directus_files',
|
||||
many_field: 'uploaded_by',
|
||||
many_primary: 'id',
|
||||
one_collection: 'directus_users',
|
||||
one_primary: 'id',
|
||||
},
|
||||
{
|
||||
many_collection: 'directus_fields',
|
||||
many_field: 'collection',
|
||||
many_primary: 'id',
|
||||
one_collection: 'directus_collections',
|
||||
one_field: 'fields',
|
||||
one_primary: 'collection',
|
||||
},
|
||||
{
|
||||
many_collection: 'directus_activity',
|
||||
many_field: 'user',
|
||||
many_primary: 'id',
|
||||
one_collection: 'directus_users',
|
||||
one_primary: 'id',
|
||||
},
|
||||
{
|
||||
many_collection: 'directus_settings',
|
||||
many_field: 'project_logo',
|
||||
many_primary: 'id',
|
||||
one_collection: 'directus_files',
|
||||
one_primary: 'id',
|
||||
},
|
||||
{
|
||||
many_collection: 'directus_settings',
|
||||
many_field: 'public_foreground',
|
||||
many_primary: 'id',
|
||||
one_collection: 'directus_files',
|
||||
one_primary: 'id',
|
||||
},
|
||||
{
|
||||
many_collection: 'directus_settings',
|
||||
many_field: 'public_background',
|
||||
many_primary: 'id',
|
||||
one_collection: 'directus_files',
|
||||
one_primary: 'id',
|
||||
},
|
||||
].map((row) => {
|
||||
for (const [key, value] of Object.entries(row)) {
|
||||
if (value !== null && (typeof value === 'object' || Array.isArray(value))) {
|
||||
(row as any)[key] = JSON.stringify(value);
|
||||
}
|
||||
}
|
||||
|
||||
return merge({}, defaults, row);
|
||||
});
|
||||
|
||||
await knex.insert(systemRelations).into('directus_relations');
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import Knex from 'knex';
|
||||
import { merge } from 'lodash';
|
||||
|
||||
export async function up(knex: Knex) {
|
||||
await knex('directus_collections').delete().where('collection', 'like', 'directus_%');
|
||||
}
|
||||
|
||||
export async function down(knex: Knex) {
|
||||
const defaults = {
|
||||
collection: null,
|
||||
hidden: false,
|
||||
singleton: false,
|
||||
icon: null,
|
||||
note: null,
|
||||
translations: null,
|
||||
display_template: null,
|
||||
};
|
||||
|
||||
const systemCollections = [
|
||||
{
|
||||
collection: 'directus_activity',
|
||||
note: 'Accountability logs for all events',
|
||||
},
|
||||
{
|
||||
collection: 'directus_collections',
|
||||
icon: 'list_alt',
|
||||
note: 'Additional collection configuration and metadata',
|
||||
},
|
||||
{
|
||||
collection: 'directus_fields',
|
||||
icon: 'input',
|
||||
note: 'Additional field configuration and metadata',
|
||||
},
|
||||
{
|
||||
collection: 'directus_files',
|
||||
icon: 'folder',
|
||||
note: 'Metadata for all managed file assets',
|
||||
},
|
||||
{
|
||||
collection: 'directus_folders',
|
||||
note: 'Provides virtual directories for files',
|
||||
},
|
||||
{
|
||||
collection: 'directus_permissions',
|
||||
icon: 'admin_panel_settings',
|
||||
note: 'Access permissions for each role',
|
||||
},
|
||||
{
|
||||
collection: 'directus_presets',
|
||||
icon: 'bookmark_border',
|
||||
note: 'Presets for collection defaults and bookmarks',
|
||||
},
|
||||
{
|
||||
collection: 'directus_relations',
|
||||
icon: 'merge_type',
|
||||
note: 'Relationship configuration and metadata',
|
||||
},
|
||||
{
|
||||
collection: 'directus_revisions',
|
||||
note: 'Data snapshots for all activity',
|
||||
},
|
||||
{
|
||||
collection: 'directus_roles',
|
||||
icon: 'supervised_user_circle',
|
||||
note: 'Permission groups for system users',
|
||||
},
|
||||
{
|
||||
collection: 'directus_sessions',
|
||||
note: 'User session information',
|
||||
},
|
||||
{
|
||||
collection: 'directus_settings',
|
||||
singleton: true,
|
||||
note: 'Project configuration options',
|
||||
},
|
||||
{
|
||||
collection: 'directus_users',
|
||||
archive_field: 'status',
|
||||
archive_value: 'archived',
|
||||
unarchive_value: 'draft',
|
||||
icon: 'people_alt',
|
||||
note: 'System users for the platform',
|
||||
},
|
||||
{
|
||||
collection: 'directus_webhooks',
|
||||
note: 'Configuration for event-based HTTP requests',
|
||||
},
|
||||
].map((row) => {
|
||||
for (const [key, value] of Object.entries(row)) {
|
||||
if (value !== null && (typeof value === 'object' || Array.isArray(value))) {
|
||||
(row as any)[key] = JSON.stringify(value);
|
||||
}
|
||||
}
|
||||
|
||||
return merge({}, defaults, row);
|
||||
});
|
||||
|
||||
await knex.insert(systemCollections).into('directus_collections');
|
||||
}
|
||||
1654
api/src/database/migrations/20201029C-remove-system-fields.ts
Normal file
1654
api/src/database/migrations/20201029C-remove-system-fields.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,156 @@
|
||||
import Knex from 'knex';
|
||||
|
||||
const updates = [
|
||||
{
|
||||
table: 'directus_fields',
|
||||
constraints: [
|
||||
{
|
||||
column: 'group',
|
||||
references: 'directus_fields.id',
|
||||
onDelete: 'SET NULL',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: 'directus_files',
|
||||
constraints: [
|
||||
{
|
||||
column: 'folder',
|
||||
references: 'directus_folders.id',
|
||||
onDelete: 'SET NULL',
|
||||
},
|
||||
{
|
||||
column: 'uploaded_by',
|
||||
references: 'directus_users.id',
|
||||
onDelete: 'SET NULL',
|
||||
},
|
||||
{
|
||||
column: 'modified_by',
|
||||
references: 'directus_users.id',
|
||||
onDelete: 'SET NULL',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: 'directus_folders',
|
||||
constraints: [
|
||||
{
|
||||
column: 'parent',
|
||||
references: 'directus_folders.id',
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: 'directus_permissions',
|
||||
constraints: [
|
||||
{
|
||||
column: 'role',
|
||||
references: 'directus_roles.id',
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: 'directus_presets',
|
||||
constraints: [
|
||||
{
|
||||
column: 'user',
|
||||
references: 'directus_users.id',
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
{
|
||||
column: 'role',
|
||||
references: 'directus_roles.id',
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: 'directus_revisions',
|
||||
constraints: [
|
||||
{
|
||||
column: 'activity',
|
||||
references: 'directus_activity.id',
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
{
|
||||
column: 'parent',
|
||||
references: 'directus_revisions.id',
|
||||
onDelete: 'SET NULL',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: 'directus_sessions',
|
||||
constraints: [
|
||||
{
|
||||
column: 'user',
|
||||
references: 'directus_users.id',
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: 'directus_settings',
|
||||
constraints: [
|
||||
{
|
||||
column: 'project_logo',
|
||||
references: 'directus_files.id',
|
||||
onDelete: 'SET NULL',
|
||||
},
|
||||
{
|
||||
column: 'public_foreground',
|
||||
references: 'directus_files.id',
|
||||
onDelete: 'SET NULL',
|
||||
},
|
||||
{
|
||||
column: 'public_background',
|
||||
references: 'directus_files.id',
|
||||
onDelete: 'SET NULL',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: 'directus_users',
|
||||
constraints: [
|
||||
{
|
||||
column: 'role',
|
||||
references: 'directus_roles.id',
|
||||
onDelete: 'SET NULL',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export async function up(knex: Knex) {
|
||||
for (const update of updates) {
|
||||
await knex.schema.alterTable(update.table, (table) => {
|
||||
for (const constraint of update.constraints) {
|
||||
table.dropForeign([constraint.column]);
|
||||
|
||||
table
|
||||
.foreign(constraint.column)
|
||||
.references(constraint.references)
|
||||
.onUpdate('CASCADE')
|
||||
.onDelete(constraint.onDelete);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex) {
|
||||
for (const update of updates) {
|
||||
await knex.schema.alterTable(update.table, (table) => {
|
||||
for (const constraint of update.constraints) {
|
||||
table.dropForeign([constraint.column]);
|
||||
|
||||
table
|
||||
.foreign(constraint.column)
|
||||
.references(constraint.references)
|
||||
.onUpdate('NO ACTION')
|
||||
.onDelete('NO ACTION');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import Knex from 'knex';
|
||||
|
||||
export async function up(knex: Knex) {
|
||||
await knex.schema.alterTable('directus_webhooks', (table) => {
|
||||
table.text('url').alter();
|
||||
});
|
||||
}
|
||||
|
||||
export async function down(knex: Knex) {
|
||||
await knex.schema.alterTable('directus_webhooks', (table) => {
|
||||
table.string('url').alter();
|
||||
});
|
||||
}
|
||||
@@ -11,7 +11,10 @@ type Migration = {
|
||||
|
||||
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');
|
||||
|
||||
migrationFiles = migrationFiles.filter(
|
||||
(file: string) => file.startsWith('run') === false && file.endsWith('.d.ts') === false
|
||||
);
|
||||
|
||||
const completedMigrations = await database
|
||||
.select<Migration[]>('*')
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { AST, NestedCollectionNode, FieldNode, M2ONode, O2MNode } from '../types/ast';
|
||||
import { AST, NestedCollectionNode, FieldNode } from '../types/ast';
|
||||
import { clone, cloneDeep, uniq, pick } from 'lodash';
|
||||
import database from './index';
|
||||
import SchemaInspector from 'knex-schema-inspector';
|
||||
import { Query, Item } from '../types';
|
||||
import { Query, Item, SchemaOverview } from '../types';
|
||||
import { PayloadService } from '../services/payload';
|
||||
import applyQuery from '../utils/apply-query';
|
||||
import Knex, { QueryBuilder } from 'knex';
|
||||
@@ -16,6 +15,7 @@ type RunASTOptions = {
|
||||
|
||||
export default async function runAST(
|
||||
originalAST: AST | NestedCollectionNode,
|
||||
schema: SchemaOverview,
|
||||
options?: RunASTOptions
|
||||
): Promise<null | Item | Item[]> {
|
||||
const ast = cloneDeep(originalAST);
|
||||
@@ -47,18 +47,25 @@ export default async function runAST(
|
||||
const { columnsToSelect, primaryKeyField, nestedCollectionNodes } = await parseCurrentLevel(
|
||||
collection,
|
||||
children,
|
||||
knex
|
||||
schema
|
||||
);
|
||||
|
||||
// The actual knex query builder instance. This is a promise that resolves with the raw items from the db
|
||||
const dbQuery = await getDBQuery(knex, collection, columnsToSelect, query, primaryKeyField);
|
||||
const dbQuery = await getDBQuery(
|
||||
knex,
|
||||
collection,
|
||||
columnsToSelect,
|
||||
query,
|
||||
primaryKeyField,
|
||||
schema
|
||||
);
|
||||
|
||||
const rawItems: Item | Item[] = await dbQuery;
|
||||
|
||||
if (!rawItems) return null;
|
||||
|
||||
// Run the items through the special transforms
|
||||
const payloadService = new PayloadService(collection, { knex });
|
||||
const payloadService = new PayloadService(collection, { knex, schema });
|
||||
let items: null | Item | Item[] = await payloadService.processValues('read', rawItems);
|
||||
|
||||
if (!items || items.length === 0) return items;
|
||||
@@ -78,10 +85,11 @@ export default async function runAST(
|
||||
nestedNode.query.limit = -1;
|
||||
}
|
||||
|
||||
let nestedItems = await runAST(nestedNode, { knex, child: true });
|
||||
let nestedItems = await runAST(nestedNode, schema, { knex, child: true });
|
||||
|
||||
if (nestedItems) {
|
||||
// Merge all fetched nested records with the parent items
|
||||
|
||||
items = mergeWithParentItems(nestedItems, items, nestedNode, tempLimit);
|
||||
}
|
||||
}
|
||||
@@ -101,15 +109,10 @@ export default async function runAST(
|
||||
async function parseCurrentLevel(
|
||||
collection: string,
|
||||
children: (NestedCollectionNode | FieldNode)[],
|
||||
knex: Knex
|
||||
schema: SchemaOverview
|
||||
) {
|
||||
const schemaInspector = SchemaInspector(knex);
|
||||
|
||||
const primaryKeyField = await schemaInspector.primary(collection);
|
||||
|
||||
const columnsInCollection = (await schemaInspector.columns(collection)).map(
|
||||
({ column }) => column
|
||||
);
|
||||
const primaryKeyField = schema[collection].primary;
|
||||
const columnsInCollection = Object.keys(schema[collection].columns);
|
||||
|
||||
const columnsToSelect: string[] = [];
|
||||
const nestedCollectionNodes: NestedCollectionNode[] = [];
|
||||
@@ -150,7 +153,8 @@ async function getDBQuery(
|
||||
table: string,
|
||||
columns: string[],
|
||||
query: Query,
|
||||
primaryKeyField: string
|
||||
primaryKeyField: string,
|
||||
schema: SchemaOverview
|
||||
): Promise<QueryBuilder> {
|
||||
let dbQuery = knex.select(columns.map((column) => `${table}.${column}`)).from(table);
|
||||
|
||||
@@ -164,7 +168,7 @@ async function getDBQuery(
|
||||
|
||||
query.sort = query.sort || [{ column: primaryKeyField, order: 'asc' }];
|
||||
|
||||
await applyQuery(table, dbQuery, queryCopy);
|
||||
await applyQuery(knex, table, dbQuery, queryCopy, schema);
|
||||
|
||||
return dbQuery;
|
||||
}
|
||||
@@ -269,7 +273,7 @@ function mergeWithParentItems(
|
||||
nestedItem[nestedNode.relation.many_field] ===
|
||||
parentItem[nestedNode.relation.one_primary!] ||
|
||||
nestedItem[nestedNode.relation.many_field]?.[
|
||||
nestedNode.relation.many_primary
|
||||
nestedNode.relation.one_primary!
|
||||
] === parentItem[nestedNode.relation.one_primary!]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
table: directus_presets
|
||||
|
||||
defaults:
|
||||
bookmark: null
|
||||
user: null
|
||||
role: null
|
||||
collection: null
|
||||
search: null
|
||||
filters: '[]'
|
||||
layout: tabular
|
||||
layout_query: null
|
||||
layout_options: null
|
||||
|
||||
data:
|
||||
- collection: directus_files
|
||||
layout: cards
|
||||
layout_query:
|
||||
cards:
|
||||
sort: -uploaded_on
|
||||
layout_options:
|
||||
cards:
|
||||
icon: insert_drive_file
|
||||
title: '{{ title }}'
|
||||
subtitle: '{{ type }} • {{ filesize }}'
|
||||
size: 4
|
||||
imageFit: crop
|
||||
|
||||
- collection: directus_users
|
||||
layout: cards
|
||||
layout_options:
|
||||
cards:
|
||||
icon: account_circle
|
||||
title: '{{ first_name }} {{ last_name }}'
|
||||
subtitle: '{{ email }}'
|
||||
size: 4
|
||||
|
||||
- collection: directus_activity
|
||||
layout: tabular
|
||||
layout_query:
|
||||
tabular:
|
||||
sort: -timestamp
|
||||
fields:
|
||||
- action
|
||||
- collection
|
||||
- timestamp
|
||||
- user
|
||||
layout_options:
|
||||
tabular:
|
||||
widths:
|
||||
action: 100
|
||||
collection: 210
|
||||
timestamp: 240
|
||||
user: 240
|
||||
|
||||
- collection: directus_webhooks
|
||||
layout: tabular
|
||||
layout_query:
|
||||
tabular:
|
||||
fields:
|
||||
- status
|
||||
- name
|
||||
- method
|
||||
- url
|
||||
layout_options:
|
||||
tabular:
|
||||
widths:
|
||||
status: 36
|
||||
name: 300
|
||||
|
||||
- collection: directus_roles
|
||||
layout: tabular
|
||||
layout_query:
|
||||
tabular:
|
||||
fields:
|
||||
- icon
|
||||
- name
|
||||
- description
|
||||
layout_options:
|
||||
tabular:
|
||||
widths:
|
||||
icon: 36
|
||||
name: 248
|
||||
description: 500
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# directus_fields isn't surfaced in the app
|
||||
table: directus_fields
|
||||
|
||||
fields:
|
||||
- collection: directus_fields
|
||||
field: options
|
||||
hidden: true
|
||||
locked: true
|
||||
special: json
|
||||
- collection: directus_fields
|
||||
field: display_options
|
||||
hidden: true
|
||||
locked: true
|
||||
special: json
|
||||
- collection: directus_fields
|
||||
field: locked
|
||||
hidden: true
|
||||
locked: true
|
||||
special: boolean
|
||||
- collection: directus_fields
|
||||
field: readonly
|
||||
hidden: true
|
||||
locked: true
|
||||
special: boolean
|
||||
- collection: directus_fields
|
||||
field: hidden
|
||||
hidden: true
|
||||
locked: true
|
||||
special: boolean
|
||||
- collection: directus_fields
|
||||
field: special
|
||||
hidden: true
|
||||
locked: true
|
||||
special: csv
|
||||
- collection: directus_fields
|
||||
field: translations
|
||||
hidden: true
|
||||
locked: true
|
||||
special: json
|
||||
@@ -1,8 +0,0 @@
|
||||
table: directus_folders
|
||||
|
||||
fields:
|
||||
- collection: directus_folders
|
||||
field: id
|
||||
interface: text-input
|
||||
locked: true
|
||||
special: uuid
|
||||
@@ -1,14 +0,0 @@
|
||||
# directus_permissions isn't surfaced in the app
|
||||
table: directus_permissions
|
||||
|
||||
fields:
|
||||
- collection: directus_permissions
|
||||
field: permissions
|
||||
hidden: true
|
||||
locked: true
|
||||
special: json
|
||||
- collection: directus_permissions
|
||||
field: presets
|
||||
hidden: true
|
||||
locked: true
|
||||
special: json
|
||||
@@ -1,19 +0,0 @@
|
||||
table: directus_presets
|
||||
|
||||
fields:
|
||||
# directus_presets isn't surfaced in the app
|
||||
- collection: directus_presets
|
||||
field: filters
|
||||
hidden: true
|
||||
locked: true
|
||||
special: json
|
||||
- collection: directus_presets
|
||||
field: layout_query
|
||||
hidden: true
|
||||
locked: true
|
||||
special: json
|
||||
- collection: directus_presets
|
||||
field: layout_options
|
||||
hidden: true
|
||||
locked: true
|
||||
special: json
|
||||
@@ -1,14 +0,0 @@
|
||||
table: directus_revisions
|
||||
|
||||
fields:
|
||||
# directus_revisions isn't surfaced in the app
|
||||
- collection: directus_revisions
|
||||
field: data
|
||||
hidden: true
|
||||
locked: true
|
||||
special: json
|
||||
- collection: directus_revisions
|
||||
field: delta
|
||||
hidden: true
|
||||
locked: true
|
||||
special: json
|
||||
@@ -1,7 +0,0 @@
|
||||
table: directus_relations
|
||||
|
||||
fields:
|
||||
- collection: directus_relations
|
||||
field: one_allowed_collections
|
||||
locked: true
|
||||
special: csv
|
||||
@@ -25,33 +25,6 @@ type TableSeed = {
|
||||
};
|
||||
};
|
||||
|
||||
type RowSeed = {
|
||||
table: string;
|
||||
defaults: Record<string, any>;
|
||||
data: Record<string, any>[];
|
||||
};
|
||||
|
||||
type FieldSeed = {
|
||||
table: string;
|
||||
fields: {
|
||||
collection: string;
|
||||
field: string;
|
||||
special: string | null;
|
||||
interface: string | null;
|
||||
options: Record<string, any> | null;
|
||||
display: string | null;
|
||||
display_options: Record<string, any> | null;
|
||||
locked: boolean;
|
||||
readonly: boolean;
|
||||
hidden: boolean;
|
||||
sort: number | null;
|
||||
width: string | null;
|
||||
group: number | null;
|
||||
translations: Record<string, any> | null;
|
||||
note: string | null;
|
||||
}[];
|
||||
};
|
||||
|
||||
export default async function runSeed(database: Knex) {
|
||||
const exists = await database.schema.hasTable('directus_collections');
|
||||
|
||||
@@ -59,19 +32,13 @@ export default async function runSeed(database: Knex) {
|
||||
throw new Error('Database is already installed');
|
||||
}
|
||||
|
||||
await createTables(database);
|
||||
await insertRows(database);
|
||||
await insertFields(database);
|
||||
}
|
||||
|
||||
async function createTables(database: Knex) {
|
||||
const tableSeeds = await fse.readdir(path.resolve(__dirname, './01-tables/'));
|
||||
const tableSeeds = await fse.readdir(path.resolve(__dirname));
|
||||
|
||||
for (const tableSeedFile of tableSeeds) {
|
||||
const yamlRaw = await fse.readFile(
|
||||
path.resolve(__dirname, './01-tables', tableSeedFile),
|
||||
'utf8'
|
||||
);
|
||||
if (tableSeedFile.startsWith('run')) continue;
|
||||
|
||||
const yamlRaw = await fse.readFile(path.resolve(__dirname, tableSeedFile), 'utf8');
|
||||
|
||||
const seedData = yaml.safeLoad(yamlRaw) as TableSeed;
|
||||
|
||||
await database.schema.createTable(seedData.table, (tableBuilder) => {
|
||||
@@ -119,8 +86,7 @@ async function createTables(database: Knex) {
|
||||
}
|
||||
|
||||
if (columnInfo.references) {
|
||||
tableBuilder
|
||||
.foreign(columnName)
|
||||
column
|
||||
.references(columnInfo.references.column)
|
||||
.inTable(columnInfo.references.table);
|
||||
}
|
||||
@@ -128,61 +94,3 @@ async function createTables(database: Knex) {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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 seedData = yaml.safeLoad(yamlRaw) as RowSeed;
|
||||
|
||||
const dataWithDefaults = seedData.data.map((row) => {
|
||||
for (const [key, value] of Object.entries(row)) {
|
||||
if (value !== null && (typeof value === 'object' || Array.isArray(value))) {
|
||||
row[key] = JSON.stringify(value);
|
||||
}
|
||||
}
|
||||
|
||||
return merge({}, seedData.defaults, row);
|
||||
});
|
||||
|
||||
await database.batchInsert(seedData.table, dataWithDefaults);
|
||||
}
|
||||
}
|
||||
|
||||
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 defaults = yaml.safeLoad(defaultsYaml) as FieldSeed;
|
||||
|
||||
for (const fieldSeedFile of fieldSeeds) {
|
||||
const yamlRaw = await fse.readFile(
|
||||
path.resolve(__dirname, './03-fields', fieldSeedFile),
|
||||
'utf8'
|
||||
);
|
||||
const seedData = yaml.safeLoad(yamlRaw) as FieldSeed;
|
||||
|
||||
if (fieldSeedFile === '_defaults.yaml') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const dataWithDefaults = seedData.fields.map((row) => {
|
||||
for (const [key, value] of Object.entries(row)) {
|
||||
if (value !== null && (typeof value === 'object' || Array.isArray(value))) {
|
||||
(row as any)[key] = JSON.stringify(value);
|
||||
}
|
||||
}
|
||||
|
||||
return merge({}, defaults, row);
|
||||
});
|
||||
|
||||
await database.batchInsert('directus_fields', dataWithDefaults);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ data:
|
||||
note: Metadata for all managed file assets
|
||||
- collection: directus_folders
|
||||
note: Provides virtual directories for files
|
||||
- collection: directus_migrations
|
||||
note: What version of the database you're using
|
||||
- collection: directus_permissions
|
||||
icon: admin_panel_settings
|
||||
note: Access permissions for each role
|
||||
11
api/src/database/system-data/collections/index.ts
Normal file
11
api/src/database/system-data/collections/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { requireYAML } from '../../../utils/require-yaml';
|
||||
import { merge } from 'lodash';
|
||||
import { CollectionMeta } from '../../../types';
|
||||
|
||||
const systemData = requireYAML(require.resolve('./collections.yaml'));
|
||||
|
||||
export const systemCollectionRows: CollectionMeta[] = systemData.data.map(
|
||||
(row: Record<string, any>) => {
|
||||
return merge({ system: true }, systemData.defaults, row);
|
||||
}
|
||||
);
|
||||
@@ -1,8 +1,13 @@
|
||||
table: directus_activity
|
||||
|
||||
fields:
|
||||
- collection: directus_activity
|
||||
field: action
|
||||
- field: id
|
||||
width: half
|
||||
|
||||
- field: item
|
||||
width: half
|
||||
|
||||
- field: action
|
||||
display: labels
|
||||
display_options:
|
||||
defaultForeground: '#263238'
|
||||
@@ -24,41 +29,47 @@ fields:
|
||||
value: authenticate
|
||||
foreground: '#9b51e0'
|
||||
background: '#e6d3f7'
|
||||
- collection: directus_activity
|
||||
field: collection
|
||||
width: half
|
||||
|
||||
- field: collection
|
||||
display: collection
|
||||
display_options:
|
||||
icon: true
|
||||
- collection: directus_activity
|
||||
field: timestamp
|
||||
width: half
|
||||
|
||||
- field: timestamp
|
||||
display: datetime
|
||||
options:
|
||||
relative: true
|
||||
- collection: directus_activity
|
||||
field: user
|
||||
width: half
|
||||
|
||||
- field: user
|
||||
display: user
|
||||
- collection: directus_activity
|
||||
field: comment
|
||||
width: half
|
||||
|
||||
- field: comment
|
||||
display: formatted-text
|
||||
display_options:
|
||||
subdued: true
|
||||
- collection: directus_activity
|
||||
field: user_agent
|
||||
width: half
|
||||
|
||||
- field: user_agent
|
||||
display: formatted-text
|
||||
display_options:
|
||||
font: monospace
|
||||
- collection: directus_activity
|
||||
field: ip
|
||||
width: half
|
||||
|
||||
- field: ip
|
||||
display: formatted-text
|
||||
display_options:
|
||||
font: monospace
|
||||
- collection: directus_activity
|
||||
field: revisions
|
||||
width: half
|
||||
|
||||
- field: revisions
|
||||
interface: one-to-many
|
||||
locked: true
|
||||
special: o2m
|
||||
options:
|
||||
fields:
|
||||
- collection
|
||||
- item
|
||||
width: full
|
||||
width: half
|
||||
@@ -1,69 +1,54 @@
|
||||
table: directus_collections
|
||||
|
||||
fields:
|
||||
- collection: directus_collections
|
||||
field: collection_divider
|
||||
- field: collection_divider
|
||||
special: alias
|
||||
interface: divider
|
||||
options:
|
||||
icon: box
|
||||
title: Collection Setup
|
||||
color: '#2F80ED'
|
||||
locked: true
|
||||
sort: 1
|
||||
width: full
|
||||
- collection: directus_collections
|
||||
field: collection
|
||||
|
||||
- field: collection
|
||||
interface: text-input
|
||||
options:
|
||||
font: monospace
|
||||
locked: true
|
||||
readonly: true
|
||||
sort: 2
|
||||
width: half
|
||||
- collection: directus_collections
|
||||
field: icon
|
||||
|
||||
- field: icon
|
||||
interface: icon
|
||||
options:
|
||||
locked: true
|
||||
sort: 3
|
||||
width: half
|
||||
- collection: directus_collections
|
||||
field: note
|
||||
|
||||
- field: note
|
||||
interface: text-input
|
||||
options:
|
||||
placeholder: A description of this collection...
|
||||
locked: true
|
||||
sort: 4
|
||||
width: full
|
||||
- collection: directus_collections
|
||||
field: display_template
|
||||
|
||||
- field: display_template
|
||||
interface: display-template
|
||||
options:
|
||||
collectionField: collection
|
||||
locked: true
|
||||
sort: 5
|
||||
width: full
|
||||
- collection: directus_collections
|
||||
field: hidden
|
||||
|
||||
- field: hidden
|
||||
special: boolean
|
||||
interface: toggle
|
||||
options:
|
||||
label: Hide within the App
|
||||
locked: true
|
||||
sort: 6
|
||||
width: half
|
||||
- collection: directus_collections
|
||||
field: singleton
|
||||
|
||||
- field: singleton
|
||||
special: boolean
|
||||
interface: toggle
|
||||
options:
|
||||
label: Treat as single object
|
||||
locked: true
|
||||
sort: 7
|
||||
width: half
|
||||
- collection: directus_collections
|
||||
field: translations
|
||||
|
||||
- field: translations
|
||||
special: json
|
||||
interface: repeater
|
||||
options:
|
||||
@@ -85,72 +70,58 @@ fields:
|
||||
width: half
|
||||
options:
|
||||
placeholder: Enter a translation...
|
||||
locked: true
|
||||
sort: 8
|
||||
width: full
|
||||
- collection: directus_collections
|
||||
field: archive_divider
|
||||
|
||||
- field: archive_divider
|
||||
special: alias
|
||||
interface: divider
|
||||
options:
|
||||
icon: archive
|
||||
title: Archive
|
||||
color: '#2F80ED'
|
||||
locked: true
|
||||
sort: 9
|
||||
width: full
|
||||
- collection: directus_collections
|
||||
field: archive_field
|
||||
|
||||
- field: archive_field
|
||||
interface: field
|
||||
options:
|
||||
collectionField: collection
|
||||
allowNone: true
|
||||
placeholder: Choose a field...
|
||||
locked: true
|
||||
sort: 10
|
||||
width: half
|
||||
- collection: directus_collections
|
||||
field: archive_app_filter
|
||||
|
||||
- field: archive_app_filter
|
||||
interface: toggle
|
||||
special: boolean
|
||||
options:
|
||||
label: Enable App Archive Filter
|
||||
locked: true
|
||||
sort: 11
|
||||
width: half
|
||||
- collection: directus_collections
|
||||
field: archive_value
|
||||
|
||||
- field: archive_value
|
||||
interface: text-input
|
||||
options:
|
||||
font: monospace
|
||||
iconRight: archive
|
||||
placeholder: Value set when archiving...
|
||||
locked: true
|
||||
sort: 12
|
||||
width: half
|
||||
- collection: directus_collections
|
||||
field: unarchive_value
|
||||
|
||||
- field: unarchive_value
|
||||
interface: text-input
|
||||
options:
|
||||
font: monospace
|
||||
iconRight: unarchive
|
||||
placeholder: Value set when unarchiving...
|
||||
locked: true
|
||||
sort: 13
|
||||
width: half
|
||||
- collection: directus_collections
|
||||
field: sort_divider
|
||||
|
||||
- field: sort_divider
|
||||
special: alias
|
||||
interface: divider
|
||||
options:
|
||||
icon: sort
|
||||
title: Sort
|
||||
color: '#2F80ED'
|
||||
locked: true
|
||||
sort: 14
|
||||
width: full
|
||||
- collection: directus_collections
|
||||
field: sort_field
|
||||
|
||||
- field: sort_field
|
||||
interface: field
|
||||
options:
|
||||
collectionField: collection
|
||||
@@ -160,6 +131,4 @@ fields:
|
||||
- decimal
|
||||
- integer
|
||||
allowNone: true
|
||||
locked: true
|
||||
sort: 15
|
||||
width: half
|
||||
81
api/src/database/system-data/fields/fields.yaml
Normal file
81
api/src/database/system-data/fields/fields.yaml
Normal file
@@ -0,0 +1,81 @@
|
||||
# directus_fields isn't surfaced in the app
|
||||
table: directus_fields
|
||||
|
||||
fields:
|
||||
- collection: directus_fields
|
||||
field: id
|
||||
width: half
|
||||
|
||||
- collection: directus_fields
|
||||
field: collection
|
||||
width: half
|
||||
|
||||
- collection: directus_fields
|
||||
field: field
|
||||
width: half
|
||||
|
||||
- collection: directus_fields
|
||||
field: special
|
||||
hidden: true
|
||||
special: csv
|
||||
width: half
|
||||
|
||||
- collection: directus_fields
|
||||
field: interface
|
||||
width: half
|
||||
|
||||
- collection: directus_fields
|
||||
field: options
|
||||
hidden: true
|
||||
special: json
|
||||
width: half
|
||||
|
||||
- collection: directus_fields
|
||||
field: display
|
||||
width: half
|
||||
|
||||
- collection: directus_fields
|
||||
field: display_options
|
||||
hidden: true
|
||||
special: json
|
||||
width: half
|
||||
|
||||
- collection: directus_fields
|
||||
field: locked
|
||||
hidden: true
|
||||
special: boolean
|
||||
width: half
|
||||
|
||||
- collection: directus_fields
|
||||
field: readonly
|
||||
hidden: true
|
||||
special: boolean
|
||||
width: half
|
||||
|
||||
- collection: directus_fields
|
||||
field: hidden
|
||||
hidden: true
|
||||
special: boolean
|
||||
width: half
|
||||
|
||||
- collection: directus_fields
|
||||
field: sort
|
||||
width: half
|
||||
|
||||
- collection: directus_fields
|
||||
field: width
|
||||
width: half
|
||||
|
||||
- collection: directus_fields
|
||||
field: group
|
||||
width: half
|
||||
|
||||
- collection: directus_fields
|
||||
field: translations
|
||||
hidden: true
|
||||
special: json
|
||||
width: half
|
||||
|
||||
- collection: directus_fields
|
||||
field: note
|
||||
width: half
|
||||
@@ -1,114 +1,117 @@
|
||||
table: directus_files
|
||||
|
||||
fields:
|
||||
- collection: directus_files
|
||||
field: id
|
||||
- field: id
|
||||
hidden: true
|
||||
interface: text-input
|
||||
locked: true
|
||||
special: uuid
|
||||
- collection: directus_files
|
||||
field: title
|
||||
|
||||
- field: title
|
||||
interface: text-input
|
||||
locked: true
|
||||
options:
|
||||
iconRight: title
|
||||
placeholder: A unique title...
|
||||
sort: 1
|
||||
width: full
|
||||
- collection: directus_files
|
||||
field: description
|
||||
|
||||
- field: description
|
||||
interface: textarea
|
||||
locked: true
|
||||
sort: 2
|
||||
width: full
|
||||
options:
|
||||
placeholder: An optional description...
|
||||
- collection: directus_files
|
||||
field: tags
|
||||
|
||||
- field: tags
|
||||
interface: tags
|
||||
locked: true
|
||||
options:
|
||||
iconRight: local_offer
|
||||
special: json
|
||||
sort: 3
|
||||
width: full
|
||||
display: tags
|
||||
- collection: directus_files
|
||||
field: location
|
||||
|
||||
- field: location
|
||||
interface: text-input
|
||||
locked: true
|
||||
options:
|
||||
iconRight: place
|
||||
placeholder: An optional location...
|
||||
sort: 4
|
||||
width: half
|
||||
- collection: directus_files
|
||||
field: storage
|
||||
|
||||
- field: storage
|
||||
interface: text-input
|
||||
locked: true
|
||||
options:
|
||||
iconRight: storage
|
||||
sort: 5
|
||||
width: half
|
||||
readonly: true
|
||||
- collection: directus_files
|
||||
field: storage_divider
|
||||
|
||||
- field: storage_divider
|
||||
interface: divider
|
||||
locked: true
|
||||
options:
|
||||
icon: insert_drive_file
|
||||
title: File Naming
|
||||
color: '#2F80ED'
|
||||
special: alias
|
||||
sort: 6
|
||||
width: full
|
||||
- collection: directus_files
|
||||
field: filename_disk
|
||||
|
||||
- field: filename_disk
|
||||
interface: text-input
|
||||
locked: true
|
||||
options:
|
||||
iconRight: publish
|
||||
placeholder: Name on disk storage...
|
||||
sort: 7
|
||||
width: half
|
||||
- collection: directus_files
|
||||
field: filename_download
|
||||
|
||||
- field: filename_download
|
||||
interface: text-input
|
||||
locked: true
|
||||
options:
|
||||
iconRight: get_app
|
||||
placeholder: Name when downloading...
|
||||
sort: 8
|
||||
width: half
|
||||
- collection: directus_files
|
||||
field: metadata
|
||||
|
||||
- field: metadata
|
||||
hidden: true
|
||||
locked: true
|
||||
special: json
|
||||
- collection: directus_files
|
||||
field: type
|
||||
|
||||
- field: type
|
||||
display: mime-type
|
||||
- collection: directus_files
|
||||
field: filesize
|
||||
|
||||
- field: filesize
|
||||
display: filesize
|
||||
- collection: directus_files
|
||||
field: modified_by
|
||||
|
||||
- field: modified_by
|
||||
interface: user
|
||||
locked: true
|
||||
special: user-updated
|
||||
width: half
|
||||
display: user
|
||||
- collection: directus_files
|
||||
field: modified_on
|
||||
|
||||
- field: modified_on
|
||||
interface: datetime
|
||||
locked: true
|
||||
special: date-updated
|
||||
width: half
|
||||
display: datetime
|
||||
- collection: directus_files
|
||||
field: created_on
|
||||
|
||||
- field: created_on
|
||||
display: datetime
|
||||
- collection: directus_files
|
||||
field: created_by
|
||||
|
||||
- field: created_by
|
||||
display: user
|
||||
|
||||
- field: embed
|
||||
width: half
|
||||
|
||||
- field: uploaded_by
|
||||
width: half
|
||||
|
||||
- field: folder
|
||||
width: half
|
||||
|
||||
- field: width
|
||||
width: half
|
||||
|
||||
- field: uploaded_on
|
||||
width: half
|
||||
|
||||
- field: height
|
||||
width: half
|
||||
|
||||
- field: charset
|
||||
width: half
|
||||
|
||||
- field: duration
|
||||
width: half
|
||||
14
api/src/database/system-data/fields/folders.yaml
Normal file
14
api/src/database/system-data/fields/folders.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
table: directus_folders
|
||||
|
||||
fields:
|
||||
- field: id
|
||||
interface: text-input
|
||||
special: uuid
|
||||
width: half
|
||||
|
||||
- field: parent
|
||||
width: half
|
||||
|
||||
- field: name
|
||||
width: full
|
||||
|
||||
25
api/src/database/system-data/fields/index.ts
Normal file
25
api/src/database/system-data/fields/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { requireYAML } from '../../../utils/require-yaml';
|
||||
import { merge } from 'lodash';
|
||||
import { FieldMeta } from '../../../types';
|
||||
import fse from 'fs-extra';
|
||||
import path from 'path';
|
||||
|
||||
const defaults = requireYAML(require.resolve('./_defaults.yaml'));
|
||||
const fieldData = fse.readdirSync(path.resolve(__dirname));
|
||||
|
||||
export let systemFieldRows: FieldMeta[] = [];
|
||||
|
||||
for (const filepath of fieldData) {
|
||||
if (filepath.includes('_defaults') || filepath.includes('index')) continue;
|
||||
|
||||
const systemFields = requireYAML(path.resolve(__dirname, filepath));
|
||||
|
||||
(systemFields.fields as FieldMeta[]).forEach((field, index) => {
|
||||
systemFieldRows.push(
|
||||
merge({ system: true }, defaults, field, {
|
||||
collection: systemFields.table,
|
||||
sort: index + 1,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
36
api/src/database/system-data/fields/permissions.yaml
Normal file
36
api/src/database/system-data/fields/permissions.yaml
Normal file
@@ -0,0 +1,36 @@
|
||||
# directus_permissions isn't surfaced in the app
|
||||
table: directus_permissions
|
||||
|
||||
fields:
|
||||
- field: permissions
|
||||
hidden: true
|
||||
special: json
|
||||
width: half
|
||||
|
||||
- field: presets
|
||||
hidden: true
|
||||
special: json
|
||||
width: half
|
||||
|
||||
- field: role
|
||||
width: half
|
||||
|
||||
- field: limit
|
||||
width: half
|
||||
|
||||
- field: collection
|
||||
width: half
|
||||
|
||||
- field: id
|
||||
width: half
|
||||
|
||||
- field: fields
|
||||
width: half
|
||||
special: csv
|
||||
|
||||
- field: action
|
||||
width: half
|
||||
|
||||
- field: validation
|
||||
width: half
|
||||
special: json
|
||||
35
api/src/database/system-data/fields/presets.yaml
Normal file
35
api/src/database/system-data/fields/presets.yaml
Normal file
@@ -0,0 +1,35 @@
|
||||
table: directus_presets
|
||||
|
||||
fields:
|
||||
- field: filters
|
||||
hidden: true
|
||||
special: json
|
||||
|
||||
- field: layout_query
|
||||
hidden: true
|
||||
special: json
|
||||
|
||||
- field: layout_options
|
||||
hidden: true
|
||||
special: json
|
||||
|
||||
- field: role
|
||||
width: half
|
||||
|
||||
- field: user
|
||||
width: half
|
||||
|
||||
- field: id
|
||||
width: half
|
||||
|
||||
- field: bookmark
|
||||
width: half
|
||||
|
||||
- field: search
|
||||
width: half
|
||||
|
||||
- field: collection
|
||||
width: half
|
||||
|
||||
- field: layout
|
||||
width: half
|
||||
33
api/src/database/system-data/fields/relations.yaml
Normal file
33
api/src/database/system-data/fields/relations.yaml
Normal file
@@ -0,0 +1,33 @@
|
||||
table: directus_relations
|
||||
|
||||
fields:
|
||||
- field: id
|
||||
width: half
|
||||
|
||||
- field: many_collection
|
||||
width: half
|
||||
|
||||
- field: many_field
|
||||
width: half
|
||||
|
||||
- field: many_primary
|
||||
width: half
|
||||
|
||||
- field: one_collection
|
||||
width: half
|
||||
|
||||
- field: one_field
|
||||
width: half
|
||||
|
||||
- field: one_primary
|
||||
width: half
|
||||
|
||||
- field: one_collection_field
|
||||
width: half
|
||||
|
||||
- field: one_allowed_collections
|
||||
special: csv
|
||||
width: half
|
||||
|
||||
- field: junction_field
|
||||
width: half
|
||||
25
api/src/database/system-data/fields/revisions.yaml
Normal file
25
api/src/database/system-data/fields/revisions.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
table: directus_revisions
|
||||
|
||||
fields:
|
||||
- field: id
|
||||
width: half
|
||||
|
||||
- field: activity
|
||||
width: half
|
||||
|
||||
- field: collection
|
||||
width: half
|
||||
|
||||
- field: item
|
||||
width: half
|
||||
|
||||
- field: data
|
||||
hidden: true
|
||||
special: json
|
||||
|
||||
- field: delta
|
||||
hidden: true
|
||||
special: json
|
||||
|
||||
- field: parent
|
||||
width: half
|
||||
@@ -1,80 +1,61 @@
|
||||
table: directus_roles
|
||||
|
||||
fields:
|
||||
- collection: directus_roles
|
||||
field: id
|
||||
- field: id
|
||||
hidden: true
|
||||
interface: text-input
|
||||
locked: true
|
||||
special: uuid
|
||||
- collection: directus_roles
|
||||
field: name
|
||||
|
||||
- field: name
|
||||
interface: text-input
|
||||
options:
|
||||
placeholder: The unique name for this role...
|
||||
locked: true
|
||||
sort: 1
|
||||
width: half
|
||||
- collection: directus_roles
|
||||
field: icon
|
||||
|
||||
- field: icon
|
||||
interface: icon
|
||||
display: icon
|
||||
locked: true
|
||||
sort: 2
|
||||
width: half
|
||||
- collection: directus_roles
|
||||
field: description
|
||||
|
||||
- field: description
|
||||
interface: text-input
|
||||
options:
|
||||
placeholder: A description of this role...
|
||||
locked: true
|
||||
sort: 3
|
||||
width: full
|
||||
- collection: directus_roles
|
||||
field: app_access
|
||||
|
||||
- field: app_access
|
||||
interface: toggle
|
||||
locked: true
|
||||
special: boolean
|
||||
sort: 4
|
||||
width: half
|
||||
- collection: directus_roles
|
||||
field: admin_access
|
||||
|
||||
- field: admin_access
|
||||
interface: toggle
|
||||
locked: true
|
||||
special: boolean
|
||||
sort: 5
|
||||
width: half
|
||||
- collection: directus_roles
|
||||
field: ip_access
|
||||
|
||||
- field: ip_access
|
||||
interface: tags
|
||||
options:
|
||||
placeholder: Add allowed IP addresses, leave empty to allow all...
|
||||
locked: true
|
||||
special: csv
|
||||
sort: 6
|
||||
width: full
|
||||
- collection: directus_roles
|
||||
field: enforce_tfa
|
||||
|
||||
- field: enforce_tfa
|
||||
interface: toggle
|
||||
locked: true
|
||||
sort: 7
|
||||
special: boolean
|
||||
width: half
|
||||
- collection: directus_roles
|
||||
field: users
|
||||
|
||||
- field: users
|
||||
interface: one-to-many
|
||||
locked: true
|
||||
special: o2m
|
||||
sort: 8
|
||||
options:
|
||||
fields:
|
||||
- first_name
|
||||
- last_name
|
||||
width: full
|
||||
- collection: directus_roles
|
||||
field: module_list
|
||||
|
||||
- field: module_list
|
||||
interface: repeater
|
||||
locked: true
|
||||
options:
|
||||
template: '{{ name }}'
|
||||
addLabel: Add New Module...
|
||||
@@ -104,12 +85,10 @@ fields:
|
||||
iconRight: link
|
||||
placeholder: Relative or absolute URL...
|
||||
special: json
|
||||
sort: 9
|
||||
width: full
|
||||
- collection: directus_roles
|
||||
field: collection_list
|
||||
|
||||
- field: collection_list
|
||||
interface: repeater
|
||||
locked: true
|
||||
options:
|
||||
template: '{{ group_name }}'
|
||||
addLabel: Add New Group...
|
||||
@@ -159,5 +138,4 @@ fields:
|
||||
schema:
|
||||
is_nullable: false
|
||||
special: json
|
||||
sort: 10
|
||||
width: full
|
||||
17
api/src/database/system-data/fields/sessions.yaml
Normal file
17
api/src/database/system-data/fields/sessions.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
table: directus_sessions
|
||||
|
||||
fields:
|
||||
- field: token
|
||||
width: half
|
||||
|
||||
- field: user
|
||||
width: half
|
||||
|
||||
- field: expires
|
||||
width: half
|
||||
|
||||
- field: ip
|
||||
width: half
|
||||
|
||||
- field: user_agent
|
||||
width: half
|
||||
@@ -1,102 +1,85 @@
|
||||
table: directus_settings
|
||||
|
||||
fields:
|
||||
- collection: directus_settings
|
||||
field: project_name
|
||||
- field: id
|
||||
hidden: true
|
||||
|
||||
- field: project_name
|
||||
interface: text-input
|
||||
locked: true
|
||||
options:
|
||||
iconRight: title
|
||||
placeholder: My project...
|
||||
sort: 1
|
||||
translations:
|
||||
language: en-US
|
||||
translations: Name
|
||||
width: half
|
||||
- collection: directus_settings
|
||||
field: project_url
|
||||
|
||||
- field: project_url
|
||||
interface: text-input
|
||||
locked: true
|
||||
options:
|
||||
iconRight: link
|
||||
placeholder: https://example.com
|
||||
sort: 2
|
||||
translations:
|
||||
language: en-US
|
||||
translations: Website
|
||||
width: half
|
||||
- collection: directus_settings
|
||||
field: project_color
|
||||
|
||||
- field: project_color
|
||||
interface: color
|
||||
locked: true
|
||||
note: Login & Logo Background
|
||||
sort: 3
|
||||
translations:
|
||||
language: en-US
|
||||
translations: Brand Color
|
||||
width: half
|
||||
- collection: directus_settings
|
||||
field: project_logo
|
||||
|
||||
- field: project_logo
|
||||
interface: file
|
||||
locked: true
|
||||
note: White 40x40 SVG/PNG
|
||||
sort: 4
|
||||
translations:
|
||||
language: en-US
|
||||
translations: Brand Logo
|
||||
width: half
|
||||
- collection: directus_settings
|
||||
field: public_divider
|
||||
|
||||
- field: public_divider
|
||||
interface: divider
|
||||
locked: true
|
||||
options:
|
||||
icon: public
|
||||
title: Public Pages
|
||||
color: '#2F80ED'
|
||||
special: alias
|
||||
sort: 5
|
||||
width: full
|
||||
- collection: directus_settings
|
||||
field: public_foreground
|
||||
|
||||
- field: public_foreground
|
||||
interface: file
|
||||
locked: true
|
||||
sort: 6
|
||||
translations:
|
||||
language: en-US
|
||||
translations: Login Foreground
|
||||
width: half
|
||||
- collection: directus_settings
|
||||
field: public_background
|
||||
|
||||
- field: public_background
|
||||
interface: file
|
||||
locked: true
|
||||
sort: 7
|
||||
translations:
|
||||
language: en-US
|
||||
translations: Login Background
|
||||
width: half
|
||||
- collection: directus_settings
|
||||
field: public_note
|
||||
|
||||
- field: public_note
|
||||
interface: textarea
|
||||
locked: true
|
||||
options:
|
||||
placeholder: A short, public message that supports markdown formatting...
|
||||
sort: 8
|
||||
width: full
|
||||
- collection: directus_settings
|
||||
field: security_divider
|
||||
|
||||
- field: security_divider
|
||||
interface: divider
|
||||
locked: true
|
||||
options:
|
||||
icon: security
|
||||
title: Security
|
||||
color: '#2F80ED'
|
||||
special: alias
|
||||
sort: 9
|
||||
width: full
|
||||
- collection: directus_settings
|
||||
field: auth_password_policy
|
||||
|
||||
- field: auth_password_policy
|
||||
interface: dropdown
|
||||
locked: true
|
||||
options:
|
||||
choices:
|
||||
- value: null
|
||||
@@ -105,31 +88,25 @@ fields:
|
||||
text: Weak – Minimum 8 Characters
|
||||
- value: "/(?=^.{8,}$)(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*()_+}{';'?>.<,])(?!.*\\s).*$/"
|
||||
text: Strong – Upper / Lowercase / Numbers / Special
|
||||
sort: 10
|
||||
width: half
|
||||
- collection: directus_settings
|
||||
field: auth_login_attempts
|
||||
|
||||
- field: auth_login_attempts
|
||||
interface: numeric
|
||||
locked: true
|
||||
options:
|
||||
iconRight: lock
|
||||
sort: 11
|
||||
width: half
|
||||
- collection: directus_settings
|
||||
field: files_divider
|
||||
|
||||
- field: files_divider
|
||||
interface: divider
|
||||
locked: true
|
||||
options:
|
||||
icon: storage
|
||||
title: Files & Thumbnails
|
||||
color: '#2F80ED'
|
||||
special: alias
|
||||
sort: 12
|
||||
width: full
|
||||
- collection: directus_settings
|
||||
field: storage_asset_presets
|
||||
|
||||
- field: storage_asset_presets
|
||||
interface: repeater
|
||||
locked: true
|
||||
options:
|
||||
fields:
|
||||
- field: key
|
||||
@@ -155,6 +132,11 @@ fields:
|
||||
text: Contain (preserve aspect ratio)
|
||||
- value: cover
|
||||
text: Cover (forces exact size)
|
||||
- value: inside
|
||||
text: Fit inside
|
||||
- value: outside
|
||||
text: Fit outside
|
||||
required: true
|
||||
width: half
|
||||
- field: width
|
||||
name: Width
|
||||
@@ -184,15 +166,23 @@ fields:
|
||||
max: 100
|
||||
min: 0
|
||||
step: 1
|
||||
width: full
|
||||
required: true
|
||||
width: half
|
||||
- field: withoutEnlargement
|
||||
type: boolean
|
||||
schema:
|
||||
default_value: false
|
||||
meta:
|
||||
interface: toggle
|
||||
width: half
|
||||
options:
|
||||
label: Don't upscale images
|
||||
template: '{{key}}'
|
||||
special: json
|
||||
sort: 13
|
||||
width: full
|
||||
- collection: directus_settings
|
||||
field: storage_asset_transform
|
||||
|
||||
- field: storage_asset_transform
|
||||
interface: dropdown
|
||||
locked: true
|
||||
options:
|
||||
choices:
|
||||
- value: all
|
||||
@@ -201,29 +191,20 @@ fields:
|
||||
text: None
|
||||
- value: presets
|
||||
text: Presets Only
|
||||
sort: 14
|
||||
width: half
|
||||
- collection: directus_settings
|
||||
field: id
|
||||
hidden: true
|
||||
locked: true
|
||||
- collection: directus_settings
|
||||
field: overrides_divider
|
||||
|
||||
- field: overrides_divider
|
||||
interface: divider
|
||||
locked: true
|
||||
options:
|
||||
icon: brush
|
||||
title: App Overrides
|
||||
color: '#2F80ED'
|
||||
special: alias
|
||||
sort: 15
|
||||
width: full
|
||||
- collection: directus_settings
|
||||
field: custom_css
|
||||
|
||||
- field: custom_css
|
||||
interface: code
|
||||
locked: true
|
||||
options:
|
||||
language: css
|
||||
lineNumber: true
|
||||
sort: 16
|
||||
width: full
|
||||
@@ -1,87 +1,70 @@
|
||||
table: directus_users
|
||||
|
||||
fields:
|
||||
- collection: directus_users
|
||||
field: first_name
|
||||
- field: first_name
|
||||
interface: text-input
|
||||
locked: true
|
||||
options:
|
||||
iconRight: account_circle
|
||||
sort: 1
|
||||
width: half
|
||||
- collection: directus_users
|
||||
field: last_name
|
||||
|
||||
- field: last_name
|
||||
interface: text-input
|
||||
locked: true
|
||||
options:
|
||||
iconRight: account_circle
|
||||
sort: 2
|
||||
width: half
|
||||
- collection: directus_users
|
||||
field: email
|
||||
|
||||
- field: email
|
||||
interface: text-input
|
||||
locked: true
|
||||
options:
|
||||
iconRight: email
|
||||
sort: 3
|
||||
width: half
|
||||
- collection: directus_users
|
||||
field: password
|
||||
special: hash, conceal
|
||||
|
||||
- field: password
|
||||
special: hash,conceal
|
||||
interface: hash
|
||||
locked: true
|
||||
options:
|
||||
iconRight: lock
|
||||
masked: true
|
||||
sort: 4
|
||||
width: half
|
||||
- collection: directus_users
|
||||
field: avatar
|
||||
|
||||
- field: avatar
|
||||
interface: file
|
||||
locked: true
|
||||
sort: 5
|
||||
width: full
|
||||
- collection: directus_users
|
||||
field: location
|
||||
|
||||
- field: location
|
||||
interface: text-input
|
||||
options:
|
||||
iconRight: place
|
||||
sort: 6
|
||||
width: half
|
||||
- collection: directus_users
|
||||
field: title
|
||||
|
||||
- field: title
|
||||
interface: text-input
|
||||
options:
|
||||
iconRight: work
|
||||
sort: 7
|
||||
width: half
|
||||
- collection: directus_users
|
||||
field: description
|
||||
|
||||
- field: description
|
||||
interface: textarea
|
||||
sort: 8
|
||||
width: full
|
||||
- collection: directus_users
|
||||
field: tags
|
||||
|
||||
- field: tags
|
||||
interface: tags
|
||||
special: json
|
||||
sort: 9
|
||||
width: full
|
||||
options:
|
||||
iconRight: local_offer
|
||||
- collection: directus_users
|
||||
field: preferences_divider
|
||||
|
||||
- field: preferences_divider
|
||||
interface: divider
|
||||
options:
|
||||
icon: face
|
||||
title: User Preferences
|
||||
color: '#2F80ED'
|
||||
special: alias
|
||||
sort: 10
|
||||
width: full
|
||||
- collection: directus_users
|
||||
field: language
|
||||
|
||||
- field: language
|
||||
interface: dropdown
|
||||
locked: true
|
||||
options:
|
||||
choices:
|
||||
- text: Afrikaans (South Africa)
|
||||
@@ -146,12 +129,10 @@ fields:
|
||||
value: uk-UA
|
||||
- text: Vietnamese (Vietnam)
|
||||
value: vi-VN
|
||||
sort: 11
|
||||
width: half
|
||||
- collection: directus_users
|
||||
field: theme
|
||||
|
||||
- field: theme
|
||||
interface: dropdown
|
||||
locked: true
|
||||
options:
|
||||
choices:
|
||||
- value: auto
|
||||
@@ -160,30 +141,24 @@ fields:
|
||||
text: Light Mode
|
||||
- value: dark
|
||||
text: Dark Mode
|
||||
sort: 12
|
||||
width: half
|
||||
- collection: directus_users
|
||||
field: tfa_secret
|
||||
|
||||
- field: tfa_secret
|
||||
interface: tfa-setup
|
||||
locked: true
|
||||
special: conceal
|
||||
sort: 13
|
||||
width: half
|
||||
- collection: directus_users
|
||||
field: admin_divider
|
||||
|
||||
- field: admin_divider
|
||||
interface: divider
|
||||
locked: true
|
||||
options:
|
||||
icon: verified_user
|
||||
title: Admin Options
|
||||
color: '#F2994A'
|
||||
special: alias
|
||||
sort: 14
|
||||
width: full
|
||||
- collection: directus_users
|
||||
field: status
|
||||
|
||||
- field: status
|
||||
interface: dropdown
|
||||
locked: true
|
||||
options:
|
||||
choices:
|
||||
- text: Draft
|
||||
@@ -196,32 +171,31 @@ fields:
|
||||
value: suspended
|
||||
- text: Archived
|
||||
value: archived
|
||||
sort: 15
|
||||
width: half
|
||||
- collection: directus_users
|
||||
field: role
|
||||
|
||||
- field: role
|
||||
interface: many-to-one
|
||||
locked: true
|
||||
options:
|
||||
template: '{{ name }}'
|
||||
special: m2o
|
||||
sort: 16
|
||||
width: half
|
||||
- collection: directus_users
|
||||
field: token
|
||||
|
||||
- field: token
|
||||
interface: token
|
||||
locked: true
|
||||
options:
|
||||
iconRight: vpn_key
|
||||
placeholder: Enter a secure access token...
|
||||
sort: 17
|
||||
width: full
|
||||
- collection: directus_users
|
||||
field: id
|
||||
|
||||
- field: id
|
||||
special: uuid
|
||||
interface: text-input
|
||||
locked: true
|
||||
options:
|
||||
iconRight: vpn_key
|
||||
sort: 18
|
||||
width: full
|
||||
|
||||
- field: last_page
|
||||
width: half
|
||||
|
||||
- field: last_access
|
||||
width: half
|
||||
@@ -1,43 +1,35 @@
|
||||
table: directus_webhooks
|
||||
|
||||
fields:
|
||||
- collection: directus_webhooks
|
||||
field: id
|
||||
- field: id
|
||||
hidden: true
|
||||
locked: true
|
||||
- collection: directus_webhooks
|
||||
field: name
|
||||
|
||||
- field: name
|
||||
interface: text-input
|
||||
locked: true
|
||||
options:
|
||||
iconRight: title
|
||||
sort: 1
|
||||
width: full
|
||||
- collection: directus_webhooks
|
||||
field: method
|
||||
|
||||
- field: method
|
||||
interface: dropdown
|
||||
display: labels
|
||||
display_options:
|
||||
defaultBackground: "#ECEFF1"
|
||||
choices: null
|
||||
format: false
|
||||
locked: true
|
||||
options:
|
||||
choices:
|
||||
- GET
|
||||
- POST
|
||||
sort: 2
|
||||
width: half
|
||||
- collection: directus_webhooks
|
||||
field: url
|
||||
|
||||
- field: url
|
||||
interface: text-input
|
||||
locked: true
|
||||
options:
|
||||
iconRight: link
|
||||
sort: 3
|
||||
width: half
|
||||
- collection: directus_webhooks
|
||||
field: status
|
||||
|
||||
- field: status
|
||||
interface: dropdown
|
||||
display: labels
|
||||
display_options:
|
||||
@@ -53,36 +45,31 @@ fields:
|
||||
value: inactive
|
||||
foreground: "#607D8B"
|
||||
background: "#ECEFF1"
|
||||
locked: true
|
||||
options:
|
||||
choices:
|
||||
- text: Active
|
||||
value: active
|
||||
- text: Inactive
|
||||
value: inactive
|
||||
sort: 4
|
||||
width: half
|
||||
- collection: directus_webhooks
|
||||
field: data
|
||||
|
||||
- field: data
|
||||
interface: toggle
|
||||
locked: true
|
||||
options:
|
||||
label: Send Event Data
|
||||
special: boolean
|
||||
sort: 5
|
||||
width: half
|
||||
- collection: directus_webhooks
|
||||
field: triggers_divider
|
||||
|
||||
- field: triggers_divider
|
||||
interface: divider
|
||||
options:
|
||||
icon: api
|
||||
title: Triggers
|
||||
color: '#2F80ED'
|
||||
special: alias
|
||||
sort: 6
|
||||
width: full
|
||||
- collection: directus_webhooks
|
||||
field: actions
|
||||
|
||||
- field: actions
|
||||
interface: checkboxes
|
||||
options:
|
||||
choices:
|
||||
@@ -93,11 +80,9 @@ fields:
|
||||
- text: Delete
|
||||
value: delete
|
||||
special: csv
|
||||
sort: 7
|
||||
width: full
|
||||
- collection: directus_webhooks
|
||||
field: collections
|
||||
|
||||
- field: collections
|
||||
interface: collections
|
||||
special: csv
|
||||
sort: 8
|
||||
width: full
|
||||
9
api/src/database/system-data/relations/index.ts
Normal file
9
api/src/database/system-data/relations/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { requireYAML } from '../../../utils/require-yaml';
|
||||
import { merge } from 'lodash';
|
||||
import { Relation } from '../../../types';
|
||||
|
||||
const systemData = requireYAML(require.resolve('./relations.yaml'));
|
||||
|
||||
export const systemRelationRows: Relation[] = systemData.data.map((row: Record<string, any>) => {
|
||||
return merge({ system: true }, systemData.defaults, row);
|
||||
});
|
||||
@@ -27,7 +27,7 @@ const defaults: Record<string, any> = {
|
||||
REFRESH_TOKEN_COOKIE_SECURE: false,
|
||||
REFRESH_TOKEN_COOKIE_SAME_SITE: 'lax',
|
||||
|
||||
CORS_ENABLED: false,
|
||||
CORS_ENABLED: true,
|
||||
|
||||
CACHE_ENABLED: false,
|
||||
CACHE_STORE: 'memory',
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import database from '../database';
|
||||
import logger from '../logger';
|
||||
import nodemailer, { Transporter } from 'nodemailer';
|
||||
import { Liquid } from 'liquidjs';
|
||||
@@ -42,6 +43,34 @@ export type EmailOptions = {
|
||||
html: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get an object with default template options to pass to the email templates.
|
||||
*/
|
||||
async function getDefaultTemplateOptions() {
|
||||
const projectInfo = await database
|
||||
.select(['project_name', 'project_logo', 'project_color'])
|
||||
.from('directus_settings')
|
||||
.first();
|
||||
|
||||
return {
|
||||
projectName: projectInfo?.project_name || 'Directus',
|
||||
projectColor: projectInfo?.project_color || '#546e7a',
|
||||
projectLogo: projectInfo?.project_logo
|
||||
? getProjectLogoURL(projectInfo.project_logo)
|
||||
: 'https://directus.io/assets/directus-white.png',
|
||||
};
|
||||
|
||||
function getProjectLogoURL(logoID: string) {
|
||||
let projectLogoURL = env.PUBLIC_URL;
|
||||
|
||||
if (projectLogoURL.endsWith('/') === false) {
|
||||
projectLogoURL += '/';
|
||||
}
|
||||
|
||||
projectLogoURL += `assets/${logoID}`;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function sendMail(options: EmailOptions) {
|
||||
const templateString = await readFile(path.join(__dirname, 'templates/base.liquid'), 'utf8');
|
||||
const html = await liquidEngine.parseAndRender(templateString, { html: options.html });
|
||||
@@ -57,31 +86,35 @@ export default async function sendMail(options: EmailOptions) {
|
||||
}
|
||||
|
||||
export async function sendInviteMail(email: string, url: string) {
|
||||
/**
|
||||
* @TODO pull this from directus_settings
|
||||
*/
|
||||
const projectName = 'Directus';
|
||||
const defaultOptions = await getDefaultTemplateOptions();
|
||||
|
||||
const html = await liquidEngine.renderFile('user-invitation', {
|
||||
...defaultOptions,
|
||||
email,
|
||||
url,
|
||||
});
|
||||
|
||||
const html = await liquidEngine.renderFile('user-invitation', { email, url, projectName });
|
||||
await transporter.sendMail({
|
||||
from: env.EMAIL_FROM,
|
||||
to: email,
|
||||
html: html,
|
||||
subject: `[${projectName}] You've been invited`,
|
||||
subject: `[${defaultOptions.projectName}] You've been invited`,
|
||||
});
|
||||
}
|
||||
|
||||
export async function sendPasswordResetMail(email: string, url: string) {
|
||||
/**
|
||||
* @TODO pull this from directus_settings
|
||||
*/
|
||||
const projectName = 'Directus';
|
||||
const defaultOptions = await getDefaultTemplateOptions();
|
||||
|
||||
const html = await liquidEngine.renderFile('password-reset', {
|
||||
...defaultOptions,
|
||||
email,
|
||||
url,
|
||||
});
|
||||
|
||||
const html = await liquidEngine.renderFile('password-reset', { email, url, projectName });
|
||||
await transporter.sendMail({
|
||||
from: env.EMAIL_FROM,
|
||||
to: email,
|
||||
html: html,
|
||||
subject: `[${projectName}] Password Reset Request`,
|
||||
subject: `[${defaultOptions.projectName}] Password Reset Request`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,20 +3,30 @@
|
||||
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>Directus Email Service</title>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
|
||||
<title>{{ projectName }} Email Service</title>
|
||||
|
||||
<style type="text/css">
|
||||
a {
|
||||
border: none;
|
||||
text-decoration: none;
|
||||
color: #2196f3;
|
||||
outline: none !important;
|
||||
color: #2196f3 !important;
|
||||
display: inline-block;
|
||||
height: 52px;
|
||||
width: auto;
|
||||
min-width: 154px;
|
||||
padding: 0 20px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
line-height: 52px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
text-decoration: none !important;
|
||||
color: white !important;
|
||||
background-color: {{ projectColor }};
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #2196f3 !important;
|
||||
filter: brightness(105%);
|
||||
}
|
||||
|
||||
p {
|
||||
@@ -33,14 +43,14 @@
|
||||
<table align="center" border="0" cellpadding="0" cellspacing="0" width="600"
|
||||
style="border: 0 solid #263238; border-collapse: collapse;">
|
||||
<tr>
|
||||
<td align="center" bgcolor="#263238"
|
||||
<td align="center" bgcolor="{{ projectColor }}"
|
||||
style="padding: 30px 0 30px 0; border-radius: 4px 4px 0 0;">
|
||||
<img src="https://directus.io/assets/directus-white.png" alt="Directus" width="130"
|
||||
<img src="{{ projectLogo }}" alt="{{ projectName }}" width="130"
|
||||
style="display: block;" />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" style="padding: 60px 30px 60px 30px; border-radius: 0 0 4px 4px;">
|
||||
<td bgcolor="#ffffff" style="padding: 30px; border-radius: 0 0 4px 4px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" width="100%">
|
||||
<tr>
|
||||
<td
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
{% layout "base" %}
|
||||
{% block content %}
|
||||
|
||||
<p>You requested to reset your password. Please click the link below to reset your password:</p>
|
||||
<p>
|
||||
We have received a request to reset the password for your <i>{{ projectName }}</i> account. If you did not make this change, please contact one of your administrators. Otherwise, to complete the process, click the following link to confirm your email address and enter your new password.
|
||||
</p>
|
||||
|
||||
<p><a href="{{ url }}">{{ url }}</a></p>
|
||||
<p style="text-align: center; padding: 20px 0;">
|
||||
<a href="{{ url }}">
|
||||
Click to reset your password
|
||||
</a>
|
||||
</p>
|
||||
|
||||
{% comment %}
|
||||
@TODO
|
||||
Make this white-labeled
|
||||
{% endcomment %}
|
||||
<p>
|
||||
<b>Important: This link will expire in 24 hours.</b>
|
||||
</p>
|
||||
|
||||
<p>Love,<br>Directus</p>
|
||||
<p>
|
||||
Thank you,<br>
|
||||
{{ projectName }}
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
{% layout "base" %}
|
||||
{% block content %}
|
||||
|
||||
<p>You have been invited to {{ projectName }}. Please click the link below to join:</p>
|
||||
<p>
|
||||
You have been invited to join {{ projectName }}. Please click the button below to join:
|
||||
</p>
|
||||
|
||||
<p><a href="{{ url }}">{{ url }}</a></p>
|
||||
<p style="text-align: center; padding: 20px 0;">
|
||||
<a href="{{ url }}">
|
||||
Click to join {{ projectName }}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
{% comment %}
|
||||
@TODO
|
||||
Make this white-labeled
|
||||
{% endcomment %}
|
||||
|
||||
<p>Love,<br>Directus</p>
|
||||
<p>
|
||||
Thank You,<br>
|
||||
{{ projectName }}
|
||||
</p>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -6,25 +6,32 @@ import { RequestHandler } from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import database from '../database';
|
||||
import { ForbiddenException } from '../exceptions';
|
||||
import { systemCollectionRows } from '../database/system-data/collections';
|
||||
|
||||
const collectionExists: RequestHandler = asyncHandler(async (req, res, next) => {
|
||||
if (!req.params.collection) return next();
|
||||
|
||||
const exists = await database.schema.hasTable(req.params.collection);
|
||||
|
||||
if (exists === false) {
|
||||
if (req.params.collection in req.schema === false) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
|
||||
req.collection = req.params.collection;
|
||||
|
||||
const collectionInfo = await database
|
||||
.select('singleton')
|
||||
.from('directus_collections')
|
||||
.where({ collection: req.collection })
|
||||
.first();
|
||||
if (req.collection.startsWith('directus_')) {
|
||||
const systemRow = systemCollectionRows.find((collection) => {
|
||||
return collection?.collection === req.collection;
|
||||
});
|
||||
|
||||
req.singleton = collectionInfo?.singleton === true || collectionInfo?.singleton === 1;
|
||||
req.singleton = !!systemRow?.singleton;
|
||||
} else {
|
||||
const collectionInfo = await database
|
||||
.select('singleton')
|
||||
.from('directus_collections')
|
||||
.where({ collection: req.collection })
|
||||
.first();
|
||||
|
||||
req.singleton = collectionInfo?.singleton === true || collectionInfo?.singleton === 1;
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
@@ -63,6 +63,7 @@ function getConfig(
|
||||
|
||||
if (store === 'redis') {
|
||||
const Redis = require('ioredis');
|
||||
delete config.redis;
|
||||
config.storeClient = new Redis(
|
||||
env.RATE_LIMITER_REDIS || getConfigFromEnv('RATE_LIMITER_REDIS_')
|
||||
);
|
||||
|
||||
12
api/src/middleware/schema.ts
Normal file
12
api/src/middleware/schema.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { RequestHandler } from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import { schemaInspector } from '../database';
|
||||
|
||||
const getSchema: RequestHandler = asyncHandler(async (req, res, next) => {
|
||||
const schemaOverview = await schemaInspector.overview();
|
||||
req.schema = schemaOverview;
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
export default getSchema;
|
||||
@@ -6,7 +6,7 @@ import { AbstractServiceOptions } from '../types';
|
||||
*/
|
||||
|
||||
export class ActivityService extends ItemsService {
|
||||
constructor(options?: AbstractServiceOptions) {
|
||||
constructor(options: AbstractServiceOptions) {
|
||||
super('directus_activity', options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,17 +4,31 @@ import database from '../database';
|
||||
import path from 'path';
|
||||
import Knex from 'knex';
|
||||
import { Accountability, AbstractServiceOptions, Transformation } from '../types';
|
||||
import { AuthorizationService } from './authorization';
|
||||
|
||||
export class AssetsService {
|
||||
knex: Knex;
|
||||
accountability: Accountability | null;
|
||||
authorizationService: AuthorizationService;
|
||||
|
||||
constructor(options?: AbstractServiceOptions) {
|
||||
this.knex = options?.knex || database;
|
||||
this.accountability = options?.accountability || null;
|
||||
constructor(options: AbstractServiceOptions) {
|
||||
this.knex = options.knex || database;
|
||||
this.accountability = options.accountability || null;
|
||||
this.authorizationService = new AuthorizationService(options);
|
||||
}
|
||||
|
||||
async getAsset(id: string, transformation: Transformation) {
|
||||
const publicSettings = await this.knex
|
||||
.select('project_logo', 'public_background', 'public_foreground')
|
||||
.from('directus_settings')
|
||||
.first();
|
||||
|
||||
const systemPublicKeys = Object.values(publicSettings || {});
|
||||
|
||||
if (systemPublicKeys.includes(id) === false && this.accountability?.admin !== true) {
|
||||
await this.authorizationService.checkAccess('read', 'directus_files', id);
|
||||
}
|
||||
|
||||
const file = await database.select('*').from('directus_files').where({ id }).first();
|
||||
|
||||
const type = file.type;
|
||||
@@ -48,9 +62,11 @@ export class AssetsService {
|
||||
private parseTransformation(transformation: Transformation): ResizeOptions {
|
||||
const resizeOptions: ResizeOptions = {};
|
||||
|
||||
if (transformation.w) resizeOptions.width = Number(transformation.w);
|
||||
if (transformation.h) resizeOptions.height = Number(transformation.h);
|
||||
if (transformation.f) resizeOptions.fit = transformation.f;
|
||||
if (transformation.width) resizeOptions.width = Number(transformation.width);
|
||||
if (transformation.height) resizeOptions.height = Number(transformation.height);
|
||||
if (transformation.fit) resizeOptions.fit = transformation.fit;
|
||||
if (transformation.withoutEnlargement)
|
||||
resizeOptions.withoutEnlargement = Boolean(transformation.withoutEnlargement);
|
||||
|
||||
return resizeOptions;
|
||||
}
|
||||
|
||||
@@ -27,10 +27,10 @@ export class AuthenticationService {
|
||||
accountability: Accountability | null;
|
||||
activityService: ActivityService;
|
||||
|
||||
constructor(options?: AbstractServiceOptions) {
|
||||
this.knex = options?.knex || database;
|
||||
this.accountability = options?.accountability || null;
|
||||
this.activityService = new ActivityService();
|
||||
constructor(options: AbstractServiceOptions) {
|
||||
this.knex = options.knex || database;
|
||||
this.accountability = options.accountability || null;
|
||||
this.activityService = new ActivityService({ knex: this.knex, schema: options.schema });
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,8 +10,8 @@ import {
|
||||
PermissionsAction,
|
||||
Item,
|
||||
PrimaryKey,
|
||||
SchemaOverview,
|
||||
} from '../types';
|
||||
import SchemaInspector from 'knex-schema-inspector';
|
||||
import Knex from 'knex';
|
||||
import { ForbiddenException, FailedValidationException } from '../exceptions';
|
||||
import { uniq, merge, flatten } from 'lodash';
|
||||
@@ -20,16 +20,22 @@ import { ItemsService } from './items';
|
||||
import { PayloadService } from './payload';
|
||||
import { parseFilter } from '../utils/parse-filter';
|
||||
import { toArray } from '../utils/to-array';
|
||||
import { systemFieldRows } from '../database/system-data/fields';
|
||||
|
||||
export class AuthorizationService {
|
||||
knex: Knex;
|
||||
accountability: Accountability | null;
|
||||
payloadService: PayloadService;
|
||||
schema: SchemaOverview;
|
||||
|
||||
constructor(options?: AbstractServiceOptions) {
|
||||
this.knex = options?.knex || database;
|
||||
this.accountability = options?.accountability || null;
|
||||
this.payloadService = new PayloadService('directus_permissions', { knex: this.knex });
|
||||
constructor(options: AbstractServiceOptions) {
|
||||
this.knex = options.knex || database;
|
||||
this.accountability = options.accountability || null;
|
||||
this.schema = options.schema;
|
||||
this.payloadService = new PayloadService('directus_permissions', {
|
||||
knex: this.knex,
|
||||
schema: this.schema,
|
||||
});
|
||||
}
|
||||
|
||||
async processAST(ast: AST, action: PermissionsAction = 'read'): Promise<AST> {
|
||||
@@ -231,6 +237,8 @@ export class AuthorizationService {
|
||||
.where({ action, collection, role: this.accountability?.role || null })
|
||||
.first();
|
||||
|
||||
if (!permission) throw new ForbiddenException();
|
||||
|
||||
permission = (await this.payloadService.processValues(
|
||||
'read',
|
||||
permission as Item
|
||||
@@ -238,8 +246,6 @@ export class AuthorizationService {
|
||||
|
||||
// Check if you have permission to access the fields you're trying to acces
|
||||
|
||||
if (!permission) throw new ForbiddenException();
|
||||
|
||||
const allowedFields = permission.fields || [];
|
||||
|
||||
if (allowedFields.includes('*') === false) {
|
||||
@@ -258,23 +264,28 @@ export class AuthorizationService {
|
||||
}
|
||||
}
|
||||
|
||||
const preset = permission.presets || {};
|
||||
const preset = parseFilter(permission.presets || {}, this.accountability);
|
||||
|
||||
payloads = payloads.map((payload) => merge({}, preset, payload));
|
||||
|
||||
const schemaInspector = SchemaInspector(this.knex);
|
||||
const columns = await schemaInspector.columnInfo(collection);
|
||||
const columns = Object.values(this.schema[collection].columns);
|
||||
|
||||
let requiredColumns: string[] = [];
|
||||
|
||||
for (const column of columns) {
|
||||
const field = await this.knex
|
||||
.select<{ special: string }>('special')
|
||||
.from('directus_fields')
|
||||
.where({ collection, field: column.name })
|
||||
.first();
|
||||
const field =
|
||||
(await this.knex
|
||||
.select<{ special: string }>('special')
|
||||
.from('directus_fields')
|
||||
.where({ collection, field: column.column_name })
|
||||
.first()) ||
|
||||
systemFieldRows.find(
|
||||
(fieldMeta) =>
|
||||
fieldMeta.field === column.column_name &&
|
||||
fieldMeta.collection === collection
|
||||
);
|
||||
|
||||
const specials = (field?.special || '').split(',');
|
||||
const specials = field?.special ? toArray(field.special) : [];
|
||||
|
||||
const hasGenerateSpecial = [
|
||||
'uuid',
|
||||
@@ -285,12 +296,11 @@ export class AuthorizationService {
|
||||
|
||||
const isRequired =
|
||||
column.is_nullable === false &&
|
||||
column.has_auto_increment === false &&
|
||||
column.default_value === null &&
|
||||
hasGenerateSpecial === false;
|
||||
|
||||
if (isRequired) {
|
||||
requiredColumns.push(column.name);
|
||||
requiredColumns.push(column.column_name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,9 +336,11 @@ export class AuthorizationService {
|
||||
}
|
||||
|
||||
validateJoi(
|
||||
validation: Record<string, any>,
|
||||
validation: null | Record<string, any>,
|
||||
payloads: Partial<Record<string, any>>[]
|
||||
): FailedValidationException[] {
|
||||
if (!validation) return [];
|
||||
|
||||
const errors: FailedValidationException[] = [];
|
||||
|
||||
/**
|
||||
@@ -381,7 +393,11 @@ export class AuthorizationService {
|
||||
) {
|
||||
if (this.accountability?.admin === true) return;
|
||||
|
||||
const itemsService = new ItemsService(collection, { accountability: this.accountability });
|
||||
const itemsService = new ItemsService(collection, {
|
||||
accountability: this.accountability,
|
||||
knex: this.knex,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
try {
|
||||
const query: Query = {
|
||||
@@ -391,7 +407,7 @@ export class AuthorizationService {
|
||||
const result = await itemsService.readByKey(pk as any, query, action);
|
||||
|
||||
if (!result) throw '';
|
||||
if (Array.isArray(pk) && result.length !== pk.length) throw '';
|
||||
if (Array.isArray(pk) && pk.length > 1 && result.length !== pk.length) throw '';
|
||||
} catch {
|
||||
throw new ForbiddenException(
|
||||
`You're not allowed to ${action} item "${pk}" in collection "${collection}".`,
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
import database, { schemaInspector } from '../database';
|
||||
import { AbstractServiceOptions, Accountability, Collection, Relation } from '../types';
|
||||
import {
|
||||
AbstractServiceOptions,
|
||||
Accountability,
|
||||
Collection,
|
||||
CollectionMeta,
|
||||
Relation,
|
||||
SchemaOverview,
|
||||
} from '../types';
|
||||
import Knex from 'knex';
|
||||
import { ForbiddenException, InvalidPayloadException } from '../exceptions';
|
||||
import SchemaInspector from 'knex-schema-inspector';
|
||||
import { FieldsService } from '../services/fields';
|
||||
import { ItemsService } from '../services/items';
|
||||
import cache from '../cache';
|
||||
import { toArray } from '../utils/to-array';
|
||||
import { systemCollectionRows } from '../database/system-data/collections';
|
||||
|
||||
export class CollectionsService {
|
||||
knex: Knex;
|
||||
accountability: Accountability | null;
|
||||
schema: SchemaOverview;
|
||||
|
||||
constructor(options?: AbstractServiceOptions) {
|
||||
this.knex = options?.knex || database;
|
||||
this.accountability = options?.accountability || null;
|
||||
constructor(options: AbstractServiceOptions) {
|
||||
this.knex = options.knex || database;
|
||||
this.accountability = options.accountability || null;
|
||||
this.schema = options.schema;
|
||||
}
|
||||
|
||||
create(data: Partial<Collection>[]): Promise<string[]>;
|
||||
@@ -45,15 +54,18 @@ export class CollectionsService {
|
||||
const createdCollections: string[] = [];
|
||||
|
||||
await this.knex.transaction(async (trx) => {
|
||||
const schemaInspector = SchemaInspector(trx);
|
||||
const fieldsService = new FieldsService({ knex: trx });
|
||||
const fieldsService = new FieldsService({ knex: trx, schema: this.schema });
|
||||
|
||||
const collectionItemsService = new ItemsService('directus_collections', {
|
||||
knex: trx,
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
const fieldItemsService = new ItemsService('directus_fields', {
|
||||
knex: trx,
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
for (const payload of payloads) {
|
||||
@@ -65,7 +77,7 @@ export class CollectionsService {
|
||||
throw new InvalidPayloadException(`Collections can't start with "directus_"`);
|
||||
}
|
||||
|
||||
if (await schemaInspector.hasTable(payload.collection)) {
|
||||
if (payload.collection in this.schema) {
|
||||
throw new InvalidPayloadException(
|
||||
`Collection "${payload.collection}" already exists.`
|
||||
);
|
||||
@@ -105,7 +117,9 @@ export class CollectionsService {
|
||||
const collectionItemsService = new ItemsService('directus_collections', {
|
||||
knex: this.knex,
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
const collectionKeys = toArray(collection);
|
||||
|
||||
if (this.accountability && this.accountability.admin !== true) {
|
||||
@@ -135,7 +149,9 @@ export class CollectionsService {
|
||||
const tables = tablesInDatabase.filter((table) => collectionKeys.includes(table.name));
|
||||
const meta = (await collectionItemsService.readByQuery({
|
||||
filter: { collection: { _in: collectionKeys } },
|
||||
})) as Collection['meta'][];
|
||||
})) as CollectionMeta[];
|
||||
|
||||
meta.push(...systemCollectionRows);
|
||||
|
||||
const collections: Collection[] = [];
|
||||
|
||||
@@ -154,7 +170,10 @@ export class CollectionsService {
|
||||
|
||||
/** @todo, read by query without query support is a bit ironic, isnt it */
|
||||
async readByQuery(): Promise<Collection[]> {
|
||||
const collectionItemsService = new ItemsService('directus_collections');
|
||||
const collectionItemsService = new ItemsService('directus_collections', {
|
||||
knex: this.knex,
|
||||
schema: this.schema,
|
||||
});
|
||||
let tablesInDatabase = await schemaInspector.tableInfo();
|
||||
|
||||
if (this.accountability && this.accountability.admin !== true) {
|
||||
@@ -173,7 +192,9 @@ export class CollectionsService {
|
||||
const tablesToFetchInfoFor = tablesInDatabase.map((table) => table.name);
|
||||
const meta = (await collectionItemsService.readByQuery({
|
||||
filter: { collection: { _in: tablesToFetchInfoFor } },
|
||||
})) as Collection['meta'][];
|
||||
})) as CollectionMeta[];
|
||||
|
||||
meta.push(...systemCollectionRows);
|
||||
|
||||
const collections: Collection[] = [];
|
||||
|
||||
@@ -204,6 +225,7 @@ export class CollectionsService {
|
||||
const collectionItemsService = new ItemsService('directus_collections', {
|
||||
knex: this.knex,
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
if (data && key) {
|
||||
@@ -261,9 +283,10 @@ export class CollectionsService {
|
||||
const fieldsService = new FieldsService({
|
||||
knex: this.knex,
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
const tablesInDatabase = await schemaInspector.tables();
|
||||
const tablesInDatabase = Object.keys(this.schema);
|
||||
|
||||
const collectionKeys = toArray(collection);
|
||||
|
||||
@@ -307,7 +330,9 @@ export class CollectionsService {
|
||||
const collectionItemsService = new ItemsService('directus_collections', {
|
||||
knex: this.knex,
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
await collectionItemsService.delete(collectionKeys);
|
||||
|
||||
for (const collectionKey of collectionKeys) {
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import database, { schemaInspector } from '../database';
|
||||
import { Field } from '../types/field';
|
||||
import { Accountability, AbstractServiceOptions, FieldMeta, Relation } from '../types';
|
||||
import {
|
||||
Accountability,
|
||||
AbstractServiceOptions,
|
||||
FieldMeta,
|
||||
Relation,
|
||||
SchemaOverview,
|
||||
} from '../types';
|
||||
import { ItemsService } from '../services/items';
|
||||
import { ColumnBuilder } from 'knex';
|
||||
import getLocalType from '../utils/get-local-type';
|
||||
@@ -10,7 +16,10 @@ import Knex, { CreateTableBuilder } from 'knex';
|
||||
import { PayloadService } from '../services/payload';
|
||||
import getDefaultValue from '../utils/get-default-value';
|
||||
import cache from '../cache';
|
||||
import SchemaInspector from 'knex-schema-inspector';
|
||||
import SchemaInspector from '@directus/schema';
|
||||
import { toArray } from '../utils/to-array';
|
||||
|
||||
import { systemFieldRows } from '../database/system-data/fields/';
|
||||
|
||||
type RawField = Partial<Field> & { field: string; type: typeof types[number] };
|
||||
|
||||
@@ -20,29 +29,39 @@ export class FieldsService {
|
||||
itemsService: ItemsService;
|
||||
payloadService: PayloadService;
|
||||
schemaInspector: typeof schemaInspector;
|
||||
schema: SchemaOverview;
|
||||
|
||||
constructor(options?: AbstractServiceOptions) {
|
||||
this.knex = options?.knex || database;
|
||||
this.schemaInspector = options?.knex ? SchemaInspector(options.knex) : schemaInspector;
|
||||
this.accountability = options?.accountability || null;
|
||||
constructor(options: AbstractServiceOptions) {
|
||||
this.knex = options.knex || database;
|
||||
this.schemaInspector = options.knex ? SchemaInspector(options.knex) : schemaInspector;
|
||||
this.accountability = options.accountability || null;
|
||||
this.itemsService = new ItemsService('directus_fields', options);
|
||||
this.payloadService = new PayloadService('directus_fields');
|
||||
this.payloadService = new PayloadService('directus_fields', options);
|
||||
this.schema = options.schema;
|
||||
}
|
||||
|
||||
async readAll(collection?: string): Promise<Field[]> {
|
||||
let fields: FieldMeta[];
|
||||
const nonAuthorizedItemsService = new ItemsService('directus_fields', { knex: this.knex });
|
||||
const nonAuthorizedItemsService = new ItemsService('directus_fields', {
|
||||
knex: this.knex,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
if (collection) {
|
||||
fields = (await nonAuthorizedItemsService.readByQuery({
|
||||
filter: { collection: { _eq: collection } },
|
||||
limit: -1,
|
||||
})) as FieldMeta[];
|
||||
|
||||
fields.push(
|
||||
...systemFieldRows.filter((fieldMeta) => fieldMeta.collection === collection)
|
||||
);
|
||||
} else {
|
||||
fields = (await nonAuthorizedItemsService.readByQuery({ limit: -1 })) as FieldMeta[];
|
||||
fields.push(...systemFieldRows);
|
||||
}
|
||||
|
||||
let columns = await schemaInspector.columnInfo(collection);
|
||||
let columns = await this.schemaInspector.columnInfo(collection);
|
||||
|
||||
columns = columns.map((column) => {
|
||||
return {
|
||||
@@ -73,12 +92,22 @@ export class FieldsService {
|
||||
aliasQuery.andWhere('collection', collection);
|
||||
}
|
||||
|
||||
let aliasFields = await aliasQuery;
|
||||
let aliasFields = [
|
||||
...((await this.payloadService.processValues('read', await aliasQuery)) as FieldMeta[]),
|
||||
];
|
||||
|
||||
if (collection) {
|
||||
aliasFields.push(
|
||||
...systemFieldRows.filter((fieldMeta) => fieldMeta.collection === collection)
|
||||
);
|
||||
} else {
|
||||
aliasFields.push(...systemFieldRows);
|
||||
}
|
||||
|
||||
const aliasTypes = ['alias', 'o2m', 'm2m', 'files', 'files', 'translations'];
|
||||
|
||||
aliasFields = aliasFields.filter((field) => {
|
||||
const specials = (field.special || '').split(',');
|
||||
const specials = toArray(field.special);
|
||||
|
||||
for (const type of aliasTypes) {
|
||||
if (specials.includes(type)) return true;
|
||||
@@ -87,19 +116,17 @@ export class FieldsService {
|
||||
return false;
|
||||
});
|
||||
|
||||
aliasFields = (await this.payloadService.processValues('read', aliasFields)) as FieldMeta[];
|
||||
|
||||
const aliasFieldsAsField = aliasFields.map((field) => {
|
||||
const data = {
|
||||
collection: field.collection,
|
||||
field: field.field,
|
||||
type: field.special[0],
|
||||
type: Array.isArray(field.special) ? field.special[0] : field.special,
|
||||
schema: null,
|
||||
meta: field,
|
||||
};
|
||||
|
||||
return data;
|
||||
});
|
||||
}) as Field[];
|
||||
|
||||
const result = [...columnsWithSystem, ...aliasFieldsAsField];
|
||||
|
||||
@@ -163,8 +190,14 @@ export class FieldsService {
|
||||
fieldInfo = (await this.payloadService.processValues('read', fieldInfo)) as FieldMeta[];
|
||||
}
|
||||
|
||||
fieldInfo =
|
||||
fieldInfo ||
|
||||
systemFieldRows.find(
|
||||
(fieldMeta) => fieldMeta.collection === collection && fieldMeta.field === field
|
||||
);
|
||||
|
||||
try {
|
||||
column = await schemaInspector.columnInfo(collection, field);
|
||||
column = await this.schemaInspector.columnInfo(collection, field);
|
||||
column.default_value = getDefaultValue(column);
|
||||
} catch {}
|
||||
|
||||
@@ -189,7 +222,7 @@ export class FieldsService {
|
||||
}
|
||||
|
||||
// Check if field already exists, either as a column, or as a row in directus_fields
|
||||
if (await this.schemaInspector.hasColumn(collection, field.field)) {
|
||||
if (field.field in this.schema[collection].columns) {
|
||||
throw new InvalidPayloadException(
|
||||
`Field "${field.field}" already exists in collection "${collection}"`
|
||||
);
|
||||
@@ -250,8 +283,8 @@ export class FieldsService {
|
||||
const type = field.type as 'float' | 'decimal';
|
||||
column = table[type](
|
||||
field.field,
|
||||
field.schema?.precision || 10,
|
||||
field.schema?.scale || 5
|
||||
field.schema?.numeric_precision || 10,
|
||||
field.schema?.numeric_scale || 5
|
||||
);
|
||||
} else if (field.type === 'csv') {
|
||||
column = table.string(field.field);
|
||||
@@ -259,10 +292,11 @@ export class FieldsService {
|
||||
column = table[field.type](field.field);
|
||||
}
|
||||
|
||||
if (field.schema.default_value) {
|
||||
const defaultValue = field.schema.default_value.toLowerCase();
|
||||
|
||||
if (defaultValue === 'now()') {
|
||||
if (field.schema.default_value !== undefined) {
|
||||
if (
|
||||
typeof field.schema.default_value === 'string' &&
|
||||
field.schema.default_value.toLowerCase() === 'now()'
|
||||
) {
|
||||
column.defaultTo(this.knex.fn.now());
|
||||
} else {
|
||||
column.defaultTo(field.schema.default_value);
|
||||
@@ -319,7 +353,7 @@ export class FieldsService {
|
||||
|
||||
await this.knex('directus_fields').delete().where({ collection, field });
|
||||
|
||||
if (await schemaInspector.hasColumn(collection, field)) {
|
||||
if (field in this.schema[collection].columns) {
|
||||
await this.knex.schema.table(collection, (table) => {
|
||||
table.dropColumn(field);
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ import { extension } from 'mime-types';
|
||||
import path from 'path';
|
||||
|
||||
export class FilesService extends ItemsService {
|
||||
constructor(options?: AbstractServiceOptions) {
|
||||
constructor(options: AbstractServiceOptions) {
|
||||
super('directus_files', options);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ export class FilesService extends ItemsService {
|
||||
const fileExtension =
|
||||
(payload.type && extension(payload.type)) || path.extname(payload.filename_download);
|
||||
|
||||
payload.filename_disk = primaryKey + fileExtension;
|
||||
payload.filename_disk = primaryKey + '.' + fileExtension;
|
||||
|
||||
if (!payload.type) {
|
||||
payload.type = 'application/octet-stream';
|
||||
@@ -81,7 +81,10 @@ export class FilesService extends ItemsService {
|
||||
|
||||
// We do this in a service without accountability. Even if you don't have update permissions to the file,
|
||||
// we still want to be able to set the extracted values from the file on create
|
||||
const sudoService = new ItemsService('directus_files');
|
||||
const sudoService = new ItemsService('directus_files', {
|
||||
knex: this.knex,
|
||||
schema: this.schema,
|
||||
});
|
||||
await sudoService.update(payload, primaryKey);
|
||||
|
||||
if (cache) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ItemsService } from './items';
|
||||
import { AbstractServiceOptions } from '../types';
|
||||
|
||||
export class FoldersService extends ItemsService {
|
||||
constructor(options?: AbstractServiceOptions) {
|
||||
constructor(options: AbstractServiceOptions) {
|
||||
super('directus_folders', options);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
Field,
|
||||
Relation,
|
||||
Query,
|
||||
AbstractService,
|
||||
SchemaOverview,
|
||||
} from '../types';
|
||||
import {
|
||||
GraphQLString,
|
||||
@@ -49,6 +49,7 @@ import { UsersService } from './users';
|
||||
import { WebhooksService } from './webhooks';
|
||||
|
||||
import { getRelationType } from '../utils/get-relation-type';
|
||||
import { systemCollectionRows } from '../database/system-data/collections';
|
||||
|
||||
export class GraphQLService {
|
||||
accountability: Accountability | null;
|
||||
@@ -56,13 +57,15 @@ export class GraphQLService {
|
||||
fieldsService: FieldsService;
|
||||
collectionsService: CollectionsService;
|
||||
relationsService: RelationsService;
|
||||
schema: SchemaOverview;
|
||||
|
||||
constructor(options?: AbstractServiceOptions) {
|
||||
constructor(options: AbstractServiceOptions) {
|
||||
this.accountability = options?.accountability || null;
|
||||
this.knex = options?.knex || database;
|
||||
this.fieldsService = new FieldsService(options);
|
||||
this.collectionsService = new CollectionsService(options);
|
||||
this.relationsService = new RelationsService(options);
|
||||
this.schema = options.schema;
|
||||
}
|
||||
|
||||
args = {
|
||||
@@ -204,8 +207,9 @@ export class GraphQLService {
|
||||
return fieldsObject;
|
||||
},
|
||||
}),
|
||||
resolve: (source: any, args: any, context: any, info: GraphQLResolveInfo) =>
|
||||
this.resolve(info),
|
||||
resolve: (source: any, args: any, context: any, info: GraphQLResolveInfo) => {
|
||||
return this.resolve(info);
|
||||
},
|
||||
args: {
|
||||
...this.args,
|
||||
filter: {
|
||||
@@ -240,19 +244,36 @@ export class GraphQLService {
|
||||
}
|
||||
}
|
||||
|
||||
schemaWithLists.items = {
|
||||
type: new GraphQLObjectType({
|
||||
name: 'items',
|
||||
fields: schemaWithLists.items,
|
||||
}),
|
||||
resolve: () => ({}),
|
||||
const queryBase: any = {
|
||||
name: 'Directus',
|
||||
fields: {
|
||||
server: {
|
||||
type: new GraphQLObjectType({
|
||||
name: 'server',
|
||||
fields: {
|
||||
ping: {
|
||||
type: GraphQLString,
|
||||
resolve: () => 'pong',
|
||||
},
|
||||
},
|
||||
}),
|
||||
resolve: () => ({}),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (Object.keys(schemaWithLists.items).length > 0) {
|
||||
queryBase.fields.items = {
|
||||
type: new GraphQLObjectType({
|
||||
name: 'items',
|
||||
fields: schemaWithLists.items,
|
||||
}),
|
||||
resolve: () => ({}),
|
||||
};
|
||||
}
|
||||
|
||||
return new GraphQLSchema({
|
||||
query: new GraphQLObjectType({
|
||||
name: 'Directus',
|
||||
fields: schemaWithLists,
|
||||
}),
|
||||
query: new GraphQLObjectType(queryBase),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -380,20 +401,28 @@ export class GraphQLService {
|
||||
const systemField = info.path.prev?.key !== 'items';
|
||||
|
||||
const collection = systemField ? `directus_${info.fieldName}` : info.fieldName;
|
||||
|
||||
const selections = info.fieldNodes[0]?.selectionSet?.selections?.filter(
|
||||
(node) => node.kind === 'Field'
|
||||
) as FieldNode[] | undefined;
|
||||
|
||||
if (!selections) return null;
|
||||
|
||||
return await this.getData(collection, selections, info.fieldNodes[0].arguments);
|
||||
return await this.getData(
|
||||
collection,
|
||||
selections,
|
||||
info.fieldNodes[0].arguments || [],
|
||||
info.variableValues
|
||||
);
|
||||
}
|
||||
|
||||
async getData(
|
||||
collection: string,
|
||||
selections: FieldNode[],
|
||||
argsArray?: readonly ArgumentNode[]
|
||||
argsArray: readonly ArgumentNode[],
|
||||
variableValues: GraphQLResolveInfo['variableValues']
|
||||
) {
|
||||
const args: Record<string, any> = this.parseArgs(argsArray);
|
||||
const args: Record<string, any> = this.parseArgs(argsArray, variableValues);
|
||||
|
||||
const query: Query = sanitizeQuery(args, this.accountability);
|
||||
|
||||
@@ -418,7 +447,10 @@ export class GraphQLService {
|
||||
if (selection.arguments && selection.arguments.length > 0) {
|
||||
if (!query.deep) query.deep = {};
|
||||
|
||||
const args: Record<string, any> = this.parseArgs(selection.arguments);
|
||||
const args: Record<string, any> = this.parseArgs(
|
||||
selection.arguments,
|
||||
variableValues
|
||||
);
|
||||
query.deep[current] = sanitizeQuery(args, this.accountability);
|
||||
}
|
||||
}
|
||||
@@ -437,6 +469,7 @@ export class GraphQLService {
|
||||
service = new ActivityService({
|
||||
knex: this.knex,
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
// case 'directus_collections':
|
||||
// service = new CollectionsService({ knex: this.knex, accountability: this.accountability });
|
||||
@@ -446,82 +479,101 @@ export class GraphQLService {
|
||||
service = new FilesService({
|
||||
knex: this.knex,
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
case 'directus_folders':
|
||||
service = new FoldersService({
|
||||
knex: this.knex,
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
case 'directus_folders':
|
||||
service = new FoldersService({
|
||||
knex: this.knex,
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
case 'directus_permissions':
|
||||
service = new PermissionsService({
|
||||
knex: this.knex,
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
case 'directus_presets':
|
||||
service = new PresetsService({
|
||||
knex: this.knex,
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
case 'directus_relations':
|
||||
service = new RelationsService({
|
||||
knex: this.knex,
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
case 'directus_revisions':
|
||||
service = new RevisionsService({
|
||||
knex: this.knex,
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
case 'directus_roles':
|
||||
service = new RolesService({
|
||||
knex: this.knex,
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
case 'directus_settings':
|
||||
service = new SettingsService({
|
||||
knex: this.knex,
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
case 'directus_users':
|
||||
service = new UsersService({
|
||||
knex: this.knex,
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
case 'directus_webhooks':
|
||||
service = new WebhooksService({
|
||||
knex: this.knex,
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
default:
|
||||
service = new ItemsService(collection, {
|
||||
knex: this.knex,
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
}
|
||||
|
||||
const collectionInfo = await this.knex
|
||||
.select('singleton')
|
||||
.from('directus_collections')
|
||||
.where({ collection: collection })
|
||||
.first();
|
||||
const result =
|
||||
collectionInfo?.singleton === true
|
||||
? await service.readSingleton(query)
|
||||
: await service.readByQuery(query);
|
||||
const collectionInfo =
|
||||
(await this.knex
|
||||
.select('singleton')
|
||||
.from('directus_collections')
|
||||
.where({ collection: collection })
|
||||
.first()) ||
|
||||
systemCollectionRows.find(
|
||||
(collectionMeta) => collectionMeta?.collection === collection
|
||||
);
|
||||
|
||||
const result = collectionInfo?.singleton
|
||||
? await service.readSingleton(query)
|
||||
: await service.readByQuery(query);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
parseArgs(args?: readonly ArgumentNode[] | readonly ObjectFieldNode[]): Record<string, any> {
|
||||
if (!args) return {};
|
||||
parseArgs(
|
||||
args: readonly ArgumentNode[] | readonly ObjectFieldNode[],
|
||||
variableValues: GraphQLResolveInfo['variableValues']
|
||||
): Record<string, any> {
|
||||
if (!args || args.length === 0) return {};
|
||||
|
||||
const parseObjectValue = (arg: ObjectValueNode) => {
|
||||
return this.parseArgs(arg.fields);
|
||||
return this.parseArgs(arg.fields, variableValues);
|
||||
};
|
||||
|
||||
const argsObject: any = {};
|
||||
@@ -529,12 +581,14 @@ export class GraphQLService {
|
||||
for (const argument of args) {
|
||||
if (argument.value.kind === 'ObjectValue') {
|
||||
argsObject[argument.name.value] = parseObjectValue(argument.value);
|
||||
} else if (argument.value.kind === 'Variable') {
|
||||
argsObject[argument.name.value] = variableValues[argument.value.name.value];
|
||||
} else if (argument.value.kind === 'ListValue') {
|
||||
const values: any = [];
|
||||
|
||||
for (const valueNode of argument.value.values) {
|
||||
if (valueNode.kind === 'ObjectValue') {
|
||||
values.push(this.parseArgs(valueNode.fields));
|
||||
values.push(this.parseArgs(valueNode.fields, variableValues));
|
||||
} else {
|
||||
values.push((valueNode as any).value);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import database from '../database';
|
||||
import SchemaInspector from 'knex-schema-inspector';
|
||||
import runAST from '../database/run-ast';
|
||||
import getASTFromQuery from '../utils/get-ast-from-query';
|
||||
import {
|
||||
@@ -11,6 +10,7 @@ import {
|
||||
PrimaryKey,
|
||||
AbstractService,
|
||||
AbstractServiceOptions,
|
||||
SchemaOverview,
|
||||
} from '../types';
|
||||
import Knex from 'knex';
|
||||
import cache from '../cache';
|
||||
@@ -31,17 +31,16 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
knex: Knex;
|
||||
accountability: Accountability | null;
|
||||
eventScope: string;
|
||||
schemaInspector: ReturnType<typeof SchemaInspector>;
|
||||
schema: SchemaOverview;
|
||||
|
||||
constructor(collection: string, options?: AbstractServiceOptions) {
|
||||
constructor(collection: string, options: AbstractServiceOptions) {
|
||||
this.collection = collection;
|
||||
this.knex = options?.knex || database;
|
||||
this.accountability = options?.accountability || null;
|
||||
this.knex = options.knex || database;
|
||||
this.accountability = options.accountability || null;
|
||||
this.eventScope = this.collection.startsWith('directus_')
|
||||
? this.collection.substring(9)
|
||||
: 'items';
|
||||
|
||||
this.schemaInspector = SchemaInspector(this.knex);
|
||||
this.schema = options.schema;
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -49,8 +48,8 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
async create(data: Partial<Item>[]): Promise<PrimaryKey[]>;
|
||||
async create(data: Partial<Item>): Promise<PrimaryKey>;
|
||||
async create(data: Partial<Item> | Partial<Item>[]): Promise<PrimaryKey | PrimaryKey[]> {
|
||||
const primaryKeyField = await this.schemaInspector.primary(this.collection);
|
||||
const columns = await this.schemaInspector.columns(this.collection);
|
||||
const primaryKeyField = this.schema[this.collection].primary;
|
||||
const columns = Object.keys(this.schema[this.collection].columns);
|
||||
|
||||
let payloads: AnyItem[] = clone(toArray(data));
|
||||
|
||||
@@ -58,6 +57,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
const payloadService = new PayloadService(this.collection, {
|
||||
accountability: this.accountability,
|
||||
knex: trx,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
const customProcessed = await emitter.emitAsync(
|
||||
@@ -70,6 +70,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
item: null,
|
||||
action: 'create',
|
||||
payload: payloads,
|
||||
schema: this.schema,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -81,6 +82,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
const authorizationService = new AuthorizationService({
|
||||
accountability: this.accountability,
|
||||
knex: trx,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
payloads = await authorizationService.validatePayload(
|
||||
@@ -92,12 +94,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
|
||||
payloads = await payloadService.processM2O(payloads);
|
||||
|
||||
let payloadsWithoutAliases = payloads.map((payload) =>
|
||||
pick(
|
||||
payload,
|
||||
columns.map(({ column }) => column)
|
||||
)
|
||||
);
|
||||
let payloadsWithoutAliases = payloads.map((payload) => pick(payload, columns));
|
||||
|
||||
payloadsWithoutAliases = await payloadService.processValues(
|
||||
'create',
|
||||
@@ -185,6 +182,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
item: primaryKeys,
|
||||
action: 'create',
|
||||
payload: payloads,
|
||||
schema: this.schema,
|
||||
})
|
||||
.catch((err) => logger.warn(err));
|
||||
|
||||
@@ -198,9 +196,10 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
const authorizationService = new AuthorizationService({
|
||||
accountability: this.accountability,
|
||||
knex: this.knex,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
let ast = await getASTFromQuery(this.collection, query, {
|
||||
let ast = await getASTFromQuery(this.collection, query, this.schema, {
|
||||
accountability: this.accountability,
|
||||
knex: this.knex,
|
||||
});
|
||||
@@ -209,7 +208,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
ast = await authorizationService.processAST(ast);
|
||||
}
|
||||
|
||||
const records = await runAST(ast, { knex: this.knex });
|
||||
const records = await runAST(ast, this.schema, { knex: this.knex });
|
||||
return records as Partial<Item> | Partial<Item>[] | null;
|
||||
}
|
||||
|
||||
@@ -229,7 +228,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
action: PermissionsAction = 'read'
|
||||
): Promise<null | Partial<Item> | Partial<Item>[]> {
|
||||
query = clone(query);
|
||||
const primaryKeyField = await this.schemaInspector.primary(this.collection);
|
||||
const primaryKeyField = this.schema[this.collection].primary;
|
||||
const keys = toArray(key);
|
||||
|
||||
if (keys.length === 1) {
|
||||
@@ -246,7 +245,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
},
|
||||
};
|
||||
|
||||
let ast = await getASTFromQuery(this.collection, queryWithFilter, {
|
||||
let ast = await getASTFromQuery(this.collection, queryWithFilter, this.schema, {
|
||||
accountability: this.accountability,
|
||||
action,
|
||||
knex: this.knex,
|
||||
@@ -256,11 +255,13 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
const authorizationService = new AuthorizationService({
|
||||
accountability: this.accountability,
|
||||
knex: this.knex,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
ast = await authorizationService.processAST(ast, action);
|
||||
}
|
||||
|
||||
const result = await runAST(ast, { knex: this.knex });
|
||||
const result = await runAST(ast, this.schema, { knex: this.knex });
|
||||
|
||||
if (result === null) throw new ForbiddenException();
|
||||
|
||||
@@ -274,8 +275,8 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
data: Partial<Item> | Partial<Item>[],
|
||||
key?: PrimaryKey | PrimaryKey[]
|
||||
): Promise<PrimaryKey | PrimaryKey[]> {
|
||||
const primaryKeyField = await this.schemaInspector.primary(this.collection);
|
||||
const columns = await this.schemaInspector.columns(this.collection);
|
||||
const primaryKeyField = this.schema[this.collection].primary;
|
||||
const columns = Object.keys(this.schema[this.collection].columns);
|
||||
|
||||
// Updating one or more items to the same payload
|
||||
if (data && key) {
|
||||
@@ -290,9 +291,10 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
event: `${this.eventScope}.update.before`,
|
||||
accountability: this.accountability,
|
||||
collection: this.collection,
|
||||
item: null,
|
||||
item: key,
|
||||
action: 'update',
|
||||
payload,
|
||||
schema: this.schema,
|
||||
}
|
||||
);
|
||||
|
||||
@@ -304,6 +306,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
const authorizationService = new AuthorizationService({
|
||||
accountability: this.accountability,
|
||||
knex: this.knex,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
await authorizationService.checkAccess('update', this.collection, keys);
|
||||
@@ -319,14 +322,12 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
const payloadService = new PayloadService(this.collection, {
|
||||
accountability: this.accountability,
|
||||
knex: trx,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
payload = await payloadService.processM2O(payload);
|
||||
|
||||
let payloadWithoutAliases = pick(
|
||||
payload,
|
||||
columns.map(({ column }) => column)
|
||||
);
|
||||
let payloadWithoutAliases = pick(payload, columns);
|
||||
|
||||
payloadWithoutAliases = await payloadService.processValues(
|
||||
'update',
|
||||
@@ -369,7 +370,10 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
activityPrimaryKeys.push(primaryKey);
|
||||
}
|
||||
|
||||
const itemsService = new ItemsService(this.collection, { knex: trx });
|
||||
const itemsService = new ItemsService(this.collection, {
|
||||
knex: trx,
|
||||
schema: this.schema,
|
||||
});
|
||||
const snapshots = await itemsService.readByKey(keys);
|
||||
|
||||
const revisionRecords = activityPrimaryKeys.map((key, index) => ({
|
||||
@@ -399,6 +403,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
item: key,
|
||||
action: 'update',
|
||||
payload,
|
||||
schema: this.schema,
|
||||
})
|
||||
.catch((err) => logger.warn(err));
|
||||
|
||||
@@ -411,6 +416,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
const itemsService = new ItemsService(this.collection, {
|
||||
accountability: this.accountability,
|
||||
knex: trx,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
const payloads = toArray(data);
|
||||
@@ -433,12 +439,15 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
}
|
||||
|
||||
async updateByQuery(data: Partial<Item>, query: Query): Promise<PrimaryKey[]> {
|
||||
const primaryKeyField = await this.schemaInspector.primary(this.collection);
|
||||
const primaryKeyField = this.schema[this.collection].primary;
|
||||
const readQuery = cloneDeep(query);
|
||||
readQuery.fields = [primaryKeyField];
|
||||
|
||||
// Not authenticated:
|
||||
const itemsService = new ItemsService(this.collection, { knex: this.knex });
|
||||
const itemsService = new ItemsService(this.collection, {
|
||||
knex: this.knex,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
let itemsToUpdate = await itemsService.readByQuery(readQuery);
|
||||
itemsToUpdate = toArray(itemsToUpdate);
|
||||
@@ -453,7 +462,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
upsert(data: Partial<Item>[]): Promise<PrimaryKey[]>;
|
||||
upsert(data: Partial<Item>): Promise<PrimaryKey>;
|
||||
async upsert(data: Partial<Item> | Partial<Item>[]): Promise<PrimaryKey | PrimaryKey[]> {
|
||||
const primaryKeyField = await this.schemaInspector.primary(this.collection);
|
||||
const primaryKeyField = this.schema[this.collection].primary;
|
||||
const payloads = toArray(data);
|
||||
const primaryKeys: PrimaryKey[] = [];
|
||||
|
||||
@@ -483,11 +492,12 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
delete(keys: PrimaryKey[]): Promise<PrimaryKey[]>;
|
||||
async delete(key: PrimaryKey | PrimaryKey[]): Promise<PrimaryKey | PrimaryKey[]> {
|
||||
const keys = toArray(key);
|
||||
const primaryKeyField = await this.schemaInspector.primary(this.collection);
|
||||
const primaryKeyField = this.schema[this.collection].primary;
|
||||
|
||||
if (this.accountability && this.accountability.admin !== true) {
|
||||
const authorizationService = new AuthorizationService({
|
||||
accountability: this.accountability,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
await authorizationService.checkAccess('delete', this.collection, key);
|
||||
@@ -500,6 +510,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
item: keys,
|
||||
action: 'delete',
|
||||
payload: null,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
await this.knex.transaction(async (trx) => {
|
||||
@@ -531,6 +542,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
item: keys,
|
||||
action: 'delete',
|
||||
payload: null,
|
||||
schema: this.schema,
|
||||
})
|
||||
.catch((err) => logger.warn(err));
|
||||
|
||||
@@ -538,12 +550,15 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
}
|
||||
|
||||
async deleteByQuery(query: Query): Promise<PrimaryKey[]> {
|
||||
const primaryKeyField = await this.schemaInspector.primary(this.collection);
|
||||
const primaryKeyField = this.schema[this.collection].primary;
|
||||
const readQuery = cloneDeep(query);
|
||||
readQuery.fields = [primaryKeyField];
|
||||
|
||||
// Not authenticated:
|
||||
const itemsService = new ItemsService(this.collection);
|
||||
const itemsService = new ItemsService(this.collection, {
|
||||
knex: this.knex,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
let itemsToDelete = await itemsService.readByQuery(readQuery);
|
||||
itemsToDelete = toArray(itemsToDelete);
|
||||
@@ -561,11 +576,17 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
const record = (await this.readByQuery(query)) as Partial<Item>;
|
||||
|
||||
if (!record) {
|
||||
const columns = await this.schemaInspector.columnInfo(this.collection);
|
||||
let columns = Object.values(this.schema[this.collection].columns);
|
||||
const defaults: Record<string, any> = {};
|
||||
|
||||
if (query.fields && query.fields.includes('*') === false) {
|
||||
columns = columns.filter((column) => {
|
||||
return query.fields!.includes(column.column_name);
|
||||
});
|
||||
}
|
||||
|
||||
for (const column of columns) {
|
||||
defaults[column.name] = getDefaultValue(column);
|
||||
defaults[column.column_name] = getDefaultValue(column);
|
||||
}
|
||||
|
||||
return defaults as Partial<Item>;
|
||||
@@ -575,7 +596,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
}
|
||||
|
||||
async upsertSingleton(data: Partial<Item>) {
|
||||
const primaryKeyField = await this.schemaInspector.primary(this.collection);
|
||||
const primaryKeyField = this.schema[this.collection].primary;
|
||||
|
||||
const record = await this.knex
|
||||
.select(primaryKeyField)
|
||||
|
||||
@@ -32,15 +32,15 @@ export class MetaService {
|
||||
}
|
||||
|
||||
async totalCount(collection: string) {
|
||||
const records = await database(collection).count('*', { as: 'count' });
|
||||
const records = await this.knex(collection).count('*', { as: 'count' });
|
||||
return Number(records[0].count);
|
||||
}
|
||||
|
||||
async filterCount(collection: string, query: Query) {
|
||||
const dbQuery = database(collection).count('*', { as: 'count' });
|
||||
const dbQuery = this.knex(collection).count('*', { as: 'count' });
|
||||
|
||||
if (query.filter) {
|
||||
await applyFilter(dbQuery, query.filter, collection);
|
||||
await applyFilter(this.knex, dbQuery, query.filter, collection);
|
||||
}
|
||||
|
||||
const records = await dbQuery;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user