Merge branch 'main' into assets

This commit is contained in:
rijkvanzanten
2020-10-29 16:03:04 -04:00
91 changed files with 2857 additions and 772 deletions

View File

@@ -46,15 +46,13 @@ router.post(
const ip = req.ip;
const userAgent = req.get('user-agent');
const { accessToken, refreshToken, expires, id } = await authenticationService.authenticate(
{
ip,
userAgent,
email,
password,
otp,
}
);
const { accessToken, refreshToken, expires } = await authenticationService.authenticate({
ip,
userAgent,
email,
password,
otp,
});
const payload = {
data: { access_token: accessToken, expires },

View File

@@ -0,0 +1,55 @@
import Knex from 'knex';
export async function up(knex: Knex) {
await knex.schema.alterTable('directus_fields', (table) => {
table.dropForeign(['collection']);
});
await knex.schema.alterTable('directus_activity', (table) => {
table.dropForeign(['collection']);
});
await knex.schema.alterTable('directus_permissions', (table) => {
table.dropForeign(['collection']);
});
await knex.schema.alterTable('directus_presets', (table) => {
table.dropForeign(['collection']);
});
await knex.schema.alterTable('directus_relations', (table) => {
table.dropForeign(['one_collection']);
table.dropForeign(['many_collection']);
});
await knex.schema.alterTable('directus_revisions', (table) => {
table.dropForeign(['collection']);
});
}
export async function down(knex: Knex) {
await knex.schema.alterTable('directus_fields', (table) => {
table.foreign('collection').references('directus_collections.collection');
});
await knex.schema.alterTable('directus_activity', (table) => {
table.foreign('collection').references('directus_collections.collection');
});
await knex.schema.alterTable('directus_permissions', (table) => {
table.foreign('collection').references('directus_collections.collection');
});
await knex.schema.alterTable('directus_presets', (table) => {
table.foreign('collection').references('directus_collections.collection');
});
await knex.schema.alterTable('directus_relations', (table) => {
table.foreign('one_collection').references('directus_collections.collection');
table.foreign('many_collection').references('directus_collections.collection');
});
await knex.schema.alterTable('directus_revisions', (table) => {
table.foreign('collection').references('directus_collections.collection');
});
}

View File

@@ -0,0 +1,128 @@
import Knex from 'knex';
import { merge } from 'lodash';
export async function up(knex: Knex) {
await knex('directus_relations')
.delete()
.where('many_collection', 'like', 'directus_%')
.andWhere('one_collection', 'like', 'directus_%');
}
export async function down(knex: Knex) {
const defaults = {
many_collection: 'directus_users',
many_field: null,
many_primary: null,
one_collection: null,
one_field: null,
one_primary: null,
junction_field: null,
};
const systemRelations = [
{
many_collection: 'directus_users',
many_field: 'role',
many_primary: 'id',
one_collection: 'directus_roles',
one_field: 'users',
one_primary: 'id',
},
{
many_collection: 'directus_users',
many_field: 'avatar',
many_primary: 'id',
one_collection: 'directus_files',
one_primary: 'id',
},
{
many_collection: 'directus_revisions',
many_field: 'activity',
many_primary: 'id',
one_collection: 'directus_activity',
one_field: 'revisions',
one_primary: 'id',
},
{
many_collection: 'directus_presets',
many_field: 'user',
many_primary: 'id',
one_collection: 'directus_users',
one_primary: 'id',
},
{
many_collection: 'directus_presets',
many_field: 'role',
many_primary: 'id',
one_collection: 'directus_roles',
one_primary: 'id',
},
{
many_collection: 'directus_folders',
many_field: 'parent',
many_primary: 'id',
one_collection: 'directus_folders',
one_primary: 'id',
},
{
many_collection: 'directus_files',
many_field: 'folder',
many_primary: 'id',
one_collection: 'directus_folders',
one_primary: 'id',
},
{
many_collection: 'directus_files',
many_field: 'uploaded_by',
many_primary: 'id',
one_collection: 'directus_users',
one_primary: 'id',
},
{
many_collection: 'directus_fields',
many_field: 'collection',
many_primary: 'id',
one_collection: 'directus_collections',
one_field: 'fields',
one_primary: 'collection',
},
{
many_collection: 'directus_activity',
many_field: 'user',
many_primary: 'id',
one_collection: 'directus_users',
one_primary: 'id',
},
{
many_collection: 'directus_settings',
many_field: 'project_logo',
many_primary: 'id',
one_collection: 'directus_files',
one_primary: 'id',
},
{
many_collection: 'directus_settings',
many_field: 'public_foreground',
many_primary: 'id',
one_collection: 'directus_files',
one_primary: 'id',
},
{
many_collection: 'directus_settings',
many_field: 'public_background',
many_primary: 'id',
one_collection: 'directus_files',
one_primary: 'id',
},
].map((row) => {
for (const [key, value] of Object.entries(row)) {
if (value !== null && (typeof value === 'object' || Array.isArray(value))) {
(row as any)[key] = JSON.stringify(value);
}
}
return merge({}, defaults, row);
});
await knex.insert(systemRelations).into('directus_relations');
}

View File

@@ -0,0 +1,99 @@
import Knex from 'knex';
import { merge } from 'lodash';
export async function up(knex: Knex) {
await knex('directus_collections').delete().where('collection', 'like', 'directus_%');
}
export async function down(knex: Knex) {
const defaults = {
collection: null,
hidden: false,
singleton: false,
icon: null,
note: null,
translations: null,
display_template: null,
};
const systemCollections = [
{
collection: 'directus_activity',
note: 'Accountability logs for all events',
},
{
collection: 'directus_collections',
icon: 'list_alt',
note: 'Additional collection configuration and metadata',
},
{
collection: 'directus_fields',
icon: 'input',
note: 'Additional field configuration and metadata',
},
{
collection: 'directus_files',
icon: 'folder',
note: 'Metadata for all managed file assets',
},
{
collection: 'directus_folders',
note: 'Provides virtual directories for files',
},
{
collection: 'directus_permissions',
icon: 'admin_panel_settings',
note: 'Access permissions for each role',
},
{
collection: 'directus_presets',
icon: 'bookmark_border',
note: 'Presets for collection defaults and bookmarks',
},
{
collection: 'directus_relations',
icon: 'merge_type',
note: 'Relationship configuration and metadata',
},
{
collection: 'directus_revisions',
note: 'Data snapshots for all activity',
},
{
collection: 'directus_roles',
icon: 'supervised_user_circle',
note: 'Permission groups for system users',
},
{
collection: 'directus_sessions',
note: 'User session information',
},
{
collection: 'directus_settings',
singleton: true,
note: 'Project configuration options',
},
{
collection: 'directus_users',
archive_field: 'status',
archive_value: 'archived',
unarchive_value: 'draft',
icon: 'people_alt',
note: 'System users for the platform',
},
{
collection: 'directus_webhooks',
note: 'Configuration for event-based HTTP requests',
},
].map((row) => {
for (const [key, value] of Object.entries(row)) {
if (value !== null && (typeof value === 'object' || Array.isArray(value))) {
(row as any)[key] = JSON.stringify(value);
}
}
return merge({}, defaults, row);
});
await knex.insert(systemCollections).into('directus_collections');
}

File diff suppressed because it is too large Load Diff

View File

@@ -164,7 +164,7 @@ async function getDBQuery(
query.sort = query.sort || [{ column: primaryKeyField, order: 'asc' }];
await applyQuery(table, dbQuery, queryCopy);
await applyQuery(knex, table, dbQuery, queryCopy);
return dbQuery;
}

View File

@@ -1,84 +0,0 @@
table: directus_presets
defaults:
bookmark: null
user: null
role: null
collection: null
search: null
filters: '[]'
layout: tabular
layout_query: null
layout_options: null
data:
- collection: directus_files
layout: cards
layout_query:
cards:
sort: -uploaded_on
layout_options:
cards:
icon: insert_drive_file
title: '{{ title }}'
subtitle: '{{ type }} • {{ filesize }}'
size: 4
imageFit: crop
- collection: directus_users
layout: cards
layout_options:
cards:
icon: account_circle
title: '{{ first_name }} {{ last_name }}'
subtitle: '{{ email }}'
size: 4
- collection: directus_activity
layout: tabular
layout_query:
tabular:
sort: -timestamp
fields:
- action
- collection
- timestamp
- user
layout_options:
tabular:
widths:
action: 100
collection: 210
timestamp: 240
user: 240
- collection: directus_webhooks
layout: tabular
layout_query:
tabular:
fields:
- status
- name
- method
- url
layout_options:
tabular:
widths:
status: 36
name: 300
- collection: directus_roles
layout: tabular
layout_query:
tabular:
fields:
- icon
- name
- description
layout_options:
tabular:
widths:
icon: 36
name: 248
description: 500

View File

@@ -1,39 +0,0 @@
# directus_fields isn't surfaced in the app
table: directus_fields
fields:
- collection: directus_fields
field: options
hidden: true
locked: true
special: json
- collection: directus_fields
field: display_options
hidden: true
locked: true
special: json
- collection: directus_fields
field: locked
hidden: true
locked: true
special: boolean
- collection: directus_fields
field: readonly
hidden: true
locked: true
special: boolean
- collection: directus_fields
field: hidden
hidden: true
locked: true
special: boolean
- collection: directus_fields
field: special
hidden: true
locked: true
special: csv
- collection: directus_fields
field: translations
hidden: true
locked: true
special: json

View File

@@ -1,8 +0,0 @@
table: directus_folders
fields:
- collection: directus_folders
field: id
interface: text-input
locked: true
special: uuid

View File

@@ -1,14 +0,0 @@
# directus_permissions isn't surfaced in the app
table: directus_permissions
fields:
- collection: directus_permissions
field: permissions
hidden: true
locked: true
special: json
- collection: directus_permissions
field: presets
hidden: true
locked: true
special: json

View File

@@ -1,19 +0,0 @@
table: directus_presets
fields:
# directus_presets isn't surfaced in the app
- collection: directus_presets
field: filters
hidden: true
locked: true
special: json
- collection: directus_presets
field: layout_query
hidden: true
locked: true
special: json
- collection: directus_presets
field: layout_options
hidden: true
locked: true
special: json

View File

@@ -1,14 +0,0 @@
table: directus_revisions
fields:
# directus_revisions isn't surfaced in the app
- collection: directus_revisions
field: data
hidden: true
locked: true
special: json
- collection: directus_revisions
field: delta
hidden: true
locked: true
special: json

View File

@@ -1,7 +0,0 @@
table: directus_relations
fields:
- collection: directus_relations
field: one_allowed_collections
locked: true
special: csv

View File

@@ -7,9 +7,6 @@ columns:
type: string
length: 64
nullable: false
references:
table: directus_collections
column: collection
field:
type: string
length: 64

View File

@@ -25,9 +25,6 @@ columns:
type: string
length: 64
nullable: false
references:
table: directus_collections
column: collection
item:
type: string
length: 255

View File

@@ -12,9 +12,6 @@ columns:
type: string
length: 64
nullable: false
references:
table: directus_collections
column: collection
action:
type: string
length: 10

View File

@@ -19,9 +19,6 @@ columns:
collection:
type: string
length: 64
references:
table: directus_collections
column: collection
search:
type: string
length: 100

View File

@@ -7,9 +7,6 @@ columns:
type: string
length: 64
nullable: false
references:
table: directus_collections
column: collection
many_field:
type: string
length: 64
@@ -21,9 +18,6 @@ columns:
one_collection:
type: string
length: 64
references:
table: directus_collections
column: collection
one_field:
type: string
length: 64

View File

@@ -25,33 +25,6 @@ type TableSeed = {
};
};
type RowSeed = {
table: string;
defaults: Record<string, any>;
data: Record<string, any>[];
};
type FieldSeed = {
table: string;
fields: {
collection: string;
field: string;
special: string | null;
interface: string | null;
options: Record<string, any> | null;
display: string | null;
display_options: Record<string, any> | null;
locked: boolean;
readonly: boolean;
hidden: boolean;
sort: number | null;
width: string | null;
group: number | null;
translations: Record<string, any> | null;
note: string | null;
}[];
};
export default async function runSeed(database: Knex) {
const exists = await database.schema.hasTable('directus_collections');
@@ -59,19 +32,13 @@ export default async function runSeed(database: Knex) {
throw new Error('Database is already installed');
}
await createTables(database);
await insertRows(database);
await insertFields(database);
}
async function createTables(database: Knex) {
const tableSeeds = await fse.readdir(path.resolve(__dirname, './01-tables/'));
const tableSeeds = await fse.readdir(path.resolve(__dirname));
for (const tableSeedFile of tableSeeds) {
const yamlRaw = await fse.readFile(
path.resolve(__dirname, './01-tables', tableSeedFile),
'utf8'
);
if (tableSeedFile === 'run.ts') continue;
const yamlRaw = await fse.readFile(path.resolve(__dirname, tableSeedFile), 'utf8');
const seedData = yaml.safeLoad(yamlRaw) as TableSeed;
await database.schema.createTable(seedData.table, (tableBuilder) => {
@@ -128,61 +95,3 @@ async function createTables(database: Knex) {
});
}
}
async function insertRows(database: Knex) {
const rowSeeds = await fse.readdir(path.resolve(__dirname, './02-rows/'));
for (const rowSeedFile of rowSeeds) {
const yamlRaw = await fse.readFile(
path.resolve(__dirname, './02-rows', rowSeedFile),
'utf8'
);
const seedData = yaml.safeLoad(yamlRaw) as RowSeed;
const dataWithDefaults = seedData.data.map((row) => {
for (const [key, value] of Object.entries(row)) {
if (value !== null && (typeof value === 'object' || Array.isArray(value))) {
row[key] = JSON.stringify(value);
}
}
return merge({}, seedData.defaults, row);
});
await database.batchInsert(seedData.table, dataWithDefaults);
}
}
async function insertFields(database: Knex) {
const fieldSeeds = await fse.readdir(path.resolve(__dirname, './03-fields/'));
const defaultsYaml = await fse.readFile(
path.resolve(__dirname, './03-fields/_defaults.yaml'),
'utf8'
);
const defaults = yaml.safeLoad(defaultsYaml) as FieldSeed;
for (const fieldSeedFile of fieldSeeds) {
const yamlRaw = await fse.readFile(
path.resolve(__dirname, './03-fields', fieldSeedFile),
'utf8'
);
const seedData = yaml.safeLoad(yamlRaw) as FieldSeed;
if (fieldSeedFile === '_defaults.yaml') {
continue;
}
const dataWithDefaults = seedData.fields.map((row) => {
for (const [key, value] of Object.entries(row)) {
if (value !== null && (typeof value === 'object' || Array.isArray(value))) {
(row as any)[key] = JSON.stringify(value);
}
}
return merge({}, defaults, row);
});
await database.batchInsert('directus_fields', dataWithDefaults);
}
}

View File

@@ -23,6 +23,8 @@ data:
note: Metadata for all managed file assets
- collection: directus_folders
note: Provides virtual directories for files
- collection: directus_migrations
note: What version of the database you're using
- collection: directus_permissions
icon: admin_panel_settings
note: Access permissions for each role

View File

@@ -0,0 +1,11 @@
import { requireYAML } from '../../../utils/require-yaml';
import { merge } from 'lodash';
import { CollectionMeta } from '../../../types';
const systemData = requireYAML(require.resolve('./collections.yaml'));
export const systemCollectionRows: CollectionMeta[] = systemData.data.map(
(row: Record<string, any>) => {
return merge({ system: true }, systemData.defaults, row);
}
);

View File

@@ -1,8 +1,13 @@
table: directus_activity
fields:
- collection: directus_activity
field: action
- field: id
width: half
- field: item
width: half
- field: action
display: labels
display_options:
defaultForeground: '#263238'
@@ -24,41 +29,47 @@ fields:
value: authenticate
foreground: '#9b51e0'
background: '#e6d3f7'
- collection: directus_activity
field: collection
width: half
- field: collection
display: collection
display_options:
icon: true
- collection: directus_activity
field: timestamp
width: half
- field: timestamp
display: datetime
options:
relative: true
- collection: directus_activity
field: user
width: half
- field: user
display: user
- collection: directus_activity
field: comment
width: half
- field: comment
display: formatted-text
display_options:
subdued: true
- collection: directus_activity
field: user_agent
width: half
- field: user_agent
display: formatted-text
display_options:
font: monospace
- collection: directus_activity
field: ip
width: half
- field: ip
display: formatted-text
display_options:
font: monospace
- collection: directus_activity
field: revisions
width: half
- field: revisions
interface: one-to-many
locked: true
special: o2m
options:
fields:
- collection
- item
width: full
width: half

View File

@@ -1,69 +1,54 @@
table: directus_collections
fields:
- collection: directus_collections
field: collection_divider
- field: collection_divider
special: alias
interface: divider
options:
icon: box
title: Collection Setup
color: '#2F80ED'
locked: true
sort: 1
width: full
- collection: directus_collections
field: collection
- field: collection
interface: text-input
options:
font: monospace
locked: true
readonly: true
sort: 2
width: half
- collection: directus_collections
field: icon
- field: icon
interface: icon
options:
locked: true
sort: 3
width: half
- collection: directus_collections
field: note
- field: note
interface: text-input
options:
placeholder: A description of this collection...
locked: true
sort: 4
width: full
- collection: directus_collections
field: display_template
- field: display_template
interface: display-template
options:
collectionField: collection
locked: true
sort: 5
width: full
- collection: directus_collections
field: hidden
- field: hidden
special: boolean
interface: toggle
options:
label: Hide within the App
locked: true
sort: 6
width: half
- collection: directus_collections
field: singleton
- field: singleton
special: boolean
interface: toggle
options:
label: Treat as single object
locked: true
sort: 7
width: half
- collection: directus_collections
field: translations
- field: translations
special: json
interface: repeater
options:
@@ -85,72 +70,58 @@ fields:
width: half
options:
placeholder: Enter a translation...
locked: true
sort: 8
width: full
- collection: directus_collections
field: archive_divider
- field: archive_divider
special: alias
interface: divider
options:
icon: archive
title: Archive
color: '#2F80ED'
locked: true
sort: 9
width: full
- collection: directus_collections
field: archive_field
- field: archive_field
interface: field
options:
collectionField: collection
allowNone: true
placeholder: Choose a field...
locked: true
sort: 10
width: half
- collection: directus_collections
field: archive_app_filter
- field: archive_app_filter
interface: toggle
special: boolean
options:
label: Enable App Archive Filter
locked: true
sort: 11
width: half
- collection: directus_collections
field: archive_value
- field: archive_value
interface: text-input
options:
font: monospace
iconRight: archive
placeholder: Value set when archiving...
locked: true
sort: 12
width: half
- collection: directus_collections
field: unarchive_value
- field: unarchive_value
interface: text-input
options:
font: monospace
iconRight: unarchive
placeholder: Value set when unarchiving...
locked: true
sort: 13
width: half
- collection: directus_collections
field: sort_divider
- field: sort_divider
special: alias
interface: divider
options:
icon: sort
title: Sort
color: '#2F80ED'
locked: true
sort: 14
width: full
- collection: directus_collections
field: sort_field
- field: sort_field
interface: field
options:
collectionField: collection
@@ -160,6 +131,4 @@ fields:
- decimal
- integer
allowNone: true
locked: true
sort: 15
width: half

View File

@@ -0,0 +1,81 @@
# directus_fields isn't surfaced in the app
table: directus_fields
fields:
- collection: directus_fields
field: id
width: half
- collection: directus_fields
field: collection
width: half
- collection: directus_fields
field: field
width: half
- collection: directus_fields
field: special
hidden: true
special: csv
width: half
- collection: directus_fields
field: interface
width: half
- collection: directus_fields
field: options
hidden: true
special: json
width: half
- collection: directus_fields
field: display
width: half
- collection: directus_fields
field: display_options
hidden: true
special: json
width: half
- collection: directus_fields
field: locked
hidden: true
special: boolean
width: half
- collection: directus_fields
field: readonly
hidden: true
special: boolean
width: half
- collection: directus_fields
field: hidden
hidden: true
special: boolean
width: half
- collection: directus_fields
field: sort
width: half
- collection: directus_fields
field: width
width: half
- collection: directus_fields
field: group
width: half
- collection: directus_fields
field: translations
hidden: true
special: json
width: half
- collection: directus_fields
field: note
width: half

View File

@@ -1,114 +1,117 @@
table: directus_files
fields:
- collection: directus_files
field: id
- field: id
hidden: true
interface: text-input
locked: true
special: uuid
- collection: directus_files
field: title
- field: title
interface: text-input
locked: true
options:
iconRight: title
placeholder: A unique title...
sort: 1
width: full
- collection: directus_files
field: description
- field: description
interface: textarea
locked: true
sort: 2
width: full
options:
placeholder: An optional description...
- collection: directus_files
field: tags
- field: tags
interface: tags
locked: true
options:
iconRight: local_offer
special: json
sort: 3
width: full
display: tags
- collection: directus_files
field: location
- field: location
interface: text-input
locked: true
options:
iconRight: place
placeholder: An optional location...
sort: 4
width: half
- collection: directus_files
field: storage
- field: storage
interface: text-input
locked: true
options:
iconRight: storage
sort: 5
width: half
readonly: true
- collection: directus_files
field: storage_divider
- field: storage_divider
interface: divider
locked: true
options:
icon: insert_drive_file
title: File Naming
color: '#2F80ED'
special: alias
sort: 6
width: full
- collection: directus_files
field: filename_disk
- field: filename_disk
interface: text-input
locked: true
options:
iconRight: publish
placeholder: Name on disk storage...
sort: 7
width: half
- collection: directus_files
field: filename_download
- field: filename_download
interface: text-input
locked: true
options:
iconRight: get_app
placeholder: Name when downloading...
sort: 8
width: half
- collection: directus_files
field: metadata
- field: metadata
hidden: true
locked: true
special: json
- collection: directus_files
field: type
- field: type
display: mime-type
- collection: directus_files
field: filesize
- field: filesize
display: filesize
- collection: directus_files
field: modified_by
- field: modified_by
interface: user
locked: true
special: user-updated
width: half
display: user
- collection: directus_files
field: modified_on
- field: modified_on
interface: datetime
locked: true
special: date-updated
width: half
display: datetime
- collection: directus_files
field: created_on
- field: created_on
display: datetime
- collection: directus_files
field: created_by
- field: created_by
display: user
- field: embed
width: half
- field: uploaded_by
width: half
- field: folder
width: half
- field: width
width: half
- field: uploaded_on
width: half
- field: height
width: half
- field: charset
width: half
- field: duration
width: half

View File

@@ -0,0 +1,14 @@
table: directus_folders
fields:
- field: id
interface: text-input
special: uuid
width: half
- field: parent
width: half
- field: name
width: full

View File

@@ -0,0 +1,25 @@
import { requireYAML } from '../../../utils/require-yaml';
import { merge } from 'lodash';
import { FieldMeta } from '../../../types';
import fse from 'fs-extra';
import path from 'path';
const defaults = requireYAML(require.resolve('./_defaults.yaml'));
const fieldData = fse.readdirSync(path.resolve(__dirname));
export let systemFieldRows: FieldMeta[] = [];
for (const filepath of fieldData) {
if (['_defaults.yaml', 'index.ts'].includes(filepath)) continue;
const systemFields = requireYAML(path.resolve(__dirname, filepath));
(systemFields.fields as FieldMeta[]).forEach((field, index) => {
systemFieldRows.push(
merge({ system: true }, defaults, field, {
collection: systemFields.table,
sort: index + 1,
})
);
});
}

View File

@@ -0,0 +1,34 @@
# directus_permissions isn't surfaced in the app
table: directus_permissions
fields:
- field: permissions
hidden: true
special: json
width: half
- field: presets
hidden: true
special: json
width: half
- field: role
width: half
- field: limit
width: half
- field: collection
width: half
- field: id
width: half
- field: fields
width: half
- field: action
width: half
- field: validation
width: half

View File

@@ -0,0 +1,35 @@
table: directus_presets
fields:
- field: filters
hidden: true
special: json
- field: layout_query
hidden: true
special: json
- field: layout_options
hidden: true
special: json
- field: role
width: half
- field: user
width: half
- field: id
width: half
- field: bookmark
width: half
- field: search
width: half
- field: collection
width: half
- field: layout
width: half

View File

@@ -0,0 +1,33 @@
table: directus_relations
fields:
- field: id
width: half
- field: many_collection
width: half
- field: many_field
width: half
- field: many_primary
width: half
- field: one_collection
width: half
- field: one_field
width: half
- field: one_primary
width: half
- field: one_collection_field
width: half
- field: one_allowed_collections
special: csv
width: half
- field: junction_field
width: half

View File

@@ -0,0 +1,25 @@
table: directus_revisions
fields:
- field: id
width: half
- field: activity
width: half
- field: collection
width: half
- field: item
width: half
- field: data
hidden: true
special: json
- field: delta
hidden: true
special: json
- field: parent
width: half

View File

@@ -1,80 +1,61 @@
table: directus_roles
fields:
- collection: directus_roles
field: id
- field: id
hidden: true
interface: text-input
locked: true
special: uuid
- collection: directus_roles
field: name
- field: name
interface: text-input
options:
placeholder: The unique name for this role...
locked: true
sort: 1
width: half
- collection: directus_roles
field: icon
- field: icon
interface: icon
display: icon
locked: true
sort: 2
width: half
- collection: directus_roles
field: description
- field: description
interface: text-input
options:
placeholder: A description of this role...
locked: true
sort: 3
width: full
- collection: directus_roles
field: app_access
- field: app_access
interface: toggle
locked: true
special: boolean
sort: 4
width: half
- collection: directus_roles
field: admin_access
- field: admin_access
interface: toggle
locked: true
special: boolean
sort: 5
width: half
- collection: directus_roles
field: ip_access
- field: ip_access
interface: tags
options:
placeholder: Add allowed IP addresses, leave empty to allow all...
locked: true
special: csv
sort: 6
width: full
- collection: directus_roles
field: enforce_tfa
- field: enforce_tfa
interface: toggle
locked: true
sort: 7
special: boolean
width: half
- collection: directus_roles
field: users
- field: users
interface: one-to-many
locked: true
special: o2m
sort: 8
options:
fields:
- first_name
- last_name
width: full
- collection: directus_roles
field: module_list
- field: module_list
interface: repeater
locked: true
options:
template: '{{ name }}'
addLabel: Add New Module...
@@ -104,12 +85,10 @@ fields:
iconRight: link
placeholder: Relative or absolute URL...
special: json
sort: 9
width: full
- collection: directus_roles
field: collection_list
- field: collection_list
interface: repeater
locked: true
options:
template: '{{ group_name }}'
addLabel: Add New Group...
@@ -159,5 +138,4 @@ fields:
schema:
is_nullable: false
special: json
sort: 10
width: full

View File

@@ -0,0 +1,17 @@
table: directus_sessions
fields:
- field: token
width: half
- field: user
width: half
- field: expires
width: half
- field: ip
width: half
- field: user_agent
width: half

View File

@@ -1,102 +1,85 @@
table: directus_settings
fields:
- collection: directus_settings
field: project_name
- field: id
hidden: true
- field: project_name
interface: text-input
locked: true
options:
iconRight: title
placeholder: My project...
sort: 1
translations:
language: en-US
translations: Name
width: half
- collection: directus_settings
field: project_url
- field: project_url
interface: text-input
locked: true
options:
iconRight: link
placeholder: https://example.com
sort: 2
translations:
language: en-US
translations: Website
width: half
- collection: directus_settings
field: project_color
- field: project_color
interface: color
locked: true
note: Login & Logo Background
sort: 3
translations:
language: en-US
translations: Brand Color
width: half
- collection: directus_settings
field: project_logo
- field: project_logo
interface: file
locked: true
note: White 40x40 SVG/PNG
sort: 4
translations:
language: en-US
translations: Brand Logo
width: half
- collection: directus_settings
field: public_divider
- field: public_divider
interface: divider
locked: true
options:
icon: public
title: Public Pages
color: '#2F80ED'
special: alias
sort: 5
width: full
- collection: directus_settings
field: public_foreground
- field: public_foreground
interface: file
locked: true
sort: 6
translations:
language: en-US
translations: Login Foreground
width: half
- collection: directus_settings
field: public_background
- field: public_background
interface: file
locked: true
sort: 7
translations:
language: en-US
translations: Login Background
width: half
- collection: directus_settings
field: public_note
- field: public_note
interface: textarea
locked: true
options:
placeholder: A short, public message that supports markdown formatting...
sort: 8
width: full
- collection: directus_settings
field: security_divider
- field: security_divider
interface: divider
locked: true
options:
icon: security
title: Security
color: '#2F80ED'
special: alias
sort: 9
width: full
- collection: directus_settings
field: auth_password_policy
- field: auth_password_policy
interface: dropdown
locked: true
options:
choices:
- value: null
@@ -105,31 +88,25 @@ fields:
text: Weak Minimum 8 Characters
- value: "/(?=^.{8,}$)(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[!@#$%^&*()_+}{';'?>.<,])(?!.*\\s).*$/"
text: Strong Upper / Lowercase / Numbers / Special
sort: 10
width: half
- collection: directus_settings
field: auth_login_attempts
- field: auth_login_attempts
interface: numeric
locked: true
options:
iconRight: lock
sort: 11
width: half
- collection: directus_settings
field: files_divider
- field: files_divider
interface: divider
locked: true
options:
icon: storage
title: Files & Thumbnails
color: '#2F80ED'
special: alias
sort: 12
width: full
- collection: directus_settings
field: storage_asset_presets
- field: storage_asset_presets
interface: repeater
locked: true
options:
fields:
- field: key
@@ -202,12 +179,10 @@ fields:
label: Don't upscale images
template: '{{key}}'
special: json
sort: 13
width: full
- collection: directus_settings
field: storage_asset_transform
- field: storage_asset_transform
interface: dropdown
locked: true
options:
choices:
- value: all
@@ -216,29 +191,20 @@ fields:
text: None
- value: presets
text: Presets Only
sort: 14
width: half
- collection: directus_settings
field: id
hidden: true
locked: true
- collection: directus_settings
field: overrides_divider
- field: overrides_divider
interface: divider
locked: true
options:
icon: brush
title: App Overrides
color: '#2F80ED'
special: alias
sort: 15
width: full
- collection: directus_settings
field: custom_css
- field: custom_css
interface: code
locked: true
options:
language: css
lineNumber: true
sort: 16
width: full

View File

@@ -1,87 +1,70 @@
table: directus_users
fields:
- collection: directus_users
field: first_name
- field: first_name
interface: text-input
locked: true
options:
iconRight: account_circle
sort: 1
width: half
- collection: directus_users
field: last_name
- field: last_name
interface: text-input
locked: true
options:
iconRight: account_circle
sort: 2
width: half
- collection: directus_users
field: email
- field: email
interface: text-input
locked: true
options:
iconRight: email
sort: 3
width: half
- collection: directus_users
field: password
- field: password
special: hash, conceal
interface: hash
locked: true
options:
iconRight: lock
masked: true
sort: 4
width: half
- collection: directus_users
field: avatar
- field: avatar
interface: file
locked: true
sort: 5
width: full
- collection: directus_users
field: location
- field: location
interface: text-input
options:
iconRight: place
sort: 6
width: half
- collection: directus_users
field: title
- field: title
interface: text-input
options:
iconRight: work
sort: 7
width: half
- collection: directus_users
field: description
- field: description
interface: textarea
sort: 8
width: full
- collection: directus_users
field: tags
- field: tags
interface: tags
special: json
sort: 9
width: full
options:
iconRight: local_offer
- collection: directus_users
field: preferences_divider
- field: preferences_divider
interface: divider
options:
icon: face
title: User Preferences
color: '#2F80ED'
special: alias
sort: 10
width: full
- collection: directus_users
field: language
- field: language
interface: dropdown
locked: true
options:
choices:
- text: Afrikaans (South Africa)
@@ -146,12 +129,10 @@ fields:
value: uk-UA
- text: Vietnamese (Vietnam)
value: vi-VN
sort: 11
width: half
- collection: directus_users
field: theme
- field: theme
interface: dropdown
locked: true
options:
choices:
- value: auto
@@ -160,30 +141,24 @@ fields:
text: Light Mode
- value: dark
text: Dark Mode
sort: 12
width: half
- collection: directus_users
field: tfa_secret
- field: tfa_secret
interface: tfa-setup
locked: true
special: conceal
sort: 13
width: half
- collection: directus_users
field: admin_divider
- field: admin_divider
interface: divider
locked: true
options:
icon: verified_user
title: Admin Options
color: '#F2994A'
special: alias
sort: 14
width: full
- collection: directus_users
field: status
- field: status
interface: dropdown
locked: true
options:
choices:
- text: Draft
@@ -196,32 +171,31 @@ fields:
value: suspended
- text: Archived
value: archived
sort: 15
width: half
- collection: directus_users
field: role
- field: role
interface: many-to-one
locked: true
options:
template: '{{ name }}'
special: m2o
sort: 16
width: half
- collection: directus_users
field: token
- field: token
interface: token
locked: true
options:
iconRight: vpn_key
placeholder: Enter a secure access token...
sort: 17
width: full
- collection: directus_users
field: id
- field: id
special: uuid
interface: text-input
locked: true
options:
iconRight: vpn_key
sort: 18
width: full
- field: last_page
width: half
- field: last_access
width: half

View File

@@ -1,43 +1,35 @@
table: directus_webhooks
fields:
- collection: directus_webhooks
field: id
- field: id
hidden: true
locked: true
- collection: directus_webhooks
field: name
- field: name
interface: text-input
locked: true
options:
iconRight: title
sort: 1
width: full
- collection: directus_webhooks
field: method
- field: method
interface: dropdown
display: labels
display_options:
defaultBackground: "#ECEFF1"
choices: null
format: false
locked: true
options:
choices:
- GET
- POST
sort: 2
width: half
- collection: directus_webhooks
field: url
- field: url
interface: text-input
locked: true
options:
iconRight: link
sort: 3
width: half
- collection: directus_webhooks
field: status
- field: status
interface: dropdown
display: labels
display_options:
@@ -53,36 +45,31 @@ fields:
value: inactive
foreground: "#607D8B"
background: "#ECEFF1"
locked: true
options:
choices:
- text: Active
value: active
- text: Inactive
value: inactive
sort: 4
width: half
- collection: directus_webhooks
field: data
- field: data
interface: toggle
locked: true
options:
label: Send Event Data
special: boolean
sort: 5
width: half
- collection: directus_webhooks
field: triggers_divider
- field: triggers_divider
interface: divider
options:
icon: api
title: Triggers
color: '#2F80ED'
special: alias
sort: 6
width: full
- collection: directus_webhooks
field: actions
- field: actions
interface: checkboxes
options:
choices:
@@ -93,11 +80,9 @@ fields:
- text: Delete
value: delete
special: csv
sort: 7
width: full
- collection: directus_webhooks
field: collections
- field: collections
interface: collections
special: csv
sort: 8
width: full

View File

@@ -0,0 +1,9 @@
import { requireYAML } from '../../../utils/require-yaml';
import { merge } from 'lodash';
import { Relation } from '../../../types';
const systemData = requireYAML(require.resolve('./relations.yaml'));
export const systemRelationRows: Relation[] = systemData.data.map((row: Record<string, any>) => {
return merge({ system: true }, systemData.defaults, row);
});

View File

@@ -6,6 +6,8 @@ import { RequestHandler } from 'express';
import asyncHandler from 'express-async-handler';
import database from '../database';
import { ForbiddenException } from '../exceptions';
import { systemCollectionRows } from '../database/system-data/collections';
import { Collection } from '../types';
const collectionExists: RequestHandler = asyncHandler(async (req, res, next) => {
if (!req.params.collection) return next();
@@ -18,13 +20,21 @@ const collectionExists: RequestHandler = asyncHandler(async (req, res, next) =>
req.collection = req.params.collection;
const collectionInfo = await database
.select('singleton')
.from('directus_collections')
.where({ collection: req.collection })
.first();
if (req.collection.startsWith('directus_')) {
const systemRow = systemCollectionRows.find((collection) => {
return collection?.collection === req.collection;
});
req.singleton = collectionInfo?.singleton === true || collectionInfo?.singleton === 1;
req.singleton = !!systemRow?.singleton;
} else {
const collectionInfo = await database
.select('singleton')
.from('directus_collections')
.where({ collection: req.collection })
.first();
req.singleton = collectionInfo?.singleton === true || collectionInfo?.singleton === 1;
}
return next();
});

View File

@@ -20,6 +20,7 @@ import { ItemsService } from './items';
import { PayloadService } from './payload';
import { parseFilter } from '../utils/parse-filter';
import { toArray } from '../utils/to-array';
import { systemFieldRows } from '../database/system-data/fields';
export class AuthorizationService {
knex: Knex;
@@ -268,13 +269,18 @@ export class AuthorizationService {
let requiredColumns: string[] = [];
for (const column of columns) {
const field = await this.knex
.select<{ special: string }>('special')
.from('directus_fields')
.where({ collection, field: column.name })
.first();
const field =
(await this.knex
.select<{ special: string }>('special')
.from('directus_fields')
.where({ collection, field: column.name })
.first()) ||
systemFieldRows.find(
(fieldMeta) =>
fieldMeta.field === column.name && fieldMeta.collection === collection
);
const specials = (field?.special || '').split(',');
const specials = field?.special ? toArray(field.special) : [];
const hasGenerateSpecial = [
'uuid',
@@ -326,9 +332,11 @@ export class AuthorizationService {
}
validateJoi(
validation: Record<string, any>,
validation: null | Record<string, any>,
payloads: Partial<Record<string, any>>[]
): FailedValidationException[] {
if (!validation) return [];
const errors: FailedValidationException[] = [];
/**

View File

@@ -1,5 +1,11 @@
import database, { schemaInspector } from '../database';
import { AbstractServiceOptions, Accountability, Collection, Relation } from '../types';
import {
AbstractServiceOptions,
Accountability,
Collection,
CollectionMeta,
Relation,
} from '../types';
import Knex from 'knex';
import { ForbiddenException, InvalidPayloadException } from '../exceptions';
import SchemaInspector from 'knex-schema-inspector';
@@ -7,6 +13,7 @@ import { FieldsService } from '../services/fields';
import { ItemsService } from '../services/items';
import cache from '../cache';
import { toArray } from '../utils/to-array';
import { systemCollectionRows } from '../database/system-data/collections';
export class CollectionsService {
knex: Knex;
@@ -106,6 +113,7 @@ export class CollectionsService {
knex: this.knex,
accountability: this.accountability,
});
const collectionKeys = toArray(collection);
if (this.accountability && this.accountability.admin !== true) {
@@ -135,7 +143,9 @@ export class CollectionsService {
const tables = tablesInDatabase.filter((table) => collectionKeys.includes(table.name));
const meta = (await collectionItemsService.readByQuery({
filter: { collection: { _in: collectionKeys } },
})) as Collection['meta'][];
})) as CollectionMeta[];
meta.push(...systemCollectionRows);
const collections: Collection[] = [];
@@ -173,7 +183,9 @@ export class CollectionsService {
const tablesToFetchInfoFor = tablesInDatabase.map((table) => table.name);
const meta = (await collectionItemsService.readByQuery({
filter: { collection: { _in: tablesToFetchInfoFor } },
})) as Collection['meta'][];
})) as CollectionMeta[];
meta.push(...systemCollectionRows);
const collections: Collection[] = [];

View File

@@ -11,6 +11,9 @@ import { PayloadService } from '../services/payload';
import getDefaultValue from '../utils/get-default-value';
import cache from '../cache';
import SchemaInspector from 'knex-schema-inspector';
import { toArray } from '../utils/to-array';
import { systemFieldRows } from '../database/system-data/fields/';
type RawField = Partial<Field> & { field: string; type: typeof types[number] };
@@ -38,8 +41,13 @@ export class FieldsService {
filter: { collection: { _eq: collection } },
limit: -1,
})) as FieldMeta[];
fields.push(
...systemFieldRows.filter((fieldMeta) => fieldMeta.collection === collection)
);
} else {
fields = (await nonAuthorizedItemsService.readByQuery({ limit: -1 })) as FieldMeta[];
fields.push(...systemFieldRows);
}
let columns = await schemaInspector.columnInfo(collection);
@@ -73,12 +81,15 @@ export class FieldsService {
aliasQuery.andWhere('collection', collection);
}
let aliasFields = await aliasQuery;
let aliasFields = [
...((await this.payloadService.processValues('read', await aliasQuery)) as FieldMeta[]),
...systemFieldRows,
];
const aliasTypes = ['alias', 'o2m', 'm2m', 'files', 'files', 'translations'];
aliasFields = aliasFields.filter((field) => {
const specials = (field.special || '').split(',');
const specials = toArray(field.special);
for (const type of aliasTypes) {
if (specials.includes(type)) return true;
@@ -87,19 +98,17 @@ export class FieldsService {
return false;
});
aliasFields = (await this.payloadService.processValues('read', aliasFields)) as FieldMeta[];
const aliasFieldsAsField = aliasFields.map((field) => {
const data = {
collection: field.collection,
field: field.field,
type: field.special[0],
type: field.special?.[0],
schema: null,
meta: field,
};
return data;
});
}) as Field[];
const result = [...columnsWithSystem, ...aliasFieldsAsField];
@@ -163,6 +172,12 @@ export class FieldsService {
fieldInfo = (await this.payloadService.processValues('read', fieldInfo)) as FieldMeta[];
}
fieldInfo =
fieldInfo ||
systemFieldRows.find(
(fieldMeta) => fieldMeta.collection === collection && fieldMeta.field === field
);
try {
column = await schemaInspector.columnInfo(collection, field);
column.default_value = getDefaultValue(column);

View File

@@ -41,7 +41,7 @@ export class FilesService extends ItemsService {
const fileExtension =
(payload.type && extension(payload.type)) || path.extname(payload.filename_download);
payload.filename_disk = primaryKey + fileExtension;
payload.filename_disk = primaryKey + '.' + fileExtension;
if (!payload.type) {
payload.type = 'application/octet-stream';

View File

@@ -7,7 +7,6 @@ import {
Field,
Relation,
Query,
AbstractService,
} from '../types';
import {
GraphQLString,
@@ -49,6 +48,7 @@ import { UsersService } from './users';
import { WebhooksService } from './webhooks';
import { getRelationType } from '../utils/get-relation-type';
import { systemCollectionRows } from '../database/system-data/collections';
export class GraphQLService {
accountability: Accountability | null;
@@ -504,11 +504,16 @@ export class GraphQLService {
});
}
const collectionInfo = await this.knex
.select('singleton')
.from('directus_collections')
.where({ collection: collection })
.first();
const collectionInfo =
(await this.knex
.select('singleton')
.from('directus_collections')
.where({ collection: collection })
.first()) ||
systemCollectionRows.find(
(collectionMeta) => collectionMeta?.collection === collection
);
const result =
collectionInfo?.singleton === true
? await service.readSingleton(query)

View File

@@ -6,7 +6,7 @@ import {
Action,
Accountability,
PermissionsAction,
Item,
Item as AnyItem,
Query,
PrimaryKey,
AbstractService,
@@ -26,7 +26,7 @@ import getDefaultValue from '../utils/get-default-value';
import { InvalidPayloadException } from '../exceptions';
import { ForbiddenException } from '../exceptions';
export class ItemsService implements AbstractService {
export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractService {
collection: string;
knex: Knex;
accountability: Accountability | null;
@@ -52,7 +52,7 @@ export class ItemsService implements AbstractService {
const primaryKeyField = await this.schemaInspector.primary(this.collection);
const columns = await this.schemaInspector.columns(this.collection);
let payloads = clone(toArray(data));
let payloads: AnyItem[] = clone(toArray(data));
const savedPrimaryKeys = await this.knex.transaction(async (trx) => {
const payloadService = new PayloadService(this.collection, {
@@ -194,7 +194,7 @@ export class ItemsService implements AbstractService {
return Array.isArray(data) ? savedPrimaryKeys : savedPrimaryKeys[0];
}
async readByQuery(query: Query): Promise<null | Item | Item[]> {
async readByQuery(query: Query): Promise<null | Partial<Item> | Partial<Item>[]> {
const authorizationService = new AuthorizationService({
accountability: this.accountability,
knex: this.knex,
@@ -210,20 +210,24 @@ export class ItemsService implements AbstractService {
}
const records = await runAST(ast, { knex: this.knex });
return records;
return records as Partial<Item> | Partial<Item>[] | null;
}
readByKey(
keys: PrimaryKey[],
query?: Query,
action?: PermissionsAction
): Promise<null | Item[]>;
readByKey(key: PrimaryKey, query?: Query, action?: PermissionsAction): Promise<null | Item>;
): Promise<null | Partial<Item>[]>;
readByKey(
key: PrimaryKey,
query?: Query,
action?: PermissionsAction
): Promise<null | Partial<Item>>;
async readByKey(
key: PrimaryKey | PrimaryKey[],
query: Query = {},
action: PermissionsAction = 'read'
): Promise<null | Item | Item[]> {
): Promise<null | Partial<Item> | Partial<Item>[]> {
query = clone(query);
const primaryKeyField = await this.schemaInspector.primary(this.collection);
const keys = toArray(key);
@@ -260,7 +264,7 @@ export class ItemsService implements AbstractService {
if (result === null) throw new ForbiddenException();
return result;
return result as Partial<Item> | Partial<Item>[] | null;
}
update(data: Partial<Item>, keys: PrimaryKey[]): Promise<PrimaryKey[]>;
@@ -277,7 +281,7 @@ export class ItemsService implements AbstractService {
if (data && key) {
const keys = toArray(key);
let payload = clone(data);
let payload: Partial<AnyItem> | Partial<AnyItem>[] = clone(data);
const customProcessed = await emitter.emitAsync(
`${this.eventScope}.update.before`,
@@ -550,11 +554,11 @@ export class ItemsService implements AbstractService {
return await this.delete(keys);
}
async readSingleton(query: Query) {
async readSingleton(query: Query): Promise<Partial<Item>> {
query = clone(query);
query.single = true;
const record = (await this.readByQuery(query)) as Item;
const record = (await this.readByQuery(query)) as Partial<Item>;
if (!record) {
const columns = await this.schemaInspector.columnInfo(this.collection);
@@ -564,7 +568,7 @@ export class ItemsService implements AbstractService {
defaults[column.name] = getDefaultValue(column);
}
return defaults;
return defaults as Partial<Item>;
}
return record;

View File

@@ -40,7 +40,7 @@ export class MetaService {
const dbQuery = database(collection).count('*', { as: 'count' });
if (query.filter) {
await applyFilter(dbQuery, query.filter, collection);
await applyFilter(this.knex, dbQuery, query.filter, collection);
}
const records = await dbQuery;

View File

@@ -17,6 +17,9 @@ import getLocalType from '../utils/get-local-type';
import { format, formatISO } from 'date-fns';
import { ForbiddenException } from '../exceptions';
import { toArray } from '../utils/to-array';
import { FieldMeta } from '../types';
import { systemFieldRows } from '../database/system-data/fields';
import { systemRelationRows } from '../database/system-data/relations';
type Action = 'create' | 'read' | 'update';
@@ -148,17 +151,21 @@ export class PayloadService {
const fieldsInPayload = Object.keys(processedPayload[0]);
const specialFieldsQuery = this.knex
let specialFieldsInCollection: FieldMeta[] = await this.knex
.select('field', 'special')
.from('directus_fields')
.where({ collection: this.collection })
.whereNotNull('special');
if (action === 'read') {
specialFieldsQuery.whereIn('field', fieldsInPayload);
}
specialFieldsInCollection.push(
...systemFieldRows.filter((fieldMeta) => fieldMeta.collection === this.collection)
);
const specialFieldsInCollection = await specialFieldsQuery;
if (action === 'read') {
specialFieldsInCollection = specialFieldsInCollection.filter((fieldMeta) => {
return fieldsInPayload.includes(fieldMeta.field);
});
}
await Promise.all(
processedPayload.map(async (record: any) => {
@@ -203,13 +210,13 @@ export class PayloadService {
}
async processField(
field: { field: string; special: string },
field: FieldMeta,
payload: Partial<Item>,
action: Action,
accountability: Accountability | null
) {
if (!field.special) return payload[field.field];
const fieldSpecials = field.special.split(',').map((s) => s.trim());
const fieldSpecials = field.special ? toArray(field.special) : [];
let value = clone(payload[field.field]);
@@ -284,10 +291,15 @@ export class PayloadService {
async processM2O(
payload: Partial<Item> | Partial<Item>[]
): Promise<Partial<Item> | Partial<Item>[]> {
const relations = await this.knex
.select<Relation[]>('*')
.from('directus_relations')
.where({ many_collection: this.collection });
const relations = [
...(await this.knex
.select<Relation[]>('*')
.from('directus_relations')
.where({ many_collection: this.collection })),
...systemRelationRows.filter(
(systemRelation) => systemRelation.many_collection === this.collection
),
];
const payloads = clone(Array.isArray(payload) ? payload : [payload]);
@@ -334,10 +346,15 @@ export class PayloadService {
* Recursively save/update all nested related o2m items
*/
async processO2M(payload: Partial<Item> | Partial<Item>[], parent?: PrimaryKey) {
const relations = await this.knex
.select<Relation[]>('*')
.from('directus_relations')
.where({ one_collection: this.collection });
const relations = [
...(await this.knex
.select<Relation[]>('*')
.from('directus_relations')
.where({ one_collection: this.collection })),
...systemRelationRows.filter(
(systemRelation) => systemRelation.one_collection === this.collection
),
];
const payloads = clone(toArray(payload));

View File

@@ -3,6 +3,8 @@ import { AbstractServiceOptions, Query, PrimaryKey, PermissionsAction, Relation
import { PermissionsService } from './permissions';
import { toArray } from '../utils/to-array';
import { systemRelationRows } from '../database/system-data/relations';
/**
* @TODO update foreign key constraints when relations are updated
*/
@@ -26,6 +28,10 @@ export class RelationsService extends ItemsService {
| ParsedRelation[]
| null;
if (results && Array.isArray(results)) {
results.push(...(systemRelationRows as ParsedRelation[]));
}
const filteredResults = await this.filterForbidden(results);
return filteredResults;
@@ -48,6 +54,9 @@ export class RelationsService extends ItemsService {
| ParsedRelation[]
| null;
// No need to merge system relations here. They don't have PKs so can never be directly
// targetted
const filteredResults = await this.filterForbidden(results);
return filteredResults;
}

View File

@@ -3,6 +3,7 @@ import database from '../database';
import Knex from 'knex';
import { InvalidPayloadException, ForbiddenException } from '../exceptions';
import SchemaInspector from 'knex-schema-inspector';
import { systemCollectionRows } from '../database/system-data/collections';
export class UtilsService {
knex: Knex;
@@ -16,11 +17,12 @@ export class UtilsService {
async sort(collection: string, { item, to }: { item: PrimaryKey; to: PrimaryKey }) {
const schemaInspector = SchemaInspector(this.knex);
const sortFieldResponse = await this.knex
.select('sort_field')
.from('directus_collections')
.where({ collection })
.first();
const sortFieldResponse =
(await this.knex
.select('sort_field')
.from('directus_collections')
.where({ collection })
.first()) || systemCollectionRows;
const sortField = sortFieldResponse?.sort_field;

View File

@@ -1,16 +1,18 @@
import { Field } from './field';
import { Table } from 'knex-schema-inspector/lib/types/table';
export type CollectionMeta = {
collection: string;
note: string | null;
hidden: boolean;
singleton: boolean;
icon: string | null;
translations: Record<string, string>;
};
export type Collection = {
collection: string;
fields?: Field[];
meta: {
collection: string;
note: string | null;
hidden: boolean;
singleton: boolean;
icon: string | null;
translations: Record<string, string>;
} | null;
meta: CollectionMeta | null;
schema: Table;
};

View File

@@ -1,11 +1,18 @@
import { QueryBuilder } from 'knex';
import { Query, Filter } from '../types';
import database, { schemaInspector } from '../database';
import { Query, Filter, Relation } from '../types';
import { schemaInspector } from '../database';
import Knex from 'knex';
import { clone, isPlainObject } from 'lodash';
import { systemRelationRows } from '../database/system-data/relations';
export default async function applyQuery(collection: string, dbQuery: QueryBuilder, query: Query) {
export default async function applyQuery(
knex: Knex,
collection: string,
dbQuery: QueryBuilder,
query: Query
) {
if (query.filter) {
await applyFilter(dbQuery, query.filter, collection);
await applyFilter(knex, dbQuery, query.filter, collection);
}
if (query.sort) {
@@ -46,8 +53,16 @@ export default async function applyQuery(collection: string, dbQuery: QueryBuild
}
}
export async function applyFilter(rootQuery: QueryBuilder, rootFilter: Filter, collection: string) {
const relations = await database.select('*').from('directus_relations');
export async function applyFilter(
knex: Knex,
rootQuery: QueryBuilder,
rootFilter: Filter,
collection: string
) {
const relations: Relation[] = [
...(await knex.select('*').from('directus_relations')),
...systemRelationRows,
];
addWhereClauses(rootQuery, rootFilter, collection);
addJoins(rootQuery, rootFilter, collection);

View File

@@ -16,6 +16,8 @@ import { cloneDeep } from 'lodash';
import Knex from 'knex';
import SchemaInspector from 'knex-schema-inspector';
import { getRelationType } from '../utils/get-relation-type';
import { systemFieldRows } from '../database/system-data/fields';
import { systemRelationRows } from '../database/system-data/relations';
type GetASTOptions = {
accountability?: Accountability | null;
@@ -39,7 +41,10 @@ export default async function getASTFromQuery(
* we might not need al this info at all times, but it's easier to fetch it all once, than trying to fetch it for every
* requested field. @todo look into utilizing graphql/dataloader for this purpose
*/
const relations = await knex.select<Relation[]>('*').from('directus_relations');
const relations = [
...(await knex.select<Relation[]>('*').from('directus_relations')),
...systemRelationRows,
];
const permissions =
accountability && accountability.admin !== true
@@ -261,9 +266,12 @@ export default async function getASTFromQuery(
async function getFieldsInCollection(collection: string) {
const columns = (await schemaInspector.columns(collection)).map((column) => column.column);
const fields = (
await knex.select('field').from('directus_fields').where({ collection })
).map((field) => field.field);
const fields = [
...(await knex.select('field').from('directus_fields').where({ collection })).map(
(field) => field.field
),
...systemFieldRows.map((fieldMeta) => fieldMeta.field),
];
const fieldsInCollection = [
...columns,

View File

@@ -1,5 +1,6 @@
import database, { schemaInspector } from '../database';
import { uniq } from 'lodash';
import { systemFieldRows } from '../database/system-data/fields';
export default async function hasFields(fields: { collection: string; field: string }[]) {
const fieldsObject: { [collection: string]: string[] } = {};
@@ -33,6 +34,9 @@ export async function collectionHasFields(collection: string, fieldKeys: string[
const existingFields = uniq([
...columns.map(({ column }) => column),
...fields.map(({ field }) => field),
...systemFieldRows
.filter((fieldMeta) => fieldMeta.collection === collection)
.map((fieldMeta) => fieldMeta.field),
]);
for (const key of fieldKeys) {

View File

@@ -0,0 +1,8 @@
import fse from 'fs-extra';
import yaml from 'js-yaml';
export function requireYAML(filepath: string) {
const yamlRaw = fse.readFileSync(filepath, 'utf8');
return yaml.safeLoad(yamlRaw) as Record<string, any>;
}

View File

@@ -1,7 +1,6 @@
export function toArray(val: string): string[];
export function toArray(val: any | any[]): any[] {
export function toArray<T = any>(val: T | T[]): T[] {
if (typeof val === 'string') {
return val.split(',');
return (val.split(',') as unknown) as T[];
}
return Array.isArray(val) ? val : [val];