Merge branch 'main' into aggregation

This commit is contained in:
rijkvanzanten
2021-09-13 12:50:19 -04:00
476 changed files with 28319 additions and 34337 deletions

View File

@@ -1 +1 @@
export * from './use-layout-state';
export * from './use-system';

View File

@@ -1,13 +0,0 @@
import { inject, Ref, UnwrapRef } from 'vue';
import { LAYOUT_SYMBOL } from '../constants';
import { LayoutState } from '../types';
export function useLayoutState<T extends Record<string, any> = Record<string, any>, Options = any, Query = any>(): Ref<
UnwrapRef<LayoutState<T, Options, Query>>
> {
const layoutState = inject<Ref<UnwrapRef<LayoutState<T, Options, Query>>>>(LAYOUT_SYMBOL);
if (!layoutState) throw new Error('[useLayoutState]: This function has to be used inside a layout component.');
return layoutState;
}

View File

@@ -0,0 +1,19 @@
import { inject } from 'vue';
import { AxiosInstance } from 'axios';
import { API_INJECT, STORES_INJECT } from '../constants';
export function useStores(): Record<string, any> {
const stores = inject<Record<string, any>>(STORES_INJECT);
if (!stores) throw new Error('[useStores]: This function has to be used inside a Directus extension.');
return stores;
}
export function useApi(): AxiosInstance {
const api = inject<AxiosInstance>(API_INJECT);
if (!api) throw new Error('[useApi]: This function has to be used inside a Directus extension.');
return api;
}

View File

