directus 9.6.0 → 9.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/app.js +3 -1
  2. package/dist/auth/drivers/oauth2.js +3 -0
  3. package/dist/auth/drivers/openid.js +3 -0
  4. package/dist/cache.d.ts +4 -1
  5. package/dist/cache.js +29 -7
  6. package/dist/cli/commands/schema/apply.d.ts +1 -0
  7. package/dist/cli/commands/schema/apply.js +9 -5
  8. package/dist/cli/index.js +1 -0
  9. package/dist/controllers/assets.js +9 -1
  10. package/dist/controllers/utils.js +18 -1
  11. package/dist/database/helpers/date/dialects/default.d.ts +3 -0
  12. package/dist/database/helpers/date/dialects/default.js +7 -0
  13. package/dist/database/helpers/date/dialects/sqlite.d.ts +0 -9
  14. package/dist/database/helpers/date/dialects/sqlite.js +0 -24
  15. package/dist/database/helpers/date/index.d.ts +6 -6
  16. package/dist/database/helpers/date/index.js +13 -13
  17. package/dist/database/helpers/date/types.d.ts +0 -9
  18. package/dist/database/helpers/{date → fn}/dialects/mssql.d.ts +3 -2
  19. package/dist/database/helpers/{date → fn}/dialects/mssql.js +14 -3
  20. package/dist/database/helpers/{date → fn}/dialects/mysql.d.ts +3 -2
  21. package/dist/database/helpers/{date → fn}/dialects/mysql.js +14 -3
  22. package/dist/database/helpers/{date → fn}/dialects/oracle.d.ts +3 -2
  23. package/dist/database/helpers/{date → fn}/dialects/oracle.js +14 -3
  24. package/dist/database/helpers/{date → fn}/dialects/postgres.d.ts +3 -2
  25. package/dist/database/helpers/{date → fn}/dialects/postgres.js +14 -3
  26. package/dist/database/helpers/fn/dialects/sqlite.d.ts +13 -0
  27. package/dist/database/helpers/fn/dialects/sqlite.js +42 -0
  28. package/dist/database/helpers/fn/index.d.ts +7 -0
  29. package/dist/database/helpers/fn/index.js +17 -0
  30. package/dist/database/helpers/fn/types.d.ts +18 -0
  31. package/dist/database/helpers/fn/types.js +27 -0
  32. package/dist/database/helpers/index.d.ts +4 -1
  33. package/dist/database/helpers/index.js +7 -1
  34. package/dist/database/index.js +0 -3
  35. package/dist/database/migrations/20220308A-add-bookmark-icon-and-color.d.ts +3 -0
  36. package/dist/database/migrations/20220308A-add-bookmark-icon-and-color.js +17 -0
  37. package/dist/database/migrations/20220314A-add-translation-strings.d.ts +3 -0
  38. package/dist/database/migrations/20220314A-add-translation-strings.js +15 -0
  39. package/dist/database/migrations/20220322A-rename-field-typecast-flags.d.ts +3 -0
  40. package/dist/database/migrations/20220322A-rename-field-typecast-flags.js +77 -0
  41. package/dist/database/migrations/20220323A-add-field-validation.d.ts +3 -0
  42. package/dist/database/migrations/20220323A-add-field-validation.js +17 -0
  43. package/dist/database/migrations/20220325A-fix-typecast-flags.d.ts +3 -0
  44. package/dist/database/migrations/20220325A-fix-typecast-flags.js +49 -0
  45. package/dist/database/migrations/20220325B-add-default-language.d.ts +3 -0
  46. package/dist/database/migrations/20220325B-add-default-language.js +28 -0
  47. package/dist/database/migrations/run.js +1 -1
  48. package/dist/database/run-ast.js +11 -3
  49. package/dist/database/system-data/fields/activity.yaml +4 -4
  50. package/dist/database/system-data/fields/collections.yaml +4 -4
  51. package/dist/database/system-data/fields/fields.yaml +17 -8
  52. package/dist/database/system-data/fields/files.yaml +2 -2
  53. package/dist/database/system-data/fields/panels.yaml +2 -2
  54. package/dist/database/system-data/fields/permissions.yaml +4 -4
  55. package/dist/database/system-data/fields/presets.yaml +17 -3
  56. package/dist/database/system-data/fields/relations.yaml +1 -1
  57. package/dist/database/system-data/fields/revisions.yaml +2 -2
  58. package/dist/database/system-data/fields/roles.yaml +4 -4
  59. package/dist/database/system-data/fields/settings.yaml +19 -4
  60. package/dist/database/system-data/fields/users.yaml +2 -2
  61. package/dist/database/system-data/fields/webhooks.yaml +4 -4
  62. package/dist/env.js +6 -2
  63. package/dist/exceptions/database/dialects/mysql.js +23 -17
  64. package/dist/logger.js +2 -1
  65. package/dist/middleware/respond.js +7 -28
  66. package/dist/services/activity.js +4 -1
  67. package/dist/services/authorization.d.ts +1 -1
  68. package/dist/services/authorization.js +156 -14
  69. package/dist/services/collections.js +222 -198
  70. package/dist/services/fields.js +184 -173
  71. package/dist/services/files.js +69 -3
  72. package/dist/services/graphql.js +2 -2
  73. package/dist/services/import-export.d.ts +34 -0
  74. package/dist/services/import-export.js +270 -0
  75. package/dist/services/index.d.ts +2 -1
  76. package/dist/services/index.js +2 -1
  77. package/dist/services/items.js +1 -0
  78. package/dist/services/payload.js +11 -9
  79. package/dist/services/permissions.js +10 -10
  80. package/dist/services/relations.js +93 -78
  81. package/dist/services/server.js +1 -0
  82. package/dist/services/shares.js +2 -1
  83. package/dist/services/users.js +3 -1
  84. package/dist/utils/apply-query.js +21 -3
  85. package/dist/utils/get-column.d.ts +6 -5
  86. package/dist/utils/get-column.js +16 -8
  87. package/dist/utils/get-date-formatted.d.ts +1 -0
  88. package/dist/utils/get-date-formatted.js +14 -0
  89. package/dist/utils/get-local-type.js +2 -2
  90. package/dist/utils/get-permissions.js +1 -1
  91. package/dist/utils/get-schema.d.ts +1 -1
  92. package/dist/utils/get-schema.js +16 -11
  93. package/dist/utils/track.js +3 -2
  94. package/dist/utils/url.d.ts +1 -1
  95. package/dist/utils/url.js +1 -1
  96. package/dist/utils/validate-storage.js +3 -1
  97. package/package.json +13 -12
  98. package/dist/services/import.d.ts +0 -13
  99. package/dist/services/import.js +0 -118
