Improve cache reliability in DDL operations (#12400)

* Add TTL to schema cache

* Clear caches on unexpected errors in DDL

* Consistent return value use

* Don't set a default value for schema ttl
This commit is contained in:
Rijk van Zanten
2022-04-01 13:24:20 -04:00
committed by GitHub
parent 9be08a3f35
commit 3307bed5fd
5 changed files with 568 additions and 542 deletions

View File

@@ -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}`));
}

View File

@@ -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<string[]> {
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<Collection>;
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 (!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;
}
}

View File

@@ -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<string> {
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<string> {
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<void> {
@@ -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<string, null> = {};
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<string, null> = {};
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 {

View File

@@ -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();
}
}
/**