mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Merge branch 'main' into error-handling
This commit is contained in:
33
.github/actions/build-images/rootfs/directus/images/main/examples/docker-compose.yml
vendored
Normal file
33
.github/actions/build-images/rootfs/directus/images/main/examples/docker-compose.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
version: "3"
|
||||
services:
|
||||
database:
|
||||
image: postgres:12
|
||||
networks:
|
||||
- "directus"
|
||||
environment:
|
||||
POSTGRES_DB: "directus"
|
||||
POSTGRES_USER: "directus"
|
||||
POSTGRES_PASSWORD: "directus"
|
||||
|
||||
directus:
|
||||
build:
|
||||
context: "../"
|
||||
args:
|
||||
VERSION: "v9.0.0-rc.5"
|
||||
REPOSITORY: "directus/next"
|
||||
ports:
|
||||
- 8055:8055
|
||||
networks:
|
||||
- "directus"
|
||||
environment:
|
||||
KEY: mykey
|
||||
SECRET: mysecret
|
||||
DB_CLIENT: "pg"
|
||||
DB_HOST: "database"
|
||||
DB_PORT: "5432"
|
||||
DB_USER: "directus"
|
||||
DB_PASSWORD: "directus"
|
||||
DB_DATABASE: "directus"
|
||||
|
||||
networks:
|
||||
directus:
|
||||
@@ -39,6 +39,8 @@ MSG
|
||||
function bootstrap() {
|
||||
local warn=false
|
||||
|
||||
print --level=info "Initializing..."
|
||||
|
||||
if [ "${KEY}" == "" ] ; then
|
||||
export KEY=$(uuidgen)
|
||||
warn=true
|
||||
@@ -75,6 +77,13 @@ WARN
|
||||
if [ ! -f "${DB_FILENAME}" ] ; then
|
||||
mkdir -p $(dirname ${DB_FILENAME})
|
||||
fi
|
||||
else
|
||||
print --level=info "Checking database connection"
|
||||
timeout ${DB_TIMEOUT:-"30"} bash -c 'until nc -z -w 1 "$0" $1; do sleep 1; done' "${DB_HOST}" ${DB_PORT}
|
||||
#while ! nc -z -w 1 "${DB_HOST}" ${DB_PORT}; do
|
||||
# print --level=warn "Cannot connect to the database, waiting for the server."
|
||||
# sleep 1
|
||||
#done
|
||||
fi
|
||||
|
||||
should_seed=false
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -8,3 +8,5 @@ lerna-debug.log
|
||||
.nova
|
||||
*.code-workspace
|
||||
dist
|
||||
*.sublime-settings
|
||||
|
||||
|
||||
5
api/index.js
Normal file
5
api/index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
module.exports = {
|
||||
createApp: require('./dist/app').default,
|
||||
...require('./dist/exceptions'),
|
||||
...require('./dist/services'),
|
||||
};
|
||||
12229
api/package-lock.json
generated
12229
api/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "directus",
|
||||
"version": "9.0.0-rc.2",
|
||||
"version": "9.0.0-rc.7",
|
||||
"license": "GPL-3.0-only",
|
||||
"homepage": "https://github.com/directus/next#readme",
|
||||
"description": "Directus is a real-time API and App dashboard for managing SQL database content.",
|
||||
@@ -46,7 +46,7 @@
|
||||
"url": "https://github.com/benhaynes"
|
||||
}
|
||||
],
|
||||
"main": "dist/app.js",
|
||||
"main": "index.js",
|
||||
"bin": {
|
||||
"directus": "cli.js"
|
||||
},
|
||||
@@ -68,23 +68,23 @@
|
||||
"@directus/format-title": "file:../packages/format-title",
|
||||
"@directus/specs": "file:../packages/spec",
|
||||
"@godaddy/terminus": "^4.4.1",
|
||||
"@slynova/flydrive": "^1.0.2",
|
||||
"@slynova/flydrive-gcs": "^1.0.2",
|
||||
"@slynova/flydrive-s3": "^1.0.2",
|
||||
"argon2": "^0.26.2",
|
||||
"@slynova/flydrive": "^1.0.3",
|
||||
"@slynova/flydrive-gcs": "^1.0.3",
|
||||
"@slynova/flydrive-s3": "^1.0.3",
|
||||
"argon2": "^0.27.0",
|
||||
"atob": "^2.1.2",
|
||||
"axios": "^0.19.2",
|
||||
"axios": "^0.21.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"busboy": "^0.3.1",
|
||||
"camelcase": "^6.0.0",
|
||||
"camelcase": "^6.2.0",
|
||||
"chalk": "^4.1.0",
|
||||
"commander": "^5.1.0",
|
||||
"commander": "^6.2.0",
|
||||
"cookie-parser": "^1.4.5",
|
||||
"cors": "^2.8.5",
|
||||
"date-fns": "^2.16.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"eventemitter2": "^6.4.3",
|
||||
"execa": "^4.0.3",
|
||||
"execa": "^4.1.0",
|
||||
"exif-reader": "^1.0.3",
|
||||
"express": "^4.17.1",
|
||||
"express-async-handler": "^1.1.4",
|
||||
@@ -92,53 +92,54 @@
|
||||
"express-pino-logger": "^5.0.0",
|
||||
"express-session": "^1.17.1",
|
||||
"fs-extra": "^9.0.1",
|
||||
"grant": "^5.3.0",
|
||||
"graphql": "^15.3.0",
|
||||
"grant": "^5.4.5",
|
||||
"graphql": "^15.4.0",
|
||||
"graphql-type-json": "^0.3.2",
|
||||
"icc": "^2.0.0",
|
||||
"inquirer": "^7.3.3",
|
||||
"joi": "^17.1.1",
|
||||
"joi": "^17.3.0",
|
||||
"js-yaml": "^3.14.0",
|
||||
"json2csv": "^5.0.1",
|
||||
"json2csv": "^5.0.3",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"keyv": "^4.0.1",
|
||||
"knex": "^0.21.4",
|
||||
"knex-schema-inspector": "^0.0.21",
|
||||
"liquidjs": "^9.14.1",
|
||||
"lodash": "^4.17.19",
|
||||
"keyv": "^4.0.3",
|
||||
"knex": "^0.21.12",
|
||||
"knex-schema-inspector": "^0.0.25",
|
||||
"liquidjs": "^9.16.1",
|
||||
"lodash": "^4.17.20",
|
||||
"macos-release": "^2.4.1",
|
||||
"mime-types": "^2.1.27",
|
||||
"ms": "^2.1.2",
|
||||
"nanoid": "^3.1.12",
|
||||
"nanoid": "^3.1.16",
|
||||
"node-machine-id": "^1.1.12",
|
||||
"nodemailer": "^6.4.11",
|
||||
"nodemailer": "^6.4.14",
|
||||
"openapi3-ts": "^2.0.0",
|
||||
"ora": "^4.1.1",
|
||||
"ora": "^5.1.0",
|
||||
"otplib": "^12.0.1",
|
||||
"pino": "^6.4.1",
|
||||
"pino": "^6.7.0",
|
||||
"pino-colada": "^2.1.0",
|
||||
"qs": "^6.9.4",
|
||||
"rate-limiter-flexible": "^2.1.10",
|
||||
"rate-limiter-flexible": "^2.1.13",
|
||||
"resolve-cwd": "^3.0.0",
|
||||
"sharp": "^0.25.4",
|
||||
"uuid": "^8.3.0",
|
||||
"sharp": "^0.26.2",
|
||||
"uuid": "^8.3.1",
|
||||
"uuid-validate": "0.0.3"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@keyv/redis": "^2.1.2",
|
||||
"ioredis": "^4.17.3",
|
||||
"keyv-memcache": "^0.8.0",
|
||||
"ioredis": "^4.19.2",
|
||||
"keyv-memcache": "^1.0.1",
|
||||
"memcached": "^2.2.2",
|
||||
"mssql": "^6.2.0",
|
||||
"mssql": "^6.2.3",
|
||||
"mysql": "^2.18.1",
|
||||
"oracledb": "^5.0.0",
|
||||
"pg": "^8.4.1",
|
||||
"pg": "^8.4.2",
|
||||
"sqlite3": "^5.0.0"
|
||||
},
|
||||
"gitHead": "4476da28dbbc2824e680137aa28b2b91b5afabec",
|
||||
"devDependencies": {
|
||||
"copyfiles": "^2.4.0",
|
||||
"cross-env": "^7.0.2",
|
||||
"ts-node-dev": "^1.0.0-pre.64",
|
||||
"typescript": "^4.0.3"
|
||||
"ts-node-dev": "^1.0.0",
|
||||
"typescript": "^4.0.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
@@ -44,9 +46,18 @@ import { InvalidPayloadException } from './exceptions';
|
||||
import { registerExtensions } from './extensions';
|
||||
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 +91,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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:');
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -9,7 +9,6 @@ import { Transformation } from '../types/assets';
|
||||
import storage from '../storage';
|
||||
import { PayloadService, AssetsService } from '../services';
|
||||
import useCollection from '../middleware/use-collection';
|
||||
import { respond } from '../middleware/respond';
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -79,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.`
|
||||
|
||||
@@ -46,15 +46,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 },
|
||||
|
||||
@@ -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();
|
||||
@@ -84,6 +84,21 @@ 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 });
|
||||
await collectionsService.delete(req.body as string[]);
|
||||
|
||||
return next();
|
||||
}),
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:collection',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
|
||||
@@ -12,6 +12,7 @@ import url from 'url';
|
||||
import path from 'path';
|
||||
import useCollection from '../middleware/use-collection';
|
||||
import { respond } from '../middleware/respond';
|
||||
import { toArray } from '../utils/to-array';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -32,7 +33,7 @@ const multipartHandler = asyncHandler(async (req, res, next) => {
|
||||
* the row in directus_files async during the upload of the actual file.
|
||||
*/
|
||||
|
||||
let disk: string = (env.STORAGE_LOCATIONS as string).split(',')[0].trim();
|
||||
let disk: string = toArray(env.STORAGE_LOCATIONS)[0];
|
||||
let payload: Partial<File> = {};
|
||||
let fileCount = 0;
|
||||
|
||||
@@ -155,7 +156,7 @@ router.post(
|
||||
|
||||
const payload = {
|
||||
filename_download: filename,
|
||||
storage: (env.STORAGE_LOCATIONS as string).split(',')[0].trim(),
|
||||
storage: toArray(env.STORAGE_LOCATIONS)[0],
|
||||
type: fileResponse.headers['content-type'],
|
||||
title: formatTitle(filename),
|
||||
...(req.body.data || {}),
|
||||
@@ -236,6 +237,20 @@ 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 });
|
||||
await service.delete(req.body as PrimaryKey[]);
|
||||
return next();
|
||||
}),
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, 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();
|
||||
|
||||
@@ -82,6 +83,20 @@ 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 });
|
||||
await service.delete(req.body as PrimaryKey[]);
|
||||
return next();
|
||||
}),
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -90,7 +97,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);
|
||||
@@ -137,6 +172,21 @@ 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 });
|
||||
await service.delete(req.body as PrimaryKey[]);
|
||||
return next();
|
||||
}),
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:collection/:pk',
|
||||
collectionExists,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -108,6 +113,20 @@ 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 });
|
||||
await service.delete(req.body as PrimaryKey[]);
|
||||
return next();
|
||||
}),
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, 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();
|
||||
|
||||
@@ -82,6 +83,20 @@ 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 });
|
||||
await service.delete(req.body as PrimaryKey[]);
|
||||
return next();
|
||||
}),
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, 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();
|
||||
|
||||
@@ -81,6 +82,20 @@ 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 });
|
||||
await service.delete(req.body as PrimaryKey[]);
|
||||
return next();
|
||||
}),
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -81,6 +82,20 @@ 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 });
|
||||
await service.delete(req.body as PrimaryKey[]);
|
||||
return next();
|
||||
}),
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
|
||||
@@ -20,12 +20,12 @@ router.get('/ping', (req, res) => res.send('pong'));
|
||||
|
||||
router.get(
|
||||
'/info',
|
||||
(req, res, next) => {
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new ServerService({ accountability: req.accountability });
|
||||
const data = service.serverInfo();
|
||||
const data = await service.serverInfo();
|
||||
res.locals.payload = { data };
|
||||
return next();
|
||||
},
|
||||
}),
|
||||
respond
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import express from 'express';
|
||||
import argon2 from 'argon2';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import Joi from 'joi';
|
||||
import {
|
||||
@@ -10,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();
|
||||
|
||||
@@ -150,6 +150,21 @@ 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 });
|
||||
await service.delete(req.body as PrimaryKey[]);
|
||||
|
||||
return next();
|
||||
}),
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, 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();
|
||||
|
||||
@@ -82,6 +83,21 @@ 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 });
|
||||
await service.delete(req.body as PrimaryKey[]);
|
||||
|
||||
return next();
|
||||
}),
|
||||
respond
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
|
||||
@@ -57,4 +57,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 DEFAULT',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: 'directus_files',
|
||||
constraints: [
|
||||
{
|
||||
column: 'folder',
|
||||
references: 'directus_folders.id',
|
||||
onDelete: 'SET DEFAULT',
|
||||
},
|
||||
{
|
||||
column: 'uploaded_by',
|
||||
references: 'directus_users.id',
|
||||
onDelete: 'SET DEFAULT',
|
||||
},
|
||||
{
|
||||
column: 'modified_by',
|
||||
references: 'directus_users.id',
|
||||
onDelete: 'SET DEFAULT',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
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 DEFAULT',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
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 DEFAULT',
|
||||
},
|
||||
{
|
||||
column: 'public_foreground',
|
||||
references: 'directus_files.id',
|
||||
onDelete: 'SET DEFAULT',
|
||||
},
|
||||
{
|
||||
column: 'public_background',
|
||||
references: 'directus_files.id',
|
||||
onDelete: 'SET DEFAULT',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
table: 'directus_users',
|
||||
constraints: [
|
||||
{
|
||||
column: 'role',
|
||||
references: 'directus_roles.id',
|
||||
onDelete: 'SET DEFAULT',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
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[]>('*')
|
||||
|
||||
@@ -105,7 +105,7 @@ async function parseCurrentLevel(
|
||||
) {
|
||||
const schemaInspector = SchemaInspector(knex);
|
||||
|
||||
const primaryKeyField = await schemaInspector.primary(collection);
|
||||
const primaryKeyField = (await schemaInspector.primary(collection)) as string;
|
||||
|
||||
const columnsInCollection = (await schemaInspector.columns(collection)).map(
|
||||
({ column }) => column
|
||||
@@ -164,7 +164,7 @@ async function getDBQuery(
|
||||
|
||||
query.sort = query.sort || [{ column: primaryKeyField, order: 'asc' }];
|
||||
|
||||
await applyQuery(table, dbQuery, queryCopy);
|
||||
await applyQuery(knex, table, dbQuery, queryCopy);
|
||||
|
||||
return dbQuery;
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
table: directus_permissions
|
||||
|
||||
defaults:
|
||||
role: null
|
||||
collection: null
|
||||
action: null
|
||||
permissions: null
|
||||
validation: null
|
||||
presets: null
|
||||
fields: null
|
||||
limit: null
|
||||
|
||||
data:
|
||||
- collection: directus_settings
|
||||
action: read
|
||||
permissions: {}
|
||||
fields: 'project_name,project_logo,project_color,public_foreground,public_background,public_note,custom_css'
|
||||
@@ -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) => {
|
||||
@@ -128,61 +95,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,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
35
api/src/database/system-data/fields/permissions.yaml
Normal file
35
api/src/database/system-data/fields/permissions.yaml
Normal file
@@ -0,0 +1,35 @@
|
||||
# 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
|
||||
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
|
||||
|
||||
- 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',
|
||||
@@ -63,11 +63,6 @@ function processValues(env: Record<string, any>) {
|
||||
if (value === 'false') env[key] = false;
|
||||
if (value === 'null') env[key] = null;
|
||||
if (isNaN(value) === false && value.length > 0) env[key] = Number(value);
|
||||
if (typeof value === 'string' && value.includes(','))
|
||||
env[key] = value
|
||||
.split(',')
|
||||
.map((val) => val.trim())
|
||||
.filter((val) => val);
|
||||
}
|
||||
|
||||
return env;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { SettingsService } from './../services/settings';
|
||||
import logger from '../logger';
|
||||
import nodemailer, { Transporter } from 'nodemailer';
|
||||
import { Liquid } from 'liquidjs';
|
||||
@@ -5,6 +6,7 @@ import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { promisify } from 'util';
|
||||
import env from '../env';
|
||||
import { URL } from 'url';
|
||||
|
||||
const readFile = promisify(fs.readFile);
|
||||
|
||||
@@ -13,6 +15,8 @@ const liquidEngine = new Liquid({
|
||||
extname: '.liquid',
|
||||
});
|
||||
|
||||
const settingsService = new SettingsService();
|
||||
|
||||
let transporter: Transporter;
|
||||
|
||||
if (env.EMAIL_TRANSPORT === 'sendmail') {
|
||||
@@ -42,6 +46,23 @@ export type EmailOptions = {
|
||||
html: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get an object with default template options to pass to the email templates.
|
||||
*/
|
||||
async function getDefaultTemplateOptions() {
|
||||
const projectInfo = await settingsService.readSingleton({
|
||||
fields: ['project_name', 'project_logo', 'project_color'],
|
||||
});
|
||||
|
||||
return {
|
||||
projectName: projectInfo.project_name || 'Directus',
|
||||
projectColor: projectInfo.project_color || '#546e7a',
|
||||
projectLogo: projectInfo.project_logo
|
||||
? new URL(`/assets/${projectInfo.project_logo}`, env.PUBLIC_URL)
|
||||
: 'https://directus.io/assets/directus-white.png',
|
||||
};
|
||||
}
|
||||
|
||||
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,21 +78,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', { email, url, projectName });
|
||||
await transporter.sendMail({ from: env.EMAIL_FROM, to: email, html: html });
|
||||
const html = await liquidEngine.renderFile('user-invitation', {
|
||||
...defaultOptions,
|
||||
email,
|
||||
url,
|
||||
});
|
||||
|
||||
await transporter.sendMail({
|
||||
from: env.EMAIL_FROM,
|
||||
to: email,
|
||||
html: html,
|
||||
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', { email, url, projectName });
|
||||
await transporter.sendMail({ from: env.EMAIL_FROM, to: email, html: html });
|
||||
const html = await liquidEngine.renderFile('password-reset', {
|
||||
...defaultOptions,
|
||||
email,
|
||||
url,
|
||||
});
|
||||
|
||||
await transporter.sendMail({
|
||||
from: env.EMAIL_FROM,
|
||||
to: email,
|
||||
html: html,
|
||||
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,6 +6,8 @@ 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';
|
||||
import { Collection } from '../types';
|
||||
|
||||
const collectionExists: RequestHandler = asyncHandler(async (req, res, next) => {
|
||||
if (!req.params.collection) return next();
|
||||
@@ -18,13 +20,21 @@ const collectionExists: RequestHandler = asyncHandler(async (req, res, next) =>
|
||||
|
||||
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_')
|
||||
);
|
||||
|
||||
@@ -48,9 +48,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;
|
||||
}
|
||||
|
||||
@@ -17,22 +17,26 @@ import { ForbiddenException, FailedValidationException } from '../exceptions';
|
||||
import { uniq, merge, flatten } from 'lodash';
|
||||
import generateJoi from '../utils/generate-joi';
|
||||
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;
|
||||
|
||||
constructor(options?: AbstractServiceOptions) {
|
||||
this.knex = options?.knex || database;
|
||||
this.accountability = options?.accountability || null;
|
||||
this.payloadService = new PayloadService('directus_permissions', { knex: this.knex });
|
||||
}
|
||||
|
||||
async processAST(ast: AST, action: PermissionsAction = 'read'): Promise<AST> {
|
||||
const collectionsRequested = getCollectionsFromAST(ast);
|
||||
|
||||
const permissionsForCollections = await this.knex
|
||||
let permissionsForCollections = await this.knex
|
||||
.select<Permission[]>('*')
|
||||
.from('directus_permissions')
|
||||
.where({ action, role: this.accountability?.role })
|
||||
@@ -41,6 +45,11 @@ export class AuthorizationService {
|
||||
collectionsRequested.map(({ collection }) => collection)
|
||||
);
|
||||
|
||||
permissionsForCollections = (await this.payloadService.processValues(
|
||||
'read',
|
||||
permissionsForCollections
|
||||
)) as Permission[];
|
||||
|
||||
// If the permissions don't match the collections, you don't have permission to read all of them
|
||||
const uniqueCollectionsRequestedCount = uniq(
|
||||
collectionsRequested.map(({ collection }) => collection)
|
||||
@@ -111,7 +120,7 @@ export class AuthorizationService {
|
||||
(permission) => permission.collection === collection
|
||||
)!;
|
||||
|
||||
const allowedFields = permissions.fields?.split(',') || [];
|
||||
const allowedFields = permissions.fields || [];
|
||||
|
||||
for (const childNode of ast.children) {
|
||||
if (childNode.type !== 'field') {
|
||||
@@ -213,21 +222,26 @@ export class AuthorizationService {
|
||||
permissions: {},
|
||||
validation: {},
|
||||
limit: null,
|
||||
fields: '*',
|
||||
fields: ['*'],
|
||||
presets: {},
|
||||
};
|
||||
} else {
|
||||
permission = await this.knex
|
||||
.select<Permission>('*')
|
||||
.select('*')
|
||||
.from('directus_permissions')
|
||||
.where({ action, collection, role: this.accountability?.role || null })
|
||||
.first();
|
||||
|
||||
permission = (await this.payloadService.processValues(
|
||||
'read',
|
||||
permission as Item
|
||||
)) as Permission;
|
||||
|
||||
// Check if you have permission to access the fields you're trying to acces
|
||||
|
||||
if (!permission) throw new ForbiddenException();
|
||||
|
||||
const allowedFields = permission.fields?.split(',') || [];
|
||||
const allowedFields = permission.fields || [];
|
||||
|
||||
if (allowedFields.includes('*') === false) {
|
||||
for (const payload of payloads) {
|
||||
@@ -245,7 +259,7 @@ export class AuthorizationService {
|
||||
}
|
||||
}
|
||||
|
||||
const preset = permission.presets || {};
|
||||
const preset = parseFilter(permission.presets || {}, this.accountability);
|
||||
|
||||
payloads = payloads.map((payload) => merge({}, preset, payload));
|
||||
|
||||
@@ -255,18 +269,26 @@ export class AuthorizationService {
|
||||
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 specials = (field?.special || '').split(',');
|
||||
const field =
|
||||
(await this.knex
|
||||
.select<{ special: string }>('special')
|
||||
.from('directus_fields')
|
||||
.where({ collection, field: column.name })
|
||||
.first()) ||
|
||||
systemFieldRows.find(
|
||||
(fieldMeta) =>
|
||||
fieldMeta.field === column.name && fieldMeta.collection === collection
|
||||
);
|
||||
|
||||
const specials = field?.special ? toArray(field.special) : [];
|
||||
|
||||
const hasGenerateSpecial = [
|
||||
'uuid',
|
||||
'date-created',
|
||||
'role-created',
|
||||
'user-created',
|
||||
].some((name) => specials.includes(name));
|
||||
|
||||
const isRequired =
|
||||
column.is_nullable === false &&
|
||||
column.has_auto_increment === false &&
|
||||
@@ -310,9 +332,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[] = [];
|
||||
|
||||
/**
|
||||
@@ -375,7 +399,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,5 +1,11 @@
|
||||
import database, { schemaInspector } from '../database';
|
||||
import { AbstractServiceOptions, Accountability, Collection, Relation } from '../types';
|
||||
import {
|
||||
AbstractServiceOptions,
|
||||
Accountability,
|
||||
Collection,
|
||||
CollectionMeta,
|
||||
Relation,
|
||||
} from '../types';
|
||||
import Knex from 'knex';
|
||||
import { ForbiddenException, InvalidPayloadException } from '../exceptions';
|
||||
import SchemaInspector from 'knex-schema-inspector';
|
||||
@@ -7,6 +13,7 @@ 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;
|
||||
@@ -106,6 +113,7 @@ export class CollectionsService {
|
||||
knex: this.knex,
|
||||
accountability: this.accountability,
|
||||
});
|
||||
|
||||
const collectionKeys = toArray(collection);
|
||||
|
||||
if (this.accountability && this.accountability.admin !== true) {
|
||||
@@ -135,7 +143,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[] = [];
|
||||
|
||||
@@ -173,7 +183,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[] = [];
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ import { PayloadService } from '../services/payload';
|
||||
import getDefaultValue from '../utils/get-default-value';
|
||||
import cache from '../cache';
|
||||
import SchemaInspector from 'knex-schema-inspector';
|
||||
import { toArray } from '../utils/to-array';
|
||||
|
||||
import { systemFieldRows } from '../database/system-data/fields/';
|
||||
|
||||
type RawField = Partial<Field> & { field: string; type: typeof types[number] };
|
||||
|
||||
@@ -38,8 +41,13 @@ export class FieldsService {
|
||||
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);
|
||||
@@ -73,12 +81,15 @@ export class FieldsService {
|
||||
aliasQuery.andWhere('collection', collection);
|
||||
}
|
||||
|
||||
let aliasFields = await aliasQuery;
|
||||
let aliasFields = [
|
||||
...((await this.payloadService.processValues('read', await aliasQuery)) as FieldMeta[]),
|
||||
...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 +98,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: field.special?.[0],
|
||||
schema: null,
|
||||
meta: field,
|
||||
};
|
||||
|
||||
return data;
|
||||
});
|
||||
}) as Field[];
|
||||
|
||||
const result = [...columnsWithSystem, ...aliasFieldsAsField];
|
||||
|
||||
@@ -163,6 +172,12 @@ 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.default_value = getDefaultValue(column);
|
||||
@@ -260,9 +275,10 @@ export class FieldsService {
|
||||
}
|
||||
|
||||
if (field.schema.default_value) {
|
||||
const defaultValue = field.schema.default_value.toLowerCase();
|
||||
|
||||
if (defaultValue === 'now()') {
|
||||
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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
Field,
|
||||
Relation,
|
||||
Query,
|
||||
AbstractService,
|
||||
} from '../types';
|
||||
import {
|
||||
GraphQLString,
|
||||
@@ -18,7 +17,6 @@ import {
|
||||
GraphQLInputObjectType,
|
||||
ObjectFieldNode,
|
||||
GraphQLID,
|
||||
ValueNode,
|
||||
FieldNode,
|
||||
GraphQLFieldConfigMap,
|
||||
GraphQLInt,
|
||||
@@ -26,11 +24,9 @@ import {
|
||||
StringValueNode,
|
||||
BooleanValueNode,
|
||||
ArgumentNode,
|
||||
GraphQLScalarType,
|
||||
GraphQLBoolean,
|
||||
ObjectValueNode,
|
||||
GraphQLUnionType,
|
||||
GraphQLUnionTypeConfig,
|
||||
} from 'graphql';
|
||||
import { getGraphQLType } from '../utils/get-graphql-type';
|
||||
import { RelationsService } from './relations';
|
||||
@@ -52,6 +48,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;
|
||||
@@ -65,7 +62,7 @@ export class GraphQLService {
|
||||
this.knex = options?.knex || database;
|
||||
this.fieldsService = new FieldsService(options);
|
||||
this.collectionsService = new CollectionsService(options);
|
||||
this.relationsService = new RelationsService({ knex: this.knex });
|
||||
this.relationsService = new RelationsService(options);
|
||||
}
|
||||
|
||||
args = {
|
||||
@@ -138,6 +135,7 @@ export class GraphQLService {
|
||||
const relatedIsSystem = relationForField.one_collection!.startsWith(
|
||||
'directus_'
|
||||
);
|
||||
|
||||
const relatedType = relatedIsSystem
|
||||
? schema[relationForField.one_collection!.substring(9)].type
|
||||
: schema.items[relationForField.one_collection!].type;
|
||||
@@ -242,19 +240,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),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -506,15 +521,19 @@ export class GraphQLService {
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
Action,
|
||||
Accountability,
|
||||
PermissionsAction,
|
||||
Item,
|
||||
Item as AnyItem,
|
||||
Query,
|
||||
PrimaryKey,
|
||||
AbstractService,
|
||||
@@ -26,7 +26,7 @@ import getDefaultValue from '../utils/get-default-value';
|
||||
import { InvalidPayloadException } from '../exceptions';
|
||||
import { ForbiddenException } from '../exceptions';
|
||||
|
||||
export class ItemsService implements AbstractService {
|
||||
export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractService {
|
||||
collection: string;
|
||||
knex: Knex;
|
||||
accountability: Accountability | null;
|
||||
@@ -49,10 +49,10 @@ export class ItemsService implements AbstractService {
|
||||
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 primaryKeyField = (await this.schemaInspector.primary(this.collection)) as string;
|
||||
const columns = await this.schemaInspector.columns(this.collection);
|
||||
|
||||
let payloads = clone(toArray(data));
|
||||
let payloads: AnyItem[] = clone(toArray(data));
|
||||
|
||||
const savedPrimaryKeys = await this.knex.transaction(async (trx) => {
|
||||
const payloadService = new PayloadService(this.collection, {
|
||||
@@ -194,7 +194,7 @@ export class ItemsService implements AbstractService {
|
||||
return Array.isArray(data) ? savedPrimaryKeys : savedPrimaryKeys[0];
|
||||
}
|
||||
|
||||
async readByQuery(query: Query): Promise<null | Item | Item[]> {
|
||||
async readByQuery(query: Query): Promise<null | Partial<Item> | Partial<Item>[]> {
|
||||
const authorizationService = new AuthorizationService({
|
||||
accountability: this.accountability,
|
||||
knex: this.knex,
|
||||
@@ -210,22 +210,26 @@ export class ItemsService implements AbstractService {
|
||||
}
|
||||
|
||||
const records = await runAST(ast, { knex: this.knex });
|
||||
return records;
|
||||
return records as Partial<Item> | Partial<Item>[] | null;
|
||||
}
|
||||
|
||||
readByKey(
|
||||
keys: PrimaryKey[],
|
||||
query?: Query,
|
||||
action?: PermissionsAction
|
||||
): Promise<null | Item[]>;
|
||||
readByKey(key: PrimaryKey, query?: Query, action?: PermissionsAction): Promise<null | Item>;
|
||||
): Promise<null | Partial<Item>[]>;
|
||||
readByKey(
|
||||
key: PrimaryKey,
|
||||
query?: Query,
|
||||
action?: PermissionsAction
|
||||
): Promise<null | Partial<Item>>;
|
||||
async readByKey(
|
||||
key: PrimaryKey | PrimaryKey[],
|
||||
query: Query = {},
|
||||
action: PermissionsAction = 'read'
|
||||
): Promise<null | Item | Item[]> {
|
||||
): Promise<null | Partial<Item> | Partial<Item>[]> {
|
||||
query = clone(query);
|
||||
const primaryKeyField = await this.schemaInspector.primary(this.collection);
|
||||
const primaryKeyField = (await this.schemaInspector.primary(this.collection)) as string;
|
||||
const keys = toArray(key);
|
||||
|
||||
if (keys.length === 1) {
|
||||
@@ -253,6 +257,7 @@ export class ItemsService implements AbstractService {
|
||||
accountability: this.accountability,
|
||||
knex: this.knex,
|
||||
});
|
||||
|
||||
ast = await authorizationService.processAST(ast, action);
|
||||
}
|
||||
|
||||
@@ -260,7 +265,7 @@ export class ItemsService implements AbstractService {
|
||||
|
||||
if (result === null) throw new ForbiddenException();
|
||||
|
||||
return result;
|
||||
return result as Partial<Item> | Partial<Item>[] | null;
|
||||
}
|
||||
|
||||
update(data: Partial<Item>, keys: PrimaryKey[]): Promise<PrimaryKey[]>;
|
||||
@@ -270,14 +275,14 @@ export class ItemsService implements AbstractService {
|
||||
data: Partial<Item> | Partial<Item>[],
|
||||
key?: PrimaryKey | PrimaryKey[]
|
||||
): Promise<PrimaryKey | PrimaryKey[]> {
|
||||
const primaryKeyField = await this.schemaInspector.primary(this.collection);
|
||||
const primaryKeyField = (await this.schemaInspector.primary(this.collection)) as string;
|
||||
const columns = await this.schemaInspector.columns(this.collection);
|
||||
|
||||
// Updating one or more items to the same payload
|
||||
if (data && key) {
|
||||
const keys = toArray(key);
|
||||
|
||||
let payload = clone(data);
|
||||
let payload: Partial<AnyItem> | Partial<AnyItem>[] = clone(data);
|
||||
|
||||
const customProcessed = await emitter.emitAsync(
|
||||
`${this.eventScope}.update.before`,
|
||||
@@ -429,7 +434,7 @@ export class ItemsService implements AbstractService {
|
||||
}
|
||||
|
||||
async updateByQuery(data: Partial<Item>, query: Query): Promise<PrimaryKey[]> {
|
||||
const primaryKeyField = await this.schemaInspector.primary(this.collection);
|
||||
const primaryKeyField = (await this.schemaInspector.primary(this.collection)) as string;
|
||||
const readQuery = cloneDeep(query);
|
||||
readQuery.fields = [primaryKeyField];
|
||||
|
||||
@@ -449,7 +454,7 @@ export class ItemsService implements AbstractService {
|
||||
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 = (await this.schemaInspector.primary(this.collection)) as string;
|
||||
const payloads = toArray(data);
|
||||
const primaryKeys: PrimaryKey[] = [];
|
||||
|
||||
@@ -479,7 +484,7 @@ export class ItemsService implements AbstractService {
|
||||
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 = (await this.schemaInspector.primary(this.collection)) as string;
|
||||
|
||||
if (this.accountability && this.accountability.admin !== true) {
|
||||
const authorizationService = new AuthorizationService({
|
||||
@@ -534,7 +539,7 @@ export class ItemsService implements AbstractService {
|
||||
}
|
||||
|
||||
async deleteByQuery(query: Query): Promise<PrimaryKey[]> {
|
||||
const primaryKeyField = await this.schemaInspector.primary(this.collection);
|
||||
const primaryKeyField = (await this.schemaInspector.primary(this.collection)) as string;
|
||||
const readQuery = cloneDeep(query);
|
||||
readQuery.fields = [primaryKeyField];
|
||||
|
||||
@@ -550,28 +555,34 @@ export class ItemsService implements AbstractService {
|
||||
return await this.delete(keys);
|
||||
}
|
||||
|
||||
async readSingleton(query: Query) {
|
||||
async readSingleton(query: Query): Promise<Partial<Item>> {
|
||||
query = clone(query);
|
||||
query.single = true;
|
||||
|
||||
const record = (await this.readByQuery(query)) as Item;
|
||||
const record = (await this.readByQuery(query)) as Partial<Item>;
|
||||
|
||||
if (!record) {
|
||||
const columns = await this.schemaInspector.columnInfo(this.collection);
|
||||
let columns = await this.schemaInspector.columnInfo(this.collection);
|
||||
const defaults: Record<string, any> = {};
|
||||
|
||||
if (query.fields && query.fields.includes('*') === false) {
|
||||
columns = columns.filter((column) => {
|
||||
return query.fields!.includes(column.name);
|
||||
});
|
||||
}
|
||||
|
||||
for (const column of columns) {
|
||||
defaults[column.name] = getDefaultValue(column);
|
||||
}
|
||||
|
||||
return defaults;
|
||||
return defaults as Partial<Item>;
|
||||
}
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
async upsertSingleton(data: Partial<Item>) {
|
||||
const primaryKeyField = await this.schemaInspector.primary(this.collection);
|
||||
const primaryKeyField = (await this.schemaInspector.primary(this.collection)) as string;
|
||||
|
||||
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) {
|
||||
applyFilter(dbQuery, query.filter, collection);
|
||||
await applyFilter(this.knex, dbQuery, query.filter, collection);
|
||||
}
|
||||
|
||||
const records = await dbQuery;
|
||||
|
||||
@@ -17,6 +17,9 @@ import getLocalType from '../utils/get-local-type';
|
||||
import { format, formatISO } from 'date-fns';
|
||||
import { ForbiddenException } from '../exceptions';
|
||||
import { toArray } from '../utils/to-array';
|
||||
import { FieldMeta } from '../types';
|
||||
import { systemFieldRows } from '../database/system-data/fields';
|
||||
import { systemRelationRows } from '../database/system-data/relations';
|
||||
|
||||
type Action = 'create' | 'read' | 'update';
|
||||
|
||||
@@ -148,17 +151,21 @@ export class PayloadService {
|
||||
|
||||
const fieldsInPayload = Object.keys(processedPayload[0]);
|
||||
|
||||
const specialFieldsQuery = this.knex
|
||||
let specialFieldsInCollection: FieldMeta[] = await this.knex
|
||||
.select('field', 'special')
|
||||
.from('directus_fields')
|
||||
.where({ collection: this.collection })
|
||||
.whereNotNull('special');
|
||||
|
||||
if (action === 'read') {
|
||||
specialFieldsQuery.whereIn('field', fieldsInPayload);
|
||||
}
|
||||
specialFieldsInCollection.push(
|
||||
...systemFieldRows.filter((fieldMeta) => fieldMeta.collection === this.collection)
|
||||
);
|
||||
|
||||
const specialFieldsInCollection = await specialFieldsQuery;
|
||||
if (action === 'read') {
|
||||
specialFieldsInCollection = specialFieldsInCollection.filter((fieldMeta) => {
|
||||
return fieldsInPayload.includes(fieldMeta.field);
|
||||
});
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
processedPayload.map(async (record: any) => {
|
||||
@@ -203,13 +210,13 @@ export class PayloadService {
|
||||
}
|
||||
|
||||
async processField(
|
||||
field: { field: string; special: string },
|
||||
field: FieldMeta,
|
||||
payload: Partial<Item>,
|
||||
action: Action,
|
||||
accountability: Accountability | null
|
||||
) {
|
||||
if (!field.special) return payload[field.field];
|
||||
const fieldSpecials = field.special.split(',').map((s) => s.trim());
|
||||
const fieldSpecials = field.special ? toArray(field.special) : [];
|
||||
|
||||
let value = clone(payload[field.field]);
|
||||
|
||||
@@ -284,10 +291,15 @@ export class PayloadService {
|
||||
async processM2O(
|
||||
payload: Partial<Item> | Partial<Item>[]
|
||||
): Promise<Partial<Item> | Partial<Item>[]> {
|
||||
const relations = await this.knex
|
||||
.select<Relation[]>('*')
|
||||
.from('directus_relations')
|
||||
.where({ many_collection: this.collection });
|
||||
const relations = [
|
||||
...(await this.knex
|
||||
.select<Relation[]>('*')
|
||||
.from('directus_relations')
|
||||
.where({ many_collection: this.collection })),
|
||||
...systemRelationRows.filter(
|
||||
(systemRelation) => systemRelation.many_collection === this.collection
|
||||
),
|
||||
];
|
||||
|
||||
const payloads = clone(Array.isArray(payload) ? payload : [payload]);
|
||||
|
||||
@@ -313,6 +325,8 @@ export class PayloadService {
|
||||
const relatedRecord: Partial<Item> = payload[relation.many_field];
|
||||
const hasPrimaryKey = relatedRecord.hasOwnProperty(relation.one_primary);
|
||||
|
||||
if (['string', 'number'].includes(typeof relatedRecord)) continue;
|
||||
|
||||
let relatedPrimaryKey: PrimaryKey = relatedRecord[relation.one_primary];
|
||||
const exists = hasPrimaryKey && !!(await itemsService.readByKey(relatedPrimaryKey));
|
||||
|
||||
@@ -334,10 +348,15 @@ export class PayloadService {
|
||||
* Recursively save/update all nested related o2m items
|
||||
*/
|
||||
async processO2M(payload: Partial<Item> | Partial<Item>[], parent?: PrimaryKey) {
|
||||
const relations = await this.knex
|
||||
.select<Relation[]>('*')
|
||||
.from('directus_relations')
|
||||
.where({ one_collection: this.collection });
|
||||
const relations = [
|
||||
...(await this.knex
|
||||
.select<Relation[]>('*')
|
||||
.from('directus_relations')
|
||||
.where({ one_collection: this.collection })),
|
||||
...systemRelationRows.filter(
|
||||
(systemRelation) => systemRelation.one_collection === this.collection
|
||||
),
|
||||
];
|
||||
|
||||
const payloads = clone(toArray(payload));
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AbstractServiceOptions, PermissionsAction, Item, PrimaryKey } from '../types';
|
||||
import { AbstractServiceOptions, PermissionsAction } from '../types';
|
||||
import { ItemsService } from '../services/items';
|
||||
|
||||
export class PermissionsService extends ItemsService {
|
||||
|
||||
@@ -3,6 +3,8 @@ import { AbstractServiceOptions, Query, PrimaryKey, PermissionsAction, Relation
|
||||
import { PermissionsService } from './permissions';
|
||||
import { toArray } from '../utils/to-array';
|
||||
|
||||
import { systemRelationRows } from '../database/system-data/relations';
|
||||
|
||||
/**
|
||||
* @TODO update foreign key constraints when relations are updated
|
||||
*/
|
||||
@@ -25,7 +27,13 @@ export class RelationsService extends ItemsService {
|
||||
| ParsedRelation
|
||||
| ParsedRelation[]
|
||||
| null;
|
||||
|
||||
if (results && Array.isArray(results)) {
|
||||
results.push(...(systemRelationRows as ParsedRelation[]));
|
||||
}
|
||||
|
||||
const filteredResults = await this.filterForbidden(results);
|
||||
|
||||
return filteredResults;
|
||||
}
|
||||
|
||||
@@ -46,6 +54,9 @@ export class RelationsService extends ItemsService {
|
||||
| ParsedRelation[]
|
||||
| null;
|
||||
|
||||
// No need to merge system relations here. They don't have PKs so can never be directly
|
||||
// targetted
|
||||
|
||||
const filteredResults = await this.filterForbidden(results);
|
||||
return filteredResults;
|
||||
}
|
||||
@@ -58,6 +69,7 @@ export class RelationsService extends ItemsService {
|
||||
this.accountability?.role || null,
|
||||
'read'
|
||||
);
|
||||
|
||||
const allowedFields = await this.permissionsService.getAllowedFields(
|
||||
this.accountability?.role || null,
|
||||
'read'
|
||||
|
||||
@@ -2,45 +2,62 @@ import { AbstractServiceOptions, Accountability } from '../types';
|
||||
import Knex from 'knex';
|
||||
import database from '../database';
|
||||
import os from 'os';
|
||||
import { ForbiddenException } from '../exceptions';
|
||||
// @ts-ignore
|
||||
import { version } from '../../package.json';
|
||||
import macosRelease from 'macos-release';
|
||||
import { SettingsService } from './settings';
|
||||
|
||||
export class ServerService {
|
||||
knex: Knex;
|
||||
accountability: Accountability | null;
|
||||
settingsService: SettingsService;
|
||||
|
||||
constructor(options?: AbstractServiceOptions) {
|
||||
this.knex = options?.knex || database;
|
||||
this.accountability = options?.accountability || null;
|
||||
this.settingsService = new SettingsService({ knex: this.knex });
|
||||
}
|
||||
|
||||
serverInfo() {
|
||||
if (this.accountability?.admin !== true) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
async serverInfo() {
|
||||
const info: Record<string, any> = {};
|
||||
|
||||
const osType = os.type() === 'Darwin' ? 'macOS' : os.type();
|
||||
const osVersion =
|
||||
osType === 'macOS'
|
||||
? `${macosRelease().name} (${macosRelease().version})`
|
||||
: os.release();
|
||||
const projectInfo = await this.settingsService.readSingleton({
|
||||
fields: [
|
||||
'project_name',
|
||||
'project_logo',
|
||||
'project_color',
|
||||
'public_foreground',
|
||||
'public_background',
|
||||
'public_note',
|
||||
'custom_css',
|
||||
],
|
||||
});
|
||||
|
||||
return {
|
||||
directus: {
|
||||
info.project = projectInfo;
|
||||
|
||||
if (this.accountability?.admin === true) {
|
||||
const osType = os.type() === 'Darwin' ? 'macOS' : os.type();
|
||||
|
||||
const osVersion =
|
||||
osType === 'macOS'
|
||||
? `${macosRelease().name} (${macosRelease().version})`
|
||||
: os.release();
|
||||
|
||||
info.directus = {
|
||||
version,
|
||||
},
|
||||
node: {
|
||||
};
|
||||
info.node = {
|
||||
version: process.versions.node,
|
||||
uptime: Math.round(process.uptime()),
|
||||
},
|
||||
os: {
|
||||
};
|
||||
info.os = {
|
||||
type: osType,
|
||||
version: osVersion,
|
||||
uptime: Math.round(os.uptime()),
|
||||
totalmem: os.totalmem(),
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,8 +71,9 @@ export class UsersService extends ItemsService {
|
||||
|
||||
const otherAdminUsersCount = +(otherAdminUsers?.count || 0);
|
||||
|
||||
if (otherAdminUsersCount === 0)
|
||||
if (otherAdminUsersCount === 0) {
|
||||
throw new UnprocessableEntityException(`You can't delete the last admin user.`);
|
||||
}
|
||||
|
||||
await super.delete(keys as any);
|
||||
|
||||
@@ -123,7 +124,7 @@ export class UsersService extends ItemsService {
|
||||
if (!user) throw new ForbiddenException();
|
||||
|
||||
const payload = { email, scope: 'password-reset' };
|
||||
const token = jwt.sign(payload, env.SECRET as string, { expiresIn: '7d' });
|
||||
const token = jwt.sign(payload, env.SECRET as string, { expiresIn: '1d' });
|
||||
const acceptURL = env.PUBLIC_URL + '/admin/reset-password?token=' + token;
|
||||
|
||||
await sendPasswordResetMail(email, acceptURL);
|
||||
|
||||
@@ -3,6 +3,7 @@ import database from '../database';
|
||||
import Knex from 'knex';
|
||||
import { InvalidPayloadException, ForbiddenException } from '../exceptions';
|
||||
import SchemaInspector from 'knex-schema-inspector';
|
||||
import { systemCollectionRows } from '../database/system-data/collections';
|
||||
|
||||
export class UtilsService {
|
||||
knex: Knex;
|
||||
@@ -16,11 +17,12 @@ export class UtilsService {
|
||||
async sort(collection: string, { item, to }: { item: PrimaryKey; to: PrimaryKey }) {
|
||||
const schemaInspector = SchemaInspector(this.knex);
|
||||
|
||||
const sortFieldResponse = await this.knex
|
||||
.select('sort_field')
|
||||
.from('directus_collections')
|
||||
.where({ collection })
|
||||
.first();
|
||||
const sortFieldResponse =
|
||||
(await this.knex
|
||||
.select('sort_field')
|
||||
.from('directus_collections')
|
||||
.where({ collection })
|
||||
.first()) || systemCollectionRows;
|
||||
|
||||
const sortField = sortFieldResponse?.sort_field;
|
||||
|
||||
@@ -52,7 +54,7 @@ export class UtilsService {
|
||||
}
|
||||
}
|
||||
|
||||
const primaryKeyField = await schemaInspector.primary(collection);
|
||||
const primaryKeyField = (await schemaInspector.primary(collection)) as string;
|
||||
|
||||
// Make sure all rows have a sort value
|
||||
const countResponse = await this.knex
|
||||
|
||||
@@ -9,12 +9,9 @@ if (require.main === module) {
|
||||
|
||||
export default async function start() {
|
||||
const createServer = require('./server').default;
|
||||
const { validateDBConnection } = require('./database');
|
||||
|
||||
const server = await createServer();
|
||||
|
||||
await validateDBConnection();
|
||||
|
||||
await emitter.emitAsync('server.start.before', { server });
|
||||
|
||||
const port = env.PORT;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user