@@ -0,0 +1,270 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ExportService = exports.ImportService = void 0;
7
+ const database_1 = __importDefault(require("../database"));
8
+ const exceptions_1 = require("../exceptions");
9
+ const StreamArray_1 = __importDefault(require("stream-json/streamers/StreamArray"));
10
+ const items_1 = require("./items");
11
+ const async_1 = require("async");
12
+ const destroy_1 = __importDefault(require("destroy"));
13
+ const csv_parser_1 = __importDefault(require("csv-parser"));
14
+ const lodash_1 = require("lodash");
15
+ const js2xmlparser_1 = require("js2xmlparser");
16
+ const json2csv_1 = require("json2csv");
17
+ const fs_extra_1 = require("fs-extra");
18
+ const tmp_promise_1 = require("tmp-promise");
19
+ const env_1 = __importDefault(require("../env"));
20
+ const files_1 = require("./files");
21
+ const get_date_formatted_1 = require("../utils/get-date-formatted");
22
+ const utils_1 = require("@directus/shared/utils");
23
+ const notifications_1 = require("./notifications");
24
+ const logger_1 = __importDefault(require("../logger"));
25
+ class ImportService {
26
+ constructor(options) {
27
+ this.knex = options.knex || (0, database_1.default)();
28
+ this.accountability = options.accountability || null;
29
+ this.schema = options.schema;
30
+ }
31
+ async import(collection, mimetype, stream) {
32
+ var _a, _b, _c, _d, _e;
33
+ if (collection.startsWith('directus_'))
34
+ throw new exceptions_1.ForbiddenException();
35
+ const createPermissions = (_b = (_a = this.accountability) === null || _a === void 0 ? void 0 : _a.permissions) === null || _b === void 0 ? void 0 : _b.find((permission) => permission.collection === collection && permission.action === 'create');
36
+ const updatePermissions = (_d = (_c = this.accountability) === null || _c === void 0 ? void 0 : _c.permissions) === null || _d === void 0 ? void 0 : _d.find((permission) => permission.collection === collection && permission.action === 'update');
37
+ if (((_e = this.accountability) === null || _e === void 0 ? void 0 : _e.admin) !== true && (!createPermissions || !updatePermissions)) {
38
+ throw new exceptions_1.ForbiddenException();
39
+ }
40
+ switch (mimetype) {
41
+ case 'application/json':
42
+ return await this.importJSON(collection, stream);
43
+ case 'text/csv':
44
+ case 'application/vnd.ms-excel':
45
+ return await this.importCSV(collection, stream);
46
+ default:
47
+ throw new exceptions_1.UnsupportedMediaTypeException(`Can't import files of type "${mimetype}"`);
48
+ }
49
+ }
50
+ importJSON(collection, stream) {
51
+ const extractJSON = StreamArray_1.default.withParser();
52
+ return this.knex.transaction((trx) => {
53
+ const service = new items_1.ItemsService(collection, {
54
+ knex: trx,
55
+ schema: this.schema,
56
+ accountability: this.accountability,
57
+ });
58
+ const saveQueue = (0, async_1.queue)(async (value) => {
59
+ return await service.upsertOne(value);
60
+ });
61
+ return new Promise((resolve, reject) => {
62
+ stream.pipe(extractJSON);
63
+ extractJSON.on('data', ({ value }) => {
64
+ saveQueue.push(value);
65
+ });
66
+ extractJSON.on('error', (err) => {
67
+ (0, destroy_1.default)(stream);
68
+ (0, destroy_1.default)(extractJSON);
69
+ reject(new exceptions_1.InvalidPayloadException(err.message));
70
+ });
71
+ saveQueue.error((err) => {
72
+ reject(err);
73
+ });
74
+ extractJSON.on('end', () => {
75
+ saveQueue.drain(() => {
76
+ return resolve();
77
+ });
78
+ });
79
+ });
80
+ });
81
+ }
82
+ importCSV(collection, stream) {
83
+ return this.knex.transaction((trx) => {
84
+ const service = new items_1.ItemsService(collection, {
85
+ knex: trx,
86
+ schema: this.schema,
87
+ accountability: this.accountability,
88
+ });
89
+ const saveQueue = (0, async_1.queue)(async (value) => {
90
+ return await service.upsertOne(value);
91
+ });
92
+ return new Promise((resolve, reject) => {
93
+ stream
94
+ .pipe((0, csv_parser_1.default)())
95
+ .on('data', (value) => {
96
+ const obj = (0, lodash_1.transform)(value, (result, value, key) => {
97
+ if (value.length === 0) {
98
+ delete result[key];
99
+ }
100
+ else {
101
+ try {
102
+ const parsedJson = JSON.parse(value);
103
+ (0, lodash_1.set)(result, key, parsedJson);
104
+ }
105
+ catch {
106
+ (0, lodash_1.set)(result, key, value);
107
+ }
108
+ }
109
+ });
110
+ saveQueue.push(obj);
111
+ })
112
+ .on('error', (err) => {
113
+ (0, destroy_1.default)(stream);
114
+ reject(new exceptions_1.InvalidPayloadException(err.message));
115
+ })
116
+ .on('end', () => {
117
+ saveQueue.drain(() => {
118
+ return resolve();
119
+ });
120
+ });
121
+ saveQueue.error((err) => {
122
+ reject(err);
123
+ });
124
+ });
125
+ });
126
+ }
127
+ }
128
+ exports.ImportService = ImportService;
129
+ class ExportService {
130
+ constructor(options) {
131
+ this.knex = options.knex || (0, database_1.default)();
132
+ this.accountability = options.accountability || null;
133
+ this.schema = options.schema;
134
+ }
135
+ /**
136
+ * Export the query results as a named file. Will query in batches, and keep appending a tmp file
137
+ * until all the data is retrieved. Uploads the result as a new file using the regular
138
+ * FilesService upload method.
139
+ */
140
+ async exportToFile(collection, query, format, options) {
141
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
142
+ try {
143
+ const mimeTypes = {
144
+ xml: 'text/xml',
145
+ csv: 'text/csv',
146
+ json: 'application/json',
147
+ };
148
+ const database = (0, database_1.default)();
149
+ const { path, cleanup } = await (0, tmp_promise_1.file)();
150
+ await database.transaction(async (trx) => {
151
+ var _a;
152
+ const service = new items_1.ItemsService(collection, {
153
+ accountability: this.accountability,
154
+ schema: this.schema,
155
+ knex: trx,
156
+ });
157
+ const totalCount = await service
158
+ .readByQuery({
159
+ ...query,
160
+ aggregate: {
161
+ count: ['*'],
162
+ },
163
+ })
164
+ .then((result) => { var _a, _b; return Number((_b = (_a = result === null || result === void 0 ? void 0 : result[0]) === null || _a === void 0 ? void 0 : _a.count) !== null && _b !== void 0 ? _b : 0); });
165
+ const count = query.limit ? Math.min(totalCount, query.limit) : totalCount;
166
+ const requestedLimit = (_a = query.limit) !== null && _a !== void 0 ? _a : -1;
167
+ const batchesRequired = Math.ceil(count / env_1.default.EXPORT_BATCH_SIZE);
168
+ let readCount = 0;
169
+ for (let batch = 0; batch <= batchesRequired; batch++) {
170
+ let limit = env_1.default.EXPORT_BATCH_SIZE;
171
+ if (requestedLimit > 0 && env_1.default.EXPORT_BATCH_SIZE > requestedLimit - readCount) {
172
+ limit = requestedLimit - readCount;
173
+ }
174
+ const result = await service.readByQuery({
175
+ ...query,
176
+ limit,
177
+ page: batch,
178
+ });
179
+ readCount += result.length;
180
+ if (result.length) {
181
+ await (0, fs_extra_1.appendFile)(path, this.transform(result, format, {
182
+ includeHeader: batch === 0,
183
+ includeFooter: batch + 1 === batchesRequired,
184
+ }));
185
+ }
186
+ }
187
+ });
188
+ const filesService = new files_1.FilesService({
189
+ accountability: this.accountability,
190
+ schema: this.schema,
191
+ });
192
+ const storage = (0, utils_1.toArray)(env_1.default.STORAGE_LOCATIONS)[0];
193
+ const title = `export-${collection}-${(0, get_date_formatted_1.getDateFormatted)()}`;
194
+ const filename = `${title}.${format}`;
195
+ const fileWithDefaults = {
196
+ ...((_a = options === null || options === void 0 ? void 0 : options.file) !== null && _a !== void 0 ? _a : {}),
197
+ title: (_c = (_b = options === null || options === void 0 ? void 0 : options.file) === null || _b === void 0 ? void 0 : _b.title) !== null && _c !== void 0 ? _c : title,
198
+ filename_download: (_e = (_d = options === null || options === void 0 ? void 0 : options.file) === null || _d === void 0 ? void 0 : _d.filename_download) !== null && _e !== void 0 ? _e : filename,
199
+ storage: (_g = (_f = options === null || options === void 0 ? void 0 : options.file) === null || _f === void 0 ? void 0 : _f.storage) !== null && _g !== void 0 ? _g : storage,
200
+ type: mimeTypes[format],
201
+ };
202
+ const savedFile = await filesService.uploadOne((0, fs_extra_1.createReadStream)(path), fileWithDefaults);
203
+ if ((_h = this.accountability) === null || _h === void 0 ? void 0 : _h.user) {
204
+ const notificationsService = new notifications_1.NotificationsService({
205
+ accountability: this.accountability,
206
+ schema: this.schema,
207
+ });
208
+ await notificationsService.createOne({
209
+ recipient: this.accountability.user,
210
+ sender: this.accountability.user,
211
+ subject: `Your export of ${collection} is ready`,
212
+ collection: `directus_files`,
213
+ item: savedFile,
214
+ });
215
+ }
216
+ await cleanup();
217
+ }
218
+ catch (err) {
219
+ logger_1.default.error(err, `Couldn't export ${collection}: ${err.message}`);
220
+ if ((_j = this.accountability) === null || _j === void 0 ? void 0 : _j.user) {
221
+ const notificationsService = new notifications_1.NotificationsService({
222
+ accountability: this.accountability,
223
+ schema: this.schema,
224
+ });
225
+ await notificationsService.createOne({
226
+ recipient: this.accountability.user,
227
+ sender: this.accountability.user,
228
+ subject: `Your export of ${collection} failed`,
229
+ message: `Please contact your system administrator for more information.`,
230
+ });
231
+ }
232
+ }
233
+ }
234
+ /**
235
+ * Transform a given input object / array to the given type
236
+ */
237
+ transform(input, format, options) {
238
+ if (format === 'json') {
239
+ let string = JSON.stringify(input || null, null, '\t');
240
+ if ((options === null || options === void 0 ? void 0 : options.includeHeader) === false)
241
+ string = string.split('\n').slice(1).join('\n');
242
+ if ((options === null || options === void 0 ? void 0 : options.includeFooter) === false) {
243
+ const lines = string.split('\n');
244
+ string = lines.slice(0, lines.length - 1).join('\n');
245
+ string += ',\n';
246
+ }
247
+ return string;
248
+ }
249
+ if (format === 'xml') {
250
+ let string = (0, js2xmlparser_1.parse)('data', input);
251
+ if ((options === null || options === void 0 ? void 0 : options.includeHeader) === false)
252
+ string = string.split('\n').slice(2).join('\n');
253
+ if ((options === null || options === void 0 ? void 0 : options.includeFooter) === false) {
254
+ const lines = string.split('\n');
255
+ string = lines.slice(0, lines.length - 1).join('\n');
256
+ string += '\n';
257
+ }
258
+ return string;
259
+ }
260
+ if (format === 'csv') {
261
+ const parser = new json2csv_1.Parser({
262
+ transforms: [json2csv_1.transforms.flatten({ separator: '.' })],
263
+ header: (options === null || options === void 0 ? void 0 : options.includeHeader) !== false,
264
+ });
265
+ return parser.parse(input);
266
+ }
267
+ throw new exceptions_1.ServiceUnavailableException(`Illegal export type used: "${format}"`, { service: 'export' });
268
+ }
269
+ }
270
+ exports.ExportService = ExportService;
@@ -2,13 +2,14 @@ export * from './items';
2
2
  export * from './activity';
