mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Merge branch 'main' into aggregation
This commit is contained in:
@@ -1 +1 @@
|
||||
export * from './use-layout-state';
|
||||
export * from './use-system';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
19
packages/shared/src/composables/use-system.ts
Normal file
19
packages/shared/src/composables/use-system.ts
Normal 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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from './extensions';
|
||||
export * from './fields';
|
||||
export * from './injection';
|
||||
export * from './regex';
|
||||
export * from './symbols';
|
||||
|
||||
2
packages/shared/src/constants/injection.ts
Normal file
2
packages/shared/src/constants/injection.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const STORES_INJECT = 'stores';
|
||||
export const API_INJECT = 'api';
|
||||
@@ -1 +0,0 @@
|
||||
export const LAYOUT_SYMBOL = process.env.NODE_ENV === 'development' ? Symbol.for('[Directus]: Layout') : Symbol();
|
||||
@@ -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;
|
||||
|
||||
@@ -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>>;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -7,5 +7,4 @@ export type Permission = {
|
||||
validation: Record<string, any> | null;
|
||||
presets: Record<string, any> | null;
|
||||
fields: string[] | null;
|
||||
limit: number | null;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user