Merge branch 'aggregation' into insights

This commit is contained in:
rijkvanzanten
2021-09-13 13:21:02 -04:00
889 changed files with 26031 additions and 110954 deletions

View File

@@ -0,0 +1,164 @@
import { Field, RawField } from '@directus/shared/types';
import { Knex } from 'knex';
import { stringify as geojsonToWKT, GeoJSONGeometry } from 'wellknown';
import getDatabase from '..';
let geometryHelper: KnexSpatial | undefined;
export function getGeometryHelper(): KnexSpatial {
if (!geometryHelper) {
const db = getDatabase();
const client = db.client.config.client as string;
const constructor = {
mysql: KnexSpatial_MySQL,
mariadb: KnexSpatial_MySQL,
sqlite3: KnexSpatial,
pg: KnexSpatial_PG,
postgres: KnexSpatial_PG,
redshift: KnexSpatial_Redshift,
mssql: KnexSpatial_MSSQL,
oracledb: KnexSpatial_Oracle,
}[client];
if (!constructor) {
throw new Error(`Geometry helper not implemented on ${client}.`);
}
geometryHelper = new constructor(db);
}
return geometryHelper;
}
class KnexSpatial {
constructor(protected knex: Knex) {}
isTrue(expression: Knex.Raw) {
return expression;
}
isFalse(expression: Knex.Raw) {
return expression.wrap('NOT ', '');
}
createColumn(table: Knex.CreateTableBuilder, field: RawField | Field) {
const type = field.schema?.geometry_type ?? 'geometry';
return table.specificType(field.field, type);
}
asText(table: string, column: string): Knex.Raw {
return this.knex.raw('st_astext(??.??) as ??', [table, column, column]);
}
fromText(text: string): Knex.Raw {
return this.knex.raw('st_geomfromtext(?, 4326)', text);
}
fromGeoJSON(geojson: GeoJSONGeometry): Knex.Raw {
return this.fromText(geojsonToWKT(geojson));
}
_intersects(key: string, geojson: GeoJSONGeometry): Knex.Raw {
const geometry = this.fromGeoJSON(geojson);
return this.knex.raw('st_intersects(??, ?)', [key, geometry]);
}
intersects(key: string, geojson: GeoJSONGeometry): Knex.Raw {
return this.isTrue(this._intersects(key, geojson));
}
nintersects(key: string, geojson: GeoJSONGeometry): Knex.Raw {
return this.isFalse(this._intersects(key, geojson));
}
_intersects_bbox(key: string, geojson: GeoJSONGeometry): Knex.Raw {
const geometry = this.fromGeoJSON(geojson);
return this.knex.raw('intersects(??, ?)', [key, geometry]);
}
intersects_bbox(key: string, geojson: GeoJSONGeometry): Knex.Raw {
return this.isTrue(this._intersects_bbox(key, geojson));
}
nintersects_bbox(key: string, geojson: GeoJSONGeometry): Knex.Raw {
return this.isFalse(this._intersects_bbox(key, geojson));
}
collect(table: string, column: string): Knex.Raw {
return this.knex.raw('st_astext(st_collect(??.??))', [table, column]);
}
}
class KnexSpatial_PG extends KnexSpatial {
createColumn(table: Knex.CreateTableBuilder, field: RawField | Field) {
const type = field.schema?.geometry_type ?? 'geometry';
return table.specificType(field.field, `geometry(${type})`);
}
_intersects_bbox(key: string, geojson: GeoJSONGeometry): Knex.Raw {
const geometry = this.fromGeoJSON(geojson);
return this.knex.raw('?? && ?', [key, geometry]);
}
}
class KnexSpatial_MySQL extends KnexSpatial {
collect(table: string, column: string): Knex.Raw {
return this.knex.raw(
`concat('geometrycollection(', group_concat(? separator ', '), ')'`,
this.asText(table, column)
);
}
}
class KnexSpatial_Redshift extends KnexSpatial {
createColumn(table: Knex.CreateTableBuilder, field: RawField | Field) {
const type = field.schema?.geometry_type ?? 'geometry';
if (type !== 'geometry') field.meta!.special![1] = type;
return table.specificType(field.field, 'geometry');
}
}
class KnexSpatial_MSSQL extends KnexSpatial {
isTrue(expression: Knex.Raw) {
return expression.wrap(``, ` = 1`);
}
isFalse(expression: Knex.Raw) {
return expression.wrap(``, ` = 0`);
}
createColumn(table: Knex.CreateTableBuilder, field: RawField | Field) {
const type = field.schema?.geometry_type ?? 'geometry';
if (type !== 'geometry') field.meta!.special![1] = type;
return table.specificType(field.field, 'geometry');
}
asText(table: string, column: string): Knex.Raw {
return this.knex.raw('??.??.STAsText() as ??', [table, column, column]);
}
fromText(text: string): Knex.Raw {
return this.knex.raw('geometry::STGeomFromText(?, 4326)', text);
}
_intersects(key: string, geojson: GeoJSONGeometry): Knex.Raw {
const geometry = this.fromGeoJSON(geojson);
return this.knex.raw('??.STIntersects(?)', [key, geometry]);
}
_intersects_bbox(key: string, geojson: GeoJSONGeometry): Knex.Raw {
const geometry = this.fromGeoJSON(geojson);
return this.knex.raw('??.STEnvelope().STIntersects(?.STEnvelope())', [key, geometry]);
}
collect(table: string, column: string): Knex.Raw {
return this.knex.raw('geometry::CollectionAggregate(??.??).STAsText()', [table, column]);
}
}
class KnexSpatial_Oracle extends KnexSpatial {
isTrue(expression: Knex.Raw) {
return expression.wrap(``, ` = 'TRUE'`);
}
isFalse(expression: Knex.Raw) {
return expression.wrap(``, ` = 'FALSE'`);
}
createColumn(table: Knex.CreateTableBuilder, field: RawField | Field) {
const type = field.schema?.geometry_type ?? 'geometry';
if (type !== 'geometry') field.meta!.special![1] = type;
return table.specificType(field.field, 'sdo_geometry');
}
asText(table: string, column: string): Knex.Raw {
return this.knex.raw('sdo_util.from_wktgeometry(??.??) as ??', [table, column, column]);
}
fromText(text: string): Knex.Raw {
return this.knex.raw('sdo_geometry(?, 4326)', text);
}
_intersects(key: string, geojson: GeoJSONGeometry): Knex.Raw {
const geometry = this.fromGeoJSON(geojson);
return this.knex.raw(`sdo_overlapbdyintersect(??, ?)`, [key, geometry]);
}
_intersects_bbox(key: string, geojson: GeoJSONGeometry): Knex.Raw {
const geometry = this.fromGeoJSON(geojson);
return this.knex.raw(`sdo_overlapbdyintersect(sdo_geom.sdo_mbr(??), sdo_geom.sdo_mbr(?))`, [key, geometry]);
}
collect(table: string, column: string): Knex.Raw {
return this.knex.raw(`concat('geometrycollection(', listagg(?, ', '), ')'`, this.asText(table, column));
}
}