3
3
  export * from './assets';
4
4
  export * from './authentication';
5
+ export * from './authorization';
5
6
  export * from './collections';
6
7
  export * from './dashboards';
7
8
  export * from './fields';
8
9
  export * from './files';
9
10
  export * from './folders';
10
11
  export * from './graphql';
11
- export * from './import';
12
+ export * from './import-export';
12
13
  export * from './mail';
13
14
  export * from './meta';
14
15
  export * from './notifications';
@@ -15,13 +15,14 @@ __exportStar(require("./items"), exports);
15
15
  __exportStar(require("./activity"), exports);
16
16
  __exportStar(require("./assets"), exports);
17
17
  __exportStar(require("./authentication"), exports);
18
+ __exportStar(require("./authorization"), exports);
18
19
  __exportStar(require("./collections"), exports);
19
20
  __exportStar(require("./dashboards"), exports);
20
21
  __exportStar(require("./fields"), exports);
21
22
  __exportStar(require("./files"), exports);
22
23
  __exportStar(require("./folders"), exports);
23
24
  __exportStar(require("./graphql"), exports);
24
- __exportStar(require("./import"), exports);
25
+ __exportStar(require("./import-export"), exports);
25
26
  __exportStar(require("./mail"), exports);
26
27
  __exportStar(require("./meta"), exports);
