diff --git a/src/routes/activity.ts b/src/routes/activity.ts index 9bd60db6f4..b881c5a8ed 100644 --- a/src/routes/activity.ts +++ b/src/routes/activity.ts @@ -2,7 +2,7 @@ import express from 'express'; import asyncHandler from 'express-async-handler'; import sanitizeQuery from '../middleware/sanitize-query'; import validateQuery from '../middleware/validate-query'; -import { readActivities, readActivity } from '../services/activity'; +import * as ActivityService from '../services/activity'; import useCollection from '../middleware/use-collection'; const router = express.Router(); @@ -13,7 +13,7 @@ router.get( sanitizeQuery, validateQuery, asyncHandler(async (req, res) => { - const records = await readActivities(req.sanitizedQuery); + const records = await ActivityService.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, req.sanitizedQuery); + const record = await ActivityService.readActivity(req.params.pk, req.sanitizedQuery); return res.json({ data: record, diff --git a/src/routes/items.ts b/src/routes/items.ts index ff566f4324..fd03af3694 100644 --- a/src/routes/items.ts +++ b/src/routes/items.ts @@ -1,6 +1,6 @@ import express from 'express'; import asyncHandler from 'express-async-handler'; -import { createItem, readItems, readItem, updateItem, deleteItem } from '../services/items'; +import * as ItemsService from '../services/items'; import sanitizeQuery from '../middleware/sanitize-query'; import validateCollection from '../middleware/validate-collection'; import validateSingleton from '../middleware/validate-singleton'; @@ -14,8 +14,11 @@ router.post( '/:collection', validateCollection, validateSingleton, + sanitizeQuery, + validateQuery, asyncHandler(async (req, res) => { - const item = await createItem(req.params.collection, req.body); + const primaryKey = await ItemsService.createItem(req.collection, req.body); + const item = await ItemsService.readItem(req.collection, primaryKey, req.sanitizedQuery); ActivityService.createActivity({ action: ActivityService.Action.CREATE, @@ -38,8 +41,8 @@ router.get( validateQuery, asyncHandler(async (req, res) => { const [records, meta] = await Promise.all([ - readItems(req.params.collection, req.sanitizedQuery), - MetaService.getMetaForQuery(req.params.collection, req.sanitizedQuery), + ItemsService.readItems(req.collection, req.sanitizedQuery), + MetaService.getMetaForQuery(req.collection, req.sanitizedQuery), ]); return res.json({ @@ -52,8 +55,14 @@ router.get( router.get( '/:collection/:pk', validateCollection, + sanitizeQuery, + validateQuery, asyncHandler(async (req, res) => { - const record = await readItem(req.params.collection, req.params.pk); + const record = await ItemsService.readItem( + req.collection, + req.params.pk, + req.sanitizedQuery + ); return res.json({ data: record, @@ -64,8 +73,11 @@ router.get( router.patch( '/:collection/:pk', validateCollection, + sanitizeQuery, + validateQuery, asyncHandler(async (req, res) => { - const item = await updateItem(req.params.collection, req.params.pk, req.body); + const primaryKey = await ItemsService.updateItem(req.collection, req.params.pk, req.body); + const item = await ItemsService.readItem(req.collection, primaryKey, req.sanitizedQuery); ActivityService.createActivity({ action: ActivityService.Action.UPDATE, @@ -84,7 +96,7 @@ router.delete( '/:collection/:pk', validateCollection, asyncHandler(async (req, res) => { - await deleteItem(req.params.collection, req.params.pk); + await ItemsService.deleteItem(req.collection, req.params.pk); ActivityService.createActivity({ action: ActivityService.Action.DELETE, diff --git a/src/services/activity.ts b/src/services/activity.ts index d96c7c034a..aded494c81 100644 --- a/src/services/activity.ts +++ b/src/services/activity.ts @@ -12,7 +12,8 @@ export enum Action { } export const createActivity = async (data: Record, query?: Query) => { - return await ItemsService.createItem('directus_activity', data, query); + const primaryKey = await ItemsService.createItem('directus_activity', data); + return await ItemsService.readItem('directus_activity', primaryKey, query); }; export const readActivities = async (query?: Query) => { diff --git a/src/services/collection-presets.ts b/src/services/collection-presets.ts index 675870dfca..81ea03b4d2 100644 --- a/src/services/collection-presets.ts +++ b/src/services/collection-presets.ts @@ -2,7 +2,8 @@ import { Query } from '../types/query'; import * as ItemsService from './items'; export const createCollectionPreset = async (data: Record, query: Query) => { - return await ItemsService.createItem('directus_collection_presets', data, query); + const primaryKey = await ItemsService.createItem('directus_collection_presets', data); + return await ItemsService.readItem('directus_collection_presets', primaryKey, query); }; export const readCollectionPresets = async (query: Query) => { @@ -18,7 +19,8 @@ export const updateCollectionPreset = async ( data: Record, query: Query ) => { - return await ItemsService.updateItem('directus_collection_presets', pk, data, query); + const primaryKey = await ItemsService.updateItem('directus_collection_presets', pk, data); + return await ItemsService.readItem('directus_collection_presets', primaryKey, query); }; export const deleteCollectionPreset = async (pk: string | number) => { diff --git a/src/services/collections.ts b/src/services/collections.ts index 1acc7a8f3c..009080ebe2 100644 --- a/src/services/collections.ts +++ b/src/services/collections.ts @@ -36,7 +36,7 @@ export const create = async (payload: any) => { }); }); - const collection = await ItemsService.createItem('directus_collections', { + const primaryKey = await ItemsService.createItem('directus_collections', { collection: payload.collection, hidden: payload.hidden || false, single: payload.single || false, @@ -45,6 +45,8 @@ export const create = async (payload: any) => { translation: payload.translation || null, }); + const collection = await ItemsService.createItem('directus_collections', primaryKey); + /** * @TODO make this flexible and based on payload */ diff --git a/src/services/files.ts b/src/services/files.ts index ad2fe5330c..eed9e53893 100644 --- a/src/services/files.ts +++ b/src/services/files.ts @@ -51,7 +51,8 @@ export const createFile = async ( } await storage.disk(data.storage).put(payload.filename_disk, stream.pipe(pipeline)); - return await ItemsService.createItem('directus_files', payload, query); + const primaryKey = await ItemsService.createItem('directus_files', payload); + return await ItemsService.readItem('directus_files', primaryKey, query); }; export const readFiles = async (query: Query) => { @@ -90,7 +91,8 @@ export const updateFile = async ( await storage.disk(file.storage).put(file.filename_disk, stream as any); } - return await ItemsService.updateItem('directus_files', pk, data, query); + const primaryKey = await ItemsService.updateItem('directus_files', pk, data); + return await ItemsService.readItem('directus_files', primaryKey, query); }; export const deleteFile = async (pk: string | number) => { diff --git a/src/services/folders.ts b/src/services/folders.ts index 446eff2370..aa7d2417f3 100644 --- a/src/services/folders.ts +++ b/src/services/folders.ts @@ -2,7 +2,8 @@ import { Query } from '../types/query'; import * as ItemsService from './items'; export const createFolder = async (data: Record, query: Query) => { - return await ItemsService.createItem('directus_folders', data, query); + const primaryKey = await ItemsService.createItem('directus_folders', data); + return await ItemsService.readItem('directus_folders', primaryKey, query); }; export const readFolders = async (query: Query) => { @@ -18,7 +19,8 @@ export const updateFolder = async ( data: Record, query: Query ) => { - return await ItemsService.updateItem('directus_folders', pk, data, query); + const primaryKey = await ItemsService.updateItem('directus_folders', pk, data); + return await ItemsService.readItem('directus_folders', primaryKey, query); }; export const deleteFolder = async (pk: string | number) => { diff --git a/src/services/items.ts b/src/services/items.ts index e19b8017ad..bcce640fa7 100644 --- a/src/services/items.ts +++ b/src/services/items.ts @@ -3,16 +3,32 @@ import { Query } from '../types/query'; import runAST from '../database/run-ast'; import getAST from '../utils/get-ast'; import * as PayloadService from './payload'; +import { pick } from 'lodash'; + +export const createItem = async (collection: string, data: Record) => { + let payload = await PayloadService.processValues('create', collection, data); + + payload = await PayloadService.processM2O(collection, payload); -export const createItem = async ( - collection: string, - data: Record, - query: Query = {} -) => { - const payload = await PayloadService.processValues('create', collection, data); const primaryKeyField = await schemaInspector.primary(collection); - const result = await database(collection).insert(payload).returning(primaryKeyField); - return readItem(collection, result[0], query); + + // Only insert the values that actually save to an existing column. This ensures we ignore aliases etc + const columns = await schemaInspector.columns(collection); + const primaryKeys = await database(collection) + .insert( + pick( + payload, + columns.map(({ column }) => column) + ) + ) + .returning(primaryKeyField); + + // This allows the o2m values to be populated correctly + payload[primaryKeyField] = primaryKeys[0]; + + await PayloadService.processO2M(collection, payload); + + return primaryKeys[0]; }; export const readItems = async >( @@ -51,16 +67,27 @@ export const readItem = async ( export const updateItem = async ( collection: string, pk: number | string, - data: Record, - query: Query = {} + data: Record ) => { - const payload = await PayloadService.processValues('create', collection, data); + let payload = await PayloadService.processValues('create', collection, data); + + payload = await PayloadService.processM2O(collection, payload); + const primaryKeyField = await schemaInspector.primary(collection); - const result = await database(collection) - .update(payload) + + // Only insert the values that actually save to an existing column. This ensures we ignore aliases etc + const columns = await schemaInspector.columns(collection); + const primaryKeys = await database(collection) + .update( + pick( + payload, + columns.map(({ column }) => column) + ) + ) .where({ [primaryKeyField]: pk }) - .returning('id'); - return readItem(collection, result[0], query); + .returning(primaryKeyField); + + return primaryKeys[0]; }; export const deleteItem = async (collection: string, pk: number | string) => { diff --git a/src/services/payload.ts b/src/services/payload.ts index d74784599a..addf3d3743 100644 --- a/src/services/payload.ts +++ b/src/services/payload.ts @@ -9,9 +9,51 @@ import { v4 as uuidv4 } from 'uuid'; import database from '../database'; import { clone } from 'lodash'; import { File } from '../types/files'; +import { Relation } from '../types/relation'; +import * as ItemsService from './items'; type Operation = 'create' | 'read' | 'update'; +type Transformers = { + [type: string]: ( + operation: Operation, + value: any, + payload: Record + ) => Promise; +}; + +/** + * @todo allow this to be extended + */ +const transformers: Transformers = { + async hash(operation, value) { + if (!value) return; + + if (operation === 'create' || operation === 'update') { + return await argon2.hash(String(value)); + } + + return value; + }, + async uuid(operation, value) { + if (operation === 'create' && !value) { + return uuidv4(); + } + + return value; + }, + async 'file-info'(operation, value, payload: File) { + if (operation === 'read' && payload) { + return { + asset_url: new URL(`/assets/${payload.id}`, process.env.PUBLIC_URL), + }; + } + + // This is an non-existing column, so there isn't any data to save + return undefined; + }, +}; + /** * Process and update all the special fields in the given payload * @@ -47,8 +89,6 @@ export const processValues = async ( }) ); - console.log(processedPayload); - /** @TODO * * - Make config.ts file in root @@ -69,49 +109,113 @@ async function processField( payload: Record, operation: Operation ) { - switch (field.special) { - case 'hash': - return await genHash(operation, payload[field.field], payload); - case 'uuid': - return await genUUID(operation, payload[field.field], payload); - case 'file-links': - // This is a system special type that only works on directus_files - return await genFileLinks(operation, payload[field.field], payload as File); - default: - return payload[field.field]; + if (transformers.hasOwnProperty(field.special)) { + return await transformers[field.special](operation, payload[field.field], payload); } + + return payload[field.field]; } /** - * @note The following functions can be called _a lot_. Make sure to utilize some form of caching - * if you have to rely on heavy operations + * Recursively checks for nested relational items, and saves them bottom up, to ensure we have IDs etc ready */ +export const processM2O = async (collection: string, payload: Record) => { + const payloadClone = clone(payload); -async function genHash(operation: Operation, value: any, payload: Record) { - if (!value) return; + const relations = await database + .select('*') + .from('directus_relations') + .where({ collection_many: collection }); - if (operation === 'create' || operation === 'update') { - return await argon2.hash(String(value)); - } + // Only process related records that are actually in the payload + const relationsToProcess = relations.filter((relation) => { + return ( + payloadClone.hasOwnProperty(relation.field_many) && + typeof payloadClone[relation.field_many] === 'object' + ); + }); - return value; -} + // Save all nested m2o records + await Promise.all( + relationsToProcess.map(async (relation) => { + const relatedRecord = payloadClone[relation.field_many]; + const hasPrimaryKey = relatedRecord.hasOwnProperty(relation.primary_one); -async function genUUID(operation: Operation, value: any, payload: Record) { - if (operation === 'create' && !value) { - return uuidv4(); - } + let relatedPrimaryKey: string | number; - return value; -} + if (hasPrimaryKey) { + relatedPrimaryKey = relatedRecord[relation.primary_one]; + await ItemsService.updateItem( + relation.collection_one, + relatedPrimaryKey, + relatedRecord + ); + } else { + relatedPrimaryKey = await ItemsService.createItem( + relation.collection_one, + relatedRecord + ); + } -async function genFileLinks(operation: Operation, value: undefined, payload: File) { - if (operation === 'read' && payload) { - return { - asset_url: new URL(`/assets/${payload.id}`, process.env.PUBLIC_URL), - }; - } + // Overwrite the nested object with just the primary key, so the parent level can be saved correctly + payloadClone[relation.field_many] = relatedPrimaryKey; + }) + ); - // This is an non-existing column, so there isn't any data to save - return undefined; -} + return payloadClone; +}; + +export const processO2M = async (collection: string, payload: Record) => { + const payloadClone = clone(payload); + + const relations = await database + .select('*') + .from('directus_relations') + .where({ collection_one: collection }); + + // Only process related records that are actually in the payload + const relationsToProcess = relations.filter((relation) => { + return ( + payloadClone.hasOwnProperty(relation.field_one) && + Array.isArray(payloadClone[relation.field_one]) + ); + }); + + // Save all nested o2m records + await Promise.all( + relationsToProcess.map(async (relation) => { + const relatedRecords = payloadClone[relation.field_one]; + + await Promise.all( + relatedRecords.map(async (relatedRecord: any, index: number) => { + relatedRecord[relation.field_many] = payloadClone[relation.primary_one]; + + const hasPrimaryKey = relatedRecord.hasOwnProperty(relation.primary_many); + + let relatedPrimaryKey: string | number; + + if (hasPrimaryKey) { + relatedPrimaryKey = relatedRecord[relation.primary_many]; + + await ItemsService.updateItem( + relation.collection_many, + relatedPrimaryKey, + relatedRecord + ); + } else { + relatedPrimaryKey = await ItemsService.createItem( + relation.collection_many, + relatedRecord + ); + } + + relatedRecord[relation.primary_many] = relatedPrimaryKey; + + payloadClone[relation.field_one][index] = relatedRecord; + }) + ); + }) + ); + + return payloadClone; +}; diff --git a/src/services/permissions.ts b/src/services/permissions.ts index abba8f6d79..737b75db08 100644 --- a/src/services/permissions.ts +++ b/src/services/permissions.ts @@ -2,7 +2,8 @@ import { Query } from '../types/query'; import * as ItemsService from './items'; export const createPermission = async (data: Record, query: Query) => { - return await ItemsService.createItem('directus_permissions', data, query); + const primaryKey = await ItemsService.createItem('directus_permissions', data); + return await ItemsService.readItem('directus_permissions', primaryKey, query); }; export const readPermissions = async (query: Query) => { @@ -18,7 +19,8 @@ export const updatePermission = async ( data: Record, query: Query ) => { - return await ItemsService.updateItem('directus_permissions', pk, data, query); + const primaryKey = await ItemsService.updateItem('directus_permissions', pk, data); + return await ItemsService.readItem('directus_permissions', primaryKey, query); }; export const deletePermission = async (pk: string | number) => { diff --git a/src/services/relations.ts b/src/services/relations.ts index ff19fbd65d..9997fab2bf 100644 --- a/src/services/relations.ts +++ b/src/services/relations.ts @@ -2,7 +2,8 @@ import { Query } from '../types/query'; import * as ItemsService from './items'; export const createRelation = async (data: Record, query: Query) => { - return await ItemsService.createItem('directus_relations', data, query); + const primaryKey = await ItemsService.createItem('directus_relations', data); + return ItemsService.readItem('directus_relations', primaryKey, query); }; export const readRelations = async (query: Query) => { @@ -18,7 +19,8 @@ export const updateRelation = async ( data: Record, query: Query ) => { - return await ItemsService.updateItem('directus_relations', pk, data, query); + const primaryKey = await ItemsService.updateItem('directus_relations', pk, data); + return ItemsService.readItem('directus_relations', primaryKey, query); }; export const deleteRelation = async (pk: string | number) => { diff --git a/src/services/roles.ts b/src/services/roles.ts index be3c9e099c..63e9a77871 100644 --- a/src/services/roles.ts +++ b/src/services/roles.ts @@ -2,7 +2,8 @@ import { Query } from '../types/query'; import * as ItemsService from './items'; export const createRole = async (data: Record, query: Query) => { - return await ItemsService.createItem('directus_roles', data, query); + const primaryKey = await ItemsService.createItem('directus_roles', data); + return await ItemsService.readItem('directus_roles', primaryKey, query); }; export const readRoles = async (query: Query) => { @@ -14,7 +15,8 @@ export const readRole = async (pk: string | number, query: Query) => { }; export const updateRole = async (pk: string | number, data: Record, query: Query) => { - return await ItemsService.updateItem('directus_roles', pk, data, query); + const primaryKey = await ItemsService.updateItem('directus_roles', pk, data); + return await ItemsService.readItem('directus_roles', primaryKey, query); }; export const deleteRole = async (pk: string | number) => { diff --git a/src/services/settings.ts b/src/services/settings.ts index 60fa30f46e..82c0b4befa 100644 --- a/src/services/settings.ts +++ b/src/services/settings.ts @@ -16,5 +16,6 @@ export const updateSettings = async ( query: Query ) => { /** @NOTE I guess this can technically update _all_ items, as we expect there to only be one */ - return await ItemsService.updateItem('directus_settings', pk, data, query); + const primaryKey = await ItemsService.updateItem('directus_settings', pk, data); + return await ItemsService.readItem('directus_settings', primaryKey, query); }; diff --git a/src/services/users.ts b/src/services/users.ts index 8411687aa1..e7ff44d4a9 100644 --- a/src/services/users.ts +++ b/src/services/users.ts @@ -7,7 +7,8 @@ import argon2 from 'argon2'; import { InvalidPayloadException } from '../exceptions'; export const createUser = async (data: Record, query?: Query) => { - return await ItemsService.createItem('directus_users', data, query); + const primaryKey = await ItemsService.createItem('directus_users', data); + return await ItemsService.readItem('directus_user', primaryKey, query); }; export const readUsers = async (query?: Query) => { @@ -25,7 +26,8 @@ export const updateUser = async (pk: string | number, data: Record, * * Maybe make this an option? */ - return await ItemsService.updateItem('directus_users', pk, data, query); + const primaryKey = await ItemsService.updateItem('directus_users', pk, data); + return await ItemsService.readItem('directus_user', primaryKey, query); }; export const deleteUser = async (pk: string | number) => { diff --git a/src/services/webhooks.ts b/src/services/webhooks.ts index 6c648ca0e5..3b7abb9a6a 100644 --- a/src/services/webhooks.ts +++ b/src/services/webhooks.ts @@ -2,7 +2,8 @@ import { Query } from '../types/query'; import * as ItemsService from './items'; export const createWebhook = async (data: Record, query: Query) => { - return await ItemsService.createItem('directus_webhooks', data, query); + const primaryKey = await ItemsService.createItem('directus_webhooks', data); + return await ItemsService.readItem('directus_webhooks', primaryKey, query); }; export const readWebhooks = async (query: Query) => { @@ -18,7 +19,8 @@ export const updateWebhook = async ( data: Record, query: Query ) => { - return await ItemsService.updateItem('directus_webhooks', pk, data, query); + const primaryKey = await ItemsService.updateItem('directus_webhooks', pk, data); + return await ItemsService.readItem('directus_webhooks', primaryKey, query); }; export const deleteWebhook = async (pk: string | number) => { diff --git a/src/types/relation.ts b/src/types/relation.ts index 3042536071..7bfdc49099 100644 --- a/src/types/relation.ts +++ b/src/types/relation.ts @@ -1,7 +1,10 @@ export type Relation = { id: number; + collection_many: string; field_many: string; + primary_many: string; + collection_one: string; field_one: string; primary_one: string;