import SchemaInspector from '@directus/schema' ; import { Knex } from 'knex' ; import { getCache } from '../cache' ; import { ALIAS_TYPES } from '../constants' ; import getDatabase , { getSchemaInspector } from '../database' ; import { systemCollectionRows } from '../database/system-data/collections' ; import env from '../env' ; import { ForbiddenException , InvalidPayloadException } from '../exceptions' ; import { FieldsService } from '../services/fields' ; import { ItemsService } from '../services/items' ; import Keyv from 'keyv' ; import { AbstractServiceOptions , Collection , CollectionMeta , SchemaOverview , MutationOptions } from '../types' ; import { Accountability , FieldMeta , RawField } from '@directus/shared/types' ; import { Table } from 'knex-schema-inspector/dist/types/table' ; export type RawCollection = { collection : string ; fields ? : RawField [ ] ; schema ? : Partial < Table > | null ; meta ? : Partial < CollectionMeta > | null ; } ; export class CollectionsService { knex : Knex ; accountability : Accountability | null ; schemaInspector : ReturnType < typeof SchemaInspector > ; schema : SchemaOverview ; cache : Keyv < any > | null ; systemCache : Keyv < any > ; constructor ( options ) { this . knex = options . knex || getDatabase ( ) ; this . accountability = options . accountability || null ; this . schemaInspector = options . knex ? SchemaInspector ( options . knex ) : getSchemaInspector ( ) ; this . schema = options . schema ; const { cache , systemCache } = getCache ( ) ; this . cache = cache ; this . systemCache = systemCache ; } /**
     * Create a single new collection
     */ async createOne ( payload , opts ? ) { if ( this . accountability && this . accountability . admin !== true ) { throw new ForbiddenException ( ) ; } if ( ! payload . collection ) throw new InvalidPayloadException ( `"collection" is required` ) ; if ( payload . collection . startsWith ( 'directus_' ) ) { throw new InvalidPayloadException ( `Collections can't start with "directus_"` ) ; } const existingCollections = [ ... ( ( await this . knex . select ( 'collection' ) . from ( 'directus_collections' ) ) ?. map ( ( { collection } ) => collection ) ?? [ ] ) , ... Object . keys ( this . schema . collections ) , ] ; if ( existingCollections . includes ( payload . collection ) ) { throw new InvalidPayloadException ( `Collection "${ payload . collection }" already exists.` ) ; } await this . knex . transaction ( async ( trx ) => { if ( payload . meta ) { const collectionItemsService = new ItemsService ( 'directus_collections' , { knex : trx , accountability : this . accountability , schema : this . schema , } ) ; await collectionItemsService . createOne ( { ... payload . meta , collection : payload . collection , } ) ; } if ( payload . schema ) { const fieldsService = new FieldsService ( { knex : trx , schema : this . schema } ) ; const fieldItemsService = new ItemsService ( 'directus_fields' , { knex : trx , accountability : this . accountability , schema : this . schema , } ) ; if ( ! payload . fields ) payload . fields = [ { field : 'id' , type : 'integer' , meta : { hidden : true , interface : 'numeric' , readonly : true , } , schema : { is_primary_key : true , has_auto_increment : true , } , } , ] ; payload . fields = payload . fields . map ( ( field ) => { if ( field . meta ) { field . meta = { ... field . meta , field : field . field , collection : payload . collection ! , } ; } return field ; } ) ; await trx . schema . createTable ( payload . collection , ( table ) => { for ( const field of payload . fields ! ) { if ( field . type && ALIAS_TYPES . includes ( field . type ) === false ) { fieldsService . addColumnToTable ( table , field ) ; } } } ) ; const fieldPayloads = payload . fields ! . filter ( ( field ) => field . meta ) . map ( ( field ) => field . meta ) as FieldMeta [ ] ; await fieldItemsService . createMany ( fieldPayloads ) ; } return payload . collection ; } ) ; if ( this . cache && env . CACHE_AUTO_PURGE && opts ?. autoPurgeCache !== false ) { await this . cache . clear ( ) ; } await this . systemCache . clear ( ) ; return payload . collection ; } /**
     * Create multiple new collections
     */ async createMany ( payloads , opts ? ) { const collections = await this . knex . transaction ( async ( trx ) => { const service = new CollectionsService ( { schema : this . schema , accountability : this . accountability , knex : trx , } ) ; const collectionNames = [ ] ; for ( const payload of payloads ) { const name = await service . createOne ( payload , { autoPurgeCache : false } ) ; collectionNames . push ( name ) ; } return collectionNames ; } ) ; if ( this . cache && env . CACHE_AUTO_PURGE && opts ?. autoPurgeCache !== false ) { await this . cache . clear ( ) ; } await this . systemCache . clear ( ) ; return collections ; } /**
     * Read all collections. Currently doesn't support any query.
     */ async readByQuery ( ) { const collectionItemsService = new ItemsService ( 'directus_collections' , { knex : this . knex , schema : this . schema , accountability : this . accountability , } ) ; let tablesInDatabase = await this . schemaInspector . tableInfo ( ) ; let meta = ( await collectionItemsService . readByQuery ( { limit : - 1 , } ) ) as CollectionMeta [ ] ; meta . push ( ... systemCollectionRows ) ; if ( this . accountability && this . accountability . admin !== true ) { const collectionsGroups = meta . reduce ( ( meta , item ) => ( { ... meta , [ item . collection ] : item . group , } ) , { } ) ; let collectionsYouHavePermissionToRead = this . accountability . permissions ! . filter ( ( permission ) => { return permission . action === 'read' ; } ) . map ( ( { collection } ) => collection ) ; for ( const collection of collectionsYouHavePermissionToRead ) { const group = collectionsGroups [ collection ] ; if ( group ) collectionsYouHavePermissionToRead . push ( group ) ; delete collectionsGroups [ collection ] ; } collectionsYouHavePermissionToRead = [ ... new Set ( [ ... collectionsYouHavePermissionToRead ] ) ] ; tablesInDatabase = tablesInDatabase . filter ( ( table ) => { return collectionsYouHavePermissionToRead . includes ( table . name ) ; } ) ; meta = meta . filter ( ( collectionMeta ) => { return collectionsYouHavePermissionToRead . includes ( collectionMeta . collection ) ; } ) ; } const collections = [ ] ; for ( const collectionMeta of meta ) { const collection = { collection : collectionMeta . collection , meta : collectionMeta , schema : tablesInDatabase . find ( ( table ) => table . name === collectionMeta . collection ) ?? null , } ; collections . push ( collection ) ; } for ( const table of tablesInDatabase ) { const exists = ! ! collections . find ( ( { collection } ) => collection === table . name ) ; if ( ! exists ) { collections . push ( { collection : table . name , schema : table , meta : null , } ) ; } } if ( env . DB_EXCLUDE_TABLES ) { return collections . filter ( ( collection ) => env . DB_EXCLUDE_TABLES . includes ( collection . collection ) === false ) ; } return collections ; } /**
     * Get a single collection by name
     */ async readOne ( collectionKey ) { const result = await this . readMany ( [ collectionKey ] ) ; return result [ 0 ] ; } /**
     * Read many collections by name
     */ async readMany ( collectionKeys ) { if ( this . accountability && this . accountability . admin !== true ) { const permissions = this . accountability . permissions ! . filter ( ( permission ) => { return permission . action === 'read' && collectionKeys . includes ( permission . collection ) ; } ) ; if ( collectionKeys . length !== permissions . length ) { const collectionsYouHavePermissionToRead = permissions . map ( ( { collection } ) => collection ) ; for ( const collectionKey of collectionKeys ) { if ( collectionsYouHavePermissionToRead . includes ( collectionKey ) === false ) { throw new ForbiddenException ( ) ; } } } } const collections = await this . readByQuery ( ) ; return collections . filter ( ( { collection } ) => collectionKeys . includes ( collection ) ) ; } /**
     * Update a single collection by name
     */ async updateOne ( collectionKey , data , opts ? ) { if ( this . accountability && this . accountability . admin !== true ) { throw new ForbiddenException ( ) ; } const collectionItemsService = new ItemsService ( 'directus_collections' , { knex : this . knex , accountability : this . accountability , schema : this . schema , } ) ; const payload = data as Partial < Collection > ; if ( ! payload . meta ) { return collectionKey ; } const exists = ! ! ( await this . knex . select ( 'collection' ) . from ( 'directus_collections' ) . where ( { collection : collectionKey } ) . first ( ) ) ; if ( exists ) { await collectionItemsService . updateOne ( collectionKey , payload . meta , opts ) ; } else { await collectionItemsService . createOne ( { ... payload . meta , collection : collectionKey } , opts ) ; } if ( this . cache && env . CACHE_AUTO_PURGE && opts ?. autoPurgeCache !== false ) { await this . cache . clear ( ) ; } await this . systemCache . clear ( ) ; return collectionKey ; } /**
     * Update multiple collections by name
     */ async updateMany ( collectionKeys , data , opts ? ) { if ( this . accountability && this . accountability . admin !== true ) { throw new ForbiddenException ( ) ; } await this . knex . transaction ( async ( trx ) => { const service = new CollectionsService ( { schema : this . schema , accountability : this . accountability , knex : trx , } ) ; for ( const collectionKey of collectionKeys ) { await service . updateOne ( collectionKey , data , { autoPurgeCache : false } ) ; } } ) ; if ( this . cache && env . CACHE_AUTO_PURGE && opts ?. autoPurgeCache !== false ) { await this . cache . clear ( ) ; } await this . systemCache . clear ( ) ; return collectionKeys ; } /**
     * Delete a single collection This will delete the table and all records within. It'll also
     * delete any fields, presets, activity, revisions, and permissions relating to this collection
     */ async deleteOne ( collectionKey , opts ? ) { if ( this . accountability && this . accountability . admin !== true ) { throw new ForbiddenException ( ) ; } const collections = await this . readByQuery ( ) ; const collectionToBeDeleted = collections . find ( ( collection ) => collection . collection === collectionKey ) ; if ( ! ! collectionToBeDeleted === false ) { throw new ForbiddenException ( ) ; } await this . knex . transaction ( async ( trx ) => { await trx ( 'directus_collections' ) . update ( { group : null } ) . where ( { group : collectionKey } ) ; if ( collectionToBeDeleted ! . meta ) { const collectionItemsService = new ItemsService ( 'directus_collections' , { knex : trx , accountability : this . accountability , schema : this . schema , } ) ; await collectionItemsService . deleteOne ( collectionKey ) ; } if ( collectionToBeDeleted ! . schema ) { const fieldsService = new FieldsService ( { knex : trx , accountability : this . accountability , schema : this . schema , } ) ; await trx ( 'directus_fields' ) . delete ( ) . where ( 'collection' , '=' , collectionKey ) ; await trx ( 'directus_presets' ) . delete ( ) . where ( 'collection' , '=' , collectionKey ) ; const revisionsToDelete = await trx . select ( 'id' ) . from ( 'directus_revisions' ) . where ( { collection : collectionKey } ) ; if ( revisionsToDelete . length > 0 ) { const keys = revisionsToDelete . map ( ( record ) => record . id ) ; await trx ( 'directus_revisions' ) . update ( { parent : null } ) . whereIn ( 'parent' , keys ) ; } await trx ( 'directus_revisions' ) . delete ( ) . where ( 'collection' , '=' , collectionKey ) ; await trx ( 'directus_activity' ) . delete ( ) . where ( 'collection' , '=' , collectionKey ) ; await trx ( 'directus_permissions' ) . delete ( ) . where ( 'collection' , '=' , collectionKey ) ; await trx ( 'directus_relations' ) . delete ( ) . where ( { many_collection : collectionKey } ) ; const relations = this . schema . relations . filter ( ( relation ) => { return relation . collection === collectionKey || relation . related_collection === collectionKey ; } ) ; for ( const relation of relations ) { if ( relation . related_collection && relation . meta ?. one_field ) { await fieldsService . deleteField ( relation . related_collection , relation . meta . one_field ) ; } if ( relation . related_collection === collectionKey ) { await fieldsService . deleteField ( relation . collection , relation . field ) ; } } const a2oRelationsThatIncludeThisCollection = this . schema . relations . filter ( ( relation ) => { return relation . meta ?. one_allowed_collections ?. includes ( collectionKey ) ; } ) ; for ( const relation of a2oRelationsThatIncludeThisCollection ) { const newAllowedCollections = relation . meta ! . one_allowed_collections ! . filter ( ( collection ) => collectionKey !== collection ) . join ( ',' ) ; await trx ( 'directus_relations' ) . update ( { one_allowed_collections : newAllowedCollections } ) . where ( { id : relation . meta ! . id } ) ; } await trx . schema . dropTable ( collectionKey ) ; } } ) ; if ( this . cache && env . CACHE_AUTO_PURGE && opts ?. autoPurgeCache !== false ) { await this . cache . clear ( ) ; } await this . systemCache . clear ( ) ; return collectionKey ; } /**
     * Delete multiple collections by key
     */ async deleteMany ( collectionKeys , opts ? ) { if ( this . accountability && this . accountability . admin !== true ) { throw new ForbiddenException ( ) ; } await this . knex . transaction ( async ( trx ) => { const service = new CollectionsService ( { schema : this . schema , accountability : this . accountability , knex : trx , } ) ; for ( const collectionKey of collectionKeys ) { await service . deleteOne ( collectionKey , { autoPurgeCache : false } ) ; } } ) ; if ( this . cache && env . CACHE_AUTO_PURGE && opts ?. autoPurgeCache !== false ) { await this . cache . clear ( ) ; } await this . systemCache . clear ( ) ; return collectionKeys ; } }