mirror of
https://github.com/directus/directus.git
synced 2026-02-09 00:25:23 -05:00
Merge branch 'main' into aggregation
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
import { ApiExtensionType, AppExtensionType, ExtensionType } from '../types';
|
||||
export const APP_SHARED_DEPS = ['@directus/extension-sdk', 'vue'];
|
||||
export const API_SHARED_DEPS = ['axios'];
|
||||
|
||||
export const SHARED_DEPS = ['@directus/extension-sdk', 'vue'];
|
||||
|
||||
export const APP_EXTENSION_TYPES: AppExtensionType[] = ['interface', 'display', 'layout', 'module'];
|
||||
export const API_EXTENSION_TYPES: ApiExtensionType[] = ['endpoint', 'hook'];
|
||||
export const EXTENSION_TYPES: ExtensionType[] = [...APP_EXTENSION_TYPES, ...API_EXTENSION_TYPES];
|
||||
export const APP_EXTENSION_TYPES = ['interface', 'display', 'layout', 'module'] as const;
|
||||
export const API_EXTENSION_TYPES = ['hook', 'endpoint'] as const;
|
||||
export const EXTENSION_TYPES = [...APP_EXTENSION_TYPES, ...API_EXTENSION_TYPES] as const;
|
||||
export const EXTENSION_PACKAGE_TYPES = [...EXTENSION_TYPES, 'pack'] as const;
|
||||
|
||||
export const EXTENSION_NAME_REGEX = /^(?:(?:@[^/]+\/)?directus-extension-|@directus\/extension-).+$/;
|
||||
|
||||
export const EXTENSION_PKG_KEY = 'directus:extension';
|
||||
|
||||
33
packages/shared/src/constants/fields.ts
Normal file
33
packages/shared/src/constants/fields.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
export const TYPES = [
|
||||
'alias',
|
||||
'bigInteger',
|
||||
'boolean',
|
||||
'date',
|
||||
'dateTime',
|
||||
'decimal',
|
||||
'float',
|
||||
'integer',
|
||||
'json',
|
||||
'string',
|
||||
'text',
|
||||
'time',
|
||||
'timestamp',
|
||||
'binary',
|
||||
'uuid',
|
||||
'hash',
|
||||
'csv',
|
||||
'unknown',
|
||||
] as const;
|
||||
|
||||
export const LOCAL_TYPES = [
|
||||
'standard',
|
||||
'file',
|
||||
'files',
|
||||
'm2o',
|
||||
'o2m',
|
||||
'm2m',
|
||||
'm2a',
|
||||
'presentation',
|
||||
'translations',
|
||||
'group',
|
||||
] as const;
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './extensions';
|
||||
export * from './fields';
|
||||
export * from './symbols';
|
||||
|
||||
13
packages/shared/src/exceptions/base.ts
Normal file
13
packages/shared/src/exceptions/base.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export class BaseException extends Error {
|
||||
status: number;
|
||||
code: string;
|
||||
extensions: Record<string, any>;
|
||||
|
||||
constructor(message: string, status: number, code: string, extensions?: Record<string, any>) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
this.code = code;
|
||||
|
||||
this.extensions = extensions || {};
|
||||
}
|
||||
}
|
||||
107
packages/shared/src/exceptions/failed-validation.ts
Normal file
107
packages/shared/src/exceptions/failed-validation.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { ValidationErrorItem } from 'joi';
|
||||
import { FilterOperator } from '../types';
|
||||
import { BaseException } from './base';
|
||||
|
||||
type FailedValidationExtensions = {
|
||||
field: string;
|
||||
type: FilterOperator | 'required' | 'regex';
|
||||
valid?: number | string | (number | string)[];
|
||||
invalid?: number | string | (number | string)[];
|
||||
substring?: string;
|
||||
};
|
||||
|
||||
export class FailedValidationException extends BaseException {
|
||||
constructor(error: ValidationErrorItem) {
|
||||
const extensions: Partial<FailedValidationExtensions> = {
|
||||
field: error.path[0] as string,
|
||||
};
|
||||
|
||||
const joiType = error.type;
|
||||
|
||||
// eq | in | null | empty
|
||||
if (joiType.endsWith('only')) {
|
||||
if (error.context?.valids.length > 1) {
|
||||
extensions.type = 'in';
|
||||
extensions.valid = error.context?.valids;
|
||||
} else {
|
||||
const valid = error.context?.valids[0];
|
||||
|
||||
if (valid === null) {
|
||||
extensions.type = 'null';
|
||||
} else if (valid === '') {
|
||||
extensions.type = 'empty';
|
||||
} else {
|
||||
extensions.type = 'eq';
|
||||
extensions.valid = error.context?.valids[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// neq | nin | nnull | nempty
|
||||
if (joiType.endsWith('invalid')) {
|
||||
if (error.context?.invalids.length > 1) {
|
||||
extensions.type = 'nin';
|
||||
extensions.invalid = error.context?.invalids;
|
||||
} else {
|
||||
const invalid = error.context?.invalids[0];
|
||||
|
||||
if (invalid === null) {
|
||||
extensions.type = 'nnull';
|
||||
} else if (invalid === '') {
|
||||
extensions.type = 'nempty';
|
||||
} else {
|
||||
extensions.type = 'neq';
|
||||
extensions.invalid = invalid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// gt
|
||||
if (joiType.endsWith('greater')) {
|
||||
extensions.type = 'gt';
|
||||
extensions.valid = error.context?.limit;
|
||||
}
|
||||
|
||||
// gte
|
||||
if (joiType.endsWith('min')) {
|
||||
extensions.type = 'gte';
|
||||
extensions.valid = error.context?.limit;
|
||||
}
|
||||
|
||||
// lt
|
||||
if (joiType.endsWith('less')) {
|
||||
extensions.type = 'lt';
|
||||
extensions.valid = error.context?.limit;
|
||||
}
|
||||
|
||||
// lte
|
||||
if (joiType.endsWith('max')) {
|
||||
extensions.type = 'lte';
|
||||
extensions.valid = error.context?.limit;
|
||||
}
|
||||
|
||||
// contains
|
||||
if (joiType.endsWith('contains')) {
|
||||
extensions.type = 'contains';
|
||||
extensions.substring = error.context?.substring;
|
||||
}
|
||||
|
||||
// ncontains
|
||||
if (joiType.endsWith('ncontains')) {
|
||||
extensions.type = 'ncontains';
|
||||
extensions.substring = error.context?.substring;
|
||||
}
|
||||
|
||||
// required
|
||||
if (joiType.endsWith('required')) {
|
||||
extensions.type = 'required';
|
||||
}
|
||||
|
||||
if (joiType.endsWith('.pattern.base')) {
|
||||
extensions.type = 'regex';
|
||||
extensions.invalid = error.context?.value;
|
||||
}
|
||||
|
||||
super(error.message, 400, 'FAILED_VALIDATION', extensions);
|
||||
}
|
||||
}
|
||||
2
packages/shared/src/exceptions/index.ts
Normal file
2
packages/shared/src/exceptions/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './base';
|
||||
export * from './failed-validation';
|
||||
9
packages/shared/src/types/accountability.ts
Normal file
9
packages/shared/src/types/accountability.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export type Accountability = {
|
||||
role: string | null;
|
||||
user?: string | null;
|
||||
admin?: boolean;
|
||||
app?: boolean;
|
||||
|
||||
ip?: string;
|
||||
userAgent?: string;
|
||||
};
|
||||
35
packages/shared/src/types/displays.ts
Normal file
35
packages/shared/src/types/displays.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Component, ComponentOptions } from 'vue';
|
||||
import { Field, LocalType, Type } from './fields';
|
||||
import { DeepPartial } from './misc';
|
||||
|
||||
export type DisplayHandlerFunctionContext = {
|
||||
type: string;
|
||||
};
|
||||
|
||||
export type DisplayHandlerFunction = (
|
||||
value: any,
|
||||
options: Record<string, any> | null,
|
||||
context?: DisplayHandlerFunctionContext
|
||||
) => string | null;
|
||||
|
||||
export type DisplayFieldsFunction = (
|
||||
options: any,
|
||||
context: {
|
||||
collection: string;
|
||||
field: string;
|
||||
type: string;
|
||||
}
|
||||
) => string[];
|
||||
|
||||
export interface DisplayConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
description?: string;
|
||||
|
||||
handler: DisplayHandlerFunction | ComponentOptions;
|
||||
options: DeepPartial<Field>[] | Component | null;
|
||||
types: readonly Type[];
|
||||
groups?: readonly LocalType[];
|
||||
fields?: string[] | DisplayFieldsFunction;
|
||||
}
|
||||
4
packages/shared/src/types/endpoints.ts
Normal file
4
packages/shared/src/types/endpoints.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { Router } from 'express';
|
||||
import { ApiExtensionContext } from './extensions';
|
||||
|
||||
export type EndpointRegisterFunction = (router: Router, context: ApiExtensionContext) => void;
|
||||
@@ -1,7 +1,15 @@
|
||||
export type ApiExtensionType = 'endpoint' | 'hook';
|
||||
export type AppExtensionType = 'interface' | 'display' | 'layout' | 'module';
|
||||
export type ExtensionType = ApiExtensionType | AppExtensionType;
|
||||
export type ExtensionPackageType = ExtensionType | 'pack';
|
||||
import {
|
||||
API_EXTENSION_TYPES,
|
||||
APP_EXTENSION_TYPES,
|
||||
EXTENSION_PACKAGE_TYPES,
|
||||
EXTENSION_PKG_KEY,
|
||||
EXTENSION_TYPES,
|
||||
} from '../constants';
|
||||
|
||||
export type ApiExtensionType = typeof API_EXTENSION_TYPES[number];
|
||||
export type AppExtensionType = typeof APP_EXTENSION_TYPES[number];
|
||||
export type ExtensionType = typeof EXTENSION_TYPES[number];
|
||||
export type ExtensionPackageType = typeof EXTENSION_PACKAGE_TYPES[number];
|
||||
|
||||
export type Extension = {
|
||||
path: string;
|
||||
@@ -16,3 +24,39 @@ export type Extension = {
|
||||
local: boolean;
|
||||
root: boolean;
|
||||
};
|
||||
|
||||
export type ExtensionManifestRaw = {
|
||||
name?: string;
|
||||
version?: string;
|
||||
dependencies?: Record<string, string>;
|
||||
|
||||
[EXTENSION_PKG_KEY]?: {
|
||||
type?: string;
|
||||
path?: string;
|
||||
source?: string;
|
||||
host?: string;
|
||||
hidden?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type ExtensionManifest = {
|
||||
name: string;
|
||||
version: string;
|
||||
dependencies?: Record<string, string>;
|
||||
|
||||
[EXTENSION_PKG_KEY]: {
|
||||
type: ExtensionPackageType;
|
||||
path: string;
|
||||
source: string;
|
||||
host: string;
|
||||
hidden: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type ApiExtensionContext = {
|
||||
services: any;
|
||||
exceptions: any;
|
||||
database: any;
|
||||
env: any;
|
||||
getSchema: any;
|
||||
};
|
||||
|
||||
67
packages/shared/src/types/fields.ts
Normal file
67
packages/shared/src/types/fields.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { FilterOperator } from './filter';
|
||||
import { Column } from 'knex-schema-inspector/dist/types/column';
|
||||
import { LOCAL_TYPES, TYPES } from '../constants';
|
||||
|
||||
type Translations = {
|
||||
language: string;
|
||||
translation: string;
|
||||
};
|
||||
|
||||
export type Width = 'half' | 'half-left' | 'half-right' | 'full' | 'fill';
|
||||
|
||||
export type Type = typeof TYPES[number];
|
||||
|
||||
export type LocalType = typeof LOCAL_TYPES[number];
|
||||
|
||||
export type FieldMeta = {
|
||||
id: number;
|
||||
collection: string;
|
||||
field: string;
|
||||
group: number | null;
|
||||
hidden: boolean;
|
||||
interface: string | null;
|
||||
display: string | null;
|
||||
options: Record<string, any> | null;
|
||||
display_options: Record<string, any> | null;
|
||||
readonly: boolean;
|
||||
required: boolean;
|
||||
sort: number | null;
|
||||
special: string[] | null;
|
||||
translations: Translations[] | null;
|
||||
width: Width | null;
|
||||
note: string | null;
|
||||
conditions: Condition[] | null;
|
||||
system?: true;
|
||||
};
|
||||
|
||||
export interface FieldRaw {
|
||||
collection: string;
|
||||
field: string;
|
||||
type: Type;
|
||||
schema: Column | null;
|
||||
meta: FieldMeta | null;
|
||||
}
|
||||
|
||||
export interface Field extends FieldRaw {
|
||||
name: string;
|
||||
children?: Field[] | null;
|
||||
}
|
||||
|
||||
export type ValidationError = {
|
||||
code: string;
|
||||
field: string;
|
||||
type: FilterOperator;
|
||||
valid?: number | string | (number | string)[];
|
||||
invalid?: number | string | (number | string)[];
|
||||
substring?: string;
|
||||
};
|
||||
|
||||
export type Condition = {
|
||||
name: string;
|
||||
rule: Record<string, any>;
|
||||
|
||||
readonly?: boolean;
|
||||
hidden?: boolean;
|
||||
options?: Record<string, any>;
|
||||
required?: boolean;
|
||||
};
|
||||
52
packages/shared/src/types/filter.ts
Normal file
52
packages/shared/src/types/filter.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
export type FilterOperator =
|
||||
| 'eq'
|
||||
| 'neq'
|
||||
| 'lt'
|
||||
| 'lte'
|
||||
| 'gt'
|
||||
| 'gte'
|
||||
| 'in'
|
||||
| 'nin'
|
||||
| 'null'
|
||||
| 'nnull'
|
||||
| 'contains'
|
||||
| 'ncontains'
|
||||
| 'between'
|
||||
| 'nbetween'
|
||||
| 'empty'
|
||||
| 'nempty';
|
||||
|
||||
export type ClientFilterOperator = FilterOperator | 'starts_with' | 'nstarts_with' | 'ends_with' | 'nends_with';
|
||||
|
||||
export type Filter = FieldFilter & {
|
||||
_and?: FieldFilter[];
|
||||
_or?: FieldFilter[];
|
||||
};
|
||||
|
||||
export type FieldFilter = {
|
||||
[field: string]: FieldFilterOperator | FieldValidationOperator | FieldFilter;
|
||||
};
|
||||
|
||||
export type FieldFilterOperator = {
|
||||
_eq?: string | number | boolean;
|
||||
_neq?: string | number | boolean;
|
||||
_lt?: string | number;
|
||||
_lte?: string | number;
|
||||
_gt?: string | number;
|
||||
_gte?: string | number;
|
||||
_in?: (string | number)[];
|
||||
_nin?: (string | number)[];
|
||||
_null?: boolean;
|
||||
_nnull?: boolean;
|
||||
_contains?: string;
|
||||
_ncontains?: string;
|
||||
_between?: (string | number)[];
|
||||
_nbetween?: (string | number)[];
|
||||
_empty?: boolean;
|
||||
_nempty?: boolean;
|
||||
};
|
||||
|
||||
export type FieldValidationOperator = {
|
||||
_submitted?: boolean;
|
||||
_regex?: string;
|
||||
};
|
||||
3
packages/shared/src/types/hooks.ts
Normal file
3
packages/shared/src/types/hooks.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { ApiExtensionContext } from './extensions';
|
||||
|
||||
export type HookRegisterFunction = (context: ApiExtensionContext) => Record<string, (...values: any[]) => void>;
|
||||
@@ -1,5 +1,15 @@
|
||||
export * from './accountability';
|
||||
export * from './displays';
|
||||
export * from './endpoints';
|
||||
export * from './extensions';
|
||||
export * from './fields';
|
||||
export * from './filter';
|
||||
export * from './hooks';
|
||||
export * from './interfaces';
|
||||
export * from './items';
|
||||
export * from './layouts';
|
||||
export * from './misc';
|
||||
export * from './modules';
|
||||
export * from './permissions';
|
||||
export * from './presets';
|
||||
export * from './users';
|
||||
|
||||
19
packages/shared/src/types/interfaces.ts
Normal file
19
packages/shared/src/types/interfaces.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Field, LocalType, Type } from './fields';
|
||||
import { Component } from 'vue';
|
||||
import { DeepPartial } from './misc';
|
||||
|
||||
export interface InterfaceConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
description?: string;
|
||||
component: Component;
|
||||
options: DeepPartial<Field>[] | Component | null;
|
||||
types: readonly Type[];
|
||||
groups?: readonly LocalType[];
|
||||
relational?: boolean;
|
||||
hideLabel?: boolean;
|
||||
hideLoader?: boolean;
|
||||
system?: boolean;
|
||||
recommendedDisplays?: string[];
|
||||
}
|
||||
@@ -1,12 +1,26 @@
|
||||
import { Component } from 'vue';
|
||||
import { Item } from './items';
|
||||
import { Filter } from './presets';
|
||||
import { AppFilter } from './presets';
|
||||
|
||||
export interface LayoutConfig<Options = any, Query = any> {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
component: Component;
|
||||
slots: {
|
||||
options: Component;
|
||||
sidebar: Component;
|
||||
actions: Component;
|
||||
};
|
||||
setup: (LayoutOptions: LayoutProps<Options, Query>) => any;
|
||||
}
|
||||
|
||||
export interface LayoutProps<Options = any, Query = any> {
|
||||
collection: string | null;
|
||||
selection: Item[];
|
||||
layoutOptions: Options;
|
||||
layoutQuery: Query;
|
||||
filters: Filter[];
|
||||
filters: AppFilter[];
|
||||
searchQuery: string | null;
|
||||
selectMode: boolean;
|
||||
readonly: boolean;
|
||||
|
||||
@@ -1 +1,36 @@
|
||||
type Primitive = undefined | null | string | number | boolean | bigint | symbol;
|
||||
type Builtin = Primitive | Date | Error | RegExp | ((...args: any[]) => unknown);
|
||||
type Tuple =
|
||||
| [unknown]
|
||||
| [unknown, unknown]
|
||||
| [unknown, unknown, unknown]
|
||||
| [unknown, unknown, unknown, unknown]
|
||||
| [unknown, unknown, unknown, unknown, unknown];
|
||||
|
||||
export type DeepPartial<T> = T extends Builtin
|
||||
? T
|
||||
: T extends Tuple
|
||||
? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: T extends Array<infer U>
|
||||
? Array<DeepPartial<U>>
|
||||
: T extends ReadonlyArray<infer U>
|
||||
? ReadonlyArray<DeepPartial<U>>
|
||||
: T extends Map<infer K, infer V>
|
||||
? Map<DeepPartial<K>, DeepPartial<V>>
|
||||
: T extends ReadonlyMap<infer K, infer V>
|
||||
? ReadonlyMap<DeepPartial<K>, DeepPartial<V>>
|
||||
: T extends WeakMap<infer K, infer V>
|
||||
? WeakMap<DeepPartial<K>, DeepPartial<V>>
|
||||
: T extends Set<infer U>
|
||||
? Set<DeepPartial<U>>
|
||||
: T extends ReadonlySet<infer U>
|
||||
? ReadonlySet<DeepPartial<U>>
|
||||
: T extends WeakSet<infer U>
|
||||
? WeakSet<DeepPartial<U>>
|
||||
: T extends Promise<infer U>
|
||||
? Promise<DeepPartial<U>>
|
||||
: T extends Record<any, any>
|
||||
? { [K in keyof T]?: DeepPartial<T[K]> }
|
||||
: Partial<T>;
|
||||
|
||||
export type Plural<T extends string> = `${T}s`;
|
||||
|
||||
16
packages/shared/src/types/modules.ts
Normal file
16
packages/shared/src/types/modules.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Ref } from 'vue';
|
||||
import { RouteRecordRaw } from 'vue-router';
|
||||
import { Permission, User } from '../types';
|
||||
|
||||
export interface ModuleConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
hidden?: boolean | Ref<boolean>;
|
||||
icon: string;
|
||||
routes?: RouteRecordRaw[];
|
||||
link?: string;
|
||||
color?: string;
|
||||
preRegisterCheck?: (user: User, permissions: Permission[]) => Promise<boolean> | boolean;
|
||||
order?: number;
|
||||
persistent?: boolean;
|
||||
}
|
||||
11
packages/shared/src/types/permissions.ts
Normal file
11
packages/shared/src/types/permissions.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export type Permission = {
|
||||
id: number;
|
||||
role: string | null;
|
||||
collection: string;
|
||||
action: 'create' | 'read' | 'update' | 'delete';
|
||||
permissions: Record<string, any> | null;
|
||||
validation: Record<string, any> | null;
|
||||
presets: Record<string, any> | null;
|
||||
fields: string[] | null;
|
||||
limit: number | null;
|
||||
};
|
||||
@@ -1,22 +1,6 @@
|
||||
export type FilterOperator =
|
||||
| 'eq'
|
||||
| 'neq'
|
||||
| 'lt'
|
||||
| 'lte'
|
||||
| 'gt'
|
||||
| 'gte'
|
||||
| 'in'
|
||||
| 'nin'
|
||||
| 'null'
|
||||
| 'nnull'
|
||||
| 'contains'
|
||||
| 'ncontains'
|
||||
| 'between'
|
||||
| 'nbetween'
|
||||
| 'empty'
|
||||
| 'nempty';
|
||||
import { FilterOperator } from './filter';
|
||||
|
||||
export type Filter = {
|
||||
export type AppFilter = {
|
||||
key: string;
|
||||
field: string;
|
||||
operator: FilterOperator;
|
||||
@@ -31,7 +15,7 @@ export type Preset = {
|
||||
role: string | null;
|
||||
collection: string;
|
||||
search: string | null;
|
||||
filters: readonly Filter[] | null;
|
||||
filters: readonly AppFilter[] | null;
|
||||
layout: string | null;
|
||||
layout_query: { [layout: string]: any } | null;
|
||||
layout_options: { [layout: string]: any } | null;
|
||||
|
||||
55
packages/shared/src/types/users.ts
Normal file
55
packages/shared/src/types/users.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
export type Role = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
collection_list:
|
||||
| null
|
||||
| {
|
||||
group_name: string;
|
||||
accordion: string;
|
||||
collections: {
|
||||
collection: string;
|
||||
}[];
|
||||
}[];
|
||||
module_list:
|
||||
| null
|
||||
| {
|
||||
link: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
}[];
|
||||
enforce_2fa: null | boolean;
|
||||
external_id: null | string;
|
||||
ip_whitelist: string[];
|
||||
app_access: boolean;
|
||||
admin_access: boolean;
|
||||
};
|
||||
|
||||
export type Avatar = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
// There's more data returned in thumbnails and the avatar data, but we
|
||||
// only care about the thumbnails in this context
|
||||
|
||||
export type User = {
|
||||
id: string;
|
||||
status: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
token: string;
|
||||
last_login: string;
|
||||
last_page: string;
|
||||
external_id: string;
|
||||
'2fa_secret': string;
|
||||
theme: 'auto' | 'dark' | 'light';
|
||||
role: Role;
|
||||
password_reset_token: string | null;
|
||||
timezone: string;
|
||||
language: string;
|
||||
avatar: null | Avatar;
|
||||
company: string | null;
|
||||
title: string | null;
|
||||
email_notifications: boolean;
|
||||
};
|
||||
95
packages/shared/src/utils/adjust-date.ts
Normal file
95
packages/shared/src/utils/adjust-date.ts
Normal file
@@ -0,0 +1,95 @@
|
||||
import {
|
||||
addYears,
|
||||
subWeeks,
|
||||
subYears,
|
||||
addWeeks,
|
||||
subMonths,
|
||||
addMonths,
|
||||
subDays,
|
||||
addDays,
|
||||
subHours,
|
||||
addHours,
|
||||
subMinutes,
|
||||
addMinutes,
|
||||
subSeconds,
|
||||
addSeconds,
|
||||
addMilliseconds,
|
||||
subMilliseconds,
|
||||
} from 'date-fns';
|
||||
import { clone } from 'lodash';
|
||||
|
||||
/**
|
||||
* Adjust a given date by a given change in duration. The adjustment value uses the exact same syntax
|
||||
* and logic as Vercel's `ms`.
|
||||
*
|
||||
* The conversion is lifted straight from `ms`.
|
||||
*/
|
||||
export function adjustDate(date: Date, adjustment: string): Date | undefined {
|
||||
date = clone(date);
|
||||
|
||||
const subtract = adjustment.startsWith('-');
|
||||
|
||||
if (subtract || adjustment.startsWith('+')) {
|
||||
adjustment = adjustment.substring(1);
|
||||
}
|
||||
|
||||
const match =
|
||||
/^(-?(?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|months?|mth|mo|years?|yrs?|y)?$/i.exec(
|
||||
adjustment
|
||||
);
|
||||
|
||||
if (!match || !match[1]) {
|
||||
return;
|
||||
}
|
||||
|
||||
const amount = parseFloat(match[1]);
|
||||
const type = (match[2] || 'days').toLowerCase();
|
||||
|
||||
switch (type) {
|
||||
case 'years':
|
||||
case 'year':
|
||||
case 'yrs':
|
||||
case 'yr':
|
||||
case 'y':
|
||||
return subtract ? subYears(date, amount) : addYears(date, amount);
|
||||
case 'months':
|
||||
case 'month':
|
||||
case 'mth':
|
||||
case 'mo':
|
||||
return subtract ? subMonths(date, amount) : addMonths(date, amount);
|
||||
case 'weeks':
|
||||
case 'week':
|
||||
case 'w':
|
||||
return subtract ? subWeeks(date, amount) : addWeeks(date, amount);
|
||||
case 'days':
|
||||
case 'day':
|
||||
case 'd':
|
||||
return subtract ? subDays(date, amount) : addDays(date, amount);
|
||||
case 'hours':
|
||||
case 'hour':
|
||||
case 'hrs':
|
||||
case 'hr':
|
||||
case 'h':
|
||||
return subtract ? subHours(date, amount) : addHours(date, amount);
|
||||
case 'minutes':
|
||||
case 'minute':
|
||||
case 'mins':
|
||||
case 'min':
|
||||
case 'm':
|
||||
return subtract ? subMinutes(date, amount) : addMinutes(date, amount);
|
||||
case 'seconds':
|
||||
case 'second':
|
||||
case 'secs':
|
||||
case 'sec':
|
||||
case 's':
|
||||
return subtract ? subSeconds(date, amount) : addSeconds(date, amount);
|
||||
case 'milliseconds':
|
||||
case 'millisecond':
|
||||
case 'msecs':
|
||||
case 'msec':
|
||||
case 'ms':
|
||||
return subtract ? subMilliseconds(date, amount) : addMilliseconds(date, amount);
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
2
packages/shared/src/utils/browser/index.ts
Normal file
2
packages/shared/src/utils/browser/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
// nothing to see here
|
||||
export {};
|
||||
28
packages/shared/src/utils/deep-map.ts
Normal file
28
packages/shared/src/utils/deep-map.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
export function deepMap(
|
||||
object: Record<string, any>,
|
||||
iterator: (value: any, key: string | number) => any,
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
context?: any
|
||||
): any {
|
||||
if (Array.isArray(object)) {
|
||||
return object.map(function (val, key) {
|
||||
return typeof val === 'object' ? deepMap(val, iterator, context) : iterator.call(context, val, key);
|
||||
});
|
||||
} else if (typeof object === 'object') {
|
||||
const res: Record<string, any> = {};
|
||||
|
||||
for (const key in object) {
|
||||
const val = object[key];
|
||||
|
||||
if (typeof val === 'object') {
|
||||
res[key] = deepMap(val, iterator, context);
|
||||
} else {
|
||||
res[key] = iterator.call(context, val, key);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
} else {
|
||||
return object;
|
||||
}
|
||||
}
|
||||
34
packages/shared/src/utils/define-extension.ts
Normal file
34
packages/shared/src/utils/define-extension.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import {
|
||||
InterfaceConfig,
|
||||
DisplayConfig,
|
||||
LayoutConfig,
|
||||
ModuleConfig,
|
||||
HookRegisterFunction,
|
||||
EndpointRegisterFunction,
|
||||
} from '../types';
|
||||
|
||||
export function defineInterface(config: InterfaceConfig): InterfaceConfig {
|
||||
return config;
|
||||
}
|
||||
|
||||
export function defineDisplay(config: DisplayConfig): DisplayConfig {
|
||||
return config;
|
||||
}
|
||||
|
||||
export function defineLayout<Options = any, Query = any>(
|
||||
config: LayoutConfig<Options, Query>
|
||||
): LayoutConfig<Options, Query> {
|
||||
return config;
|
||||
}
|
||||
|
||||
export function defineModule(config: ModuleConfig): ModuleConfig {
|
||||
return config;
|
||||
}
|
||||
|
||||
export function defineHook(config: HookRegisterFunction): HookRegisterFunction {
|
||||
return config;
|
||||
}
|
||||
|
||||
export function defineEndpoint(config: EndpointRegisterFunction): EndpointRegisterFunction {
|
||||
return config;
|
||||
}
|
||||
207
packages/shared/src/utils/generate-joi.ts
Normal file
207
packages/shared/src/utils/generate-joi.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
import BaseJoi, { AnySchema } from 'joi';
|
||||
import { escapeRegExp, merge } from 'lodash';
|
||||
import { FieldFilter } from '../types/filter';
|
||||
|
||||
const Joi = BaseJoi.extend({
|
||||
type: 'string',
|
||||
base: BaseJoi.string(),
|
||||
messages: {
|
||||
'string.contains': '{{#label}} must contain [{{#substring}}]',
|
||||
'string.ncontains': "{{#label}} can't contain [{{#substring}}]",
|
||||
},
|
||||
rules: {
|
||||
contains: {
|
||||
args: [
|
||||
{
|
||||
name: 'substring',
|
||||
ref: true,
|
||||
assert: (val) => typeof val === 'string',
|
||||
message: 'must be a string',
|
||||
},
|
||||
],
|
||||
method(substring) {
|
||||
return this.$_addRule({ name: 'contains', args: { substring } });
|
||||
},
|
||||
validate(value, helpers, { substring }) {
|
||||
if (value.includes(substring) === false) {
|
||||
return helpers.error('string.contains', { substring });
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
},
|
||||
ncontains: {
|
||||
args: [
|
||||
{
|
||||
name: 'substring',
|
||||
ref: true,
|
||||
assert: (val) => typeof val === 'string',
|
||||
message: 'must be a string',
|
||||
},
|
||||
],
|
||||
method(substring) {
|
||||
return this.$_addRule({ name: 'ncontains', args: { substring } });
|
||||
},
|
||||
validate(value, helpers, { substring }) {
|
||||
if (value.includes(substring) === true) {
|
||||
return helpers.error('string.ncontains', { substring });
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export type JoiOptions = {
|
||||
requireAll?: boolean;
|
||||
};
|
||||
|
||||
const defaults: JoiOptions = {
|
||||
requireAll: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a Joi schema from a filter object.
|
||||
*
|
||||
* @param {FieldFilter} filter - Field filter object. Note: does not support _and/_or filters.
|
||||
* @param {JoiOptions} [options] - Options for the schema generation.
|
||||
* @returns {AnySchema} Joi schema.
|
||||
*/
|
||||
export function generateJoi(filter: FieldFilter, options?: JoiOptions): AnySchema {
|
||||
filter = filter || {};
|
||||
|
||||
options = merge({}, defaults, options);
|
||||
|
||||
const schema: Record<string, AnySchema> = {};
|
||||
|
||||
const key = Object.keys(filter)[0];
|
||||
|
||||
if (!key) {
|
||||
throw new Error(`[generateJoi] Filter doesn't contain field key. Passed filter: ${JSON.stringify(filter)}`);
|
||||
}
|
||||
|
||||
const value = Object.values(filter)[0];
|
||||
|
||||
if (!value) {
|
||||
throw new Error(`[generateJoi] Filter doesn't contain filter rule. Passed filter: ${JSON.stringify(filter)}`);
|
||||
}
|
||||
|
||||
if (Object.keys(value)[0]?.startsWith('_') === false) {
|
||||
schema[key] = Joi.object({
|
||||
[key]: generateJoi(value as FieldFilter, options),
|
||||
});
|
||||
} else {
|
||||
const operator = Object.keys(value)[0];
|
||||
const compareValue = Object.values(value)[0];
|
||||
|
||||
if (operator === '_eq') {
|
||||
schema[key] = (schema[key] ?? Joi.any()).equal(compareValue);
|
||||
}
|
||||
|
||||
if (operator === '_neq') {
|
||||
schema[key] = (schema[key] ?? Joi.any()).not(compareValue);
|
||||
}
|
||||
|
||||
if (operator === '_contains') {
|
||||
schema[key] = (schema[key] ?? Joi.string()).contains(compareValue);
|
||||
}
|
||||
|
||||
if (operator === '_ncontains') {
|
||||
schema[key] = (schema[key] ?? Joi.string()).ncontains(compareValue);
|
||||
}
|
||||
|
||||
if (operator === '_starts_with') {
|
||||
schema[key] = (schema[key] ?? Joi.string()).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)}.*`), {
|
||||
name: 'starts_with',
|
||||
invert: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (operator === '_ends_with') {
|
||||
schema[key] = (schema[key] ?? Joi.string()).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)}$`), {
|
||||
name: 'ends_with',
|
||||
invert: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (operator === '_in') {
|
||||
schema[key] = (schema[key] ?? Joi.any()).equal(...(compareValue as (string | number)[]));
|
||||
}
|
||||
|
||||
if (operator === '_nin') {
|
||||
schema[key] = (schema[key] ?? Joi.any()).not(...(compareValue as (string | number)[]));
|
||||
}
|
||||
|
||||
if (operator === '_gt') {
|
||||
schema[key] = (schema[key] ?? Joi.number()).greater(Number(compareValue));
|
||||
}
|
||||
|
||||
if (operator === '_gte') {
|
||||
schema[key] = (schema[key] ?? Joi.number()).min(Number(compareValue));
|
||||
}
|
||||
|
||||
if (operator === '_lt') {
|
||||
schema[key] = (schema[key] ?? Joi.number()).less(Number(compareValue));
|
||||
}
|
||||
|
||||
if (operator === '_lte') {
|
||||
schema[key] = (schema[key] ?? Joi.number()).max(Number(compareValue));
|
||||
}
|
||||
|
||||
if (operator === '_null') {
|
||||
schema[key] = (schema[key] ?? Joi.any()).valid(null);
|
||||
}
|
||||
|
||||
if (operator === '_nnull') {
|
||||
schema[key] = (schema[key] ?? Joi.any()).invalid(null);
|
||||
}
|
||||
|
||||
if (operator === '_empty') {
|
||||
schema[key] = (schema[key] ?? Joi.any()).valid('');
|
||||
}
|
||||
|
||||
if (operator === '_nempty') {
|
||||
schema[key] = (schema[key] ?? Joi.any()).invalid('');
|
||||
}
|
||||
|
||||
if (operator === '_between') {
|
||||
const values = compareValue as number[];
|
||||
schema[key] = (schema[key] ?? Joi.number()).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]);
|
||||
}
|
||||
|
||||
if (operator === '_submitted') {
|
||||
schema[key] = (schema[key] ?? Joi.any()).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] = schema[key] ?? Joi.any();
|
||||
|
||||
if (options.requireAll) {
|
||||
schema[key] = schema[key]!.required();
|
||||
}
|
||||
|
||||
return Joi.object(schema).unknown();
|
||||
}
|
||||
60
packages/shared/src/utils/get-filter-operators-for-type.ts
Normal file
60
packages/shared/src/utils/get-filter-operators-for-type.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { ClientFilterOperator, Type } from '../types';
|
||||
|
||||
export function getFilterOperatorsForType(type: Type): ClientFilterOperator[] {
|
||||
switch (type) {
|
||||
// Text
|
||||
case 'binary':
|
||||
case 'json':
|
||||
case 'hash':
|
||||
case 'string':
|
||||
return [
|
||||
'contains',
|
||||
'ncontains',
|
||||
'starts_with',
|
||||
'nstarts_with',
|
||||
'ends_with',
|
||||
'nends_with',
|
||||
'eq',
|
||||
'neq',
|
||||
'empty',
|
||||
'nempty',
|
||||
'in',
|
||||
'nin',
|
||||
];
|
||||
case 'uuid':
|
||||
return ['eq', 'neq', 'empty', 'nempty', 'in', 'nin'];
|
||||
|
||||
// Boolean
|
||||
case 'boolean':
|
||||
return ['eq', 'neq', 'empty', 'nempty'];
|
||||
|
||||
// Numbers
|
||||
case 'integer':
|
||||
case 'decimal':
|
||||
return ['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'between', 'nbetween', 'empty', 'nempty', 'in', 'nin'];
|
||||
|
||||
// Datetime
|
||||
case 'dateTime':
|
||||
case 'date':
|
||||
case 'time':
|
||||
return ['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'between', 'nbetween', 'empty', 'nempty', 'in', 'nin'];
|
||||
|
||||
default:
|
||||
return [
|
||||
'eq',
|
||||
'neq',
|
||||
'lt',
|
||||
'lte',
|
||||
'gt',
|
||||
'gte',
|
||||
'contains',
|
||||
'ncontains',
|
||||
'between',
|
||||
'nbetween',
|
||||
'empty',
|
||||
'nempty',
|
||||
'in',
|
||||
'nin',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
export * from './ensure-extensions-dirs';
|
||||
export * from './generate-extensions-entry';
|
||||
export * from './get-extensions';
|
||||
export * from './list-folders';
|
||||
export * from './adjust-date';
|
||||
export * from './deep-map';
|
||||
export * from './define-extension';
|
||||
export * from './generate-joi';
|
||||
export * from './get-filter-operators-for-type';
|
||||
export * from './is-extension';
|
||||
export * from './parse-filter';
|
||||
export * from './pluralize';
|
||||
export * from './resolve-package';
|
||||
export * from './to-array';
|
||||
export * from './validate-extension-manifest';
|
||||
export * from './validate-payload';
|
||||
|
||||
18
packages/shared/src/utils/is-extension.ts
Normal file
18
packages/shared/src/utils/is-extension.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { API_EXTENSION_TYPES, APP_EXTENSION_TYPES, EXTENSION_PACKAGE_TYPES, EXTENSION_TYPES } from '../constants';
|
||||
import { ApiExtensionType, AppExtensionType, ExtensionPackageType, ExtensionType } from '../types';
|
||||
|
||||
export function isExtension(type: string): type is ExtensionType {
|
||||
return (EXTENSION_TYPES as readonly string[]).includes(type);
|
||||
}
|
||||
|
||||
export function isAppExtension(type: string): type is AppExtensionType {
|
||||
return (APP_EXTENSION_TYPES as readonly string[]).includes(type);
|
||||
}
|
||||
|
||||
export function isApiExtension(type: string): type is ApiExtensionType {
|
||||
return (API_EXTENSION_TYPES as readonly string[]).includes(type);
|
||||
}
|
||||
|
||||
export function isExtensionPackage(type: string): type is ExtensionPackageType {
|
||||
return (EXTENSION_PACKAGE_TYPES as readonly string[]).includes(type);
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
import path from 'path';
|
||||
import fse from 'fs-extra';
|
||||
import { pluralize } from './pluralize';
|
||||
import { EXTENSION_TYPES } from '../constants';
|
||||
import { pluralize } from '../pluralize';
|
||||
import { EXTENSION_TYPES } from '../../constants';
|
||||
|
||||
export async function ensureExtensionDirs(extensionsPath: string): Promise<void> {
|
||||
for (const extensionType of EXTENSION_TYPES) {
|
||||
const dirPath = path.resolve(extensionsPath, pluralize(extensionType));
|
||||
await fse.ensureDir(dirPath);
|
||||
try {
|
||||
await fse.ensureDir(dirPath);
|
||||
} catch {
|
||||
throw new Error(`Extension folder "${dirPath}" couldn't be opened`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from 'path/posix';
|
||||
import { AppExtensionType, Extension } from '../types';
|
||||
import path from 'path';
|
||||
import { AppExtensionType, Extension } from '../../types';
|
||||
|
||||
export function generateExtensionsEntry(type: AppExtensionType, extensions: Extension[]): string {
|
||||
const filteredExtensions = extensions.filter((extension) => extension.type === type);
|
||||
@@ -7,7 +7,10 @@ export function generateExtensionsEntry(type: AppExtensionType, extensions: Exte
|
||||
return `${filteredExtensions
|
||||
.map(
|
||||
(extension, i) =>
|
||||
`import e${i} from './${path.relative('.', path.resolve(extension.path, extension.entrypoint || ''))}';\n`
|
||||
`import e${i} from './${path
|
||||
.relative('.', path.resolve(extension.path, extension.entrypoint || ''))
|
||||
.split(path.sep)
|
||||
.join(path.posix.sep)}';\n`
|
||||
)
|
||||
.join('')}export default [${filteredExtensions.map((_, i) => `e${i}`).join(',')}];`;
|
||||
}
|
||||
@@ -1,14 +1,22 @@
|
||||
import path from 'path';
|
||||
import fse from 'fs-extra';
|
||||
import { Extension } from '../types';
|
||||
import { Extension, ExtensionManifestRaw } from '../../types';
|
||||
import { resolvePackage } from './resolve-package';
|
||||
import { listFolders } from './list-folders';
|
||||
import { EXTENSION_NAME_REGEX, EXTENSION_TYPES } from '../constants';
|
||||
import { pluralize } from './pluralize';
|
||||
import { EXTENSION_NAME_REGEX, EXTENSION_PKG_KEY, EXTENSION_TYPES } from '../../constants';
|
||||
import { pluralize } from '../pluralize';
|
||||
import { validateExtensionManifest } from '../validate-extension-manifest';
|
||||
|
||||
export async function getPackageExtensions(root: string): Promise<Extension[]> {
|
||||
const pkg = await fse.readJSON(path.resolve(path.join(root, 'package.json')));
|
||||
const extensionNames = Object.keys(pkg.dependencies).filter((dep) => EXTENSION_NAME_REGEX.test(dep));
|
||||
let pkg: { dependencies?: Record<string, string> };
|
||||
|
||||
try {
|
||||
pkg = await fse.readJSON(path.resolve(root, 'package.json'));
|
||||
} catch {
|
||||
throw new Error('Current folder does not contain a package.json file');
|
||||
}
|
||||
|
||||
const extensionNames = Object.keys(pkg.dependencies ?? {}).filter((dep) => EXTENSION_NAME_REGEX.test(dep));
|
||||
|
||||
return listExtensionsChildren(extensionNames);
|
||||
|
||||
@@ -17,19 +25,23 @@ export async function getPackageExtensions(root: string): Promise<Extension[]> {
|
||||
|
||||
for (const extensionName of extensionNames) {
|
||||
const extensionPath = resolvePackage(extensionName, root);
|
||||
const extensionPkg = await fse.readJSON(path.join(extensionPath, 'package.json'));
|
||||
const extensionManifest: ExtensionManifestRaw = await fse.readJSON(path.join(extensionPath, 'package.json'));
|
||||
|
||||
if (extensionPkg['directus:extension'].type === 'pack') {
|
||||
const extensionChildren = Object.keys(extensionPkg.dependencies).filter((dep) =>
|
||||
if (!validateExtensionManifest(extensionManifest)) {
|
||||
throw new Error(`The extension manifest of "${extensionName}" is not valid.`);
|
||||
}
|
||||
|
||||
if (extensionManifest[EXTENSION_PKG_KEY].type === 'pack') {
|
||||
const extensionChildren = Object.keys(extensionManifest.dependencies ?? {}).filter((dep) =>
|
||||
EXTENSION_NAME_REGEX.test(dep)
|
||||
);
|
||||
|
||||
const extension: Extension = {
|
||||
path: extensionPath,
|
||||
name: extensionName,
|
||||
version: extensionPkg.version,
|
||||
type: extensionPkg['directus:extension'].type,
|
||||
host: extensionPkg['directus:extension'].host,
|
||||
version: extensionManifest.version,
|
||||
type: extensionManifest[EXTENSION_PKG_KEY].type,
|
||||
host: extensionManifest[EXTENSION_PKG_KEY].host,
|
||||
children: extensionChildren,
|
||||
local: false,
|
||||
root: root === undefined,
|
||||
@@ -41,10 +53,10 @@ export async function getPackageExtensions(root: string): Promise<Extension[]> {
|
||||
extensions.push({
|
||||
path: extensionPath,
|
||||
name: extensionName,
|
||||
version: extensionPkg.version,
|
||||
type: extensionPkg['directus:extension'].type,
|
||||
entrypoint: extensionPkg['directus:extension'].path,
|
||||
host: extensionPkg['directus:extension'].host,
|
||||
version: extensionManifest.version,
|
||||
type: extensionManifest[EXTENSION_PKG_KEY].type,
|
||||
entrypoint: extensionManifest[EXTENSION_PKG_KEY].path,
|
||||
host: extensionManifest[EXTENSION_PKG_KEY].host,
|
||||
local: false,
|
||||
root: root === undefined,
|
||||
});
|
||||
@@ -60,7 +72,7 @@ export async function getLocalExtensions(root: string): Promise<Extension[]> {
|
||||
|
||||
for (const extensionType of EXTENSION_TYPES) {
|
||||
const typeDir = pluralize(extensionType);
|
||||
const typePath = path.resolve(path.join(root, typeDir));
|
||||
const typePath = path.resolve(root, typeDir);
|
||||
|
||||
try {
|
||||
const extensionNames = await listFolders(typePath);
|
||||
@@ -77,9 +89,8 @@ export async function getLocalExtensions(root: string): Promise<Extension[]> {
|
||||
root: true,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') throw new Error(`Extension folder "${typePath}" couldn't be opened`);
|
||||
throw err;
|
||||
} catch {
|
||||
throw new Error(`Extension folder "${typePath}" couldn't be opened`);
|
||||
}
|
||||
}
|
||||
|
||||
5
packages/shared/src/utils/node/index.ts
Normal file
5
packages/shared/src/utils/node/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export * from './ensure-extension-dirs';
|
||||
export * from './generate-extensions-entry';
|
||||
export * from './get-extensions';
|
||||
export * from './list-folders';
|
||||
export * from './resolve-package';
|
||||
33
packages/shared/src/utils/parse-filter.ts
Normal file
33
packages/shared/src/utils/parse-filter.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { REGEX_BETWEEN_PARENS } from '../constants';
|
||||
import { Accountability, Filter } from '../types';
|
||||
import { toArray } from './to-array';
|
||||
import { adjustDate } from './adjust-date';
|
||||
import { deepMap } from './deep-map';
|
||||
|
||||
export function parseFilter(filter: Filter, accountability: Accountability | null): any {
|
||||
return deepMap(filter, (val, key) => {
|
||||
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 (val && typeof val === 'string' && val.startsWith('$NOW')) {
|
||||
if (val.includes('(') && val.includes(')')) {
|
||||
const adjustment = val.match(REGEX_BETWEEN_PARENS)?.[1];
|
||||
if (!adjustment) return new Date();
|
||||
return adjustDate(new Date(), adjustment);
|
||||
}
|
||||
|
||||
return new Date();
|
||||
}
|
||||
|
||||
if (val === '$CURRENT_USER') return accountability?.user || null;
|
||||
if (val === '$CURRENT_ROLE') return accountability?.role || null;
|
||||
|
||||
return val;
|
||||
});
|
||||
}
|
||||
7
packages/shared/src/utils/to-array.ts
Normal file
7
packages/shared/src/utils/to-array.ts
Normal file
@@ -0,0 +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];
|
||||
}
|
||||
33
packages/shared/src/utils/validate-extension-manifest.ts
Normal file
33
packages/shared/src/utils/validate-extension-manifest.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { EXTENSION_PKG_KEY } from '../constants';
|
||||
import { ExtensionManifest, ExtensionManifestRaw } from '../types';
|
||||
import { isExtensionPackage } from './is-extension';
|
||||
|
||||
export function validateExtensionManifest(
|
||||
extensionManifest: ExtensionManifestRaw
|
||||
): extensionManifest is ExtensionManifest {
|
||||
if (extensionManifest.name === undefined || extensionManifest.version === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const extensionOptions = extensionManifest[EXTENSION_PKG_KEY];
|
||||
|
||||
if (extensionOptions === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
extensionOptions.type === undefined ||
|
||||
extensionOptions.path === undefined ||
|
||||
extensionOptions.source === undefined ||
|
||||
extensionOptions.host === undefined ||
|
||||
extensionOptions.hidden === undefined
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isExtensionPackage(extensionOptions.type)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
58
packages/shared/src/utils/validate-payload.ts
Normal file
58
packages/shared/src/utils/validate-payload.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { FieldFilter, Filter } from '../types/filter';
|
||||
import { flatten } from 'lodash';
|
||||
import { generateJoi, JoiOptions } from './generate-joi';
|
||||
import Joi from 'joi';
|
||||
|
||||
/**
|
||||
* Validate the payload against the given filter rules
|
||||
*
|
||||
* @param {Filter} filter - The filter rules to check against
|
||||
* @param {Record<string, any>} payload - The payload to validate
|
||||
* @param {JoiOptions} [options] - Optional options to pass to Joi
|
||||
* @returns Array of errors
|
||||
*/
|
||||
export function validatePayload(
|
||||
filter: Filter,
|
||||
payload: Record<string, any>,
|
||||
options?: JoiOptions
|
||||
): Joi.ValidationError[] {
|
||||
const errors: Joi.ValidationError[] = [];
|
||||
|
||||
/**
|
||||
* Note there can only be a single _and / _or per level
|
||||
*/
|
||||
|
||||
if (Object.keys(filter)[0] === '_and') {
|
||||
const subValidation = Object.values(filter)[0] as FieldFilter[];
|
||||
|
||||
const nestedErrors = flatten<Joi.ValidationError>(
|
||||
subValidation.map((subObj: Record<string, any>) => {
|
||||
return validatePayload(subObj, payload, options);
|
||||
})
|
||||
).filter((err?: Joi.ValidationError) => err);
|
||||
|
||||
errors.push(...nestedErrors);
|
||||
} else if (Object.keys(filter)[0] === '_or') {
|
||||
const subValidation = Object.values(filter)[0] as FieldFilter[];
|
||||
|
||||
const nestedErrors = flatten<Joi.ValidationError>(
|
||||
subValidation.map((subObj: Record<string, any>) => validatePayload(subObj, payload, options))
|
||||
);
|
||||
|
||||
const allErrored = subValidation.length === nestedErrors.length;
|
||||
|
||||
if (allErrored) {
|
||||
errors.push(...nestedErrors);
|
||||
}
|
||||
} else {
|
||||
const schema = generateJoi(filter, options);
|
||||
|
||||
const { error } = schema.validate(payload, { abortEarly: false });
|
||||
|
||||
if (error) {
|
||||
errors.push(error);
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
Reference in New Issue
Block a user