mirror of
https://github.com/directus/directus.git
synced 2026-01-30 23:08:15 -05:00
Merge branch 'main' into aggregation
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "directus",
|
||||
"version": "9.0.0-rc.76",
|
||||
"version": "9.0.0-rc.80",
|
||||
"license": "GPL-3.0-only",
|
||||
"homepage": "https://github.com/directus/directus#readme",
|
||||
"description": "Directus is a real-time API and App dashboard for managing SQL database content.",
|
||||
@@ -59,6 +59,9 @@
|
||||
"cli": "cross-env DIRECTUS_DEV=true NODE_ENV=development ts-node --script-mode --transpile-only src/cli/index.ts",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"LICENSE",
|
||||
@@ -66,15 +69,18 @@
|
||||
"example.env"
|
||||
],
|
||||
"dependencies": {
|
||||
"@directus/app": "9.0.0-rc.76",
|
||||
"@directus/drive": "9.0.0-rc.76",
|
||||
"@directus/drive-azure": "9.0.0-rc.76",
|
||||
"@directus/drive-gcs": "9.0.0-rc.76",
|
||||
"@directus/drive-s3": "9.0.0-rc.76",
|
||||
"@directus/format-title": "9.0.0-rc.76",
|
||||
"@directus/schema": "9.0.0-rc.76",
|
||||
"@directus/specs": "9.0.0-rc.76",
|
||||
"@directus/app": "9.0.0-rc.80",
|
||||
"@directus/drive": "9.0.0-rc.80",
|
||||
"@directus/drive-azure": "9.0.0-rc.80",
|
||||
"@directus/drive-gcs": "9.0.0-rc.80",
|
||||
"@directus/drive-s3": "9.0.0-rc.80",
|
||||
"@directus/format-title": "9.0.0-rc.80",
|
||||
"@directus/schema": "9.0.0-rc.80",
|
||||
"@directus/shared": "9.0.0-rc.80",
|
||||
"@directus/specs": "9.0.0-rc.80",
|
||||
"@godaddy/terminus": "^4.9.0",
|
||||
"@rollup/plugin-alias": "^3.1.2",
|
||||
"@rollup/plugin-virtual": "^2.0.3",
|
||||
"argon2": "^0.28.1",
|
||||
"async": "^3.2.0",
|
||||
"async-mutex": "^0.3.1",
|
||||
@@ -129,6 +135,7 @@
|
||||
"qs": "^6.9.4",
|
||||
"rate-limiter-flexible": "^2.2.2",
|
||||
"resolve-cwd": "^3.0.0",
|
||||
"rollup": "^2.52.1",
|
||||
"sharp": "^0.28.3",
|
||||
"stream-json": "^1.7.1",
|
||||
"uuid": "^8.3.2",
|
||||
@@ -162,7 +169,7 @@
|
||||
"@types/express-pino-logger": "4.0.2",
|
||||
"@types/express-session": "1.17.3",
|
||||
"@types/fs-extra": "9.0.11",
|
||||
"@types/inquirer": "7.3.1",
|
||||
"@types/inquirer": "7.3.2",
|
||||
"@types/js-yaml": "4.0.1",
|
||||
"@types/json2csv": "5.0.2",
|
||||
"@types/jsonwebtoken": "8.5.2",
|
||||
|
||||
@@ -56,7 +56,7 @@ export default async function createApp(): Promise<express.Application> {
|
||||
|
||||
await initializeExtensions();
|
||||
|
||||
await registerExtensionHooks();
|
||||
registerExtensionHooks();
|
||||
|
||||
const app = express();
|
||||
|
||||
@@ -170,7 +170,7 @@ export default async function createApp(): Promise<express.Application> {
|
||||
|
||||
// Register custom hooks / endpoints
|
||||
await emitAsyncSafe('routes.custom.init.before', { app });
|
||||
await registerExtensionEndpoints(customRouter);
|
||||
registerExtensionEndpoints(customRouter);
|
||||
await emitAsyncSafe('routes.custom.init.after', { app });
|
||||
|
||||
app.use(notFoundHandler);
|
||||
|
||||
@@ -6,31 +6,39 @@ import { getConfigFromEnv } from './utils/get-config-from-env';
|
||||
import { validateEnv } from './utils/validate-env';
|
||||
|
||||
let cache: Keyv | null = null;
|
||||
let schemaCache: Keyv | null = null;
|
||||
|
||||
if (env.CACHE_ENABLED === true) {
|
||||
validateEnv(['CACHE_NAMESPACE', 'CACHE_TTL', 'CACHE_STORE']);
|
||||
cache = getKeyvInstance();
|
||||
cache.on('error', (err) => logger.error(err));
|
||||
export function getCache(): { cache: Keyv | null; schemaCache: Keyv | null } {
|
||||
if (env.CACHE_ENABLED === true && cache === null) {
|
||||
validateEnv(['CACHE_NAMESPACE', 'CACHE_TTL', 'CACHE_STORE']);
|
||||
cache = getKeyvInstance(ms(env.CACHE_TTL as string));
|
||||
cache.on('error', (err) => logger.error(err));
|
||||
}
|
||||
|
||||
if (env.CACHE_SCHEMA !== false && schemaCache === null) {
|
||||
schemaCache = getKeyvInstance(typeof env.CACHE_SCHEMA === 'string' ? ms(env.CACHE_SCHEMA) : undefined);
|
||||
schemaCache.on('error', (err) => logger.error(err));
|
||||
}
|
||||
|
||||
return { cache, schemaCache };
|
||||
}
|
||||
|
||||
export default cache;
|
||||
|
||||
function getKeyvInstance() {
|
||||
function getKeyvInstance(ttl: number | undefined): Keyv {
|
||||
switch (env.CACHE_STORE) {
|
||||
case 'redis':
|
||||
return new Keyv(getConfig('redis'));
|
||||
return new Keyv(getConfig('redis', ttl));
|
||||
case 'memcache':
|
||||
return new Keyv(getConfig('memcache'));
|
||||
return new Keyv(getConfig('memcache', ttl));
|
||||
case 'memory':
|
||||
default:
|
||||
return new Keyv(getConfig());
|
||||
return new Keyv(getConfig('memory', ttl));
|
||||
}
|
||||
}
|
||||
|
||||
function getConfig(store: 'memory' | 'redis' | 'memcache' = 'memory'): Options<any> {
|
||||
function getConfig(store: 'memory' | 'redis' | 'memcache' = 'memory', ttl: number | undefined): Options<any> {
|
||||
const config: Options<any> = {
|
||||
namespace: env.CACHE_NAMESPACE,
|
||||
ttl: ms(env.CACHE_TTL as string),
|
||||
ttl,
|
||||
};
|
||||
|
||||
if (store === 'redis') {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Transformation } from './types/assets';
|
||||
import { Transformation } from './types';
|
||||
|
||||
export const SYSTEM_ASSET_ALLOW_LIST: Transformation[] = [
|
||||
{
|
||||
|
||||
@@ -1,28 +1,24 @@
|
||||
import express, { Router } from 'express';
|
||||
import env from '../env';
|
||||
import { RouteNotFoundException } from '../exceptions';
|
||||
import { listExtensions } from '../extensions';
|
||||
import { respond } from '../middleware/respond';
|
||||
import { Router } from 'express';
|
||||
import asyncHandler from '../utils/async-handler';
|
||||
import { RouteNotFoundException } from '../exceptions';
|
||||
import { listExtensions, getAppExtensionSource } from '../extensions';
|
||||
import { respond } from '../middleware/respond';
|
||||
import { depluralize } from '@directus/shared/utils';
|
||||
import { AppExtensionType, Plural } from '@directus/shared/types';
|
||||
import { APP_EXTENSION_TYPES } from '@directus/shared/constants';
|
||||
|
||||
const router = Router();
|
||||
|
||||
const extensionsPath = env.EXTENSIONS_PATH as string;
|
||||
|
||||
const appExtensions = ['interfaces', 'layouts', 'displays', 'modules'];
|
||||
|
||||
router.get(
|
||||
['/:type', '/:type/*'],
|
||||
'/:type',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
if (appExtensions.includes(req.params.type) === false) {
|
||||
const type = depluralize(req.params.type as Plural<AppExtensionType>);
|
||||
|
||||
if (APP_EXTENSION_TYPES.includes(type) === false) {
|
||||
throw new RouteNotFoundException(req.path);
|
||||
}
|
||||
|
||||
return next();
|
||||
}),
|
||||
express.static(extensionsPath),
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const extensions = await listExtensions(req.params.type);
|
||||
const extensions = listExtensions(type);
|
||||
|
||||
res.locals.payload = {
|
||||
data: extensions,
|
||||
@@ -33,4 +29,23 @@ router.get(
|
||||
respond
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:type/index.js',
|
||||
asyncHandler(async (req, res) => {
|
||||
const type = depluralize(req.params.type as Plural<AppExtensionType>);
|
||||
|
||||
if (APP_EXTENSION_TYPES.includes(type) === false) {
|
||||
throw new RouteNotFoundException(req.path);
|
||||
}
|
||||
|
||||
const extensionSource = getAppExtensionSource(type);
|
||||
if (extensionSource === undefined) {
|
||||
throw new RouteNotFoundException(req.path);
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'application/javascript; charset=UTF-8');
|
||||
res.end(extensionSource);
|
||||
})
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -51,6 +51,7 @@ const defaults: Record<string, any> = {
|
||||
CACHE_NAMESPACE: 'system-cache',
|
||||
CACHE_AUTO_PURGE: false,
|
||||
CACHE_CONTROL_S_MAXAGE: '0',
|
||||
CACHE_SCHEMA: true,
|
||||
|
||||
OAUTH_PROVIDERS: '',
|
||||
|
||||
|
||||
@@ -1,91 +1,139 @@
|
||||
import express, { Router } from 'express';
|
||||
import { ensureDir } from 'fs-extra';
|
||||
import path from 'path';
|
||||
import { AppExtensionType, Extension, ExtensionType } from '@directus/shared/types';
|
||||
import {
|
||||
generateExtensionsEntry,
|
||||
getLocalExtensions,
|
||||
getPackageExtensions,
|
||||
pluralize,
|
||||
resolvePackage,
|
||||
} from '@directus/shared/utils';
|
||||
import { APP_EXTENSION_TYPES, EXTENSION_TYPES, SHARED_DEPS } from '@directus/shared/constants';
|
||||
import getDatabase from './database';
|
||||
import emitter from './emitter';
|
||||
import env from './env';
|
||||
import * as exceptions from './exceptions';
|
||||
import { ServiceUnavailableException } from './exceptions';
|
||||
import logger from './logger';
|
||||
import * as services from './services';
|
||||
import { EndpointRegisterFunction, HookRegisterFunction } from './types';
|
||||
import { HookRegisterFunction, EndpointRegisterFunction } from './types';
|
||||
import fse from 'fs-extra';
|
||||
import { getSchema } from './utils/get-schema';
|
||||
import listFolders from './utils/list-folders';
|
||||
|
||||
import * as services from './services';
|
||||
import { schedule, validate } from 'node-cron';
|
||||
import { REGEX_BETWEEN_PARENS } from './constants';
|
||||
import { rollup } from 'rollup';
|
||||
// @TODO Remove this once a new version of @rollup/plugin-virtual has been released
|
||||
// @ts-expect-error
|
||||
import virtual from '@rollup/plugin-virtual';
|
||||
import alias from '@rollup/plugin-alias';
|
||||
|
||||
export async function ensureFoldersExist(): Promise<void> {
|
||||
const folders = ['endpoints', 'hooks', 'interfaces', 'modules', 'layouts', 'displays'];
|
||||
let extensions: Extension[] = [];
|
||||
let extensionBundles: Partial<Record<AppExtensionType, string>> = {};
|
||||
|
||||
for (const folder of folders) {
|
||||
const folderPath = path.resolve(env.EXTENSIONS_PATH, folder);
|
||||
export async function initializeExtensions(): Promise<void> {
|
||||
await ensureDirsExist();
|
||||
extensions = await getExtensions();
|
||||
extensionBundles = await generateExtensionBundles();
|
||||
|
||||
logger.info(`Loaded extensions: ${listExtensions().join(', ')}`);
|
||||
}
|
||||
|
||||
export function listExtensions(type?: ExtensionType): string[] {
|
||||
if (type === undefined) {
|
||||
return extensions.map((extension) => extension.name);
|
||||
} else {
|
||||
return extensions.filter((extension) => extension.type === type).map((extension) => extension.name);
|
||||
}
|
||||
}
|
||||
|
||||
export function getAppExtensionSource(type: AppExtensionType): string | undefined {
|
||||
return extensionBundles[type];
|
||||
}
|
||||
|
||||
export function registerExtensionEndpoints(router: Router): void {
|
||||
const endpoints = extensions.filter((extension) => extension.type === 'endpoint');
|
||||
registerEndpoints(endpoints, router);
|
||||
}
|
||||
|
||||
export function registerExtensionHooks(): void {
|
||||
const hooks = extensions.filter((extension) => extension.type === 'hook');
|
||||
registerHooks(hooks);
|
||||
}
|
||||
|
||||
async function getExtensions(): Promise<Extension[]> {
|
||||
const packageExtensions = await getPackageExtensions('.');
|
||||
const localExtensions = await getLocalExtensions(env.EXTENSIONS_PATH);
|
||||
|
||||
return [...packageExtensions, ...localExtensions];
|
||||
}
|
||||
|
||||
async function generateExtensionBundles() {
|
||||
const sharedDepsMapping = await getSharedDepsMapping(SHARED_DEPS);
|
||||
const internalImports = Object.entries(sharedDepsMapping).map(([name, path]) => ({
|
||||
find: name,
|
||||
replacement: path,
|
||||
}));
|
||||
|
||||
const bundles: Partial<Record<AppExtensionType, string>> = {};
|
||||
|
||||
for (const extensionType of APP_EXTENSION_TYPES) {
|
||||
const entry = generateExtensionsEntry(extensionType, extensions);
|
||||
|
||||
const bundle = await rollup({
|
||||
input: 'entry',
|
||||
external: SHARED_DEPS,
|
||||
plugins: [virtual({ entry }), alias({ entries: internalImports })],
|
||||
});
|
||||
const { output } = await bundle.generate({ format: 'es' });
|
||||
|
||||
bundles[extensionType] = output[0].code;
|
||||
|
||||
await bundle.close();
|
||||
}
|
||||
|
||||
return bundles;
|
||||
}
|
||||
|
||||
async function ensureDirsExist() {
|
||||
for (const extensionType of EXTENSION_TYPES) {
|
||||
const dirPath = path.resolve(env.EXTENSIONS_PATH, pluralize(extensionType));
|
||||
try {
|
||||
await ensureDir(folderPath);
|
||||
await fse.ensureDir(dirPath);
|
||||
} catch (err) {
|
||||
logger.warn(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function initializeExtensions(): Promise<void> {
|
||||
await ensureFoldersExist();
|
||||
}
|
||||
async function getSharedDepsMapping(deps: string[]) {
|
||||
const appDir = await fse.readdir(path.join(resolvePackage('@directus/app'), 'dist'));
|
||||
|
||||
export async function listExtensions(type: string): Promise<string[]> {
|
||||
const extensionsPath = env.EXTENSIONS_PATH as string;
|
||||
const location = path.join(extensionsPath, type);
|
||||
const depsMapping: Record<string, string> = {};
|
||||
for (const dep of deps) {
|
||||
const depName = appDir.find((file) => dep.replace(/\//g, '_') === file.substring(0, file.indexOf('.')));
|
||||
|
||||
try {
|
||||
return await listFolders(location);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
throw new ServiceUnavailableException(`Extension folder "extensions/${type}" couldn't be opened`, {
|
||||
service: 'extensions',
|
||||
});
|
||||
if (depName) {
|
||||
depsMapping[dep] = `${env.PUBLIC_URL}/admin/${depName}`;
|
||||
} else {
|
||||
logger.warn(`Couldn't find extension internal dependency "${dep}"`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
return depsMapping;
|
||||
}
|
||||
|
||||
export async function registerExtensions(router: Router): Promise<void> {
|
||||
await registerExtensionHooks();
|
||||
await registerExtensionEndpoints(router);
|
||||
}
|
||||
|
||||
export async function registerExtensionEndpoints(router: Router): Promise<void> {
|
||||
let endpoints: string[] = [];
|
||||
try {
|
||||
endpoints = await listExtensions('endpoints');
|
||||
registerEndpoints(endpoints, router);
|
||||
} catch (err) {
|
||||
logger.warn(err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function registerExtensionHooks(): Promise<void> {
|
||||
let hooks: string[] = [];
|
||||
try {
|
||||
hooks = await listExtensions('hooks');
|
||||
registerHooks(hooks);
|
||||
} catch (err) {
|
||||
logger.warn(err);
|
||||
}
|
||||
}
|
||||
|
||||
function registerHooks(hooks: string[]) {
|
||||
const extensionsPath = env.EXTENSIONS_PATH as string;
|
||||
|
||||
function registerHooks(hooks: Extension[]) {
|
||||
for (const hook of hooks) {
|
||||
try {
|
||||
registerHook(hook);
|
||||
} catch (error) {
|
||||
logger.warn(`Couldn't register hook "${hook}"`);
|
||||
logger.warn(`Couldn't register hook "${hook.name}"`);
|
||||
logger.warn(error);
|
||||
}
|
||||
}
|
||||
|
||||
function registerHook(hook: string) {
|
||||
const hookPath = path.resolve(extensionsPath, 'hooks', hook, 'index.js');
|
||||
function registerHook(hook: Extension) {
|
||||
const hookPath = path.resolve(hook.path, hook.entrypoint || '');
|
||||
const hookInstance: HookRegisterFunction | { default?: HookRegisterFunction } = require(hookPath);
|
||||
|
||||
let register: HookRegisterFunction = hookInstance as HookRegisterFunction;
|
||||
@@ -113,20 +161,18 @@ function registerHooks(hooks: string[]) {
|
||||
}
|
||||
}
|
||||
|
||||
function registerEndpoints(endpoints: string[], router: Router) {
|
||||
const extensionsPath = env.EXTENSIONS_PATH as string;
|
||||
|
||||
function registerEndpoints(endpoints: Extension[], router: Router) {
|
||||
for (const endpoint of endpoints) {
|
||||
try {
|
||||
registerEndpoint(endpoint);
|
||||
} catch (error) {
|
||||
logger.warn(`Couldn't register endpoint "${endpoint}"`);
|
||||
logger.warn(`Couldn't register endpoint "${endpoint.name}"`);
|
||||
logger.warn(error);
|
||||
}
|
||||
}
|
||||
|
||||
function registerEndpoint(endpoint: string) {
|
||||
const endpointPath = path.resolve(extensionsPath, 'endpoints', endpoint, 'index.js');
|
||||
function registerEndpoint(endpoint: Extension) {
|
||||
const endpointPath = path.resolve(endpoint.path, endpoint.entrypoint || '');
|
||||
const endpointInstance: EndpointRegisterFunction | { default?: EndpointRegisterFunction } = require(endpointPath);
|
||||
|
||||
let register: EndpointRegisterFunction = endpointInstance as EndpointRegisterFunction;
|
||||
@@ -137,7 +183,7 @@ function registerEndpoints(endpoints: string[], router: Router) {
|
||||
}
|
||||
|
||||
const scopedRouter = express.Router();
|
||||
router.use(`/${endpoint}/`, scopedRouter);
|
||||
router.use(`/${endpoint.name}/`, scopedRouter);
|
||||
|
||||
register(scopedRouter, { services, exceptions, env, database: getDatabase(), getSchema });
|
||||
}
|
||||
|
||||
@@ -78,10 +78,6 @@ const authenticate: RequestHandler = asyncHandler(async (req, res, next) => {
|
||||
req.accountability.app = user.app_access === true || user.app_access == 1;
|
||||
}
|
||||
|
||||
if (req.accountability?.user) {
|
||||
await database('directus_users').update({ last_access: new Date() }).where({ id: req.accountability.user });
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { RequestHandler } from 'express';
|
||||
import cache from '../cache';
|
||||
import { getCache } from '../cache';
|
||||
import env from '../env';
|
||||
import asyncHandler from '../utils/async-handler';
|
||||
import { getCacheControlHeader } from '../utils/get-cache-headers';
|
||||
import { getCacheKey } from '../utils/get-cache-key';
|
||||
|
||||
const checkCacheMiddleware: RequestHandler = asyncHandler(async (req, res, next) => {
|
||||
const { cache } = getCache();
|
||||
|
||||
if (req.method.toLowerCase() !== 'get') return next();
|
||||
if (env.CACHE_ENABLED !== true) return next();
|
||||
if (!cache) return next();
|
||||
|
||||
@@ -2,7 +2,7 @@ import { RequestHandler } from 'express';
|
||||
import { Transform, transforms } from 'json2csv';
|
||||
import ms from 'ms';
|
||||
import { PassThrough } from 'stream';
|
||||
import cache from '../cache';
|
||||
import { getCache } from '../cache';
|
||||
import env from '../env';
|
||||
import asyncHandler from '../utils/async-handler';
|
||||
import { getCacheKey } from '../utils/get-cache-key';
|
||||
@@ -10,6 +10,8 @@ import { parse as toXML } from 'js2xmlparser';
|
||||
import { getCacheControlHeader } from '../utils/get-cache-headers';
|
||||
|
||||
export const respond: RequestHandler = asyncHandler(async (req, res) => {
|
||||
const { cache } = getCache();
|
||||
|
||||
if (
|
||||
req.method.toLowerCase() === 'get' &&
|
||||
env.CACHE_ENABLED === true &&
|
||||
|
||||
@@ -185,6 +185,8 @@ export class AuthenticationService {
|
||||
});
|
||||
}
|
||||
|
||||
await this.knex('directus_users').update({ last_access: new Date() }).where({ id: user.id });
|
||||
|
||||
emitStatus('success');
|
||||
|
||||
if (allowedAttempts !== null) {
|
||||
@@ -230,6 +232,8 @@ export class AuthenticationService {
|
||||
.update({ token: newRefreshToken, expires: refreshTokenExpiration })
|
||||
.where({ token: refreshToken });
|
||||
|
||||
await this.knex('directus_users').update({ last_access: new Date() }).where({ id: record.id });
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken: newRefreshToken,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import SchemaInspector from '@directus/schema';
|
||||
import { Knex } from 'knex';
|
||||
import cache from '../cache';
|
||||
import { getCache } from '../cache';
|
||||
import { ALIAS_TYPES } from '../constants';
|
||||
import getDatabase, { getSchemaInspector } from '../database';
|
||||
import { systemCollectionRows } from '../database/system-data/collections';
|
||||
@@ -9,6 +9,7 @@ import { ForbiddenException, InvalidPayloadException } from '../exceptions';
|
||||
import logger from '../logger';
|
||||
import { FieldsService, RawField } from '../services/fields';
|
||||
import { ItemsService, MutationOptions } from '../services/items';
|
||||
import Keyv from 'keyv';
|
||||
import {
|
||||
AbstractServiceOptions,
|
||||
Accountability,
|
||||
@@ -29,12 +30,18 @@ export class CollectionsService {
|
||||
accountability: Accountability | null;
|
||||
schemaInspector: ReturnType<typeof SchemaInspector>;
|
||||
schema: SchemaOverview;
|
||||
cache: Keyv<any> | null;
|
||||
schemaCache: Keyv<any> | null;
|
||||
|
||||
constructor(options: AbstractServiceOptions) {
|
||||
this.knex = options.knex || getDatabase();
|
||||
this.accountability = options.accountability || null;
|
||||
this.schemaInspector = options.knex ? SchemaInspector(options.knex) : getSchemaInspector();
|
||||
this.schema = options.schema;
|
||||
|
||||
const { cache, schemaCache } = getCache();
|
||||
this.cache = cache;
|
||||
this.schemaCache = schemaCache;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,8 +135,12 @@ export class CollectionsService {
|
||||
return payload.collection;
|
||||
});
|
||||
|
||||
if (cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
|
||||
await cache.clear();
|
||||
if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
|
||||
await this.cache.clear();
|
||||
}
|
||||
|
||||
if (this.schemaCache) {
|
||||
await this.schemaCache.clear();
|
||||
}
|
||||
|
||||
return payload.collection;
|
||||
@@ -156,8 +167,12 @@ export class CollectionsService {
|
||||
return collectionNames;
|
||||
});
|
||||
|
||||
if (cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
|
||||
await cache.clear();
|
||||
if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
|
||||
await this.cache.clear();
|
||||
}
|
||||
|
||||
if (this.schemaCache) {
|
||||
await this.schemaCache.clear();
|
||||
}
|
||||
|
||||
return collections;
|
||||
@@ -416,8 +431,12 @@ export class CollectionsService {
|
||||
await trx.schema.dropTable(collectionKey);
|
||||
});
|
||||
|
||||
if (cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
|
||||
await cache.clear();
|
||||
if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
|
||||
await this.cache.clear();
|
||||
}
|
||||
|
||||
if (this.schemaCache) {
|
||||
await this.schemaCache.clear();
|
||||
}
|
||||
|
||||
return collectionKey;
|
||||
@@ -443,8 +462,12 @@ export class CollectionsService {
|
||||
}
|
||||
});
|
||||
|
||||
if (cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
|
||||
await cache.clear();
|
||||
if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
|
||||
await this.cache.clear();
|
||||
}
|
||||
|
||||
if (this.schemaCache) {
|
||||
await this.schemaCache.clear();
|
||||
}
|
||||
|
||||
return collectionKeys;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import SchemaInspector from '@directus/schema';
|
||||
import { Knex } from 'knex';
|
||||
import { Column } from 'knex-schema-inspector/dist/types/column';
|
||||
import cache from '../cache';
|
||||
import { getCache } from '../cache';
|
||||
import { ALIAS_TYPES } from '../constants';
|
||||
import getDatabase, { getSchemaInspector } from '../database';
|
||||
import { systemFieldRows } from '../database/system-data/fields/';
|
||||
@@ -18,6 +18,7 @@ import getLocalType from '../utils/get-local-type';
|
||||
import { toArray } from '../utils/to-array';
|
||||
import { isEqual } from 'lodash';
|
||||
import { RelationsService } from './relations';
|
||||
import Keyv from 'keyv';
|
||||
|
||||
export type RawField = DeepPartial<Field> & { field: string; type: typeof types[number] };
|
||||
|
||||
@@ -28,6 +29,8 @@ export class FieldsService {
|
||||
payloadService: PayloadService;
|
||||
schemaInspector: ReturnType<typeof SchemaInspector>;
|
||||
schema: SchemaOverview;
|
||||
cache: Keyv<any> | null;
|
||||
schemaCache: Keyv<any> | null;
|
||||
|
||||
constructor(options: AbstractServiceOptions) {
|
||||
this.knex = options.knex || getDatabase();
|
||||
@@ -36,6 +39,10 @@ export class FieldsService {
|
||||
this.itemsService = new ItemsService('directus_fields', options);
|
||||
this.payloadService = new PayloadService('directus_fields', options);
|
||||
this.schema = options.schema;
|
||||
|
||||
const { cache, schemaCache } = getCache();
|
||||
this.cache = cache;
|
||||
this.schemaCache = schemaCache;
|
||||
}
|
||||
|
||||
private get hasReadAccess() {
|
||||
@@ -244,8 +251,12 @@ export class FieldsService {
|
||||
}
|
||||
});
|
||||
|
||||
if (cache && env.CACHE_AUTO_PURGE) {
|
||||
await cache.clear();
|
||||
if (this.cache && env.CACHE_AUTO_PURGE) {
|
||||
await this.cache.clear();
|
||||
}
|
||||
|
||||
if (this.schemaCache) {
|
||||
await this.schemaCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,8 +302,12 @@ export class FieldsService {
|
||||
}
|
||||
}
|
||||
|
||||
if (cache && env.CACHE_AUTO_PURGE) {
|
||||
await cache.clear();
|
||||
if (this.cache && env.CACHE_AUTO_PURGE) {
|
||||
await this.cache.clear();
|
||||
}
|
||||
|
||||
if (this.schemaCache) {
|
||||
await this.schemaCache.clear();
|
||||
}
|
||||
|
||||
return field.field;
|
||||
@@ -396,8 +411,12 @@ export class FieldsService {
|
||||
}
|
||||
});
|
||||
|
||||
if (cache && env.CACHE_AUTO_PURGE) {
|
||||
await cache.clear();
|
||||
if (this.cache && env.CACHE_AUTO_PURGE) {
|
||||
await this.cache.clear();
|
||||
}
|
||||
|
||||
if (this.schemaCache) {
|
||||
await this.schemaCache.clear();
|
||||
}
|
||||
|
||||
emitAsyncSafe(`fields.delete`, {
|
||||
|
||||
@@ -7,7 +7,6 @@ import { extension } from 'mime-types';
|
||||
import path from 'path';
|
||||
import sharp from 'sharp';
|
||||
import url from 'url';
|
||||
import cache from '../cache';
|
||||
import { emitAsyncSafe } from '../emitter';
|
||||
import env from '../env';
|
||||
import { ForbiddenException, ServiceUnavailableException } from '../exceptions';
|
||||
@@ -121,8 +120,8 @@ export class FilesService extends ItemsService {
|
||||
|
||||
await sudoService.updateOne(primaryKey, payload, { emitEvents: false });
|
||||
|
||||
if (cache && env.CACHE_AUTO_PURGE) {
|
||||
await cache.clear();
|
||||
if (this.cache && env.CACHE_AUTO_PURGE) {
|
||||
await this.cache.clear();
|
||||
}
|
||||
|
||||
emitAsyncSafe(`files.upload`, {
|
||||
@@ -208,8 +207,8 @@ export class FilesService extends ItemsService {
|
||||
}
|
||||
}
|
||||
|
||||
if (cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
|
||||
await cache.clear();
|
||||
if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
|
||||
await this.cache.clear();
|
||||
}
|
||||
|
||||
return keys;
|
||||
|
||||
@@ -416,6 +416,18 @@ export class GraphQLService {
|
||||
_ncontains: {
|
||||
type: GraphQLString,
|
||||
},
|
||||
_starts_with: {
|
||||
type: GraphQLString,
|
||||
},
|
||||
_nstarts_with: {
|
||||
type: GraphQLString,
|
||||
},
|
||||
_ends_with: {
|
||||
type: GraphQLString,
|
||||
},
|
||||
_nends_with: {
|
||||
type: GraphQLString,
|
||||
},
|
||||
_in: {
|
||||
type: new GraphQLList(GraphQLString),
|
||||
},
|
||||
@@ -1265,10 +1277,10 @@ export class GraphQLService {
|
||||
},
|
||||
}),
|
||||
resolve: async () => ({
|
||||
interfaces: await listExtensions('interfaces'),
|
||||
displays: await listExtensions('displays'),
|
||||
layouts: await listExtensions('layouts'),
|
||||
modules: await listExtensions('modules'),
|
||||
interfaces: listExtensions('interface'),
|
||||
displays: listExtensions('display'),
|
||||
layouts: listExtensions('layout'),
|
||||
modules: listExtensions('module'),
|
||||
}),
|
||||
},
|
||||
server_specs_oas: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Knex } from 'knex';
|
||||
import { clone, cloneDeep, merge, pick, without } from 'lodash';
|
||||
import cache from '../cache';
|
||||
import { getCache } from '../cache';
|
||||
import Keyv from 'keyv';
|
||||
import getDatabase from '../database';
|
||||
import runAST from '../database/run-ast';
|
||||
import emitter, { emitAsyncSafe } from '../emitter';
|
||||
@@ -52,6 +53,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
accountability: Accountability | null;
|
||||
eventScope: string;
|
||||
schema: SchemaOverview;
|
||||
cache: Keyv<any> | null;
|
||||
|
||||
constructor(collection: string, options: AbstractServiceOptions) {
|
||||
this.collection = collection;
|
||||
@@ -59,6 +61,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
this.accountability = options.accountability || null;
|
||||
this.eventScope = this.collection.startsWith('directus_') ? this.collection.substring(9) : 'items';
|
||||
this.schema = options.schema;
|
||||
this.cache = getCache().cache;
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -208,8 +211,8 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
});
|
||||
}
|
||||
|
||||
if (cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
|
||||
await cache.clear();
|
||||
if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
|
||||
await this.cache.clear();
|
||||
}
|
||||
|
||||
return primaryKey;
|
||||
@@ -236,8 +239,8 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
return primaryKeys;
|
||||
});
|
||||
|
||||
if (cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
|
||||
await cache.clear();
|
||||
if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
|
||||
await this.cache.clear();
|
||||
}
|
||||
|
||||
return primaryKeys;
|
||||
@@ -524,8 +527,8 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
}
|
||||
});
|
||||
|
||||
if (cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
|
||||
await cache.clear();
|
||||
if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
|
||||
await this.cache.clear();
|
||||
}
|
||||
|
||||
if (opts?.emitEvents !== false) {
|
||||
@@ -589,8 +592,8 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
return primaryKeys;
|
||||
});
|
||||
|
||||
if (cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
|
||||
await cache.clear();
|
||||
if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
|
||||
await this.cache.clear();
|
||||
}
|
||||
|
||||
return primaryKeys;
|
||||
@@ -673,8 +676,8 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
}
|
||||
});
|
||||
|
||||
if (cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
|
||||
await cache.clear();
|
||||
if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) {
|
||||
await this.cache.clear();
|
||||
}
|
||||
|
||||
if (opts?.emitEvents !== false) {
|
||||
@@ -717,6 +720,11 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
}
|
||||
|
||||
for (const [name, field] of fields) {
|
||||
if (this.schema.collections[this.collection].primary === name) {
|
||||
defaults[name] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
defaults[name] = field.defaultValue;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import SchemaInspector from '@directus/schema';
|
||||
import { ForeignKey } from 'knex-schema-inspector/dist/types/foreign-key';
|
||||
import getDatabase, { getSchemaInspector } from '../database';
|
||||
import { getDefaultIndexName } from '../utils/get-default-index-name';
|
||||
import { getCache } from '../cache';
|
||||
import Keyv from 'keyv';
|
||||
|
||||
export class RelationsService {
|
||||
knex: Knex;
|
||||
@@ -17,6 +19,7 @@ export class RelationsService {
|
||||
accountability: Accountability | null;
|
||||
schema: SchemaOverview;
|
||||
relationsItemService: ItemsService<RelationMeta>;
|
||||
schemaCache: Keyv<any> | null;
|
||||
|
||||
constructor(options: AbstractServiceOptions) {
|
||||
this.knex = options.knex || getDatabase();
|
||||
@@ -31,6 +34,8 @@ export class RelationsService {
|
||||
// allowed to extract the relations regardless of permissions to directus_relations. This
|
||||
// happens in `filterForbidden` down below
|
||||
});
|
||||
|
||||
this.schemaCache = getCache().schemaCache;
|
||||
}
|
||||
|
||||
async readAll(collection?: string, opts?: QueryOptions): Promise<Relation[]> {
|
||||
@@ -183,6 +188,10 @@ export class RelationsService {
|
||||
|
||||
await relationsItemService.createOne(metaRow);
|
||||
});
|
||||
|
||||
if (this.schemaCache) {
|
||||
await this.schemaCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -259,6 +268,10 @@ export class RelationsService {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (this.schemaCache) {
|
||||
await this.schemaCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -296,6 +309,10 @@ export class RelationsService {
|
||||
await trx('directus_relations').delete().where({ many_collection: collection, many_field: field });
|
||||
}
|
||||
});
|
||||
|
||||
if (this.schemaCache) {
|
||||
await this.schemaCache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,7 @@ import os from 'os';
|
||||
import { performance } from 'perf_hooks';
|
||||
// @ts-ignore
|
||||
import { version } from '../../package.json';
|
||||
import cache from '../cache';
|
||||
import { getCache } from '../cache';
|
||||
import getDatabase, { hasDatabaseConnection } from '../database';
|
||||
import env from '../env';
|
||||
import logger from '../logger';
|
||||
@@ -189,6 +189,8 @@ export class ServerService {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { cache } = getCache();
|
||||
|
||||
const checks: Record<string, HealthCheck[]> = {
|
||||
'cache:responseTime': [
|
||||
{
|
||||
|
||||
@@ -2,7 +2,6 @@ import argon2 from 'argon2';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { Knex } from 'knex';
|
||||
import { clone } from 'lodash';
|
||||
import cache from '../cache';
|
||||
import getDatabase from '../database';
|
||||
import env from '../env';
|
||||
import {
|
||||
@@ -287,8 +286,8 @@ export class UsersService extends ItemsService {
|
||||
|
||||
await this.knex('directus_users').update({ password: passwordHashed, status: 'active' }).where({ id: user.id });
|
||||
|
||||
if (cache && env.CACHE_AUTO_PURGE) {
|
||||
await cache.clear();
|
||||
if (this.cache && env.CACHE_AUTO_PURGE) {
|
||||
await this.cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,8 +342,8 @@ export class UsersService extends ItemsService {
|
||||
|
||||
await this.knex('directus_users').update({ password: passwordHashed, status: 'active' }).where({ id: user.id });
|
||||
|
||||
if (cache && env.CACHE_AUTO_PURGE) {
|
||||
await cache.clear();
|
||||
if (this.cache && env.CACHE_AUTO_PURGE) {
|
||||
await this.cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -361,6 +361,22 @@ export function applyFilter(
|
||||
dbQuery[logical].whereNot(selectionRaw, 'like', `%${compareValue}%`);
|
||||
}
|
||||
|
||||
if (operator === '_starts_with') {
|
||||
dbQuery[logical].where(key, 'like', `${compareValue}%`);
|
||||
}
|
||||
|
||||
if (operator === '_nstarts_with') {
|
||||
dbQuery[logical].whereNot(key, 'like', `${compareValue}%`);
|
||||
}
|
||||
|
||||
if (operator === '_ends_with') {
|
||||
dbQuery[logical].where(key, 'like', `%${compareValue}`);
|
||||
}
|
||||
|
||||
if (operator === '_nends_with') {
|
||||
dbQuery[logical].whereNot(key, 'like', `%${compareValue}`);
|
||||
}
|
||||
|
||||
if (operator === '_gt') {
|
||||
dbQuery[logical].where(selectionRaw, '>', compareValue);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import BaseJoi, { AnySchema } from 'joi';
|
||||
import { Filter } from '../types';
|
||||
import { escapeRegExp } from 'lodash';
|
||||
|
||||
const Joi: typeof BaseJoi = BaseJoi.extend({
|
||||
type: 'string',
|
||||
@@ -92,6 +93,22 @@ function getJoi(operator: string, value: any) {
|
||||
return Joi.string().ncontains(value);
|
||||
}
|
||||
|
||||
if (operator === '_starts_with') {
|
||||
return Joi.string().pattern(new RegExp(`^${escapeRegExp(value)}.*`), { name: 'starts_with' });
|
||||
}
|
||||
|
||||
if (operator === '_nstarts_with') {
|
||||
return Joi.string().pattern(new RegExp(`^${escapeRegExp(value)}.*`), { name: 'starts_with', invert: true });
|
||||
}
|
||||
|
||||
if (operator === '_ends_with') {
|
||||
return Joi.string().pattern(new RegExp(`.*${escapeRegExp(value)}$`), { name: 'ends_with' });
|
||||
}
|
||||
|
||||
if (operator === '_nends_with') {
|
||||
return Joi.string().pattern(new RegExp(`.*${escapeRegExp(value)}$`), { name: 'ends_with', invert: true });
|
||||
}
|
||||
|
||||
if (operator === '_in') {
|
||||
return Joi.any().equal(...(value as (string | number)[]));
|
||||
}
|
||||
|
||||
@@ -12,20 +12,32 @@ import getDefaultValue from './get-default-value';
|
||||
import getLocalType from './get-local-type';
|
||||
import { mergePermissions } from './merge-permissions';
|
||||
import getDatabase from '../database';
|
||||
import { getCache } from '../cache';
|
||||
import env from '../env';
|
||||
import ms from 'ms';
|
||||
|
||||
export async function getSchema(options?: {
|
||||
accountability?: Accountability;
|
||||
database?: Knex;
|
||||
}): Promise<SchemaOverview> {
|
||||
// Allows for use in the CLI
|
||||
const database = options?.database || getDatabase();
|
||||
const schemaInspector = SchemaInspector(database);
|
||||
const { schemaCache } = getCache();
|
||||
|
||||
const result: SchemaOverview = {
|
||||
collections: {},
|
||||
relations: [],
|
||||
permissions: [],
|
||||
};
|
||||
let result: SchemaOverview;
|
||||
|
||||
if (env.CACHE_SCHEMA !== false && schemaCache) {
|
||||
const cachedSchema = (await schemaCache.get('schema')) as SchemaOverview;
|
||||
|
||||
if (cachedSchema) {
|
||||
result = cachedSchema;
|
||||
} else {
|
||||
result = await getDatabaseSchema(database, schemaInspector);
|
||||
await schemaCache.set('schema', result, typeof env.CACHE_SCHEMA === 'string' ? ms(env.CACHE_SCHEMA) : undefined);
|
||||
}
|
||||
} else {
|
||||
result = await getDatabaseSchema(database, schemaInspector);
|
||||
}
|
||||
|
||||
let permissions: Permission[] = [];
|
||||
|
||||
@@ -65,6 +77,19 @@ export async function getSchema(options?: {
|
||||
|
||||
result.permissions = permissions;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function getDatabaseSchema(
|
||||
database: Knex,
|
||||
schemaInspector: ReturnType<typeof SchemaInspector>
|
||||
): Promise<SchemaOverview> {
|
||||
const result: SchemaOverview = {
|
||||
collections: {},
|
||||
relations: [],
|
||||
permissions: [],
|
||||
};
|
||||
|
||||
const schemaOverview = await schemaInspector.overview();
|
||||
|
||||
const collections = [
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const readdir = promisify(fs.readdir);
|
||||
const stat = promisify(fs.stat);
|
||||
|
||||
export default async function listFolders(location: string): Promise<string[]> {
|
||||
const fullPath = path.resolve(location);
|
||||
const files = await readdir(fullPath);
|
||||
|
||||
const directories: string[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(fullPath, file);
|
||||
const stats = await stat(filePath);
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
directories.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
return directories;
|
||||
}
|
||||
@@ -53,6 +53,10 @@ function validateFilter(filter: Query['filter']) {
|
||||
case '_neq':
|
||||
case '_contains':
|
||||
case '_ncontains':
|
||||
case '_starts_with':
|
||||
case '_nstarts_with':
|
||||
case '_ends_with':
|
||||
case '_nends_with':
|
||||
case '_gt':
|
||||
case '_gte':
|
||||
case '_lt':
|
||||
|
||||
Reference in New Issue
Block a user