From a7d8d0fafc59be2f2cf00543c455d895cd541d1e Mon Sep 17 00:00:00 2001 From: rijkvanzanten Date: Tue, 25 May 2021 16:16:42 -0400 Subject: [PATCH] Add schema, rest/gql resolvers for insights --- api/src/app.ts | 26 ++- api/src/controllers/dashboards.ts | 184 ++++++++++++++++++ api/src/controllers/panels.ts | 184 ++++++++++++++++++ .../migrations/20210525A-add-insights.ts | 9 +- .../system-data/fields/dashboards.yaml | 2 + .../system-data/relations/relations.yaml | 4 + api/src/services/dashboards.ts | 8 + api/src/services/index.ts | 2 + api/src/services/items.ts | 2 +- api/src/services/panels.ts | 8 + 10 files changed, 415 insertions(+), 14 deletions(-) create mode 100644 api/src/controllers/dashboards.ts create mode 100644 api/src/controllers/panels.ts create mode 100644 api/src/services/dashboards.ts create mode 100644 api/src/services/panels.ts diff --git a/api/src/app.ts b/api/src/app.ts index 7a4d0c4bbc..24303d2cc4 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -5,17 +5,28 @@ import expressLogger from 'express-pino-logger'; import fse from 'fs-extra'; import path from 'path'; import qs from 'qs'; + +import { emitAsyncSafe } from './emitter'; +import { initializeExtensions, registerExtensionEndpoints, registerExtensionHooks } from './extensions'; +import { InvalidPayloadException } from './exceptions'; +import { isInstalled, validateDBConnection } from './database'; +import { register as registerWebhooks } from './webhooks'; +import env from './env'; +import logger from './logger'; + import activityRouter from './controllers/activity'; import assetsRouter from './controllers/assets'; import authRouter from './controllers/auth'; import collectionsRouter from './controllers/collections'; import extensionsRouter from './controllers/extensions'; +import dashboardsRouter from './controllers/dashboards'; import fieldsRouter from './controllers/fields'; import filesRouter from './controllers/files'; import foldersRouter from './controllers/folders'; import graphqlRouter from './controllers/graphql'; import itemsRouter from './controllers/items'; import notFoundHandler from './controllers/not-found'; +import panelsRouter from './controllers/panels'; import permissionsRouter from './controllers/permissions'; import presetsRouter from './controllers/presets'; import relationsRouter from './controllers/relations'; @@ -26,25 +37,20 @@ import settingsRouter from './controllers/settings'; import usersRouter from './controllers/users'; import utilsRouter from './controllers/utils'; import webhooksRouter from './controllers/webhooks'; -import { isInstalled, validateDBConnection } from './database'; -import { emitAsyncSafe } from './emitter'; -import env from './env'; -import { InvalidPayloadException } from './exceptions'; -import { initializeExtensions, registerExtensionEndpoints, registerExtensionHooks } from './extensions'; -import logger from './logger'; + +import { checkIP } from './middleware/check-ip'; +import { session } from './middleware/session'; import authenticate from './middleware/authenticate'; import cache from './middleware/cache'; -import { checkIP } from './middleware/check-ip'; import cors from './middleware/cors'; import errorHandler from './middleware/error-handler'; import extractToken from './middleware/extract-token'; import rateLimiter from './middleware/rate-limiter'; import sanitizeQuery from './middleware/sanitize-query'; import schema from './middleware/schema'; + import { track } from './utils/track'; import { validateEnv } from './utils/validate-env'; -import { register as registerWebhooks } from './webhooks'; -import { session } from './middleware/session'; export default async function createApp(): Promise { validateEnv(['KEY', 'SECRET']); @@ -152,11 +158,13 @@ export default async function createApp(): Promise { app.use('/activity', activityRouter); app.use('/assets', assetsRouter); app.use('/collections', collectionsRouter); + app.use('/dashboards', dashboardsRouter); app.use('/extensions', extensionsRouter); app.use('/fields', fieldsRouter); app.use('/files', filesRouter); app.use('/folders', foldersRouter); app.use('/items', itemsRouter); + app.use('/panels', panelsRouter); app.use('/permissions', permissionsRouter); app.use('/presets', presetsRouter); app.use('/relations', relationsRouter); diff --git a/api/src/controllers/dashboards.ts b/api/src/controllers/dashboards.ts new file mode 100644 index 0000000000..73c14d037e --- /dev/null +++ b/api/src/controllers/dashboards.ts @@ -0,0 +1,184 @@ +import express from 'express'; +import { ForbiddenException } from '../exceptions'; +import { respond } from '../middleware/respond'; +import useCollection from '../middleware/use-collection'; +import { validateBatch } from '../middleware/validate-batch'; +import { MetaService, DashboardsService } from '../services'; +import { PrimaryKey } from '../types'; +import asyncHandler from '../utils/async-handler'; + +const router = express.Router(); + +router.use(useCollection('directus_dashboards')); + +router.post( + '/', + asyncHandler(async (req, res, next) => { + const service = new DashboardsService({ + accountability: req.accountability, + schema: req.schema, + }); + + const savedKeys: PrimaryKey[] = []; + + if (Array.isArray(req.body)) { + const keys = await service.createMany(req.body); + savedKeys.push(...keys); + } else { + const key = await service.createOne(req.body); + savedKeys.push(key); + } + + try { + if (Array.isArray(req.body)) { + const items = await service.readMany(savedKeys, req.sanitizedQuery); + res.locals.payload = { data: items }; + } else { + const item = await service.readOne(savedKeys[0], req.sanitizedQuery); + res.locals.payload = { data: item }; + } + } catch (error) { + if (error instanceof ForbiddenException) { + return next(); + } + + throw error; + } + + return next(); + }), + respond +); + +const readHandler = asyncHandler(async (req, res, next) => { + const service = new DashboardsService({ + accountability: req.accountability, + schema: req.schema, + }); + const metaService = new MetaService({ + accountability: req.accountability, + schema: req.schema, + }); + + const records = await service.readByQuery(req.sanitizedQuery); + const meta = await metaService.getMetaForQuery(req.collection, req.sanitizedQuery); + + res.locals.payload = { data: records || null, meta }; + return next(); +}); + +router.get('/', validateBatch('read'), readHandler, respond); +router.search('/', validateBatch('read'), readHandler, respond); + +router.get( + '/:pk', + asyncHandler(async (req, res, next) => { + const service = new DashboardsService({ + accountability: req.accountability, + schema: req.schema, + }); + + const record = await service.readOne(req.params.pk, req.sanitizedQuery); + + res.locals.payload = { data: record || null }; + return next(); + }), + respond +); + +router.patch( + '/', + validateBatch('update'), + asyncHandler(async (req, res, next) => { + const service = new DashboardsService({ + accountability: req.accountability, + schema: req.schema, + }); + + let keys: PrimaryKey[] = []; + + if (req.body.keys) { + keys = await service.updateMany(req.body.keys, req.body.data); + } else { + keys = await service.updateByQuery(req.body.query, req.body.data); + } + + try { + const result = await service.readMany(keys, req.sanitizedQuery); + res.locals.payload = { data: result }; + } catch (error) { + if (error instanceof ForbiddenException) { + return next(); + } + + throw error; + } + + return next(); + }), + respond +); + +router.patch( + '/:pk', + asyncHandler(async (req, res, next) => { + const service = new DashboardsService({ + accountability: req.accountability, + schema: req.schema, + }); + + const primaryKey = await service.updateOne(req.params.pk, req.body); + + try { + const item = await service.readOne(primaryKey, req.sanitizedQuery); + res.locals.payload = { data: item || null }; + } catch (error) { + if (error instanceof ForbiddenException) { + return next(); + } + + throw error; + } + + return next(); + }), + respond +); + +router.delete( + '/', + asyncHandler(async (req, res, next) => { + const service = new DashboardsService({ + accountability: req.accountability, + schema: req.schema, + }); + + if (Array.isArray(req.body)) { + await service.deleteMany(req.body); + } else if (req.body.keys) { + await service.deleteMany(req.body.keys); + } else { + await service.deleteByQuery(req.body.query); + } + + return next(); + }), + respond +); + +router.delete( + '/:pk', + asyncHandler(async (req, res, next) => { + const service = new DashboardsService({ + accountability: req.accountability, + schema: req.schema, + }); + + await service.deleteOne(req.params.pk); + + return next(); + }), + respond +); + +export default router; diff --git a/api/src/controllers/panels.ts b/api/src/controllers/panels.ts new file mode 100644 index 0000000000..5b6a667bbe --- /dev/null +++ b/api/src/controllers/panels.ts @@ -0,0 +1,184 @@ +import express from 'express'; +import { ForbiddenException } from '../exceptions'; +import { respond } from '../middleware/respond'; +import useCollection from '../middleware/use-collection'; +import { validateBatch } from '../middleware/validate-batch'; +import { MetaService, PanelsService } from '../services'; +import { PrimaryKey } from '../types'; +import asyncHandler from '../utils/async-handler'; + +const router = express.Router(); + +router.use(useCollection('directus_panels')); + +router.post( + '/', + asyncHandler(async (req, res, next) => { + const service = new PanelsService({ + accountability: req.accountability, + schema: req.schema, + }); + + const savedKeys: PrimaryKey[] = []; + + if (Array.isArray(req.body)) { + const keys = await service.createMany(req.body); + savedKeys.push(...keys); + } else { + const key = await service.createOne(req.body); + savedKeys.push(key); + } + + try { + if (Array.isArray(req.body)) { + const items = await service.readMany(savedKeys, req.sanitizedQuery); + res.locals.payload = { data: items }; + } else { + const item = await service.readOne(savedKeys[0], req.sanitizedQuery); + res.locals.payload = { data: item }; + } + } catch (error) { + if (error instanceof ForbiddenException) { + return next(); + } + + throw error; + } + + return next(); + }), + respond +); + +const readHandler = asyncHandler(async (req, res, next) => { + const service = new PanelsService({ + accountability: req.accountability, + schema: req.schema, + }); + const metaService = new MetaService({ + accountability: req.accountability, + schema: req.schema, + }); + + const records = await service.readByQuery(req.sanitizedQuery); + const meta = await metaService.getMetaForQuery(req.collection, req.sanitizedQuery); + + res.locals.payload = { data: records || null, meta }; + return next(); +}); + +router.get('/', validateBatch('read'), readHandler, respond); +router.search('/', validateBatch('read'), readHandler, respond); + +router.get( + '/:pk', + asyncHandler(async (req, res, next) => { + const service = new PanelsService({ + accountability: req.accountability, + schema: req.schema, + }); + + const record = await service.readOne(req.params.pk, req.sanitizedQuery); + + res.locals.payload = { data: record || null }; + return next(); + }), + respond +); + +router.patch( + '/', + validateBatch('update'), + asyncHandler(async (req, res, next) => { + const service = new PanelsService({ + accountability: req.accountability, + schema: req.schema, + }); + + let keys: PrimaryKey[] = []; + + if (req.body.keys) { + keys = await service.updateMany(req.body.keys, req.body.data); + } else { + keys = await service.updateByQuery(req.body.query, req.body.data); + } + + try { + const result = await service.readMany(keys, req.sanitizedQuery); + res.locals.payload = { data: result }; + } catch (error) { + if (error instanceof ForbiddenException) { + return next(); + } + + throw error; + } + + return next(); + }), + respond +); + +router.patch( + '/:pk', + asyncHandler(async (req, res, next) => { + const service = new PanelsService({ + accountability: req.accountability, + schema: req.schema, + }); + + const primaryKey = await service.updateOne(req.params.pk, req.body); + + try { + const item = await service.readOne(primaryKey, req.sanitizedQuery); + res.locals.payload = { data: item || null }; + } catch (error) { + if (error instanceof ForbiddenException) { + return next(); + } + + throw error; + } + + return next(); + }), + respond +); + +router.delete( + '/', + asyncHandler(async (req, res, next) => { + const service = new PanelsService({ + accountability: req.accountability, + schema: req.schema, + }); + + if (Array.isArray(req.body)) { + await service.deleteMany(req.body); + } else if (req.body.keys) { + await service.deleteMany(req.body.keys); + } else { + await service.deleteByQuery(req.body.query); + } + + return next(); + }), + respond +); + +router.delete( + '/:pk', + asyncHandler(async (req, res, next) => { + const service = new PanelsService({ + accountability: req.accountability, + schema: req.schema, + }); + + await service.deleteOne(req.params.pk); + + return next(); + }), + respond +); + +export default router; diff --git a/api/src/database/migrations/20210525A-add-insights.ts b/api/src/database/migrations/20210525A-add-insights.ts index 7ea419e9b3..0dd3f4ff84 100644 --- a/api/src/database/migrations/20210525A-add-insights.ts +++ b/api/src/database/migrations/20210525A-add-insights.ts @@ -5,12 +5,13 @@ export async function up(knex: Knex): Promise { table.uuid('id').primary(); table.string('name'); table.string('icon', 30); - table.timestamp('date_created'); - table.timestamp('user_created'); + table.timestamp('date_created').defaultTo(knex.fn.now()); + table.uuid('user_created').references('id').inTable('directus_users').onDelete('SET NULL'); }); await knex.schema.createTable('directus_panels', (table) => { table.uuid('id').primary(); + table.uuid('dashboard').notNullable().references('id').inTable('directus_dashboards').onDelete('CASCADE'); table.string('name'); table.string('icon', 30); table.string('color', 10); @@ -21,8 +22,8 @@ export async function up(knex: Knex): Promise { table.integer('width'); table.integer('height'); table.json('options'); - table.timestamp('date_created'); - table.timestamp('user_created'); + table.timestamp('date_created').defaultTo(knex.fn.now()); + table.uuid('user_created').references('id').inTable('directus_users').onDelete('SET NULL'); }); } diff --git a/api/src/database/system-data/fields/dashboards.yaml b/api/src/database/system-data/fields/dashboards.yaml index d7a6b55eda..dabca3bee8 100644 --- a/api/src/database/system-data/fields/dashboards.yaml +++ b/api/src/database/system-data/fields/dashboards.yaml @@ -5,6 +5,8 @@ fields: special: uuid - field: name - field: icon + - field: panels + special: o2m - field: date_created special: date-created - field: user_created diff --git a/api/src/database/system-data/relations/relations.yaml b/api/src/database/system-data/relations/relations.yaml index 592a248288..41d14cbb68 100644 --- a/api/src/database/system-data/relations/relations.yaml +++ b/api/src/database/system-data/relations/relations.yaml @@ -54,3 +54,7 @@ data: - many_collection: directus_settings many_field: public_background one_collection: directus_files + - many_collection: directus_panels + many_field: dashboard + one_collection: directus_dashboards + one_field: panels diff --git a/api/src/services/dashboards.ts b/api/src/services/dashboards.ts new file mode 100644 index 0000000000..828f533fbf --- /dev/null +++ b/api/src/services/dashboards.ts @@ -0,0 +1,8 @@ +import { AbstractServiceOptions } from '../types'; +import { ItemsService } from './items'; + +export class DashboardsService extends ItemsService { + constructor(options: AbstractServiceOptions) { + super('directus_dashboards', options); + } +} diff --git a/api/src/services/index.ts b/api/src/services/index.ts index ef1276c358..9fb3b6b78b 100644 --- a/api/src/services/index.ts +++ b/api/src/services/index.ts @@ -2,6 +2,7 @@ export * from './activity'; export * from './assets'; export * from './authentication'; export * from './collections'; +export * from './dashboards'; export * from './fields'; export * from './files'; export * from './folders'; @@ -10,6 +11,7 @@ export * from './import'; export * from './items'; export * from './mail'; export * from './meta'; +export * from './panels'; export * from './payload'; export * from './permissions'; export * from './presets'; diff --git a/api/src/services/items.ts b/api/src/services/items.ts index 2b6f4dbe33..957edab94e 100644 --- a/api/src/services/items.ts +++ b/api/src/services/items.ts @@ -130,7 +130,7 @@ export class ItemsService implements AbstractSer let primaryKey = payloadWithTypeCasting[primaryKeyField]; try { - await trx.insert(payloadWithoutAliases).into(this.collection); + await trx.insert(payloadWithTypeCasting).into(this.collection); } catch (err) { throw await translateDatabaseError(err); } diff --git a/api/src/services/panels.ts b/api/src/services/panels.ts new file mode 100644 index 0000000000..8c0a7ff141 --- /dev/null +++ b/api/src/services/panels.ts @@ -0,0 +1,8 @@ +import { AbstractServiceOptions } from '../types'; +import { ItemsService } from './items'; + +export class PanelsService extends ItemsService { + constructor(options: AbstractServiceOptions) { + super('directus_panels', options); + } +}