From 9c76d14d201c7cad9a1a76adf71bcc77ee0cec52 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Thu, 2 Jul 2020 16:14:08 -0400 Subject: [PATCH 1/2] Add relational reading --- src/{database.ts => database/index.ts} | 4 +- src/database/run-ast.ts | 266 +++++++++++++++++++++++++ src/middleware/validate-query.ts | 4 +- src/routes/activity.ts | 4 +- src/routes/collection-presets.ts | 8 +- src/routes/files.ts | 4 +- src/routes/folders.ts | 12 +- src/routes/items.ts | 4 +- src/routes/permissions.ts | 8 +- src/routes/relations.ts | 8 +- src/routes/revisions.ts | 4 +- src/routes/roles.ts | 8 +- src/routes/settings.ts | 4 +- src/routes/users.ts | 8 +- src/services/items.ts | 73 ++----- src/services/meta.ts | 2 +- src/types/ast.ts | 24 +++ src/types/query.ts | 2 +- src/types/relation.ts | 8 + src/utils/get-ast.ts | 108 ++++++++++ 20 files changed, 471 insertions(+), 92 deletions(-) rename src/{database.ts => database/index.ts} (82%) create mode 100644 src/database/run-ast.ts create mode 100644 src/types/ast.ts create mode 100644 src/types/relation.ts create mode 100644 src/utils/get-ast.ts diff --git a/src/database.ts b/src/database/index.ts similarity index 82% rename from src/database.ts rename to src/database/index.ts index ec61552544..1700c83fdb 100644 --- a/src/database.ts +++ b/src/database/index.ts @@ -1,7 +1,7 @@ import knex from 'knex'; -import logger from './logger'; +import logger from '../logger'; -import SchemaInspector from './knex-schema-inspector/lib/index'; +import SchemaInspector from '../knex-schema-inspector/lib/index'; const log = logger.child({ module: 'sql' }); diff --git a/src/database/run-ast.ts b/src/database/run-ast.ts new file mode 100644 index 0000000000..4682c96b6d --- /dev/null +++ b/src/database/run-ast.ts @@ -0,0 +1,266 @@ +import { AST, NestedCollectionAST } from '../types/ast'; +import { uniq } from 'lodash'; +import database from './index'; +import { Query } from '../types/query'; + +// const testAST: AST = { +// type: 'collection', +// name: 'articles', +// query: {}, +// children: [ +// { +// type: 'field', +// name: 'id' +// }, +// { +// type: 'field', +// name: 'title', +// }, +// { +// type: 'collection', +// name: 'authors', +// fieldKey: 'author_id', +// parentKey: 'id', +// relation: { +// id: 2, +// collection_many: 'articles', +// field_many: 'author_id', +// collection_one: 'authors', +// primary_one: 'id', +// field_one: null +// }, +// query: {}, +// children: [ +// { +// type: 'field', +// name: 'id' +// }, +// { +// type: 'field', +// name: 'name' +// }, +// { +// type: 'collection', +// name: 'movies', +// fieldKey: 'movies', +// parentKey: 'id', +// relation: { +// id: 3, +// collection_many: 'movies', +// field_many: 'author_id', +// collection_one: 'authors', +// primary_one: 'id', +// field_one: 'movies' +// }, +// query: {}, +// children: [ +// { +// type: 'field', +// name: 'id', +// }, +// { +// type: 'field', +// name: 'title' +// }, +// { +// type: 'collection', +// name: 'authors', +// fieldKey: 'author_id', +// parentKey: 'id', +// relation: { +// id: 4, +// collection_many: 'movies', +// field_many: 'author_id', +// collection_one: 'authors', +// primary_one: 'id', +// field_one: 'movies', +// }, +// query: {}, +// children: [ +// { +// type: 'field', +// name: 'id', +// }, +// { +// type: 'field', +// name: 'name', +// }, +// { +// type: 'collection', +// name: 'movies', +// fieldKey: 'movies', +// parentKey: 'id', +// relation: { +// id: 6, +// collection_many: 'movies', +// field_many: 'author_id', +// collection_one: 'authors', +// primary_one: 'id', +// field_one: 'movies' +// }, +// query: { +// sort: [ +// { +// column: 'title', +// order: 'asc' +// } +// ] +// }, +// children: [ +// { +// type: 'field', +// name: 'id' +// }, +// { +// type: 'field', +// name: 'title' +// }, +// { +// type: 'field', +// name: 'author_id' +// } +// ] +// } +// ] +// } +// ] +// } +// ] +// } +// ] +// } + +export default async function runAST(ast: AST, query = ast.query) { + const toplevelFields: string[] = []; + const nestedCollections: NestedCollectionAST[] = []; + + for (const child of ast.children) { + if (child.type === 'field') { + toplevelFields.push(child.name); + continue; + } + + const m2o = isM2O(child); + + if (m2o) { + toplevelFields.push(child.relation.field_many); + } + + nestedCollections.push(child); + } + + const dbQuery = database.select(toplevelFields).from(ast.name); + + if (query.filter) { + query.filter.forEach((filter) => { + if (filter.operator === 'in') { + dbQuery.whereIn(filter.column, filter.value as (string | number)[]); + } + + if (filter.operator === 'eq') { + dbQuery.where({ [filter.column]: filter.value }); + } + + if (filter.operator === 'neq') { + dbQuery.whereNot({ [filter.column]: filter.value }); + } + + if (filter.operator === 'null') { + dbQuery.whereNull(filter.column); + } + + if (filter.operator === 'nnull') { + dbQuery.whereNotNull(filter.column); + } + }); + } + + if (query.sort) { + dbQuery.orderBy(query.sort); + } + + if (query.limit && !query.offset) { + dbQuery.limit(query.limit); + } + + if (query.offset) { + dbQuery.offset(query.offset); + } + + if (query.page) { + dbQuery.offset(query.limit * (query.page - 1)); + } + + if (query.single) { + dbQuery.limit(1).first(); + } + + let results = await dbQuery; + + for (const batch of nestedCollections) { + const m2o = isM2O(batch); + + let batchQuery: Query = {}; + + if (m2o) { + batchQuery = { + ...batch.query, + filter: [ + ...(batch.query.filter || []), + { + column: 'id', + operator: 'in', + value: uniq(results.map((res) => res[batch.relation.field_many])), + }, + ], + }; + } else { + batchQuery = { + ...batch.query, + filter: [ + ...(batch.query.filter || []), + { + column: batch.relation.field_many, + operator: 'in', + value: uniq(results.map((res) => res[batch.parentKey])), + }, + ], + }; + } + + const nestedResults = await runAST(batch, batchQuery); + + results = results.map((record) => { + if (m2o) { + return { + ...record, + [batch.fieldKey]: nestedResults.find((nestedRecord) => { + return nestedRecord[batch.relation.primary_one] === record[batch.fieldKey]; + }), + }; + } + + return { + ...record, + [batch.fieldKey]: nestedResults.filter((nestedRecord) => { + /** + * @todo + * pull the name ID from somewhere real + */ + return ( + nestedRecord[batch.relation.field_many] === record.id || + nestedRecord[batch.relation.field_many]?.id === record.id + ); + }), + }; + }); + } + + return results; +} + +function isM2O(child: NestedCollectionAST) { + return ( + child.relation.collection_one === child.name && child.relation.field_many === child.fieldKey + ); +} diff --git a/src/middleware/validate-query.ts b/src/middleware/validate-query.ts index 5689b52057..08e2c53400 100644 --- a/src/middleware/validate-query.ts +++ b/src/middleware/validate-query.ts @@ -38,13 +38,15 @@ async function validateFields(collection: string, query: Query) { const fieldsToCheck = new Set(); if (query.fields) { - query.fields.forEach((field) => fieldsToCheck.add(field)); + /** @todo support relationships in here */ + // query.fields.forEach((field) => fieldsToCheck.add(field)); } if (query.sort) { query.sort.forEach((sort) => fieldsToCheck.add(sort.column)); } + /** @todo swap with more efficient schemaInspector version */ const fieldsExist = await hasFields(collection, Array.from(fieldsToCheck)); Array.from(fieldsToCheck).forEach((field, index) => { diff --git a/src/routes/activity.ts b/src/routes/activity.ts index 0bd15f97df..9bd60db6f4 100644 --- a/src/routes/activity.ts +++ b/src/routes/activity.ts @@ -13,7 +13,7 @@ router.get( sanitizeQuery, validateQuery, asyncHandler(async (req, res) => { - const records = await readActivities(res.locals.query); + const records = await readActivities(req.sanitizedQuery); return res.json({ data: records, }); @@ -26,7 +26,7 @@ router.get( sanitizeQuery, validateQuery, asyncHandler(async (req, res) => { - const record = await readActivity(req.params.pk, res.locals.query); + const record = await readActivity(req.params.pk, req.sanitizedQuery); return res.json({ data: record, diff --git a/src/routes/collection-presets.ts b/src/routes/collection-presets.ts index 052972c67e..c5c3b5f21f 100644 --- a/src/routes/collection-presets.ts +++ b/src/routes/collection-presets.ts @@ -14,7 +14,7 @@ router.post( asyncHandler(async (req, res) => { const record = await CollectionPresetsService.createCollectionPreset( req.body, - res.locals.query + req.sanitizedQuery ); ActivityService.createActivity({ @@ -36,7 +36,7 @@ router.get( sanitizeQuery, validateQuery, asyncHandler(async (req, res) => { - const records = await CollectionPresetsService.readCollectionPresets(res.locals.query); + const records = await CollectionPresetsService.readCollectionPresets(req.sanitizedQuery); return res.json({ data: records }); }) ); @@ -49,7 +49,7 @@ router.get( asyncHandler(async (req, res) => { const record = await CollectionPresetsService.readCollectionPreset( req.params.pk, - res.locals.query + req.sanitizedQuery ); return res.json({ data: record }); }) @@ -62,7 +62,7 @@ router.patch( const record = await CollectionPresetsService.updateCollectionPreset( req.params.pk, req.body, - res.locals.query + req.sanitizedQuery ); ActivityService.createActivity({ diff --git a/src/routes/files.ts b/src/routes/files.ts index b03ac0417a..b6b2e32758 100644 --- a/src/routes/files.ts +++ b/src/routes/files.ts @@ -105,7 +105,7 @@ router.get( sanitizeQuery, validateQuery, asyncHandler(async (req, res) => { - const records = await FilesService.readFiles(res.locals.query); + const records = await FilesService.readFiles(req.sanitizedQuery); return res.json({ data: records }); }) ); @@ -116,7 +116,7 @@ router.get( sanitizeQuery, validateQuery, asyncHandler(async (req, res) => { - const record = await FilesService.readFile(req.params.pk, res.locals.query); + const record = await FilesService.readFile(req.params.pk, req.sanitizedQuery); return res.json({ data: record }); }) ); diff --git a/src/routes/folders.ts b/src/routes/folders.ts index f61836264b..b8208addf1 100644 --- a/src/routes/folders.ts +++ b/src/routes/folders.ts @@ -14,7 +14,7 @@ router.post( useCollection('directus_folders'), asyncHandler(async (req, res) => { const payload = await PayloadService.processValues('create', req.collection, req.body); - const record = await FoldersService.createFolder(payload, res.locals.query); + const record = await FoldersService.createFolder(payload, req.sanitizedQuery); ActivityService.createActivity({ action: ActivityService.Action.CREATE, @@ -35,7 +35,7 @@ router.get( sanitizeQuery, validateQuery, asyncHandler(async (req, res) => { - const records = await FoldersService.readFolders(res.locals.query); + const records = await FoldersService.readFolders(req.sanitizedQuery); return res.json({ data: records }); }) ); @@ -46,7 +46,7 @@ router.get( sanitizeQuery, validateQuery, asyncHandler(async (req, res) => { - const record = await FoldersService.readFolder(req.params.pk, res.locals.query); + const record = await FoldersService.readFolder(req.params.pk, req.sanitizedQuery); return res.json({ data: record }); }) ); @@ -57,7 +57,11 @@ router.patch( asyncHandler(async (req, res) => { const payload = await PayloadService.processValues('create', req.collection, req.body); - const record = await FoldersService.updateFolder(req.params.pk, payload, res.locals.query); + const record = await FoldersService.updateFolder( + req.params.pk, + payload, + req.sanitizedQuery + ); ActivityService.createActivity({ action: ActivityService.Action.UPDATE, diff --git a/src/routes/items.ts b/src/routes/items.ts index 6231963252..75df3e9af3 100644 --- a/src/routes/items.ts +++ b/src/routes/items.ts @@ -40,8 +40,8 @@ router.get( validateQuery, asyncHandler(async (req, res) => { const [records, meta] = await Promise.all([ - readItems(req.params.collection, res.locals.query), - MetaService.getMetaForQuery(req.params.collection, res.locals.query), + readItems(req.params.collection, req.sanitizedQuery), + MetaService.getMetaForQuery(req.params.collection, req.sanitizedQuery), ]); return res.json({ diff --git a/src/routes/permissions.ts b/src/routes/permissions.ts index 9d2a1a7802..ab20962e9a 100644 --- a/src/routes/permissions.ts +++ b/src/routes/permissions.ts @@ -12,7 +12,7 @@ router.post( '/', useCollection('directus_permissions'), asyncHandler(async (req, res) => { - const item = await PermissionsService.createPermission(req.body, res.locals.query); + const item = await PermissionsService.createPermission(req.body, req.sanitizedQuery); ActivityService.createActivity({ action: ActivityService.Action.CREATE, @@ -33,7 +33,7 @@ router.get( sanitizeQuery, validateQuery, asyncHandler(async (req, res) => { - const item = await PermissionsService.readPermissions(res.locals.query); + const item = await PermissionsService.readPermissions(req.sanitizedQuery); return res.json({ data: item }); }) ); @@ -44,7 +44,7 @@ router.get( sanitizeQuery, validateQuery, asyncHandler(async (req, res) => { - const record = await PermissionsService.readPermission(req.params.pk, res.locals.query); + const record = await PermissionsService.readPermission(req.params.pk, req.sanitizedQuery); return res.json({ data: record }); }) ); @@ -56,7 +56,7 @@ router.patch( const item = await PermissionsService.updatePermission( req.params.pk, req.body, - res.locals.query + req.sanitizedQuery ); ActivityService.createActivity({ diff --git a/src/routes/relations.ts b/src/routes/relations.ts index 0df8e0ad9e..cddd364dfe 100644 --- a/src/routes/relations.ts +++ b/src/routes/relations.ts @@ -12,7 +12,7 @@ router.post( '/', useCollection('directus_relations'), asyncHandler(async (req, res) => { - const item = await RelationsService.createRelation(req.body, res.locals.query); + const item = await RelationsService.createRelation(req.body, req.sanitizedQuery); ActivityService.createActivity({ action: ActivityService.Action.CREATE, @@ -33,7 +33,7 @@ router.get( sanitizeQuery, validateQuery, asyncHandler(async (req, res) => { - const records = await RelationsService.readRelations(res.locals.query); + const records = await RelationsService.readRelations(req.sanitizedQuery); return res.json({ data: records }); }) ); @@ -44,7 +44,7 @@ router.get( sanitizeQuery, validateQuery, asyncHandler(async (req, res) => { - const record = await RelationsService.readRelation(req.params.pk, res.locals.query); + const record = await RelationsService.readRelation(req.params.pk, req.sanitizedQuery); return res.json({ data: record }); }) ); @@ -56,7 +56,7 @@ router.patch( const item = await RelationsService.updateRelation( req.params.pk, req.body, - res.locals.query + req.sanitizedQuery ); ActivityService.createActivity({ diff --git a/src/routes/revisions.ts b/src/routes/revisions.ts index a44b198f93..7160527e0e 100644 --- a/src/routes/revisions.ts +++ b/src/routes/revisions.ts @@ -13,7 +13,7 @@ router.get( sanitizeQuery, validateQuery, asyncHandler(async (req, res) => { - const records = await RevisionsService.readRevisions(res.locals.query); + const records = await RevisionsService.readRevisions(req.sanitizedQuery); return res.json({ data: records }); }) ); @@ -24,7 +24,7 @@ router.get( sanitizeQuery, validateQuery, asyncHandler(async (req, res) => { - const record = await RevisionsService.readRevision(req.params.pk, res.locals.query); + const record = await RevisionsService.readRevision(req.params.pk, req.sanitizedQuery); return res.json({ data: record }); }) ); diff --git a/src/routes/roles.ts b/src/routes/roles.ts index 582c30801a..c43267b6d1 100644 --- a/src/routes/roles.ts +++ b/src/routes/roles.ts @@ -12,7 +12,7 @@ router.post( '/', useCollection('directus_roles'), asyncHandler(async (req, res) => { - const item = await RolesService.createRole(req.body, res.locals.query); + const item = await RolesService.createRole(req.body, req.sanitizedQuery); ActivityService.createActivity({ action: ActivityService.Action.CREATE, @@ -33,7 +33,7 @@ router.get( sanitizeQuery, validateQuery, asyncHandler(async (req, res) => { - const records = await RolesService.readRoles(res.locals.query); + const records = await RolesService.readRoles(req.sanitizedQuery); return res.json({ data: records }); }) ); @@ -44,7 +44,7 @@ router.get( sanitizeQuery, validateQuery, asyncHandler(async (req, res) => { - const record = await RolesService.readRole(req.params.pk, res.locals.query); + const record = await RolesService.readRole(req.params.pk, req.sanitizedQuery); return res.json({ data: record }); }) ); @@ -53,7 +53,7 @@ router.patch( '/:pk', useCollection('directus_roles'), asyncHandler(async (req, res) => { - const item = await RolesService.updateRole(req.params.pk, req.body, res.locals.query); + const item = await RolesService.updateRole(req.params.pk, req.body, req.sanitizedQuery); ActivityService.createActivity({ action: ActivityService.Action.UPDATE, diff --git a/src/routes/settings.ts b/src/routes/settings.ts index 2274160dc2..a347df4507 100644 --- a/src/routes/settings.ts +++ b/src/routes/settings.ts @@ -13,7 +13,7 @@ router.get( sanitizeQuery, validateQuery, asyncHandler(async (req, res) => { - const records = await SettingsService.readSettings(1, res.locals.query); + const records = await SettingsService.readSettings(1, req.sanitizedQuery); return res.json({ data: records }); }) ); @@ -25,7 +25,7 @@ router.patch( const records = await SettingsService.updateSettings( req.params.pk /** @TODO Singleton */, req.body, - res.locals.query + req.sanitizedQuery ); return res.json({ data: records }); }) diff --git a/src/routes/users.ts b/src/routes/users.ts index d6ff89ea39..25de21627e 100644 --- a/src/routes/users.ts +++ b/src/routes/users.ts @@ -14,7 +14,7 @@ router.post( '/', useCollection('directus_users'), asyncHandler(async (req, res) => { - const item = await UsersService.createUser(req.body, res.locals.query); + const item = await UsersService.createUser(req.body, req.sanitizedQuery); ActivityService.createActivity({ action: ActivityService.Action.CREATE, @@ -35,7 +35,7 @@ router.get( sanitizeQuery, validateQuery, asyncHandler(async (req, res) => { - const item = await UsersService.readUsers(res.locals.query); + const item = await UsersService.readUsers(req.sanitizedQuery); return res.json({ data: item }); }) @@ -47,7 +47,7 @@ router.get( sanitizeQuery, validateQuery, asyncHandler(async (req, res) => { - const items = await UsersService.readUser(req.params.pk, res.locals.query); + const items = await UsersService.readUser(req.params.pk, req.sanitizedQuery); return res.json({ data: items }); }) ); @@ -56,7 +56,7 @@ router.patch( '/:pk', useCollection('directus_users'), asyncHandler(async (req, res) => { - const item = await UsersService.updateUser(req.params.pk, req.body, res.locals.query); + const item = await UsersService.updateUser(req.params.pk, req.body, req.sanitizedQuery); ActivityService.createActivity({ action: ActivityService.Action.UPDATE, diff --git a/src/services/items.ts b/src/services/items.ts index b3c5ce9efe..ca961624b6 100644 --- a/src/services/items.ts +++ b/src/services/items.ts @@ -1,5 +1,7 @@ import database, { schemaInspector } from '../database'; import { Query } from '../types/query'; +import runAST from '../database/run-ast'; +import getAST from '../utils/get-ast'; export const createItem = async ( collection: string, @@ -15,54 +17,8 @@ export const readItems = async >( collection: string, query: Query = {} ): Promise => { - const dbQuery = database.select(query?.fields || '*').from(collection); - - if (query.sort) { - dbQuery.orderBy(query.sort); - } - - if (query.filter) { - query.filter.forEach((filter) => { - if (filter.operator === 'eq') { - dbQuery.where({ [filter.column]: filter.value }); - } - - if (filter.operator === 'neq') { - dbQuery.whereNot({ [filter.column]: filter.value }); - } - - if (filter.operator === 'null') { - dbQuery.whereNull(filter.column); - } - - if (filter.operator === 'nnull') { - dbQuery.whereNotNull(filter.column); - } - }); - } - - if (query.limit && !query.offset) { - dbQuery.limit(query.limit); - } - - if (query.offset) { - dbQuery.offset(query.offset); - } - - if (query.page) { - dbQuery.offset(query.limit * (query.page - 1)); - } - - if (query.single) { - dbQuery.limit(1); - } - - const records = await dbQuery; - - if (query.single) { - return records[0]; - } - + const ast = await getAST(collection, query); + const records = await runAST(ast); return records; }; @@ -72,11 +28,22 @@ export const readItem = async ( query: Query = {} ): Promise => { const primaryKeyField = await schemaInspector.primary(collection); - return await database - .select('*') - .from(collection) - .where({ [primaryKeyField]: pk }) - .first(); + + query = { + ...query, + filter: [ + ...query.filter, + { + column: primaryKeyField, + operator: 'eq', + value: pk, + }, + ], + }; + + const ast = await getAST(collection, query); + const records = await runAST(ast); + return records[0]; }; export const updateItem = async ( diff --git a/src/services/meta.ts b/src/services/meta.ts index c97588856a..99ecbe0495 100644 --- a/src/services/meta.ts +++ b/src/services/meta.ts @@ -2,7 +2,7 @@ import { Query } from '../types/query'; import database from '../database'; export const getMetaForQuery = async (collection: string, query: Query) => { - if (!query.meta) return; + if (!query || !query.meta) return; const results = await Promise.all( query.meta.map((metaVal) => { diff --git a/src/types/ast.ts b/src/types/ast.ts new file mode 100644 index 0000000000..5160c301ff --- /dev/null +++ b/src/types/ast.ts @@ -0,0 +1,24 @@ +import { Query } from './query'; +import { Relation } from './relation'; + +export type NestedCollectionAST = { + type: 'collection'; + name: string; + children: (NestedCollectionAST | FieldAST)[]; + query: Query; + fieldKey: string; + relation: Relation; + parentKey: string; +}; + +export type FieldAST = { + type: 'field'; + name: string; +}; + +export type AST = { + type: 'collection'; + name: string; + children: (NestedCollectionAST | FieldAST)[]; + query: Query; +}; diff --git a/src/types/query.ts b/src/types/query.ts index f7980421d6..71aee8b8aa 100644 --- a/src/types/query.ts +++ b/src/types/query.ts @@ -19,7 +19,7 @@ export type Sort = { export type Filter = { column: string; operator: FilterOperator; - value: null | string | number; + value: null | string | number | (string | number)[]; }; export type FilterOperator = 'eq' | 'neq' | 'in' | 'nin' | 'null' | 'nnull'; diff --git a/src/types/relation.ts b/src/types/relation.ts new file mode 100644 index 0000000000..3042536071 --- /dev/null +++ b/src/types/relation.ts @@ -0,0 +1,8 @@ +export type Relation = { + id: number; + collection_many: string; + field_many: string; + collection_one: string; + field_one: string; + primary_one: string; +}; diff --git a/src/utils/get-ast.ts b/src/utils/get-ast.ts new file mode 100644 index 0000000000..82a8adec68 --- /dev/null +++ b/src/utils/get-ast.ts @@ -0,0 +1,108 @@ +/** + * Generate an AST based on a given collection and query + */ + +import { Query } from '../types/query'; +import { Relation } from '../types/relation'; +import { AST, NestedCollectionAST, FieldAST } from '../types/ast'; +import database from '../database'; + +export default async function getAST(collection: string, query: Query): Promise { + const ast: AST = { + type: 'collection', + name: collection, + query: query, + children: [], + }; + + if (!query.fields) query.fields = ['*']; + + /** @todo support wildcard */ + const fields = query.fields; + + // If no relational fields are requested, we can stop early + const hasRelations = query.fields.some((field) => field.includes('.')); + if (hasRelations === false) { + fields.forEach((field) => { + ast.children.push({ + type: 'field', + name: field, + }); + }); + + return ast; + } + + // Even though we might not need all records from relations, it'll be faster to load all records + // into memory once and search through it in JS than it would be to do individual queries to fetch + // this data field by field + const relations = await database.select('*').from('directus_relations'); + + ast.children = await parseFields(collection, query.fields); + + console.log(JSON.stringify(ast, null, 2)); + + return ast; + + async function parseFields(parentCollection: string, fields: string[]) { + const children: (NestedCollectionAST | FieldAST)[] = []; + + const relationalStructure: Record = {}; + + for (const field of fields) { + if (field.includes('.') === false) { + children.push({ type: 'field', name: field }); + } else { + // field is relational + const parts = field.split('.'); + + if (relationalStructure.hasOwnProperty(parts[0]) === false) { + relationalStructure[parts[0]] = []; + } + + relationalStructure[parts[0]].push(parts.slice(1).join('.')); + } + } + + for (const [relationalField, nestedFields] of Object.entries(relationalStructure)) { + const relatedCollection = getRelatedCollection(parentCollection, relationalField); + + const child: NestedCollectionAST = { + type: 'collection', + name: relatedCollection, + fieldKey: relationalField, + parentKey: 'id' /** @todo this needs to come from somewhere real */, + relation: getRelation(parentCollection, relationalField), + query: {} /** @todo inject nested query here */, + children: await parseFields(relatedCollection, nestedFields), + }; + + children.push(child); + } + + return children; + } + + function getRelation(collection: string, field: string) { + const relation = relations.find((relation) => { + return ( + (relation.collection_many === collection && relation.field_many === field) || + (relation.collection_one === collection && relation.field_one === field) + ); + }); + + return relation; + } + + function getRelatedCollection(collection: string, field: string) { + const relation = getRelation(collection, field); + + if (relation.collection_many === collection && relation.field_many === field) { + return relation.collection_one; + } + + if (relation.collection_one === collection && relation.field_one === field) { + return relation.collection_many; + } + } +} From 23200630da4c28bddb88ee1076da1203ebfb6421 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Thu, 2 Jul 2020 17:40:50 -0400 Subject: [PATCH 2/2] Support *.* notation --- src/services/fields.ts | 10 ++++++++++ src/utils/get-ast.ts | 24 ++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/services/fields.ts b/src/services/fields.ts index 93f44348d9..4050d68737 100644 --- a/src/services/fields.ts +++ b/src/services/fields.ts @@ -1,5 +1,15 @@ import database, { schemaInspector } from '../database'; import { Field } from '../types/field'; +import { uniq } from 'lodash'; + +export const fieldsInCollection = async (collection: string) => { + const [fields, columns] = await Promise.all([ + database.select('field').from('directus_fields').where({ collection }), + schemaInspector.columns(collection), + ]); + + return uniq([...fields.map(({ field }) => field), ...columns.map(({ column }) => column)]); +}; export const readAll = async (collection?: string) => { const fieldsQuery = database.select('*').from('directus_fields'); diff --git a/src/utils/get-ast.ts b/src/utils/get-ast.ts index 82a8adec68..132b69746b 100644 --- a/src/utils/get-ast.ts +++ b/src/utils/get-ast.ts @@ -6,6 +6,7 @@ import { Query } from '../types/query'; import { Relation } from '../types/relation'; import { AST, NestedCollectionAST, FieldAST } from '../types/ast'; import database from '../database'; +import * as FieldsService from '../services/fields'; export default async function getAST(collection: string, query: Query): Promise { const ast: AST = { @@ -40,8 +41,6 @@ export default async function getAST(collection: string, query: Query): Promise< ast.children = await parseFields(collection, query.fields); - console.log(JSON.stringify(ast, null, 2)); - return ast; async function parseFields(parentCollection: string, fields: string[]) { @@ -49,6 +48,27 @@ export default async function getAST(collection: string, query: Query): Promise< const relationalStructure: Record = {}; + // Swap *.* case for *,.* + for (let i = 0; i < fields.length; i++) { + const fieldKey = fields[i]; + + if (fieldKey.includes('.') === false) continue; + + const parts = fieldKey.split('.'); + + if (parts[0] === '*') { + const availableFields = await FieldsService.fieldsInCollection(parentCollection); + fields.splice( + i, + 1, + ...availableFields + .filter((field) => !!getRelation(parentCollection, field)) + .map((field) => `${field}.${parts.slice(1).join('.')}`) + ); + fields.push('*'); + } + } + for (const field of fields) { if (field.includes('.') === false) { children.push({ type: 'field', name: field });