mirror of
https://github.com/directus/directus.git
synced 2026-01-31 02:57:56 -05:00
5
api/package-lock.json
generated
5
api/package-lock.json
generated
@@ -27875,6 +27875,11 @@
|
||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="
|
||||
},
|
||||
"eventemitter2": {
|
||||
"version": "6.4.3",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.3.tgz",
|
||||
"integrity": "sha512-t0A2msp6BzOf+QAcI6z9XMktLj52OjGQg+8SJH6v5+3uxNpWYRR3wQmfA+6xtMU9kOC59qk9licus5dYcrYkMQ=="
|
||||
},
|
||||
"events": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
|
||||
|
||||
@@ -80,6 +80,7 @@
|
||||
"cookie-parser": "^1.4.5",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^8.2.0",
|
||||
"eventemitter2": "^6.4.3",
|
||||
"execa": "^4.0.3",
|
||||
"exif-reader": "^1.0.3",
|
||||
"express": "^4.17.1",
|
||||
|
||||
@@ -36,10 +36,12 @@ import webhooksRouter from './controllers/webhooks';
|
||||
|
||||
import notFoundHandler from './controllers/not-found';
|
||||
import sanitizeQuery from './middleware/sanitize-query';
|
||||
import WebhooksService from './services/webhooks';
|
||||
|
||||
validateEnv(['KEY', 'SECRET']);
|
||||
|
||||
const app = express();
|
||||
|
||||
app.disable('x-powered-by');
|
||||
app.set('trust proxy', true);
|
||||
|
||||
@@ -99,4 +101,8 @@ app.use(respond);
|
||||
app.use(notFoundHandler);
|
||||
app.use(errorHandler);
|
||||
|
||||
// Register all webhooks
|
||||
const webhooksService = new WebhooksService();
|
||||
webhooksService.register();
|
||||
|
||||
export default app;
|
||||
|
||||
@@ -14,7 +14,7 @@ router.post(
|
||||
|
||||
res.locals.payload = { data: record || null };
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
@@ -28,39 +28,42 @@ router.get(
|
||||
|
||||
res.locals.payload = { data: records || null, meta };
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new FoldersService({ accountability: req.accountability });
|
||||
const record = await service.readByKey(req.params.pk, req.sanitizedQuery);
|
||||
const primaryKey = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const record = await service.readByKey(primaryKey as any, req.sanitizedQuery);
|
||||
|
||||
res.locals.payload = { data: record || null };
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new FoldersService({ accountability: req.accountability });
|
||||
const primaryKey = await service.update(req.body, req.params.pk);
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const primaryKey = await service.update(req.body, pk as any);
|
||||
const record = await service.readByKey(primaryKey, req.sanitizedQuery);
|
||||
|
||||
res.locals.payload = { data: record || null };
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new FoldersService({ accountability: req.accountability });
|
||||
await service.delete(req.params.pk);
|
||||
const primaryKey = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
await service.delete(primaryKey as any);
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -16,7 +16,7 @@ router.post(
|
||||
|
||||
res.locals.payload = { data: item || null };
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
@@ -30,7 +30,7 @@ router.get(
|
||||
|
||||
res.locals.payload = { data: item || null, meta };
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
@@ -54,7 +54,7 @@ router.get(
|
||||
|
||||
res.locals.payload = { data: items || null };
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
@@ -62,33 +62,35 @@ router.get(
|
||||
asyncHandler(async (req, res, next) => {
|
||||
if (req.path.endsWith('me')) return next();
|
||||
const service = new PermissionsService({ accountability: req.accountability });
|
||||
const record = await service.readByKey(Number(req.params.pk), req.sanitizedQuery);
|
||||
const primaryKey = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const record = await service.readByKey(primaryKey as any, req.sanitizedQuery);
|
||||
|
||||
res.locals.payload = { data: record || null };
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new PermissionsService({ accountability: req.accountability });
|
||||
const primaryKey = await service.update(req.body, Number(req.params.pk));
|
||||
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const primaryKey = await service.update(req.body, pk as any);
|
||||
const item = await service.readByKey(primaryKey, req.sanitizedQuery);
|
||||
|
||||
res.locals.payload = { data: item || null };
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new PermissionsService({ accountability: req.accountability });
|
||||
await service.delete(Number(req.params.pk));
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
await service.delete(pk as any);
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -35,7 +35,8 @@ router.get(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new PresetsService({ accountability: req.accountability });
|
||||
const record = await service.readByKey(req.params.pk, req.sanitizedQuery);
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const record = await service.readByKey(pk as any, req.sanitizedQuery);
|
||||
|
||||
res.locals.payload = { data: record || null };
|
||||
return next();
|
||||
@@ -46,7 +47,8 @@ router.patch(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new PresetsService({ accountability: req.accountability });
|
||||
const primaryKey = await service.update(req.body, req.params.pk);
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const primaryKey = await service.update(req.body, pk as any);
|
||||
const record = await service.readByKey(primaryKey, req.sanitizedQuery);
|
||||
|
||||
res.locals.payload = { data: record || null };
|
||||
@@ -58,7 +60,8 @@ router.delete(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new PresetsService({ accountability: req.accountability });
|
||||
await service.delete(req.params.pk);
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
await service.delete(pk as any);
|
||||
return next();
|
||||
})
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@ router.post(
|
||||
const item = await service.readByKey(primaryKey, req.sanitizedQuery);
|
||||
res.locals.payload = { data: item || null };
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
@@ -27,37 +27,40 @@ router.get(
|
||||
|
||||
res.locals.payload = { data: records || null, meta };
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new RelationsService({ accountability: req.accountability });
|
||||
const record = await service.readByKey(req.params.pk, req.sanitizedQuery);
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const record = await service.readByKey(pk as any, req.sanitizedQuery);
|
||||
res.locals.payload = { data: record || null };
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new RelationsService({ accountability: req.accountability });
|
||||
const primaryKey = await service.update(req.body, req.params.pk);
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const primaryKey = await service.update(req.body, pk as any);
|
||||
const item = await service.readByKey(primaryKey, req.sanitizedQuery);
|
||||
res.locals.payload = { data: item || null };
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new RelationsService({ accountability: req.accountability });
|
||||
await service.delete(Number(req.params.pk));
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
await service.delete(pk as any);
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -16,17 +16,18 @@ router.get(
|
||||
|
||||
res.locals.payload = { data: records || null, meta };
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new RevisionsService({ accountability: req.accountability });
|
||||
const record = await service.readByKey(req.params.pk, req.sanitizedQuery);
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const record = await service.readByKey(pk as any, req.sanitizedQuery);
|
||||
res.locals.payload = { data: record || null };
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -13,7 +13,7 @@ router.post(
|
||||
const item = await service.readByKey(primaryKey, req.sanitizedQuery);
|
||||
res.locals.payload = { data: item || null };
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
@@ -27,37 +27,40 @@ router.get(
|
||||
|
||||
res.locals.payload = { data: records || null, meta };
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new RolesService({ accountability: req.accountability });
|
||||
const record = await service.readByKey(req.params.pk, req.sanitizedQuery);
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const record = await service.readByKey(pk as any, req.sanitizedQuery);
|
||||
res.locals.payload = { data: record || null };
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new RolesService({ accountability: req.accountability });
|
||||
const primaryKey = await service.update(req.body, req.params.pk);
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const primaryKey = await service.update(req.body, pk as any);
|
||||
const item = await service.readByKey(primaryKey, req.sanitizedQuery);
|
||||
res.locals.payload = { data: item || null };
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new RolesService({ accountability: req.accountability });
|
||||
await service.delete(req.params.pk);
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
await service.delete(pk as any);
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -16,7 +16,7 @@ router.post(
|
||||
const item = await service.readByKey(primaryKey, req.sanitizedQuery);
|
||||
res.locals.payload = { data: item || null };
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
@@ -30,7 +30,7 @@ router.get(
|
||||
|
||||
res.locals.payload = { data: item || null, meta };
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
@@ -45,7 +45,7 @@ router.get(
|
||||
|
||||
res.locals.payload = { data: item || null };
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
@@ -53,10 +53,11 @@ router.get(
|
||||
asyncHandler(async (req, res, next) => {
|
||||
if (req.path.endsWith('me')) return next();
|
||||
const service = new UsersService({ accountability: req.accountability });
|
||||
const items = await service.readByKey(req.params.pk, req.sanitizedQuery);
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const items = await service.readByKey(pk as any, req.sanitizedQuery);
|
||||
res.locals.payload = { data: items || null };
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.patch(
|
||||
@@ -72,7 +73,7 @@ router.patch(
|
||||
|
||||
res.locals.payload = { data: item || null };
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.patch(
|
||||
@@ -97,20 +98,22 @@ router.patch(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new UsersService({ accountability: req.accountability });
|
||||
const primaryKey = await service.update(req.body, req.params.pk);
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const primaryKey = await service.update(req.body, pk as any);
|
||||
|
||||
const item = await service.readByKey(primaryKey, req.sanitizedQuery);
|
||||
|
||||
res.locals.payload = { data: item || null };
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new UsersService({ accountability: req.accountability });
|
||||
await service.delete(req.params.pk);
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
await service.delete(pk as any);
|
||||
|
||||
return next();
|
||||
})
|
||||
@@ -161,7 +164,7 @@ router.post(
|
||||
|
||||
res.locals.payload = { data: { secret, otpauth_url: url } };
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
router.post(
|
||||
|
||||
@@ -13,7 +13,8 @@ router.post(
|
||||
const item = await service.readByKey(primaryKey, req.sanitizedQuery);
|
||||
|
||||
res.locals.payload = { data: item || null };
|
||||
}),
|
||||
return next();
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
@@ -26,38 +27,44 @@ router.get(
|
||||
const meta = await metaService.getMetaForQuery(req.collection, req.sanitizedQuery);
|
||||
|
||||
res.locals.payload = { data: records || null, meta };
|
||||
}),
|
||||
return next();
|
||||
})
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new WebhooksService({ accountability: req.accountability });
|
||||
const record = await service.readByKey(req.params.pk, req.sanitizedQuery);
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const record = await service.readByKey(pk as any, req.sanitizedQuery);
|
||||
|
||||
res.locals.payload = { data: record || null };
|
||||
}),
|
||||
return next();
|
||||
})
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new WebhooksService({ accountability: req.accountability });
|
||||
const primaryKey = await service.update(req.body, req.params.pk);
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
const primaryKey = await service.update(req.body, pk as any);
|
||||
const item = await service.readByKey(primaryKey, req.sanitizedQuery);
|
||||
|
||||
res.locals.payload = { data: item || null };
|
||||
}),
|
||||
return next();
|
||||
})
|
||||
);
|
||||
|
||||
router.delete(
|
||||
'/:pk',
|
||||
asyncHandler(async (req, res, next) => {
|
||||
const service = new WebhooksService({ accountability: req.accountability });
|
||||
await service.delete(req.params.pk);
|
||||
const pk = req.params.pk.includes(',') ? req.params.pk.split(',') : req.params.pk;
|
||||
await service.delete(pk as any);
|
||||
|
||||
return next();
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -520,7 +520,29 @@ tables:
|
||||
directus_webhooks:
|
||||
id:
|
||||
increments: true
|
||||
# TBD
|
||||
name:
|
||||
type: string
|
||||
length: 255
|
||||
method:
|
||||
type: string
|
||||
length: 10
|
||||
default: POST
|
||||
url:
|
||||
type: string
|
||||
length: 255
|
||||
status:
|
||||
type: string
|
||||
length: 10
|
||||
default: inactive
|
||||
data:
|
||||
type: boolean
|
||||
default: false
|
||||
actions:
|
||||
type: string
|
||||
length: 100
|
||||
collections:
|
||||
type: string
|
||||
length: 255
|
||||
|
||||
rows:
|
||||
directus_collections:
|
||||
@@ -543,6 +565,7 @@ rows:
|
||||
- collection: directus_relations
|
||||
- collection: directus_revisions
|
||||
- collection: directus_roles
|
||||
- collection: directus_sessions
|
||||
- collection: directus_settings
|
||||
- collection: directus_users
|
||||
archive_field: status
|
||||
@@ -881,7 +904,7 @@ rows:
|
||||
field: collection
|
||||
type: string
|
||||
system:
|
||||
interface: collections
|
||||
interface: collection
|
||||
width: full
|
||||
special: json
|
||||
sort: 10
|
||||
@@ -1816,7 +1839,85 @@ rows:
|
||||
hidden: true
|
||||
locked: true
|
||||
|
||||
# directus_webhooks TBD
|
||||
- collection: directus_webhooks
|
||||
field: id
|
||||
hidden: true
|
||||
locked: true
|
||||
- collection: directus_webhooks
|
||||
field: name
|
||||
interface: text-input
|
||||
locked: true
|
||||
sort: 1
|
||||
width: full
|
||||
- collection: directus_webhooks
|
||||
field: method
|
||||
interface: dropdown
|
||||
locked: true
|
||||
options:
|
||||
choices:
|
||||
- GET
|
||||
- POST
|
||||
sort: 2
|
||||
width: half
|
||||
- collection: directus_webhooks
|
||||
field: url
|
||||
interface: text-input
|
||||
locked: true
|
||||
options:
|
||||
iconRight: link
|
||||
sort: 3
|
||||
width: half
|
||||
- collection: directus_webhooks
|
||||
field: status
|
||||
interface: dropdown
|
||||
locked: true
|
||||
options:
|
||||
choices:
|
||||
- text: Active
|
||||
value: active
|
||||
- text: Inactive
|
||||
value: inactive
|
||||
sort: 4
|
||||
width: half
|
||||
- collection: directus_webhooks
|
||||
field: data
|
||||
interface: toggle
|
||||
locked: true
|
||||
options:
|
||||
choices:
|
||||
label: Include item data in request
|
||||
sort: 5
|
||||
width: half
|
||||
- collection: directus_webhooks
|
||||
field: triggers_divider
|
||||
interface: divider
|
||||
options:
|
||||
icon: api
|
||||
title: Triggers
|
||||
color: '#2F80ED'
|
||||
special: alias
|
||||
sort: 6
|
||||
width: full
|
||||
- collection: directus_webhooks
|
||||
field: actions
|
||||
interface: checkboxes
|
||||
options:
|
||||
choices:
|
||||
- text: Create
|
||||
value: create
|
||||
- text: Update
|
||||
value: update
|
||||
- text: Delete
|
||||
value: delete
|
||||
special: csv
|
||||
sort: 7
|
||||
width: full
|
||||
- collection: directus_webhooks
|
||||
field: collections
|
||||
interface: collections
|
||||
special: csv
|
||||
sort: 8
|
||||
width: full
|
||||
|
||||
- collection: directus_activity
|
||||
field: action
|
||||
|
||||
5
api/src/emitter.ts
Normal file
5
api/src/emitter.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { EventEmitter2 } from 'eventemitter2';
|
||||
|
||||
const emitter = new EventEmitter2({ wildcard: true, verboseMemoryLeak: true });
|
||||
|
||||
export default emitter;
|
||||
@@ -234,6 +234,8 @@ export default class FieldsService {
|
||||
const type = field.type as 'float' | 'decimal';
|
||||
/** @todo add precision and scale support */
|
||||
column = table[type](field.field /* precision, scale */);
|
||||
} else if (field.type === 'csv') {
|
||||
column = table.string(field.field);
|
||||
} else {
|
||||
column = table[field.type](field.field);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from '../types';
|
||||
import Knex from 'knex';
|
||||
import cache from '../cache';
|
||||
import emitter from '../emitter';
|
||||
|
||||
import PayloadService from './payload';
|
||||
import AuthorizationService from './authorization';
|
||||
@@ -150,6 +151,13 @@ export default class ItemsService implements AbstractService {
|
||||
await cache.clear();
|
||||
}
|
||||
|
||||
emitter.emitAsync(`item.create.${this.collection}`, {
|
||||
collection: this.collection,
|
||||
item: primaryKeys,
|
||||
action: 'create',
|
||||
payload: payloads,
|
||||
});
|
||||
|
||||
return primaryKeys;
|
||||
});
|
||||
|
||||
@@ -167,6 +175,7 @@ export default class ItemsService implements AbstractService {
|
||||
}
|
||||
|
||||
const records = await runAST(ast);
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
@@ -311,6 +320,13 @@ export default class ItemsService implements AbstractService {
|
||||
await cache.clear();
|
||||
}
|
||||
|
||||
emitter.emitAsync(`item.update.${this.collection}`, {
|
||||
collection: this.collection,
|
||||
item: key,
|
||||
action: 'update',
|
||||
payload,
|
||||
});
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
@@ -373,6 +389,12 @@ export default class ItemsService implements AbstractService {
|
||||
await cache.clear();
|
||||
}
|
||||
|
||||
emitter.emitAsync(`item.delete.${this.collection}`, {
|
||||
collection: this.collection,
|
||||
item: key,
|
||||
action: 'delete',
|
||||
});
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,115 @@
|
||||
import ItemsService from './items';
|
||||
import { AbstractServiceOptions } from '../types';
|
||||
import { Item, PrimaryKey, AbstractServiceOptions } from '../types';
|
||||
import emitter from '../emitter';
|
||||
import { ListenerFn } from 'eventemitter2';
|
||||
import { Webhook } from '../types';
|
||||
import axios from 'axios';
|
||||
import logger from '../logger';
|
||||
|
||||
let registered: { event: string; handler: ListenerFn }[] = [];
|
||||
|
||||
export default class WebhooksService extends ItemsService {
|
||||
constructor(options?: AbstractServiceOptions) {
|
||||
super('directus_webhooks', options);
|
||||
}
|
||||
|
||||
async register() {
|
||||
this.unregister();
|
||||
|
||||
const webhooks = await this.knex
|
||||
.select<Webhook[]>('*')
|
||||
.from('directus_webhooks')
|
||||
.where({ status: 'active' });
|
||||
|
||||
for (const webhook of webhooks) {
|
||||
if (webhook.actions === '*') {
|
||||
if (webhook.collections === '*') {
|
||||
const event = 'item.*.*';
|
||||
const handler = this.createHandler(webhook);
|
||||
emitter.on(event, handler);
|
||||
registered.push({ event, handler });
|
||||
} else {
|
||||
for (const collection of webhook.collections.split(',')) {
|
||||
const event = `item.*.${collection}`;
|
||||
const handler = this.createHandler(webhook);
|
||||
emitter.on(event, handler);
|
||||
registered.push({ event, handler });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const action of webhook.actions.split(',')) {
|
||||
if (webhook.collections === '*') {
|
||||
const event = `item.${action}.*`;
|
||||
const handler = this.createHandler(webhook);
|
||||
emitter.on(event, handler);
|
||||
registered.push({ event, handler });
|
||||
} else {
|
||||
for (const collection of webhook.collections.split(',')) {
|
||||
const event = `item.${action}.${collection}`;
|
||||
const handler = this.createHandler(webhook);
|
||||
emitter.on(event, handler);
|
||||
registered.push({ event, handler });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unregister() {
|
||||
for (const { event, handler } of registered) {
|
||||
emitter.off(event, handler);
|
||||
}
|
||||
|
||||
registered = [];
|
||||
}
|
||||
|
||||
createHandler(webhook: Webhook): ListenerFn {
|
||||
return async (data) => {
|
||||
try {
|
||||
await axios({
|
||||
url: webhook.url,
|
||||
method: webhook.method,
|
||||
data: webhook.data ? data : null,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.warn(`Webhook "${webhook.name}" (id: ${webhook.id}) failed`);
|
||||
logger.warn(error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async create(data: Partial<Item>[]): Promise<PrimaryKey[]>;
|
||||
async create(data: Partial<Item>): Promise<PrimaryKey>;
|
||||
async create(data: Partial<Item> | Partial<Item>[]): Promise<PrimaryKey | PrimaryKey[]> {
|
||||
const result = await super.create(data);
|
||||
|
||||
await this.register();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
update(data: Partial<Item>, keys: PrimaryKey[]): Promise<PrimaryKey[]>;
|
||||
update(data: Partial<Item>, key: PrimaryKey): Promise<PrimaryKey>;
|
||||
update(data: Partial<Item>[]): Promise<PrimaryKey[]>;
|
||||
async update(
|
||||
data: Partial<Item> | Partial<Item>[],
|
||||
key?: PrimaryKey | PrimaryKey[]
|
||||
): Promise<PrimaryKey | PrimaryKey[]> {
|
||||
const result = await super.update(data, key as any);
|
||||
|
||||
await this.register();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
delete(key: PrimaryKey): Promise<PrimaryKey>;
|
||||
delete(keys: PrimaryKey[]): Promise<PrimaryKey[]>;
|
||||
async delete(key: PrimaryKey | PrimaryKey[]): Promise<PrimaryKey | PrimaryKey[]> {
|
||||
const result = await super.delete(key as any);
|
||||
|
||||
await this.register();
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,3 +12,4 @@ export * from './query';
|
||||
export * from './relation';
|
||||
export * from './services';
|
||||
export * from './sessions';
|
||||
export * from './webhooks';
|
||||
|
||||
10
api/src/types/webhooks.ts
Normal file
10
api/src/types/webhooks.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export type Webhook = {
|
||||
id: number;
|
||||
name: string;
|
||||
method: 'GET' | 'POST';
|
||||
url: string;
|
||||
status: 'active' | 'inactive';
|
||||
data: boolean;
|
||||
actions: string;
|
||||
collections: string;
|
||||
};
|
||||
30
app/src/interfaces/_system/collection/index.ts
Normal file
30
app/src/interfaces/_system/collection/index.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { defineInterface } from '@/interfaces/define';
|
||||
import InterfaceCollection from './collection.vue';
|
||||
|
||||
export default defineInterface(({ i18n }) => ({
|
||||
id: 'collection',
|
||||
name: i18n.t('interfaces.collection.collection'),
|
||||
description: i18n.t('interfaces.collection.description'),
|
||||
icon: 'featured_play_list',
|
||||
component: InterfaceCollection,
|
||||
types: ['string'],
|
||||
options: [
|
||||
{
|
||||
field: 'includeSystem',
|
||||
name: i18n.t('system'),
|
||||
type: 'boolean',
|
||||
meta: {
|
||||
width: 'half',
|
||||
interface: 'toggle',
|
||||
options: {
|
||||
label: i18n.t('interfaces.collection.include_system_collections'),
|
||||
},
|
||||
},
|
||||
schema: {
|
||||
default_value: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
system: true,
|
||||
recommendedDisplays: ['collection'],
|
||||
}));
|
||||
48
app/src/interfaces/_system/collections/collections.vue
Normal file
48
app/src/interfaces/_system/collections/collections.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<v-notice v-if="items.length === 0">
|
||||
{{ $t('no_collections') }}
|
||||
</v-notice>
|
||||
<interface-checkboxes v-else :choices="items" @input="$listeners.input" :value="value" :disabled="disabled" />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, computed } from '@vue/composition-api';
|
||||
import { useCollectionsStore } from '@/stores/';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
value: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
includeSystem: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const collectionsStore = useCollectionsStore();
|
||||
|
||||
const collections = computed(() => {
|
||||
if (props.includeSystem) return collectionsStore.state.collections;
|
||||
|
||||
return collectionsStore.state.collections.filter(
|
||||
(collection) => collection.collection.startsWith('directus_') === false
|
||||
);
|
||||
});
|
||||
|
||||
const items = computed(() => {
|
||||
return collections.value.map((collection) => ({
|
||||
text: collection.name,
|
||||
value: collection.collection,
|
||||
}));
|
||||
});
|
||||
|
||||
return { items };
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -7,7 +7,7 @@ export default defineInterface(({ i18n }) => ({
|
||||
description: i18n.t('interfaces.collections.description'),
|
||||
icon: 'featured_play_list',
|
||||
component: InterfaceCollections,
|
||||
types: ['string'],
|
||||
types: ['json', 'csv'],
|
||||
options: [
|
||||
{
|
||||
field: 'includeSystem',
|
||||
@@ -25,5 +25,6 @@ export default defineInterface(({ i18n }) => ({
|
||||
},
|
||||
},
|
||||
],
|
||||
recommendedDisplays: ['collection'],
|
||||
system: true,
|
||||
recommendedDisplays: ['labels'],
|
||||
}));
|
||||
@@ -17,6 +17,7 @@
|
||||
"create_preset": "Create Preset",
|
||||
"create_role": "Create Role",
|
||||
"create_user": "Create User",
|
||||
"create_webhook": "Create Webhook",
|
||||
|
||||
"rename_folder": "Rename Folder",
|
||||
"delete_folder": "Delete Folder",
|
||||
@@ -459,6 +460,9 @@
|
||||
"user_count": "No Users | One User | {count} Users",
|
||||
"no_users_copy": "There are no users in this role yet.",
|
||||
|
||||
"webhooks_count": "No Webhooks | One Webhook | {count} Webhooks",
|
||||
"no_webhooks_copy": "There are no webhooks yet.",
|
||||
|
||||
"all_items": "All Items",
|
||||
|
||||
"no_collections": "No Collections",
|
||||
|
||||
@@ -14,6 +14,11 @@
|
||||
"line_number": "Line Number",
|
||||
"placeholder": "Enter code here..."
|
||||
},
|
||||
"collection": {
|
||||
"collection": "Collection",
|
||||
"description": "Select between existing collections",
|
||||
"include_system_collections": "Include System Collections"
|
||||
},
|
||||
"collections": {
|
||||
"collections": "Collections",
|
||||
"description": "Select between existing collections",
|
||||
|
||||
@@ -32,6 +32,14 @@ const checkForSystem: NavigationGuard = (to, from, next) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (to.params.collection === 'directus_webhooks') {
|
||||
if (to.params.primaryKey) {
|
||||
return next(`/settings/webhooks/${to.params.primaryKey}`);
|
||||
} else {
|
||||
return next('/settings/webhooks');
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
|
||||
@@ -12,9 +12,78 @@
|
||||
<settings-navigation />
|
||||
</template>
|
||||
|
||||
<div class="content">
|
||||
<v-notice>Pre-Release: Feature not yet available</v-notice>
|
||||
</div>
|
||||
<template #actions>
|
||||
<search-input v-model="searchQuery" />
|
||||
|
||||
<v-dialog v-model="confirmDelete" v-if="selection.length > 0">
|
||||
<template #activator="{ on }">
|
||||
<v-button rounded icon class="action-delete" @click="on">
|
||||
<v-icon name="delete" outline />
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
<v-card>
|
||||
<v-card-title>{{ $tc('batch_delete_confirm', selection.length) }}</v-card-title>
|
||||
|
||||
<v-card-actions>
|
||||
<v-button @click="confirmDelete = false" secondary>
|
||||
{{ $t('cancel') }}
|
||||
</v-button>
|
||||
<v-button @click="batchDelete" class="action-delete" :loading="deleting">
|
||||
{{ $t('delete') }}
|
||||
</v-button>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<v-button
|
||||
rounded
|
||||
icon
|
||||
class="action-batch"
|
||||
v-if="selection.length > 1"
|
||||
:to="batchLink"
|
||||
v-tooltip.bottom="$t('edit')"
|
||||
>
|
||||
<v-icon name="edit" outline />
|
||||
</v-button>
|
||||
|
||||
<v-button rounded icon :to="addNewLink" v-tooltip.bottom="$t('create_webhook')">
|
||||
<v-icon name="add" />
|
||||
</v-button>
|
||||
</template>
|
||||
|
||||
<component
|
||||
class="layout"
|
||||
ref="layoutRef"
|
||||
:is="`layout-${layout}`"
|
||||
collection="directus_webhooks"
|
||||
:selection.sync="selection"
|
||||
:layout-options.sync="layoutOptions"
|
||||
:layout-query.sync="layoutQuery"
|
||||
:filters="filters"
|
||||
:search-query="searchQuery"
|
||||
@update:filters="filters = $event"
|
||||
>
|
||||
<template #no-results>
|
||||
<v-info :title="$t('no_results')" icon="search" center>
|
||||
{{ $t('no_results_copy') }}
|
||||
|
||||
<template #append>
|
||||
<v-button @click="clearFilters">{{ $t('clear_filters') }}</v-button>
|
||||
</template>
|
||||
</v-info>
|
||||
</template>
|
||||
|
||||
<template #no-items>
|
||||
<v-info :title="$tc('webhooks_count', 0)" icon="anchor" center type="warning">
|
||||
{{ $t('no_webhooks_copy') }}
|
||||
|
||||
<template #append>
|
||||
<v-button :to="{ path: '/settings/webhooks/+' }">{{ $t('create_webhook') }}</v-button>
|
||||
</template>
|
||||
</v-info>
|
||||
</template>
|
||||
</component>
|
||||
|
||||
<template #drawer>
|
||||
<drawer-detail icon="info_outline" :title="$t('information')" close>
|
||||
@@ -27,10 +96,15 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from '@vue/composition-api';
|
||||
import { defineComponent, computed, ref } from '@vue/composition-api';
|
||||
import SettingsNavigation from '../../components/navigation.vue';
|
||||
|
||||
import LayoutDrawerDetail from '@/views/private/components/layout-drawer-detail';
|
||||
import marked from 'marked';
|
||||
import { LayoutComponent } from '@/layouts/types';
|
||||
import { usePreset } from '@/composables/use-preset';
|
||||
import { i18n } from '@/lang';
|
||||
import api from '@/api';
|
||||
import SearchInput from '@/views/private/components/search-input';
|
||||
|
||||
type Item = {
|
||||
[field: string]: any;
|
||||
@@ -38,7 +112,74 @@ type Item = {
|
||||
|
||||
export default defineComponent({
|
||||
name: 'webhooks-browse',
|
||||
components: { SettingsNavigation, LayoutDrawerDetail },
|
||||
components: { SettingsNavigation, LayoutDrawerDetail, SearchInput },
|
||||
setup(props) {
|
||||
const layoutRef = ref<LayoutComponent | null>(null);
|
||||
|
||||
const selection = ref<Item[]>([]);
|
||||
|
||||
const { layout, layoutOptions, layoutQuery, filters, searchQuery } = usePreset(ref('directus_webhooks'));
|
||||
const { addNewLink, batchLink } = useLinks();
|
||||
const { confirmDelete, deleting, batchDelete } = useBatchDelete();
|
||||
|
||||
return {
|
||||
addNewLink,
|
||||
batchDelete,
|
||||
batchLink,
|
||||
confirmDelete,
|
||||
deleting,
|
||||
filters,
|
||||
layoutRef,
|
||||
selection,
|
||||
layoutOptions,
|
||||
layoutQuery,
|
||||
layout,
|
||||
searchQuery,
|
||||
marked,
|
||||
clearFilters,
|
||||
};
|
||||
|
||||
function useBatchDelete() {
|
||||
const confirmDelete = ref(false);
|
||||
const deleting = ref(false);
|
||||
|
||||
return { confirmDelete, deleting, batchDelete };
|
||||
|
||||
async function batchDelete() {
|
||||
deleting.value = true;
|
||||
|
||||
confirmDelete.value = false;
|
||||
|
||||
const batchPrimaryKeys = selection.value;
|
||||
|
||||
await api.delete(`/webhooks/${batchPrimaryKeys}`);
|
||||
|
||||
await layoutRef.value?.refresh();
|
||||
|
||||
selection.value = [];
|
||||
deleting.value = false;
|
||||
confirmDelete.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function useLinks() {
|
||||
const addNewLink = computed<string>(() => {
|
||||
return `/settings/webhooks/+`;
|
||||
});
|
||||
|
||||
const batchLink = computed<string>(() => {
|
||||
const batchPrimaryKeys = selection.value;
|
||||
return `/settings/webhooks/${batchPrimaryKeys}`;
|
||||
});
|
||||
|
||||
return { addNewLink, batchLink };
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
filters.value = [];
|
||||
searchQuery.value = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -48,7 +189,21 @@ export default defineComponent({
|
||||
--v-button-background-color-disabled: var(--warning-25);
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: var(--content-padding);
|
||||
.action-delete {
|
||||
--v-button-background-color: var(--danger-25);
|
||||
--v-button-color: var(--danger);
|
||||
--v-button-background-color-hover: var(--danger-50);
|
||||
--v-button-color-hover: var(--danger);
|
||||
}
|
||||
|
||||
.action-batch {
|
||||
--v-button-background-color: var(--warning-25);
|
||||
--v-button-color: var(--warning);
|
||||
--v-button-background-color-hover: var(--warning-50);
|
||||
--v-button-color-hover: var(--warning);
|
||||
}
|
||||
|
||||
.layout {
|
||||
--layout-offset-top: 64px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -23,6 +23,7 @@ export const types = [
|
||||
'timestamp',
|
||||
'binary',
|
||||
'uuid',
|
||||
'csv',
|
||||
'unknown',
|
||||
] as const;
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ const defaultInterfaceMap: Record<typeof types[number], string> = {
|
||||
timestamp: 'datetime',
|
||||
uuid: 'text-input',
|
||||
unknown: 'text-input',
|
||||
csv: 'tags'
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user