mirror of
https://github.com/directus/directus.git
synced 2026-01-26 05:38:09 -05:00
Rework collections flow to include permissions/accountability
This commit is contained in:
@@ -2,8 +2,6 @@ import { Router } from 'express';
|
||||
import asyncHandler from 'express-async-handler';
|
||||
import sanitizeQuery from '../middleware/sanitize-query';
|
||||
import CollectionsService from '../services/collections';
|
||||
import { schemaInspector } from '../database';
|
||||
import { CollectionNotFoundException } from '../exceptions';
|
||||
import useCollection from '../middleware/use-collection';
|
||||
|
||||
const router = Router();
|
||||
@@ -24,10 +22,9 @@ router.post(
|
||||
router.get(
|
||||
'/',
|
||||
useCollection('directus_collections'),
|
||||
sanitizeQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
const collectionsService = new CollectionsService({ accountability: req.accountability });
|
||||
const collections = await collectionsService.readByQuery(req.sanitizedQuery);
|
||||
const collections = await collectionsService.readByQuery();
|
||||
|
||||
res.json({ data: collections || null });
|
||||
})
|
||||
@@ -38,17 +35,25 @@ router.get(
|
||||
useCollection('directus_collections'),
|
||||
sanitizeQuery,
|
||||
asyncHandler(async (req, res) => {
|
||||
/** @todo move this validation to CollectionsService methods */
|
||||
const exists = await schemaInspector.hasTable(req.params.collection);
|
||||
if (exists === false) throw new CollectionNotFoundException(req.params.collection);
|
||||
|
||||
const collectionsService = new CollectionsService({ accountability: req.accountability });
|
||||
const collectionKey = req.params.collection.includes(',')
|
||||
? req.params.collection.split(',')
|
||||
: req.params.collection;
|
||||
const collection = await collectionsService.readByKey(collectionKey as any);
|
||||
res.json({ data: collection || null });
|
||||
})
|
||||
);
|
||||
|
||||
const collection = await collectionsService.readByKey(
|
||||
req.params.collection,
|
||||
req.sanitizedQuery
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/:collection',
|
||||
useCollection('directus_collections'),
|
||||
asyncHandler(async (req, res) => {
|
||||
const collectionsService = new CollectionsService({ accountability: req.accountability });
|
||||
const collectionKey = req.params.collection.includes(',')
|
||||
? req.params.collection.split(',')
|
||||
: req.params.collection;
|
||||
await collectionsService.update(req.body, collectionKey as any);
|
||||
const collection = await collectionsService.readByKey(collectionKey as any);
|
||||
res.json({ data: collection || null });
|
||||
})
|
||||
);
|
||||
@@ -57,13 +62,11 @@ router.delete(
|
||||
'/:collection',
|
||||
useCollection('directus_collections'),
|
||||
asyncHandler(async (req, res) => {
|
||||
/** @todo move this validation to CollectionsService methods */
|
||||
if ((await schemaInspector.hasTable(req.params.collection)) === false) {
|
||||
throw new CollectionNotFoundException(req.params.collection);
|
||||
}
|
||||
|
||||
const collectionsService = new CollectionsService({ accountability: req.accountability });
|
||||
await collectionsService.delete(req.params.collection);
|
||||
const collectionKey = req.params.collection.includes(',')
|
||||
? req.params.collection.split(',')
|
||||
: req.params.collection;
|
||||
await collectionsService.delete(collectionKey as any);
|
||||
|
||||
res.end();
|
||||
})
|
||||
|
||||
@@ -1,74 +1,254 @@
|
||||
import database from '../database';
|
||||
import database, { schemaInspector } from '../database';
|
||||
import { AbstractServiceOptions, Accountability, Collection } from '../types';
|
||||
import Knex from 'knex';
|
||||
import ItemsService from '../services/items';
|
||||
import { ForbiddenException, InvalidPayloadException } from '../exceptions';
|
||||
import SchemaInspector from 'knex-schema-inspector';
|
||||
import FieldsService from '../services/fields';
|
||||
import { omit } from 'lodash';
|
||||
import ItemsService from '../services/items';
|
||||
|
||||
export default class CollectionsService extends ItemsService {
|
||||
export default class CollectionsService {
|
||||
knex: Knex;
|
||||
accountability: Accountability | null;
|
||||
itemsService: ItemsService;
|
||||
fieldsService: FieldsService;
|
||||
|
||||
constructor(options?: AbstractServiceOptions) {
|
||||
super('directus_collections', options);
|
||||
|
||||
this.knex = options?.knex || database;
|
||||
this.accountability = options?.accountability || null;
|
||||
this.itemsService = new ItemsService('directus_collections', options);
|
||||
this.fieldsService = new FieldsService(options);
|
||||
}
|
||||
|
||||
create(data: Partial<Collection>[]): Promise<string[]>;
|
||||
create(data: Partial<Collection>): Promise<string>;
|
||||
async create(data: Partial<Collection> | Partial<Collection>[]): Promise<string | string[]> {
|
||||
const payloads = Array.isArray(data) ? data : [data];
|
||||
if (this.accountability && this.accountability.admin !== true) {
|
||||
throw new ForbiddenException('Only admins can perform this action.');
|
||||
}
|
||||
|
||||
// We'll create the fields separately. We don't want them to be inserted relationally
|
||||
const payloadsWithoutFields = payloads.map((payload) => omit(payload, 'fields'));
|
||||
const payloads = (Array.isArray(data) ? data : [data]).map((collection) => {
|
||||
if (!collection.fields) collection.fields = [];
|
||||
|
||||
await this.itemsService.create(payloadsWithoutFields);
|
||||
|
||||
for (const payload of payloads) {
|
||||
// @TODO add basic validation to ensure all used fields are provided before attempting to save
|
||||
await this.knex.schema.createTable(payload.collection!, async (table) => {
|
||||
for (const field of payload.fields!) {
|
||||
await this.fieldsService.createField(payload.collection!, field, table);
|
||||
collection.fields = collection.fields.map((field) => {
|
||||
if (field.system) {
|
||||
field.system = {
|
||||
...field.system,
|
||||
field: field.field,
|
||||
collection: collection.collection!,
|
||||
};
|
||||
}
|
||||
|
||||
return field;
|
||||
});
|
||||
|
||||
return collection;
|
||||
});
|
||||
|
||||
const createdCollections: string[] = [];
|
||||
|
||||
await this.knex.transaction(async (trx) => {
|
||||
const schemaInspector = SchemaInspector(trx);
|
||||
const fieldsService = new FieldsService({ knex: trx });
|
||||
const collectionItemsService = new ItemsService('directus_collections', {
|
||||
knex: trx,
|
||||
accountability: this.accountability,
|
||||
});
|
||||
const fieldItemsService = new ItemsService('directus_fields', {
|
||||
knex: trx,
|
||||
accountability: this.accountability,
|
||||
});
|
||||
|
||||
for (const payload of payloads) {
|
||||
if (!payload.collection) {
|
||||
throw new InvalidPayloadException(`The "collection" key is required.`);
|
||||
}
|
||||
|
||||
if (await schemaInspector.hasTable(payload.collection)) {
|
||||
throw new InvalidPayloadException(
|
||||
`Collection "${payload.collection}" already exists.`
|
||||
);
|
||||
}
|
||||
|
||||
await trx.schema.createTable(payload.collection, (table) => {
|
||||
for (const field of payload.fields!) {
|
||||
fieldsService.addColumnToTable(table, field);
|
||||
}
|
||||
});
|
||||
|
||||
const collectionInfo = omit(payload, 'fields');
|
||||
await collectionItemsService.create(collectionInfo);
|
||||
const fieldPayloads = payload
|
||||
.fields!.filter((field) => field.system)
|
||||
.map((field) => field.system);
|
||||
await fieldItemsService.create(fieldPayloads);
|
||||
createdCollections.push(payload.collection);
|
||||
}
|
||||
});
|
||||
|
||||
return Array.isArray(data) ? createdCollections : createdCollections[0];
|
||||
}
|
||||
|
||||
readByKey(collection: string[]): Promise<Collection[]>;
|
||||
readByKey(collection: string): Promise<Collection>;
|
||||
async readByKey(collection: string | string[]): Promise<Collection | Collection[]> {
|
||||
const collectionItemsService = new ItemsService('directus_collections', {
|
||||
knex: this.knex,
|
||||
accountability: this.accountability,
|
||||
});
|
||||
const collectionKeys = Array.isArray(collection) ? collection : [collection];
|
||||
|
||||
if (this.accountability && this.accountability.admin !== true) {
|
||||
const permissions = await this.knex
|
||||
.select('collection')
|
||||
.from('directus_permissions')
|
||||
.where({ operation: 'read' })
|
||||
.where({ role: this.accountability.role })
|
||||
.whereIn('collection', collectionKeys);
|
||||
|
||||
if (collectionKeys.length !== permissions.length) {
|
||||
const collectionsYouHavePermissionToRead = permissions.map(
|
||||
({ collection }) => collection
|
||||
);
|
||||
|
||||
for (const collectionKey of collectionKeys) {
|
||||
if (collectionsYouHavePermissionToRead.includes(collectionKey) === false) {
|
||||
throw new ForbiddenException(
|
||||
`You don't have access to the "${collectionKey}" collection.`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const tablesInDatabase = await schemaInspector.tableInfo();
|
||||
const tables = tablesInDatabase.filter((table) => collectionKeys.includes(table.name));
|
||||
const system: any[] = await collectionItemsService.readByQuery({
|
||||
filter: { collection: { _in: collectionKeys } },
|
||||
});
|
||||
|
||||
const collections: Collection[] = [];
|
||||
|
||||
for (const table of tables) {
|
||||
const collection: Collection = {
|
||||
collection: table.name,
|
||||
system: system.find((systemInfo) => systemInfo.collection === table.name) || null,
|
||||
database: table,
|
||||
};
|
||||
|
||||
collections.push(collection);
|
||||
}
|
||||
|
||||
return Array.isArray(collection) ? collections : collections[0];
|
||||
}
|
||||
|
||||
/** @todo, read by query without query support is a bit ironic, isnt it */
|
||||
async readByQuery(): Promise<Collection[]> {
|
||||
const collectionItemsService = new ItemsService('directus_collections');
|
||||
let tablesInDatabase = await schemaInspector.tableInfo();
|
||||
|
||||
if (this.accountability && this.accountability.admin !== true) {
|
||||
const collectionsYouHavePermissionToRead: string[] = (
|
||||
await this.knex.select('collection').from('directus_permissions').where({
|
||||
role: this.accountability.role,
|
||||
operation: 'read',
|
||||
})
|
||||
).map(({ collection }) => collection);
|
||||
|
||||
tablesInDatabase = tablesInDatabase.filter((table) => {
|
||||
return collectionsYouHavePermissionToRead.includes(table.name);
|
||||
});
|
||||
}
|
||||
|
||||
const collectionNames = payloads.map((payload) => payload.collection!);
|
||||
return Array.isArray(data) ? collectionNames : collectionNames[0];
|
||||
const tablesToFetchInfoFor = tablesInDatabase.map((table) => table.name);
|
||||
const system: any[] = await collectionItemsService.readByQuery({
|
||||
filter: { collection: { _in: tablesToFetchInfoFor } },
|
||||
});
|
||||
|
||||
const collections: Collection[] = [];
|
||||
|
||||
for (const table of tablesInDatabase) {
|
||||
const collection: Collection = {
|
||||
collection: table.name,
|
||||
system: system.find((systemInfo) => systemInfo.collection === table.name),
|
||||
database: table,
|
||||
};
|
||||
|
||||
collections.push(collection);
|
||||
}
|
||||
|
||||
return collections;
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo
|
||||
* update w/ nested fields
|
||||
* @NOTE
|
||||
* We only suppport updating the content in directus_collections
|
||||
*/
|
||||
update(data: Partial<Collection>, keys: string[]): Promise<string[]>;
|
||||
update(data: Partial<Collection>, key: string): Promise<string>;
|
||||
update(data: Partial<Collection>[]): Promise<string[]>;
|
||||
async update(
|
||||
data: Partial<Collection> | Partial<Collection>[],
|
||||
key?: string | string[]
|
||||
): Promise<string | string[]> {
|
||||
const collectionItemsService = new ItemsService('directus_collections', {
|
||||
knex: this.knex,
|
||||
accountability: this.accountability,
|
||||
});
|
||||
|
||||
if (data && key) {
|
||||
const payload = data as Partial<Collection>;
|
||||
|
||||
if (!payload.system) {
|
||||
throw new InvalidPayloadException(`"system" key is required`);
|
||||
}
|
||||
|
||||
return (await collectionItemsService.update(payload.system!, key as any)) as
|
||||
| string
|
||||
| string[];
|
||||
}
|
||||
|
||||
const payloads = Array.isArray(data) ? data : [data];
|
||||
|
||||
const collectionUpdates = payloads.map((collection) => {
|
||||
return {
|
||||
...collection.system,
|
||||
collection: collection.collection,
|
||||
};
|
||||
});
|
||||
|
||||
await collectionItemsService.update(collectionUpdates);
|
||||
|
||||
return key!;
|
||||
}
|
||||
|
||||
delete(collection: string): Promise<string>;
|
||||
delete(collections: string[]): Promise<string[]>;
|
||||
async delete(collection: string | string[]): Promise<string | string[]> {
|
||||
const collections = Array.isArray(collection) ? collection : [collection];
|
||||
delete(collection: string): Promise<string>;
|
||||
async delete(collection: string[] | string): Promise<string[] | string> {
|
||||
if (this.accountability && this.accountability.admin !== true) {
|
||||
throw new ForbiddenException('Only admins can perform this action.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @todo check permissions manually
|
||||
* this.itemsService.delete does the permissions check, but we have to delete the records from fields/relations first
|
||||
* to adhere to the foreign key constraints
|
||||
*/
|
||||
const tablesInDatabase = await schemaInspector.tables();
|
||||
|
||||
await this.knex('directus_fields').delete().whereIn('collection', collections);
|
||||
const collectionKeys = Array.isArray(collection) ? collection : [collection];
|
||||
|
||||
for (const collectionKey of collectionKeys) {
|
||||
if (tablesInDatabase.includes(collectionKey) === false) {
|
||||
throw new InvalidPayloadException(`Collection "${collectionKey}" doesn't exist.`);
|
||||
}
|
||||
}
|
||||
|
||||
await this.knex('directus_fields').delete().whereIn('collection', collectionKeys);
|
||||
await this.knex('directus_relations')
|
||||
.delete()
|
||||
.whereIn('many_collection', collections)
|
||||
.orWhereIn('one_collection', collections);
|
||||
.whereIn('many_collection', collectionKeys)
|
||||
.orWhereIn('one_collection', collectionKeys);
|
||||
|
||||
await this.itemsService.delete(collection as any);
|
||||
const collectionItemsService = new ItemsService('directus_collections', {
|
||||
knex: this.knex,
|
||||
accountability: this.accountability,
|
||||
});
|
||||
await collectionItemsService.delete(collectionKeys);
|
||||
|
||||
for (const collectionName of collections) {
|
||||
await this.knex.schema.dropTable(collectionName);
|
||||
for (const collectionKey of collectionKeys) {
|
||||
await this.knex.schema.dropTable(collectionKey);
|
||||
}
|
||||
|
||||
return collection;
|
||||
|
||||
Reference in New Issue
Block a user