@@ -1,5 +1,5 @@
export const APP_SHARED_DEPS = ['@directus/extension-sdk', 'vue'];
export const API_SHARED_DEPS = ['axios'];
export const APP_SHARED_DEPS = ['@directus/extensions-sdk', 'vue', 'vue-router'];
export const API_SHARED_DEPS = ['@directus/extensions-sdk', 'axios'];
export const APP_EXTENSION_TYPES = ['interface', 'display', 'layout', 'module'] as const;
export const API_EXTENSION_TYPES = ['hook', 'endpoint'] as const;
@@ -10,6 +10,8 @@ export const APP_EXTENSION_PACKAGE_TYPES = [...APP_EXTENSION_TYPES, EXTENSION_PA
export const API_EXTENSION_PACKAGE_TYPES = [...API_EXTENSION_TYPES, EXTENSION_PACK_TYPE] as const;
export const EXTENSION_PACKAGE_TYPES = [...EXTENSION_TYPES, EXTENSION_PACK_TYPE] as const;
export const EXTENSION_LANGUAGES = ['javascript', 'typescript'] as const;
export const EXTENSION_NAME_REGEX = /^(?:(?:@[^/]+\/)?directus-extension-|@directus\/extension-).+$/;
export const EXTENSION_PKG_KEY = 'directus:extension';

View File

@@ -1,4 +1,4 @@
export * from './extensions';
export * from './fields';
export * from './injection';
export * from './regex';
export * from './symbols';

View File

@@ -0,0 +1,2 @@
export const STORES_INJECT = 'stores';
export const API_INJECT = 'api';

View File

@@ -1 +0,0 @@
export const LAYOUT_SYMBOL = process.env.NODE_ENV === 'development' ? Symbol.for('[Directus]: Layout') : Symbol();

View File

@@ -1,4 +1,10 @@
import { Router } from 'express';
import { ApiExtensionContext } from './extensions';
export type EndpointRegisterFunction = (router: Router, context: ApiExtensionContext) => void;
type EndpointHandlerFunction = (router: Router, context: ApiExtensionContext) => void;
interface EndpointAdvancedConfig {
id: string;
handler: EndpointHandlerFunction;
}
export type EndpointConfig = EndpointHandlerFunction | EndpointAdvancedConfig;

View File

@@ -1,3 +1,5 @@
import { Knex } from 'knex';
import { Logger } from 'pino';
import {
API_EXTENSION_PACKAGE_TYPES,
API_EXTENSION_TYPES,
@@ -7,6 +9,7 @@ import {
EXTENSION_PKG_KEY,
EXTENSION_TYPES,
} from '../constants';
import { Accountability } from './accountability';
export type AppExtensionType = typeof APP_EXTENSION_TYPES[number];
export type ApiExtensionType = typeof API_EXTENSION_TYPES[number];
@@ -61,7 +64,8 @@ export type ExtensionManifest = {
export type ApiExtensionContext = {
services: any;
exceptions: any;
database: any;
env: any;
getSchema: any;
database: Knex;
env: Record<string, any>;
logger: Logger;
getSchema: (options?: { accountability?: Accountability; database?: Knex }) => Promise<Record<string, any>>;
};

View File

@@ -1,3 +1,5 @@
import { ApiExtensionContext } from './extensions';
export type HookRegisterFunction = (context: ApiExtensionContext) => Record<string, (...values: any[]) => void>;
type HookHandlerFunction = (context: ApiExtensionContext) => Record<string, (...values: any[]) => void>;
export type HookConfig = HookHandlerFunction;

View File

@@ -7,12 +7,13 @@ export interface LayoutConfig<Options = any, Query = any> {
name: string;
icon: string;
component: Component;
smallHeader?: boolean;
slots: {
options: Component;
sidebar: Component;
actions: Component;
};
setup: (LayoutOptions: LayoutProps<Options, Query>) => any;
setup: (props: LayoutProps<Options, Query>, ctx: LayoutContext) => Record<string, unknown>;
}
export interface LayoutProps<Options = any, Query = any> {
@@ -27,6 +28,13 @@ export interface LayoutProps<Options = any, Query = any> {
resetPreset?: () => Promise<void>;
}
interface LayoutContext {
emit: (
event: 'update:selection' | 'update:layoutOptions' | 'update:layoutQuery' | 'update:filters' | 'update:searchQuery',
...args: any[]
) => void;
}
export type LayoutState<T, Options, Query> = {
props: LayoutProps<Options, Query>;
} & T;

View File

@@ -7,5 +7,4 @@ export type Permission = {
validation: Record<string, any> | null;
presets: Record<string, any> | null;
fields: string[] | null;
limit: number | null;
};

View File

@@ -1,11 +1,4 @@
import {
InterfaceConfig,
DisplayConfig,
LayoutConfig,
ModuleConfig,
HookRegisterFunction,
EndpointRegisterFunction,
} from '../types';
import { InterfaceConfig, DisplayConfig, LayoutConfig, ModuleConfig, HookConfig, EndpointConfig } from '../types';
export function defineInterface(config: InterfaceConfig): InterfaceConfig {
return config;
@@ -25,10 +18,10 @@ export function defineModule(config: ModuleConfig): ModuleConfig {
return config;
}
export function defineHook(config: HookRegisterFunction): HookRegisterFunction {
export function defineHook(config: HookConfig): HookConfig {
return config;
}
export function defineEndpoint(config: EndpointRegisterFunction): EndpointRegisterFunction {
export function defineEndpoint(config: EndpointConfig): EndpointConfig {
return config;
}

View File

@@ -1,8 +1,13 @@
import BaseJoi, { AnySchema } from 'joi';
import BaseJoi, { AnySchema, StringSchema as BaseStringSchema, NumberSchema } from 'joi';
import { escapeRegExp, merge } from 'lodash';
import { FieldFilter } from '../types/filter';
const Joi = BaseJoi.extend({
interface StringSchema extends BaseStringSchema {
contains(substring: string): this;
ncontains(substring: string): this;
}
const Joi: typeof BaseJoi = BaseJoi.extend({
type: 'string',
base: BaseJoi.string(),
messages: {
@@ -95,105 +100,109 @@ export function generateJoi(filter: FieldFilter, options?: JoiOptions): AnySchem
const operator = Object.keys(value)[0];
const compareValue = Object.values(value)[0];
const getAnySchema = () => schema[key] ?? Joi.any();
const getStringSchema = () => (schema[key] ?? Joi.string()) as StringSchema;
const getNumberSchema = () => (schema[key] ?? Joi.number()) as NumberSchema;
if (operator === '_eq') {
schema[key] = (schema[key] ?? Joi.any()).equal(compareValue);
schema[key] = getAnySchema().equal(compareValue);
}
if (operator === '_neq') {
schema[key] = (schema[key] ?? Joi.any()).not(compareValue);
schema[key] = getAnySchema().not(compareValue);
}
if (operator === '_contains') {
schema[key] = (schema[key] ?? Joi.string()).contains(compareValue);
schema[key] = getStringSchema().contains(compareValue);
}
if (operator === '_ncontains') {
schema[key] = (schema[key] ?? Joi.string()).ncontains(compareValue);
schema[key] = getStringSchema().ncontains(compareValue);
}
if (operator === '_starts_with') {
schema[key] = (schema[key] ?? Joi.string()).pattern(new RegExp(`^${escapeRegExp(compareValue as string)}.*`), {
schema[key] = getStringSchema().pattern(new RegExp(`^${escapeRegExp(compareValue as string)}.*`), {
name: 'starts_with',
});
}
if (operator === '_nstarts_with') {
schema[key] = (schema[key] ?? Joi.string()).pattern(new RegExp(`^${escapeRegExp(compareValue as string)}.*`), {
schema[key] = getStringSchema().pattern(new RegExp(`^${escapeRegExp(compareValue as string)}.*`), {
name: 'starts_with',
invert: true,
});
}
if (operator === '_ends_with') {
schema[key] = (schema[key] ?? Joi.string()).pattern(new RegExp(`.*${escapeRegExp(compareValue as string)}$`), {
schema[key] = getStringSchema().pattern(new RegExp(`.*${escapeRegExp(compareValue as string)}$`), {
name: 'ends_with',
});
}
if (operator === '_nends_with') {
schema[key] = (schema[key] ?? Joi.string()).pattern(new RegExp(`.*${escapeRegExp(compareValue as string)}$`), {
schema[key] = getStringSchema().pattern(new RegExp(`.*${escapeRegExp(compareValue as string)}$`), {
name: 'ends_with',
invert: true,
});
}
if (operator === '_in') {
schema[key] = (schema[key] ?? Joi.any()).equal(...(compareValue as (string | number)[]));
schema[key] = getAnySchema().equal(...(compareValue as (string | number)[]));
}
if (operator === '_nin') {
schema[key] = (schema[key] ?? Joi.any()).not(...(compareValue as (string | number)[]));
schema[key] = getAnySchema().not(...(compareValue as (string | number)[]));
}
if (operator === '_gt') {
schema[key] = (schema[key] ?? Joi.number()).greater(Number(compareValue));
schema[key] = getNumberSchema().greater(Number(compareValue));
}
if (operator === '_gte') {
schema[key] = (schema[key] ?? Joi.number()).min(Number(compareValue));
schema[key] = getNumberSchema().min(Number(compareValue));
}
if (operator === '_lt') {
schema[key] = (schema[key] ?? Joi.number()).less(Number(compareValue));
schema[key] = getNumberSchema().less(Number(compareValue));
}
if (operator === '_lte') {
schema[key] = (schema[key] ?? Joi.number()).max(Number(compareValue));
schema[key] = getNumberSchema().max(Number(compareValue));
}
if (operator === '_null') {
schema[key] = (schema[key] ?? Joi.any()).valid(null);
schema[key] = getAnySchema().valid(null);
}
if (operator === '_nnull') {
schema[key] = (schema[key] ?? Joi.any()).invalid(null);
schema[key] = getAnySchema().invalid(null);
}
if (operator === '_empty') {
schema[key] = (schema[key] ?? Joi.any()).valid('');
schema[key] = getAnySchema().valid('');
}
if (operator === '_nempty') {
schema[key] = (schema[key] ?? Joi.any()).invalid('');
schema[key] = getAnySchema().invalid('');
}
if (operator === '_between') {
const values = compareValue as number[];
schema[key] = (schema[key] ?? Joi.number()).greater(values[0]).less(values[1]);
const values = compareValue as [number, number];
schema[key] = getNumberSchema().greater(values[0]).less(values[1]);
}
if (operator === '_nbetween') {
const values = compareValue as number[];
schema[key] = (schema[key] ?? Joi.number()).less(values[0]).greater(values[1]);
const values = compareValue as [number, number];
schema[key] = getNumberSchema().less(values[0]).greater(values[1]);
}
if (operator === '_submitted') {
schema[key] = (schema[key] ?? Joi.any()).required();
schema[key] = getAnySchema().required();
}
if (operator === '_regex') {
const wrapped = compareValue.startsWith('/') && compareValue.endsWith('/');
schema[key] = (schema[key] ?? Joi.string()).regex(new RegExp(wrapped ? compareValue.slice(1, -1) : compareValue));
schema[key] = getStringSchema().regex(new RegExp(wrapped ? compareValue.slice(1, -1) : compareValue));
}
}

View File

@@ -4,7 +4,6 @@ export function getFilterOperatorsForType(type: Type): ClientFilterOperator[] {
switch (type) {
// Text
case 'binary':
case 'json':
case 'hash':
case 'string':
case 'csv':
@@ -22,23 +21,44 @@ export function getFilterOperatorsForType(type: Type): ClientFilterOperator[] {
'in',
'nin',
];
// JSON
case 'json':
return ['eq', 'neq', 'null', 'nnull', 'in', 'nin'];
// UUID
case 'uuid':
return ['eq', 'neq', 'empty', 'nempty', 'in', 'nin'];
return ['eq', 'neq', 'null', 'nnull', 'in', 'nin'];
// Boolean
case 'boolean':
return ['eq', 'neq', 'empty', 'nempty'];
return ['eq', 'neq', 'null', 'nnull'];
// Numbers
case 'integer':
case 'decimal':
return ['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'between', 'nbetween', 'empty', 'nempty', 'in', 'nin'];
return ['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'between', 'nbetween', 'null', 'nnull', 'in', 'nin'];
// Datetime
case 'dateTime':
case 'date':
case 'time':
return ['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'between', 'nbetween', 'empty', 'nempty', 'in', 'nin'];
return [
'eq',
'neq',
'null',
'nnull',
'lt',
'lte',
'gt',
'gte',
'between',
'nbetween',
'null',
'nnull',
'in',
'nin',
];
case 'geometry':
return ['eq', 'neq', 'intersects', 'nintersects', 'intersects_bbox', 'nintersects_bbox'];
@@ -57,6 +77,8 @@ export function getFilterOperatorsForType(type: Type): ClientFilterOperator[] {
'nbetween',
'empty',
'nempty',
'null',
'nnull',
'in',
'nin',
];

View File

@@ -5,14 +5,16 @@ import { adjustDate } from './adjust-date';
import { deepMap } from './deep-map';
export function parseFilter(filter: Filter, accountability: Accountability | null): any {
return deepMap(filter, (val, key) => {
return deepMap(filter, applyFilter);
function applyFilter(val: any, key: string | number) {
if (val === 'true') return true;
if (val === 'false') return false;
if (val === 'null' || val === 'NULL') return null;
if (['_in', '_nin', '_between', '_nbetween'].includes(String(key))) {
if (typeof val === 'string' && val.includes(',')) return val.split(',');
else return toArray(val);
if (typeof val === 'string' && val.includes(',')) return deepMap(val.split(','), applyFilter);
else return deepMap(toArray(val), applyFilter);
}
if (val && typeof val === 'string' && val.startsWith('$NOW')) {
@@ -29,5 +31,5 @@ export function parseFilter(filter: Filter, accountability: Accountability | nul
if (val === '$CURRENT_ROLE') return accountability?.role || null;
return val;
});
}
}