diff --git a/api/src/routes/fields.ts b/api/src/routes/fields.ts index e0ac301dc7..45d9880f88 100644 --- a/api/src/routes/fields.ts +++ b/api/src/routes/fields.ts @@ -35,7 +35,7 @@ router.get( asyncHandler(async (req, res) => { const service = new FieldsService({ accountability: req.accountability }); - const fields = await service.readAll(req.collection); + const fields = await service.readAll(req.params.collection); return res.json({ data: fields || null }); }) ); @@ -50,7 +50,7 @@ router.get( const exists = await schemaInspector.hasColumn(req.collection, req.params.field); if (exists === false) throw new FieldNotFoundException(req.collection, req.params.field); - const field = await service.readOne(req.collection, req.params.field); + const field = await service.readOne(req.params.collection, req.params.field); return res.json({ data: field || null }); }) ); diff --git a/api/src/routes/files.ts b/api/src/routes/files.ts index 2c772dadb6..929feebd1a 100644 --- a/api/src/routes/files.ts +++ b/api/src/routes/files.ts @@ -100,7 +100,8 @@ router.post( } const record = await service.readByKey(keys as any, req.sanitizedQuery); - return res.json({ data: record || null }); + + return res.json({ data: res.locals.savedFiles.length === 1 ? record[0] : record || null }); }) ); diff --git a/api/src/services/fields.ts b/api/src/services/fields.ts index f5d23052a4..a26dc5bd0d 100644 --- a/api/src/services/fields.ts +++ b/api/src/services/fields.ts @@ -81,10 +81,16 @@ export default class FieldsService { return data as Field; }); - let aliasFields = await this.knex + const aliasQuery = this.knex .select('*') .from('directus_fields') - .whereIn('special', ['alias', 'o2m']); + .whereIn('special', ['alias', 'o2m', 'm2m']); + + if (collection) { + aliasQuery.andWhere('collection', collection); + } + + let aliasFields = await aliasQuery; aliasFields = (await this.payloadService.processValues('read', aliasFields)) as FieldMeta[]; diff --git a/api/src/services/items.ts b/api/src/services/items.ts index 66f7ab2309..09441e34cd 100644 --- a/api/src/services/items.ts +++ b/api/src/services/items.ts @@ -102,7 +102,9 @@ export default class ItemsService implements AbstractService { return payload; }); - await payloadService.processO2M(payloads); + for (const key of primaryKeys) { + await payloadService.processO2M(payloads, key); + } if (this.accountability) { const activityRecords = primaryKeys.map((key) => ({ @@ -238,18 +240,23 @@ export default class ItemsService implements AbstractService { }); payload = await payloadService.processM2O(payload); - payload = await payloadService.processValues('update', payload); - const payloadWithoutAliases = pick( + let payloadWithoutAliases = pick( payload, columns.map(({ column }) => column) ); - await trx(this.collection) - .update(payloadWithoutAliases) - .whereIn(primaryKeyField, keys); + payloadWithoutAliases = await payloadService.processValues('update', payloadWithoutAliases); - await payloadService.processO2M(payload); + if (Object.keys(payloadWithoutAliases).length > 0) { + await trx(this.collection) + .update(payloadWithoutAliases) + .whereIn(primaryKeyField, keys); + } + + for (const key of keys) { + await payloadService.processO2M(payload, key); + } if (this.accountability) { const activityRecords = keys.map((key) => ({ diff --git a/api/src/services/payload.ts b/api/src/services/payload.ts index 1bee983eeb..23cc7ef74f 100644 --- a/api/src/services/payload.ts +++ b/api/src/services/payload.ts @@ -195,7 +195,12 @@ export default class PayloadService { if (hasPrimaryKey) { relatedPrimaryKey = relatedRecord[relation.one_primary]; - await itemsService.update(relatedRecord, relatedPrimaryKey); + + if (relatedRecord.hasOwnProperty('$delete') && relatedRecord.$delete) { + await itemsService.delete(relatedPrimaryKey); + } else { + await itemsService.update(relatedRecord, relatedPrimaryKey); + } } else { relatedPrimaryKey = await itemsService.create(relatedRecord); } @@ -211,7 +216,7 @@ export default class PayloadService { /** * Recursively save/update all nested related o2m items */ - async processO2M(payload: Partial | Partial[]) { + async processO2M(payload: Partial | Partial[], parent?: PrimaryKey) { const relations = await this.knex .select('*') .from('directus_relations') @@ -234,7 +239,7 @@ export default class PayloadService { const relatedRecords: Partial[] = payload[relation.one_field].map( (record: Partial) => ({ ...record, - [relation.many_field]: payload[relation.one_primary], + [relation.many_field]: parent || payload[relation.one_primary], }) ); @@ -246,12 +251,18 @@ export default class PayloadService { const toBeCreated = relatedRecords.filter( (record) => record.hasOwnProperty(relation.many_primary) === false ); + const toBeUpdated = relatedRecords.filter( - (record) => record.hasOwnProperty(relation.many_primary) === true + (record) => record.hasOwnProperty(relation.many_primary) === true && record.hasOwnProperty('$delete') === false ); + const toBeDeleted = relatedRecords + .filter(record => record.hasOwnProperty(relation.many_primary) === true && record.hasOwnProperty('$delete') && record.$delete === true) + .map(record => record[relation.many_primary]); + await itemsService.create(toBeCreated); await itemsService.update(toBeUpdated); + await itemsService.delete(toBeDeleted); } } } diff --git a/app/package-lock.json b/app/package-lock.json index 8192fb6e2d..fad0dd2397 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -7396,6 +7396,12 @@ "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "dev": true }, + "compare-versions": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-3.6.0.tgz", + "integrity": "sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA==", + "dev": true + }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", @@ -10305,6 +10311,15 @@ } } }, + "find-versions": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz", + "integrity": "sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==", + "dev": true, + "requires": { + "semver-regex": "^2.0.0" + } + }, "flat-cache": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", @@ -11607,6 +11622,57 @@ "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", "dev": true }, + "husky": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/husky/-/husky-4.2.5.tgz", + "integrity": "sha512-SYZ95AjKcX7goYVZtVZF2i6XiZcHknw50iXvY7b0MiGoj5RwdgRQNEHdb+gPDPCXKlzwrybjFjkL6FOj8uRhZQ==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "ci-info": "^2.0.0", + "compare-versions": "^3.6.0", + "cosmiconfig": "^6.0.0", + "find-versions": "^3.2.0", + "opencollective-postinstall": "^2.0.2", + "pkg-dir": "^4.2.0", + "please-upgrade-node": "^3.2.0", + "slash": "^3.0.0", + "which-pm-runs": "^1.0.0" + }, + "dependencies": { + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + } + } + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -19583,6 +19649,12 @@ } } }, + "semver-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz", + "integrity": "sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==", + "dev": true + }, "send": { "version": "0.17.1", "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", @@ -23896,6 +23968,12 @@ "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=", "dev": true }, + "which-pm-runs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.0.0.tgz", + "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=", + "dev": true + }, "wide-align": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", diff --git a/app/package.json b/app/package.json index aa0c5c83df..f980f8c4ef 100644 --- a/app/package.json +++ b/app/package.json @@ -112,6 +112,7 @@ "eslint-plugin-prettier": "^3.1.4", "eslint-plugin-vue": "^6.2.2", "html-loader": "^1.1.0", + "husky": "^4.2.5", "jest-sonar": "^0.2.10", "lint-staged": "^10.2.11", "mockdate": "^3.0.2", @@ -137,8 +138,10 @@ "webpack-assets-manifest": "^3.1.1", "webpack-merge": "^5.0.9" }, - "gitHooks": { - "pre-commit": "lint-staged" + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } }, "lint-staged": { "*.{js,ts}": [ diff --git a/app/src/components/v-upload/v-upload.vue b/app/src/components/v-upload/v-upload.vue index f4b6315030..b70e4036d1 100644 --- a/app/src/components/v-upload/v-upload.vue +++ b/app/src/components/v-upload/v-upload.vue @@ -15,7 +15,7 @@ @@ -29,12 +29,17 @@ diff --git a/app/src/modules/settings/routes/data-model/collections/components/collection-options/collection-options.vue b/app/src/modules/settings/routes/data-model/collections/components/collection-options/collection-options.vue index 8a1ac2bae7..f204cf5bb9 100644 --- a/app/src/modules/settings/routes/data-model/collections/components/collection-options/collection-options.vue +++ b/app/src/modules/settings/routes/data-model/collections/components/collection-options/collection-options.vue @@ -1,6 +1,6 @@