mirror of
https://github.com/directus/directus.git
synced 2026-01-29 23:47:57 -05:00
@@ -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' });
|
||||
|
||||
266
src/database/run-ast.ts
Normal file
266
src/database/run-ast.ts
Normal file
@@ -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
|
||||
);
|
||||
}
|
||||
@@ -38,13 +38,15 @@ async function validateFields(collection: string, query: Query) {
|
||||
const fieldsToCheck = new Set<string>();
|
||||
|
||||
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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 });
|
||||
})
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 });
|
||||
})
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 });
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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 <T = Record<string, any>>(
|
||||
collection: string,
|
||||
query: Query = {}
|
||||
): Promise<T[]> => {
|
||||
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 <T = any>(
|
||||
query: Query = {}
|
||||
): Promise<T> => {
|
||||
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 (
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
24
src/types/ast.ts
Normal file
24
src/types/ast.ts
Normal file
@@ -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;
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
8
src/types/relation.ts
Normal file
8
src/types/relation.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export type Relation = {
|
||||
id: number;
|
||||
collection_many: string;
|
||||
field_many: string;
|
||||
collection_one: string;
|
||||
field_one: string;
|
||||
primary_one: string;
|
||||
};
|
||||
128
src/utils/get-ast.ts
Normal file
128
src/utils/get-ast.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* 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';
|
||||
import * as FieldsService from '../services/fields';
|
||||
|
||||
export default async function getAST(collection: string, query: Query): Promise<AST> {
|
||||
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<Relation[]>('*').from('directus_relations');
|
||||
|
||||
ast.children = await parseFields(collection, query.fields);
|
||||
|
||||
return ast;
|
||||
|
||||
async function parseFields(parentCollection: string, fields: string[]) {
|
||||
const children: (NestedCollectionAST | FieldAST)[] = [];
|
||||
|
||||
const relationalStructure: Record<string, string[]> = {};
|
||||
|
||||
// Swap *.* case for *,<relational-field>.*
|
||||
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 });
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user