import database, { schemaInspector } from '../database'; import { Query } from '../types/query'; import runAST from '../database/run-ast'; import getASTFromQuery from '../utils/get-ast-from-query'; import { Accountability, Operation, Item } from '../types'; import Knex from 'knex'; import * as PayloadService from './payload'; import * as PermissionsService from './permissions'; import * as ActivityService from './activity'; import { pick, clone } from 'lodash'; import logger from '../logger'; async function saveActivityAndRevision( action: ActivityService.Action, collection: string, item: string, payload: Partial, accountability: Accountability, knex: Knex = database ) { const activityID = await knex('directus_activity') .insert({ action, collection, item, ip: accountability.ip, user_agent: accountability.userAgent, action_by: accountability.user, }) .returning('id'); if (action !== ActivityService.Action.DELETE) { await knex('directus_revisions').insert({ activity: activityID[0], collection, item, delta: payload, /** @todo make this configurable */ data: await readItem(collection, item, { fields: ['*'] }), parent: accountability.parent, }); } } export async function createItem( collection: string, data: Partial[], accountability?: Accountability ): Promise<(string | number)[]>; export async function createItem( collection: string, data: Partial, accountability?: Accountability ): Promise; export async function createItem( collection: string, data: Partial | Partial[], accountability?: Accountability ): Promise { const isBatch = Array.isArray(data); return database.transaction(async (tx) => { let payloads = isBatch ? data : [data]; const primaryKeys: (string | number)[] = await Promise.all( payloads.map(async (payload: Partial) => { if (accountability && accountability.admin === false) { payload = await PermissionsService.processValues( 'create', collection, accountability?.role, payload ); } payload = await PayloadService.processValues('create', collection, payload); payload = await PayloadService.processM2O(collection, payload); const primaryKeyField = await schemaInspector.primary(collection); // Only insert the values that actually save to an existing column. This ensures we ignore aliases etc const columns = await schemaInspector.columns(collection); const payloadWithoutAlias = pick( payload, columns.map(({ column }) => column) ); const primaryKeys = await tx(collection) .insert(payloadWithoutAlias) .returning(primaryKeyField); // This allows the o2m values to be populated correctly payload[primaryKeyField] = primaryKeys[0]; await PayloadService.processO2M(collection, payload); if (accountability) { await saveActivityAndRevision( ActivityService.Action.CREATE, collection, primaryKeys[0], payloadWithoutAlias, accountability, tx ); } return primaryKeys[0]; }) ); if (isBatch) { return primaryKeys; } else { return primaryKeys[0]; } }); } export const readItems = async >( collection: string, query: Query, accountability?: Accountability ): Promise => { let ast = await getASTFromQuery(collection, query, accountability); if (accountability && accountability.admin === false) { ast = await PermissionsService.processAST(ast, accountability.role); } const records = await runAST(ast); return await PayloadService.processValues('read', collection, records); }; export const readItem = async ( collection: string, pk: T, query: Query = {}, accountability?: Accountability, operation?: Operation ): Promise : Partial[]> => { // We allow overriding the operation, so we can use the item read logic to validate permissions // for update and delete as well operation = operation || 'read'; const primaryKeyField = await schemaInspector.primary(collection); const primaryKeys: any[] = Array.isArray(pk) ? pk : [pk]; const isBatch = Array.isArray(pk); if (isBatch) { query = { ...query, filter: { ...(query.filter || {}), [primaryKeyField]: { _in: primaryKeys, }, }, }; } else { query = { ...query, filter: { ...(query.filter || {}), [primaryKeyField]: { _eq: pk, }, }, }; } let ast = await getASTFromQuery(collection, query, accountability, operation); if (accountability && accountability.admin === false) { ast = await PermissionsService.processAST(ast, accountability.role, operation); } const records = await runAST(ast); const processedRecords = await PayloadService.processValues('read', collection, records); return isBatch ? processedRecords : processedRecords[0]; }; export const updateItem = async ( collection: string, pk: T, data: Partial, accountability?: Accountability ): Promise => { const primaryKeys: any[] = Array.isArray(pk) ? pk : [pk]; await database.transaction(async (tx) => { let payload = clone(data); return await Promise.all( primaryKeys.map(async (key) => { if (accountability && accountability.admin === false) { await PermissionsService.checkAccess( 'update', collection, key, accountability.role ); payload = await PermissionsService.processValues( 'validate', collection, accountability.role, data ); } payload = await PayloadService.processValues('update', collection, payload); payload = await PayloadService.processM2O(collection, payload); const primaryKeyField = await schemaInspector.primary(collection); // Only insert the values that actually save to an existing column. This ensures we ignore aliases etc const columns = await schemaInspector.columns(collection); const payloadWithoutAlias = pick( payload, columns.map(({ column }) => column) ); await tx(collection) .update(payloadWithoutAlias) .where({ [primaryKeyField]: key }); if (accountability) { await saveActivityAndRevision( ActivityService.Action.UPDATE, collection, String(key), payloadWithoutAlias, accountability, tx ); } return pk; }) ); }); return pk; }; export const deleteItem = async ( collection: string, pk: T, accountability?: Accountability ): Promise => { const primaryKeyField = await schemaInspector.primary(collection); const primaryKeys: any[] = Array.isArray(pk) ? pk : [pk]; await database.transaction(async (tx) => { await Promise.all( primaryKeys.map(async (key) => { if (accountability && accountability.admin === false) { await PermissionsService.checkAccess( 'delete', collection, key, accountability.role ); } await tx(collection) .where({ [primaryKeyField]: key }) .delete(); await saveActivityAndRevision( ActivityService.Action.DELETE, collection, String(key), {}, accountability, tx ); }) ); }); return pk; }; export const readSingleton = async ( collection: string, query: Query, accountability?: Accountability ) => { query.limit = 1; const records = await readItems(collection, query, accountability); const record = records[0]; if (!record) { const columns = await schemaInspector.columnInfo(collection); const defaults = {}; for (const column of columns) { defaults[column.name] = column.default_value; } return defaults; } return record; }; export const upsertSingleton = async ( collection: string, data: Partial, accountability: Accountability ) => { const primaryKeyField = await schemaInspector.primary(collection); const record = await database.select(primaryKeyField).from(collection).limit(1).first(); if (record) { return await updateItem(collection, record.id, data, accountability); } return await createItem(collection, data, accountability); };