From febdd0adbdcb639f72706c3a830786b125d4a672 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Tue, 20 Oct 2020 11:02:02 -0400 Subject: [PATCH 01/18] Attempt at resolving joins in nested where queries --- api/src/services/meta.ts | 2 +- api/src/utils/apply-query.ts | 299 ++++++++++++++++++----------------- 2 files changed, 157 insertions(+), 144 deletions(-) diff --git a/api/src/services/meta.ts b/api/src/services/meta.ts index 9764dcdcee..fad8c127b8 100644 --- a/api/src/services/meta.ts +++ b/api/src/services/meta.ts @@ -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; diff --git a/api/src/utils/apply-query.ts b/api/src/utils/apply-query.ts index d0ead782c3..77bd8adece 100644 --- a/api/src/utils/apply-query.ts +++ b/api/src/utils/apply-query.ts @@ -46,116 +46,179 @@ 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) => { - 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; + parseLevel(rootQuery, rootFilter, collection); + + function parseLevel(dbQuery: QueryBuilder, filter: Filter, collection: string) { + for (const [key, value] of Object.entries(filter)) { + if (key === '_or') { + dbQuery.orWhere((subQuery) => { + value.forEach((subFilter: Record) => { + parseLevel(subQuery, subFilter, collection); + }); + }); + + continue; + } + + if (key === '_and') { + dbQuery.andWhere((subQuery) => { + value.forEach((subFilter: Record) => { + parseLevel(subQuery, subFilter, collection); + }); + }); + + continue; + } + + const filterPath = getFilterPath(key, value); + const { operator: filterOperator, value: filterValue } = getOperation(key, value); + + const column = + filterPath.length > 1 + ? applyJoins(filterPath, collection) + : `${collection}.${filterPath[0]}`; + + applyFilterToQuery(column, filterOperator, filterValue); } - if (key === '_and') { - value.forEach((subFilter: Record) => { - 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); - - const column = - filterPath.length > 1 - ? await applyJoins(dbQuery, filterPath, collection) - : `${collection}.${filterPath[0]}`; - - applyFilterToQuery(column, filterOperator, filterValue); } - function applyFilterToQuery(key: string, operator: string, compareValue: any) { - if (operator === '_eq') { - dbQuery.where({ [key]: compareValue }); - } + function applyJoins(path: string[], collection: string) { + path = clone(path); - if (operator === '_neq') { - dbQuery.whereNot({ [key]: compareValue }); - } + let keyName = ''; - if (operator === '_contains') { - dbQuery.where(key, 'like', `%${compareValue}%`); - } + addJoins(path); - if (operator === '_ncontains') { - dbQuery.where(key, 'like', `%${compareValue}%`); - } + return keyName; - 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, '=', ''); + function addJoins(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 === '_nempty') { - dbQuery.andWhere((query) => { - query.whereNotNull(key); - query.orWhere(key, '!=', ''); - }); - } + if (!relation) return; - if (operator === '_between') { - let value = compareValue; - if (typeof value === 'string') value = value.split(','); + const isM2O = + relation.many_collection === parentCollection && + relation.many_field === pathParts[0]; - dbQuery.whereBetween(key, value); - } + if (isM2O) { + rootQuery.leftJoin( + relation.one_collection!, + `${parentCollection}.${relation.many_field}`, + `${relation.one_collection}.${relation.one_primary}` + ); + } else { + rootQuery.leftJoin( + relation.many_collection, + `${parentCollection}.${relation.one_primary}`, + `${relation.many_collection}.${relation.many_field}` + ); + } - if (operator === '_nbetween') { - let value = compareValue; - if (typeof value === 'string') value = value.split(','); + pathParts.shift(); - dbQuery.whereNotBetween(key, value); + const parent = isM2O ? relation.one_collection! : relation.many_collection; + + if (pathParts.length === 1) { + keyName = `${parent}.${pathParts[0]}`; + } + + if (pathParts.length) { + addJoins(pathParts, parent); + } } } } @@ -175,53 +238,3 @@ function getOperation(key: string, value: Record): { operator: stri return { operator: key as string, 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); - } - } -} From 6d262539ca212a11cc1f674cbaa13d650e5d31a9 Mon Sep 17 00:00:00 2001 From: Aleksandar Stanisic Date: Sat, 24 Oct 2020 21:34:00 +0200 Subject: [PATCH 02/18] Add generics for ItemsService --- api/src/services/items.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/api/src/services/items.ts b/api/src/services/items.ts index cb28ca0607..344692bb2e 100644 --- a/api/src/services/items.ts +++ b/api/src/services/items.ts @@ -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 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, { @@ -210,7 +210,7 @@ export class ItemsService implements AbstractService { } const records = await runAST(ast, { knex: this.knex }); - return records; + return records as Item | Item[] | null; } readByKey( @@ -260,7 +260,7 @@ export class ItemsService implements AbstractService { if (result === null) throw new ForbiddenException(); - return result; + return result as Item | Item[] | null; } update(data: Partial, keys: PrimaryKey[]): Promise; @@ -277,7 +277,7 @@ export class ItemsService implements AbstractService { if (data && key) { const keys = toArray(key); - let payload = clone(data); + let payload: Partial | Partial[] = clone(data); const customProcessed = await emitter.emitAsync( `${this.eventScope}.update.before`, From 0dab1cdf90f1eecbc726730ccc2c7f222cea6d58 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Mon, 26 Oct 2020 15:11:12 +0100 Subject: [PATCH 03/18] Add search input to collections drawer --- .../drawer-collection/drawer-collection.vue | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/src/views/private/components/drawer-collection/drawer-collection.vue b/app/src/views/private/components/drawer-collection/drawer-collection.vue index 2ef3754287..69b04af7e9 100644 --- a/app/src/views/private/components/drawer-collection/drawer-collection.vue +++ b/app/src/views/private/components/drawer-collection/drawer-collection.vue @@ -1,5 +1,13 @@ - - @@ -33,8 +35,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, From 6fac205a2b7abf5bba781d4a99e8425687e5254c Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Mon, 26 Oct 2020 15:13:20 +0100 Subject: [PATCH 04/18] Pass search query to layout --- .../drawer-collection/drawer-collection.vue | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/app/src/views/private/components/drawer-collection/drawer-collection.vue b/app/src/views/private/components/drawer-collection/drawer-collection.vue index 69b04af7e9..fe730f7e62 100644 --- a/app/src/views/private/components/drawer-collection/drawer-collection.vue +++ b/app/src/views/private/components/drawer-collection/drawer-collection.vue @@ -15,6 +15,7 @@ :filters="filters" :layout-query.sync="localQuery" :layout-options.sync="localOptions" + :search-query="searchQuery" @update:selection="onSelect" select-mode class="layout" @@ -69,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 @@ -77,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); From 9f6d5efbddd72a17cec89bcba8b1330de3393502 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Mon, 26 Oct 2020 17:11:14 +0100 Subject: [PATCH 05/18] Fix user interface --- app/src/interfaces/user/user.vue | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src/interfaces/user/user.vue b/app/src/interfaces/user/user.vue index 28c7744fd7..00940ee42a 100644 --- a/app/src/interfaces/user/user.vue +++ b/app/src/interfaces/user/user.vue @@ -61,7 +61,7 @@ @click="setCurrent(item)" > - {{ userName(currentUser) }} + {{ userName(item) }} @@ -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() { From 8ecb8da3abe04b4ed08e2415a97a203c2aaf41ad Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Mon, 26 Oct 2020 17:30:27 +0100 Subject: [PATCH 06/18] Default to _eq for filter --- api/src/utils/apply-query.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/api/src/utils/apply-query.ts b/api/src/utils/apply-query.ts index 77bd8adece..c5d1b0b5ab 100644 --- a/api/src/utils/apply-query.ts +++ b/api/src/utils/apply-query.ts @@ -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) { @@ -226,7 +226,11 @@ export async function applyFilter(rootQuery: QueryBuilder, rootFilter: Filter, c function getFilterPath(key: string, value: Record) { 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])); } @@ -234,7 +238,11 @@ function getFilterPath(key: string, value: Record) { } function getOperation(key: string, value: Record): { 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]); } From c49e1b02796011994864036ed5487c7e16e87519 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Mon, 26 Oct 2020 18:33:38 +0100 Subject: [PATCH 07/18] Extract adding joins to separate function --- api/src/utils/apply-query.ts | 171 ++++++++++++++++++++++++----------- 1 file changed, 118 insertions(+), 53 deletions(-) diff --git a/api/src/utils/apply-query.ts b/api/src/utils/apply-query.ts index c5d1b0b5ab..13f3f6ad93 100644 --- a/api/src/utils/apply-query.ts +++ b/api/src/utils/apply-query.ts @@ -49,14 +49,16 @@ export default async function applyQuery(collection: string, dbQuery: QueryBuild export async function applyFilter(rootQuery: QueryBuilder, rootFilter: Filter, collection: string) { const relations = await database.select('*').from('directus_relations'); - parseLevel(rootQuery, rootFilter, collection); + addWhereClauses(rootQuery, rootFilter, collection); + addJoins(rootQuery, rootFilter, collection); - function parseLevel(dbQuery: QueryBuilder, filter: Filter, collection: string) { + 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) => { - parseLevel(subQuery, subFilter, collection); + addWhereClauses(subQuery, subFilter, collection); }); }); @@ -64,9 +66,10 @@ export async function applyFilter(rootQuery: QueryBuilder, rootFilter: Filter, c } if (key === '_and') { + /** @NOTE these callback functions aren't called until Knex runs the query */ dbQuery.andWhere((subQuery) => { value.forEach((subFilter: Record) => { - parseLevel(subQuery, subFilter, collection); + addWhereClauses(subQuery, subFilter, collection); }); }); @@ -76,12 +79,12 @@ export async function applyFilter(rootQuery: QueryBuilder, rootFilter: Filter, c const filterPath = getFilterPath(key, value); const { operator: filterOperator, value: filterValue } = getOperation(key, value); - const column = - filterPath.length > 1 - ? applyJoins(filterPath, collection) - : `${collection}.${filterPath[0]}`; - - applyFilterToQuery(column, filterOperator, filterValue); + if (filterPath.length > 1) { + const columnName = getWhereColumn(filterPath, collection); + applyFilterToQuery(columnName, filterOperator, filterValue); + } else { + applyFilterToQuery(`${collection}.${filterPath[0]}`, filterOperator, filterValue); + } } function applyFilterToQuery(key: string, operator: string, compareValue: any) { @@ -167,57 +170,119 @@ export async function applyFilter(rootQuery: QueryBuilder, rootFilter: Filter, c dbQuery.whereNotBetween(key, value); } } + + function getWhereColumn(path: string[], collection: string) { + path = clone(path); + + let columnName = ''; + + 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 applyJoins(path: string[], collection: string) { - path = clone(path); + /** + * @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) => { + addJoins(dbQuery, subFilter, collection); + }); - let keyName = ''; - - addJoins(path); - - return keyName; - - function addJoins(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]; - - if (isM2O) { - rootQuery.leftJoin( - relation.one_collection!, - `${parentCollection}.${relation.many_field}`, - `${relation.one_collection}.${relation.one_primary}` - ); - } else { - rootQuery.leftJoin( - relation.many_collection, - `${parentCollection}.${relation.one_primary}`, - `${relation.many_collection}.${relation.many_field}` - ); + continue; } - pathParts.shift(); + if (key === '_and') { + value.forEach((subFilter: Record) => { + addJoins(dbQuery, subFilter, collection); + }); - const parent = isM2O ? relation.one_collection! : relation.many_collection; - - if (pathParts.length === 1) { - keyName = `${parent}.${pathParts[0]}`; + continue; } - if (pathParts.length) { - addJoins(pathParts, parent); + const filterPath = getFilterPath(key, value); + + if (filterPath.length > 1) { + addJoin(filterPath, collection); + } + } + + function addJoin(path: string[], collection: string) { + path = clone(path); + + followRelation(path); + + 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]; + + 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}` + ); + } + + pathParts.shift(); + + const parent = isM2O ? relation.one_collection! : relation.many_collection; + + if (pathParts.length) { + followRelation(pathParts, parent); + } } } } From 498233c09a4434fc88f40360adf074c1de47e652 Mon Sep 17 00:00:00 2001 From: Aleksandar Stanisic Date: Mon, 26 Oct 2020 18:38:33 +0100 Subject: [PATCH 08/18] Add generics per method --- api/src/services/items.ts | 53 +++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/api/src/services/items.ts b/api/src/services/items.ts index 344692bb2e..1791badc47 100644 --- a/api/src/services/items.ts +++ b/api/src/services/items.ts @@ -46,9 +46,11 @@ export class ItemsService implements AbstractService { return this; } - async create(data: Partial[]): Promise; - async create(data: Partial): Promise; - async create(data: Partial | Partial[]): Promise { + async create>(data: T[]): Promise; + async create>(data: T): Promise; + async create>( + data: T | T[] + ): Promise { const primaryKeyField = await this.schemaInspector.primary(this.collection); const columns = await this.schemaInspector.columns(this.collection); @@ -194,7 +196,7 @@ export class ItemsService implements AbstractService { return Array.isArray(data) ? savedPrimaryKeys : savedPrimaryKeys[0]; } - async readByQuery(query: Query): Promise { + async readByQuery(query: Query): Promise { const authorizationService = new AuthorizationService({ accountability: this.accountability, knex: this.knex, @@ -210,20 +212,24 @@ export class ItemsService implements AbstractService { } const records = await runAST(ast, { knex: this.knex }); - return records as Item | Item[] | null; + return records as T | T[] | null; } - readByKey( + readByKey( keys: PrimaryKey[], query?: Query, action?: PermissionsAction - ): Promise; - readByKey(key: PrimaryKey, query?: Query, action?: PermissionsAction): Promise; - async readByKey( + ): Promise; + readByKey( + key: PrimaryKey, + query?: Query, + action?: PermissionsAction + ): Promise; + async readByKey( key: PrimaryKey | PrimaryKey[], query: Query = {}, action: PermissionsAction = 'read' - ): Promise { + ): Promise { query = clone(query); const primaryKeyField = await this.schemaInspector.primary(this.collection); const keys = toArray(key); @@ -260,14 +266,14 @@ export class ItemsService implements AbstractService { if (result === null) throw new ForbiddenException(); - return result as Item | Item[] | null; + return result as T | T[] | null; } - update(data: Partial, keys: PrimaryKey[]): Promise; - update(data: Partial, key: PrimaryKey): Promise; - update(data: Partial[]): Promise; - async update( - data: Partial | Partial[], + update>(data: T, keys: PrimaryKey[]): Promise; + update>(data: T, key: PrimaryKey): Promise; + update>(data: T[]): Promise; + async update>( + data: T | T[], key?: PrimaryKey | PrimaryKey[] ): Promise { const primaryKeyField = await this.schemaInspector.primary(this.collection); @@ -428,7 +434,10 @@ export class ItemsService implements AbstractService { return keys; } - async updateByQuery(data: Partial, query: Query): Promise { + async updateByQuery>( + data: T, + query: Query + ): Promise { const primaryKeyField = await this.schemaInspector.primary(this.collection); const readQuery = cloneDeep(query); readQuery.fields = [primaryKeyField]; @@ -446,9 +455,11 @@ export class ItemsService implements AbstractService { return await this.update(data, keys); } - upsert(data: Partial[]): Promise; - upsert(data: Partial): Promise; - async upsert(data: Partial | Partial[]): Promise { + upsert>(data: T[]): Promise; + upsert>(data: T): Promise; + async upsert>( + data: T | T[] + ): Promise { const primaryKeyField = await this.schemaInspector.primary(this.collection); const payloads = toArray(data); const primaryKeys: PrimaryKey[] = []; @@ -570,7 +581,7 @@ export class ItemsService implements AbstractService { return record; } - async upsertSingleton(data: Partial) { + async upsertSingleton>(data: T) { const primaryKeyField = await this.schemaInspector.primary(this.collection); const record = await this.knex From 58a6aa3f2f308fcaa13352e9f7d9da40b660abc3 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Mon, 26 Oct 2020 18:39:09 +0100 Subject: [PATCH 09/18] Fix permissions filter on role detail Fixes #785 --- .../routes/roles/item/components/permissions-overview.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/modules/settings/routes/roles/item/components/permissions-overview.vue b/app/src/modules/settings/routes/roles/item/components/permissions-overview.vue index c15e055126..b93db0f70d 100644 --- a/app/src/modules/settings/routes/roles/item/components/permissions-overview.vue +++ b/app/src/modules/settings/routes/roles/item/components/permissions-overview.vue @@ -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) { From b4f82efcee9d1ae4e0008551bbd922fff77e248e Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Mon, 26 Oct 2020 18:52:51 +0100 Subject: [PATCH 10/18] Return project branding in /server/info endpoint at all times --- api/src/controllers/server.ts | 6 +- .../seeds/02-rows/02-permissions.yaml | 17 ------ .../{03-presets.yaml => 02-presets.yaml} | 0 .../{04-relations.yaml => 03-relations.yaml} | 0 api/src/services/server.ts | 61 +++++++++++++------ 5 files changed, 46 insertions(+), 38 deletions(-) delete mode 100644 api/src/database/seeds/02-rows/02-permissions.yaml rename api/src/database/seeds/02-rows/{03-presets.yaml => 02-presets.yaml} (100%) rename api/src/database/seeds/02-rows/{04-relations.yaml => 03-relations.yaml} (100%) diff --git a/api/src/controllers/server.ts b/api/src/controllers/server.ts index d6fb0f688f..0f20b5fd7c 100644 --- a/api/src/controllers/server.ts +++ b/api/src/controllers/server.ts @@ -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 ); diff --git a/api/src/database/seeds/02-rows/02-permissions.yaml b/api/src/database/seeds/02-rows/02-permissions.yaml deleted file mode 100644 index 4b32d9ede6..0000000000 --- a/api/src/database/seeds/02-rows/02-permissions.yaml +++ /dev/null @@ -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' diff --git a/api/src/database/seeds/02-rows/03-presets.yaml b/api/src/database/seeds/02-rows/02-presets.yaml similarity index 100% rename from api/src/database/seeds/02-rows/03-presets.yaml rename to api/src/database/seeds/02-rows/02-presets.yaml diff --git a/api/src/database/seeds/02-rows/04-relations.yaml b/api/src/database/seeds/02-rows/03-relations.yaml similarity index 100% rename from api/src/database/seeds/02-rows/04-relations.yaml rename to api/src/database/seeds/02-rows/03-relations.yaml diff --git a/api/src/services/server.ts b/api/src/services/server.ts index 8c24228ae9..e734934c26 100644 --- a/api/src/services/server.ts +++ b/api/src/services/server.ts @@ -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 = {}; - 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 + ? { + name: projectInfo.project_name, + logo: projectInfo.project_logo, + color: projectInfo.project_color, + foreground: projectInfo.public_foreground, + background: projectInfo.public_background, + note: projectInfo.public_note, + customCSS: 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; } } From 9130c1b0b8188f321047bd4564a71ad17db2128c Mon Sep 17 00:00:00 2001 From: Aleksandar Stanisic Date: Mon, 26 Oct 2020 18:52:52 +0100 Subject: [PATCH 11/18] Add types to readSingleton --- api/src/services/items.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/api/src/services/items.ts b/api/src/services/items.ts index 1791badc47..c7cf21d9c2 100644 --- a/api/src/services/items.ts +++ b/api/src/services/items.ts @@ -561,11 +561,11 @@ export class ItemsService implements AbstractService { return await this.delete(keys); } - async readSingleton(query: Query) { + async readSingleton(query: Query): Promise { query = clone(query); query.single = true; - const record = (await this.readByQuery(query)) as Item; + const record = (await this.readByQuery(query)) as T; if (!record) { const columns = await this.schemaInspector.columnInfo(this.collection); @@ -575,7 +575,7 @@ export class ItemsService implements AbstractService { defaults[column.name] = getDefaultValue(column); } - return defaults; + return defaults as T; } return record; From 8a01d9b2deb69f29a2d78157f69cfe601223f7bf Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Mon, 26 Oct 2020 18:53:08 +0100 Subject: [PATCH 12/18] Add server info store --- app/src/stores/server.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 app/src/stores/server.ts diff --git a/app/src/stores/server.ts b/app/src/stores/server.ts new file mode 100644 index 0000000000..c0b3fed0ec --- /dev/null +++ b/app/src/stores/server.ts @@ -0,0 +1,18 @@ +import { createStore } from 'pinia'; +import api from '@/api'; + +export const useServerStore = createStore({ + id: 'serverStore', + state: () => ({ + info: null, + }), + actions: { + async hydrate() { + const response = await api.get(`/server/info`); + this.state.info = response.data.data; + }, + dehydrate() { + this.reset(); + }, + }, +}); From 9559497785627bc9e1b84f8448773fed2004c2b6 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Mon, 26 Oct 2020 19:08:07 +0100 Subject: [PATCH 13/18] Fetch public info from /server/info endpoint Fixes #784 --- api/src/services/server.ts | 14 +++++----- app/src/app.vue | 18 ++++++------- app/src/hydrate.ts | 2 ++ app/src/router.ts | 8 +++--- app/src/routes/login/login.vue | 6 ++--- app/src/stores/index.ts | 1 + app/src/stores/server.ts | 27 ++++++++++++++++++- .../components/project-info/project-info.vue | 6 ++--- app/src/views/public/public-view.vue | 24 ++++++++--------- 9 files changed, 66 insertions(+), 40 deletions(-) diff --git a/api/src/services/server.ts b/api/src/services/server.ts index e734934c26..f243647f25 100644 --- a/api/src/services/server.ts +++ b/api/src/services/server.ts @@ -33,13 +33,13 @@ export class ServerService { info.project = projectInfo ? { - name: projectInfo.project_name, - logo: projectInfo.project_logo, - color: projectInfo.project_color, - foreground: projectInfo.public_foreground, - background: projectInfo.public_background, - note: projectInfo.public_note, - customCSS: projectInfo.custom_css, + 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; diff --git a/app/src/app.vue b/app/src/app.vue index 3d63279fbb..40f01bd710 100644 --- a/app/src/app.vue +++ b/app/src/app.vue @@ -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); diff --git a/app/src/hydrate.ts b/app/src/hydrate.ts index 603ed6a16a..d686c18e4a 100644 --- a/app/src/hydrate.ts +++ b/app/src/hydrate.ts @@ -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, diff --git a/app/src/router.ts b/app/src/router.ts index 0e7c23b424..4074fd9125 100644 --- a/app/src/router.ts +++ b/app/src/router.ts @@ -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) { diff --git a/app/src/routes/login/login.vue b/app/src/routes/login/login.vue index d96a928ba0..ecf55788a9 100644 --- a/app/src/routes/login/login.vue +++ b/app/src/routes/login/login.vue @@ -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 }; }, }); diff --git a/app/src/stores/index.ts b/app/src/stores/index.ts index 9cdad9755c..d7fbf37892 100644 --- a/app/src/stores/index.ts +++ b/app/src/stores/index.ts @@ -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'; diff --git a/app/src/stores/server.ts b/app/src/stores/server.ts index c0b3fed0ec..90c2d4e976 100644 --- a/app/src/stores/server.ts +++ b/app/src/stores/server.ts @@ -1,10 +1,35 @@ 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, + info: null as null | Info, }), actions: { async hydrate() { diff --git a/app/src/views/private/components/project-info/project-info.vue b/app/src/views/private/components/project-info/project-info.vue index 028cdb862f..952d79722f 100644 --- a/app/src/views/private/components/project-info/project-info.vue +++ b/app/src/views/private/components/project-info/project-info.vue @@ -8,16 +8,16 @@ From 005200ef0d2aff6102489adc8ee847d2fd625913 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Mon, 26 Oct 2020 19:14:08 +0100 Subject: [PATCH 14/18] Add subject to invite/pw reset email Fixes #783 --- api/src/mail/index.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/api/src/mail/index.ts b/api/src/mail/index.ts index 565a1db276..70944e3579 100644 --- a/api/src/mail/index.ts +++ b/api/src/mail/index.ts @@ -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`, + }); } From 91bfcb875f1ab58aa5855bcad1104c3540becf5b Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Mon, 26 Oct 2020 19:38:36 +0100 Subject: [PATCH 15/18] Don't auto convert `,` to arrays in env Fixes #782 --- api/src/controllers/files.ts | 5 +++-- api/src/env.ts | 5 ----- api/src/utils/to-array.ts | 7 ++++++- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/api/src/controllers/files.ts b/api/src/controllers/files.ts index d33beb55e5..15e154fd66 100644 --- a/api/src/controllers/files.ts +++ b/api/src/controllers/files.ts @@ -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 = {}; 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 || {}), diff --git a/api/src/env.ts b/api/src/env.ts index 520e6c2e7e..d1cf1d1520 100644 --- a/api/src/env.ts +++ b/api/src/env.ts @@ -63,11 +63,6 @@ function processValues(env: Record) { 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; diff --git a/api/src/utils/to-array.ts b/api/src/utils/to-array.ts index 77cf11fae2..94ad5b83e6 100644 --- a/api/src/utils/to-array.ts +++ b/api/src/utils/to-array.ts @@ -1,3 +1,8 @@ -export function toArray(val: T | T[]): T[] { +export function toArray(val: string): string[]; +export function toArray(val: any | any[]): any[] { + if (typeof val === 'string') { + return val.split(','); + } + return Array.isArray(val) ? val : [val]; } From 8401c4cdbc42d1cc8bb6fd889949dcd1c2c6211b Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Mon, 26 Oct 2020 20:23:20 +0100 Subject: [PATCH 16/18] Fix typing of toArray helper --- api/src/utils/to-array.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/api/src/utils/to-array.ts b/api/src/utils/to-array.ts index 94ad5b83e6..ae20f6862b 100644 --- a/api/src/utils/to-array.ts +++ b/api/src/utils/to-array.ts @@ -1,7 +1,6 @@ -export function toArray(val: string): string[]; -export function toArray(val: any | any[]): any[] { +export function toArray(val: T | T[]): T[] { if (typeof val === 'string') { - return val.split(','); + return (val.split(',') as unknown) as T[]; } return Array.isArray(val) ? val : [val]; From 3f8e4378dcbcd37c3e368074ba9b7270c4d12a60 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Mon, 26 Oct 2020 20:38:11 +0100 Subject: [PATCH 17/18] Prevent racing conditions in preset saving Fixes #638 --- app/src/stores/presets.ts | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/app/src/stores/presets.ts b/app/src/stores/presets.ts index 7b5233e1af..ccd17fe5e3 100644 --- a/app/src/stores/presets.ts +++ b/app/src/stores/presets.ts @@ -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 = { bookmark: null, @@ -14,6 +15,8 @@ const defaultPreset: Omit = { layout_options: null, }; +let currentUpdate: Record = {}; + 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) { + 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; }, From c30a5fcb0cc387bf3987fc5371327b9bd15c590c Mon Sep 17 00:00:00 2001 From: Aleksandar Stanisic Date: Mon, 26 Oct 2020 21:32:04 +0100 Subject: [PATCH 18/18] Simplify generics --- api/src/services/items.ts | 59 +++++++++++++++++---------------------- 1 file changed, 26 insertions(+), 33 deletions(-) diff --git a/api/src/services/items.ts b/api/src/services/items.ts index c7cf21d9c2..0f94f8d1e6 100644 --- a/api/src/services/items.ts +++ b/api/src/services/items.ts @@ -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 implements AbstractService { collection: string; knex: Knex; accountability: Accountability | null; @@ -46,11 +46,9 @@ export class ItemsService implements AbstractService { return this; } - async create>(data: T[]): Promise; - async create>(data: T): Promise; - async create>( - data: T | T[] - ): Promise { + async create(data: Partial[]): Promise; + async create(data: Partial): Promise; + async create(data: Partial | Partial[]): Promise { const primaryKeyField = await this.schemaInspector.primary(this.collection); const columns = await this.schemaInspector.columns(this.collection); @@ -196,7 +194,7 @@ export class ItemsService implements AbstractService { return Array.isArray(data) ? savedPrimaryKeys : savedPrimaryKeys[0]; } - async readByQuery(query: Query): Promise { + async readByQuery(query: Query): Promise | Partial[]> { const authorizationService = new AuthorizationService({ accountability: this.accountability, knex: this.knex, @@ -212,24 +210,24 @@ export class ItemsService implements AbstractService { } const records = await runAST(ast, { knex: this.knex }); - return records as T | T[] | null; + return records as Partial | Partial[] | null; } - readByKey( + readByKey( keys: PrimaryKey[], query?: Query, action?: PermissionsAction - ): Promise; - readByKey( + ): Promise[]>; + readByKey( key: PrimaryKey, query?: Query, action?: PermissionsAction - ): Promise; - async readByKey( + ): Promise>; + async readByKey( key: PrimaryKey | PrimaryKey[], query: Query = {}, action: PermissionsAction = 'read' - ): Promise { + ): Promise | Partial[]> { query = clone(query); const primaryKeyField = await this.schemaInspector.primary(this.collection); const keys = toArray(key); @@ -266,14 +264,14 @@ export class ItemsService implements AbstractService { if (result === null) throw new ForbiddenException(); - return result as T | T[] | null; + return result as Partial | Partial[] | null; } - update>(data: T, keys: PrimaryKey[]): Promise; - update>(data: T, key: PrimaryKey): Promise; - update>(data: T[]): Promise; - async update>( - data: T | T[], + update(data: Partial, keys: PrimaryKey[]): Promise; + update(data: Partial, key: PrimaryKey): Promise; + update(data: Partial[]): Promise; + async update( + data: Partial | Partial[], key?: PrimaryKey | PrimaryKey[] ): Promise { const primaryKeyField = await this.schemaInspector.primary(this.collection); @@ -434,10 +432,7 @@ export class ItemsService implements AbstractService { return keys; } - async updateByQuery>( - data: T, - query: Query - ): Promise { + async updateByQuery(data: Partial, query: Query): Promise { const primaryKeyField = await this.schemaInspector.primary(this.collection); const readQuery = cloneDeep(query); readQuery.fields = [primaryKeyField]; @@ -455,11 +450,9 @@ export class ItemsService implements AbstractService { return await this.update(data, keys); } - upsert>(data: T[]): Promise; - upsert>(data: T): Promise; - async upsert>( - data: T | T[] - ): Promise { + upsert(data: Partial[]): Promise; + upsert(data: Partial): Promise; + async upsert(data: Partial | Partial[]): Promise { const primaryKeyField = await this.schemaInspector.primary(this.collection); const payloads = toArray(data); const primaryKeys: PrimaryKey[] = []; @@ -561,11 +554,11 @@ export class ItemsService implements AbstractService { return await this.delete(keys); } - async readSingleton(query: Query): Promise { + async readSingleton(query: Query): Promise> { query = clone(query); query.single = true; - const record = (await this.readByQuery(query)) as T; + const record = (await this.readByQuery(query)) as Partial; if (!record) { const columns = await this.schemaInspector.columnInfo(this.collection); @@ -575,13 +568,13 @@ export class ItemsService implements AbstractService { defaults[column.name] = getDefaultValue(column); } - return defaults as T; + return defaults as Partial; } return record; } - async upsertSingleton>(data: T) { + async upsertSingleton(data: Partial) { const primaryKeyField = await this.schemaInspector.primary(this.collection); const record = await this.knex