Merge branch 'main' into aggregation

This commit is contained in:
rijkvanzanten
2021-06-09 14:16:36 -04:00
529 changed files with 36532 additions and 32331 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "directus",
"version": "9.0.0-rc.73",
"version": "9.0.0-rc.74",
"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.",
@@ -66,14 +66,14 @@
"example.env"
],
"dependencies": {
"@directus/app": "9.0.0-rc.73",
"@directus/drive": "9.0.0-rc.73",
"@directus/drive-azure": "9.0.0-rc.73",
"@directus/drive-gcs": "9.0.0-rc.73",
"@directus/drive-s3": "9.0.0-rc.73",
"@directus/format-title": "9.0.0-rc.73",
"@directus/schema": "9.0.0-rc.73",
"@directus/specs": "9.0.0-rc.73",
"@directus/app": "9.0.0-rc.74",
"@directus/drive": "9.0.0-rc.74",
"@directus/drive-azure": "9.0.0-rc.74",
"@directus/drive-gcs": "9.0.0-rc.74",
"@directus/drive-s3": "9.0.0-rc.74",
"@directus/format-title": "9.0.0-rc.74",
"@directus/schema": "9.0.0-rc.74",
"@directus/specs": "9.0.0-rc.74",
"@godaddy/terminus": "^4.9.0",
"argon2": "^0.28.1",
"async": "^3.2.0",
@@ -90,9 +90,9 @@
"date-fns": "^2.21.1",
"deep-map": "^2.0.0",
"destroy": "^1.0.4",
"dotenv": "^9.0.2",
"dotenv": "^10.0.0",
"eventemitter2": "^6.4.3",
"execa": "^5.0.1",
"execa": "^5.1.1",
"exif-reader": "^1.0.3",
"express": "^4.17.1",
"express-pino-logger": "^6.0.0",
@@ -110,13 +110,14 @@
"jsonwebtoken": "^8.5.1",
"keyv": "^4.0.3",
"knex": "^0.95.6",
"knex-schema-inspector": "^1.5.6",
"knex-schema-inspector": "^1.5.7",
"liquidjs": "^9.25.0",
"lodash": "^4.17.21",
"macos-release": "^2.4.1",
"mime-types": "^2.1.31",
"ms": "^2.1.3",
"nanoid": "^3.1.23",
"node-cron": "^3.0.0",
"node-machine-id": "^1.1.12",
"nodemailer": "^6.6.1",
"openapi3-ts": "^2.0.0",
@@ -135,7 +136,7 @@
"optionalDependencies": {
"@keyv/redis": "^2.1.2",
"connect-memcached": "^1.0.0",
"connect-redis": "^5.2.0",
"connect-redis": "^6.0.0",
"connect-session-knex": "^2.1.0",
"ioredis": "^4.27.2",
"keyv-memcache": "^1.2.5",
@@ -169,9 +170,10 @@
"@types/mime-types": "^2.1.0",
"@types/ms": "^0.7.31",
"@types/node": "^15.12.0",
"@types/node-cron": "^2.0.3",
"@types/nodemailer": "^6.4.1",
"@types/qs": "^6.9.6",
"@types/sharp": "^0.28.1",
"@types/sharp": "^0.28.3",
"@types/stream-json": "^1.7.0",
"@types/uuid": "^8.3.0",
"@types/uuid-validate": "^0.0.1",

View File

@@ -0,0 +1,13 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable('directus_collections', (table) => {
table.json('item_duplication_fields').nullable();
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable('directus_collections', (table) => {
table.dropColumn('item_duplication_fields');
});
}

View File

@@ -179,3 +179,19 @@ fields:
- text: '$t:field_options.directus_collections.do_not_track_anything'
value: null
width: half
- field: duplication_divider
special:
- alias
- no-data
interface: presentation-divider
options:
icon: content_copy
title: Duplication
- field: item_duplication_fields
special:
- json
interface: code
options:
language: JSON

View File

@@ -7,11 +7,10 @@ import dotenv from 'dotenv';
import fs from 'fs';
import { clone, toNumber, toString } from 'lodash';
import path from 'path';
import logger from './logger';
import { requireYAML } from './utils/require-yaml';
import { toArray } from './utils/to-array';
const acceptableEnvTypes = ['string', 'number', 'regex', 'array'];
const acceptedEnvTypes = ['string', 'number', 'regex', 'array'];
const defaults: Record<string, any> = {
CONFIG_PATH: path.resolve(process.cwd(), '.env'),
@@ -125,7 +124,7 @@ function getEnv() {
return exported;
}
logger.warn(
throw new Error(
`Invalid JS configuration file export type. Requires one of "function", "object", received: "${typeof exported}"`
);
}
@@ -141,11 +140,11 @@ function getEnv() {
return data as Record<string, string>;
}
logger.warn('Invalid YAML configuration. Root has to ben an object.');
throw new Error('Invalid YAML configuration. Root has to be an object.');
}
// Default to env vars plain text files
return dotenv.parse(fs.readFileSync(configPath).toString());
return dotenv.parse(fs.readFileSync(configPath, { encoding: 'utf8' }));
}
function getVariableType(variable: string) {
@@ -175,12 +174,33 @@ function getEnvironmentValueByType(envVariableString: string) {
function processValues(env: Record<string, any>) {
env = clone(env);
for (const [key, value] of Object.entries(env)) {
if (typeof value === 'string' && acceptableEnvTypes.some((envType) => value.includes(`${envType}:`))) {
for (let [key, value] of Object.entries(env)) {
// If key ends with '_FILE', try to get the value from the file defined in this variable
// and store it in the variable with the same name but without '_FILE' at the end
let newKey;
if (key.length > 5 && key.endsWith('_FILE')) {
try {
value = fs.readFileSync(value, { encoding: 'utf8' });
newKey = key.slice(0, -5);
if (newKey in env) {
throw new Error(
`Duplicate environment variable encountered: you can't use "${key}" and "${newKey}" simultaneously.`
);
}
key = newKey;
} catch {
throw new Error(`Failed to read value from file "${value}", defined in environment variable "${key}".`);
}
}
// Convert values with a type prefix
// (see https://docs.directus.io/reference/environment-variables/#environment-syntax-prefix)
if (typeof value === 'string' && acceptedEnvTypes.some((envType) => value.includes(`${envType}:`))) {
env[key] = getEnvironmentValueByType(value);
continue;
}
// Convert values where the key is defined in typeMap
if (typeMap[key]) {
switch (typeMap[key]) {
case 'number':
@@ -193,14 +213,35 @@ function processValues(env: Record<string, any>) {
env[key] = toArray(value);
break;
}
continue;
}
if (value === 'true') env[key] = true;
if (value === 'false') env[key] = false;
if (value === 'null') env[key] = null;
if (String(value).startsWith('0') === false && isNaN(value) === false && value.length > 0) env[key] = Number(value);
// Try to convert remaining values:
// - boolean values to boolean
// - 'null' to null
// - number values (> 0 <= Number.MAX_SAFE_INTEGER) to number
if (value === 'true' || value === 'false') {
env[key] = !!value;
continue;
}
if (value === 'null') {
env[key] = null;
continue;
}
if (
String(value).startsWith('0') === false &&
isNaN(value) === false &&
value.length > 0 &&
value <= Number.MAX_SAFE_INTEGER
) {
env[key] = Number(value);
continue;
}
// If '_FILE' variable hasn't been processed yet, store it as it is (string)
if (newKey) {
env[key] = value;
}
}
return env;

View File

@@ -11,6 +11,7 @@ import * as services from './services';
import { EndpointRegisterFunction, HookRegisterFunction } from './types';
import { getSchema } from './utils/get-schema';
import listFolders from './utils/list-folders';
import { schedule, validate } from 'node-cron';
export async function ensureFoldersExist(): Promise<void> {
const folders = ['endpoints', 'hooks', 'interfaces', 'modules', 'layouts', 'displays'];
@@ -94,8 +95,19 @@ function registerHooks(hooks: string[]) {
}
const events = register({ services, exceptions, env, database: getDatabase(), getSchema });
for (const [event, handler] of Object.entries(events)) {
emitter.on(event, handler);
if (event.startsWith('cron(')) {
const cron = event.match(/\(([^)]+)\)/)?.[1];
if (!cron || validate(cron) === false) {
logger.warn(`Couldn't register cron hook. Provided cron is invalid: ${cron}`);
} else {
schedule(cron, handler);
}
} else {
emitter.on(event, handler);
}
}
}
}

View File

@@ -4,6 +4,7 @@
import env from './env';
import { toArray } from './utils/to-array';
import { getConfigFromEnv } from './utils/get-config-from-env';
const enabledProviders = toArray(env.OAUTH_PROVIDERS).map((provider) => provider.toLowerCase());
@@ -16,23 +17,8 @@ const config: any = {
},
};
for (const [key, value] of Object.entries(env)) {
if (key.startsWith('OAUTH') === false) continue;
const parts = key.split('_');
const provider = parts[1].toLowerCase();
if (enabledProviders.includes(provider) === false) continue;
// OAUTH <PROVIDER> SETTING = VALUE
parts.splice(0, 2);
const configKey = parts.join('_').toLowerCase();
config[provider] = {
...(config[provider] || {}),
[configKey]: value,
};
for (const provider of enabledProviders) {
config[provider] = getConfigFromEnv(`OAUTH_${provider.toUpperCase()}_`, undefined, 'underscore');
}
export default config;

View File

@@ -8,6 +8,7 @@ export type CollectionMeta = {
singleton: boolean;
icon: string | null;
translations: Record<string, string>;
item_duplication_fields: string[] | null;
accountability: 'all' | 'accountability' | null;
};

View File

@@ -2,7 +2,11 @@ import camelcase from 'camelcase';
import { set } from 'lodash';
import env from '../env';
export function getConfigFromEnv(prefix: string, omitPrefix?: string | string[]): any {
export function getConfigFromEnv(
prefix: string,
omitPrefix?: string | string[],
type: 'camelcase' | 'underscore' = 'camelcase'
): Record<string, any> {
const config: any = {};
for (const [key, value] of Object.entries(env)) {
@@ -23,12 +27,22 @@ export function getConfigFromEnv(prefix: string, omitPrefix?: string | string[])
if (key.includes('__')) {
const path = key
.split('__')
.map((key, index) => (index === 0 ? camelcase(camelcase(key.slice(prefix.length))) : camelcase(key)));
.map((key, index) => (index === 0 ? transform(transform(key.slice(prefix.length))) : transform(key)));
set(config, path.join('.'), value);
} else {
config[camelcase(key.slice(prefix.length))] = value;
config[transform(key.slice(prefix.length))] = value;
}
}
return config;
function transform(key: string): string {
if (type === 'camelcase') {
return camelcase(key);
} else if (type === 'underscore') {
return key.toLowerCase();
}
return key;
}
}