27
28
  __exportStar(require("./notifications"), exports);
@@ -496,6 +496,7 @@ class ItemsService {
496
496
  if ((opts === null || opts === void 0 ? void 0 : opts.emitEvents) !== false) {
497
497
  emitter_1.default.emitAction(this.eventScope === 'items' ? ['items.delete', `${this.collection}.items.delete`] : `${this.eventScope}.delete`, {
498
498
  payload: keys,
499
+ keys: keys,
499
500
  collection: this.collection,
500
501
  }, {
501
502
  // This hook is called async. If we would pass the transaction here, the hook can be
@@ -37,7 +37,7 @@ class PayloadService {
37
37
  }
38
38
  return value;
39
39
  },
40
- async boolean({ action, value }) {
40
+ async 'cast-boolean'({ action, value }) {
41
41
  if (action === 'read') {
42
42
  if (value === true || value === 1 || value === '1') {
43
43
  return true;
@@ -51,7 +51,7 @@ class PayloadService {
51
51
  }
52
52
  return value;
53
53
  },
54
- async json({ action, value }) {
54
+ async 'cast-json'({ action, value }) {
55
55
  if (action === 'read') {
56
56
  if (typeof value === 'string') {
57
57
  try {
@@ -99,7 +99,7 @@ class PayloadService {
99
99
  return new Date();
100
100
  return value;
101
101
  },
102
- async csv({ action, value }) {
102
+ async 'cast-csv'({ action, value }) {
103
103
  if (Array.isArray(value) === false && typeof value !== 'string')
104
104
  return;
105
105
  if (action === 'read' && Array.isArray(value) === false) {
@@ -247,7 +247,7 @@ class PayloadService {
247
247
  payload[name] = newValue;
248
248
  }
249
249
  if (dateColumn.type === 'date') {
250
- const [year, month, day] = value.toISOString().substr(0, 10).split('-');
250
+ const [year, month, day] = value.toISOString().slice(0, 10).split('-');
251
251
  // Strip off the time / timezone information from a date-only value
252
252
  const newValue = `${year}-${month}-${day}`;
253
253
  payload[name] = newValue;
@@ -429,7 +429,7 @@ class PayloadService {
429
429
  delete: joi_1.default.array().items(joi_1.default.string(), joi_1.default.number()),
430
430
  });
431
431
  for (const relation of relationsToProcess) {
432
- if (!relation.meta || !payload[relation.meta.one_field])
432
+ if (!relation.meta)
433
433
  continue;
434
434
  const currentPrimaryKeyField = this.schema.collections[relation.related_collection].primary;
435
435
  const relatedPrimaryKeyField = this.schema.collections[relation.collection].primary;
@@ -441,9 +441,11 @@ class PayloadService {
441
441
  const recordsToUpsert = [];
442
442
  const savedPrimaryKeys = [];
443
443
  // Nested array of individual items
444
- if (Array.isArray(payload[relation.meta.one_field])) {
445
- for (let i = 0; i < (payload[relation.meta.one_field] || []).length; i++) {
446
- const relatedRecord = (payload[relation.meta.one_field] || [])[i];
444
+ const field = payload[relation.meta.one_field];
445
+ if (!field || Array.isArray(field)) {
446
+ const updates = field || []; // treat falsey values as removing all children
447
+ for (let i = 0; i < updates.length; i++) {
448
+ const relatedRecord = updates[i];
447
449
  let record = (0, lodash_1.cloneDeep)(relatedRecord);
448
450
  if (typeof relatedRecord === 'string' || typeof relatedRecord === 'number') {
449
451
  const existingRecord = await this.knex
@@ -507,7 +509,7 @@ class PayloadService {
507
509
  }
508
510
  // "Updates" object w/ create/update/delete
509
511
  else {
510
- const alterations = payload[relation.meta.one_field];
512
+ const alterations = field;
511
513
  const { error } = nestedUpdateSchema.validate(alterations);
512
514
  if (error)
513
515
  throw new exceptions_1.InvalidPayloadException(`Invalid one-to-many update structure: ${error.message}`);
@@ -52,52 +52,52 @@ class PermissionsService extends items_1.ItemsService {
52
52
  }
53
53
  async createOne(data, opts) {
54
54
  const res = await super.createOne(data, opts);
55
- await this.systemCache.clear();
55
+ await (0, cache_1.clearSystemCache)();
56
56
  return res;
57
57
  }
58
58
  async createMany(data, opts) {
59
59
  const res = await super.createMany(data, opts);
60
- await this.systemCache.clear();
60
+ await (0, cache_1.clearSystemCache)();
61
61
  return res;
62
62
  }
63
63
  async updateByQuery(query, data, opts) {
64
64
  const res = await super.updateByQuery(query, data, opts);
65
- await this.systemCache.clear();
65
+ await (0, cache_1.clearSystemCache)();
66
66
  return res;
67
67
  }
68
68
  async updateOne(key, data, opts) {
69
69
  const res = await super.updateOne(key, data, opts);
70
- await this.systemCache.clear();
70
+ await (0, cache_1.clearSystemCache)();
71
71
  return res;
72
72
  }
73
73
  async updateMany(keys, data, opts) {
74
74
  const res = await super.updateMany(keys, data, opts);
75
- await this.systemCache.clear();
75
+ await (0, cache_1.clearSystemCache)();
76
76
  return res;
77
77
  }
78
78
  async upsertOne(payload, opts) {
79
79
  const res = await super.upsertOne(payload, opts);
80
- await this.systemCache.clear();
80
+ await (0, cache_1.clearSystemCache)();
81
81
  return res;
82
82
  }
83
83
  async upsertMany(payloads, opts) {
84
84
  const res = await super.upsertMany(payloads, opts);
85
- await this.systemCache.clear();
85
+ await (0, cache_1.clearSystemCache)();
86
86
  return res;
87
87
  }
88
88
  async deleteByQuery(query, opts) {
89
89
  const res = await super.deleteByQuery(query, opts);
90
- await this.systemCache.clear();
90
+ await (0, cache_1.clearSystemCache)();
91
91
  return res;
92
92
  }
93
93
  async deleteOne(key, opts) {
94
94
  const res = await super.deleteOne(key, opts);
95
- await this.systemCache.clear();
95
+ await (0, cache_1.clearSystemCache)();
96
96
  return res;
97
97
  }
98
98
  async deleteMany(keys, opts) {
99
99
  const res = await super.deleteMany(keys, opts);
100
- await this.systemCache.clear();
100
+ await (0, cache_1.clearSystemCache)();
101
101
  return res;
102
102
  }
103
103
  }
@@ -146,36 +146,40 @@ class RelationsService {
146
146
  if (existingRelation) {
147
147
  throw new exceptions_1.InvalidPayloadException(`Field "${relation.field}" in collection "${relation.collection}" already has an associated relationship`);
148
148
  }
149
- const metaRow = {
150
- ...(relation.meta || {}),
151
- many_collection: relation.collection,
152
- many_field: relation.field,
153
- one_collection: relation.related_collection || null,
154
- };
155
- await this.knex.transaction(async (trx) => {
156
- if (relation.related_collection) {
157
- await trx.schema.alterTable(relation.collection, async (table) => {
158
- var _a;
159
- this.alterType(table, relation);
160
- const constraintName = (0, get_default_index_name_1.getDefaultIndexName)('foreign', relation.collection, relation.field);
161
- const builder = table
162
- .foreign(relation.field, constraintName)
163
- .references(`${relation.related_collection}.${this.schema.collections[relation.related_collection].primary}`);
164
- if ((_a = relation.schema) === null || _a === void 0 ? void 0 : _a.on_delete) {
165
- builder.onDelete(relation.schema.on_delete);
166
- }
149
+ try {
150
+ const metaRow = {
151
+ ...(relation.meta || {}),
152
+ many_collection: relation.collection,
153
+ many_field: relation.field,
154
+ one_collection: relation.related_collection || null,
155
+ };
156
+ await this.knex.transaction(async (trx) => {
157
+ if (relation.related_collection) {
158
+ await trx.schema.alterTable(relation.collection, async (table) => {
159
+ var _a;
160
+ this.alterType(table, relation);
161
+ const constraintName = (0, get_default_index_name_1.getDefaultIndexName)('foreign', relation.collection, relation.field);
162
+ const builder = table
163
+ .foreign(relation.field, constraintName)
164
+ .references(`${relation.related_collection}.${this.schema.collections[relation.related_collection].primary}`);
165
+ if ((_a = relation.schema) === null || _a === void 0 ? void 0 : _a.on_delete) {
166
+ builder.onDelete(relation.schema.on_delete);
167
+ }
168
+ });
169
+ }
170
+ const relationsItemService = new items_1.ItemsService('directus_relations', {
171
+ knex: trx,
172
+ schema: this.schema,
173
+ // We don't set accountability here. If you have read access to certain fields, you are
174
+ // allowed to extract the relations regardless of permissions to directus_relations. This
175
+ // happens in `filterForbidden` down below
167
176
  });
168
- }
169
- const relationsItemService = new items_1.ItemsService('directus_relations', {
170
- knex: trx,
171
- schema: this.schema,
172
- // We don't set accountability here. If you have read access to certain fields, you are
173
- // allowed to extract the relations regardless of permissions to directus_relations. This
174
- // happens in `filterForbidden` down below
177
+ await relationsItemService.createOne(metaRow);
175
178
  });
176
- await relationsItemService.createOne(metaRow);
177
- });
178
- await this.systemCache.clear();
179
+ }
180
+ finally {
181
+ await (0, cache_1.clearSystemCache)();
182
+ }
179
183
  }
180
184
  /**
181
185
  * Update an existing foreign key constraint
@@ -196,47 +200,51 @@ class RelationsService {
196
200
  if (!existingRelation) {
197
201
  throw new exceptions_1.InvalidPayloadException(`Field "${field}" in collection "${collection}" doesn't have a relationship.`);
198
202
  }
199
- await this.knex.transaction(async (trx) => {
200
- if (existingRelation.related_collection) {
201
- await trx.schema.alterTable(collection, async (table) => {
202
- var _a;
203
- let constraintName = (0, get_default_index_name_1.getDefaultIndexName)('foreign', collection, field);
204
- // If the FK already exists in the DB, drop it first
205
- if (existingRelation === null || existingRelation === void 0 ? void 0 : existingRelation.schema) {
206
- constraintName = existingRelation.schema.constraint_name || constraintName;
207
- table.dropForeign(field, constraintName);
203
+ try {
204
+ await this.knex.transaction(async (trx) => {
205
+ if (existingRelation.related_collection) {
206
+ await trx.schema.alterTable(collection, async (table) => {
207
+ var _a;
208
+ let constraintName = (0, get_default_index_name_1.getDefaultIndexName)('foreign', collection, field);
209
+ // If the FK already exists in the DB, drop it first
210
+ if (existingRelation === null || existingRelation === void 0 ? void 0 : existingRelation.schema) {
211
+ constraintName = existingRelation.schema.constraint_name || constraintName;
212
+ table.dropForeign(field, constraintName);
213
+ }
214
+ this.alterType(table, relation);
215
+ const builder = table
216
+ .foreign(field, constraintName || undefined)
217
+ .references(`${existingRelation.related_collection}.${this.schema.collections[existingRelation.related_collection].primary}`);
218
+ if ((_a = relation.schema) === null || _a === void 0 ? void 0 : _a.on_delete) {
219
+ builder.onDelete(relation.schema.on_delete);
220
+ }
221
+ });
222
+ }
223
+ const relationsItemService = new items_1.ItemsService('directus_relations', {
224
+ knex: trx,
225
+ schema: this.schema,
226
+ // We don't set accountability here. If you have read access to certain fields, you are
227
+ // allowed to extract the relations regardless of permissions to directus_relations. This
228
+ // happens in `filterForbidden` down below
229
+ });
230
+ if (relation.meta) {
231
+ if (existingRelation === null || existingRelation === void 0 ? void 0 : existingRelation.meta) {
232
+ await relationsItemService.updateOne(existingRelation.meta.id, relation.meta);
208
233
  }
209
- this.alterType(table, relation);
210
- const builder = table
211
- .foreign(field, constraintName || undefined)
212
- .references(`${existingRelation.related_collection}.${this.schema.collections[existingRelation.related_collection].primary}`);
213
- if ((_a = relation.schema) === null || _a === void 0 ? void 0 : _a.on_delete) {
214
- builder.onDelete(relation.schema.on_delete);
234
+ else {
235
+ await relationsItemService.createOne({
236
+ ...(relation.meta || {}),
237
+ many_collection: relation.collection,
238
+ many_field: relation.field,
239
+ one_collection: existingRelation.related_collection || null,
240
+ });
215
241
  }
216
- });
217
- }
218
- const relationsItemService = new items_1.ItemsService('directus_relations', {
219
- knex: trx,
220
- schema: this.schema,
221
- // We don't set accountability here. If you have read access to certain fields, you are
222
- // allowed to extract the relations regardless of permissions to directus_relations. This
223
- // happens in `filterForbidden` down below
224
- });
225
- if (relation.meta) {
226
- if (existingRelation === null || existingRelation === void 0 ? void 0 : existingRelation.meta) {
227
- await relationsItemService.updateOne(existingRelation.meta.id, relation.meta);
228
- }
229
- else {
230
- await relationsItemService.createOne({
231
- ...(relation.meta || {}),
232
- many_collection: relation.collection,
233
- many_field: relation.field,
234
- one_collection: existingRelation.related_collection || null,
235
- });
236
242
  }
237
- }
238
- });
239
- await this.systemCache.clear();
243
+ });
244
+ }
245
+ finally {
246
+ await (0, cache_1.clearSystemCache)();
247
+ }
240
248
  }
241
249
  /**
242
250
  * Delete an existing relationship
@@ -255,18 +263,25 @@ class RelationsService {
255
263
  if (!existingRelation) {
256
264
  throw new exceptions_1.InvalidPayloadException(`Field "${field}" in collection "${collection}" doesn't have a relationship.`);
257
265
  }
258
- await this.knex.transaction(async (trx) => {
259
- var _a;
260
- if ((_a = existingRelation.schema) === null || _a === void 0 ? void 0 : _a.constraint_name) {
261
- await trx.schema.alterTable(existingRelation.collection, (table) => {
262
- table.dropForeign(existingRelation.field, existingRelation.schema.constraint_name);
263
- });
264
- }
265
- if (existingRelation.meta) {
266
- await trx('directus_relations').delete().where({ many_collection: collection, many_field: field });
267
- }
268
- });
269
- await this.systemCache.clear();
266
+ try {
267
+ await this.knex.transaction(async (trx) => {
268
+ var _a;
269
+ const existingConstraints = await this.schemaInspector.foreignKeys();
270
+ const constraintNames = existingConstraints.map((key) => key.constraint_name);
271
+ if (((_a = existingRelation.schema) === null || _a === void 0 ? void 0 : _a.constraint_name) &&
272
+ constraintNames.includes(existingRelation.schema.constraint_name)) {
273
+ await trx.schema.alterTable(existingRelation.collection, (table) => {
274
+ table.dropForeign(existingRelation.field, existingRelation.schema.constraint_name);
275
+ });
276
+ }
277
+ if (existingRelation.meta) {
278
+ await trx('directus_relations').delete().where({ many_collection: collection, many_field: field });
279
+ }
280
+ });
281
+ }
282
+ finally {
283
+ await (0, cache_1.clearSystemCache)();
284
+ }
270
285
  }
271
286
  /**
272
287
  * Whether or not the current user has read access to relations
@@ -55,6 +55,7 @@ class ServerService {
55
55
  'project_descriptor',
56
56
  'project_logo',
57
57
  'project_color',
58
+ 'default_language',
58
59
  'public_foreground',
59
60
  'public_background',
60
61
  'public_note',