View File

@@ -5,6 +5,10 @@ import env from '../env';
import logger from '../logger';
import { getConfigFromEnv } from '../utils/get-config-from-env';
import { validateEnv } from '../utils/validate-env';
import fse from 'fs-extra';
import path from 'path';
import { merge } from 'lodash';
import { promisify } from 'util';
let database: Knex | null = null;
let inspector: ReturnType<typeof SchemaInspector> | null = null;
@@ -19,6 +23,7 @@ export default function getDatabase(): Knex {
'DB_SEARCH_PATH',
'DB_CONNECTION_STRING',
'DB_POOL',
'DB_EXCLUDE_TABLES',
]);
const poolConfig = getConfigFromEnv('DB_POOL');
@@ -50,7 +55,15 @@ export default function getDatabase(): Knex {
searchPath: env.DB_SEARCH_PATH,
connection: env.DB_CONNECTION_STRING || connectionConfig,
log: {
warn: (msg) => logger.warn(msg),
warn: (msg) => {
// Ignore warnings about returning not being supported in some DBs
if (msg.startsWith('.returning()')) return;
// Ignore warning about MySQL not supporting TRX for DDL
if (msg.startsWith('Transaction was implicitly committed, do not mix transactions and DDL with MySQL')) return;
return logger.warn(msg);
},
error: (msg) => logger.error(msg),
deprecate: (msg) => logger.info(msg),
debug: (msg) => logger.debug(msg),
@@ -60,11 +73,24 @@ export default function getDatabase(): Knex {
if (env.DB_CLIENT === 'sqlite3') {
knexConfig.useNullAsDefault = true;
poolConfig.afterCreate = (conn: any, cb: any) => {
conn.run('PRAGMA foreign_keys = ON', cb);
poolConfig.afterCreate = async (conn: any, callback: any) => {
logger.trace('Enabling SQLite Foreign Keys support...');
const run = promisify(conn.run.bind(conn));
await run('PRAGMA foreign_keys = ON');
callback(null, conn);
};
}
if (env.DB_CLIENT === 'mssql') {
// This brings MS SQL in line with the other DB vendors. We shouldn't do any automatic
// timezone conversion on the database level, especially not when other database vendors don't
// act the same
merge(knexConfig, { connection: { options: { useUTC: false } } });
}
database = knex(knexConfig);
const times: Record<string, number> = {};
@@ -94,36 +120,131 @@ export function getSchemaInspector(): ReturnType<typeof SchemaInspector> {
return inspector;
}
export async function hasDatabaseConnection(): Promise<boolean> {
const database = getDatabase();
export async function hasDatabaseConnection(database?: Knex): Promise<boolean> {
database = database ?? getDatabase();
try {
if (env.DB_CLIENT === 'oracledb') {
if (getDatabaseClient(database) === 'oracle') {
await database.raw('select 1 from DUAL');
} else {
await database.raw('SELECT 1');
}
return true;
} catch {
return false;
}
}
export async function validateDBConnection(): Promise<void> {
export async function validateDatabaseConnection(database?: Knex): Promise<void> {
database = database ?? getDatabase();
try {
await hasDatabaseConnection();
} catch (error) {
if (getDatabaseClient(database) === 'oracle') {
await database.raw('select 1 from DUAL');
} else {
await database.raw('SELECT 1');
}
} catch (error: any) {
logger.error(`Can't connect to the database.`);
logger.error(error);
process.exit(1);
}
}
export function getDatabaseClient(database?: Knex): 'mysql' | 'postgres' | 'sqlite' | 'oracle' | 'mssql' {
database = database ?? getDatabase();
switch (database.client.constructor.name) {
case 'Client_MySQL':
return 'mysql';
case 'Client_PG':
return 'postgres';
case 'Client_SQLite3':
return 'sqlite';
case 'Client_Oracledb':
case 'Client_Oracle':
return 'oracle';
case 'Client_MSSQL':
return 'mssql';
}
throw new Error(`Couldn't extract database client`);
}
export async function isInstalled(): Promise<boolean> {
const inspector = getSchemaInspector();
// 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.
// exists when Directus is properly installed.
return await inspector.hasTable('directus_collections');
}
export async function validateMigrations(): Promise<boolean> {
const database = getDatabase();
try {
let migrationFiles = await fse.readdir(path.join(__dirname, 'migrations'));
const customMigrationsPath = path.resolve(env.EXTENSIONS_PATH, 'migrations');
let customMigrationFiles =
((await fse.pathExists(customMigrationsPath)) && (await fse.readdir(customMigrationsPath))) || [];
migrationFiles = migrationFiles.filter(
(file: string) => file.startsWith('run') === false && file.endsWith('.d.ts') === false
);
customMigrationFiles = customMigrationFiles.filter((file: string) => file.endsWith('.js'));
migrationFiles.push(...customMigrationFiles);
const requiredVersions = migrationFiles.map((filePath) => filePath.split('-')[0]);
const completedVersions = (await database.select('version').from('directus_migrations')).map(
({ version }) => version
);
return requiredVersions.every((version) => completedVersions.includes(version));
} catch (error: any) {
logger.error(`Database migrations cannot be found`);
logger.error(error);
throw process.exit(1);
}
}
/**
* These database extensions should be optional, so we don't throw or return any problem states when they don't
*/
export async function validateDatabaseExtensions(): Promise<void> {
const database = getDatabase();
const databaseClient = getDatabaseClient(database);
if (databaseClient === 'postgres') {
let available = false;
let installed = false;
const exists = await database.raw(`SELECT name FROM pg_available_extensions WHERE name = 'postgis';`);
if (exists.rows.length > 0) {
available = true;
}
if (available) {
try {
await database.raw(`SELECT PostGIS_version();`);
installed = true;
} catch {
installed = false;
}
}
if (available === false) {
logger.warn(`PostGIS isn't installed. Geometry type support will be limited.`);
} else if (available === true && installed === false) {
logger.warn(
`PostGIS is installed, but hasn't been activated on this database. Geometry type support will be limited.`
);
}
}
}

View File

@@ -1,7 +1,6 @@
import { Knex } from 'knex';
// @ts-ignore
import Client_Oracledb from 'knex/lib/dialects/oracledb';
import env from '../../env';
async function oracleAlterUrl(knex: Knex, type: string): Promise<void> {
await knex.raw('ALTER TABLE "directus_webhooks" ADD "url__temp" ?', [knex.raw(type)]);
@@ -23,7 +22,7 @@ export async function up(knex: Knex): Promise<void> {
}
export async function down(knex: Knex): Promise<void> {
if (env.DB_CLIENT === 'oracledb') {
if (knex.client instanceof Client_Oracledb) {
await oracleAlterUrl(knex, 'VARCHAR2(255)');
return;
}

View File

@@ -1,7 +1,6 @@
import { Knex } from 'knex';
// @ts-ignore
import Client_Oracledb from 'knex/lib/dialects/oracledb';
import env from '../../env';
async function oracleAlterCollections(knex: Knex, type: string): Promise<void> {
await knex.raw('ALTER TABLE "directus_webhooks" ADD "collections__temp" ?', [knex.raw(type)]);
@@ -23,7 +22,7 @@ export async function up(knex: Knex): Promise<void> {
}
export async function down(knex: Knex): Promise<void> {
if (env.DB_CLIENT === 'oracledb') {
if (knex.client instanceof Client_Oracledb) {
await oracleAlterCollections(knex, 'VARCHAR2(255)');
return;
}

View File

@@ -68,7 +68,7 @@ export async function up(knex: Knex): Promise<void> {
await knex(constraint.many_collection)
.update({ [constraint.many_field]: null })
.whereIn(currentPrimaryKeyField, ids);
} catch (err) {
} catch (err: any) {
logger.error(
`${constraint.many_collection}.${constraint.many_field} contains illegal foreign keys which couldn't be set to NULL. Please fix these references and rerun this migration to complete the upgrade.`
);
@@ -111,7 +111,7 @@ export async function up(knex: Knex): Promise<void> {
builder.onDelete('SET NULL');
}
});
} catch (err) {
} catch (err: any) {
logger.warn(
`Couldn't add foreign key constraint for ${constraint.many_collection}.${constraint.many_field}<->${constraint.one_collection}`
);
@@ -140,7 +140,7 @@ export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable(relation.many_collection, (table) => {
table.dropForeign([relation.many_field]);
});
} catch (err) {
} catch (err: any) {
logger.warn(
`Couldn't drop foreign key constraint for ${relation.many_collection}.${relation.many_field}<->${relation.one_collection}`
);

View File

@@ -99,7 +99,7 @@ export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable(update.table, (table) => {
table.dropForeign([constraint.column], existingForeignKey?.constraint_name || undefined);
});
} catch (err) {
} catch (err: any) {
logger.warn(`Couldn't drop foreign key ${update.table}.${constraint.column}->${constraint.references}`);
logger.warn(err);
}
@@ -114,7 +114,7 @@ export async function up(knex: Knex): Promise<void> {
// Knex uses a default convention for index names: `table_column_type`
table.dropIndex([constraint.column], `${update.table}_${constraint.column}_foreign`);
});
} catch (err) {
} catch (err: any) {
logger.warn(
`Couldn't clean up index for foreign key ${update.table}.${constraint.column}->${constraint.references}`
);
@@ -126,7 +126,7 @@ export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable(update.table, (table) => {
table.foreign(constraint.column).references(constraint.references).onDelete(constraint.on_delete);
});
} catch (err) {
} catch (err: any) {
logger.warn(`Couldn't add foreign key to ${update.table}.${constraint.column}->${constraint.references}`);
logger.warn(err);
}
@@ -141,7 +141,7 @@ export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable(update.table, (table) => {
table.dropForeign([constraint.column]);
});
} catch (err) {
} catch (err: any) {
logger.warn(`Couldn't drop foreign key ${update.table}.${constraint.column}->${constraint.references}`);
logger.warn(err);
}
@@ -156,7 +156,7 @@ export async function down(knex: Knex): Promise<void> {
// Knex uses a default convention for index names: `table_column_type`
table.dropIndex([constraint.column], `${update.table}_${constraint.column}_foreign`);
});
} catch (err) {
} catch (err: any) {
logger.warn(
`Couldn't clean up index for foreign key ${update.table}.${constraint.column}->${constraint.references}`
);
@@ -168,7 +168,7 @@ export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable(update.table, (table) => {
table.foreign(constraint.column).references(constraint.references);
});
} catch (err) {
} catch (err: any) {
logger.warn(`Couldn't add foreign key to ${update.table}.${constraint.column}->${constraint.references}`);
logger.warn(err);
}

View File

@@ -1,12 +1,22 @@
import { Knex } from 'knex';
// @ts-ignore
import Client_Oracledb from 'knex/lib/dialects/oracledb';
export async function up(knex: Knex): Promise<void> {
if (knex.client instanceof Client_Oracledb) {
return;
}
await knex.schema.alterTable('directus_files', (table) => {
table.bigInteger('filesize').nullable().defaultTo(null).alter();
});
}
export async function down(knex: Knex): Promise<void> {
if (knex.client instanceof Client_Oracledb) {
return;
}
await knex.schema.alterTable('directus_files', (table) => {
table.integer('filesize').nullable().defaultTo(null).alter();
});

View File

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

View File

@@ -0,0 +1,22 @@
import { Knex } from 'knex';
import { getDefaultIndexName } from '../../utils/get-default-index-name';
const indexName = getDefaultIndexName('foreign', 'directus_settings', 'storage_default_folder');
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable('directus_settings', (table) => {
table
.uuid('storage_default_folder')
.references('id')
.inTable('directus_folders')
.withKeyName(indexName)
.onDelete('SET NULL');
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable('directus_files', (table) => {
table.dropForeign(['storage_default_folder'], indexName);
table.dropColumn('storage_default_folder');
});
}

View File

@@ -0,0 +1,49 @@
import { Knex } from 'knex';
import logger from '../../logger';
export async function up(knex: Knex): Promise<void> {
const dividerGroups = await knex.select('*').from('directus_fields').where('interface', '=', 'group-divider');
for (const dividerGroup of dividerGroups) {
const newOptions: { showHeader: true; headerIcon?: string; headerColor?: string } = { showHeader: true };
if (dividerGroup.options) {
try {
const options =
typeof dividerGroup.options === 'string' ? JSON.parse(dividerGroup.options) : dividerGroup.options;
if (options.icon) newOptions.headerIcon = options.icon;
if (options.color) newOptions.headerColor = options.color;
} catch (err: any) {
logger.warn(`Couldn't convert previous options from field ${dividerGroup.collection}.${dividerGroup.field}`);
logger.warn(err);
}
}
try {
await knex('directus_fields')
.update({
interface: 'group-standard',
options: JSON.stringify(newOptions),
})
.where('id', '=', dividerGroup.id);
} catch (err: any) {
logger.warn(`Couldn't update ${dividerGroup.collection}.${dividerGroup.field} to new group interface`);
logger.warn(err);
}
}
await knex('directus_fields')
.update({
interface: 'group-standard',
})
.where({ interface: 'group-raw' });
}
export async function down(knex: Knex): Promise<void> {
await knex('directus_fields')
.update({
interface: 'group-raw',
})
.where('interface', '=', 'group-standard');
}

View File

@@ -0,0 +1,13 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable('directus_fields', (table) => {
table.boolean('required').defaultTo(false);
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable('directus_fields', (table) => {
table.dropColumn('required');
});
}

View File

@@ -0,0 +1,35 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
const groups = await knex.select('*').from('directus_fields').where({ interface: 'group-standard' });
const raw = [];
const detail = [];
for (const group of groups) {
const options = typeof group.options === 'string' ? JSON.parse(group.options) : group.options || {};
if (options.showHeader === true) {
detail.push(group);
} else {
raw.push(group);
}
}
for (const field of raw) {
await knex('directus_fields').update({ interface: 'group-raw' }).where({ id: field.id });
}
for (const field of detail) {
await knex('directus_fields').update({ interface: 'group-detail' }).where({ id: field.id });
}
}
export async function down(knex: Knex): Promise<void> {
await knex('directus_fields')
.update({
interface: 'group-standard',
})
.where({ interface: 'group-detail' })
.orWhere({ interface: 'group-raw' });
}

View File

@@ -0,0 +1,94 @@
import { Knex } from 'knex';
// Change image metadata structure to match the output from 'exifr'
export async function up(knex: Knex): Promise<void> {
const files = await knex
.select<{ id: number; metadata: string }[]>('id', 'metadata')
.from('directus_files')
.whereNotNull('metadata');
for (const { id, metadata } of files) {
let prevMetadata;
try {
prevMetadata = JSON.parse(metadata);
} catch {
continue;
}
// Update only required if metadata has 'exif' data
if (prevMetadata.exif) {
// Get all data from 'exif' and rename the following keys:
// - 'image' to 'ifd0'
// - 'thumbnail to 'ifd1'
// - 'interoperability' to 'interop'
const newMetadata = prevMetadata.exif;
if (newMetadata.image) {
newMetadata.ifd0 = newMetadata.image;
delete newMetadata.image;
}
if (newMetadata.thumbnail) {
newMetadata.ifd1 = newMetadata.thumbnail;
delete newMetadata.thumbnail;
}
if (newMetadata.interoperability) {
newMetadata.interop = newMetadata.interoperability;
delete newMetadata.interoperability;
}
if (prevMetadata.icc) {
newMetadata.icc = prevMetadata.icc;
}
if (prevMetadata.iptc) {
newMetadata.iptc = prevMetadata.iptc;
}
await knex('directus_files')
.update({ metadata: JSON.stringify(newMetadata) })
.where({ id });
}
}
}
export async function down(knex: Knex): Promise<void> {
const files = await knex
.select<{ id: number; metadata: string }[]>('id', 'metadata')
.from('directus_files')
.whereNotNull('metadata')
.whereNot('metadata', '{}');
for (const { id, metadata } of files) {
const prevMetadata = JSON.parse(metadata);
// Update only required if metadata has keys other than 'icc' and 'iptc'
if (Object.keys(prevMetadata).filter((key) => key !== 'icc' && key !== 'iptc').length > 0) {
// Put all data under 'exif' and rename/move keys afterwards
const newMetadata: { exif: Record<string, unknown>; icc?: unknown; iptc?: unknown } = { exif: prevMetadata };
if (newMetadata.exif.ifd0) {
newMetadata.exif.image = newMetadata.exif.ifd0;
delete newMetadata.exif.ifd0;
}
if (newMetadata.exif.ifd1) {
newMetadata.exif.thumbnail = newMetadata.exif.ifd1;
delete newMetadata.exif.ifd1;
}
if (newMetadata.exif.interop) {
newMetadata.exif.interoperability = newMetadata.exif.interop;
delete newMetadata.exif.interop;
}
if (newMetadata.exif.icc) {
newMetadata.icc = newMetadata.exif.icc;
delete newMetadata.exif.icc;
}
if (newMetadata.exif.iptc) {
newMetadata.iptc = newMetadata.exif.iptc;
delete newMetadata.exif.iptc;
}
await knex('directus_files')
.update({ metadata: JSON.stringify(newMetadata) })
.where({ id });
}
}
}

View File

@@ -0,0 +1,15 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable('directus_settings', (table) => {
table.json('basemaps');
table.string('mapbox_key');
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable('directus_settings', (table) => {
table.dropColumn('basemaps');
table.dropColumn('mapbox_key');
});
}

View File

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

View File

@@ -0,0 +1,13 @@
import { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable('directus_webhooks', (table) => {
table.text('collections').notNullable().alter();
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable('directus_webhooks', (table) => {
table.text('collections').alter();
});
}

View File

@@ -1,16 +1,10 @@
/* eslint-disable no-console */
import formatTitle from '@directus/format-title';
import fse from 'fs-extra';
import { Knex } from 'knex';
import path from 'path';
import env from '../../env';
type Migration = {
version: string;
name: string;
timestamp: Date;
};
import logger from '../../logger';
import { Migration } from '../../types';
export default async function run(database: Knex, direction: 'up' | 'down' | 'latest'): Promise<void> {
let migrationFiles = await fse.readdir(__dirname);
@@ -67,7 +61,7 @@ export default async function run(database: Knex, direction: 'up' | 'down' | 'la
const { up } = require(nextVersion.file);
console.log(`Applying ${nextVersion.name}...`);
logger.info(`Applying ${nextVersion.name}...`);
await up(database);
await database.insert({ version: nextVersion.version, name: nextVersion.name }).into('directus_migrations');
@@ -88,7 +82,7 @@ export default async function run(database: Knex, direction: 'up' | 'down' | 'la
const { down } = require(migration.file);
console.log(`Undoing ${migration.name}...`);
logger.info(`Undoing ${migration.name}...`);
await down(database);
await database('directus_migrations').delete().where({ version: migration.version });
@@ -99,7 +93,7 @@ export default async function run(database: Knex, direction: 'up' | 'down' | 'la
if (migration.completed === false) {
const { up } = require(migration.file);
console.log(`Applying ${migration.name}...`);
logger.info(`Applying ${migration.name}...`);
await up(database);
await database.insert({ version: migration.version, name: migration.name }).into('directus_migrations');

View File

@@ -2,13 +2,15 @@ import { Knex } from 'knex';
import { clone, cloneDeep, pick, uniq } from 'lodash';
import { PayloadService } from '../services/payload';
import { Item, Query, SchemaOverview } from '../types';
import { AST, FieldNode, NestedCollectionNode } from '../types/ast';
import { AST, FieldNode, NestedCollectionNode, M2ONode } from '../types/ast';
import { applyFunctionToColumnName } from '../utils/apply-function-to-column-name';
import applyQuery from '../utils/apply-query';
import { getColumn } from '../utils/get-column';
import { stripFunction } from '../utils/strip-function';
import { toArray } from '../utils/to-array';
import { toArray } from '@directus/shared/utils';
import getDatabase from './index';
import { isNativeGeometry } from '../utils/geometry';
import { getGeometryHelper } from '../database/helpers/geometry';
type RunASTOptions = {
/**
@@ -58,7 +60,7 @@ export default async function runAST(
async function run(collection: string, children: (NestedCollectionNode | FieldNode)[], query: Query) {
// Retrieve the database columns to select in the current AST
const { columnsToSelect, primaryKeyField, nestedCollectionNodes } = await parseCurrentLevel(
const { fieldNodes, primaryKeyField, nestedCollectionNodes } = await parseCurrentLevel(
schema,
collection,
children,
@@ -66,7 +68,7 @@ export default async function runAST(
);
// The actual knex query builder instance. This is a promise that resolves with the raw items from the db
const dbQuery = await getDBQuery(schema, knex, collection, columnsToSelect, query, options?.nested);
const dbQuery = await getDBQuery(schema, knex, collection, fieldNodes, query, options?.nested);
const rawItems: Item | Item[] = await dbQuery;
@@ -117,8 +119,17 @@ async function parseCurrentLevel(
for (const child of children) {
if (child.type === 'field') {
const fieldKey = stripFunction(child.name);
if (columnsInCollection.includes(fieldKey) || fieldKey === '*') {
columnsToSelectInternal.push(child.name); // maintain original name here (includes functions)
if (query.alias) {
columnsToSelectInternal.push(
...Object.entries(query.alias)
.filter(([_key, value]) => value === child.name)
.map(([key]) => key)
);
}
}
continue;
@@ -127,7 +138,7 @@ async function parseCurrentLevel(
if (!child.relation) continue;
if (child.type === 'm2o') {
columnsToSelectInternal.push(child.relation.field);
columnsToSelectInternal.push(child.fieldKey);
}
if (child.type === 'm2a') {
@@ -138,31 +149,62 @@ async function parseCurrentLevel(
nestedCollectionNodes.push(child);
}
/**
* Always fetch primary key in case there's a nested relation that needs it, but only if there's
* no aggregation / grouping going on
const isAggregate = (query.aggregate && Object.keys(query.aggregate).length > 0) ?? false;
/** Always fetch primary key in case there's a nested relation that needs it. Aggregate payloads
* can't have nested relational fields
*/
const hasAggregationOrGrouping = 'aggregate' in query || 'group' in query;
if (columnsToSelectInternal.includes(primaryKeyField) === false && hasAggregationOrGrouping === false) {
if (isAggregate === false && columnsToSelectInternal.includes(primaryKeyField) === false) {
columnsToSelectInternal.push(primaryKeyField);
}
/** Make sure select list has unique values */
const columnsToSelect = [...new Set(columnsToSelectInternal)];
return { columnsToSelect, nestedCollectionNodes, primaryKeyField };
const fieldNodes = columnsToSelect.map(
(column: string) =>
children.find((childNode) => childNode.fieldKey === column) ?? { type: 'field', name: column, fieldKey: column }
) as FieldNode[];
return { fieldNodes, nestedCollectionNodes, primaryKeyField };
}
function getColumnPreprocessor(knex: Knex, schema: SchemaOverview, table: string) {
const helper = getGeometryHelper();
return function (fieldNode: FieldNode | M2ONode): Knex.Raw<string> {
let field;
if (fieldNode.type === 'field') {
field = schema.collections[table].fields[stripFunction(fieldNode.name)];
} else {
field = schema.collections[fieldNode.relation.collection].fields[fieldNode.relation.field];
}
let alias = undefined;
if (fieldNode.name !== fieldNode.fieldKey) {
alias = fieldNode.fieldKey;
}
if (isNativeGeometry(field)) {
return helper.asText(table, field.field);
}
return getColumn(knex, table, fieldNode.name, alias);
};
}
function getDBQuery(
schema: SchemaOverview,
knex: Knex,
table: string,
columns: string[],
fieldNodes: FieldNode[],
query: Query,
nested?: boolean
): Knex.QueryBuilder {
const dbQuery = knex.select(columns.map((column) => getColumn(knex, table, column))).from(table);
const preProcess = getColumnPreprocessor(knex, schema, table);
const dbQuery = knex.select(fieldNodes.map(preProcess)).from(table);
const queryCopy = clone(query);
queryCopy.limit = typeof queryCopy.limit === 'number' ? queryCopy.limit : 100;
@@ -205,11 +247,19 @@ function applyParentFilters(
});
if (relatedM2OisFetched === false) {
nestedNode.children.push({ type: 'field', name: nestedNode.relation.field });
nestedNode.children.push({
type: 'field',
name: nestedNode.relation.field,
fieldKey: nestedNode.relation.field,
});
}
if (nestedNode.relation.meta?.sort_field) {
nestedNode.children.push({ type: 'field', name: nestedNode.relation.meta.sort_field });
nestedNode.children.push({
type: 'field',
name: nestedNode.relation.meta.sort_field,
fieldKey: nestedNode.relation.meta.sort_field,
});
}
nestedNode.query = {
@@ -387,10 +437,9 @@ function removeTemporaryFields(
const nestedCollectionNodes: NestedCollectionNode[] = [];
for (const child of ast.children) {
if (child.type === 'field') {
fields.push(child.name);
} else {
fields.push(child.fieldKey);
fields.push(child.fieldKey);
if (child.type !== 'field') {
nestedCollectionNodes.push(child);
}
}
@@ -402,7 +451,7 @@ function removeTemporaryFields(
if (operation === 'count' && aggregateFields.includes('*')) fields.push('count');
fields.push(...aggregateFields.map((field) => `${field}_${operation}`));
fields.push(...aggregateFields.map((field) => `${operation}.${field}`));
}
}

View File

@@ -3,13 +3,14 @@ import yaml from 'js-yaml';
import { Knex } from 'knex';
import { isObject } from 'lodash';
import path from 'path';
import { types } from '../../types';
import { Type, Field } from '@directus/shared/types';
import { getGeometryHelper } from '../helpers/geometry';
type TableSeed = {
table: string;
columns: {
[column: string]: {
type?: typeof types[number];
type?: Type;
primary?: boolean;
nullable?: boolean;
default?: any;
@@ -45,6 +46,8 @@ export default async function runSeed(database: Knex): Promise<void> {
for (const [columnName, columnInfo] of Object.entries(seedData.columns)) {
let column: Knex.ColumnBuilder;
if (columnInfo.type === 'alias' || columnInfo.type === 'unknown') return;
if (columnInfo.type === 'string') {
column = tableBuilder.string(columnName, columnInfo.length);
} else if (columnInfo.increments) {
@@ -53,6 +56,9 @@ export default async function runSeed(database: Knex): Promise<void> {
column = tableBuilder.string(columnName);
} else if (columnInfo.type === 'hash') {
column = tableBuilder.string(columnName, 255);
} else if (columnInfo.type === 'geometry') {
const helper = getGeometryHelper();
column = helper.createColumn(tableBuilder, { field: columnName } as Field);
} else {
column = tableBuilder[columnInfo.type!](columnName);
}

View File

@@ -8,7 +8,6 @@ const defaults: Partial<Permission> = {
validation: null,
presets: null,
fields: ['*'],
limit: null,
system: true,
};

View File

@@ -8,51 +8,52 @@ defaults:
note: null
translations: null
display_template: null
accountability: 'all'
data:
- collection: directus_activity
note: Accountability logs for all events
note: $t:directus_collection.directus_activity
- collection: directus_collections
icon: list_alt
note: Additional collection configuration and metadata
note: $t:directus_collection.directus_collections
- collection: directus_fields
icon: input
note: Additional field configuration and metadata
note: $t:directus_collection.directus_fields
- collection: directus_files
icon: folder
note: Metadata for all managed file assets
note: $t:directus_collection.directus_files
display_template: '{{ $thumbnail }} {{ title }}'
- collection: directus_folders
note: Provides virtual directories for files
note: $t:directus_collection.directus_folders
display_template: '{{ name }}'
- collection: directus_migrations
note: What version of the database you're using
note: $t:directus_collection.directus_migrations
- collection: directus_permissions
icon: admin_panel_settings
note: Access permissions for each role
note: $t:directus_collection.directus_permissions
- collection: directus_presets
icon: bookmark_border
note: Presets for collection defaults and bookmarks
note: $t:directus_collection.directus_presets
accountability: null
- collection: directus_relations
icon: merge_type
note: Relationship configuration and metadata
note: $t:directus_collection.directus_relations
- collection: directus_revisions
note: Data snapshots for all activity
note: $t:directus_collection.directus_revisions
- collection: directus_roles
icon: supervised_user_circle
note: Permission groups for system users
note: $t:directus_collection.directus_roles
- collection: directus_sessions
note: User session information
note: $t:directus_collection.directus_sessions
- collection: directus_settings
singleton: true
note: Project configuration options
note: $t:directus_collection.directus_settings
- collection: directus_users
archive_field: status
archive_value: archived
unarchive_value: draft
icon: people_alt
note: System users for the platform
note: $t:directus_collection.directus_users
display_template: '{{ first_name }} {{ last_name }}'
- collection: directus_webhooks
note: Configuration for event-based HTTP requests
note: $t:directus_collection.directus_webhooks

View File

@@ -13,19 +13,19 @@ fields:
defaultForeground: 'var(--foreground-normal)'
defaultBackground: 'var(--background-normal-alt)'
choices:
- text: Create
- text: $t:field_options.directus_activity.create
value: create
foreground: 'var(--primary)'
background: 'var(--primary-25)'
- text: Update
- text: $t:field_options.directus_activity.update
value: update
foreground: 'var(--blue)'
background: 'var(--blue-25)'
- text: Delete
- text: $t:field_options.directus_activity.delete
value: delete
foreground: 'var(--danger)'
background: 'var(--danger-25)'
- text: Login
- text: $t:field_options.directus_activity.login
value: authenticate
foreground: 'var(--purple)'
background: 'var(--purple-25)'

View File

@@ -8,7 +8,7 @@ fields:
interface: presentation-divider
options:
icon: box
title: Collection Setup
title: $t:field_options.directus_collections.collection_setup
width: full
- field: collection
@@ -32,7 +32,7 @@ fields:
- field: color
interface: select-color
options:
placeholder: Choose a color...
placeholder: $t:field_options.directus_collections.note_placeholder
width: half
- field: display_template
@@ -45,7 +45,7 @@ fields:
special: boolean
interface: boolean
options:
label: Hide within the App
label: $t:field_options.directus_collections.hidden_label
width: half
- field: singleton
@@ -102,7 +102,7 @@ fields:
interface: presentation-divider
options:
icon: archive
title: Archive
title: $t:field_options.directus_collections.archive_divider
width: full
- field: archive_field
@@ -110,14 +110,14 @@ fields:
options:
collectionField: collection
allowNone: true
placeholder: Choose a field...
placeholder: $t:field_options.directus_collections.archive_field
width: half
- field: archive_app_filter
interface: boolean
special: boolean
options:
label: Enable App Archive Filter
label: $t:field_options.directus_collections.archive_app_filter
width: half
- field: archive_value
@@ -125,7 +125,7 @@ fields:
options:
font: monospace
iconRight: archive
placeholder: Value set when archiving...
placeholder: $t:field_options.directus_collections.archive_value
width: half
- field: unarchive_value
@@ -133,7 +133,7 @@ fields:
options:
font: monospace
iconRight: unarchive
placeholder: Value set when unarchiving...
placeholder: $t:field_options.directus_collections.unarchive_value
width: half
- field: sort_divider
@@ -143,14 +143,14 @@ fields:
interface: presentation-divider
options:
icon: sort
title: Sort
title: $t:field_options.directus_collections.divider
width: full
- field: sort_field
interface: system-field
options:
collectionField: collection
placeholder: Choose a field...
placeholder: $t:field_options.directus_collections.sort_field
typeAllowList:
- float
- decimal
@@ -165,7 +165,7 @@ fields:
interface: presentation-divider
options:
icon: admin_panel_settings
title: Accountability
title: $t:field_options.directus_collections.accountability_divider
width: full
- field: accountability

View File

@@ -52,6 +52,12 @@ fields:
special: boolean
width: half
- collection: directus_fields
field: required
hidden: true
special: boolean
width: half
- collection: directus_fields
field: sort
width: half
@@ -73,3 +79,8 @@ fields:
- collection: directus_fields
field: note
width: half
- collection: directus_fields
field: conditions
hidden: true
special: json

View File

@@ -10,14 +10,14 @@ fields:
interface: input
options:
iconRight: title
placeholder: A unique title...
placeholder: $t:field_options.directus_files.title
width: full
- field: description
interface: input-multiline
width: full
options:
placeholder: An optional description...
placeholder: $t:field_options.directus_files.description
- field: tags
interface: tags
@@ -35,7 +35,7 @@ fields:
interface: input
options:
iconRight: place
placeholder: An optional location...
placeholder: $t:field_options.directus_files.location
width: half
- field: storage
@@ -49,7 +49,7 @@ fields:
interface: presentation-divider
options:
icon: insert_drive_file
title: File Naming
title: $t:field_options.directus_files.storage_divider
special:
- alias
- no-data
@@ -59,7 +59,7 @@ fields:
interface: input
options:
iconRight: publish
placeholder: Name on disk storage...
placeholder: $t:field_options.directus_files.filename_disk
readonly: true
width: half
@@ -67,7 +67,7 @@ fields:
interface: input
options:
iconRight: get_app
placeholder: Name when downloading...
placeholder: $t:field_options.directus_files.filename_download
width: half
- field: metadata
@@ -106,6 +106,7 @@ fields:
display: user
width: half
hidden: true
special: user-created
- field: uploaded_on
display: datetime

View File

@@ -1,7 +1,7 @@
import fse from 'fs-extra';
import { merge } from 'lodash';
import path from 'path';
import { FieldMeta } from '../../../types';
import { FieldMeta } from '@directus/shared/types';
import { requireYAML } from '../../../utils/require-yaml';
const defaults = requireYAML(require.resolve('./_defaults.yaml'));

View File

@@ -15,9 +15,6 @@ fields:
- field: role
width: half
- field: limit
width: half
- field: collection
width: half

View File

@@ -9,7 +9,7 @@ fields:
- field: name
interface: input
options:
placeholder: The unique name for this role...
placeholder: $t:field_options.directus_roles.name
width: half
- field: icon
@@ -20,7 +20,7 @@ fields:
- field: description
interface: input
options:
placeholder: A description of this role...
placeholder: $t:field_options.directus_roles.description
width: full
- field: app_access
@@ -36,7 +36,7 @@ fields:
- field: ip_access
interface: tags
options:
placeholder: Add allowed IP addresses, leave empty to allow all...
placeholder: $t:field_options.directus_roles.ip_access
special: csv
width: full
@@ -60,13 +60,13 @@ fields:
template: '{{ name }}'
addLabel: Add New Module...
fields:
- name: Icon
- name: $t:field_options.directus_roles.fields.icon_name
field: icon
type: string
meta:
interface: select-icon
width: half
- name: Name
- name: $t:field_options.directus_roles.fields.name_name
field: name
type: string
meta:
@@ -74,8 +74,8 @@ fields:
width: half
options:
iconRight: title
placeholder: Enter a title...
- name: Link
placeholder:
- name: $t:field_options.directus_roles.fields.link_name
field: link
type: string
meta:
@@ -83,7 +83,7 @@ fields:
width: full
options:
iconRight: link
placeholder: Relative or absolute URL...
placeholder: $t:field_options.directus_roles.fields.link_placeholder
special: json
width: full
@@ -91,9 +91,9 @@ fields:
interface: list
options:
template: '{{ group_name }}'
addLabel: Add New Group...
addLabel: $t:field_options.directus_roles.collection_list.group_name_addLabel
fields:
- name: Group Name
- name: $t:field_options.directus_roles.collection_list.fields.group_name
field: group_name
type: string
meta:
@@ -101,10 +101,10 @@ fields:
interface: input
options:
iconRight: title
placeholder: Label this group...
placeholder: $t:field_options.directus_roles.collection_list.fields.group_placeholder
schema:
is_nullable: false
- name: Type
- name: $t:field_options.directus_roles.collection_list.fields.type_name
field: accordion
type: string
schema:
@@ -115,21 +115,21 @@ fields:
options:
choices:
- value: always_open
text: Always Open
text: $t:field_options.directus_roles.collection_list.fields.choices_always
- value: start_open
text: Start Open
text: $t:field_options.directus_roles.collection_list.fields.choices_start_open
- value: start_collapsed
text: Start Collapsed
- name: Collections
text: $t:field_options.directus_roles.collection_list.fields.choices_start_collapsed
- name: $t:field_options.directus_roles.collections_name
field: collections
type: JSON
meta:
interface: list
options:
addLabel: Add New Collection...
addLabel: $t:field_options.directus_roles.collections_addLabel
template: '{{ collection }}'
fields:
- name: Collection
- name: $t:field_options.directus_roles.collections_name
field: collection
type: string
meta:

View File

@@ -8,7 +8,7 @@ fields:
interface: input
options:
iconRight: title
placeholder: My project...
placeholder: $t:field_options.directus_settings.project_name_placeholder
translations:
language: en-US
translations: Name
@@ -26,7 +26,7 @@ fields:
- field: project_color
interface: select-color
note: Login & Logo Background
note: $t:field_options.directus_settings.project_logo_note
translations:
language: en-US
translations: Brand Color
@@ -44,7 +44,7 @@ fields:
interface: presentation-divider
options:
icon: public
title: Public Pages
title: $t:fields.directus_settings.public_pages
special:
- alias
- no-data
@@ -67,14 +67,14 @@ fields:
- field: public_note
interface: input-multiline
options:
placeholder: A short, public message that supports markdown formatting...
placeholder: $t:field_options.directus_settings.public_note_placeholder
width: full
- field: security_divider
interface: presentation-divider
options:
icon: security
title: Security
title: $t:security
special:
- alias
- no-data
@@ -85,11 +85,11 @@ fields:
options:
choices:
- value: null
text: None  Not Recommended
text: $t:field_options.directus_settings.auth_password_policy.none_text
- value: '/^.{8,}$/'
text: Weak Minimum 8 Characters
text: $t:field_options.directus_settings.auth_password_policy.weak_text
- value: "/(?=^.{8,}$)(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*()_+}{';'?>.<,])(?!.*\\s).*$/"
text: Strong Upper / Lowercase / Numbers / Special
text: $t:field_options.directus_settings.auth_password_policy.strong_text
allowOther: true
width: half
@@ -104,7 +104,7 @@ fields:
interface: presentation-divider
options:
icon: storage
title: Files & Thumbnails
title: $t:fields.directus_settings.files_and_thumbnails
special:
- alias
- no-data
@@ -115,7 +115,7 @@ fields:
options:
fields:
- field: key
name: Key
name: $t:key
type: string
schema:
is_nullable: false
@@ -124,7 +124,7 @@ fields:
options:
slug: true
onlyOnCreate: false
width: half
width: full
- field: fit
name: Fit
type: string
@@ -135,16 +135,16 @@ fields:
options:
choices:
- value: contain
text: Contain (preserve aspect ratio)
text: $t:field_options.directus_settings.storage_asset_presets.fit.contain_text
- value: cover
text: Cover (forces exact size)
text: $t:field_options.directus_settings.storage_asset_presets.fit.cover_text
- value: inside
text: Fit inside
text: $t:field_options.directus_settings.storage_asset_presets.fit.fit_text
- value: outside
text: Fit outside
text: $t:field_options.directus_settings.storage_asset_presets.fit.outside_text
width: half
- field: width
name: Width
name: $t:width
type: integer
schema:
is_nullable: false
@@ -152,7 +152,7 @@ fields:
interface: input
width: half
- field: height
name: Height
name: $t:height
type: integer
schema:
is_nullable: false
@@ -161,7 +161,7 @@ fields:
width: half
- field: quality
type: integer
name: Quality
name: $t:quality
schema:
default_value: 80
is_nullable: false
@@ -173,6 +173,7 @@ fields:
step: 1
width: half
- field: withoutEnlargement
name: Upscaling
type: boolean
schema:
default_value: false
@@ -180,7 +181,51 @@ fields:
interface: boolean
width: half
options:
label: Don't upscale images
label: $t:no_upscale
- field: format
name: Format
type: string
schema:
is_nullable: false
default_value: ''
meta:
interface: select-dropdown
options:
allowNone: true
choices:
- value: jpeg
text: JPEG
- value: png
text: PNG
- value: webp
text: WebP
- value: tiff
text: Tiff
width: half
- field: transforms
name: $t:field_options.directus_settings.additional_transforms
type: json
schema:
is_nullable: false
default_value: []
meta:
note: $t:field_options.directus_settings.transforms_note
interface: json
options:
template: >
[
["blur", 45],
["grayscale"],
["extend", { "right": 500, "background": "rgb(255, 0, 0)" }]
]
placeholder: >
[
["blur", 45],
["grayscale"],
["extend", { "right": 500, "background": "rgb(255, 0, 0)" }]
]
width: full
template: '{{key}}'
special: json
width: full
@@ -190,18 +235,23 @@ fields:
options:
choices:
- value: all
text: All
text: $t:all
- value: none
text: None
text: $t:none
- value: presets
text: Presets Only
text: $t:presets_only
width: half
- field: storage_default_folder
interface: system-folder
width: half
note: $t:interfaces.system-folder.field_hint
- field: overrides_divider
interface: presentation-divider
options:
icon: brush
title: App Overrides
title: $t:fields.directus_settings.overrides
special:
- alias
- no-data
@@ -213,3 +263,72 @@ fields:
language: css
lineNumber: true
width: full
- field: map_divider
interface: presentation-divider
options:
icon: map
title: $t:maps
special:
- alias
- no-data
width: full
- field: mapbox_key
interface: input
options:
icon: key
title: $t:field_options.directus_settings.mapbox_key
placeholder: $t:field_options.directus_settings.mapbox_placeholder
iconLeft: vpn_key
font: monospace
width: half
- field: basemaps
interface: list
special: json
options:
template: '{{name}}'
fields:
- field: name
name: $t:name
schema:
is_nullable: false
meta:
interface: text-input
options:
placeholder: Enter the basemap name...
- field: type
name: $t:type
meta:
interface: select-dropdown
options:
choices:
- value: raster
text: $t:field_options.directus_settings.basemaps_raster
- value: tile
text: $t:field_options.directus_settings.basemaps_tile
- value: style
text: $t:field_options.directus_settings.basemaps_style
- field: url
name: $t:url
schema:
is_nullable: false
meta:
interface: text-input
options:
placeholder: http://{a-c}.tile.openstreetmap.org/{z}/{x}/{y}.png
- field: tileSize
name: $t:tile_size
schema:
is_nullable: true
meta:
interface: input
options:
placeholder: '512'
conditions:
- name: typeNeqRaster
rule:
type:
_neq: 'raster'
hidden: true

View File

@@ -64,7 +64,7 @@ fields:
interface: presentation-divider
options:
icon: face
title: User Preferences
title: $t:fields.directus_users.user_preferences
special:
- alias
- no-data
@@ -79,11 +79,11 @@ fields:
options:
choices:
- value: auto
text: Automatic (Based on System)
text: $t:fields.directus_users.theme_auto
- value: light
text: Light Mode
text: $t:fields.directus_users.theme_light
- value: dark
text: Dark Mode
text: $t:fields.directus_users.theme_dark
width: half
- field: tfa_secret
@@ -95,7 +95,7 @@ fields:
interface: presentation-divider
options:
icon: verified_user
title: Admin Options
title: $t:fields.directus_users.admin_options
color: '#E35169'
special:
- alias
@@ -106,15 +106,15 @@ fields:
interface: select-dropdown
options:
choices:
- text: Draft
- text: $t:fields.directus_users.status_draft
value: draft
- text: Invited
- text: $t:fields.directus_users.status_invited
value: invited
- text: Active
- text: $t:fields.directus_users.status_active
value: active
- text: Suspended
- text: $t:fields.directus_users.status_suspended
value: suspended
- text: Archived
- text: $t:fields.directus_users.status_archived
value: archived
width: half
@@ -132,7 +132,7 @@ fields:
interface: token
options:
iconRight: vpn_key
placeholder: Enter a secure access token...
placeholder: $t:fields.directus_users.token_placeholder
width: full
- field: id

View File

@@ -38,26 +38,26 @@ fields:
defaultBackground: 'var(--background-normal-alt)'
showAsDot: true
choices:
- text: Active
- text: $t:active
value: active
foreground: 'var(--primary-10)'
background: 'var(--primary)'
- text: Inactive
- text: $t:inactive
value: inactive
foreground: 'var(--foreground-normal)'
background: 'var(--background-normal-alt)'
options:
choices:
- text: Active
- text: $t:active
value: active
- text: Inactive
- text: $t:inactive
value: inactive
width: half
- field: data
interface: boolean
options:
label: Send Event Data
label: $t:fields.directus_webhooks.data_label
special: boolean
width: half
display: boolean
@@ -66,7 +66,7 @@ fields:
interface: presentation-divider
options:
icon: api
title: Triggers
title: $t:fields.directus_webhooks.triggers
special:
- alias
- no-data
@@ -76,11 +76,11 @@ fields:
interface: select-multiple-checkbox
options:
choices:
- text: Create
- text: $t:create
value: create
- text: Update
- text: $t:update
value: update
- text: Delete
- text: $t:delete_label
value: delete
special: csv
width: full
@@ -89,19 +89,19 @@ fields:
defaultForeground: 'var(--foreground-normal)'
defaultBackground: 'var(--background-normal-alt)'
choices:
- text: Create
- text: $t:create
value: create
foreground: 'var(--primary)'
background: 'var(--primary-25)'
- text: Update
- text: $t:update
value: update
foreground: 'var(--blue)'
background: 'var(--blue-25)'
- text: Delete
- text: $t:delete_label
value: delete
foreground: 'var(--danger)'
background: 'var(--danger-25)'
- text: Login
- text: $t:login
value: authenticate
foreground: 'var(--purple)'
background: 'var(--purple-25)'