From c00645f259e28175ee3de775feb51d902c30e45c Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Tue, 30 Jun 2020 14:45:54 -0400 Subject: [PATCH 01/16] Add GET /collections endpoint --- src/app.ts | 2 ++ src/database.ts | 4 ++++ src/middleware/sanitize-query.ts | 2 +- src/middleware/validate-query.ts | 4 ++-- src/routes/collections.ts | 41 ++++++++++++++++++++++++++++++++ src/routes/users.ts | 27 +++++++++++---------- src/routes/webhooks.ts | 12 ++++++---- src/services/items.ts | 5 +++- src/types/collection.ts | 8 +++++++ src/types/express.d.ts | 1 + 10 files changed, 85 insertions(+), 21 deletions(-) create mode 100644 src/routes/collections.ts create mode 100644 src/types/collection.ts diff --git a/src/app.ts b/src/app.ts index 15a16bff60..6caf032162 100644 --- a/src/app.ts +++ b/src/app.ts @@ -13,6 +13,7 @@ import authenticate from './middleware/authenticate'; import activityRouter from './routes/activity'; import assetsRouter from './routes/assets'; import authRouter from './routes/auth'; +import collectionsRouter from './routes/collections'; import collectionPresetsRouter from './routes/collection-presets'; import extensionsRouter from './routes/extensions'; import filesRouter from './routes/files'; @@ -39,6 +40,7 @@ const app = express() .use('/activity', activityRouter) .use('/assets', assetsRouter) .use('/auth', authRouter) + .use('/collections', collectionsRouter) .use('/collection_presets', collectionPresetsRouter) .use('/extensions', extensionsRouter) .use('/files', filesRouter) diff --git a/src/database.ts b/src/database.ts index 0832c4cc8d..8f7ea4e148 100644 --- a/src/database.ts +++ b/src/database.ts @@ -1,6 +1,8 @@ import knex from 'knex'; import logger from './logger'; +import SchemaInspector from '../../../knex-schema-inspector/lib/index'; + const log = logger.child({ module: 'sql' }); const database = knex({ @@ -16,4 +18,6 @@ const database = knex({ database.on('query', (data) => log.trace(data.sql)); +export const schemaInspector = SchemaInspector(database); + export default database; diff --git a/src/middleware/sanitize-query.ts b/src/middleware/sanitize-query.ts index e82cb0c119..1c332c6b10 100644 --- a/src/middleware/sanitize-query.ts +++ b/src/middleware/sanitize-query.ts @@ -44,7 +44,7 @@ const sanitizeQuery: RequestHandler = (req, res, next) => { query.meta = sanitizeMeta(req.query.meta); } - res.locals.query = query; + req.sanitizedQuery = query; return next(); }; diff --git a/src/middleware/validate-query.ts b/src/middleware/validate-query.ts index 51dd85d2ce..5689b52057 100644 --- a/src/middleware/validate-query.ts +++ b/src/middleware/validate-query.ts @@ -13,9 +13,9 @@ import { InvalidQueryException } from '../exceptions'; const validateQuery: RequestHandler = asyncHandler(async (req, res, next) => { if (!req.collection) return next(); - if (!req.query) return next(); + if (!req.sanitizedQuery) return next(); - const query: Query = req.query; + const query: Query = req.sanitizedQuery; await Promise.all([ validateParams(req.params.collection, query), diff --git a/src/routes/collections.ts b/src/routes/collections.ts new file mode 100644 index 0000000000..6212d2fdf9 --- /dev/null +++ b/src/routes/collections.ts @@ -0,0 +1,41 @@ +import { Router } from 'express'; +import asyncHandler from 'express-async-handler'; +import database, { schemaInspector } from '../database'; +import * as ItemsService from '../services/items'; +import sanitizeQuery from '../middleware/sanitize-query'; +import validateQuery from '../middleware/validate-query'; +import { Table } from '../../../../knex-schema-inspector/lib/types/table'; +import { Collection } from '../types/collection'; + +const router = Router(); + +router.get( + '/', + sanitizeQuery, + validateQuery, + asyncHandler(async (req, res) => { + const [tables, collections] = await Promise.all([ + schemaInspector.tables(), + ItemsService.readItems('directus_collections', req.sanitizedQuery), + ]); + + const data = (tables as Table[]).map((table) => { + const collectionInfo = collections.find((collection) => { + return collection.collection === table.name; + }); + + return { + collection: table.name, + note: table.comment, + hidden: collectionInfo?.hidden || false, + single: collectionInfo?.single || false, + icon: collectionInfo?.icon || null, + translation: collectionInfo?.translation || null, + }; + }); + + res.json({ data }); + }) +); + +export default router; diff --git a/src/routes/users.ts b/src/routes/users.ts index 3957276c08..d6ff89ea39 100644 --- a/src/routes/users.ts +++ b/src/routes/users.ts @@ -37,15 +37,6 @@ router.get( asyncHandler(async (req, res) => { const item = await UsersService.readUsers(res.locals.query); - ActivityService.createActivity({ - action: ActivityService.Action.UPDATE, - collection: req.collection, - item: item.id, - ip: req.ip, - user_agent: req.get('user-agent'), - action_by: req.user, - }); - return res.json({ data: item }); }) ); @@ -56,8 +47,8 @@ router.get( sanitizeQuery, validateQuery, asyncHandler(async (req, res) => { - const record = await UsersService.readUser(req.params.pk, res.locals.query); - return res.json({ data: record }); + const items = await UsersService.readUser(req.params.pk, res.locals.query); + return res.json({ data: items }); }) ); @@ -65,8 +56,18 @@ router.patch( '/:pk', useCollection('directus_users'), asyncHandler(async (req, res) => { - const records = await UsersService.updateUser(req.params.pk, req.body, res.locals.query); - return res.json({ data: records }); + const item = await UsersService.updateUser(req.params.pk, req.body, res.locals.query); + + ActivityService.createActivity({ + action: ActivityService.Action.UPDATE, + collection: req.collection, + item: item.id, + ip: req.ip, + user_agent: req.get('user-agent'), + action_by: req.user, + }); + + return res.json({ data: item }); }) ); diff --git a/src/routes/webhooks.ts b/src/routes/webhooks.ts index 4c93138906..6689630378 100644 --- a/src/routes/webhooks.ts +++ b/src/routes/webhooks.ts @@ -12,7 +12,7 @@ router.post( '/', useCollection('directus_webhooks'), asyncHandler(async (req, res) => { - const item = await WebhooksService.createWebhook(req.body, req.query); + const item = await WebhooksService.createWebhook(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 WebhooksService.readWebhooks(req.query); + const records = await WebhooksService.readWebhooks(req.sanitizedQuery); return res.json({ data: records }); }) ); @@ -44,7 +44,7 @@ router.get( sanitizeQuery, validateQuery, asyncHandler(async (req, res) => { - const record = await WebhooksService.readWebhook(req.params.pk, req.query); + const record = await WebhooksService.readWebhook(req.params.pk, req.sanitizedQuery); return res.json({ data: record }); }) ); @@ -53,7 +53,11 @@ router.patch( '/:pk', useCollection('directus_webhooks'), asyncHandler(async (req, res) => { - const item = await WebhooksService.updateWebhook(req.params.pk, req.body, req.query); + const item = await WebhooksService.updateWebhook( + 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 25ba488f04..79333bb4bb 100644 --- a/src/services/items.ts +++ b/src/services/items.ts @@ -10,7 +10,10 @@ export const createItem = async ( return readItem(collection, result[0], query); }; -export const readItems = async (collection: string, query: Query = {}) => { +export const readItems = async >( + collection: string, + query: Query = {} +): Promise => { const dbQuery = database.select(query?.fields || '*').from(collection); if (query.sort) { diff --git a/src/types/collection.ts b/src/types/collection.ts new file mode 100644 index 0000000000..929320fd72 --- /dev/null +++ b/src/types/collection.ts @@ -0,0 +1,8 @@ +export type Collection = { + collection: string; + note: string | null; + hidden: boolean; + single: boolean; + icon: string | null; + translation: Record; +}; diff --git a/src/types/express.d.ts b/src/types/express.d.ts index 392b1aabd4..01ba743a4e 100644 --- a/src/types/express.d.ts +++ b/src/types/express.d.ts @@ -11,6 +11,7 @@ declare global { user?: string; role?: string; collection?: string; + sanitizedQuery?: Record; } } } From d52f1e57587366af5e3f7f750d2090736a8123a1 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Tue, 30 Jun 2020 14:49:57 -0400 Subject: [PATCH 02/16] Move collection stuff into service --- src/constants.ts | 1 + src/routes/collections.ts | 26 ++------------------------ src/services/collections.ts | 29 +++++++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 24 deletions(-) create mode 100644 src/services/collections.ts diff --git a/src/constants.ts b/src/constants.ts index 8623b7408b..7f3762367f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,4 +1,5 @@ import { Transformation } from './types/assets'; +import { Collection } from './types/collection'; export const SYSTEM_ASSET_WHITELIST: Transformation[] = [ { diff --git a/src/routes/collections.ts b/src/routes/collections.ts index 6212d2fdf9..9a5fdf3b2c 100644 --- a/src/routes/collections.ts +++ b/src/routes/collections.ts @@ -1,11 +1,8 @@ import { Router } from 'express'; import asyncHandler from 'express-async-handler'; -import database, { schemaInspector } from '../database'; -import * as ItemsService from '../services/items'; import sanitizeQuery from '../middleware/sanitize-query'; import validateQuery from '../middleware/validate-query'; -import { Table } from '../../../../knex-schema-inspector/lib/types/table'; -import { Collection } from '../types/collection'; +import * as CollectionsService from '../services/collections'; const router = Router(); @@ -14,26 +11,7 @@ router.get( sanitizeQuery, validateQuery, asyncHandler(async (req, res) => { - const [tables, collections] = await Promise.all([ - schemaInspector.tables(), - ItemsService.readItems('directus_collections', req.sanitizedQuery), - ]); - - const data = (tables as Table[]).map((table) => { - const collectionInfo = collections.find((collection) => { - return collection.collection === table.name; - }); - - return { - collection: table.name, - note: table.comment, - hidden: collectionInfo?.hidden || false, - single: collectionInfo?.single || false, - icon: collectionInfo?.icon || null, - translation: collectionInfo?.translation || null, - }; - }); - + const data = await CollectionsService.readAll(req.sanitizedQuery); res.json({ data }); }) ); diff --git a/src/services/collections.ts b/src/services/collections.ts new file mode 100644 index 0000000000..bbe4cbe88e --- /dev/null +++ b/src/services/collections.ts @@ -0,0 +1,29 @@ +import { schemaInspector } from '../database'; +import * as ItemsService from '../services/items'; +import { Table } from '../../../../knex-schema-inspector/lib/types/table'; +import { Collection } from '../types/collection'; +import { Query } from '../types/query'; + +export const readAll = async (query?: Query) => { + const [tables, collections] = await Promise.all([ + schemaInspector.tables(), + ItemsService.readItems('directus_collections', query), + ]); + + const data = (tables as Table[]).map((table) => { + const collectionInfo = collections.find((collection) => { + return collection.collection === table.name; + }); + + return { + collection: table.name, + note: table.comment, + hidden: collectionInfo?.hidden || false, + single: collectionInfo?.single || false, + icon: collectionInfo?.icon || null, + translation: collectionInfo?.translation || null, + }; + }); + + return data; +}; From 0f8daaac79ca26659b898cbcd28e2647bf1b0115 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Tue, 30 Jun 2020 15:03:18 -0400 Subject: [PATCH 03/16] Add read single --- src/exceptions/collection-not-found.ts | 7 +++++++ src/exceptions/index.ts | 1 + src/routes/collections.ts | 16 ++++++++++++++++ src/services/collections.ts | 16 ++++++++++++++++ src/services/items.ts | 12 ++++++------ 5 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 src/exceptions/collection-not-found.ts diff --git a/src/exceptions/collection-not-found.ts b/src/exceptions/collection-not-found.ts new file mode 100644 index 0000000000..434ed097c9 --- /dev/null +++ b/src/exceptions/collection-not-found.ts @@ -0,0 +1,7 @@ +import { BaseException } from './base'; + +export class CollectionNotFoundException extends BaseException { + constructor(collection: string) { + super(`Collection "${collection}" doesn't exist.`, 404, 'COLLECTION_NOT_FOUND'); + } +} diff --git a/src/exceptions/index.ts b/src/exceptions/index.ts index 73cc014255..c35145cc91 100644 --- a/src/exceptions/index.ts +++ b/src/exceptions/index.ts @@ -5,3 +5,4 @@ export * from './invalid-query'; export * from './item-limit'; export * from './route-not-found'; export * from './item-not-found'; +export * from './collection-not-found'; diff --git a/src/routes/collections.ts b/src/routes/collections.ts index 9a5fdf3b2c..07a934d426 100644 --- a/src/routes/collections.ts +++ b/src/routes/collections.ts @@ -3,6 +3,8 @@ import asyncHandler from 'express-async-handler'; import sanitizeQuery from '../middleware/sanitize-query'; import validateQuery from '../middleware/validate-query'; import * as CollectionsService from '../services/collections'; +import { schemaInspector } from '../database'; +import { CollectionNotFoundException } from '../exceptions'; const router = Router(); @@ -16,4 +18,18 @@ router.get( }) ); +router.get( + '/:collection', + sanitizeQuery, + validateQuery, + asyncHandler(async (req, res) => { + const exists = await schemaInspector.hasTable(req.params.collection); + + if (exists === false) throw new CollectionNotFoundException(req.params.collection); + + const data = await CollectionsService.readOne(req.params.collection, req.sanitizedQuery); + res.json({ data }); + }) +); + export default router; diff --git a/src/services/collections.ts b/src/services/collections.ts index bbe4cbe88e..669a50f621 100644 --- a/src/services/collections.ts +++ b/src/services/collections.ts @@ -27,3 +27,19 @@ export const readAll = async (query?: Query) => { return data; }; + +export const readOne = async (collection: string, query?: Query) => { + const [table, collectionInfo] = await Promise.all([ + schemaInspector.table(collection), + ItemsService.readItem('directus_collections', collection, query), + ]); + + return { + collection: table.name, + note: table.comment, + hidden: collectionInfo?.hidden || false, + single: collectionInfo?.single || false, + icon: collectionInfo?.icon || null, + translation: collectionInfo?.translation || null, + }; +}; diff --git a/src/services/items.ts b/src/services/items.ts index 79333bb4bb..6a74f448d8 100644 --- a/src/services/items.ts +++ b/src/services/items.ts @@ -65,12 +65,12 @@ export const readItems = async >( return records; }; -export const readItem = async (collection: string, pk: number | string, query: Query = {}) => { - const dbQuery = database.select('*').from(collection).where({ id: pk }); - - const records = await dbQuery; - - return records[0]; +export const readItem = async ( + collection: string, + pk: number | string, + query: Query = {} +): Promise => { + return await database.select('*').from(collection).where({ id: pk }).first(); }; export const updateItem = async ( From 6f264eb9a6c75d17d14c023f98d544446bda27c2 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Tue, 30 Jun 2020 17:05:54 -0400 Subject: [PATCH 04/16] Add knex-schema-inspector as submodule (temp) --- .gitmodules | 3 +++ src/knex-schema-inspector | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 src/knex-schema-inspector diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..016f428f1c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "src/knex-schema-inspector"] + path = src/knex-schema-inspector + url = git@github.com:knex/knex-schema-inspector.git diff --git a/src/knex-schema-inspector b/src/knex-schema-inspector new file mode 160000 index 0000000000..24b576fb22 --- /dev/null +++ b/src/knex-schema-inspector @@ -0,0 +1 @@ +Subproject commit 24b576fb22745d9101f33a9ab6cbf2d61ba991fd From 67202260f78fb0dbe00b84cc52ac199e3f747f82 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Tue, 30 Jun 2020 17:06:01 -0400 Subject: [PATCH 05/16] Start on collections endpoint --- .gitignore | 2 ++ package-lock.json | 4 ---- package.json | 1 - src/database.ts | 2 +- src/services/collections.ts | 2 +- src/services/items.ts | 20 ++++++++++++++++---- 6 files changed, 20 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 0404d60d02..3e09d15a11 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.DS_Store + # Logs logs *.log diff --git a/package-lock.json b/package-lock.json index 8172c8e812..081ed76f3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3668,10 +3668,6 @@ } } }, - "knex-schema-inspector": { - "version": "github:knex/knex-schema-inspector#b01d5ff067e0f49b9a6b48e830f016a0bf10d315", - "from": "github:knex/knex-schema-inspector" - }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", diff --git a/package.json b/package.json index 6b002590fe..426bff6a7f 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,6 @@ "icc": "^2.0.0", "jsonwebtoken": "^8.5.1", "knex": "^0.21.1", - "knex-schema-inspector": "github:knex/knex-schema-inspector", "liquidjs": "^9.12.0", "lodash": "^4.17.15", "mssql": "^6.2.0", diff --git a/src/database.ts b/src/database.ts index 8f7ea4e148..ec61552544 100644 --- a/src/database.ts +++ b/src/database.ts @@ -1,7 +1,7 @@ import knex from 'knex'; 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/services/collections.ts b/src/services/collections.ts index 669a50f621..19a5648c69 100644 --- a/src/services/collections.ts +++ b/src/services/collections.ts @@ -1,6 +1,6 @@ import { schemaInspector } from '../database'; import * as ItemsService from '../services/items'; -import { Table } from '../../../../knex-schema-inspector/lib/types/table'; +import { Table } from '../knex-schema-inspector/lib/types/table'; import { Collection } from '../types/collection'; import { Query } from '../types/query'; diff --git a/src/services/items.ts b/src/services/items.ts index 6a74f448d8..6b8546190b 100644 --- a/src/services/items.ts +++ b/src/services/items.ts @@ -1,4 +1,4 @@ -import database from '../database'; +import database, { schemaInspector } from '../database'; import { Query } from '../types/query'; export const createItem = async ( @@ -70,7 +70,12 @@ export const readItem = async ( pk: number | string, query: Query = {} ): Promise => { - return await database.select('*').from(collection).where({ id: pk }).first(); + const primaryKeyField = await schemaInspector.primary(collection); + return await database + .select('*') + .from(collection) + .where({ [primaryKeyField]: pk }) + .first(); }; export const updateItem = async ( @@ -79,10 +84,17 @@ export const updateItem = async ( data: Record, query: Query = {} ) => { - const result = await database(collection).update(data).where({ id: pk }).returning('id'); + const primaryKeyField = await schemaInspector.primary(collection); + const result = await database(collection) + .update(data) + .where({ [primaryKeyField]: pk }) + .returning('id'); return readItem(collection, result[0], query); }; export const deleteItem = async (collection: string, pk: number | string) => { - return await database(collection).delete().where({ id: pk }); + const primaryKeyField = await schemaInspector.primary(collection); + return await database(collection) + .delete() + .where({ [primaryKeyField]: pk }); }; From ebf38ee6bc8e6f48c4c77fc3c08f60f32d674672 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Tue, 30 Jun 2020 17:15:29 -0400 Subject: [PATCH 06/16] Update schema inspector --- src/knex-schema-inspector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/knex-schema-inspector b/src/knex-schema-inspector index 24b576fb22..4c4c93c7bf 160000 --- a/src/knex-schema-inspector +++ b/src/knex-schema-inspector @@ -1 +1 @@ -Subproject commit 24b576fb22745d9101f33a9ab6cbf2d61ba991fd +Subproject commit 4c4c93c7bf83a1553940ac5a509a11a72b762837 From 0e5e2db084697ce6cd6238680faddd36b007c1ef Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Tue, 30 Jun 2020 17:53:54 -0400 Subject: [PATCH 07/16] Rough in collection creation --- src/routes/collections.ts | 28 +++++++++++++++++++++++++- src/services/collections.ts | 40 ++++++++++++++++++++++++++++++++++++- src/services/items.ts | 3 ++- 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/src/routes/collections.ts b/src/routes/collections.ts index 07a934d426..dabfe77961 100644 --- a/src/routes/collections.ts +++ b/src/routes/collections.ts @@ -4,10 +4,36 @@ import sanitizeQuery from '../middleware/sanitize-query'; import validateQuery from '../middleware/validate-query'; import * as CollectionsService from '../services/collections'; import { schemaInspector } from '../database'; -import { CollectionNotFoundException } from '../exceptions'; +import { InvalidPayloadException, CollectionNotFoundException } from '../exceptions'; +import Joi from '@hapi/joi'; const router = Router(); +const fieldSchema = Joi.object({ + field: Joi.string().required(), + datatype: Joi.string().required(), + note: Joi.string().required(), + primary_key: Joi.boolean(), + auto_increment: Joi.boolean(), +}); + +const collectionSchema = Joi.object({ + collection: Joi.string().required(), + fields: Joi.array().items(fieldSchema).min(1).unique().required(), + note: Joi.string(), +}); + +router.post( + '/', + asyncHandler(async (req, res) => { + const { error } = collectionSchema.validate(req.body); + if (error) throw new InvalidPayloadException(error.message); + + const createdCollection = await CollectionsService.create(req.body); + res.json({ data: createdCollection }); + }) +); + router.get( '/', sanitizeQuery, diff --git a/src/services/collections.ts b/src/services/collections.ts index 19a5648c69..59b7a9129f 100644 --- a/src/services/collections.ts +++ b/src/services/collections.ts @@ -1,8 +1,46 @@ -import { schemaInspector } from '../database'; +import database, { schemaInspector } from '../database'; import * as ItemsService from '../services/items'; import { Table } from '../knex-schema-inspector/lib/types/table'; import { Collection } from '../types/collection'; import { Query } from '../types/query'; +import { ColumnBuilder } from 'knex'; + +/** @Todo properly type this */ +export const create = async (payload: any) => { + await database.schema.createTable(payload.collection, (table) => { + if (payload.note) { + table.comment(payload.note); + } + + payload.fields?.forEach((field: any) => { + let column: ColumnBuilder; + + if (field.auto_increment) { + column = table.increments(field.field); + } else { + column = table.specificType(field.field, field.datatype); + + // increments() also sets primary key + if (field.primary_key) { + column.primary(); + } + } + + if (field.note) { + column.comment(field.note); + } + }); + }); + + return await ItemsService.createItem('directus_collections', { + collection: payload.collection, + hidden: payload.hidden || false, + single: payload.single || false, + icon: payload.icon || null, + note: payload.note || null, + translation: payload.translation || null, + }); +}; export const readAll = async (query?: Query) => { const [tables, collections] = await Promise.all([ diff --git a/src/services/items.ts b/src/services/items.ts index 6b8546190b..b3c5ce9efe 100644 --- a/src/services/items.ts +++ b/src/services/items.ts @@ -6,7 +6,8 @@ export const createItem = async ( data: Record, query: Query = {} ) => { - const result = await database(collection).insert(data).returning('id'); + const primaryKeyField = await schemaInspector.primary(collection); + const result = await database(collection).insert(data).returning(primaryKeyField); return readItem(collection, result[0], query); }; From 49e78d064d60a14826ef163be2899a10307a8798 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Tue, 30 Jun 2020 18:08:03 -0400 Subject: [PATCH 08/16] Add delete collection --- src/routes/collections.ts | 14 +++++++++++++- src/services/collections.ts | 19 ++++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/routes/collections.ts b/src/routes/collections.ts index dabfe77961..41dabb66df 100644 --- a/src/routes/collections.ts +++ b/src/routes/collections.ts @@ -3,7 +3,7 @@ import asyncHandler from 'express-async-handler'; import sanitizeQuery from '../middleware/sanitize-query'; import validateQuery from '../middleware/validate-query'; import * as CollectionsService from '../services/collections'; -import { schemaInspector } from '../database'; +import database, { schemaInspector } from '../database'; import { InvalidPayloadException, CollectionNotFoundException } from '../exceptions'; import Joi from '@hapi/joi'; @@ -58,4 +58,16 @@ router.get( }) ); +router.delete( + '/:collection', + asyncHandler(async (req, res) => { + if ((await schemaInspector.hasTable(req.params.collection)) === false) { + throw new CollectionNotFoundException(req.params.collection); + } + + await CollectionsService.deleteCollection(req.params.collection); + res.end(); + }) +); + export default router; diff --git a/src/services/collections.ts b/src/services/collections.ts index 59b7a9129f..057162a796 100644 --- a/src/services/collections.ts +++ b/src/services/collections.ts @@ -32,7 +32,7 @@ export const create = async (payload: any) => { }); }); - return await ItemsService.createItem('directus_collections', { + const collection = await ItemsService.createItem('directus_collections', { collection: payload.collection, hidden: payload.hidden || false, single: payload.single || false, @@ -40,6 +40,10 @@ export const create = async (payload: any) => { note: payload.note || null, translation: payload.translation || null, }); + + /** @TODO insert all fields */ + + return collection; }; export const readAll = async (query?: Query) => { @@ -81,3 +85,16 @@ export const readOne = async (collection: string, query?: Query) => { translation: collectionInfo?.translation || null, }; }; + +export const deleteCollection = async (collection: string) => { + await Promise.all([ + database.schema.dropTable(collection), + ItemsService.deleteItem('directus_collections', collection), + database.delete().from('directus_fields').where({ collection }), + database + .delete() + .from('directus_relations') + .where({ collection_many: collection }) + .orWhere({ collection_one: collection }), + ]); +}; From df219c95a955a57ee3b92b72b543a4c733b52ba7 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Tue, 30 Jun 2020 18:16:40 -0400 Subject: [PATCH 09/16] Add support for length --- src/services/collections.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/services/collections.ts b/src/services/collections.ts index 057162a796..fa06f72fdc 100644 --- a/src/services/collections.ts +++ b/src/services/collections.ts @@ -18,7 +18,10 @@ export const create = async (payload: any) => { if (field.auto_increment) { column = table.increments(field.field); } else { - column = table.specificType(field.field, field.datatype); + const datatype = field.length + ? `${field.datatype}(${field.length})` + : field.datatype; + column = table.specificType(field.field, datatype); // increments() also sets primary key if (field.primary_key) { @@ -41,7 +44,7 @@ export const create = async (payload: any) => { translation: payload.translation || null, }); - /** @TODO insert all fields */ + /** @TODO insert all fields to directus_fields */ return collection; }; From 1b1cb301710df2452201b8cafc0c45f4c7c46634 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Wed, 1 Jul 2020 11:01:01 -0400 Subject: [PATCH 10/16] Update schema inspector --- src/knex-schema-inspector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/knex-schema-inspector b/src/knex-schema-inspector index 4c4c93c7bf..55af51c05d 160000 --- a/src/knex-schema-inspector +++ b/src/knex-schema-inspector @@ -1 +1 @@ -Subproject commit 4c4c93c7bf83a1553940ac5a509a11a72b762837 +Subproject commit 55af51c05d3e80f2a511f8ef06b8920b62e19fb1 From 77a80077c9beca3c80b690394adf35c99127ebc6 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Wed, 1 Jul 2020 11:02:54 -0400 Subject: [PATCH 11/16] Update schema inspector --- src/knex-schema-inspector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/knex-schema-inspector b/src/knex-schema-inspector index 55af51c05d..3ac774cc0d 160000 --- a/src/knex-schema-inspector +++ b/src/knex-schema-inspector @@ -1 +1 @@ -Subproject commit 55af51c05d3e80f2a511f8ef06b8920b62e19fb1 +Subproject commit 3ac774cc0dcac5b76abbf15e8bbc7bc4df9c3e0b From 1e07ee892190c76471312fbc33f98fd8e8f69928 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Wed, 1 Jul 2020 11:43:01 -0400 Subject: [PATCH 12/16] Fix method overloads --- src/knex-schema-inspector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/knex-schema-inspector b/src/knex-schema-inspector index 3ac774cc0d..5b4c08cc91 160000 --- a/src/knex-schema-inspector +++ b/src/knex-schema-inspector @@ -1 +1 @@ -Subproject commit 3ac774cc0dcac5b76abbf15e8bbc7bc4df9c3e0b +Subproject commit 5b4c08cc91dfa06c0cce9190e50403a01def5437 From db90fddbcd84b30bd462127a00ab6827ad92dbc4 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Wed, 1 Jul 2020 12:13:28 -0400 Subject: [PATCH 13/16] Fix types in inspector --- src/knex-schema-inspector | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/knex-schema-inspector b/src/knex-schema-inspector index 5b4c08cc91..5b60c08c32 160000 --- a/src/knex-schema-inspector +++ b/src/knex-schema-inspector @@ -1 +1 @@ -Subproject commit 5b4c08cc91dfa06c0cce9190e50403a01def5437 +Subproject commit 5b60c08c32493095fbcd51701a9470ac4ddd4d2f From 2680092dc95ce3448d3593c15c25e339392895f6 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Wed, 1 Jul 2020 12:35:22 -0400 Subject: [PATCH 14/16] Finish create collection --- src/services/collections.ts | 22 +++++++++++++++++----- src/services/fields.ts | 0 src/services/schema.ts | 2 ++ 3 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 src/services/fields.ts diff --git a/src/services/collections.ts b/src/services/collections.ts index fa06f72fdc..5ec4d5914e 100644 --- a/src/services/collections.ts +++ b/src/services/collections.ts @@ -1,6 +1,5 @@ import database, { schemaInspector } from '../database'; import * as ItemsService from '../services/items'; -import { Table } from '../knex-schema-inspector/lib/types/table'; import { Collection } from '../types/collection'; import { Query } from '../types/query'; import { ColumnBuilder } from 'knex'; @@ -44,18 +43,31 @@ export const create = async (payload: any) => { translation: payload.translation || null, }); - /** @TODO insert all fields to directus_fields */ + /** + * @TODO make this flexible and based on payload + */ + await database('directus_fields').insert( + payload.fields.map((field: any) => ({ + collection: payload.collection, + field: field.field, + locked: false, + required: false, + readonly: false, + hidden_detail: false, + hidden_browse: false, + })) + ); return collection; }; export const readAll = async (query?: Query) => { const [tables, collections] = await Promise.all([ - schemaInspector.tables(), + schemaInspector.tableInfo(), ItemsService.readItems('directus_collections', query), ]); - const data = (tables as Table[]).map((table) => { + const data = tables.map((table) => { const collectionInfo = collections.find((collection) => { return collection.collection === table.name; }); @@ -75,7 +87,7 @@ export const readAll = async (query?: Query) => { export const readOne = async (collection: string, query?: Query) => { const [table, collectionInfo] = await Promise.all([ - schemaInspector.table(collection), + schemaInspector.tableInfo(collection), ItemsService.readItem('directus_collections', collection, query), ]); diff --git a/src/services/fields.ts b/src/services/fields.ts new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/services/schema.ts b/src/services/schema.ts index d5e1501547..706676ade1 100644 --- a/src/services/schema.ts +++ b/src/services/schema.ts @@ -1,5 +1,7 @@ import database from '../database'; +/** @TODO replace this with schema inspector */ + export const hasCollection = async (collection: string) => { return await database.schema.hasTable(collection); }; From 90aa74bfce55e5ce21aba2ebe09826c497f733ad Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Wed, 1 Jul 2020 13:46:45 -0400 Subject: [PATCH 15/16] Add read fields endpoint --- src/app.ts | 2 ++ src/exceptions/field-not-found.ts | 11 ++++++ src/exceptions/index.ts | 5 +-- src/knex-schema-inspector | 2 +- src/routes/fields.ts | 39 ++++++++++++++++++++ src/services/collections.ts | 2 ++ src/services/fields.ts | 59 +++++++++++++++++++++++++++++++ 7 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 src/exceptions/field-not-found.ts create mode 100644 src/routes/fields.ts diff --git a/src/app.ts b/src/app.ts index 6caf032162..97fa20ba6a 100644 --- a/src/app.ts +++ b/src/app.ts @@ -16,6 +16,7 @@ import authRouter from './routes/auth'; import collectionsRouter from './routes/collections'; import collectionPresetsRouter from './routes/collection-presets'; import extensionsRouter from './routes/extensions'; +import fieldsRouter from './routes/fields'; import filesRouter from './routes/files'; import foldersRouter from './routes/folders'; import itemsRouter from './routes/items'; @@ -43,6 +44,7 @@ const app = express() .use('/collections', collectionsRouter) .use('/collection_presets', collectionPresetsRouter) .use('/extensions', extensionsRouter) + .use('/fields', fieldsRouter) .use('/files', filesRouter) .use('/folders', foldersRouter) .use('/items', itemsRouter) diff --git a/src/exceptions/field-not-found.ts b/src/exceptions/field-not-found.ts new file mode 100644 index 0000000000..27dd3c540e --- /dev/null +++ b/src/exceptions/field-not-found.ts @@ -0,0 +1,11 @@ +import { BaseException } from './base'; + +export class FieldNotFoundException extends BaseException { + constructor(collection: string, field: string) { + super( + `Field "${field}" in collection "${collection}" doesn't exist.`, + 404, + 'FIELD_NOT_FOUND' + ); + } +} diff --git a/src/exceptions/index.ts b/src/exceptions/index.ts index c35145cc91..25619832ff 100644 --- a/src/exceptions/index.ts +++ b/src/exceptions/index.ts @@ -1,8 +1,9 @@ export * from './base'; +export * from './collection-not-found'; +export * from './field-not-found'; export * from './invalid-credentials'; export * from './invalid-payload'; export * from './invalid-query'; export * from './item-limit'; -export * from './route-not-found'; export * from './item-not-found'; -export * from './collection-not-found'; +export * from './route-not-found'; diff --git a/src/knex-schema-inspector b/src/knex-schema-inspector index 5b60c08c32..7afe3b04d1 160000 --- a/src/knex-schema-inspector +++ b/src/knex-schema-inspector @@ -1 +1 @@ -Subproject commit 5b60c08c32493095fbcd51701a9470ac4ddd4d2f +Subproject commit 7afe3b04d1bc47669292491c1befc35c5344d679 diff --git a/src/routes/fields.ts b/src/routes/fields.ts new file mode 100644 index 0000000000..4797a5d1ba --- /dev/null +++ b/src/routes/fields.ts @@ -0,0 +1,39 @@ +import { Router } from 'express'; +import asyncHandler from 'express-async-handler'; +import * as FieldsService from '../services/fields'; +import validateCollection from '../middleware/validate-collection'; +import { schemaInspector } from '../database'; +import { FieldNotFoundException } from '../exceptions'; + +const router = Router(); + +router.get( + '/', + asyncHandler(async (req, res) => { + const fields = await FieldsService.readAll(); + return res.json({ data: fields }); + }) +); + +router.get( + '/:collection', + validateCollection, + asyncHandler(async (req, res) => { + const fields = await FieldsService.readAll(req.collection); + return res.json({ data: fields }); + }) +); + +router.get( + '/:collection/:field', + validateCollection, + asyncHandler(async (req, res) => { + const exists = await schemaInspector.hasColumn(req.collection, req.params.field); + if (exists === false) throw new FieldNotFoundException(req.collection, req.params.field); + + const field = await FieldsService.readOne(req.collection, req.params.field); + return res.json({ data: field }); + }) +); + +export default router; diff --git a/src/services/collections.ts b/src/services/collections.ts index 5ec4d5914e..1acc7a8f3c 100644 --- a/src/services/collections.ts +++ b/src/services/collections.ts @@ -11,6 +11,8 @@ export const create = async (payload: any) => { table.comment(payload.note); } + /** @todo move this into fields service */ + payload.fields?.forEach((field: any) => { let column: ColumnBuilder; diff --git a/src/services/fields.ts b/src/services/fields.ts index e69de29bb2..c5c5c5415f 100644 --- a/src/services/fields.ts +++ b/src/services/fields.ts @@ -0,0 +1,59 @@ +import database, { schemaInspector } from '../database'; + +export const readAll = async (collection?: string) => { + const fieldsQuery = database.select('*').from('directus_fields'); + + if (collection) { + fieldsQuery.where({ collection }); + } + + const [columns, fields] = await Promise.all([ + schemaInspector.columnInfo(collection), + fieldsQuery, + ]); + + return columns.map((column) => { + const field = fields.find( + (field) => field.field === column.name && field.collection === column.table + ); + + /** @TODO + * return field defaults if field doesn't exist in directus_fields + */ + + const data = { + ...column, + ...field, + collection: column.table, + field: column.name, + }; + + delete data.table; + delete data.name; + + return data; + }); +}; + +export const readOne = async (collection: string, field: string) => { + const [column, fieldInfo] = await Promise.all([ + schemaInspector.columnInfo(collection, field), + database.select('*').from('directus_fields').where({ collection, field }).first(), + ]); + + /** @TODO + * return field defaults if field doesn't exist in directus_fields + */ + + const data = { + ...column, + ...fieldInfo, + collection: column.table, + field: column.name, + }; + + delete data.table; + delete data.name; + + return data; +}; From 0196496c1a3f2b93b002ba95d1c85b3031032397 Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Wed, 1 Jul 2020 14:54:19 -0400 Subject: [PATCH 16/16] Add create field --- src/middleware/validate-collection.ts | 4 +-- src/routes/fields.ts | 33 ++++++++++++++++++++- src/services/fields.ts | 41 +++++++++++++++------------ src/services/payload.ts | 9 +++--- src/types/field.ts | 11 ++++++- 5 files changed, 71 insertions(+), 27 deletions(-) diff --git a/src/middleware/validate-collection.ts b/src/middleware/validate-collection.ts index 5bd96bc445..aa63c41886 100644 --- a/src/middleware/validate-collection.ts +++ b/src/middleware/validate-collection.ts @@ -5,7 +5,7 @@ import { RequestHandler } from 'express'; import asyncHandler from 'express-async-handler'; import database from '../database'; -import { RouteNotFoundException } from '../exceptions'; +import { CollectionNotFoundException } from '../exceptions'; const validateCollection: RequestHandler = asyncHandler(async (req, res, next) => { if (!req.params.collection) return next(); @@ -17,7 +17,7 @@ const validateCollection: RequestHandler = asyncHandler(async (req, res, next) = return next(); } - throw new RouteNotFoundException(req.path); + throw new CollectionNotFoundException(req.params.collection); }); export default validateCollection; diff --git a/src/routes/fields.ts b/src/routes/fields.ts index 4797a5d1ba..c1dbb0554e 100644 --- a/src/routes/fields.ts +++ b/src/routes/fields.ts @@ -3,7 +3,9 @@ import asyncHandler from 'express-async-handler'; import * as FieldsService from '../services/fields'; import validateCollection from '../middleware/validate-collection'; import { schemaInspector } from '../database'; -import { FieldNotFoundException } from '../exceptions'; +import { FieldNotFoundException, InvalidPayloadException } from '../exceptions'; +import Joi from '@hapi/joi'; +import { Field } from '../types/field'; const router = Router(); @@ -36,4 +38,33 @@ router.get( }) ); +const newFieldSchema = Joi.object({ + field: Joi.string().required(), + database: Joi.object({ + type: Joi.string().required(), + }).required(), + system: Joi.object({ + hidden_browse: Joi.boolean(), + /** @todo extract this dynamically from the DB schema */ + }), +}); + +router.post( + '/:collection', + validateCollection, + asyncHandler(async (req, res) => { + const { error } = newFieldSchema.validate(req.body); + + if (error) { + throw new InvalidPayloadException(error.message); + } + + const field: Partial = req.body; + + const createdField = await FieldsService.createField(req.collection, field); + + res.json({ data: createdField }); + }) +); + export default router; diff --git a/src/services/fields.ts b/src/services/fields.ts index c5c5c5415f..93f44348d9 100644 --- a/src/services/fields.ts +++ b/src/services/fields.ts @@ -1,4 +1,5 @@ import database, { schemaInspector } from '../database'; +import { Field } from '../types/field'; export const readAll = async (collection?: string) => { const fieldsQuery = database.select('*').from('directus_fields'); @@ -17,20 +18,13 @@ export const readAll = async (collection?: string) => { (field) => field.field === column.name && field.collection === column.table ); - /** @TODO - * return field defaults if field doesn't exist in directus_fields - */ - const data = { - ...column, - ...field, collection: column.table, field: column.name, + database: column, + system: field || null, }; - delete data.table; - delete data.name; - return data; }); }; @@ -41,19 +35,30 @@ export const readOne = async (collection: string, field: string) => { database.select('*').from('directus_fields').where({ collection, field }).first(), ]); - /** @TODO - * return field defaults if field doesn't exist in directus_fields - */ - const data = { - ...column, - ...fieldInfo, collection: column.table, field: column.name, + database: column, + system: fieldInfo || null, }; - delete data.table; - delete data.name; - return data; }; + +export const createField = async (collection: string, field: Partial) => { + await database.schema.alterTable('articles', (table) => { + table.specificType(field.field, field.database.type); + /** @todo add support for other database info (length etc) */ + }); + + if (field.system) { + await database('directus_fields').insert({ + ...field.system, + collection: collection, + field: field.field, + }); + } + + const createdField = await readOne(collection, field.field); + return createdField; +}; diff --git a/src/services/payload.ts b/src/services/payload.ts index 17dcb969a5..cfa28f3f87 100644 --- a/src/services/payload.ts +++ b/src/services/payload.ts @@ -1,11 +1,9 @@ /** - * # PayloadService - * * Process a given payload for a collection to ensure the special fields (hash, uuid, date etc) are * handled correctly. */ -import { FieldInfo } from '../types/field'; +import { System } from '../types/field'; import argon2 from 'argon2'; import { v4 as uuidv4 } from 'uuid'; import database from '../database'; @@ -25,9 +23,10 @@ export const processValues = async ( payload: Record ) => { const processedPayload = clone(payload); + const specialFieldsInCollection = await database .select('field', 'special') - .from('directus_fields') + .from('directus_fields') .where({ collection: collection }) .whereNotNull('special'); @@ -39,7 +38,7 @@ export const processValues = async ( }; async function processField( - field: FieldInfo, + field: Pick, payload: Record, operation: 'create' | 'update' ) { diff --git a/src/types/field.ts b/src/types/field.ts index b4d37ac826..3cffd25e07 100644 --- a/src/types/field.ts +++ b/src/types/field.ts @@ -1,4 +1,6 @@ -export type FieldInfo = { +import { Column } from '../knex-schema-inspector/lib/types/column'; + +export type System = { id: number; collection: string; field: string; @@ -16,3 +18,10 @@ export type FieldInfo = { note: string | null; translation: null; }; + +export type Field = { + collection: string; + field: string; + database: Column; + system: System | null; +};