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

@@ -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;