diff --git a/api/src/cache.ts b/api/src/cache.ts index 171fc73562..faa89348c3 100644 --- a/api/src/cache.ts +++ b/api/src/cache.ts @@ -12,12 +12,12 @@ let lockCache: Keyv | null = null; export function getCache(): { cache: Keyv | null; systemCache: Keyv; lockCache: Keyv } { if (env.CACHE_ENABLED === true && cache === null) { validateEnv(['CACHE_NAMESPACE', 'CACHE_TTL', 'CACHE_STORE']); - cache = getKeyvInstance(ms(env.CACHE_TTL as string)); + cache = getKeyvInstance(env.CACHE_TTL ? ms(env.CACHE_TTL as string) : undefined); cache.on('error', (err) => logger.warn(err, `[cache] ${err}`)); } if (systemCache === null) { - systemCache = getKeyvInstance(undefined, '_system'); + systemCache = getKeyvInstance(env.CACHE_SYSTEM_TTL ? ms(env.CACHE_SYSTEM_TTL as string) : undefined, '_system'); systemCache.on('error', (err) => logger.warn(err, `[cache] ${err}`)); } diff --git a/api/src/services/collections.ts b/api/src/services/collections.ts index 593467d2a8..5397122a5e 100644 --- a/api/src/services/collections.ts +++ b/api/src/services/collections.ts @@ -53,127 +53,131 @@ export class CollectionsService { throw new InvalidPayloadException(`Collections can't start with "directus_"`); } - const existingCollections: string[] = [ - ...((await this.knex.select('collection').from('directus_collections'))?.map(({ collection }) => collection) ?? - []), - ...Object.keys(this.schema.collections), - ]; + try { + const existingCollections: string[] = [ + ...((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.`); - } + if (existingCollections.includes(payload.collection)) { + throw new InvalidPayloadException(`Collection "${payload.collection}" already exists.`); + } - // Create the collection/fields in a transaction so it'll be reverted in case of errors or - // permission problems. This might not work reliably in MySQL, as it doesn't support DDL in - // transactions. - await this.knex.transaction(async (trx) => { - if (payload.schema) { - // Directus heavily relies on the primary key of a collection, so we have to make sure that - // every collection that is created has a primary key. If no primary key field is created - // while making the collection, we default to an auto incremented id named `id` - if (!payload.fields) - payload.fields = [ - { - field: 'id', - type: 'integer', - meta: { - hidden: true, - interface: 'numeric', - readonly: true, + // Create the collection/fields in a transaction so it'll be reverted in case of errors or + // permission problems. This might not work reliably in MySQL, as it doesn't support DDL in + // transactions. + await this.knex.transaction(async (trx) => { + if (payload.schema) { + // Directus heavily relies on the primary key of a collection, so we have to make sure that + // every collection that is created has a primary key. If no primary key field is created + // while making the collection, we default to an auto incremented id named `id` + 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, + }, }, - schema: { - is_primary_key: true, - has_auto_increment: true, - }, - }, - ]; + ]; - // Ensure that every field meta has the field/collection fields filled correctly - payload.fields = payload.fields.map((field) => { - if (field.meta) { - field.meta = { - ...field.meta, - field: field.field, - collection: payload.collection!, - }; - } - - return field; - }); - - const fieldsService = new FieldsService({ knex: trx, schema: this.schema }); - - 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); + // Ensure that every field meta has the field/collection fields filled correctly + payload.fields = payload.fields.map((field) => { + if (field.meta) { + field.meta = { + ...field.meta, + field: field.field, + collection: payload.collection!, + }; } - } - }); - const fieldItemsService = new ItemsService('directus_fields', { - knex: trx, - accountability: this.accountability, - schema: this.schema, - }); + return field; + }); - const fieldPayloads = payload.fields!.filter((field) => field.meta).map((field) => field.meta) as FieldMeta[]; - await fieldItemsService.createMany(fieldPayloads); - } + const fieldsService = new FieldsService({ knex: trx, schema: this.schema }); - if (payload.meta) { - const collectionItemsService = new ItemsService('directus_collections', { - knex: trx, - accountability: this.accountability, - schema: this.schema, - }); + 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); + } + } + }); - await collectionItemsService.createOne({ - ...payload.meta, - collection: payload.collection, - }); - } + const fieldItemsService = new ItemsService('directus_fields', { + knex: trx, + accountability: this.accountability, + schema: this.schema, + }); + + const fieldPayloads = payload.fields!.filter((field) => field.meta).map((field) => field.meta) as FieldMeta[]; + await fieldItemsService.createMany(fieldPayloads); + } + + 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, + }); + } + + return payload.collection; + }); return payload.collection; - }); + } finally { + if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) { + await this.cache.clear(); + } - if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) { - await this.cache.clear(); + await clearSystemCache(); } - - await clearSystemCache(); - - return payload.collection; } /** * Create multiple new collections */ async createMany(payloads: RawCollection[], opts?: MutationOptions): Promise { - const collections = await this.knex.transaction(async (trx) => { - const service = new CollectionsService({ - schema: this.schema, - accountability: this.accountability, - knex: trx, + try { + const collections = await this.knex.transaction(async (trx) => { + const service = new CollectionsService({ + schema: this.schema, + accountability: this.accountability, + knex: trx, + }); + + const collectionNames: string[] = []; + + for (const payload of payloads) { + const name = await service.createOne(payload, { autoPurgeCache: false }); + collectionNames.push(name); + } + + return collectionNames; }); - const collectionNames: string[] = []; - - for (const payload of payloads) { - const name = await service.createOne(payload, { autoPurgeCache: false }); - collectionNames.push(name); + return collections; + } finally { + if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) { + await this.cache.clear(); } - return collectionNames; - }); - - if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) { - await this.cache.clear(); + await clearSystemCache(); } - - await clearSystemCache(); - - return collections; } /** @@ -297,37 +301,39 @@ export class CollectionsService { throw new ForbiddenException(); } - const collectionItemsService = new ItemsService('directus_collections', { - knex: this.knex, - accountability: this.accountability, - schema: this.schema, - }); + try { + const collectionItemsService = new ItemsService('directus_collections', { + knex: this.knex, + accountability: this.accountability, + schema: this.schema, + }); - const payload = data as Partial; + const payload = data as Partial; + + 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 (!payload.meta) { return collectionKey; + } finally { + if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) { + await this.cache.clear(); + } + + await clearSystemCache(); } - - 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 clearSystemCache(); - - return collectionKey; } /** @@ -338,25 +344,27 @@ export class CollectionsService { throw new ForbiddenException(); } - await this.knex.transaction(async (trx) => { - const service = new CollectionsService({ - schema: this.schema, - accountability: this.accountability, - knex: trx, + try { + 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 }); + } }); - for (const collectionKey of collectionKeys) { - await service.updateOne(collectionKey, data, { autoPurgeCache: false }); + return collectionKeys; + } finally { + if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) { + await this.cache.clear(); } - }); - if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) { - await this.cache.clear(); + await clearSystemCache(); } - - await clearSystemCache(); - - return collectionKeys; } /** @@ -368,96 +376,98 @@ export class CollectionsService { throw new ForbiddenException(); } - const collections = await this.readByQuery(); + try { + const collections = await this.readByQuery(); - const collectionToBeDeleted = collections.find((collection) => collection.collection === collectionKey); + const collectionToBeDeleted = collections.find((collection) => collection.collection === collectionKey); - if (!!collectionToBeDeleted === false) { - throw new ForbiddenException(); - } - - await this.knex.transaction(async (trx) => { - if (collectionToBeDeleted!.schema) { - await trx.schema.dropTable(collectionKey); + if (!!collectionToBeDeleted === false) { + throw new ForbiddenException(); } - // Make sure this collection isn't used as a group in any other collections - 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 this.knex.transaction(async (trx) => { + if (collectionToBeDeleted!.schema) { + await trx.schema.dropTable(collectionKey); } - await trx('directus_revisions').delete().where('collection', '=', collectionKey); + // Make sure this collection isn't used as a group in any other collections + await trx('directus_collections').update({ group: null }).where({ group: 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 }); + if (collectionToBeDeleted!.meta) { + const collectionItemsService = new ItemsService('directus_collections', { + knex: trx, + accountability: this.accountability, + schema: this.schema, + }); - const relations = this.schema.relations.filter((relation) => { - return relation.collection === collectionKey || relation.related_collection === collectionKey; - }); + await collectionItemsService.deleteOne(collectionKey); + } - for (const relation of relations) { - // Delete related o2m fields that point to current collection - if (relation.related_collection && relation.meta?.one_field) { - await fieldsService.deleteField(relation.related_collection, relation.meta.one_field); + 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); } - // Delete related m2o fields that point to current collection - if (relation.related_collection === collectionKey) { - await fieldsService.deleteField(relation.collection, relation.field); + 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) { + // Delete related o2m fields that point to current collection + if (relation.related_collection && relation.meta?.one_field) { + await fieldsService.deleteField(relation.related_collection, relation.meta.one_field); + } + + // Delete related m2o fields that point to current collection + 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 }); } } + }); - 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 }); - } + return collectionKey; + } finally { + if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) { + await this.cache.clear(); } - }); - if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) { - await this.cache.clear(); + await clearSystemCache(); } - - await clearSystemCache(); - - return collectionKey; } /** @@ -468,24 +478,26 @@ export class CollectionsService { throw new ForbiddenException(); } - await this.knex.transaction(async (trx) => { - const service = new CollectionsService({ - schema: this.schema, - accountability: this.accountability, - knex: trx, + try { + 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 }); + } }); - for (const collectionKey of collectionKeys) { - await service.deleteOne(collectionKey, { autoPurgeCache: false }); + return collectionKeys; + } finally { + if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) { + await this.cache.clear(); } - }); - if (this.cache && env.CACHE_AUTO_PURGE && opts?.autoPurgeCache !== false) { - await this.cache.clear(); + await clearSystemCache(); } - - await clearSystemCache(); - - return collectionKeys; } } diff --git a/api/src/services/fields.ts b/api/src/services/fields.ts index 959a2fad7a..a67c50e7fb 100644 --- a/api/src/services/fields.ts +++ b/api/src/services/fields.ts @@ -228,62 +228,149 @@ export class FieldsService { throw new ForbiddenException(); } - const exists = - field.field in this.schema.collections[collection].fields || - isNil(await this.knex.select('id').from('directus_fields').where({ collection, field: field.field }).first()) === - false; + try { + const exists = + field.field in this.schema.collections[collection].fields || + isNil( + await this.knex.select('id').from('directus_fields').where({ collection, field: field.field }).first() + ) === false; - // Check if field already exists, either as a column, or as a row in directus_fields - if (exists) { - throw new InvalidPayloadException(`Field "${field.field}" already exists in collection "${collection}"`); + // Check if field already exists, either as a column, or as a row in directus_fields + if (exists) { + throw new InvalidPayloadException(`Field "${field.field}" already exists in collection "${collection}"`); + } + + await this.knex.transaction(async (trx) => { + const itemsService = new ItemsService('directus_fields', { + knex: trx, + accountability: this.accountability, + schema: this.schema, + }); + + const hookAdjustedField = await emitter.emitFilter( + `fields.create`, + field, + { + collection: collection, + }, + { + database: trx, + schema: this.schema, + accountability: this.accountability, + } + ); + + if (hookAdjustedField.type && ALIAS_TYPES.includes(hookAdjustedField.type) === false) { + if (table) { + this.addColumnToTable(table, hookAdjustedField as Field); + } else { + await trx.schema.alterTable(collection, (table) => { + this.addColumnToTable(table, hookAdjustedField as Field); + }); + } + } + + if (hookAdjustedField.meta) { + await itemsService.createOne( + { + ...hookAdjustedField.meta, + collection: collection, + field: hookAdjustedField.field, + }, + { emitEvents: false } + ); + } + + emitter.emitAction( + `fields.create`, + { + payload: hookAdjustedField, + key: hookAdjustedField.field, + collection: collection, + }, + { + database: getDatabase(), + schema: this.schema, + accountability: this.accountability, + } + ); + }); + } finally { + if (this.cache && env.CACHE_AUTO_PURGE) { + await this.cache.clear(); + } + + await clearSystemCache(); + } + } + + async updateField(collection: string, field: RawField): Promise { + if (this.accountability && this.accountability.admin !== true) { + throw new ForbiddenException(); } - await this.knex.transaction(async (trx) => { - const itemsService = new ItemsService('directus_fields', { - knex: trx, - accountability: this.accountability, - schema: this.schema, - }); - + try { const hookAdjustedField = await emitter.emitFilter( - `fields.create`, + `fields.update`, field, { + keys: [field.field], collection: collection, }, { - database: trx, + database: this.knex, schema: this.schema, accountability: this.accountability, } ); - if (hookAdjustedField.type && ALIAS_TYPES.includes(hookAdjustedField.type) === false) { - if (table) { - this.addColumnToTable(table, hookAdjustedField as Field); - } else { - await trx.schema.alterTable(collection, (table) => { - this.addColumnToTable(table, hookAdjustedField as Field); - }); + const record = field.meta + ? await this.knex.select('id').from('directus_fields').where({ collection, field: field.field }).first() + : null; + + if (hookAdjustedField.schema) { + const existingColumn = await this.schemaInspector.columnInfo(collection, hookAdjustedField.field); + + if (!isEqual(existingColumn, hookAdjustedField.schema)) { + try { + await this.knex.schema.alterTable(collection, (table) => { + if (!hookAdjustedField.schema) return; + this.addColumnToTable(table, field, existingColumn); + }); + } catch (err: any) { + throw await translateDatabaseError(err); + } } } if (hookAdjustedField.meta) { - await itemsService.createOne( - { - ...hookAdjustedField.meta, - collection: collection, - field: hookAdjustedField.field, - }, - { emitEvents: false } - ); + if (record) { + await this.itemsService.updateOne( + record.id, + { + ...hookAdjustedField.meta, + collection: collection, + field: hookAdjustedField.field, + }, + { emitEvents: false } + ); + } else { + await this.itemsService.createOne( + { + ...hookAdjustedField.meta, + collection: collection, + field: hookAdjustedField.field, + }, + { emitEvents: false } + ); + } } emitter.emitAction( - `fields.create`, + `fields.update`, { payload: hookAdjustedField, - key: hookAdjustedField.field, + keys: [hookAdjustedField.field], collection: collection, }, { @@ -292,97 +379,15 @@ export class FieldsService { accountability: this.accountability, } ); - }); - if (this.cache && env.CACHE_AUTO_PURGE) { - await this.cache.clear(); - } - - await clearSystemCache(); - } - - async updateField(collection: string, field: RawField): Promise { - if (this.accountability && this.accountability.admin !== true) { - throw new ForbiddenException(); - } - - const hookAdjustedField = await emitter.emitFilter( - `fields.update`, - field, - { - keys: [field.field], - collection: collection, - }, - { - database: this.knex, - schema: this.schema, - accountability: this.accountability, + return field.field; + } finally { + if (this.cache && env.CACHE_AUTO_PURGE) { + await this.cache.clear(); } - ); - const record = field.meta - ? await this.knex.select('id').from('directus_fields').where({ collection, field: field.field }).first() - : null; - - if (hookAdjustedField.schema) { - const existingColumn = await this.schemaInspector.columnInfo(collection, hookAdjustedField.field); - - if (!isEqual(existingColumn, hookAdjustedField.schema)) { - try { - await this.knex.schema.alterTable(collection, (table) => { - if (!hookAdjustedField.schema) return; - this.addColumnToTable(table, field, existingColumn); - }); - } catch (err: any) { - throw await translateDatabaseError(err); - } - } + await clearSystemCache(); } - - if (hookAdjustedField.meta) { - if (record) { - await this.itemsService.updateOne( - record.id, - { - ...hookAdjustedField.meta, - collection: collection, - field: hookAdjustedField.field, - }, - { emitEvents: false } - ); - } else { - await this.itemsService.createOne( - { - ...hookAdjustedField.meta, - collection: collection, - field: hookAdjustedField.field, - }, - { emitEvents: false } - ); - } - } - - if (this.cache && env.CACHE_AUTO_PURGE) { - await this.cache.clear(); - } - - await clearSystemCache(); - - emitter.emitAction( - `fields.update`, - { - payload: hookAdjustedField, - keys: [hookAdjustedField.field], - collection: collection, - }, - { - database: getDatabase(), - schema: this.schema, - accountability: this.accountability, - } - ); - - return field.field; } async deleteField(collection: string, field: string): Promise { @@ -390,124 +395,126 @@ export class FieldsService { throw new ForbiddenException(); } - await emitter.emitFilter( - 'fields.delete', - [field], - { - collection: collection, - }, - { - database: this.knex, - schema: this.schema, - accountability: this.accountability, - } - ); + try { + await emitter.emitFilter( + 'fields.delete', + [field], + { + collection: collection, + }, + { + database: this.knex, + schema: this.schema, + accountability: this.accountability, + } + ); - await this.knex.transaction(async (trx) => { - if ( - this.schema.collections[collection] && - field in this.schema.collections[collection].fields && - this.schema.collections[collection].fields[field].alias === false - ) { - await trx.schema.table(collection, (table) => { - table.dropColumn(field); + await this.knex.transaction(async (trx) => { + if ( + this.schema.collections[collection] && + field in this.schema.collections[collection].fields && + this.schema.collections[collection].fields[field].alias === false + ) { + await trx.schema.table(collection, (table) => { + table.dropColumn(field); + }); + } + + const relations = this.schema.relations.filter((relation) => { + return ( + (relation.collection === collection && relation.field === field) || + (relation.related_collection === collection && relation.meta?.one_field === field) + ); }); - } - const relations = this.schema.relations.filter((relation) => { - return ( - (relation.collection === collection && relation.field === field) || - (relation.related_collection === collection && relation.meta?.one_field === field) - ); - }); + const relationsService = new RelationsService({ + knex: trx, + accountability: this.accountability, + schema: this.schema, + }); - const relationsService = new RelationsService({ - knex: trx, - accountability: this.accountability, - schema: this.schema, - }); + const fieldsService = new FieldsService({ + knex: trx, + accountability: this.accountability, + schema: this.schema, + }); - const fieldsService = new FieldsService({ - knex: trx, - accountability: this.accountability, - schema: this.schema, - }); + for (const relation of relations) { + const isM2O = relation.collection === collection && relation.field === field; - for (const relation of relations) { - const isM2O = relation.collection === collection && relation.field === field; + // If the current field is a m2o, delete the related o2m if it exists and remove the relationship + if (isM2O) { + await relationsService.deleteOne(collection, field); - // If the current field is a m2o, delete the related o2m if it exists and remove the relationship - if (isM2O) { - await relationsService.deleteOne(collection, field); + if (relation.related_collection && relation.meta?.one_field) { + await fieldsService.deleteField(relation.related_collection, relation.meta.one_field); + } + } - if (relation.related_collection && relation.meta?.one_field) { - await fieldsService.deleteField(relation.related_collection, relation.meta.one_field); + // If the current field is a o2m, just delete the one field config from the relation + if (!isM2O && relation.meta?.one_field) { + await trx('directus_relations') + .update({ one_field: null }) + .where({ many_collection: relation.collection, many_field: relation.field }); } } - // If the current field is a o2m, just delete the one field config from the relation - if (!isM2O && relation.meta?.one_field) { - await trx('directus_relations') - .update({ one_field: null }) - .where({ many_collection: relation.collection, many_field: relation.field }); + const collectionMeta = await trx + .select('archive_field', 'sort_field') + .from('directus_collections') + .where({ collection }) + .first(); + + const collectionMetaUpdates: Record = {}; + + if (collectionMeta?.archive_field === field) { + collectionMetaUpdates.archive_field = null; } + + if (collectionMeta?.sort_field === field) { + collectionMetaUpdates.sort_field = null; + } + + if (Object.keys(collectionMetaUpdates).length > 0) { + await trx('directus_collections').update(collectionMetaUpdates).where({ collection }); + } + + // Cleanup directus_fields + const metaRow = await trx + .select('collection', 'field') + .from('directus_fields') + .where({ collection, field }) + .first(); + + if (metaRow) { + // Handle recursive FK constraints + await trx('directus_fields') + .update({ group: null }) + .where({ group: metaRow.field, collection: metaRow.collection }); + } + + await trx('directus_fields').delete().where({ collection, field }); + }); + + emitter.emitAction( + 'fields.delete', + { + payload: [field], + collection: collection, + }, + { + database: this.knex, + schema: this.schema, + accountability: this.accountability, + } + ); + } finally { + if (this.cache && env.CACHE_AUTO_PURGE) { + await this.cache.clear(); } - const collectionMeta = await trx - .select('archive_field', 'sort_field') - .from('directus_collections') - .where({ collection }) - .first(); - - const collectionMetaUpdates: Record = {}; - - if (collectionMeta?.archive_field === field) { - collectionMetaUpdates.archive_field = null; - } - - if (collectionMeta?.sort_field === field) { - collectionMetaUpdates.sort_field = null; - } - - if (Object.keys(collectionMetaUpdates).length > 0) { - await trx('directus_collections').update(collectionMetaUpdates).where({ collection }); - } - - // Cleanup directus_fields - const metaRow = await trx - .select('collection', 'field') - .from('directus_fields') - .where({ collection, field }) - .first(); - - if (metaRow) { - // Handle recursive FK constraints - await trx('directus_fields') - .update({ group: null }) - .where({ group: metaRow.field, collection: metaRow.collection }); - } - - await trx('directus_fields').delete().where({ collection, field }); - }); - - if (this.cache && env.CACHE_AUTO_PURGE) { - await this.cache.clear(); + await clearSystemCache(); } - - await clearSystemCache(); - - emitter.emitAction( - 'fields.delete', - { - payload: [field], - collection: collection, - }, - { - database: this.knex, - schema: this.schema, - accountability: this.accountability, - } - ); } public addColumnToTable(table: Knex.CreateTableBuilder, field: RawField | Field, alter: Column | null = null): void { diff --git a/api/src/services/relations.ts b/api/src/services/relations.ts index 8fa84aaaef..ba54b22acb 100644 --- a/api/src/services/relations.ts +++ b/api/src/services/relations.ts @@ -165,43 +165,45 @@ export class RelationsService { ); } - const metaRow = { - ...(relation.meta || {}), - many_collection: relation.collection, - many_field: relation.field, - one_collection: relation.related_collection || null, - }; + try { + const metaRow = { + ...(relation.meta || {}), + many_collection: relation.collection, + many_field: relation.field, + one_collection: relation.related_collection || null, + }; - await this.knex.transaction(async (trx) => { - if (relation.related_collection) { - await trx.schema.alterTable(relation.collection!, async (table) => { - this.alterType(table, relation); + await this.knex.transaction(async (trx) => { + if (relation.related_collection) { + await trx.schema.alterTable(relation.collection!, async (table) => { + this.alterType(table, relation); - const constraintName: string = getDefaultIndexName('foreign', relation.collection!, relation.field!); - const builder = table - .foreign(relation.field!, constraintName) - .references( - `${relation.related_collection!}.${this.schema.collections[relation.related_collection!].primary}` - ); + const constraintName: string = getDefaultIndexName('foreign', relation.collection!, relation.field!); + const builder = table + .foreign(relation.field!, constraintName) + .references( + `${relation.related_collection!}.${this.schema.collections[relation.related_collection!].primary}` + ); - if (relation.schema?.on_delete) { - builder.onDelete(relation.schema.on_delete); - } + if (relation.schema?.on_delete) { + builder.onDelete(relation.schema.on_delete); + } + }); + } + + const relationsItemService = new ItemsService('directus_relations', { + knex: trx, + schema: this.schema, + // We don't set accountability here. If you have read access to certain fields, you are + // allowed to extract the relations regardless of permissions to directus_relations. This + // happens in `filterForbidden` down below }); - } - const relationsItemService = new ItemsService('directus_relations', { - knex: trx, - schema: this.schema, - // We don't set accountability here. If you have read access to certain fields, you are - // allowed to extract the relations regardless of permissions to directus_relations. This - // happens in `filterForbidden` down below + await relationsItemService.createOne(metaRow); }); - - await relationsItemService.createOne(metaRow); - }); - - await clearSystemCache(); + } finally { + await clearSystemCache(); + } } /** @@ -230,56 +232,58 @@ export class RelationsService { throw new InvalidPayloadException(`Field "${field}" in collection "${collection}" doesn't have a relationship.`); } - await this.knex.transaction(async (trx) => { - if (existingRelation.related_collection) { - await trx.schema.alterTable(collection, async (table) => { - let constraintName: string = getDefaultIndexName('foreign', collection, field); + try { + await this.knex.transaction(async (trx) => { + if (existingRelation.related_collection) { + await trx.schema.alterTable(collection, async (table) => { + let constraintName: string = getDefaultIndexName('foreign', collection, field); - // If the FK already exists in the DB, drop it first - if (existingRelation?.schema) { - constraintName = existingRelation.schema.constraint_name || constraintName; - table.dropForeign(field, constraintName); - } + // If the FK already exists in the DB, drop it first + if (existingRelation?.schema) { + constraintName = existingRelation.schema.constraint_name || constraintName; + table.dropForeign(field, constraintName); + } - this.alterType(table, relation); + this.alterType(table, relation); - const builder = table - .foreign(field, constraintName || undefined) - .references( - `${existingRelation.related_collection!}.${ - this.schema.collections[existingRelation.related_collection!].primary - }` - ); + const builder = table + .foreign(field, constraintName || undefined) + .references( + `${existingRelation.related_collection!}.${ + this.schema.collections[existingRelation.related_collection!].primary + }` + ); - if (relation.schema?.on_delete) { - builder.onDelete(relation.schema.on_delete); - } - }); - } - - const relationsItemService = new ItemsService('directus_relations', { - knex: trx, - schema: this.schema, - // We don't set accountability here. If you have read access to certain fields, you are - // allowed to extract the relations regardless of permissions to directus_relations. This - // happens in `filterForbidden` down below - }); - - if (relation.meta) { - if (existingRelation?.meta) { - await relationsItemService.updateOne(existingRelation.meta.id, relation.meta); - } else { - await relationsItemService.createOne({ - ...(relation.meta || {}), - many_collection: relation.collection, - many_field: relation.field, - one_collection: existingRelation.related_collection || null, + if (relation.schema?.on_delete) { + builder.onDelete(relation.schema.on_delete); + } }); } - } - }); - await clearSystemCache(); + const relationsItemService = new ItemsService('directus_relations', { + knex: trx, + schema: this.schema, + // We don't set accountability here. If you have read access to certain fields, you are + // allowed to extract the relations regardless of permissions to directus_relations. This + // happens in `filterForbidden` down below + }); + + if (relation.meta) { + if (existingRelation?.meta) { + await relationsItemService.updateOne(existingRelation.meta.id, relation.meta); + } else { + await relationsItemService.createOne({ + ...(relation.meta || {}), + many_collection: relation.collection, + many_field: relation.field, + one_collection: existingRelation.related_collection || null, + }); + } + } + }); + } finally { + await clearSystemCache(); + } } /** @@ -306,25 +310,27 @@ export class RelationsService { throw new InvalidPayloadException(`Field "${field}" in collection "${collection}" doesn't have a relationship.`); } - await this.knex.transaction(async (trx) => { - const existingConstraints = await this.schemaInspector.foreignKeys(); - const constraintNames = existingConstraints.map((key) => key.constraint_name); + try { + await this.knex.transaction(async (trx) => { + const existingConstraints = await this.schemaInspector.foreignKeys(); + const constraintNames = existingConstraints.map((key) => key.constraint_name); - if ( - existingRelation.schema?.constraint_name && - constraintNames.includes(existingRelation.schema.constraint_name) - ) { - await trx.schema.alterTable(existingRelation.collection, (table) => { - table.dropForeign(existingRelation.field, existingRelation.schema!.constraint_name!); - }); - } + if ( + existingRelation.schema?.constraint_name && + constraintNames.includes(existingRelation.schema.constraint_name) + ) { + await trx.schema.alterTable(existingRelation.collection, (table) => { + table.dropForeign(existingRelation.field, existingRelation.schema!.constraint_name!); + }); + } - if (existingRelation.meta) { - await trx('directus_relations').delete().where({ many_collection: collection, many_field: field }); - } - }); - - await clearSystemCache(); + if (existingRelation.meta) { + await trx('directus_relations').delete().where({ many_collection: collection, many_field: field }); + } + }); + } finally { + await clearSystemCache(); + } } /** diff --git a/docs/configuration/config-options.md b/docs/configuration/config-options.md index 18da76eea1..783c94d216 100644 --- a/docs/configuration/config-options.md +++ b/docs/configuration/config-options.md @@ -416,9 +416,10 @@ often possible to cache assets for far longer than you would cache database cont | Variable | Description | Default Value | | --------------------------------- | ---------------------------------------------------------------------------------------- | ---------------- | | `CACHE_ENABLED` | Whether or not caching is enabled. | `false` | -| `CACHE_TTL`[1] | How long the cache is persisted. | `30m` | +| `CACHE_TTL`[1] | How long the cache is persisted. | `5m` | | `CACHE_CONTROL_S_MAXAGE` | Whether to not to add the `s-maxage` expiration flag. Set to a number for a custom value | `0` | | `CACHE_AUTO_PURGE`[2] | Automatically purge the cache on `create`, `update`, and `delete` actions. | `false` | +| `CACHE_SYSTEM_TTL`[3] | How long the schema caches (schema/permissions) are persisted. | `10m` | | `CACHE_SCHEMA`[3] | Whether or not the database schema is cached. One of `false`, `true` | `true` | | `CACHE_PERMISSIONS`[3] | Whether or not the user permissions are cached. One of `false`, `true` | `true` | | `CACHE_NAMESPACE` | How to scope the cache data. | `directus-cache` |