Merge branch 'main' into aggregation

This commit is contained in:
rijkvanzanten
2021-06-07 09:31:44 -04:00
142 changed files with 9129 additions and 5252 deletions

View File

@@ -2,7 +2,7 @@ import { Range, StatResponse } from '@directus/drive';
import { Knex } from 'knex';
import path from 'path';
import sharp, { ResizeOptions } from 'sharp';
import database from '../database';
import getDatabase from '../database';
import { RangeNotSatisfiableException, IllegalAssetTransformation } from '../exceptions';
import storage from '../storage';
import { AbstractServiceOptions, Accountability, Transformation } from '../types';
@@ -23,7 +23,7 @@ export class AssetsService {
authorizationService: AuthorizationService;
constructor(options: AbstractServiceOptions) {
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.accountability = options.accountability || null;
this.authorizationService = new AuthorizationService(options);
}
@@ -44,7 +44,7 @@ export class AssetsService {
await this.authorizationService.checkAccess('read', 'directus_files', id);
}
const file = (await database.select('*').from('directus_files').where({ id }).first()) as File;
const file = (await this.knex.select('*').from('directus_files').where({ id }).first()) as File;
if (range) {
if (range.start >= file.filesize || (range.end && range.end >= file.filesize)) {

View File

@@ -4,7 +4,7 @@ import { Knex } from 'knex';
import ms from 'ms';
import { nanoid } from 'nanoid';
import { authenticator } from 'otplib';
import database from '../database';
import getDatabase from '../database';
import emitter, { emitAsyncSafe } from '../emitter';
import env from '../env';
import {
@@ -37,7 +37,7 @@ export class AuthenticationService {
schema: SchemaOverview;
constructor(options: AbstractServiceOptions) {
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.accountability = options.accountability || null;
this.activityService = new ActivityService({ knex: this.knex, schema: options.schema });
this.schema = options.schema;
@@ -59,7 +59,7 @@ export class AuthenticationService {
const { email, password, ip, userAgent, otp } = options;
let user = await database
let user = await this.knex
.select('id', 'password', 'role', 'tfa_secret', 'status')
.from('directus_users')
.whereRaw('LOWER(??) = ?', ['email', email.toLowerCase()])
@@ -114,7 +114,7 @@ export class AuthenticationService {
try {
await loginAttemptsLimiter.consume(user.id);
} catch (err) {
await database('directus_users').update({ status: 'suspended' }).where({ id: user.id });
await this.knex('directus_users').update({ status: 'suspended' }).where({ id: user.id });
user.status = 'suspended';
// This means that new attempts after the user has been re-activated will be accepted
@@ -164,7 +164,7 @@ export class AuthenticationService {
const refreshToken = nanoid(64);
const refreshTokenExpiration = new Date(Date.now() + ms(env.REFRESH_TOKEN_TTL as string));
await database('directus_sessions').insert({
await this.knex('directus_sessions').insert({
token: refreshToken,
user: user.id,
expires: refreshTokenExpiration,
@@ -172,7 +172,7 @@ export class AuthenticationService {
user_agent: userAgent,
});
await database('directus_sessions').delete().where('expires', '<', new Date());
await this.knex('directus_sessions').delete().where('expires', '<', new Date());
if (this.accountability) {
await this.activityService.createOne({
@@ -204,7 +204,7 @@ export class AuthenticationService {
throw new InvalidCredentialsException();
}
const record = await database
const record = await this.knex
.select<Session & { email: string; id: string }>(
'directus_sessions.*',
'directus_users.email',

View File

@@ -1,6 +1,6 @@
import { Knex } from 'knex';
import { cloneDeep, flatten, merge, uniq, uniqWith } from 'lodash';
import database from '../database';
import getDatabase from '../database';
import { FailedValidationException, ForbiddenException } from '../exceptions';
import {
AbstractServiceOptions,
@@ -28,7 +28,7 @@ export class AuthorizationService {
schema: SchemaOverview;
constructor(options: AbstractServiceOptions) {
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.accountability = options.accountability || null;
this.schema = options.schema;
this.payloadService = new PayloadService('directus_permissions', {

View File

@@ -2,7 +2,7 @@ import SchemaInspector from '@directus/schema';
import { Knex } from 'knex';
import cache from '../cache';
import { ALIAS_TYPES } from '../constants';
import database, { schemaInspector } from '../database';
import getDatabase, { getSchemaInspector } from '../database';
import { systemCollectionRows } from '../database/system-data/collections';
import env from '../env';
import { ForbiddenException, InvalidPayloadException } from '../exceptions';
@@ -27,13 +27,13 @@ export type RawCollection = {
export class CollectionsService {
knex: Knex;
accountability: Accountability | null;
schemaInspector: typeof schemaInspector;
schemaInspector: ReturnType<typeof SchemaInspector>;
schema: SchemaOverview;
constructor(options: AbstractServiceOptions) {
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.accountability = options.accountability || null;
this.schemaInspector = options.knex ? SchemaInspector(options.knex) : schemaInspector;
this.schemaInspector = options.knex ? SchemaInspector(options.knex) : getSchemaInspector();
this.schema = options.schema;
}

View File

@@ -3,7 +3,7 @@ import { Knex } from 'knex';
import { Column } from 'knex-schema-inspector/dist/types/column';
import cache from '../cache';
import { ALIAS_TYPES } from '../constants';
import database, { schemaInspector } from '../database';
import getDatabase, { getSchemaInspector } from '../database';
import { systemFieldRows } from '../database/system-data/fields/';
import emitter, { emitAsyncSafe } from '../emitter';
import env from '../env';
@@ -26,12 +26,12 @@ export class FieldsService {
accountability: Accountability | null;
itemsService: ItemsService;
payloadService: PayloadService;
schemaInspector: typeof schemaInspector;
schemaInspector: ReturnType<typeof SchemaInspector>;
schema: SchemaOverview;
constructor(options: AbstractServiceOptions) {
this.knex = options.knex || database;
this.schemaInspector = options.knex ? SchemaInspector(options.knex) : schemaInspector;
this.knex = options.knex || getDatabase();
this.schemaInspector = options.knex ? SchemaInspector(options.knex) : getSchemaInspector();
this.accountability = options.accountability || null;
this.itemsService = new ItemsService('directus_fields', options);
this.payloadService = new PayloadService('directus_fields', options);

View File

@@ -102,8 +102,9 @@ export class FilesService extends ItemsService {
if (meta.iptc) {
try {
payload.metadata.iptc = parseIPTC(meta.iptc);
payload.title = payload.title || payload.metadata.iptc.headline;
payload.title = payload.metadata.iptc.headline || payload.title;
payload.description = payload.description || payload.metadata.iptc.caption;
payload.tags = payload.metadata.iptc.keywords;
} catch (err) {
logger.warn(`Couldn't extract IPTC information from file`);
logger.warn(err);

View File

@@ -44,7 +44,7 @@ import {
import { Knex } from 'knex';
import { flatten, get, mapKeys, merge, set, uniq } from 'lodash';
import ms from 'ms';
import database from '../database';
import getDatabase from '../database';
import env from '../env';
import { BaseException, GraphQLValidationException, InvalidPayloadException } from '../exceptions';
import { listExtensions } from '../extensions';
@@ -115,7 +115,7 @@ export class GraphQLService {
constructor(options: AbstractServiceOptions & { scope: 'items' | 'system' }) {
this.accountability = options?.accountability || null;
this.knex = options?.knex || database;
this.knex = options?.knex || getDatabase();
this.schema = options.schema;
this.scope = options.scope;
}

View File

@@ -1,5 +1,5 @@
import { Knex } from 'knex';
import database from '../database';
import getDatabase from '../database';
import { AbstractServiceOptions, Accountability, SchemaOverview } from '../types';
import { ForbiddenException, InvalidPayloadException } from '../exceptions';
import StreamArray from 'stream-json/streamers/StreamArray';
@@ -15,7 +15,7 @@ export class ImportService {
schema: SchemaOverview;
constructor(options: AbstractServiceOptions) {
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.accountability = options.accountability || null;
this.schema = options.schema;
}

View File

@@ -1,7 +1,7 @@
import { Knex } from 'knex';
import { clone, cloneDeep, merge, pick, without } from 'lodash';
import cache from '../cache';
import database from '../database';
import getDatabase from '../database';
import runAST from '../database/run-ast';
import emitter, { emitAsyncSafe } from '../emitter';
import env from '../env';
@@ -55,7 +55,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
constructor(collection: string, options: AbstractServiceOptions) {
this.collection = collection;
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.accountability = options.accountability || null;
this.eventScope = this.collection.startsWith('directus_') ? this.collection.substring(9) : 'items';
this.schema = options.schema;
@@ -204,7 +204,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
schema: this.schema,
// This hook is called async. If we would pass the transaction here, the hook can be
// called after the transaction is done #5460
database: database,
database: getDatabase(),
});
}
@@ -516,7 +516,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
schema: this.schema,
// This hook is called async. If we would pass the transaction here, the hook can be
// called after the transaction is done #5460
database: database,
database: getDatabase(),
});
}
@@ -665,7 +665,7 @@ export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractSer
schema: this.schema,
// This hook is called async. If we would pass the transaction here, the hook can be
// called after the transaction is done #5460
database: database,
database: getDatabase(),
});
}

View File

@@ -2,7 +2,7 @@ import fse from 'fs-extra';
import { Knex } from 'knex';
import { Liquid } from 'liquidjs';
import path from 'path';
import database from '../../database';
import getDatabase from '../../database';
import env from '../../env';
import { InvalidPayloadException } from '../../exceptions';
import logger from '../../logger';
@@ -30,7 +30,7 @@ export class MailService {
constructor(opts: AbstractServiceOptions) {
this.schema = opts.schema;
this.accountability = opts.accountability || null;
this.knex = opts?.knex || database;
this.knex = opts?.knex || getDatabase();
}
async send(options: EmailOptions): Promise<void> {

View File

@@ -1,5 +1,5 @@
import { Knex } from 'knex';
import database from '../database';
import getDatabase from '../database';
import { ForbiddenException } from '../exceptions';
import { AbstractServiceOptions, Accountability, SchemaOverview } from '../types';
import { Query } from '../types/query';
@@ -12,7 +12,7 @@ export class MetaService {
schema: SchemaOverview;
constructor(options: AbstractServiceOptions) {
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.accountability = options.accountability || null;
this.schema = options.schema;
}

View File

@@ -2,9 +2,9 @@ import argon2 from 'argon2';
import { format, formatISO, parse, parseISO } from 'date-fns';
import Joi from 'joi';
import { Knex } from 'knex';
import { clone, cloneDeep, isObject, isPlainObject } from 'lodash';
import { clone, cloneDeep, isObject, isPlainObject, omit } from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import database from '../database';
import getDatabase from '../database';
import { ForbiddenException, InvalidPayloadException } from '../exceptions';
import { AbstractServiceOptions, Accountability, Item, PrimaryKey, Query, SchemaOverview } from '../types';
import { toArray } from '../utils/to-array';
@@ -43,7 +43,7 @@ export class PayloadService {
constructor(collection: string, options: AbstractServiceOptions) {
this.accountability = options.accountability || null;
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.collection = collection;
this.schema = options.schema;
@@ -331,7 +331,13 @@ export class PayloadService {
.first());
if (exists) {
await itemsService.updateOne(relatedPrimaryKey, relatedRecord);
const fieldsToUpdate = omit(relatedRecord, relatedPrimary);
if (Object.keys(fieldsToUpdate).length > 0) {
await itemsService.updateOne(relatedPrimaryKey, relatedRecord, {
onRevisionCreate: (id) => revisions.push(id),
});
}
} else {
relatedPrimaryKey = await itemsService.createOne(relatedRecord, {
onRevisionCreate: (id) => revisions.push(id),
@@ -393,9 +399,13 @@ export class PayloadService {
.first());
if (exists) {
await itemsService.updateOne(relatedPrimaryKey, relatedRecord, {
onRevisionCreate: (id) => revisions.push(id),
});
const fieldsToUpdate = omit(relatedRecord, relatedPrimaryKeyField);
if (Object.keys(fieldsToUpdate).length > 0) {
await itemsService.updateOne(relatedPrimaryKey, relatedRecord, {
onRevisionCreate: (id) => revisions.push(id),
});
}
} else {
relatedPrimaryKey = await itemsService.createOne(relatedRecord, {
onRevisionCreate: (id) => revisions.push(id),

View File

@@ -7,20 +7,21 @@ import { ItemsService, QueryOptions } from './items';
import { PermissionsService } from './permissions';
import SchemaInspector from '@directus/schema';
import { ForeignKey } from 'knex-schema-inspector/dist/types/foreign-key';
import database, { schemaInspector } from '../database';
import getDatabase, { getSchemaInspector } from '../database';
import { getDefaultIndexName } from '../utils/get-default-index-name';
export class RelationsService {
knex: Knex;
permissionsService: PermissionsService;
schemaInspector: typeof schemaInspector;
schemaInspector: ReturnType<typeof SchemaInspector>;
accountability: Accountability | null;
schema: SchemaOverview;
relationsItemService: ItemsService<RelationMeta>;
constructor(options: AbstractServiceOptions) {
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.permissionsService = new PermissionsService(options);
this.schemaInspector = options.knex ? SchemaInspector(options.knex) : schemaInspector;
this.schemaInspector = options.knex ? SchemaInspector(options.knex) : getSchemaInspector();
this.schema = options.schema;
this.accountability = options.accountability || null;
this.relationsItemService = new ItemsService('directus_relations', {
@@ -159,8 +160,10 @@ export class RelationsService {
await trx.schema.alterTable(relation.collection!, async (table) => {
this.alterType(table, relation);
const constraintName: string = getDefaultIndexName('foreign', relation.collection!, relation.field!);
table
.foreign(relation.field!)
.foreign(relation.field!, constraintName)
.references(
`${relation.related_collection!}.${this.schema.collections[relation.related_collection!].primary}`
)
@@ -168,7 +171,15 @@ export class RelationsService {
});
}
await this.relationsItemService.createOne(metaRow);
const relationsItemService = new ItemsService('directus_relations', {
knex: trx,
schema: this.schema,
// We don't set accountability here. If you have read access to certain fields, you are
// allowed to extract the relations regardless of permissions to directus_relations. This
// happens in `filterForbidden` down below
});
await relationsItemService.createOne(metaRow);
});
}
@@ -201,15 +212,18 @@ export class RelationsService {
await this.knex.transaction(async (trx) => {
if (existingRelation.related_collection) {
await trx.schema.alterTable(collection, async (table) => {
let constraintName: string = getDefaultIndexName('foreign', collection, field);
// If the FK already exists in the DB, drop it first
if (existingRelation?.schema) {
table.dropForeign(field);
constraintName = existingRelation.schema.constraint_name || constraintName;
table.dropForeign(field, constraintName);
}
this.alterType(table, relation);
table
.foreign(field)
.foreign(field, constraintName || undefined)
.references(
`${existingRelation.related_collection!}.${
this.schema.collections[existingRelation.related_collection!].primary
@@ -219,11 +233,19 @@ export class RelationsService {
});
}
const relationsItemService = new ItemsService('directus_relations', {
knex: trx,
schema: this.schema,
// We don't set accountability here. If you have read access to certain fields, you are
// allowed to extract the relations regardless of permissions to directus_relations. This
// happens in `filterForbidden` down below
});
if (relation.meta) {
if (existingRelation?.meta) {
await this.relationsItemService.updateOne(existingRelation.meta.id, relation.meta);
await relationsItemService.updateOne(existingRelation.meta.id, relation.meta);
} else {
await this.relationsItemService.createOne({
await relationsItemService.createOne({
...(relation.meta || {}),
many_collection: relation.collection,
many_field: relation.field,
@@ -259,9 +281,9 @@ export class RelationsService {
}
await this.knex.transaction(async (trx) => {
if (existingRelation.schema) {
if (existingRelation.schema?.constraint_name) {
await trx.schema.alterTable(existingRelation.collection, (table) => {
table.dropForeign(existingRelation.field);
table.dropForeign(existingRelation.field, existingRelation.schema!.constraint_name!);
});
}

View File

@@ -7,7 +7,7 @@ import { performance } from 'perf_hooks';
// @ts-ignore
import { version } from '../../package.json';
import cache from '../cache';
import database, { hasDatabaseConnection } from '../database';
import getDatabase, { hasDatabaseConnection } from '../database';
import env from '../env';
import logger from '../logger';
import { rateLimiter } from '../middleware/rate-limiter';
@@ -24,7 +24,7 @@ export class ServerService {
schema: SchemaOverview;
constructor(options: AbstractServiceOptions) {
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.accountability = options.accountability || null;
this.schema = options.schema;
this.settingsService = new SettingsService({ knex: this.knex, schema: this.schema });
@@ -129,6 +129,7 @@ export class ServerService {
}
async function testDatabase(): Promise<Record<string, HealthCheck[]>> {
const database = getDatabase();
const client = env.DB_CLIENT;
const checks: Record<string, HealthCheck[]> = {};

View File

@@ -5,7 +5,7 @@ import { cloneDeep, mergeWith } from 'lodash';
import { OpenAPIObject, OperationObject, PathItemObject, SchemaObject, TagObject } from 'openapi3-ts';
// @ts-ignore
import { version } from '../../package.json';
import database from '../database';
import getDatabase from '../database';
import env from '../env';
import {
AbstractServiceOptions,
@@ -37,7 +37,7 @@ export class SpecificationService {
constructor(options: AbstractServiceOptions) {
this.accountability = options.accountability || null;
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.schema = options.schema;
this.fieldsService = new FieldsService(options);
@@ -80,7 +80,7 @@ class OASSpecsService implements SpecificationSubService {
}
) {
this.accountability = options.accountability || null;
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.schema = options.schema;
this.fieldsService = fieldsService;
@@ -541,7 +541,7 @@ class GraphQLSpecsService implements SpecificationSubService {
constructor(options: AbstractServiceOptions) {
this.accountability = options.accountability || null;
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.schema = options.schema;
this.items = new GraphQLService({ ...options, scope: 'items' });

View File

@@ -3,7 +3,7 @@ import jwt from 'jsonwebtoken';
import { Knex } from 'knex';
import { clone } from 'lodash';
import cache from '../cache';
import database from '../database';
import getDatabase from '../database';
import env from '../env';
import {
FailedValidationException,
@@ -29,7 +29,7 @@ export class UsersService extends ItemsService {
constructor(options: AbstractServiceOptions) {
super('directus_users', options);
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.accountability = options.accountability || null;
this.service = new ItemsService('directus_users', options);
this.schema = options.schema;

View File

@@ -1,5 +1,5 @@
import { Knex } from 'knex';
import database from '../database';
import getDatabase from '../database';
import { systemCollectionRows } from '../database/system-data/collections';
import { ForbiddenException, InvalidPayloadException } from '../exceptions';
import { AbstractServiceOptions, Accountability, PrimaryKey, SchemaOverview } from '../types';
@@ -10,7 +10,7 @@ export class UtilsService {
schema: SchemaOverview;
constructor(options: AbstractServiceOptions) {
this.knex = options.knex || database;
this.knex = options.knex || getDatabase();
this.accountability = options.accountability || null;
this.schema = options.schema;
}