mirror of
https://github.com/directus/directus.git
synced 2026-02-01 23:45:02 -05:00
Merge branch 'main' into Kinzi/main
This commit is contained in:
@@ -3,11 +3,7 @@ import jwt from 'jsonwebtoken';
|
||||
import argon2 from 'argon2';
|
||||
import { nanoid } from 'nanoid';
|
||||
import ms from 'ms';
|
||||
import {
|
||||
InvalidCredentialsException,
|
||||
InvalidPayloadException,
|
||||
InvalidOTPException,
|
||||
} from '../exceptions';
|
||||
import { InvalidCredentialsException, InvalidPayloadException, InvalidOTPException } from '../exceptions';
|
||||
import { Session, Accountability, AbstractServiceOptions, Action } from '../types';
|
||||
import Knex from 'knex';
|
||||
import { ActivityService } from '../services/activity';
|
||||
@@ -158,21 +154,13 @@ export class AuthenticationService {
|
||||
}
|
||||
|
||||
async generateOTPAuthURL(pk: string, secret: string) {
|
||||
const user = await this.knex
|
||||
.select('first_name', 'last_name')
|
||||
.from('directus_users')
|
||||
.where({ id: pk })
|
||||
.first();
|
||||
const user = await this.knex.select('first_name', 'last_name').from('directus_users').where({ id: pk }).first();
|
||||
const name = `${user.first_name} ${user.last_name}`;
|
||||
return authenticator.keyuri(name, 'Directus', secret);
|
||||
}
|
||||
|
||||
async verifyOTP(pk: string, otp: string): Promise<boolean> {
|
||||
const user = await this.knex
|
||||
.select('tfa_secret')
|
||||
.from('directus_users')
|
||||
.where({ id: pk })
|
||||
.first();
|
||||
const user = await this.knex.select('tfa_secret').from('directus_users').where({ id: pk }).first();
|
||||
|
||||
if (!user.tfa_secret) {
|
||||
throw new InvalidPayloadException(`User "${pk}" doesn't have TFA enabled.`);
|
||||
@@ -183,11 +171,7 @@ export class AuthenticationService {
|
||||
}
|
||||
|
||||
async verifyPassword(pk: string, password: string) {
|
||||
const userRecord = await this.knex
|
||||
.select('password')
|
||||
.from('directus_users')
|
||||
.where({ id: pk })
|
||||
.first();
|
||||
const userRecord = await this.knex.select('password').from('directus_users').where({ id: pk }).first();
|
||||
|
||||
if (!userRecord || !userRecord.password) {
|
||||
throw new InvalidCredentialsException();
|
||||
|
||||
@@ -56,27 +56,19 @@ export class AuthorizationService {
|
||||
)) as Permission[];
|
||||
|
||||
// If the permissions don't match the collections, you don't have permission to read all of them
|
||||
const uniqueCollectionsRequestedCount = uniq(
|
||||
collectionsRequested.map(({ collection }) => collection)
|
||||
).length;
|
||||
const uniqueCollectionsRequestedCount = uniq(collectionsRequested.map(({ collection }) => collection)).length;
|
||||
|
||||
if (uniqueCollectionsRequestedCount !== permissionsForCollections.length) {
|
||||
// Find the first collection that doesn't have permissions configured
|
||||
const { collection, field } = collectionsRequested.find(
|
||||
({ collection }) =>
|
||||
permissionsForCollections.find(
|
||||
(permission) => permission.collection === collection
|
||||
) === undefined
|
||||
permissionsForCollections.find((permission) => permission.collection === collection) === undefined
|
||||
)!;
|
||||
|
||||
if (field) {
|
||||
throw new ForbiddenException(
|
||||
`You don't have permission to access the "${field}" field.`
|
||||
);
|
||||
throw new ForbiddenException(`You don't have permission to access the "${field}" field.`);
|
||||
} else {
|
||||
throw new ForbiddenException(
|
||||
`You don't have permission to access the "${collection}" collection.`
|
||||
);
|
||||
throw new ForbiddenException(`You don't have permission to access the "${collection}" collection.`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,15 +80,11 @@ export class AuthorizationService {
|
||||
/**
|
||||
* Traverses the AST and returns an array of all collections that are being fetched
|
||||
*/
|
||||
function getCollectionsFromAST(
|
||||
ast: AST | NestedCollectionNode
|
||||
): { collection: string; field: string }[] {
|
||||
function getCollectionsFromAST(ast: AST | NestedCollectionNode): { collection: string; field: string }[] {
|
||||
const collections = [];
|
||||
|
||||
if (ast.type === 'm2a') {
|
||||
collections.push(
|
||||
...ast.names.map((name) => ({ collection: name, field: ast.fieldKey }))
|
||||
);
|
||||
collections.push(...ast.names.map((name) => ({ collection: name, field: ast.fieldKey })));
|
||||
|
||||
/** @TODO add nestedNode */
|
||||
} else {
|
||||
@@ -121,9 +109,7 @@ export class AuthorizationService {
|
||||
const collection = ast.name;
|
||||
|
||||
// We check the availability of the permissions in the step before this is run
|
||||
const permissions = permissionsForCollections.find(
|
||||
(permission) => permission.collection === collection
|
||||
)!;
|
||||
const permissions = permissionsForCollections.find((permission) => permission.collection === collection)!;
|
||||
|
||||
const allowedFields = permissions.fields || [];
|
||||
|
||||
@@ -138,9 +124,7 @@ export class AuthorizationService {
|
||||
const fieldKey = childNode.name;
|
||||
|
||||
if (allowedFields.includes(fieldKey) === false) {
|
||||
throw new ForbiddenException(
|
||||
`You don't have permission to access the "${fieldKey}" field.`
|
||||
);
|
||||
throw new ForbiddenException(`You don't have permission to access the "${fieldKey}" field.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,9 +139,7 @@ export class AuthorizationService {
|
||||
const collection = ast.name;
|
||||
|
||||
// We check the availability of the permissions in the step before this is run
|
||||
const permissions = permissionsForCollections.find(
|
||||
(permission) => permission.collection === collection
|
||||
)!;
|
||||
const permissions = permissionsForCollections.find((permission) => permission.collection === collection)!;
|
||||
|
||||
const parsedPermissions = parseFilter(permissions.permissions, accountability);
|
||||
|
||||
@@ -174,9 +156,7 @@ export class AuthorizationService {
|
||||
if (ast.query.filter._and.length === 0) delete ast.query.filter._and;
|
||||
|
||||
if (permissions.limit && ast.query.limit && ast.query.limit > permissions.limit) {
|
||||
throw new ForbiddenException(
|
||||
`You can't read more than ${permissions.limit} items at a time.`
|
||||
);
|
||||
throw new ForbiddenException(`You can't read more than ${permissions.limit} items at a time.`);
|
||||
}
|
||||
|
||||
// Default to the permissions limit if limit hasn't been set
|
||||
@@ -197,16 +177,8 @@ export class AuthorizationService {
|
||||
/**
|
||||
* Checks if the provided payload matches the configured permissions, and adds the presets to the payload.
|
||||
*/
|
||||
validatePayload(
|
||||
action: PermissionsAction,
|
||||
collection: string,
|
||||
payloads: Partial<Item>[]
|
||||
): Promise<Partial<Item>[]>;
|
||||
validatePayload(
|
||||
action: PermissionsAction,
|
||||
collection: string,
|
||||
payload: Partial<Item>
|
||||
): Promise<Partial<Item>>;
|
||||
validatePayload(action: PermissionsAction, collection: string, payloads: Partial<Item>[]): Promise<Partial<Item>[]>;
|
||||
validatePayload(action: PermissionsAction, collection: string, payload: Partial<Item>): Promise<Partial<Item>>;
|
||||
async validatePayload(
|
||||
action: PermissionsAction,
|
||||
collection: string,
|
||||
@@ -239,10 +211,7 @@ export class AuthorizationService {
|
||||
|
||||
if (!permission) throw new ForbiddenException();
|
||||
|
||||
permission = (await this.payloadService.processValues(
|
||||
'read',
|
||||
permission as Item
|
||||
)) as Permission;
|
||||
permission = (await this.payloadService.processValues('read', permission as Item)) as Permission;
|
||||
|
||||
// Check if you have permission to access the fields you're trying to acces
|
||||
|
||||
@@ -251,9 +220,7 @@ export class AuthorizationService {
|
||||
if (allowedFields.includes('*') === false) {
|
||||
for (const payload of payloads) {
|
||||
const keysInData = Object.keys(payload);
|
||||
const invalidKeys = keysInData.filter(
|
||||
(fieldKey) => allowedFields.includes(fieldKey) === false
|
||||
);
|
||||
const invalidKeys = keysInData.filter((fieldKey) => allowedFields.includes(fieldKey) === false);
|
||||
|
||||
if (invalidKeys.length > 0) {
|
||||
throw new ForbiddenException(
|
||||
@@ -280,24 +247,16 @@ export class AuthorizationService {
|
||||
.where({ collection, field: column.column_name })
|
||||
.first()) ||
|
||||
systemFieldRows.find(
|
||||
(fieldMeta) =>
|
||||
fieldMeta.field === column.column_name &&
|
||||
fieldMeta.collection === collection
|
||||
(fieldMeta) => fieldMeta.field === column.column_name && fieldMeta.collection === collection
|
||||
);
|
||||
|
||||
const specials = field?.special ? toArray(field.special) : [];
|
||||
|
||||
const hasGenerateSpecial = [
|
||||
'uuid',
|
||||
'date-created',
|
||||
'role-created',
|
||||
'user-created',
|
||||
].some((name) => specials.includes(name));
|
||||
const hasGenerateSpecial = ['uuid', 'date-created', 'role-created', 'user-created'].some((name) =>
|
||||
specials.includes(name)
|
||||
);
|
||||
|
||||
const isRequired =
|
||||
column.is_nullable === false &&
|
||||
column.default_value === null &&
|
||||
hasGenerateSpecial === false;
|
||||
const isRequired = column.is_nullable === false && column.default_value === null && hasGenerateSpecial === false;
|
||||
|
||||
if (isRequired) {
|
||||
requiredColumns.push(column.column_name);
|
||||
@@ -350,9 +309,7 @@ export class AuthorizationService {
|
||||
if (Object.keys(validation)[0] === '_and') {
|
||||
const subValidation = Object.values(validation)[0];
|
||||
const nestedErrors = flatten<FailedValidationException>(
|
||||
subValidation.map((subObj: Record<string, any>) =>
|
||||
this.validateJoi(subObj, payloads)
|
||||
)
|
||||
subValidation.map((subObj: Record<string, any>) => this.validateJoi(subObj, payloads))
|
||||
).filter((err?: FailedValidationException) => err);
|
||||
errors.push(...nestedErrors);
|
||||
}
|
||||
@@ -360,9 +317,7 @@ export class AuthorizationService {
|
||||
if (Object.keys(validation)[0] === '_or') {
|
||||
const subValidation = Object.values(validation)[0];
|
||||
const nestedErrors = flatten<FailedValidationException>(
|
||||
subValidation.map((subObj: Record<string, any>) =>
|
||||
this.validateJoi(subObj, payloads)
|
||||
)
|
||||
subValidation.map((subObj: Record<string, any>) => this.validateJoi(subObj, payloads))
|
||||
);
|
||||
const allErrored = nestedErrors.every((err?: FailedValidationException) => err);
|
||||
|
||||
@@ -377,20 +332,14 @@ export class AuthorizationService {
|
||||
const { error } = schema.validate(payload, { abortEarly: false });
|
||||
|
||||
if (error) {
|
||||
errors.push(
|
||||
...error.details.map((details) => new FailedValidationException(details))
|
||||
);
|
||||
errors.push(...error.details.map((details) => new FailedValidationException(details)));
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
async checkAccess(
|
||||
action: PermissionsAction,
|
||||
collection: string,
|
||||
pk: PrimaryKey | PrimaryKey[]
|
||||
) {
|
||||
async checkAccess(action: PermissionsAction, collection: string, pk: PrimaryKey | PrimaryKey[]) {
|
||||
if (this.accountability?.admin === true) return;
|
||||
|
||||
const itemsService = new ItemsService(collection, {
|
||||
@@ -409,14 +358,11 @@ export class AuthorizationService {
|
||||
if (!result) throw '';
|
||||
if (Array.isArray(pk) && pk.length > 1 && result.length !== pk.length) throw '';
|
||||
} catch {
|
||||
throw new ForbiddenException(
|
||||
`You're not allowed to ${action} item "${pk}" in collection "${collection}".`,
|
||||
{
|
||||
collection,
|
||||
item: pk,
|
||||
action,
|
||||
}
|
||||
);
|
||||
throw new ForbiddenException(`You're not allowed to ${action} item "${pk}" in collection "${collection}".`, {
|
||||
collection,
|
||||
item: pk,
|
||||
action,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import database, { schemaInspector } from '../database';
|
||||
import {
|
||||
AbstractServiceOptions,
|
||||
Accountability,
|
||||
Collection,
|
||||
CollectionMeta,
|
||||
Relation,
|
||||
SchemaOverview,
|
||||
} from '../types';
|
||||
import { AbstractServiceOptions, Accountability, Collection, CollectionMeta, Relation, SchemaOverview } from '../types';
|
||||
import Knex from 'knex';
|
||||
import { ForbiddenException, InvalidPayloadException } from '../exceptions';
|
||||
import { FieldsService } from '../services/fields';
|
||||
@@ -14,6 +7,7 @@ import { ItemsService } from '../services/items';
|
||||
import cache from '../cache';
|
||||
import { toArray } from '../utils/to-array';
|
||||
import { systemCollectionRows } from '../database/system-data/collections';
|
||||
import env from '../env';
|
||||
|
||||
export class CollectionsService {
|
||||
knex: Knex;
|
||||
@@ -78,9 +72,7 @@ export class CollectionsService {
|
||||
}
|
||||
|
||||
if (payload.collection in this.schema) {
|
||||
throw new InvalidPayloadException(
|
||||
`Collection "${payload.collection}" already exists.`
|
||||
);
|
||||
throw new InvalidPayloadException(`Collection "${payload.collection}" already exists.`);
|
||||
}
|
||||
|
||||
await trx.schema.createTable(payload.collection, (table) => {
|
||||
@@ -94,9 +86,7 @@ export class CollectionsService {
|
||||
collection: payload.collection,
|
||||
});
|
||||
|
||||
const fieldPayloads = payload
|
||||
.fields!.filter((field) => field.meta)
|
||||
.map((field) => field.meta);
|
||||
const fieldPayloads = payload.fields!.filter((field) => field.meta).map((field) => field.meta);
|
||||
|
||||
await fieldItemsService.create(fieldPayloads);
|
||||
|
||||
@@ -104,7 +94,7 @@ export class CollectionsService {
|
||||
}
|
||||
});
|
||||
|
||||
if (cache) {
|
||||
if (cache && env.CACHE_AUTO_PURGE) {
|
||||
await cache.clear();
|
||||
}
|
||||
|
||||
@@ -131,15 +121,11 @@ export class CollectionsService {
|
||||
.whereIn('collection', collectionKeys);
|
||||
|
||||
if (collectionKeys.length !== permissions.length) {
|
||||
const collectionsYouHavePermissionToRead = permissions.map(
|
||||
({ collection }) => collection
|
||||
);
|
||||
const collectionsYouHavePermissionToRead = permissions.map(({ collection }) => collection);
|
||||
|
||||
for (const collectionKey of collectionKeys) {
|
||||
if (collectionsYouHavePermissionToRead.includes(collectionKey) === false) {
|
||||
throw new ForbiddenException(
|
||||
`You don't have access to the "${collectionKey}" collection.`
|
||||
);
|
||||
throw new ForbiddenException(`You don't have access to the "${collectionKey}" collection.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,10 +204,7 @@ export class CollectionsService {
|
||||
update(data: Partial<Collection>, keys: string[]): Promise<string[]>;
|
||||
update(data: Partial<Collection>, key: string): Promise<string>;
|
||||
update(data: Partial<Collection>[]): Promise<string[]>;
|
||||
async update(
|
||||
data: Partial<Collection> | Partial<Collection>[],
|
||||
key?: string | string[]
|
||||
): Promise<string | string[]> {
|
||||
async update(data: Partial<Collection> | Partial<Collection>[], key?: string | string[]): Promise<string | string[]> {
|
||||
const collectionItemsService = new ItemsService('directus_collections', {
|
||||
knex: this.knex,
|
||||
accountability: this.accountability,
|
||||
@@ -239,11 +222,8 @@ export class CollectionsService {
|
||||
|
||||
for (const key of keys) {
|
||||
const exists =
|
||||
(await this.knex
|
||||
.select('collection')
|
||||
.from('directus_collections')
|
||||
.where({ collection: key })
|
||||
.first()) !== undefined;
|
||||
(await this.knex.select('collection').from('directus_collections').where({ collection: key }).first()) !==
|
||||
undefined;
|
||||
|
||||
if (exists) {
|
||||
await collectionItemsService.update(payload.meta, key);
|
||||
@@ -266,7 +246,7 @@ export class CollectionsService {
|
||||
|
||||
await collectionItemsService.update(collectionUpdates);
|
||||
|
||||
if (cache) {
|
||||
if (cache && env.CACHE_AUTO_PURGE) {
|
||||
await cache.clear();
|
||||
}
|
||||
|
||||
@@ -311,15 +291,13 @@ export class CollectionsService {
|
||||
for (const relation of relations) {
|
||||
const isM2O = relation.many_collection === collection;
|
||||
|
||||
/** @TODO M2A — Handle m2a case here */
|
||||
|
||||
if (isM2O) {
|
||||
await this.knex('directus_relations')
|
||||
.delete()
|
||||
.where({ many_collection: collection, many_field: relation.many_field });
|
||||
|
||||
await fieldsService.deleteField(relation.one_collection!, relation.one_field!);
|
||||
} else {
|
||||
} else if (!!relation.one_collection) {
|
||||
await this.knex('directus_relations')
|
||||
.update({ one_field: null })
|
||||
.where({ one_collection: collection, one_field: relation.one_field });
|
||||
@@ -339,7 +317,7 @@ export class CollectionsService {
|
||||
await this.knex.schema.dropTable(collectionKey);
|
||||
}
|
||||
|
||||
if (cache) {
|
||||
if (cache && env.CACHE_AUTO_PURGE) {
|
||||
await cache.clear();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import database, { schemaInspector } from '../database';
|
||||
import { Field } from '../types/field';
|
||||
import {
|
||||
Accountability,
|
||||
AbstractServiceOptions,
|
||||
FieldMeta,
|
||||
Relation,
|
||||
SchemaOverview,
|
||||
} from '../types';
|
||||
import { Accountability, AbstractServiceOptions, FieldMeta, Relation, SchemaOverview } from '../types';
|
||||
import { ItemsService } from '../services/items';
|
||||
import { ColumnBuilder } from 'knex';
|
||||
import getLocalType from '../utils/get-local-type';
|
||||
@@ -18,6 +12,7 @@ import getDefaultValue from '../utils/get-default-value';
|
||||
import cache from '../cache';
|
||||
import SchemaInspector from '@directus/schema';
|
||||
import { toArray } from '../utils/to-array';
|
||||
import env from '../env';
|
||||
|
||||
import { systemFieldRows } from '../database/system-data/fields/';
|
||||
|
||||
@@ -53,9 +48,7 @@ export class FieldsService {
|
||||
limit: -1,
|
||||
})) as FieldMeta[];
|
||||
|
||||
fields.push(
|
||||
...systemFieldRows.filter((fieldMeta) => fieldMeta.collection === collection)
|
||||
);
|
||||
fields.push(...systemFieldRows.filter((fieldMeta) => fieldMeta.collection === collection));
|
||||
} else {
|
||||
fields = (await nonAuthorizedItemsService.readByQuery({ limit: -1 })) as FieldMeta[];
|
||||
fields.push(...systemFieldRows);
|
||||
@@ -92,19 +85,15 @@ export class FieldsService {
|
||||
aliasQuery.andWhere('collection', collection);
|
||||
}
|
||||
|
||||
let aliasFields = [
|
||||
...((await this.payloadService.processValues('read', await aliasQuery)) as FieldMeta[]),
|
||||
];
|
||||
let aliasFields = [...((await this.payloadService.processValues('read', await aliasQuery)) as FieldMeta[])];
|
||||
|
||||
if (collection) {
|
||||
aliasFields.push(
|
||||
...systemFieldRows.filter((fieldMeta) => fieldMeta.collection === collection)
|
||||
);
|
||||
aliasFields.push(...systemFieldRows.filter((fieldMeta) => fieldMeta.collection === collection));
|
||||
} else {
|
||||
aliasFields.push(...systemFieldRows);
|
||||
}
|
||||
|
||||
const aliasTypes = ['alias', 'o2m', 'm2m', 'files', 'files', 'translations'];
|
||||
const aliasTypes = ['alias', 'o2m', 'm2m', 'm2a', 'files', 'files', 'translations'];
|
||||
|
||||
aliasFields = aliasFields.filter((field) => {
|
||||
const specials = toArray(field.special);
|
||||
@@ -139,9 +128,7 @@ export class FieldsService {
|
||||
const allowedFieldsInCollection: Record<string, string[]> = {};
|
||||
|
||||
permissions.forEach((permission) => {
|
||||
allowedFieldsInCollection[permission.collection] = (permission.fields || '').split(
|
||||
','
|
||||
);
|
||||
allowedFieldsInCollection[permission.collection] = (permission.fields || '').split(',');
|
||||
});
|
||||
|
||||
if (collection && allowedFieldsInCollection.hasOwnProperty(collection) === false) {
|
||||
@@ -149,8 +136,7 @@ export class FieldsService {
|
||||
}
|
||||
|
||||
return result.filter((field) => {
|
||||
if (allowedFieldsInCollection.hasOwnProperty(field.collection) === false)
|
||||
return false;
|
||||
if (allowedFieldsInCollection.hasOwnProperty(field.collection) === false) return false;
|
||||
const allowedFields = allowedFieldsInCollection[field.collection];
|
||||
if (allowedFields[0] === '*') return true;
|
||||
return allowedFields.includes(field.field);
|
||||
@@ -180,11 +166,7 @@ export class FieldsService {
|
||||
}
|
||||
|
||||
let column;
|
||||
let fieldInfo = await this.knex
|
||||
.select('*')
|
||||
.from('directus_fields')
|
||||
.where({ collection, field })
|
||||
.first();
|
||||
let fieldInfo = await this.knex.select('*').from('directus_fields').where({ collection, field }).first();
|
||||
|
||||
if (fieldInfo) {
|
||||
fieldInfo = (await this.payloadService.processValues('read', fieldInfo)) as FieldMeta[];
|
||||
@@ -192,9 +174,7 @@ export class FieldsService {
|
||||
|
||||
fieldInfo =
|
||||
fieldInfo ||
|
||||
systemFieldRows.find(
|
||||
(fieldMeta) => fieldMeta.collection === collection && fieldMeta.field === field
|
||||
);
|
||||
systemFieldRows.find((fieldMeta) => fieldMeta.collection === collection && fieldMeta.field === field);
|
||||
|
||||
try {
|
||||
column = await this.schemaInspector.columnInfo(collection, field);
|
||||
@@ -223,19 +203,11 @@ export class FieldsService {
|
||||
|
||||
// Check if field already exists, either as a column, or as a row in directus_fields
|
||||
if (field.field in this.schema[collection].columns) {
|
||||
throw new InvalidPayloadException(
|
||||
`Field "${field.field}" already exists in collection "${collection}"`
|
||||
);
|
||||
throw new InvalidPayloadException(`Field "${field.field}" already exists in collection "${collection}"`);
|
||||
} else if (
|
||||
!!(await this.knex
|
||||
.select('id')
|
||||
.from('directus_fields')
|
||||
.where({ collection, field: field.field })
|
||||
.first())
|
||||
!!(await this.knex.select('id').from('directus_fields').where({ collection, field: field.field }).first())
|
||||
) {
|
||||
throw new InvalidPayloadException(
|
||||
`Field "${field.field}" already exists in collection "${collection}"`
|
||||
);
|
||||
throw new InvalidPayloadException(`Field "${field.field}" already exists in collection "${collection}"`);
|
||||
}
|
||||
|
||||
if (field.schema) {
|
||||
@@ -256,13 +228,11 @@ export class FieldsService {
|
||||
});
|
||||
}
|
||||
|
||||
if (cache) {
|
||||
if (cache && env.CACHE_AUTO_PURGE) {
|
||||
await cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/** @todo research how to make this happen in SQLite / Redshift */
|
||||
|
||||
async updateField(collection: string, field: RawField) {
|
||||
if (this.accountability && this.accountability.admin !== true) {
|
||||
throw new ForbiddenException('Only admins can perform this action');
|
||||
@@ -270,46 +240,8 @@ export class FieldsService {
|
||||
|
||||
if (field.schema) {
|
||||
await this.knex.schema.alterTable(collection, (table) => {
|
||||
let column: ColumnBuilder;
|
||||
|
||||
if (!field.schema) return;
|
||||
|
||||
if (field.type === 'string') {
|
||||
column = table.string(
|
||||
field.field,
|
||||
field.schema.max_length !== null ? field.schema.max_length : undefined
|
||||
);
|
||||
} else if (['float', 'decimal'].includes(field.type)) {
|
||||
const type = field.type as 'float' | 'decimal';
|
||||
column = table[type](
|
||||
field.field,
|
||||
field.schema?.numeric_precision || 10,
|
||||
field.schema?.numeric_scale || 5
|
||||
);
|
||||
} else if (field.type === 'csv') {
|
||||
column = table.string(field.field);
|
||||
} else {
|
||||
column = table[field.type](field.field);
|
||||
}
|
||||
|
||||
if (field.schema.default_value !== undefined) {
|
||||
if (
|
||||
typeof field.schema.default_value === 'string' &&
|
||||
field.schema.default_value.toLowerCase() === 'now()'
|
||||
) {
|
||||
column.defaultTo(this.knex.fn.now());
|
||||
} else {
|
||||
column.defaultTo(field.schema.default_value);
|
||||
}
|
||||
}
|
||||
|
||||
if (field.schema.is_nullable !== undefined && field.schema.is_nullable === false) {
|
||||
column.notNullable();
|
||||
} else {
|
||||
column.nullable();
|
||||
}
|
||||
|
||||
column.alter();
|
||||
this.addColumnToTable(table, field, true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -338,7 +270,7 @@ export class FieldsService {
|
||||
}
|
||||
}
|
||||
|
||||
if (cache) {
|
||||
if (cache && env.CACHE_AUTO_PURGE) {
|
||||
await cache.clear();
|
||||
}
|
||||
|
||||
@@ -371,9 +303,7 @@ export class FieldsService {
|
||||
/** @TODO M2A — Handle m2a case here */
|
||||
|
||||
if (isM2O) {
|
||||
await this.knex('directus_relations')
|
||||
.delete()
|
||||
.where({ many_collection: collection, many_field: field });
|
||||
await this.knex('directus_relations').delete().where({ many_collection: collection, many_field: field });
|
||||
await this.deleteField(relation.one_collection!, relation.one_field!);
|
||||
} else {
|
||||
await this.knex('directus_relations')
|
||||
@@ -382,35 +312,38 @@ export class FieldsService {
|
||||
}
|
||||
}
|
||||
|
||||
if (cache) {
|
||||
if (cache && env.CACHE_AUTO_PURGE) {
|
||||
await cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public addColumnToTable(table: CreateTableBuilder, field: Field) {
|
||||
public addColumnToTable(table: CreateTableBuilder, field: RawField | Field, alter: boolean = false) {
|
||||
if (!field.schema) return;
|
||||
|
||||
let column: ColumnBuilder;
|
||||
|
||||
if (field.schema?.has_auto_increment) {
|
||||
column = table.increments(field.field);
|
||||
} else if (field.type === 'string') {
|
||||
column = table.string(field.field, field.schema?.max_length || undefined);
|
||||
column = table.string(field.field, field.schema.max_length !== null ? field.schema.max_length : undefined);
|
||||
} else if (['float', 'decimal'].includes(field.type)) {
|
||||
const type = field.type as 'float' | 'decimal';
|
||||
/** @todo add precision and scale support */
|
||||
column = table[type](field.field /* precision, scale */);
|
||||
column = table[type](field.field, field.schema?.numeric_precision || 10, field.schema?.numeric_scale || 5);
|
||||
} else if (field.type === 'csv') {
|
||||
column = table.string(field.field);
|
||||
} else if (field.type === 'dateTime') {
|
||||
column = table.dateTime(field.field, { useTz: false });
|
||||
} else {
|
||||
column = table[field.type](field.field);
|
||||
}
|
||||
|
||||
if (field.schema?.default_value) {
|
||||
column.defaultTo(field.schema.default_value);
|
||||
if (field.schema.default_value !== undefined) {
|
||||
if (typeof field.schema.default_value === 'string' && field.schema.default_value.toLowerCase() === 'now()') {
|
||||
column.defaultTo(this.knex.fn.now());
|
||||
} else {
|
||||
column.defaultTo(field.schema.default_value);
|
||||
}
|
||||
}
|
||||
|
||||
if (field.schema?.is_nullable !== undefined && field.schema.is_nullable === false) {
|
||||
if (field.schema.is_nullable !== undefined && field.schema.is_nullable === false) {
|
||||
column.notNullable();
|
||||
} else {
|
||||
column.nullable();
|
||||
@@ -419,5 +352,9 @@ export class FieldsService {
|
||||
if (field.schema?.is_primary_key) {
|
||||
column.primary();
|
||||
}
|
||||
|
||||
if (alter) {
|
||||
column.alter();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { ForbiddenException } from '../exceptions';
|
||||
import { toArray } from '../utils/to-array';
|
||||
import { extension } from 'mime-types';
|
||||
import path from 'path';
|
||||
import env from '../env';
|
||||
|
||||
export class FilesService extends ItemsService {
|
||||
constructor(options: AbstractServiceOptions) {
|
||||
@@ -38,8 +39,7 @@ export class FilesService extends ItemsService {
|
||||
primaryKey = await this.create(payload);
|
||||
}
|
||||
|
||||
const fileExtension =
|
||||
(payload.type && extension(payload.type)) || path.extname(payload.filename_download);
|
||||
const fileExtension = (payload.type && extension(payload.type)) || path.extname(payload.filename_download);
|
||||
|
||||
payload.filename_disk = primaryKey + '.' + fileExtension;
|
||||
|
||||
@@ -87,7 +87,7 @@ export class FilesService extends ItemsService {
|
||||
});
|
||||
await sudoService.update(payload, primaryKey);
|
||||
|
||||
if (cache) {
|
||||
if (cache && env.CACHE_AUTO_PURGE) {
|
||||
await cache.clear();
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@ export class FilesService extends ItemsService {
|
||||
|
||||
await super.delete(keys);
|
||||
|
||||
if (cache) {
|
||||
if (cache && env.CACHE_AUTO_PURGE) {
|
||||
await cache.clear();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import Knex from 'knex';
|
||||
import database from '../database';
|
||||
import {
|
||||
AbstractServiceOptions,
|
||||
Accountability,
|
||||
Collection,
|
||||
Field,
|
||||
Relation,
|
||||
Query,
|
||||
SchemaOverview,
|
||||
} from '../types';
|
||||
import { AbstractServiceOptions, Accountability, Collection, Field, Relation, Query, SchemaOverview } from '../types';
|
||||
import {
|
||||
GraphQLString,
|
||||
GraphQLSchema,
|
||||
@@ -91,11 +83,7 @@ export class GraphQLService {
|
||||
const fieldsInSystem = await this.fieldsService.readAll();
|
||||
const relationsInSystem = (await this.relationsService.readByQuery({})) as Relation[];
|
||||
|
||||
const schema = this.getGraphQLSchema(
|
||||
collectionsInSystem,
|
||||
fieldsInSystem,
|
||||
relationsInSystem
|
||||
);
|
||||
const schema = this.getGraphQLSchema(collectionsInSystem, fieldsInSystem, relationsInSystem);
|
||||
|
||||
return schema;
|
||||
}
|
||||
@@ -113,17 +101,13 @@ export class GraphQLService {
|
||||
description: collection.meta?.note,
|
||||
fields: () => {
|
||||
const fieldsObject: GraphQLFieldConfigMap<any, any> = {};
|
||||
const fieldsInCollection = fields.filter(
|
||||
(field) => field.collection === collection.collection
|
||||
);
|
||||
const fieldsInCollection = fields.filter((field) => field.collection === collection.collection);
|
||||
|
||||
for (const field of fieldsInCollection) {
|
||||
const relationForField = relations.find((relation) => {
|
||||
return (
|
||||
(relation.many_collection === collection.collection &&
|
||||
relation.many_field === field.field) ||
|
||||
(relation.one_collection === collection.collection &&
|
||||
relation.one_field === field.field)
|
||||
(relation.many_collection === collection.collection && relation.many_field === field.field) ||
|
||||
(relation.one_collection === collection.collection && relation.one_field === field.field)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -135,9 +119,7 @@ export class GraphQLService {
|
||||
});
|
||||
|
||||
if (relationType === 'm2o') {
|
||||
const relatedIsSystem = relationForField.one_collection!.startsWith(
|
||||
'directus_'
|
||||
);
|
||||
const relatedIsSystem = relationForField.one_collection!.startsWith('directus_');
|
||||
|
||||
const relatedType = relatedIsSystem
|
||||
? schema[relationForField.one_collection!.substring(9)].type
|
||||
@@ -147,9 +129,7 @@ export class GraphQLService {
|
||||
type: relatedType,
|
||||
};
|
||||
} else if (relationType === 'o2m') {
|
||||
const relatedIsSystem = relationForField.many_collection.startsWith(
|
||||
'directus_'
|
||||
);
|
||||
const relatedIsSystem = relationForField.many_collection.startsWith('directus_');
|
||||
|
||||
const relatedType = relatedIsSystem
|
||||
? schema[relationForField.many_collection.substring(9)].type
|
||||
@@ -170,9 +150,7 @@ export class GraphQLService {
|
||||
const types: any = [];
|
||||
|
||||
for (const relatedCollection of relatedCollections) {
|
||||
const relatedType = relatedCollection.startsWith(
|
||||
'directus_'
|
||||
)
|
||||
const relatedType = relatedCollection.startsWith('directus_')
|
||||
? schema[relatedCollection.substring(9)].type
|
||||
: schema.items[relatedCollection].type;
|
||||
|
||||
@@ -195,9 +173,7 @@ export class GraphQLService {
|
||||
}
|
||||
} else {
|
||||
fieldsObject[field.field] = {
|
||||
type: field.schema?.is_primary_key
|
||||
? GraphQLID
|
||||
: getGraphQLType(field.type),
|
||||
type: field.schema?.is_primary_key ? GraphQLID : getGraphQLType(field.type),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -293,17 +269,13 @@ export class GraphQLService {
|
||||
},
|
||||
};
|
||||
|
||||
const fieldsInCollection = fields.filter(
|
||||
(field) => field.collection === collection.collection
|
||||
);
|
||||
const fieldsInCollection = fields.filter((field) => field.collection === collection.collection);
|
||||
|
||||
for (const field of fieldsInCollection) {
|
||||
const relationForField = relations.find((relation) => {
|
||||
return (
|
||||
(relation.many_collection === collection.collection &&
|
||||
relation.many_field === field.field) ||
|
||||
(relation.one_collection === collection.collection &&
|
||||
relation.one_field === field.field)
|
||||
(relation.many_collection === collection.collection && relation.many_field === field.field) ||
|
||||
(relation.one_collection === collection.collection && relation.one_field === field.field)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -332,9 +304,7 @@ export class GraphQLService {
|
||||
* Figure out how to setup filter fields for a union type output
|
||||
*/
|
||||
} else {
|
||||
const fieldType = field.schema?.is_primary_key
|
||||
? GraphQLID
|
||||
: getGraphQLType(field.type);
|
||||
const fieldType = field.schema?.is_primary_key ? GraphQLID : getGraphQLType(field.type);
|
||||
|
||||
filterFields[field.field] = {
|
||||
type: new GraphQLInputObjectType({
|
||||
@@ -402,18 +372,13 @@ export class GraphQLService {
|
||||
|
||||
const collection = systemField ? `directus_${info.fieldName}` : info.fieldName;
|
||||
|
||||
const selections = info.fieldNodes[0]?.selectionSet?.selections?.filter(
|
||||
(node) => node.kind === 'Field'
|
||||
) as FieldNode[] | undefined;
|
||||
const selections = info.fieldNodes[0]?.selectionSet?.selections?.filter((node) => node.kind === 'Field') as
|
||||
| FieldNode[]
|
||||
| undefined;
|
||||
|
||||
if (!selections) return null;
|
||||
|
||||
return await this.getData(
|
||||
collection,
|
||||
selections,
|
||||
info.fieldNodes[0].arguments || [],
|
||||
info.variableValues
|
||||
);
|
||||
return await this.getData(collection, selections, info.fieldNodes[0].arguments || [], info.variableValues);
|
||||
}
|
||||
|
||||
async getData(
|
||||
@@ -436,9 +401,7 @@ export class GraphQLService {
|
||||
fields.push(current);
|
||||
} else {
|
||||
const children = parseFields(
|
||||
selection.selectionSet.selections.filter(
|
||||
(selection) => selection.kind === 'Field'
|
||||
) as FieldNode[],
|
||||
selection.selectionSet.selections.filter((selection) => selection.kind === 'Field') as FieldNode[],
|
||||
current
|
||||
);
|
||||
fields.push(...children);
|
||||
@@ -447,10 +410,7 @@ export class GraphQLService {
|
||||
if (selection.arguments && selection.arguments.length > 0) {
|
||||
if (!query.deep) query.deep = {};
|
||||
|
||||
const args: Record<string, any> = this.parseArgs(
|
||||
selection.arguments,
|
||||
variableValues
|
||||
);
|
||||
const args: Record<string, any> = this.parseArgs(selection.arguments, variableValues);
|
||||
query.deep[current] = sanitizeQuery(args, this.accountability);
|
||||
}
|
||||
}
|
||||
@@ -458,9 +418,7 @@ export class GraphQLService {
|
||||
return fields;
|
||||
};
|
||||
|
||||
query.fields = parseFields(
|
||||
selections.filter((selection) => selection.kind === 'Field') as FieldNode[]
|
||||
);
|
||||
query.fields = parseFields(selections.filter((selection) => selection.kind === 'Field') as FieldNode[]);
|
||||
|
||||
let service: ItemsService;
|
||||
|
||||
@@ -550,18 +508,10 @@ export class GraphQLService {
|
||||
}
|
||||
|
||||
const collectionInfo =
|
||||
(await this.knex
|
||||
.select('singleton')
|
||||
.from('directus_collections')
|
||||
.where({ collection: collection })
|
||||
.first()) ||
|
||||
systemCollectionRows.find(
|
||||
(collectionMeta) => collectionMeta?.collection === collection
|
||||
);
|
||||
(await this.knex.select('singleton').from('directus_collections').where({ collection: collection }).first()) ||
|
||||
systemCollectionRows.find((collectionMeta) => collectionMeta?.collection === collection);
|
||||
|
||||
const result = collectionInfo?.singleton
|
||||
? await service.readSingleton(query)
|
||||
: await service.readByQuery(query);
|
||||
const result = collectionInfo?.singleton ? await service.readSingleton(query) : await service.readByQuery(query);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -596,10 +546,7 @@ export class GraphQLService {
|
||||
|
||||
argsObject[argument.name.value] = values;
|
||||
} else {
|
||||
argsObject[argument.name.value] = (argument.value as
|
||||
| IntValueNode
|
||||
| StringValueNode
|
||||
| BooleanValueNode).value;
|
||||
argsObject[argument.name.value] = (argument.value as IntValueNode | StringValueNode | BooleanValueNode).value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import cache from '../cache';
|
||||
import emitter from '../emitter';
|
||||
import logger from '../logger';
|
||||
import { toArray } from '../utils/to-array';
|
||||
import env from '../env';
|
||||
|
||||
import { PayloadService } from './payload';
|
||||
import { AuthorizationService } from './authorization';
|
||||
@@ -37,9 +38,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
this.collection = collection;
|
||||
this.knex = options.knex || database;
|
||||
this.accountability = options.accountability || null;
|
||||
this.eventScope = this.collection.startsWith('directus_')
|
||||
? this.collection.substring(9)
|
||||
: 'items';
|
||||
this.eventScope = this.collection.startsWith('directus_') ? this.collection.substring(9) : 'items';
|
||||
this.schema = options.schema;
|
||||
|
||||
return this;
|
||||
@@ -60,19 +59,15 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
const customProcessed = await emitter.emitAsync(
|
||||
`${this.eventScope}.create.before`,
|
||||
payloads,
|
||||
{
|
||||
event: `${this.eventScope}.create.before`,
|
||||
accountability: this.accountability,
|
||||
collection: this.collection,
|
||||
item: null,
|
||||
action: 'create',
|
||||
payload: payloads,
|
||||
schema: this.schema,
|
||||
}
|
||||
);
|
||||
const customProcessed = await emitter.emitAsync(`${this.eventScope}.create.before`, payloads, {
|
||||
event: `${this.eventScope}.create.before`,
|
||||
accountability: this.accountability,
|
||||
collection: this.collection,
|
||||
item: null,
|
||||
action: 'create',
|
||||
payload: payloads,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
if (customProcessed) {
|
||||
payloads = customProcessed[customProcessed.length - 1];
|
||||
@@ -85,21 +80,15 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
payloads = await authorizationService.validatePayload(
|
||||
'create',
|
||||
this.collection,
|
||||
payloads
|
||||
);
|
||||
payloads = await authorizationService.validatePayload('create', this.collection, payloads);
|
||||
}
|
||||
|
||||
payloads = await payloadService.processM2O(payloads);
|
||||
payloads = await payloadService.processA2O(payloads);
|
||||
|
||||
let payloadsWithoutAliases = payloads.map((payload) => pick(payload, columns));
|
||||
|
||||
payloadsWithoutAliases = await payloadService.processValues(
|
||||
'create',
|
||||
payloadsWithoutAliases
|
||||
);
|
||||
payloadsWithoutAliases = await payloadService.processValues('create', payloadsWithoutAliases);
|
||||
|
||||
const primaryKeys: PrimaryKey[] = [];
|
||||
|
||||
@@ -148,11 +137,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
|
||||
let primaryKey;
|
||||
|
||||
const result = await trx
|
||||
.select('id')
|
||||
.from('directus_activity')
|
||||
.orderBy('id', 'desc')
|
||||
.first();
|
||||
const result = await trx.select('id').from('directus_activity').orderBy('id', 'desc').first();
|
||||
|
||||
primaryKey = result.id;
|
||||
|
||||
@@ -170,7 +155,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
await trx.insert(revisionRecords).into('directus_revisions');
|
||||
}
|
||||
|
||||
if (cache) {
|
||||
if (cache && env.CACHE_AUTO_PURGE) {
|
||||
await cache.clear();
|
||||
}
|
||||
|
||||
@@ -212,16 +197,8 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
return records as Partial<Item> | Partial<Item>[] | null;
|
||||
}
|
||||
|
||||
readByKey(
|
||||
keys: PrimaryKey[],
|
||||
query?: Query,
|
||||
action?: PermissionsAction
|
||||
): Promise<null | Partial<Item>[]>;
|
||||
readByKey(
|
||||
key: PrimaryKey,
|
||||
query?: Query,
|
||||
action?: PermissionsAction
|
||||
): Promise<null | Partial<Item>>;
|
||||
readByKey(keys: PrimaryKey[], query?: Query, action?: PermissionsAction): Promise<null | Partial<Item>[]>;
|
||||
readByKey(key: PrimaryKey, query?: Query, action?: PermissionsAction): Promise<null | Partial<Item>>;
|
||||
async readByKey(
|
||||
key: PrimaryKey | PrimaryKey[],
|
||||
query: Query = {},
|
||||
@@ -284,19 +261,15 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
|
||||
let payload: Partial<AnyItem> | Partial<AnyItem>[] = clone(data);
|
||||
|
||||
const customProcessed = await emitter.emitAsync(
|
||||
`${this.eventScope}.update.before`,
|
||||
const customProcessed = await emitter.emitAsync(`${this.eventScope}.update.before`, payload, {
|
||||
event: `${this.eventScope}.update.before`,
|
||||
accountability: this.accountability,
|
||||
collection: this.collection,
|
||||
item: key,
|
||||
action: 'update',
|
||||
payload,
|
||||
{
|
||||
event: `${this.eventScope}.update.before`,
|
||||
accountability: this.accountability,
|
||||
collection: this.collection,
|
||||
item: key,
|
||||
action: 'update',
|
||||
payload,
|
||||
schema: this.schema,
|
||||
}
|
||||
);
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
if (customProcessed) {
|
||||
payload = customProcessed[customProcessed.length - 1];
|
||||
@@ -311,11 +284,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
|
||||
await authorizationService.checkAccess('update', this.collection, keys);
|
||||
|
||||
payload = await authorizationService.validatePayload(
|
||||
'update',
|
||||
this.collection,
|
||||
payload
|
||||
);
|
||||
payload = await authorizationService.validatePayload('update', this.collection, payload);
|
||||
}
|
||||
|
||||
await this.knex.transaction(async (trx) => {
|
||||
@@ -326,18 +295,14 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
});
|
||||
|
||||
payload = await payloadService.processM2O(payload);
|
||||
payload = await payloadService.processA2O(payload);
|
||||
|
||||
let payloadWithoutAliases = pick(payload, columns);
|
||||
|
||||
payloadWithoutAliases = await payloadService.processValues(
|
||||
'update',
|
||||
payloadWithoutAliases
|
||||
);
|
||||
payloadWithoutAliases = await payloadService.processValues('update', payloadWithoutAliases);
|
||||
|
||||
if (Object.keys(payloadWithoutAliases).length > 0) {
|
||||
await trx(this.collection)
|
||||
.update(payloadWithoutAliases)
|
||||
.whereIn(primaryKeyField, keys);
|
||||
await trx(this.collection).update(payloadWithoutAliases).whereIn(primaryKeyField, keys);
|
||||
}
|
||||
|
||||
for (const key of keys) {
|
||||
@@ -360,11 +325,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
await trx.insert(activityRecord).into('directus_activity');
|
||||
let primaryKey;
|
||||
|
||||
const result = await trx
|
||||
.select('id')
|
||||
.from('directus_activity')
|
||||
.orderBy('id', 'desc')
|
||||
.first();
|
||||
const result = await trx.select('id').from('directus_activity').orderBy('id', 'desc').first();
|
||||
|
||||
primaryKey = result.id;
|
||||
activityPrimaryKeys.push(primaryKey);
|
||||
@@ -381,9 +342,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
collection: this.collection,
|
||||
item: keys[index],
|
||||
data:
|
||||
snapshots && Array.isArray(snapshots)
|
||||
? JSON.stringify(snapshots?.[index])
|
||||
: JSON.stringify(snapshots),
|
||||
snapshots && Array.isArray(snapshots) ? JSON.stringify(snapshots?.[index]) : JSON.stringify(snapshots),
|
||||
delta: JSON.stringify(payloadWithoutAliases),
|
||||
}));
|
||||
|
||||
@@ -391,7 +350,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
}
|
||||
});
|
||||
|
||||
if (cache) {
|
||||
if (cache && env.CACHE_AUTO_PURGE) {
|
||||
await cache.clear();
|
||||
}
|
||||
|
||||
@@ -452,9 +411,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
let itemsToUpdate = await itemsService.readByQuery(readQuery);
|
||||
itemsToUpdate = toArray(itemsToUpdate);
|
||||
|
||||
const keys: PrimaryKey[] = itemsToUpdate.map(
|
||||
(item: Partial<Item>) => item[primaryKeyField]
|
||||
);
|
||||
const keys: PrimaryKey[] = itemsToUpdate.map((item: Partial<Item>) => item[primaryKeyField]);
|
||||
|
||||
return await this.update(data, keys);
|
||||
}
|
||||
@@ -530,7 +487,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
}
|
||||
});
|
||||
|
||||
if (cache) {
|
||||
if (cache && env.CACHE_AUTO_PURGE) {
|
||||
await cache.clear();
|
||||
}
|
||||
|
||||
@@ -563,9 +520,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
let itemsToDelete = await itemsService.readByQuery(readQuery);
|
||||
itemsToDelete = toArray(itemsToDelete);
|
||||
|
||||
const keys: PrimaryKey[] = itemsToDelete.map(
|
||||
(item: Partial<Item>) => item[primaryKeyField]
|
||||
);
|
||||
const keys: PrimaryKey[] = itemsToDelete.map((item: Partial<Item>) => item[primaryKeyField]);
|
||||
return await this.delete(keys);
|
||||
}
|
||||
|
||||
@@ -598,11 +553,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
|
||||
async upsertSingleton(data: Partial<Item>) {
|
||||
const primaryKeyField = this.schema[this.collection].primary;
|
||||
|
||||
const record = await this.knex
|
||||
.select(primaryKeyField)
|
||||
.from(this.collection)
|
||||
.limit(1)
|
||||
.first();
|
||||
const record = await this.knex.select(primaryKeyField).from(this.collection).limit(1).first();
|
||||
|
||||
if (record) {
|
||||
return await this.update(data, record.id);
|
||||
|
||||
@@ -7,14 +7,7 @@ import argon2 from 'argon2';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import database from '../database';
|
||||
import { clone, isObject, cloneDeep } from 'lodash';
|
||||
import {
|
||||
Relation,
|
||||
Item,
|
||||
AbstractServiceOptions,
|
||||
Accountability,
|
||||
PrimaryKey,
|
||||
SchemaOverview,
|
||||
} from '../types';
|
||||
import { Relation, Item, AbstractServiceOptions, Accountability, PrimaryKey, SchemaOverview } from '../types';
|
||||
import { ItemsService } from './items';
|
||||
import { URL } from 'url';
|
||||
import Knex from 'knex';
|
||||
@@ -26,6 +19,8 @@ import { toArray } from '../utils/to-array';
|
||||
import { FieldMeta } from '../types';
|
||||
import { systemFieldRows } from '../database/system-data/fields';
|
||||
import { systemRelationRows } from '../database/system-data/relations';
|
||||
import { InvalidPayloadException } from '../exceptions';
|
||||
import { isPlainObject } from 'lodash';
|
||||
|
||||
type Action = 'create' | 'read' | 'update';
|
||||
|
||||
@@ -165,9 +160,7 @@ export class PayloadService {
|
||||
.where({ collection: this.collection })
|
||||
.whereNotNull('special');
|
||||
|
||||
specialFieldsInCollection.push(
|
||||
...systemFieldRows.filter((fieldMeta) => fieldMeta.collection === this.collection)
|
||||
);
|
||||
specialFieldsInCollection.push(...systemFieldRows.filter((fieldMeta) => fieldMeta.collection === this.collection));
|
||||
|
||||
if (action === 'read') {
|
||||
specialFieldsInCollection = specialFieldsInCollection.filter((fieldMeta) => {
|
||||
@@ -179,12 +172,7 @@ export class PayloadService {
|
||||
processedPayload.map(async (record: any) => {
|
||||
await Promise.all(
|
||||
specialFieldsInCollection.map(async (field) => {
|
||||
const newValue = await this.processField(
|
||||
field,
|
||||
record,
|
||||
action,
|
||||
this.accountability
|
||||
);
|
||||
const newValue = await this.processField(field, record, action, this.accountability);
|
||||
if (newValue !== undefined) record[field.field] = newValue;
|
||||
})
|
||||
);
|
||||
@@ -198,12 +186,7 @@ export class PayloadService {
|
||||
if (['create', 'update'].includes(action)) {
|
||||
processedPayload.forEach((record) => {
|
||||
for (const [key, value] of Object.entries(record)) {
|
||||
if (
|
||||
Array.isArray(value) ||
|
||||
(typeof value === 'object' &&
|
||||
value instanceof Date !== true &&
|
||||
value !== null)
|
||||
) {
|
||||
if (Array.isArray(value) || (typeof value === 'object' && value instanceof Date !== true && value !== null)) {
|
||||
record[key] = JSON.stringify(value);
|
||||
}
|
||||
}
|
||||
@@ -217,12 +200,7 @@ export class PayloadService {
|
||||
return processedPayload[0];
|
||||
}
|
||||
|
||||
async processField(
|
||||
field: FieldMeta,
|
||||
payload: Partial<Item>,
|
||||
action: Action,
|
||||
accountability: Accountability | null
|
||||
) {
|
||||
async processField(field: FieldMeta, payload: Partial<Item>, action: Action, accountability: Accountability | null) {
|
||||
if (!field.special) return payload[field.field];
|
||||
const fieldSpecials = field.special ? toArray(field.special) : [];
|
||||
|
||||
@@ -254,9 +232,7 @@ export class PayloadService {
|
||||
type: getLocalType(column),
|
||||
}));
|
||||
|
||||
const dateColumns = columnsWithType.filter((column) =>
|
||||
['dateTime', 'date', 'timestamp'].includes(column.type)
|
||||
);
|
||||
const dateColumns = columnsWithType.filter((column) => ['dateTime', 'date', 'timestamp'].includes(column.type));
|
||||
|
||||
if (dateColumns.length === 0) return payloads;
|
||||
|
||||
@@ -296,34 +272,99 @@ export class PayloadService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively save/update all nested related m2o items
|
||||
* Recursively save/update all nested related Any-to-One items
|
||||
*/
|
||||
processM2O(payloads: Partial<Item>[]): Promise<Partial<Item>[]>;
|
||||
processM2O(payloads: Partial<Item>): Promise<Partial<Item>>;
|
||||
async processM2O(
|
||||
payload: Partial<Item> | Partial<Item>[]
|
||||
): Promise<Partial<Item> | Partial<Item>[]> {
|
||||
processA2O(payloads: Partial<Item>[]): Promise<Partial<Item>[]>;
|
||||
processA2O(payloads: Partial<Item>): Promise<Partial<Item>>;
|
||||
async processA2O(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 })),
|
||||
...systemRelationRows.filter(
|
||||
(systemRelation) => systemRelation.many_collection === this.collection
|
||||
),
|
||||
...systemRelationRows.filter((systemRelation) => systemRelation.many_collection === this.collection),
|
||||
];
|
||||
|
||||
const payloads = clone(Array.isArray(payload) ? payload : [payload]);
|
||||
const payloads = clone(toArray(payload));
|
||||
|
||||
for (let i = 0; i < payloads.length; i++) {
|
||||
let payload = payloads[i];
|
||||
|
||||
// Only process related records that are actually in the payload
|
||||
const relationsToProcess = relations.filter((relation) => {
|
||||
return (
|
||||
payload.hasOwnProperty(relation.many_field) &&
|
||||
isObject(payload[relation.many_field])
|
||||
);
|
||||
return payload.hasOwnProperty(relation.many_field) && isObject(payload[relation.many_field]);
|
||||
});
|
||||
|
||||
for (const relation of relationsToProcess) {
|
||||
if (!relation.one_collection_field || !relation.one_allowed_collections) continue;
|
||||
|
||||
if (isPlainObject(payload[relation.many_field]) === false) continue;
|
||||
|
||||
const relatedCollection = payload[relation.one_collection_field];
|
||||
|
||||
if (!relatedCollection) {
|
||||
throw new InvalidPayloadException(
|
||||
`Can't update nested record "${relation.many_collection}.${relation.many_field}" without field "${relation.many_collection}.${relation.one_collection_field}" being set`
|
||||
);
|
||||
}
|
||||
|
||||
const allowedCollections = relation.one_allowed_collections.split(',');
|
||||
|
||||
if (allowedCollections.includes(relatedCollection) === false) {
|
||||
throw new InvalidPayloadException(
|
||||
`"${relation.many_collection}.${relation.many_field}" can't be linked to collection "${relatedCollection}`
|
||||
);
|
||||
}
|
||||
|
||||
const itemsService = new ItemsService(relatedCollection, {
|
||||
accountability: this.accountability,
|
||||
knex: this.knex,
|
||||
schema: this.schema,
|
||||
});
|
||||
|
||||
const relatedPrimary = this.schema[relatedCollection].primary;
|
||||
const relatedRecord: Partial<Item> = payload[relation.many_field];
|
||||
const hasPrimaryKey = relatedRecord.hasOwnProperty(relatedPrimary);
|
||||
|
||||
let relatedPrimaryKey: PrimaryKey = relatedRecord[relatedPrimary];
|
||||
const exists = hasPrimaryKey && !!(await this.knex.select(relatedPrimary).from(relatedCollection).first());
|
||||
|
||||
if (exists) {
|
||||
await itemsService.update(relatedRecord, relatedPrimaryKey);
|
||||
} else {
|
||||
relatedPrimaryKey = await itemsService.create(relatedRecord);
|
||||
}
|
||||
|
||||
// Overwrite the nested object with just the primary key, so the parent level can be saved correctly
|
||||
payload[relation.many_field] = relatedPrimaryKey;
|
||||
}
|
||||
}
|
||||
|
||||
return Array.isArray(payload) ? payloads : payloads[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively save/update all nested related m2o items
|
||||
*/
|
||||
processM2O(payloads: Partial<Item>[]): Promise<Partial<Item>[]>;
|
||||
processM2O(payloads: Partial<Item>): Promise<Partial<Item>>;
|
||||
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 })),
|
||||
...systemRelationRows.filter((systemRelation) => systemRelation.many_collection === this.collection),
|
||||
];
|
||||
|
||||
const payloads = clone(toArray(payload));
|
||||
|
||||
for (let i = 0; i < payloads.length; i++) {
|
||||
let payload = payloads[i];
|
||||
|
||||
// Only process related records that are actually in the payload
|
||||
const relationsToProcess = relations.filter((relation) => {
|
||||
return payload.hasOwnProperty(relation.many_field) && isObject(payload[relation.many_field]);
|
||||
});
|
||||
|
||||
for (const relation of relationsToProcess) {
|
||||
@@ -341,7 +382,8 @@ export class PayloadService {
|
||||
if (['string', 'number'].includes(typeof relatedRecord)) continue;
|
||||
|
||||
let relatedPrimaryKey: PrimaryKey = relatedRecord[relation.one_primary];
|
||||
const exists = hasPrimaryKey && !!(await itemsService.readByKey(relatedPrimaryKey));
|
||||
const exists =
|
||||
hasPrimaryKey && !!(await this.knex.select(relation.one_primary).from(relation.one_collection).first());
|
||||
|
||||
if (exists) {
|
||||
await itemsService.update(relatedRecord, relatedPrimaryKey);
|
||||
@@ -366,9 +408,7 @@ export class PayloadService {
|
||||
.select<Relation[]>('*')
|
||||
.from('directus_relations')
|
||||
.where({ one_collection: this.collection })),
|
||||
...systemRelationRows.filter(
|
||||
(systemRelation) => systemRelation.one_collection === this.collection
|
||||
),
|
||||
...systemRelationRows.filter((systemRelation) => systemRelation.one_collection === this.collection),
|
||||
];
|
||||
|
||||
const payloads = clone(toArray(payload));
|
||||
@@ -397,10 +437,7 @@ export class PayloadService {
|
||||
for (const relatedRecord of payload[relation.one_field!] || []) {
|
||||
let record = cloneDeep(relatedRecord);
|
||||
|
||||
if (
|
||||
typeof relatedRecord === 'string' ||
|
||||
typeof relatedRecord === 'number'
|
||||
) {
|
||||
if (typeof relatedRecord === 'string' || typeof relatedRecord === 'number') {
|
||||
const exists = !!(await this.knex
|
||||
.select(relation.many_primary)
|
||||
.from(relation.many_collection)
|
||||
|
||||
@@ -7,19 +7,13 @@ export class PermissionsService extends ItemsService {
|
||||
}
|
||||
|
||||
async getAllowedCollections(role: string | null, action: PermissionsAction) {
|
||||
const query = this.knex
|
||||
.select('collection')
|
||||
.from('directus_permissions')
|
||||
.where({ role, action });
|
||||
const query = this.knex.select('collection').from('directus_permissions').where({ role, action });
|
||||
const results = await query;
|
||||
return results.map((result) => result.collection);
|
||||
}
|
||||
|
||||
async getAllowedFields(role: string | null, action: PermissionsAction, collection?: string) {
|
||||
const query = this.knex
|
||||
.select('collection', 'fields')
|
||||
.from('directus_permissions')
|
||||
.where({ role, action });
|
||||
const query = this.knex.select('collection', 'fields').from('directus_permissions').where({ role, action });
|
||||
|
||||
if (collection) {
|
||||
query.andWhere({ collection });
|
||||
|
||||
@@ -26,10 +26,7 @@ export class RelationsService extends ItemsService {
|
||||
knex: this.knex,
|
||||
schema: this.schema,
|
||||
});
|
||||
const results = (await service.readByQuery(query)) as
|
||||
| ParsedRelation
|
||||
| ParsedRelation[]
|
||||
| null;
|
||||
const results = (await service.readByQuery(query)) as ParsedRelation | ParsedRelation[] | null;
|
||||
|
||||
if (results && Array.isArray(results)) {
|
||||
results.push(...(systemRelationRows as ParsedRelation[]));
|
||||
@@ -40,11 +37,7 @@ export class RelationsService extends ItemsService {
|
||||
return filteredResults;
|
||||
}
|
||||
|
||||
readByKey(
|
||||
keys: PrimaryKey[],
|
||||
query?: Query,
|
||||
action?: PermissionsAction
|
||||
): Promise<null | Relation[]>;
|
||||
readByKey(keys: PrimaryKey[], query?: Query, action?: PermissionsAction): Promise<null | Relation[]>;
|
||||
readByKey(key: PrimaryKey, query?: Query, action?: PermissionsAction): Promise<null | Relation>;
|
||||
async readByKey(
|
||||
key: PrimaryKey | PrimaryKey[],
|
||||
@@ -55,10 +48,7 @@ export class RelationsService extends ItemsService {
|
||||
knex: this.knex,
|
||||
schema: this.schema,
|
||||
});
|
||||
const results = (await service.readByKey(key as any, query, action)) as
|
||||
| ParsedRelation
|
||||
| ParsedRelation[]
|
||||
| null;
|
||||
const results = (await service.readByKey(key as any, query, action)) as ParsedRelation | ParsedRelation[] | null;
|
||||
|
||||
// No need to merge system relations here. They don't have PKs so can never be directly
|
||||
// targetted
|
||||
@@ -76,10 +66,7 @@ export class RelationsService extends ItemsService {
|
||||
'read'
|
||||
);
|
||||
|
||||
const allowedFields = await this.permissionsService.getAllowedFields(
|
||||
this.accountability?.role || null,
|
||||
'read'
|
||||
);
|
||||
const allowedFields = await this.permissionsService.getAllowedFields(this.accountability?.role || null, 'read');
|
||||
|
||||
relations = toArray(relations);
|
||||
|
||||
@@ -91,18 +78,13 @@ export class RelationsService extends ItemsService {
|
||||
collectionsAllowed = false;
|
||||
}
|
||||
|
||||
if (
|
||||
relation.one_collection &&
|
||||
allowedCollections.includes(relation.one_collection) === false
|
||||
) {
|
||||
if (relation.one_collection && allowedCollections.includes(relation.one_collection) === false) {
|
||||
collectionsAllowed = false;
|
||||
}
|
||||
|
||||
if (
|
||||
relation.one_allowed_collections &&
|
||||
relation.one_allowed_collections.every((collection) =>
|
||||
allowedCollections.includes(collection)
|
||||
) === false
|
||||
relation.one_allowed_collections.every((collection) => allowedCollections.includes(collection)) === false
|
||||
) {
|
||||
collectionsAllowed = false;
|
||||
}
|
||||
@@ -120,8 +102,7 @@ export class RelationsService extends ItemsService {
|
||||
relation.one_field &&
|
||||
(!allowedFields[relation.one_collection] ||
|
||||
(allowedFields[relation.one_collection].includes('*') === false &&
|
||||
allowedFields[relation.one_collection].includes(relation.one_field) ===
|
||||
false))
|
||||
allowedFields[relation.one_collection].includes(relation.one_field) === false))
|
||||
) {
|
||||
fieldsAllowed = false;
|
||||
}
|
||||
|
||||
@@ -15,8 +15,7 @@ export class RevisionsService extends ItemsService {
|
||||
const revision = (await super.readByKey(pk)) as Revision | null;
|
||||
if (!revision) throw new ForbiddenException();
|
||||
|
||||
if (!revision.data)
|
||||
throw new InvalidPayloadException(`Revision doesn't contain data to revert to`);
|
||||
if (!revision.data) throw new InvalidPayloadException(`Revision doesn't contain data to revert to`);
|
||||
|
||||
const service = new ItemsService(revision.collection, {
|
||||
accountability: this.accountability,
|
||||
|
||||
@@ -24,8 +24,7 @@ export class RolesService extends ItemsService {
|
||||
.andWhere({ admin_access: true })
|
||||
.first();
|
||||
const otherAdminRolesCount = +(otherAdminRoles?.count || 0);
|
||||
if (otherAdminRolesCount === 0)
|
||||
throw new UnprocessableEntityException(`You can't delete the last admin role.`);
|
||||
if (otherAdminRolesCount === 0) throw new UnprocessableEntityException(`You can't delete the last admin role.`);
|
||||
|
||||
// Remove all permissions associated with this role
|
||||
const permissionsService = new PermissionsService({
|
||||
|
||||
@@ -40,10 +40,7 @@ export class ServerService {
|
||||
if (this.accountability?.admin === true) {
|
||||
const osType = os.type() === 'Darwin' ? 'macOS' : os.type();
|
||||
|
||||
const osVersion =
|
||||
osType === 'macOS'
|
||||
? `${macosRelease().name} (${macosRelease().version})`
|
||||
: os.release();
|
||||
const osVersion = osType === 'macOS' ? `${macosRelease().name} (${macosRelease().version})` : os.release();
|
||||
|
||||
info.directus = {
|
||||
version,
|
||||
|
||||
@@ -14,13 +14,7 @@ import formatTitle from '@directus/format-title';
|
||||
import { cloneDeep, mergeWith } from 'lodash';
|
||||
import { RelationsService } from './relations';
|
||||
import env from '../env';
|
||||
import {
|
||||
OpenAPIObject,
|
||||
PathItemObject,
|
||||
OperationObject,
|
||||
TagObject,
|
||||
SchemaObject,
|
||||
} from 'openapi3-ts';
|
||||
import { OpenAPIObject, PathItemObject, OperationObject, TagObject, SchemaObject } from 'openapi3-ts';
|
||||
|
||||
// @ts-ignore
|
||||
import { version } from '../../package.json';
|
||||
@@ -110,8 +104,7 @@ class OASService implements SpecificationSubService {
|
||||
openapi: '3.0.1',
|
||||
info: {
|
||||
title: 'Dynamic API Specification',
|
||||
description:
|
||||
'This is a dynamicly generated API specification for all endpoints existing on the current .',
|
||||
description: 'This is a dynamicly generated API specification for all endpoints existing on the current .',
|
||||
version: version,
|
||||
},
|
||||
servers: [
|
||||
@@ -164,18 +157,13 @@ class OASService implements SpecificationSubService {
|
||||
return tags.filter((tag) => tag.name !== 'Items');
|
||||
}
|
||||
|
||||
private async generatePaths(
|
||||
permissions: Permission[],
|
||||
tags: OpenAPIObject['tags']
|
||||
): Promise<OpenAPIObject['paths']> {
|
||||
private async generatePaths(permissions: Permission[], tags: OpenAPIObject['tags']): Promise<OpenAPIObject['paths']> {
|
||||
const paths: OpenAPIObject['paths'] = {};
|
||||
|
||||
if (!tags) return paths;
|
||||
|
||||
for (const tag of tags) {
|
||||
const isSystem =
|
||||
tag.hasOwnProperty('x-collection') === false ||
|
||||
tag['x-collection'].startsWith('directus_');
|
||||
const isSystem = tag.hasOwnProperty('x-collection') === false || tag['x-collection'].startsWith('directus_');
|
||||
|
||||
if (isSystem) {
|
||||
for (const [path, pathItem] of Object.entries<PathItemObject>(openapi.paths)) {
|
||||
@@ -210,23 +198,18 @@ class OASService implements SpecificationSubService {
|
||||
this.accountability?.admin === true ||
|
||||
!!permissions.find(
|
||||
(permission) =>
|
||||
permission.collection === collection &&
|
||||
permission.action === this.getActionForMethod(method)
|
||||
permission.collection === collection && permission.action === this.getActionForMethod(method)
|
||||
);
|
||||
|
||||
if (hasPermission) {
|
||||
if (!paths[`/items/${collection}`]) paths[`/items/${collection}`] = {};
|
||||
if (!paths[`/items/${collection}/{id}`])
|
||||
paths[`/items/${collection}/{id}`] = {};
|
||||
if (!paths[`/items/${collection}/{id}`]) paths[`/items/${collection}/{id}`] = {};
|
||||
|
||||
if (listBase[method]) {
|
||||
paths[`/items/${collection}`][method] = mergeWith(
|
||||
cloneDeep(listBase[method]),
|
||||
{
|
||||
description: listBase[method].description.replace(
|
||||
'item',
|
||||
collection + ' item'
|
||||
),
|
||||
description: listBase[method].description.replace('item', collection + ' item'),
|
||||
tags: [tag.name],
|
||||
operationId: `${this.getActionForMethod(method)}${tag.name}`,
|
||||
requestBody: ['get', 'delete'].includes(method)
|
||||
@@ -281,14 +264,9 @@ class OASService implements SpecificationSubService {
|
||||
paths[`/items/${collection}/{id}`][method] = mergeWith(
|
||||
cloneDeep(detailBase[method]),
|
||||
{
|
||||
description: detailBase[method].description.replace(
|
||||
'item',
|
||||
collection + ' item'
|
||||
),
|
||||
description: detailBase[method].description.replace('item', collection + ' item'),
|
||||
tags: [tag.name],
|
||||
operationId: `${this.getActionForMethod(method)}Single${
|
||||
tag.name
|
||||
}`,
|
||||
operationId: `${this.getActionForMethod(method)}Single${tag.name}`,
|
||||
requestBody: ['get', 'delete'].includes(method)
|
||||
? undefined
|
||||
: {
|
||||
@@ -355,23 +333,17 @@ class OASService implements SpecificationSubService {
|
||||
|
||||
const isSystem = collection.collection.startsWith('directus_');
|
||||
|
||||
const fieldsInCollection = fields.filter(
|
||||
(field) => field.collection === collection.collection
|
||||
);
|
||||
const fieldsInCollection = fields.filter((field) => field.collection === collection.collection);
|
||||
|
||||
if (isSystem) {
|
||||
const schemaComponent: SchemaObject = cloneDeep(
|
||||
openapi.components!.schemas![tag.name]
|
||||
);
|
||||
const schemaComponent: SchemaObject = cloneDeep(openapi.components!.schemas![tag.name]);
|
||||
|
||||
schemaComponent.properties = {};
|
||||
|
||||
for (const field of fieldsInCollection) {
|
||||
schemaComponent.properties[field.field] =
|
||||
(cloneDeep(
|
||||
(openapi.components!.schemas![tag.name] as SchemaObject).properties![
|
||||
field.field
|
||||
]
|
||||
(openapi.components!.schemas![tag.name] as SchemaObject).properties![field.field]
|
||||
) as SchemaObject) || this.generateField(field, relations, tags, fields);
|
||||
}
|
||||
|
||||
@@ -384,12 +356,7 @@ class OASService implements SpecificationSubService {
|
||||
};
|
||||
|
||||
for (const field of fieldsInCollection) {
|
||||
schemaComponent.properties![field.field] = this.generateField(
|
||||
field,
|
||||
relations,
|
||||
tags,
|
||||
fields
|
||||
);
|
||||
schemaComponent.properties![field.field] = this.generateField(field, relations, tags, fields);
|
||||
}
|
||||
|
||||
components.schemas[tag.name] = schemaComponent;
|
||||
@@ -413,12 +380,7 @@ class OASService implements SpecificationSubService {
|
||||
}
|
||||
}
|
||||
|
||||
private generateField(
|
||||
field: Field,
|
||||
relations: Relation[],
|
||||
tags: TagObject[],
|
||||
fields: Field[]
|
||||
): SchemaObject {
|
||||
private generateField(field: Field, relations: Relation[], tags: TagObject[], fields: Field[]): SchemaObject {
|
||||
let propertyObject: SchemaObject = {
|
||||
nullable: field.schema?.is_nullable,
|
||||
description: field.meta?.note || undefined,
|
||||
@@ -426,8 +388,7 @@ class OASService implements SpecificationSubService {
|
||||
|
||||
const relation = relations.find(
|
||||
(relation) =>
|
||||
(relation.many_collection === field.collection &&
|
||||
relation.many_field === field.field) ||
|
||||
(relation.many_collection === field.collection && relation.many_field === field.field) ||
|
||||
(relation.one_collection === field.collection && relation.one_field === field.field)
|
||||
);
|
||||
|
||||
@@ -444,12 +405,9 @@ class OASService implements SpecificationSubService {
|
||||
});
|
||||
|
||||
if (relationType === 'm2o') {
|
||||
const relatedTag = tags.find(
|
||||
(tag) => tag['x-collection'] === relation.one_collection
|
||||
);
|
||||
const relatedTag = tags.find((tag) => tag['x-collection'] === relation.one_collection);
|
||||
const relatedPrimaryKeyField = fields.find(
|
||||
(field) =>
|
||||
field.collection === relation.one_collection && field.schema?.is_primary_key
|
||||
(field) => field.collection === relation.one_collection && field.schema?.is_primary_key
|
||||
);
|
||||
|
||||
if (!relatedTag || !relatedPrimaryKeyField) return propertyObject;
|
||||
@@ -463,13 +421,9 @@ class OASService implements SpecificationSubService {
|
||||
},
|
||||
];
|
||||
} else if (relationType === 'o2m') {
|
||||
const relatedTag = tags.find(
|
||||
(tag) => tag['x-collection'] === relation.many_collection
|
||||
);
|
||||
const relatedTag = tags.find((tag) => tag['x-collection'] === relation.many_collection);
|
||||
const relatedPrimaryKeyField = fields.find(
|
||||
(field) =>
|
||||
field.collection === relation.many_collection &&
|
||||
field.schema?.is_primary_key
|
||||
(field) => field.collection === relation.many_collection && field.schema?.is_primary_key
|
||||
);
|
||||
|
||||
if (!relatedTag || !relatedPrimaryKeyField) return propertyObject;
|
||||
@@ -486,9 +440,7 @@ class OASService implements SpecificationSubService {
|
||||
],
|
||||
};
|
||||
} else if (relationType === 'm2a') {
|
||||
const relatedTags = tags.filter((tag) =>
|
||||
relation.one_allowed_collections!.includes(tag['x-collection'])
|
||||
);
|
||||
const relatedTags = tags.filter((tag) => relation.one_allowed_collections!.includes(tag['x-collection']));
|
||||
|
||||
propertyObject.type = 'array';
|
||||
propertyObject.items = {
|
||||
@@ -510,15 +462,7 @@ class OASService implements SpecificationSubService {
|
||||
private fieldTypes: Record<
|
||||
typeof types[number],
|
||||
{
|
||||
type:
|
||||
| 'string'
|
||||
| 'number'
|
||||
| 'boolean'
|
||||
| 'object'
|
||||
| 'array'
|
||||
| 'integer'
|
||||
| 'null'
|
||||
| undefined;
|
||||
type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'integer' | 'null' | undefined;
|
||||
format?: string;
|
||||
items?: any;
|
||||
}
|
||||
|
||||
@@ -4,11 +4,7 @@ import jwt from 'jsonwebtoken';
|
||||
import { sendInviteMail, sendPasswordResetMail } from '../mail';
|
||||
import database from '../database';
|
||||
import argon2 from 'argon2';
|
||||
import {
|
||||
InvalidPayloadException,
|
||||
ForbiddenException,
|
||||
UnprocessableEntityException,
|
||||
} from '../exceptions';
|
||||
import { InvalidPayloadException, ForbiddenException, UnprocessableEntityException } from '../exceptions';
|
||||
import { Accountability, PrimaryKey, Item, AbstractServiceOptions, SchemaOverview } from '../types';
|
||||
import Knex from 'knex';
|
||||
import env from '../env';
|
||||
@@ -50,7 +46,7 @@ export class UsersService extends ItemsService {
|
||||
}
|
||||
}
|
||||
|
||||
if (cache) {
|
||||
if (cache && env.CACHE_AUTO_PURGE) {
|
||||
await cache.clear();
|
||||
}
|
||||
|
||||
@@ -104,11 +100,7 @@ export class UsersService extends ItemsService {
|
||||
|
||||
if (scope !== 'invite') throw new ForbiddenException();
|
||||
|
||||
const user = await this.knex
|
||||
.select('id', 'status')
|
||||
.from('directus_users')
|
||||
.where({ email })
|
||||
.first();
|
||||
const user = await this.knex.select('id', 'status').from('directus_users').where({ email }).first();
|
||||
|
||||
if (!user || user.status !== 'invited') {
|
||||
throw new InvalidPayloadException(`Email address ${email} hasn't been invited.`);
|
||||
@@ -116,11 +108,9 @@ export class UsersService extends ItemsService {
|
||||
|
||||
const passwordHashed = await argon2.hash(password);
|
||||
|
||||
await this.knex('directus_users')
|
||||
.update({ password: passwordHashed, status: 'active' })
|
||||
.where({ id: user.id });
|
||||
await this.knex('directus_users').update({ password: passwordHashed, status: 'active' }).where({ id: user.id });
|
||||
|
||||
if (cache) {
|
||||
if (cache && env.CACHE_AUTO_PURGE) {
|
||||
await cache.clear();
|
||||
}
|
||||
}
|
||||
@@ -146,11 +136,7 @@ export class UsersService extends ItemsService {
|
||||
|
||||
if (scope !== 'password-reset') throw new ForbiddenException();
|
||||
|
||||
const user = await this.knex
|
||||
.select('id', 'status')
|
||||
.from('directus_users')
|
||||
.where({ email })
|
||||
.first();
|
||||
const user = await this.knex.select('id', 'status').from('directus_users').where({ email }).first();
|
||||
|
||||
if (!user || user.status !== 'active') {
|
||||
throw new ForbiddenException();
|
||||
@@ -158,21 +144,15 @@ export class UsersService extends ItemsService {
|
||||
|
||||
const passwordHashed = await argon2.hash(password);
|
||||
|
||||
await this.knex('directus_users')
|
||||
.update({ password: passwordHashed, status: 'active' })
|
||||
.where({ id: user.id });
|
||||
await this.knex('directus_users').update({ password: passwordHashed, status: 'active' }).where({ id: user.id });
|
||||
|
||||
if (cache) {
|
||||
if (cache && env.CACHE_AUTO_PURGE) {
|
||||
await cache.clear();
|
||||
}
|
||||
}
|
||||
|
||||
async enableTFA(pk: string) {
|
||||
const user = await this.knex
|
||||
.select('tfa_secret')
|
||||
.from('directus_users')
|
||||
.where({ id: pk })
|
||||
.first();
|
||||
const user = await this.knex.select('tfa_secret').from('directus_users').where({ id: pk }).first();
|
||||
|
||||
if (user?.tfa_secret !== null) {
|
||||
throw new InvalidPayloadException('TFA Secret is already set for this user');
|
||||
|
||||
@@ -17,18 +17,13 @@ export class UtilsService {
|
||||
|
||||
async sort(collection: string, { item, to }: { item: PrimaryKey; to: PrimaryKey }) {
|
||||
const sortFieldResponse =
|
||||
(await this.knex
|
||||
.select('sort_field')
|
||||
.from('directus_collections')
|
||||
.where({ collection })
|
||||
.first()) || systemCollectionRows;
|
||||
(await this.knex.select('sort_field').from('directus_collections').where({ collection }).first()) ||
|
||||
systemCollectionRows;
|
||||
|
||||
const sortField = sortFieldResponse?.sort_field;
|
||||
|
||||
if (!sortField) {
|
||||
throw new InvalidPayloadException(
|
||||
`Collection "${collection}" doesn't have a sort field.`
|
||||
);
|
||||
throw new InvalidPayloadException(`Collection "${collection}" doesn't have a sort field.`);
|
||||
}
|
||||
|
||||
if (this.accountability?.admin !== true) {
|
||||
@@ -56,11 +51,7 @@ export class UtilsService {
|
||||
const primaryKeyField = this.schema[collection].primary;
|
||||
|
||||
// Make sure all rows have a sort value
|
||||
const countResponse = await this.knex
|
||||
.count('* as count')
|
||||
.from(collection)
|
||||
.whereNull(sortField)
|
||||
.first();
|
||||
const countResponse = await this.knex.count('* as count').from(collection).whereNull(sortField).first();
|
||||
|
||||
if (countResponse?.count && +countResponse.count !== 0) {
|
||||
const lastSortValueResponse = await this.knex.max(sortField).from(collection).first();
|
||||
|
||||
Reference in New Issue
Block a user