mirror of https://github.com/Squidex/squidex.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
371 lines
9.4 KiB
371 lines
9.4 KiB
/*
|
|
* Squidex Headless CMS
|
|
*
|
|
* @license
|
|
* Copyright (c) Squidex UG (haftungsbeschränkt). All rights reserved.
|
|
*/
|
|
|
|
// tslint:disable: readonly-array
|
|
|
|
import { QueryParams, RouteSynchronizer, Types } from '@app/framework';
|
|
import { StatusInfo } from './../services/contents.service';
|
|
import { LanguageDto } from './../services/languages.service';
|
|
import { MetaFields, SchemaDetailsDto } from './../services/schemas.service';
|
|
|
|
export type QueryValueType =
|
|
'boolean' |
|
|
'date' |
|
|
'datetime' |
|
|
'number' |
|
|
'reference' |
|
|
'status' |
|
|
'string' |
|
|
'tags' |
|
|
'user';
|
|
|
|
export interface FilterOperator {
|
|
// The optional display value.
|
|
name: string;
|
|
|
|
// The operator value.
|
|
value: string;
|
|
|
|
// True, when the operator does not require an value.
|
|
noValue?: boolean;
|
|
}
|
|
|
|
export interface QueryFieldModel {
|
|
// The value type.
|
|
type: QueryValueType;
|
|
|
|
// The allowed operator.
|
|
operators: ReadonlyArray<FilterOperator>;
|
|
|
|
// Extra values.
|
|
extra?: any;
|
|
|
|
// The optional display name for the field.
|
|
displayName?: string;
|
|
|
|
// The optional description for the field.
|
|
description?: string;
|
|
}
|
|
|
|
type QueryModelFields = { [name: string]: QueryFieldModel };
|
|
|
|
export interface QueryModel {
|
|
// All available fields.
|
|
fields: QueryModelFields;
|
|
}
|
|
|
|
export type FilterNode = FilterComparison | FilterLogical;
|
|
|
|
export interface FilterComparison {
|
|
// The full path to the property.
|
|
path: string;
|
|
|
|
// The operator.
|
|
op: string;
|
|
|
|
// The value.
|
|
value: any;
|
|
}
|
|
|
|
export interface FilterLogical {
|
|
// The child filters if the logical filter is a conjunction (AND).
|
|
and?: FilterNode[];
|
|
|
|
// The child filters if the logical filter is a conjunction (AND).
|
|
or?: FilterNode[];
|
|
}
|
|
|
|
export interface QuerySorting {
|
|
// The full path to the property.
|
|
path: string;
|
|
|
|
// The sort order.
|
|
order: SortMode;
|
|
}
|
|
|
|
export const SORT_MODES: ReadonlyArray<SortMode> = [
|
|
'ascending',
|
|
'descending'
|
|
];
|
|
|
|
export type SortMode = 'ascending' | 'descending';
|
|
|
|
export interface Query {
|
|
// The optional filter.
|
|
filter?: FilterLogical;
|
|
|
|
// The full text search.
|
|
fullText?: string;
|
|
|
|
// The sorting.
|
|
sort?: QuerySorting[];
|
|
|
|
// The number of items to take.
|
|
take?: number;
|
|
|
|
// The number of items to skip.
|
|
skip?: number;
|
|
}
|
|
|
|
const DEFAULT_QUERY = {
|
|
filter: {
|
|
and: []
|
|
},
|
|
sort: []
|
|
};
|
|
|
|
export class QueryFullTextSynchronizer implements RouteSynchronizer {
|
|
public static readonly INSTANCE = new QueryFullTextSynchronizer();
|
|
|
|
public readonly keys = ['query'];
|
|
|
|
public parseFromRoute(params: QueryParams) {
|
|
const query = params['query'];
|
|
|
|
if (Types.isString(query)) {
|
|
return { query: { fullText: query } };
|
|
}
|
|
}
|
|
|
|
public parseFromState(state: any) {
|
|
const value = state['query'];
|
|
|
|
if (Types.isObject(value) && Types.isString(value.fullText) && value.fullText.length > 0) {
|
|
return { query: value.fullText };
|
|
}
|
|
}
|
|
}
|
|
|
|
export class QuerySynchronizer implements RouteSynchronizer {
|
|
public static readonly INSTANCE = new QuerySynchronizer();
|
|
|
|
public readonly keys = ['query'];
|
|
|
|
public parseFromRoute(params: QueryParams) {
|
|
const query = params['query'];
|
|
|
|
if (Types.isString(query)) {
|
|
return { query: deserializeQuery(query) };
|
|
}
|
|
}
|
|
|
|
public parseFromState(state: any) {
|
|
const value = state['query'];
|
|
|
|
if (Types.isObject(value)) {
|
|
return { query: serializeQuery(value) };
|
|
}
|
|
}
|
|
}
|
|
|
|
export function sanitize(query?: Query) {
|
|
if (!query) {
|
|
return DEFAULT_QUERY;
|
|
}
|
|
|
|
if (!query.sort) {
|
|
query.sort = [];
|
|
}
|
|
|
|
if (!query.filter) {
|
|
query.filter = { and: [] };
|
|
}
|
|
|
|
return query;
|
|
}
|
|
|
|
export function equalsQuery(lhs?: Query, rhs?: Query) {
|
|
return Types.equals(sanitize(lhs), sanitize(rhs));
|
|
}
|
|
|
|
export function serializeQuery(query?: Query) {
|
|
return JSON.stringify(sanitize(query));
|
|
}
|
|
|
|
export function encodeQuery(query?: Query) {
|
|
return encodeURIComponent(serializeQuery(query));
|
|
}
|
|
|
|
export function deserializeQuery(raw?: string): Query | undefined {
|
|
let query: Query | undefined = undefined;
|
|
|
|
try {
|
|
if (Types.isString(raw)) {
|
|
if (raw.indexOf('{') === 0) {
|
|
query = JSON.parse(raw);
|
|
} else {
|
|
query = { fullText: raw };
|
|
}
|
|
}
|
|
} catch (ex) {
|
|
query = undefined;
|
|
}
|
|
|
|
return query;
|
|
}
|
|
|
|
export function hasFilter(query?: Query) {
|
|
return !!query && !Types.isEmpty(query.filter);
|
|
}
|
|
|
|
const EqualOperators: ReadonlyArray<FilterOperator> = [
|
|
{ name: 'i18n:common.queryOperators.eq', value: 'eq' },
|
|
{ name: 'i18n:common.queryOperators.ne', value: 'ne' }
|
|
];
|
|
|
|
const CompareOperator: ReadonlyArray<FilterOperator> = [
|
|
{ name: 'i18n:common.queryOperators.lt', value: 'lt' },
|
|
{ name: 'i18n:common.queryOperators.le', value: 'le' },
|
|
{ name: 'i18n:common.queryOperators.gt', value: 'gt' },
|
|
{ name: 'i18n:common.queryOperators.ge', value: 'ge' }
|
|
];
|
|
|
|
const StringOperators: ReadonlyArray<FilterOperator> = [
|
|
{ name: 'i18n:common.queryOperators.startsWith', value: 'startsWith' },
|
|
{ name: 'i18n:common.queryOperators.endsWith', value: 'endsWith' },
|
|
{ name: 'i18n:common.queryOperators.contains', value: 'contains' }
|
|
];
|
|
|
|
const ArrayOperators: ReadonlyArray<FilterOperator> = [
|
|
{ name: 'i18n:common.queryOperators.empty', value: 'empty', noValue: true }
|
|
];
|
|
|
|
const TypeBoolean: QueryFieldModel = {
|
|
type: 'boolean',
|
|
operators: EqualOperators
|
|
};
|
|
|
|
const TypeDateTime: QueryFieldModel = {
|
|
type: 'datetime',
|
|
operators: [...EqualOperators, ...CompareOperator]
|
|
};
|
|
|
|
const TypeNumber: QueryFieldModel = {
|
|
type: 'number',
|
|
operators: [...EqualOperators, ...CompareOperator]
|
|
};
|
|
|
|
const TypeReference: QueryFieldModel = {
|
|
type: 'reference',
|
|
operators: [...EqualOperators, ...ArrayOperators]
|
|
};
|
|
|
|
const TypeStatus: QueryFieldModel = {
|
|
type: 'status',
|
|
operators: EqualOperators
|
|
};
|
|
|
|
const TypeUser: QueryFieldModel = {
|
|
type: 'user',
|
|
operators: EqualOperators
|
|
};
|
|
|
|
const TypeString: QueryFieldModel = {
|
|
type: 'string',
|
|
operators: [...EqualOperators, ...CompareOperator, ...StringOperators, ...ArrayOperators]
|
|
};
|
|
|
|
const TypeTags: QueryFieldModel = {
|
|
type: 'string',
|
|
operators: EqualOperators
|
|
};
|
|
|
|
const DEFAULT_FIELDS: QueryModelFields = {
|
|
created: {
|
|
...TypeDateTime,
|
|
displayName: MetaFields.created,
|
|
description: 'i18n:contents.createFieldDescription'
|
|
},
|
|
createdBy: {
|
|
...TypeUser,
|
|
displayName: 'meta.createdBy',
|
|
description: 'i18n:contents.createdByFieldDescription'
|
|
},
|
|
lastModified: {
|
|
...TypeDateTime,
|
|
displayName: MetaFields.lastModified,
|
|
description: 'i18n:contents.lastModifiedFieldDescription'
|
|
},
|
|
lastModifiedBy: {
|
|
...TypeUser,
|
|
displayName: 'meta.lastModifiedBy',
|
|
description: 'i18n:contents.lastModifiedByFieldDescription'
|
|
},
|
|
version: {
|
|
...TypeNumber,
|
|
displayName: MetaFields.version,
|
|
description: 'i18n:contents.versionFieldDescription'
|
|
}
|
|
};
|
|
|
|
export function queryModelFromSchema(schema: SchemaDetailsDto, languages: ReadonlyArray<LanguageDto>, statuses: ReadonlyArray<StatusInfo> | undefined) {
|
|
const languagesCodes = languages.map(x => x.iso2Code);
|
|
|
|
const model: QueryModel = {
|
|
fields: { ...DEFAULT_FIELDS }
|
|
};
|
|
|
|
if (statuses) {
|
|
model.fields['status'] = {
|
|
...TypeStatus,
|
|
displayName: MetaFields.status,
|
|
description: 'i18n:contents.statusFieldDescription',
|
|
extra: statuses
|
|
};
|
|
|
|
model.fields['newStatus'] = {
|
|
...TypeStatus,
|
|
displayName: MetaFields.statusNext,
|
|
description: 'i18n:contents.newStatusFieldDescription',
|
|
extra: statuses
|
|
};
|
|
}
|
|
|
|
for (const field of schema.fields) {
|
|
let type: QueryFieldModel | null = null;
|
|
|
|
if (field.properties.fieldType === 'Tags') {
|
|
type = TypeTags;
|
|
} else if (field.properties.fieldType === 'Boolean') {
|
|
type = TypeBoolean;
|
|
} else if (field.properties.fieldType === 'Number') {
|
|
type = TypeNumber;
|
|
} else if (field.properties.fieldType === 'String') {
|
|
type = TypeString;
|
|
} else if (field.properties.fieldType === 'DateTime') {
|
|
type = TypeDateTime;
|
|
} else if (field.properties.fieldType === 'References') {
|
|
const extra = field.rawProperties.singleId;
|
|
|
|
type = { ...TypeReference, extra };
|
|
}
|
|
|
|
if (type) {
|
|
if (field.isLocalizable) {
|
|
for (const code of languagesCodes) {
|
|
const infos = {
|
|
displayName: `${field.name} (${code})`,
|
|
description: 'i18n:contents.localizedFieldDescription',
|
|
fieldName: field.displayName
|
|
};
|
|
|
|
model.fields[`data.${field.name}.${code}`] = { ...type, ...infos };
|
|
}
|
|
} else {
|
|
const infos = {
|
|
displayName: field.name,
|
|
description: 'i18n:contents.invariantFieldDescription',
|
|
fieldName: field.displayName
|
|
};
|
|
|
|
model.fields[`data.${field.name}.iv`] = { ...type, ...infos };
|
|
}
|
|
}
|
|
}
|
|
|
|
return model;
|
|
}
|