mirror of
https://github.com/directus/directus.git
synced 2026-01-29 02:37:57 -05:00
Merge branch 'main' into sdk
This commit is contained in:
@@ -12,6 +12,7 @@ import url from 'url';
|
||||
import path from 'path';
|
||||
import useCollection from '../middleware/use-collection';
|
||||
import { respond } from '../middleware/respond';
|
||||
import { toArray } from '../utils/to-array';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
@@ -32,7 +33,7 @@ const multipartHandler = asyncHandler(async (req, res, next) => {
|
||||
* the row in directus_files async during the upload of the actual file.
|
||||
*/
|
||||
|
||||
let disk: string = (env.STORAGE_LOCATIONS as string).split(',')[0].trim();
|
||||
let disk: string = toArray(env.STORAGE_LOCATIONS)[0];
|
||||
let payload: Partial<File> = {};
|
||||
let fileCount = 0;
|
||||
|
||||
@@ -155,7 +156,7 @@ router.post(
|
||||
|
||||
const payload = {
|
||||
filename_download: filename,
|
||||
storage: (env.STORAGE_LOCATIONS as string).split(',')[0].trim(),
|
||||
storage: toArray(env.STORAGE_LOCATIONS)[0],
|
||||
type: fileResponse.headers['content-type'],
|
||||
title: formatTitle(filename),
|
||||
...(req.body.data || {}),
|
||||
|
||||
@@ -20,12 +20,12 @@ router.get('/ping', (req, res) => res.send('pong'));
|
||||
|
||||
router.get(
|
||||
'/info',
|
||||
(req, res, next) => {
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new ServerService({ accountability: req.accountability });
|
||||
const data = service.serverInfo();
|
||||
const data = await service.serverInfo();
|
||||
res.locals.payload = { data };
|
||||
return next();
|
||||
},
|
||||
}),
|
||||
respond
|
||||
);
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
table: directus_permissions
|
||||
|
||||
defaults:
|
||||
role: null
|
||||
collection: null
|
||||
action: null
|
||||
permissions: null
|
||||
validation: null
|
||||
presets: null
|
||||
fields: null
|
||||
limit: null
|
||||
|
||||
data:
|
||||
- collection: directus_settings
|
||||
action: read
|
||||
permissions: {}
|
||||
fields: 'project_name,project_logo,project_color,public_foreground,public_background,public_note,custom_css'
|
||||
@@ -63,11 +63,6 @@ function processValues(env: Record<string, any>) {
|
||||
if (value === 'false') env[key] = false;
|
||||
if (value === 'null') env[key] = null;
|
||||
if (isNaN(value) === false && value.length > 0) env[key] = Number(value);
|
||||
if (typeof value === 'string' && value.includes(','))
|
||||
env[key] = value
|
||||
.split(',')
|
||||
.map((val) => val.trim())
|
||||
.filter((val) => val);
|
||||
}
|
||||
|
||||
return env;
|
||||
|
||||
@@ -60,18 +60,28 @@ export async function sendInviteMail(email: string, url: string) {
|
||||
/**
|
||||
* @TODO pull this from directus_settings
|
||||
*/
|
||||
const projectName = 'directus';
|
||||
const projectName = 'Directus';
|
||||
|
||||
const html = await liquidEngine.renderFile('user-invitation', { email, url, projectName });
|
||||
await transporter.sendMail({ from: env.EMAIL_FROM, to: email, html: html });
|
||||
await transporter.sendMail({
|
||||
from: env.EMAIL_FROM,
|
||||
to: email,
|
||||
html: html,
|
||||
subject: `[${projectName}] You've been invited`,
|
||||
});
|
||||
}
|
||||
|
||||
export async function sendPasswordResetMail(email: string, url: string) {
|
||||
/**
|
||||
* @TODO pull this from directus_settings
|
||||
*/
|
||||
const projectName = 'directus';
|
||||
const projectName = 'Directus';
|
||||
|
||||
const html = await liquidEngine.renderFile('password-reset', { email, url, projectName });
|
||||
await transporter.sendMail({ from: env.EMAIL_FROM, to: email, html: html });
|
||||
await transporter.sendMail({
|
||||
from: env.EMAIL_FROM,
|
||||
to: email,
|
||||
html: html,
|
||||
subject: `[${projectName}] Password Reset Request`,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
Action,
|
||||
Accountability,
|
||||
PermissionsAction,
|
||||
Item,
|
||||
Item as AnyItem,
|
||||
Query,
|
||||
PrimaryKey,
|
||||
AbstractService,
|
||||
@@ -26,7 +26,7 @@ import getDefaultValue from '../utils/get-default-value';
|
||||
import { InvalidPayloadException } from '../exceptions';
|
||||
import { ForbiddenException } from '../exceptions';
|
||||
|
||||
export class ItemsService implements AbstractService {
|
||||
export class ItemsService<Item extends AnyItem = AnyItem> implements AbstractService {
|
||||
collection: string;
|
||||
knex: Knex;
|
||||
accountability: Accountability | null;
|
||||
@@ -52,7 +52,7 @@ export class ItemsService implements AbstractService {
|
||||
const primaryKeyField = await this.schemaInspector.primary(this.collection);
|
||||
const columns = await this.schemaInspector.columns(this.collection);
|
||||
|
||||
let payloads = clone(toArray(data));
|
||||
let payloads: AnyItem[] = clone(toArray(data));
|
||||
|
||||
const savedPrimaryKeys = await this.knex.transaction(async (trx) => {
|
||||
const payloadService = new PayloadService(this.collection, {
|
||||
@@ -194,7 +194,7 @@ export class ItemsService implements AbstractService {
|
||||
return Array.isArray(data) ? savedPrimaryKeys : savedPrimaryKeys[0];
|
||||
}
|
||||
|
||||
async readByQuery(query: Query): Promise<null | Item | Item[]> {
|
||||
async readByQuery(query: Query): Promise<null | Partial<Item> | Partial<Item>[]> {
|
||||
const authorizationService = new AuthorizationService({
|
||||
accountability: this.accountability,
|
||||
knex: this.knex,
|
||||
@@ -210,20 +210,24 @@ export class ItemsService implements AbstractService {
|
||||
}
|
||||
|
||||
const records = await runAST(ast, { knex: this.knex });
|
||||
return records;
|
||||
return records as Partial<Item> | Partial<Item>[] | null;
|
||||
}
|
||||
|
||||
readByKey(
|
||||
keys: PrimaryKey[],
|
||||
query?: Query,
|
||||
action?: PermissionsAction
|
||||
): Promise<null | Item[]>;
|
||||
readByKey(key: PrimaryKey, query?: Query, action?: PermissionsAction): Promise<null | Item>;
|
||||
): Promise<null | Partial<Item>[]>;
|
||||
readByKey(
|
||||
key: PrimaryKey,
|
||||
query?: Query,
|
||||
action?: PermissionsAction
|
||||
): Promise<null | Partial<Item>>;
|
||||
async readByKey(
|
||||
key: PrimaryKey | PrimaryKey[],
|
||||
query: Query = {},
|
||||
action: PermissionsAction = 'read'
|
||||
): Promise<null | Item | Item[]> {
|
||||
): Promise<null | Partial<Item> | Partial<Item>[]> {
|
||||
query = clone(query);
|
||||
const primaryKeyField = await this.schemaInspector.primary(this.collection);
|
||||
const keys = toArray(key);
|
||||
@@ -260,7 +264,7 @@ export class ItemsService implements AbstractService {
|
||||
|
||||
if (result === null) throw new ForbiddenException();
|
||||
|
||||
return result;
|
||||
return result as Partial<Item> | Partial<Item>[] | null;
|
||||
}
|
||||
|
||||
update(data: Partial<Item>, keys: PrimaryKey[]): Promise<PrimaryKey[]>;
|
||||
@@ -277,7 +281,7 @@ export class ItemsService implements AbstractService {
|
||||
if (data && key) {
|
||||
const keys = toArray(key);
|
||||
|
||||
let payload = clone(data);
|
||||
let payload: Partial<AnyItem> | Partial<AnyItem>[] = clone(data);
|
||||
|
||||
const customProcessed = await emitter.emitAsync(
|
||||
`${this.eventScope}.update.before`,
|
||||
@@ -550,11 +554,11 @@ export class ItemsService implements AbstractService {
|
||||
return await this.delete(keys);
|
||||
}
|
||||
|
||||
async readSingleton(query: Query) {
|
||||
async readSingleton(query: Query): Promise<Partial<Item>> {
|
||||
query = clone(query);
|
||||
query.single = true;
|
||||
|
||||
const record = (await this.readByQuery(query)) as Item;
|
||||
const record = (await this.readByQuery(query)) as Partial<Item>;
|
||||
|
||||
if (!record) {
|
||||
const columns = await this.schemaInspector.columnInfo(this.collection);
|
||||
@@ -564,7 +568,7 @@ export class ItemsService implements AbstractService {
|
||||
defaults[column.name] = getDefaultValue(column);
|
||||
}
|
||||
|
||||
return defaults;
|
||||
return defaults as Partial<Item>;
|
||||
}
|
||||
|
||||
return record;
|
||||
|
||||
@@ -40,7 +40,7 @@ export class MetaService {
|
||||
const dbQuery = database(collection).count('*', { as: 'count' });
|
||||
|
||||
if (query.filter) {
|
||||
applyFilter(dbQuery, query.filter, collection);
|
||||
await applyFilter(dbQuery, query.filter, collection);
|
||||
}
|
||||
|
||||
const records = await dbQuery;
|
||||
|
||||
@@ -2,7 +2,6 @@ import { AbstractServiceOptions, Accountability } from '../types';
|
||||
import Knex from 'knex';
|
||||
import database from '../database';
|
||||
import os from 'os';
|
||||
import { ForbiddenException } from '../exceptions';
|
||||
// @ts-ignore
|
||||
import { version } from '../../package.json';
|
||||
import macosRelease from 'macos-release';
|
||||
@@ -16,31 +15,57 @@ export class ServerService {
|
||||
this.accountability = options?.accountability || null;
|
||||
}
|
||||
|
||||
serverInfo() {
|
||||
if (this.accountability?.admin !== true) {
|
||||
throw new ForbiddenException();
|
||||
}
|
||||
async serverInfo() {
|
||||
const info: Record<string, any> = {};
|
||||
|
||||
const osType = os.type() === 'Darwin' ? 'macOS' : os.type();
|
||||
const osVersion =
|
||||
osType === 'macOS'
|
||||
? `${macosRelease().name} (${macosRelease().version})`
|
||||
: os.release();
|
||||
const projectInfo = await this.knex
|
||||
.select(
|
||||
'project_name',
|
||||
'project_logo',
|
||||
'project_color',
|
||||
'public_foreground',
|
||||
'public_background',
|
||||
'public_note',
|
||||
'custom_css'
|
||||
)
|
||||
.from('directus_settings')
|
||||
.first();
|
||||
|
||||
return {
|
||||
directus: {
|
||||
info.project = projectInfo
|
||||
? {
|
||||
project_name: projectInfo.project_name,
|
||||
project_logo: projectInfo.project_logo,
|
||||
project_color: projectInfo.project_color,
|
||||
public_foreground: projectInfo.public_foreground,
|
||||
public_background: projectInfo.public_background,
|
||||
public_note: projectInfo.public_note,
|
||||
custom_css: projectInfo.custom_css,
|
||||
}
|
||||
: null;
|
||||
|
||||
if (this.accountability?.admin === true) {
|
||||
const osType = os.type() === 'Darwin' ? 'macOS' : os.type();
|
||||
|
||||
const osVersion =
|
||||
osType === 'macOS'
|
||||
? `${macosRelease().name} (${macosRelease().version})`
|
||||
: os.release();
|
||||
|
||||
info.directus = {
|
||||
version,
|
||||
},
|
||||
node: {
|
||||
};
|
||||
info.node = {
|
||||
version: process.versions.node,
|
||||
uptime: Math.round(process.uptime()),
|
||||
},
|
||||
os: {
|
||||
};
|
||||
info.os = {
|
||||
type: osType,
|
||||
version: osVersion,
|
||||
uptime: Math.round(os.uptime()),
|
||||
totalmem: os.totalmem(),
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
return info;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { QueryBuilder } from 'knex';
|
||||
import { Query, Filter } from '../types';
|
||||
import database, { schemaInspector } from '../database';
|
||||
import { clone } from 'lodash';
|
||||
import { clone, isPlainObject } from 'lodash';
|
||||
|
||||
export default async function applyQuery(collection: string, dbQuery: QueryBuilder, query: Query) {
|
||||
if (query.filter) {
|
||||
@@ -46,116 +46,244 @@ export default async function applyQuery(collection: string, dbQuery: QueryBuild
|
||||
}
|
||||
}
|
||||
|
||||
export async function applyFilter(dbQuery: QueryBuilder, filter: Filter, collection: string) {
|
||||
for (const [key, value] of Object.entries(filter)) {
|
||||
if (key === '_or') {
|
||||
value.forEach((subFilter: Record<string, any>) => {
|
||||
dbQuery.orWhere((subQuery) => applyFilter(subQuery, subFilter, collection));
|
||||
});
|
||||
export async function applyFilter(rootQuery: QueryBuilder, rootFilter: Filter, collection: string) {
|
||||
const relations = await database.select('*').from('directus_relations');
|
||||
|
||||
continue;
|
||||
addWhereClauses(rootQuery, rootFilter, collection);
|
||||
addJoins(rootQuery, rootFilter, collection);
|
||||
|
||||
function addWhereClauses(dbQuery: QueryBuilder, filter: Filter, collection: string) {
|
||||
for (const [key, value] of Object.entries(filter)) {
|
||||
if (key === '_or') {
|
||||
/** @NOTE these callback functions aren't called until Knex runs the query */
|
||||
dbQuery.orWhere((subQuery) => {
|
||||
value.forEach((subFilter: Record<string, any>) => {
|
||||
addWhereClauses(subQuery, subFilter, collection);
|
||||
});
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key === '_and') {
|
||||
/** @NOTE these callback functions aren't called until Knex runs the query */
|
||||
dbQuery.andWhere((subQuery) => {
|
||||
value.forEach((subFilter: Record<string, any>) => {
|
||||
addWhereClauses(subQuery, subFilter, collection);
|
||||
});
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const filterPath = getFilterPath(key, value);
|
||||
const { operator: filterOperator, value: filterValue } = getOperation(key, value);
|
||||
|
||||
if (filterPath.length > 1) {
|
||||
const columnName = getWhereColumn(filterPath, collection);
|
||||
applyFilterToQuery(columnName, filterOperator, filterValue);
|
||||
} else {
|
||||
applyFilterToQuery(`${collection}.${filterPath[0]}`, filterOperator, filterValue);
|
||||
}
|
||||
}
|
||||
|
||||
if (key === '_and') {
|
||||
value.forEach((subFilter: Record<string, any>) => {
|
||||
dbQuery.andWhere((subQuery) => applyFilter(subQuery, subFilter, collection));
|
||||
});
|
||||
function applyFilterToQuery(key: string, operator: string, compareValue: any) {
|
||||
if (operator === '_eq') {
|
||||
dbQuery.where({ [key]: compareValue });
|
||||
}
|
||||
|
||||
continue;
|
||||
if (operator === '_neq') {
|
||||
dbQuery.whereNot({ [key]: compareValue });
|
||||
}
|
||||
|
||||
if (operator === '_contains') {
|
||||
dbQuery.where(key, 'like', `%${compareValue}%`);
|
||||
}
|
||||
|
||||
if (operator === '_ncontains') {
|
||||
dbQuery.where(key, 'like', `%${compareValue}%`);
|
||||
}
|
||||
|
||||
if (operator === '_gt') {
|
||||
dbQuery.where(key, '>', compareValue);
|
||||
}
|
||||
|
||||
if (operator === '_gte') {
|
||||
dbQuery.where(key, '>=', compareValue);
|
||||
}
|
||||
|
||||
if (operator === '_lt') {
|
||||
dbQuery.where(key, '<', compareValue);
|
||||
}
|
||||
|
||||
if (operator === '_lte') {
|
||||
dbQuery.where(key, '<=', compareValue);
|
||||
}
|
||||
|
||||
if (operator === '_in') {
|
||||
let value = compareValue;
|
||||
if (typeof value === 'string') value = value.split(',');
|
||||
|
||||
dbQuery.whereIn(key, value as string[]);
|
||||
}
|
||||
|
||||
if (operator === '_nin') {
|
||||
let value = compareValue;
|
||||
if (typeof value === 'string') value = value.split(',');
|
||||
|
||||
dbQuery.whereNotIn(key, value as string[]);
|
||||
}
|
||||
|
||||
if (operator === '_null') {
|
||||
dbQuery.whereNull(key);
|
||||
}
|
||||
|
||||
if (operator === '_nnull') {
|
||||
dbQuery.whereNotNull(key);
|
||||
}
|
||||
|
||||
if (operator === '_empty') {
|
||||
dbQuery.andWhere((query) => {
|
||||
query.whereNull(key);
|
||||
query.orWhere(key, '=', '');
|
||||
});
|
||||
}
|
||||
|
||||
if (operator === '_nempty') {
|
||||
dbQuery.andWhere((query) => {
|
||||
query.whereNotNull(key);
|
||||
query.orWhere(key, '!=', '');
|
||||
});
|
||||
}
|
||||
|
||||
if (operator === '_between') {
|
||||
let value = compareValue;
|
||||
if (typeof value === 'string') value = value.split(',');
|
||||
|
||||
dbQuery.whereBetween(key, value);
|
||||
}
|
||||
|
||||
if (operator === '_nbetween') {
|
||||
let value = compareValue;
|
||||
if (typeof value === 'string') value = value.split(',');
|
||||
|
||||
dbQuery.whereNotBetween(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
const filterPath = getFilterPath(key, value);
|
||||
const { operator: filterOperator, value: filterValue } = getOperation(key, value);
|
||||
function getWhereColumn(path: string[], collection: string) {
|
||||
path = clone(path);
|
||||
|
||||
const column =
|
||||
filterPath.length > 1
|
||||
? await applyJoins(dbQuery, filterPath, collection)
|
||||
: `${collection}.${filterPath[0]}`;
|
||||
let columnName = '';
|
||||
|
||||
applyFilterToQuery(column, filterOperator, filterValue);
|
||||
followRelation(path);
|
||||
|
||||
return columnName;
|
||||
|
||||
function followRelation(pathParts: string[], parentCollection: string = collection) {
|
||||
const relation = relations.find((relation) => {
|
||||
return (
|
||||
(relation.many_collection === parentCollection &&
|
||||
relation.many_field === pathParts[0]) ||
|
||||
(relation.one_collection === parentCollection &&
|
||||
relation.one_field === pathParts[0])
|
||||
);
|
||||
});
|
||||
|
||||
if (!relation) return;
|
||||
|
||||
const isM2O =
|
||||
relation.many_collection === parentCollection &&
|
||||
relation.many_field === pathParts[0];
|
||||
|
||||
pathParts.shift();
|
||||
|
||||
const parent = isM2O ? relation.one_collection! : relation.many_collection;
|
||||
|
||||
if (pathParts.length === 1) {
|
||||
columnName = `${parent}.${pathParts[0]}`;
|
||||
}
|
||||
|
||||
if (pathParts.length) {
|
||||
followRelation(pathParts, parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyFilterToQuery(key: string, operator: string, compareValue: any) {
|
||||
if (operator === '_eq') {
|
||||
dbQuery.where({ [key]: compareValue });
|
||||
/**
|
||||
* @NOTE Yes this is very similar in structure and functionality as the other loop. However,
|
||||
* due to the order of execution that Knex has in the nested andWhere / orWhere structures,
|
||||
* joins that are added in there aren't added in time
|
||||
*/
|
||||
function addJoins(dbQuery: QueryBuilder, filter: Filter, collection: string) {
|
||||
for (const [key, value] of Object.entries(filter)) {
|
||||
if (key === '_or') {
|
||||
value.forEach((subFilter: Record<string, any>) => {
|
||||
addJoins(dbQuery, subFilter, collection);
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key === '_and') {
|
||||
value.forEach((subFilter: Record<string, any>) => {
|
||||
addJoins(dbQuery, subFilter, collection);
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const filterPath = getFilterPath(key, value);
|
||||
|
||||
if (filterPath.length > 1) {
|
||||
addJoin(filterPath, collection);
|
||||
}
|
||||
}
|
||||
|
||||
if (operator === '_neq') {
|
||||
dbQuery.whereNot({ [key]: compareValue });
|
||||
}
|
||||
function addJoin(path: string[], collection: string) {
|
||||
path = clone(path);
|
||||
|
||||
if (operator === '_contains') {
|
||||
dbQuery.where(key, 'like', `%${compareValue}%`);
|
||||
}
|
||||
followRelation(path);
|
||||
|
||||
if (operator === '_ncontains') {
|
||||
dbQuery.where(key, 'like', `%${compareValue}%`);
|
||||
}
|
||||
function followRelation(pathParts: string[], parentCollection: string = collection) {
|
||||
const relation = relations.find((relation) => {
|
||||
return (
|
||||
(relation.many_collection === parentCollection &&
|
||||
relation.many_field === pathParts[0]) ||
|
||||
(relation.one_collection === parentCollection &&
|
||||
relation.one_field === pathParts[0])
|
||||
);
|
||||
});
|
||||
|
||||
if (operator === '_gt') {
|
||||
dbQuery.where(key, '>', compareValue);
|
||||
}
|
||||
if (!relation) return;
|
||||
|
||||
if (operator === '_gte') {
|
||||
dbQuery.where(key, '>=', compareValue);
|
||||
}
|
||||
const isM2O =
|
||||
relation.many_collection === parentCollection &&
|
||||
relation.many_field === pathParts[0];
|
||||
|
||||
if (operator === '_lt') {
|
||||
dbQuery.where(key, '<', compareValue);
|
||||
}
|
||||
if (isM2O) {
|
||||
dbQuery.leftJoin(
|
||||
relation.one_collection!,
|
||||
`${parentCollection}.${relation.many_field}`,
|
||||
`${relation.one_collection}.${relation.one_primary}`
|
||||
);
|
||||
} else {
|
||||
dbQuery.leftJoin(
|
||||
relation.many_collection,
|
||||
`${parentCollection}.${relation.one_primary}`,
|
||||
`${relation.many_collection}.${relation.many_field}`
|
||||
);
|
||||
}
|
||||
|
||||
if (operator === '_lte') {
|
||||
dbQuery.where(key, '<=', compareValue);
|
||||
}
|
||||
pathParts.shift();
|
||||
|
||||
if (operator === '_in') {
|
||||
let value = compareValue;
|
||||
if (typeof value === 'string') value = value.split(',');
|
||||
const parent = isM2O ? relation.one_collection! : relation.many_collection;
|
||||
|
||||
dbQuery.whereIn(key, value as string[]);
|
||||
}
|
||||
|
||||
if (operator === '_nin') {
|
||||
let value = compareValue;
|
||||
if (typeof value === 'string') value = value.split(',');
|
||||
|
||||
dbQuery.whereNotIn(key, value as string[]);
|
||||
}
|
||||
|
||||
if (operator === '_null') {
|
||||
dbQuery.whereNull(key);
|
||||
}
|
||||
|
||||
if (operator === '_nnull') {
|
||||
dbQuery.whereNotNull(key);
|
||||
}
|
||||
|
||||
if (operator === '_empty') {
|
||||
dbQuery.andWhere((query) => {
|
||||
query.whereNull(key);
|
||||
query.orWhere(key, '=', '');
|
||||
});
|
||||
}
|
||||
|
||||
if (operator === '_nempty') {
|
||||
dbQuery.andWhere((query) => {
|
||||
query.whereNotNull(key);
|
||||
query.orWhere(key, '!=', '');
|
||||
});
|
||||
}
|
||||
|
||||
if (operator === '_between') {
|
||||
let value = compareValue;
|
||||
if (typeof value === 'string') value = value.split(',');
|
||||
|
||||
dbQuery.whereBetween(key, value);
|
||||
}
|
||||
|
||||
if (operator === '_nbetween') {
|
||||
let value = compareValue;
|
||||
if (typeof value === 'string') value = value.split(',');
|
||||
|
||||
dbQuery.whereNotBetween(key, value);
|
||||
if (pathParts.length) {
|
||||
followRelation(pathParts, parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,7 +291,11 @@ export async function applyFilter(dbQuery: QueryBuilder, filter: Filter, collect
|
||||
function getFilterPath(key: string, value: Record<string, any>) {
|
||||
const path = [key];
|
||||
|
||||
if (Object.keys(value)[0].startsWith('_') === false) {
|
||||
if (Object.keys(value)[0].startsWith('_') === true) {
|
||||
return path;
|
||||
}
|
||||
|
||||
if (isPlainObject(value)) {
|
||||
path.push(...getFilterPath(Object.keys(value)[0], Object.values(value)[0]));
|
||||
}
|
||||
|
||||
@@ -171,57 +303,11 @@ function getFilterPath(key: string, value: Record<string, any>) {
|
||||
}
|
||||
|
||||
function getOperation(key: string, value: Record<string, any>): { operator: string; value: any } {
|
||||
if (key.startsWith('_') && key !== '_and' && key !== '_or')
|
||||
if (key.startsWith('_') && key !== '_and' && key !== '_or') {
|
||||
return { operator: key as string, value };
|
||||
} else if (isPlainObject(value) === false) {
|
||||
return { operator: '_eq', value };
|
||||
}
|
||||
|
||||
return getOperation(Object.keys(value)[0], Object.values(value)[0]);
|
||||
}
|
||||
|
||||
async function applyJoins(dbQuery: QueryBuilder, path: string[], collection: string) {
|
||||
path = clone(path);
|
||||
|
||||
let keyName = '';
|
||||
|
||||
await addJoins(path);
|
||||
|
||||
return keyName;
|
||||
|
||||
async function addJoins(pathParts: string[], parentCollection: string = collection) {
|
||||
const relation = await database
|
||||
.select('*')
|
||||
.from('directus_relations')
|
||||
.where({ one_collection: parentCollection, one_field: pathParts[0] })
|
||||
.orWhere({ many_collection: parentCollection, many_field: pathParts[0] })
|
||||
.first();
|
||||
|
||||
if (!relation) return;
|
||||
|
||||
const isM2O =
|
||||
relation.many_collection === parentCollection && relation.many_field === pathParts[0];
|
||||
|
||||
if (isM2O) {
|
||||
dbQuery.leftJoin(
|
||||
relation.one_collection,
|
||||
`${parentCollection}.${relation.many_field}`,
|
||||
`${relation.one_collection}.${relation.one_primary}`
|
||||
);
|
||||
} else {
|
||||
dbQuery.leftJoin(
|
||||
relation.many_collection,
|
||||
`${relation.one_collection}.${relation.one_primary}`,
|
||||
`${relation.many_collection}.${relation.many_field}`
|
||||
);
|
||||
}
|
||||
|
||||
pathParts.shift();
|
||||
|
||||
const parent = isM2O ? relation.one_collection : relation.many_collection;
|
||||
|
||||
if (pathParts.length === 1) {
|
||||
keyName = `${parent}.${pathParts[0]}`;
|
||||
}
|
||||
|
||||
if (pathParts.length) {
|
||||
await addJoins(pathParts, parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
export function toArray<T = any>(val: T | T[]): T[] {
|
||||
if (typeof val === 'string') {
|
||||
return (val.split(',') as unknown) as T[];
|
||||
}
|
||||
|
||||
return Array.isArray(val) ? val : [val];
|
||||
}
|
||||
|
||||
@@ -34,25 +34,25 @@ import setFavicon from '@/utils/set-favicon';
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
const { useAppStore, useUserStore, useSettingsStore } = stores;
|
||||
const { useAppStore, useUserStore, useServerStore } = stores;
|
||||
|
||||
const appStore = useAppStore();
|
||||
const userStore = useUserStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const serverStore = useServerStore();
|
||||
|
||||
const { hydrating, sidebarOpen } = toRefs(appStore.state);
|
||||
|
||||
const brandStyle = computed(() => {
|
||||
return {
|
||||
'--brand': settingsStore.state.settings?.project_color || 'var(--primary)',
|
||||
'--brand': serverStore.state.info?.project?.project_color || 'var(--primary)',
|
||||
};
|
||||
});
|
||||
|
||||
watch(
|
||||
[() => settingsStore.state.settings?.project_color, () => settingsStore.state.settings?.project_logo],
|
||||
[() => serverStore.state.info?.project?.project_color, () => serverStore.state.info?.project?.project_logo],
|
||||
() => {
|
||||
const hasCustomLogo = !!settingsStore.state.settings?.project_logo;
|
||||
setFavicon(settingsStore.state.settings?.project_color || '#2f80ed', hasCustomLogo);
|
||||
const hasCustomLogo = !!serverStore.state.info?.project?.project_logo;
|
||||
setFavicon(serverStore.state.info?.project?.project_color || '#2f80ed', hasCustomLogo);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -90,14 +90,14 @@ export default defineComponent({
|
||||
);
|
||||
|
||||
watch(
|
||||
() => settingsStore.state.settings?.project_name,
|
||||
() => serverStore.state.info?.project?.project_name,
|
||||
(projectName) => {
|
||||
document.title = projectName;
|
||||
document.title = projectName || 'Directus';
|
||||
}
|
||||
);
|
||||
|
||||
const customCSS = computed(() => {
|
||||
return settingsStore.state?.settings?.custom_css || '';
|
||||
return serverStore.state?.info?.project?.custom_css || '';
|
||||
});
|
||||
|
||||
const error = computed(() => appStore.state.error);
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
useRequestsStore,
|
||||
usePresetsStore,
|
||||
useSettingsStore,
|
||||
useServerStore,
|
||||
useLatencyStore,
|
||||
useRelationsStore,
|
||||
usePermissionsStore,
|
||||
@@ -30,6 +31,7 @@ export function useStores(
|
||||
useRequestsStore,
|
||||
usePresetsStore,
|
||||
useSettingsStore,
|
||||
useServerStore,
|
||||
useLatencyStore,
|
||||
useRelationsStore,
|
||||
usePermissionsStore,
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
@click="setCurrent(item)"
|
||||
>
|
||||
<v-list-item-content>
|
||||
{{ userName(currentUser) }}
|
||||
{{ userName(item) }}
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</template>
|
||||
@@ -93,6 +93,7 @@ import useCollection from '@/composables/use-collection';
|
||||
import api from '@/api';
|
||||
import DrawerItem from '@/views/private/components/drawer-item';
|
||||
import DrawerCollection from '@/views/private/components/drawer-collection';
|
||||
import { userName } from '@/utils/user-name';
|
||||
|
||||
export default defineComponent({
|
||||
components: { DrawerItem, DrawerCollection },
|
||||
@@ -141,6 +142,7 @@ export default defineComponent({
|
||||
edits,
|
||||
stageEdits,
|
||||
editModalActive,
|
||||
userName,
|
||||
};
|
||||
|
||||
function useCurrent() {
|
||||
|
||||
@@ -144,7 +144,7 @@ export default defineComponent({
|
||||
params.filter.role = { _eq: props.role };
|
||||
}
|
||||
|
||||
const response = await api.get('/permissions', params);
|
||||
const response = await api.get('/permissions', { params });
|
||||
|
||||
permissions.value = response.data.data;
|
||||
} catch (err) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import LogoutRoute from '@/routes/logout';
|
||||
import ResetPasswordRoute from '@/routes/reset-password';
|
||||
import { refresh } from '@/auth';
|
||||
import { hydrate } from '@/hydrate';
|
||||
import { useAppStore, useUserStore, useSettingsStore } from '@/stores/';
|
||||
import { useAppStore, useUserStore, useServerStore } from '@/stores/';
|
||||
import PrivateNotFoundRoute from '@/routes/private-not-found';
|
||||
|
||||
import getRootPath from '@/utils/get-root-path';
|
||||
@@ -81,7 +81,7 @@ export function replaceRoutes(routeFilter: (routes: RouteConfig[]) => RouteConfi
|
||||
|
||||
export const onBeforeEach: NavigationGuard = async (to, from, next) => {
|
||||
const appStore = useAppStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const serverStore = useServerStore();
|
||||
|
||||
// First load
|
||||
if (from.name === null) {
|
||||
@@ -91,8 +91,8 @@ export const onBeforeEach: NavigationGuard = async (to, from, next) => {
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (settingsStore.state.settings === null) {
|
||||
await settingsStore.hydrate();
|
||||
if (serverStore.state.info === null) {
|
||||
await serverStore.hydrate();
|
||||
}
|
||||
|
||||
if (to.meta?.public !== true && appStore.state.hydrated === false) {
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
import { defineComponent, computed, PropType } from '@vue/composition-api';
|
||||
import LoginForm from './components/login-form/';
|
||||
import ContinueAs from './components/continue-as/';
|
||||
import { useAppStore, useSettingsStore } from '@/stores';
|
||||
import { useAppStore } from '@/stores';
|
||||
|
||||
import { LogoutReason } from '@/auth';
|
||||
|
||||
@@ -43,12 +43,10 @@ export default defineComponent({
|
||||
components: { LoginForm, ContinueAs },
|
||||
setup() {
|
||||
const appStore = useAppStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const authenticated = computed(() => appStore.state.authenticated);
|
||||
const currentProject = computed(() => settingsStore.state.settings);
|
||||
|
||||
return { authenticated, currentProject };
|
||||
return { authenticated };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -7,5 +7,6 @@ export * from './permissions';
|
||||
export * from './presets';
|
||||
export * from './relations';
|
||||
export * from './requests';
|
||||
export * from './server';
|
||||
export * from './settings';
|
||||
export * from './user';
|
||||
|
||||
@@ -2,6 +2,7 @@ import { createStore } from 'pinia';
|
||||
import { Preset } from '@/types';
|
||||
import { useUserStore } from '@/stores/';
|
||||
import api from '@/api';
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
const defaultPreset: Omit<Preset, 'collection'> = {
|
||||
bookmark: null,
|
||||
@@ -14,6 +15,8 @@ const defaultPreset: Omit<Preset, 'collection'> = {
|
||||
layout_options: null,
|
||||
};
|
||||
|
||||
let currentUpdate: Record<number, string> = {};
|
||||
|
||||
export const usePresetsStore = createStore({
|
||||
id: 'presetsStore',
|
||||
state: () => ({
|
||||
@@ -60,17 +63,22 @@ export const usePresetsStore = createStore({
|
||||
return response.data.data;
|
||||
},
|
||||
async update(id: number, updates: Partial<Preset>) {
|
||||
const updateID = nanoid();
|
||||
currentUpdate[id] = updateID;
|
||||
|
||||
const response = await api.patch(`/presets/${id}`, updates);
|
||||
|
||||
this.state.collectionPresets = this.state.collectionPresets.map((preset) => {
|
||||
const updatedPreset = response.data.data;
|
||||
if (currentUpdate[id] === updateID) {
|
||||
this.state.collectionPresets = this.state.collectionPresets.map((preset) => {
|
||||
const updatedPreset = response.data.data;
|
||||
|
||||
if (preset.id === updatedPreset.id) {
|
||||
return updatedPreset;
|
||||
}
|
||||
if (preset.id === updatedPreset.id) {
|
||||
return updatedPreset;
|
||||
}
|
||||
|
||||
return preset;
|
||||
});
|
||||
return preset;
|
||||
});
|
||||
}
|
||||
|
||||
return response.data.data;
|
||||
},
|
||||
|
||||
43
app/src/stores/server.ts
Normal file
43
app/src/stores/server.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { createStore } from 'pinia';
|
||||
import api from '@/api';
|
||||
|
||||
type Info = {
|
||||
project: null | {
|
||||
project_name: string | null;
|
||||
project_logo: string | null;
|
||||
project_color: string | null;
|
||||
public_foreground: string | null;
|
||||
public_background: string | null;
|
||||
public_note: string | null;
|
||||
custom_css: string | null;
|
||||
};
|
||||
directus?: {
|
||||
version: string;
|
||||
};
|
||||
node?: {
|
||||
version: string;
|
||||
uptime: number;
|
||||
};
|
||||
os?: {
|
||||
type: string;
|
||||
version: string;
|
||||
uptime: number;
|
||||
totalmem: number;
|
||||
};
|
||||
};
|
||||
|
||||
export const useServerStore = createStore({
|
||||
id: 'serverStore',
|
||||
state: () => ({
|
||||
info: null as null | Info,
|
||||
}),
|
||||
actions: {
|
||||
async hydrate() {
|
||||
const response = await api.get(`/server/info`);
|
||||
this.state.info = response.data.data;
|
||||
},
|
||||
dehydrate() {
|
||||
this.reset();
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,5 +1,13 @@
|
||||
<template>
|
||||
<v-drawer v-model="_active" :title="$t('select_item')" @cancel="cancel">
|
||||
<template #actions>
|
||||
<search-input v-model="searchQuery" />
|
||||
|
||||
<v-button @click="save" icon rounded v-tooltip.bottom="$t('save')">
|
||||
<v-icon name="check" />
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
<component
|
||||
:is="`layout-${localLayout}`"
|
||||
:collection="collection"
|
||||
@@ -7,6 +15,7 @@
|
||||
:filters="filters"
|
||||
:layout-query.sync="localQuery"
|
||||
:layout-options.sync="localOptions"
|
||||
:search-query="searchQuery"
|
||||
@update:selection="onSelect"
|
||||
select-mode
|
||||
class="layout"
|
||||
@@ -19,12 +28,6 @@
|
||||
<v-info :title="$tc('item_count', 0)" :icon="collectionInfo.icon" center />
|
||||
</template>
|
||||
</component>
|
||||
|
||||
<template #actions>
|
||||
<v-button @click="save" icon rounded v-tooltip.bottom="$t('save')">
|
||||
<v-icon name="check" />
|
||||
</v-button>
|
||||
</template>
|
||||
</v-drawer>
|
||||
</template>
|
||||
|
||||
@@ -33,8 +36,10 @@ import { defineComponent, PropType, ref, computed, toRefs, onUnmounted } from '@
|
||||
import { Filter } from '@/types';
|
||||
import usePreset from '@/composables/use-preset';
|
||||
import useCollection from '@/composables/use-collection';
|
||||
import SearchInput from '@/views/private/components/search-input';
|
||||
|
||||
export default defineComponent({
|
||||
components: { SearchInput },
|
||||
props: {
|
||||
active: {
|
||||
type: Boolean,
|
||||
@@ -65,7 +70,7 @@ export default defineComponent({
|
||||
const { collection } = toRefs(props);
|
||||
|
||||
const { info: collectionInfo } = useCollection(collection);
|
||||
const { layout, layoutOptions, layoutQuery } = usePreset(collection);
|
||||
const { layout, layoutOptions, layoutQuery, searchQuery } = usePreset(collection);
|
||||
|
||||
// This is a local copy of the layout. This means that we can sync it the layout without
|
||||
// having use-preset auto-save the values
|
||||
@@ -73,7 +78,18 @@ export default defineComponent({
|
||||
const localOptions = ref(layoutOptions.value);
|
||||
const localQuery = ref(layoutQuery.value);
|
||||
|
||||
return { save, cancel, _active, _selection, onSelect, localLayout, localOptions, localQuery, collectionInfo };
|
||||
return {
|
||||
save,
|
||||
cancel,
|
||||
_active,
|
||||
_selection,
|
||||
onSelect,
|
||||
localLayout,
|
||||
localOptions,
|
||||
localQuery,
|
||||
collectionInfo,
|
||||
searchQuery,
|
||||
};
|
||||
|
||||
function useActiveState() {
|
||||
const localActive = ref(false);
|
||||
|
||||
@@ -8,16 +8,16 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from '@vue/composition-api';
|
||||
import LatencyIndicator from '../latency-indicator';
|
||||
import { useSettingsStore, useLatencyStore } from '@/stores/';
|
||||
import { useServerStore, useLatencyStore } from '@/stores/';
|
||||
import { sortBy } from 'lodash';
|
||||
|
||||
export default defineComponent({
|
||||
components: { LatencyIndicator },
|
||||
setup() {
|
||||
const latencyStore = useLatencyStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const serverStore = useServerStore();
|
||||
|
||||
const name = computed(() => settingsStore.state.settings?.project_name);
|
||||
const name = computed(() => serverStore.state.info?.project?.project_name);
|
||||
|
||||
return { name };
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="public-view" :class="{ branded : isBranded }">
|
||||
<div class="public-view" :class="{ branded: isBranded }">
|
||||
<div class="container" :class="{ wide }">
|
||||
<div class="title-box">
|
||||
<div
|
||||
@@ -32,7 +32,7 @@
|
||||
<script lang="ts">
|
||||
import { version } from '../../../package.json';
|
||||
import { defineComponent, computed } from '@vue/composition-api';
|
||||
import { useSettingsStore } from '@/stores';
|
||||
import { useServerStore } from '@/stores';
|
||||
import marked from 'marked';
|
||||
import getRootPath from '../../utils/get-root-path';
|
||||
|
||||
@@ -44,21 +44,21 @@ export default defineComponent({
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const settingsStore = useSettingsStore();
|
||||
const serverStore = useServerStore();
|
||||
|
||||
const isBranded = computed(() => {
|
||||
return (settingsStore.state.settings?.project_color) ? true : false;
|
||||
return serverStore.state.info?.project?.project_color ? true : false;
|
||||
});
|
||||
|
||||
const backgroundStyles = computed<string>(() => {
|
||||
const defaultColor = '#263238';
|
||||
|
||||
if (settingsStore.state.settings?.public_background) {
|
||||
const url = getRootPath() + `assets/${settingsStore.state.settings.public_background}`;
|
||||
if (serverStore.state.info?.project?.public_background) {
|
||||
const url = getRootPath() + `assets/${serverStore.state.info.project?.public_background}`;
|
||||
return `url(${url})`;
|
||||
}
|
||||
|
||||
return settingsStore.state.settings?.project_color || defaultColor;
|
||||
return serverStore.state.info?.project?.project_color || defaultColor;
|
||||
});
|
||||
|
||||
const artStyles = computed(() => ({
|
||||
@@ -68,16 +68,16 @@ export default defineComponent({
|
||||
}));
|
||||
|
||||
const foregroundURL = computed(() => {
|
||||
if (!settingsStore.state.settings?.public_foreground) return null;
|
||||
return getRootPath() + `assets/${settingsStore.state.settings.public_foreground}`;
|
||||
if (!serverStore.state.info?.project?.public_foreground) return null;
|
||||
return getRootPath() + `assets/${serverStore.state.info.project?.public_foreground}`;
|
||||
});
|
||||
|
||||
const logoURL = computed<string | null>(() => {
|
||||
if (!settingsStore.state.settings?.project_logo) return null;
|
||||
return getRootPath() + `assets/${settingsStore.state.settings.project_logo}`;
|
||||
if (!serverStore.state.info?.project?.project_logo) return null;
|
||||
return getRootPath() + `assets/${serverStore.state.info.project?.project_logo}`;
|
||||
});
|
||||
|
||||
return { version, artStyles, marked, settings: settingsStore.state.settings, foregroundURL, logoURL, isBranded };
|
||||
return { version, artStyles, marked, settings: serverStore.state.info, foregroundURL, logoURL, isBranded };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user