directus 9.22.3 → 9.23.1

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 (120) hide show
  1. package/dist/app.js +5 -4
  2. package/dist/auth/drivers/ldap.d.ts +2 -2
  3. package/dist/auth/drivers/ldap.js +8 -8
  4. package/dist/auth/drivers/oauth2.js +2 -2
  5. package/dist/auth/drivers/openid.js +2 -2
  6. package/dist/cache.js +4 -4
  7. package/dist/cli/commands/schema/apply.js +19 -17
  8. package/dist/cli/utils/create-db-connection.d.ts +2 -1
  9. package/dist/cli/utils/create-env/env-stub.liquid +1 -1
  10. package/dist/cli/utils/drivers.d.ts +3 -9
  11. package/dist/constants.d.ts +2 -8
  12. package/dist/constants.js +3 -7
  13. package/dist/controllers/assets.js +5 -5
  14. package/dist/controllers/extensions.js +7 -7
  15. package/dist/controllers/files.js +1 -1
  16. package/dist/controllers/graphql.js +8 -0
  17. package/dist/controllers/schema.d.ts +2 -0
  18. package/dist/controllers/schema.js +98 -0
  19. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +1 -1
  20. package/dist/database/helpers/schema/dialects/oracle.d.ts +4 -1
  21. package/dist/database/helpers/schema/dialects/oracle.js +25 -0
  22. package/dist/database/helpers/schema/types.d.ts +8 -6
  23. package/dist/database/helpers/schema/types.js +7 -1
  24. package/dist/database/index.d.ts +2 -1
  25. package/dist/database/run-ast.js +2 -2
  26. package/dist/env.js +9 -2
  27. package/dist/extensions.js +1 -1
  28. package/dist/flows.js +17 -8
  29. package/dist/middleware/cache.js +2 -2
  30. package/dist/middleware/respond.js +14 -9
  31. package/dist/operations/item-create/index.test.d.ts +1 -0
  32. package/dist/operations/item-update/index.test.d.ts +1 -0
  33. package/dist/operations/log/index.test.d.ts +1 -0
  34. package/dist/operations/notification/index.test.d.ts +1 -0
  35. package/dist/operations/request/index.js +2 -1
  36. package/dist/operations/request/index.test.d.ts +1 -0
  37. package/dist/operations/sleep/index.test.d.ts +1 -0
  38. package/dist/operations/transform/index.test.d.ts +1 -0
  39. package/dist/operations/trigger/index.d.ts +2 -0
  40. package/dist/operations/trigger/index.js +26 -9
  41. package/dist/operations/trigger/index.test.d.ts +1 -0
  42. package/dist/request/index.d.ts +5 -0
  43. package/dist/request/index.js +18 -0
  44. package/dist/request/index.test.d.ts +1 -0
  45. package/dist/request/request-interceptor.d.ts +2 -0
  46. package/dist/request/request-interceptor.js +33 -0
  47. package/dist/request/request-interceptor.test.d.ts +1 -0
  48. package/dist/request/response-interceptor.d.ts +2 -0
  49. package/dist/request/response-interceptor.js +9 -0
  50. package/dist/request/response-interceptor.test.d.ts +1 -0
  51. package/dist/request/validate-ip.d.ts +1 -0
  52. package/dist/request/validate-ip.js +27 -0
  53. package/dist/request/validate-ip.test.d.ts +1 -0
  54. package/dist/services/assets.d.ts +1 -1
  55. package/dist/services/assets.js +11 -2
  56. package/dist/services/authentication.js +5 -5
  57. package/dist/services/fields.js +1 -0
  58. package/dist/services/files.js +44 -88
  59. package/dist/services/graphql/index.js +14 -8
  60. package/dist/services/graphql/utils/process-error.js +22 -9
  61. package/dist/services/import-export.d.ts +4 -2
  62. package/dist/services/import-export.js +17 -3
  63. package/dist/services/import-export.test.d.ts +1 -0
  64. package/dist/services/index.d.ts +1 -0
  65. package/dist/services/index.js +1 -0
  66. package/dist/services/items.js +34 -15
  67. package/dist/services/relations.js +2 -0
  68. package/dist/services/roles.js +32 -11
  69. package/dist/services/schema.d.ts +15 -0
  70. package/dist/services/schema.js +58 -0
  71. package/dist/services/schema.test.d.ts +1 -0
  72. package/dist/services/shares.d.ts +2 -2
  73. package/dist/services/shares.js +9 -9
  74. package/dist/services/users.js +74 -47
  75. package/dist/types/assets.d.ts +1 -1
  76. package/dist/types/database.d.ts +3 -0
  77. package/dist/types/database.js +4 -0
  78. package/dist/types/index.d.ts +1 -0
  79. package/dist/types/index.js +1 -0
  80. package/dist/types/items.d.ts +5 -0
  81. package/dist/types/snapshot.d.ts +22 -0
  82. package/dist/types/snapshot.js +14 -0
  83. package/dist/utils/apply-diff.d.ts +9 -0
  84. package/dist/utils/apply-diff.js +259 -0
  85. package/dist/utils/apply-diff.test.d.ts +1 -0
  86. package/dist/utils/apply-query.js +8 -6
  87. package/dist/utils/apply-snapshot.d.ts +1 -3
  88. package/dist/utils/apply-snapshot.js +4 -234
  89. package/dist/utils/get-cache-headers.d.ts +3 -1
  90. package/dist/utils/get-cache-headers.js +20 -19
  91. package/dist/utils/get-cache-headers.test.d.ts +1 -0
  92. package/dist/utils/get-milliseconds.d.ts +4 -0
  93. package/dist/utils/get-milliseconds.js +15 -0
  94. package/dist/utils/get-milliseconds.test.d.ts +1 -0
  95. package/dist/utils/get-snapshot-diff.js +11 -7
  96. package/dist/utils/get-snapshot.js +29 -6
  97. package/dist/utils/get-versioned-hash.d.ts +1 -0
  98. package/dist/utils/get-versioned-hash.js +12 -0
  99. package/dist/utils/get-versioned-hash.test.d.ts +1 -0
  100. package/dist/utils/map-values-deep.d.ts +1 -0
  101. package/dist/utils/map-values-deep.js +29 -0
  102. package/dist/utils/map-values-deep.test.d.ts +1 -0
  103. package/dist/utils/sanitize-schema.d.ts +30 -0
  104. package/dist/utils/sanitize-schema.js +80 -0
  105. package/dist/utils/sanitize-schema.test.d.ts +1 -0
  106. package/dist/utils/track.js +3 -3
  107. package/dist/utils/url.js +2 -6
  108. package/dist/utils/url.test.d.ts +1 -0
  109. package/dist/utils/validate-diff.d.ts +7 -0
  110. package/dist/utils/validate-diff.js +114 -0
  111. package/dist/utils/validate-diff.test.d.ts +1 -0
  112. package/dist/utils/validate-query.js +2 -2
  113. package/dist/utils/validate-query.test.d.ts +1 -0
  114. package/dist/utils/validate-snapshot.d.ts +5 -0
  115. package/dist/utils/validate-snapshot.js +71 -0
  116. package/dist/utils/validate-snapshot.test.d.ts +1 -0
  117. package/dist/utils/with-timeout.d.ts +1 -0
  118. package/dist/utils/with-timeout.js +16 -0
  119. package/dist/webhooks.js +3 -2
  120. package/package.json +54 -53
@@ -0,0 +1,259 @@
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.isNestedMetaUpdate = exports.applyDiff = void 0;
7
+ const services_1 = require("../services");
8
+ const types_1 = require("../types");
9
+ const get_schema_1 = require("./get-schema");
10
+ const database_1 = __importDefault(require("../database"));
11
+ const deep_diff_1 = require("deep-diff");
12
+ const lodash_1 = require("lodash");
13
+ const logger_1 = __importDefault(require("../logger"));
14
+ const emitter_1 = __importDefault(require("../emitter"));
15
+ const cache_1 = require("../cache");
16
+ async function applyDiff(currentSnapshot, snapshotDiff, options) {
17
+ var _a, _b;
18
+ const database = (_a = options === null || options === void 0 ? void 0 : options.database) !== null && _a !== void 0 ? _a : (0, database_1.default)();
19
+ const schema = (_b = options === null || options === void 0 ? void 0 : options.schema) !== null && _b !== void 0 ? _b : (await (0, get_schema_1.getSchema)({ database, bypassCache: true }));
20
+ const nestedActionEvents = [];
21
+ const mutationOptions = {
22
+ autoPurgeSystemCache: false,
23
+ bypassEmitAction: (params) => nestedActionEvents.push(params),
24
+ };
25
+ await database.transaction(async (trx) => {
26
+ const collectionsService = new services_1.CollectionsService({ knex: trx, schema });
27
+ const getNestedCollectionsToCreate = (currentLevelCollection) => snapshotDiff.collections.filter(({ diff }) => { var _a, _b; return ((_b = (_a = diff[0].rhs) === null || _a === void 0 ? void 0 : _a.meta) === null || _b === void 0 ? void 0 : _b.group) === currentLevelCollection; });
28
+ const getNestedCollectionsToDelete = (currentLevelCollection) => snapshotDiff.collections.filter(({ diff }) => { var _a, _b; return ((_b = (_a = diff[0].lhs) === null || _a === void 0 ? void 0 : _a.meta) === null || _b === void 0 ? void 0 : _b.group) === currentLevelCollection; });
29
+ const createCollections = async (collections) => {
30
+ for (const { collection, diff } of collections) {
31
+ if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === types_1.DiffKind.NEW && diff[0].rhs) {
32
+ // We'll nest the to-be-created fields in the same collection creation, to prevent
33
+ // creating a collection without a primary key
34
+ const fields = snapshotDiff.fields
35
+ .filter((fieldDiff) => fieldDiff.collection === collection)
36
+ .map((fieldDiff) => fieldDiff.diff[0].rhs)
37
+ .map((fieldDiff) => {
38
+ var _a, _b, _c, _d, _e;
39
+ // Casts field type to UUID when applying non-PostgreSQL schema onto PostgreSQL database.
40
+ // This is needed because they snapshots UUID fields as char/varchar with length 36.
41
+ if (['char', 'varchar'].includes(String((_a = fieldDiff.schema) === null || _a === void 0 ? void 0 : _a.data_type).toLowerCase()) &&
42
+ ((_b = fieldDiff.schema) === null || _b === void 0 ? void 0 : _b.max_length) === 36 &&
43
+ (((_c = fieldDiff.schema) === null || _c === void 0 ? void 0 : _c.is_primary_key) ||
44
+ (((_d = fieldDiff.schema) === null || _d === void 0 ? void 0 : _d.foreign_key_table) && ((_e = fieldDiff.schema) === null || _e === void 0 ? void 0 : _e.foreign_key_column)))) {
45
+ return (0, lodash_1.merge)(fieldDiff, { type: 'uuid', schema: { data_type: 'uuid', max_length: null } });
46
+ }
47
+ else {
48
+ return fieldDiff;
49
+ }
50
+ });
51
+ try {
52
+ await collectionsService.createOne({
53
+ ...diff[0].rhs,
54
+ fields,
55
+ }, mutationOptions);
56
+ }
57
+ catch (err) {
58
+ logger_1.default.error(`Failed to create collection "${collection}"`);
59
+ throw err;
60
+ }
61
+ // Now that the fields are in for this collection, we can strip them from the field edits
62
+ snapshotDiff.fields = snapshotDiff.fields.filter((fieldDiff) => fieldDiff.collection !== collection);
63
+ await createCollections(getNestedCollectionsToCreate(collection));
64
+ }
65
+ }
66
+ };
67
+ const deleteCollections = async (collections) => {
68
+ for (const { collection, diff } of collections) {
69
+ if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === types_1.DiffKind.DELETE) {
70
+ const relations = schema.relations.filter((r) => r.related_collection === collection || r.collection === collection);
71
+ if (relations.length > 0) {
72
+ const relationsService = new services_1.RelationsService({ knex: trx, schema });
73
+ for (const relation of relations) {
74
+ try {
75
+ await relationsService.deleteOne(relation.collection, relation.field, mutationOptions);
76
+ }
77
+ catch (err) {
78
+ logger_1.default.error(`Failed to delete collection "${collection}" due to relation "${relation.collection}.${relation.field}"`);
79
+ throw err;
80
+ }
81
+ }
82
+ // clean up deleted relations from existing schema
83
+ schema.relations = schema.relations.filter((r) => r.related_collection !== collection && r.collection !== collection);
84
+ }
85
+ await deleteCollections(getNestedCollectionsToDelete(collection));
86
+ try {
87
+ await collectionsService.deleteOne(collection, mutationOptions);
88
+ }
89
+ catch (err) {
90
+ logger_1.default.error(`Failed to delete collection "${collection}"`);
91
+ throw err;
92
+ }
93
+ }
94
+ }
95
+ };
96
+ // Finds all collections that need to be created
97
+ const filterCollectionsForCreation = ({ diff }) => {
98
+ var _a;
99
+ // Check new collections only
100
+ const isNewCollection = diff[0].kind === types_1.DiffKind.NEW;
101
+ if (!isNewCollection)
102
+ return false;
103
+ // Create now if no group
104
+ const groupName = (_a = diff[0].rhs.meta) === null || _a === void 0 ? void 0 : _a.group;
105
+ if (!groupName)
106
+ return true;
107
+ // Check if parent collection already exists in schema
108
+ const parentExists = currentSnapshot.collections.find((c) => c.collection === groupName) !== undefined;
109
+ // If this is a new collection and the parent collection doesn't exist in current schema ->
110
+ // Check if the parent collection will be created as part of applying this snapshot ->
111
+ // If yes -> this collection will be created recursively
112
+ // If not -> create now
113
+ // (ex.)
114
+ // TopLevelCollection - I exist in current schema
115
+ // NestedCollection - I exist in snapshotDiff as a new collection
116
+ // TheCurrentCollectionInIteration - I exist in snapshotDiff as a new collection but will be created as part of NestedCollection
117
+ const parentWillBeCreatedInThisApply = snapshotDiff.collections.filter(({ collection, diff }) => diff[0].kind === types_1.DiffKind.NEW && collection === groupName).length > 0;
118
+ // Has group, but parent is not new, parent is also not being created in this snapshot apply
119
+ if (parentExists && !parentWillBeCreatedInThisApply)
120
+ return true;
121
+ return false;
122
+ };
123
+ // Create top level collections (no group, or highest level in existing group) first,
124
+ // then continue with nested collections recursively
125
+ await createCollections(snapshotDiff.collections.filter(filterCollectionsForCreation));
126
+ // delete top level collections (no group) first, then continue with nested collections recursively
127
+ await deleteCollections(snapshotDiff.collections.filter(({ diff }) => { var _a; return diff[0].kind === types_1.DiffKind.DELETE && ((_a = diff[0].lhs.meta) === null || _a === void 0 ? void 0 : _a.group) === null; }));
128
+ for (const { collection, diff } of snapshotDiff.collections) {
129
+ if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === types_1.DiffKind.EDIT || (diff === null || diff === void 0 ? void 0 : diff[0].kind) === types_1.DiffKind.ARRAY) {
130
+ const currentCollection = currentSnapshot.collections.find((field) => {
131
+ return field.collection === collection;
132
+ });
133
+ if (currentCollection) {
134
+ try {
135
+ const newValues = diff.reduce((acc, currentDiff) => {
136
+ (0, deep_diff_1.applyChange)(acc, undefined, currentDiff);
137
+ return acc;
138
+ }, (0, lodash_1.cloneDeep)(currentCollection));
139
+ await collectionsService.updateOne(collection, newValues, mutationOptions);
140
+ }
141
+ catch (err) {
142
+ logger_1.default.error(`Failed to update collection "${collection}"`);
143
+ throw err;
144
+ }
145
+ }
146
+ }
147
+ }
148
+ const fieldsService = new services_1.FieldsService({
149
+ knex: trx,
150
+ schema: await (0, get_schema_1.getSchema)({ database: trx, bypassCache: true }),
151
+ });
152
+ for (const { collection, field, diff } of snapshotDiff.fields) {
153
+ if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === types_1.DiffKind.NEW && !isNestedMetaUpdate(diff === null || diff === void 0 ? void 0 : diff[0])) {
154
+ try {
155
+ await fieldsService.createField(collection, diff[0].rhs, undefined, mutationOptions);
156
+ }
157
+ catch (err) {
158
+ logger_1.default.error(`Failed to create field "${collection}.${field}"`);
159
+ throw err;
160
+ }
161
+ }
162
+ if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === types_1.DiffKind.EDIT || (diff === null || diff === void 0 ? void 0 : diff[0].kind) === types_1.DiffKind.ARRAY || isNestedMetaUpdate(diff === null || diff === void 0 ? void 0 : diff[0])) {
163
+ const currentField = currentSnapshot.fields.find((snapshotField) => {
164
+ return snapshotField.collection === collection && snapshotField.field === field;
165
+ });
166
+ if (currentField) {
167
+ try {
168
+ const newValues = diff.reduce((acc, currentDiff) => {
169
+ (0, deep_diff_1.applyChange)(acc, undefined, currentDiff);
170
+ return acc;
171
+ }, (0, lodash_1.cloneDeep)(currentField));
172
+ await fieldsService.updateField(collection, newValues, mutationOptions);
173
+ }
174
+ catch (err) {
175
+ logger_1.default.error(`Failed to update field "${collection}.${field}"`);
176
+ throw err;
177
+ }
178
+ }
179
+ }
180
+ if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === types_1.DiffKind.DELETE && !isNestedMetaUpdate(diff === null || diff === void 0 ? void 0 : diff[0])) {
181
+ try {
182
+ await fieldsService.deleteField(collection, field, mutationOptions);
183
+ }
184
+ catch (err) {
185
+ logger_1.default.error(`Failed to delete field "${collection}.${field}"`);
186
+ throw err;
187
+ }
188
+ // Field deletion also cleans up the relationship. We should ignore any relationship
189
+ // changes attached to this now non-existing field
190
+ snapshotDiff.relations = snapshotDiff.relations.filter((relation) => (relation.collection === collection && relation.field === field) === false);
191
+ }
192
+ }
193
+ const relationsService = new services_1.RelationsService({
194
+ knex: trx,
195
+ schema: await (0, get_schema_1.getSchema)({ database: trx, bypassCache: true }),
196
+ });
197
+ for (const { collection, field, diff } of snapshotDiff.relations) {
198
+ const structure = {};
199
+ for (const diffEdit of diff) {
200
+ (0, lodash_1.set)(structure, diffEdit.path, undefined);
201
+ }
202
+ if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === types_1.DiffKind.NEW) {
203
+ try {
204
+ await relationsService.createOne(diff[0].rhs, mutationOptions);
205
+ }
206
+ catch (err) {
207
+ logger_1.default.error(`Failed to create relation "${collection}.${field}"`);
208
+ throw err;
209
+ }
210
+ }
211
+ if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === types_1.DiffKind.EDIT || (diff === null || diff === void 0 ? void 0 : diff[0].kind) === types_1.DiffKind.ARRAY) {
212
+ const currentRelation = currentSnapshot.relations.find((relation) => {
213
+ return relation.collection === collection && relation.field === field;
214
+ });
215
+ if (currentRelation) {
216
+ try {
217
+ const newValues = diff.reduce((acc, currentDiff) => {
218
+ (0, deep_diff_1.applyChange)(acc, undefined, currentDiff);
219
+ return acc;
220
+ }, (0, lodash_1.cloneDeep)(currentRelation));
221
+ await relationsService.updateOne(collection, field, newValues, mutationOptions);
222
+ }
223
+ catch (err) {
224
+ logger_1.default.error(`Failed to update relation "${collection}.${field}"`);
225
+ throw err;
226
+ }
227
+ }
228
+ }
229
+ if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === types_1.DiffKind.DELETE) {
230
+ try {
231
+ await relationsService.deleteOne(collection, field, mutationOptions);
232
+ }
233
+ catch (err) {
234
+ logger_1.default.error(`Failed to delete relation "${collection}.${field}"`);
235
+ throw err;
236
+ }
237
+ }
238
+ }
239
+ });
240
+ await (0, cache_1.clearSystemCache)();
241
+ if (nestedActionEvents.length > 0) {
242
+ const updatedSchema = await (0, get_schema_1.getSchema)({ database, bypassCache: true });
243
+ for (const nestedActionEvent of nestedActionEvents) {
244
+ nestedActionEvent.context.schema = updatedSchema;
245
+ emitter_1.default.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
246
+ }
247
+ }
248
+ }
249
+ exports.applyDiff = applyDiff;
250
+ function isNestedMetaUpdate(diff) {
251
+ if (!diff)
252
+ return false;
253
+ if (diff.kind !== types_1.DiffKind.NEW && diff.kind !== types_1.DiffKind.DELETE)
254
+ return false;
255
+ if (!diff.path || diff.path.length < 2 || diff.path[0] !== 'meta')
256
+ return false;
257
+ return true;
258
+ }
259
+ exports.isNestedMetaUpdate = isNestedMetaUpdate;
@@ -0,0 +1 @@
1
+ export {};
@@ -23,15 +23,15 @@ function applyQuery(knex, collection, dbQuery, query, schema, options) {
23
23
  var _a;
24
24
  const aliasMap = (_a = options === null || options === void 0 ? void 0 : options.aliasMap) !== null && _a !== void 0 ? _a : Object.create(null);
25
25
  let hasMultiRelationalFilter = false;
26
- if (query.sort && !(options === null || options === void 0 ? void 0 : options.isInnerQuery) && !(options === null || options === void 0 ? void 0 : options.hasMultiRelationalSort)) {
27
- applySort(knex, schema, dbQuery, query.sort, collection, aliasMap);
28
- }
29
26
  applyLimit(knex, dbQuery, query.limit);
30
27
  if (query.offset) {
31
28
  applyOffset(knex, dbQuery, query.offset);
32
29
  }
33
30
  if (query.page && query.limit && query.limit !== -1) {
34
- dbQuery.offset(query.limit * (query.page - 1));
31
+ applyOffset(knex, dbQuery, query.limit * (query.page - 1));
32
+ }
33
+ if (query.sort && !(options === null || options === void 0 ? void 0 : options.isInnerQuery) && !(options === null || options === void 0 ? void 0 : options.hasMultiRelationalSort)) {
34
+ applySort(knex, schema, dbQuery, query.sort, collection, aliasMap);
35
35
  }
36
36
  if (query.search) {
37
37
  applySearch(schema, dbQuery, query.search, collection);
@@ -170,6 +170,8 @@ function applySort(knex, schema, rootQuery, rootSort, collection, aliasMap, retu
170
170
  });
171
171
  if (returnRecords)
172
172
  return { sortRecords, hasMultiRelationalSort };
173
+ // Clears the order if any, eg: from MSSQL offset
174
+ rootQuery.clear('order');
173
175
  rootQuery.orderBy(sortRecords);
174
176
  }
175
177
  exports.applySort = applySort;
@@ -333,12 +335,12 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, aliasMap)
333
335
  }
334
336
  if (operator === '_empty' || (operator === '_nempty' && compareValue === false)) {
335
337
  dbQuery[logical].andWhere((query) => {
336
- query.where(key, '=', '');
338
+ query.whereNull(key).orWhere(key, '=', '');
337
339
  });
338
340
  }
339
341
  if (operator === '_nempty' || (operator === '_empty' && compareValue === false)) {
340
342
  dbQuery[logical].andWhere((query) => {
341
- query.where(key, '!=', '');
343
+ query.whereNotNull(key).orWhere(key, '!=', '');
342
344
  });
343
345
  }
344
346
  // The following fields however, require a value to be run. If no value is passed, we
@@ -1,11 +1,9 @@
1
1
  import { SchemaOverview } from '@directus/shared/types';
2
- import { Diff } from 'deep-diff';
3
2
  import { Knex } from 'knex';
4
- import { Snapshot, SnapshotDiff, SnapshotField } from '../types';
3
+ import { Snapshot, SnapshotDiff } from '../types';
5
4
  export declare function applySnapshot(snapshot: Snapshot, options?: {
6
5
  database?: Knex;
7
6
  schema?: SchemaOverview;
8
7
  current?: Snapshot;
9
8
  diff?: SnapshotDiff;
10
9
  }): Promise<void>;
11
- export declare function isNestedMetaUpdate(diff: Diff<SnapshotField | undefined>): boolean;
@@ -3,16 +3,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.isNestedMetaUpdate = exports.applySnapshot = void 0;
7
- const lodash_1 = require("lodash");
6
+ exports.applySnapshot = void 0;
7
+ const cache_1 = require("../cache");
8
8
  const database_1 = __importDefault(require("../database"));
9
- const logger_1 = __importDefault(require("../logger"));
10
- const services_1 = require("../services");
9
+ const apply_diff_1 = require("./apply-diff");
11
10
  const get_schema_1 = require("./get-schema");
12
11
  const get_snapshot_1 = require("./get-snapshot");
13
12
  const get_snapshot_diff_1 = require("./get-snapshot-diff");
14
- const cache_1 = require("../cache");
15
- const emitter_1 = __importDefault(require("../emitter"));
16
13
  async function applySnapshot(snapshot, options) {
17
14
  var _a, _b, _c, _d;
18
15
  const database = (_a = options === null || options === void 0 ? void 0 : options.database) !== null && _a !== void 0 ? _a : (0, database_1.default)();
@@ -20,234 +17,7 @@ async function applySnapshot(snapshot, options) {
20
17
  const { systemCache } = (0, cache_1.getCache)();
21
18
  const current = (_c = options === null || options === void 0 ? void 0 : options.current) !== null && _c !== void 0 ? _c : (await (0, get_snapshot_1.getSnapshot)({ database, schema }));
22
19
  const snapshotDiff = (_d = options === null || options === void 0 ? void 0 : options.diff) !== null && _d !== void 0 ? _d : (0, get_snapshot_diff_1.getSnapshotDiff)(current, snapshot);
23
- const nestedActionEvents = [];
24
- const mutationOptions = {
25
- autoPurgeSystemCache: false,
26
- bypassEmitAction: (params) => nestedActionEvents.push(params),
27
- };
28
- await database.transaction(async (trx) => {
29
- const collectionsService = new services_1.CollectionsService({ knex: trx, schema });
30
- const getNestedCollectionsToCreate = (currentLevelCollection) => snapshotDiff.collections.filter(({ diff }) => { var _a, _b; return ((_b = (_a = diff[0].rhs) === null || _a === void 0 ? void 0 : _a.meta) === null || _b === void 0 ? void 0 : _b.group) === currentLevelCollection; });
31
- const getNestedCollectionsToDelete = (currentLevelCollection) => snapshotDiff.collections.filter(({ diff }) => { var _a, _b; return ((_b = (_a = diff[0].lhs) === null || _a === void 0 ? void 0 : _a.meta) === null || _b === void 0 ? void 0 : _b.group) === currentLevelCollection; });
32
- const createCollections = async (collections) => {
33
- for (const { collection, diff } of collections) {
34
- if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'N' && diff[0].rhs) {
35
- // We'll nest the to-be-created fields in the same collection creation, to prevent
36
- // creating a collection without a primary key
37
- const fields = snapshotDiff.fields
38
- .filter((fieldDiff) => fieldDiff.collection === collection)
39
- .map((fieldDiff) => fieldDiff.diff[0].rhs)
40
- .map((fieldDiff) => {
41
- var _a, _b, _c, _d, _e;
42
- // Casts field type to UUID when applying non-PostgreSQL schema onto PostgreSQL database.
43
- // This is needed because they snapshots UUID fields as char with length 36.
44
- if (((_a = fieldDiff.schema) === null || _a === void 0 ? void 0 : _a.data_type) === 'char' &&
45
- ((_b = fieldDiff.schema) === null || _b === void 0 ? void 0 : _b.max_length) === 36 &&
46
- (((_c = fieldDiff.schema) === null || _c === void 0 ? void 0 : _c.is_primary_key) ||
47
- (((_d = fieldDiff.schema) === null || _d === void 0 ? void 0 : _d.foreign_key_table) && ((_e = fieldDiff.schema) === null || _e === void 0 ? void 0 : _e.foreign_key_column)))) {
48
- return (0, lodash_1.merge)(fieldDiff, { type: 'uuid', schema: { data_type: 'uuid', max_length: null } });
49
- }
50
- else {
51
- return fieldDiff;
52
- }
53
- });
54
- try {
55
- await collectionsService.createOne({
56
- ...diff[0].rhs,
57
- fields,
58
- }, mutationOptions);
59
- }
60
- catch (err) {
61
- logger_1.default.error(`Failed to create collection "${collection}"`);
62
- throw err;
63
- }
64
- // Now that the fields are in for this collection, we can strip them from the field edits
65
- snapshotDiff.fields = snapshotDiff.fields.filter((fieldDiff) => fieldDiff.collection !== collection);
66
- await createCollections(getNestedCollectionsToCreate(collection));
67
- }
68
- }
69
- };
70
- const deleteCollections = async (collections) => {
71
- for (const { collection, diff } of collections) {
72
- if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'D') {
73
- const relations = schema.relations.filter((r) => r.related_collection === collection || r.collection === collection);
74
- if (relations.length > 0) {
75
- const relationsService = new services_1.RelationsService({ knex: trx, schema });
76
- for (const relation of relations) {
77
- try {
78
- await relationsService.deleteOne(relation.collection, relation.field, mutationOptions);
79
- }
80
- catch (err) {
81
- logger_1.default.error(`Failed to delete collection "${collection}" due to relation "${relation.collection}.${relation.field}"`);
82
- throw err;
83
- }
84
- }
85
- // clean up deleted relations from existing schema
86
- schema.relations = schema.relations.filter((r) => r.related_collection !== collection && r.collection !== collection);
87
- }
88
- await deleteCollections(getNestedCollectionsToDelete(collection));
89
- try {
90
- await collectionsService.deleteOne(collection, mutationOptions);
91
- }
92
- catch (err) {
93
- logger_1.default.error(`Failed to delete collection "${collection}"`);
94
- throw err;
95
- }
96
- }
97
- }
98
- };
99
- // Finds all collections that need to be created
100
- const filterCollectionsForCreation = ({ diff }) => {
101
- var _a;
102
- // Check new collections only
103
- const isNewCollection = diff[0].kind === 'N';
104
- if (!isNewCollection)
105
- return false;
106
- // Create now if no group
107
- const groupName = (_a = diff[0].rhs.meta) === null || _a === void 0 ? void 0 : _a.group;
108
- if (!groupName)
109
- return true;
110
- // Check if parent collection already exists in schema
111
- const parentExists = current.collections.find((c) => c.collection === groupName) !== undefined;
112
- // If this is a new collection and the parent collection doesn't exist in current schema ->
113
- // Check if the parent collection will be created as part of applying this snapshot ->
114
- // If yes -> this collection will be created recursively
115
- // If not -> create now
116
- // (ex.)
117
- // TopLevelCollection - I exist in current schema
118
- // NestedCollection - I exist in snapshotDiff as a new collection
119
- // TheCurrentCollectionInIteration - I exist in snapshotDiff as a new collection but will be created as part of NestedCollection
120
- const parentWillBeCreatedInThisApply = snapshotDiff.collections.filter(({ collection, diff }) => diff[0].kind === 'N' && collection === groupName)
121
- .length > 0;
122
- // Has group, but parent is not new, parent is also not being created in this snapshot apply
123
- if (parentExists && !parentWillBeCreatedInThisApply)
124
- return true;
125
- return false;
126
- };
127
- // Create top level collections (no group, or highest level in existing group) first,
128
- // then continue with nested collections recursively
129
- await createCollections(snapshotDiff.collections.filter(filterCollectionsForCreation));
130
- // delete top level collections (no group) first, then continue with nested collections recursively
131
- await deleteCollections(snapshotDiff.collections.filter(({ diff }) => { var _a; return diff[0].kind === 'D' && ((_a = diff[0].lhs.meta) === null || _a === void 0 ? void 0 : _a.group) === null; }));
132
- for (const { collection, diff } of snapshotDiff.collections) {
133
- if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'E' || (diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'A') {
134
- const newValues = snapshot.collections.find((field) => {
135
- return field.collection === collection;
136
- });
137
- if (newValues) {
138
- try {
139
- await collectionsService.updateOne(collection, newValues, mutationOptions);
140
- }
141
- catch (err) {
142
- logger_1.default.error(`Failed to update collection "${collection}"`);
143
- throw err;
144
- }
145
- }
146
- }
147
- }
148
- const fieldsService = new services_1.FieldsService({
149
- knex: trx,
150
- schema: await (0, get_schema_1.getSchema)({ database: trx, bypassCache: true }),
151
- });
152
- for (const { collection, field, diff } of snapshotDiff.fields) {
153
- if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'N' && !isNestedMetaUpdate(diff === null || diff === void 0 ? void 0 : diff[0])) {
154
- try {
155
- await fieldsService.createField(collection, diff[0].rhs, undefined, mutationOptions);
156
- }
157
- catch (err) {
158
- logger_1.default.error(`Failed to create field "${collection}.${field}"`);
159
- throw err;
160
- }
161
- }
162
- if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'E' || (diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'A' || isNestedMetaUpdate(diff === null || diff === void 0 ? void 0 : diff[0])) {
163
- const newValues = snapshot.fields.find((snapshotField) => {
164
- return snapshotField.collection === collection && snapshotField.field === field;
165
- });
166
- if (newValues) {
167
- try {
168
- await fieldsService.updateField(collection, {
169
- ...newValues,
170
- }, mutationOptions);
171
- }
172
- catch (err) {
173
- logger_1.default.error(`Failed to update field "${collection}.${field}"`);
174
- throw err;
175
- }
176
- }
177
- }
178
- if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'D' && !isNestedMetaUpdate(diff === null || diff === void 0 ? void 0 : diff[0])) {
179
- try {
180
- await fieldsService.deleteField(collection, field, mutationOptions);
181
- }
182
- catch (err) {
183
- logger_1.default.error(`Failed to delete field "${collection}.${field}"`);
184
- throw err;
185
- }
186
- // Field deletion also cleans up the relationship. We should ignore any relationship
187
- // changes attached to this now non-existing field
188
- snapshotDiff.relations = snapshotDiff.relations.filter((relation) => (relation.collection === collection && relation.field === field) === false);
189
- }
190
- }
191
- const relationsService = new services_1.RelationsService({
192
- knex: trx,
193
- schema: await (0, get_schema_1.getSchema)({ database: trx, bypassCache: true }),
194
- });
195
- for (const { collection, field, diff } of snapshotDiff.relations) {
196
- const structure = {};
197
- for (const diffEdit of diff) {
198
- (0, lodash_1.set)(structure, diffEdit.path, undefined);
199
- }
200
- if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'N') {
201
- try {
202
- await relationsService.createOne(diff[0].rhs, mutationOptions);
203
- }
204
- catch (err) {
205
- logger_1.default.error(`Failed to create relation "${collection}.${field}"`);
206
- throw err;
207
- }
208
- }
209
- if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'E' || (diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'A') {
210
- const newValues = snapshot.relations.find((relation) => {
211
- return relation.collection === collection && relation.field === field;
212
- });
213
- if (newValues) {
214
- try {
215
- await relationsService.updateOne(collection, field, newValues, mutationOptions);
216
- }
217
- catch (err) {
218
- logger_1.default.error(`Failed to update relation "${collection}.${field}"`);
219
- throw err;
220
- }
221
- }
222
- }
223
- if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'D') {
224
- try {
225
- await relationsService.deleteOne(collection, field, mutationOptions);
226
- }
227
- catch (err) {
228
- logger_1.default.error(`Failed to delete relation "${collection}.${field}"`);
229
- throw err;
230
- }
231
- }
232
- }
233
- });
20
+ await (0, apply_diff_1.applyDiff)(current, snapshotDiff, { database, schema });
234
21
  await (systemCache === null || systemCache === void 0 ? void 0 : systemCache.clear());
235
- if (nestedActionEvents.length > 0) {
236
- const updatedSchema = await (0, get_schema_1.getSchema)({ database, bypassCache: true });
237
- for (const nestedActionEvent of nestedActionEvents) {
238
- nestedActionEvent.context.schema = updatedSchema;
239
- emitter_1.default.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
240
- }
241
- }
242
22
  }
243
23
  exports.applySnapshot = applySnapshot;
244
- function isNestedMetaUpdate(diff) {
245
- if (!diff)
246
- return false;
247
- if (diff.kind !== 'N' && diff.kind !== 'D')
248
- return false;
249
- if (!diff.path || diff.path.length < 2 || diff.path[0] !== 'meta')
250
- return false;
251
- return true;
252
- }
253
- exports.isNestedMetaUpdate = isNestedMetaUpdate;
@@ -4,5 +4,7 @@ import { Request } from 'express';
4
4
  *
5
5
  * @param req Express request object
6
6
  * @param ttl TTL of the cache in ms
7
+ * @param globalCacheSettings Whether requests are affected by the global cache settings (i.e. for dynamic API requests)
8
+ * @param personalized Whether requests depend on the authentication status of users
7
9
  */
8
- export declare function getCacheControlHeader(req: Request, ttl: number | null): string;
10
+ export declare function getCacheControlHeader(req: Request, ttl: number | undefined, globalCacheSettings: boolean, personalized: boolean): string;
@@ -10,34 +10,35 @@ const env_1 = __importDefault(require("../env"));
10
10
  *
11
11
  * @param req Express request object
12
12
  * @param ttl TTL of the cache in ms
13
+ * @param globalCacheSettings Whether requests are affected by the global cache settings (i.e. for dynamic API requests)
14
+ * @param personalized Whether requests depend on the authentication status of users
13
15
  */
14
- function getCacheControlHeader(req, ttl) {
16
+ function getCacheControlHeader(req, ttl, globalCacheSettings, personalized) {
15
17
  var _a, _b, _c;
16
- // When the resource / current request isn't cached
17
- if (ttl === null)
18
- return 'no-cache';
19
- // When the API cache can invalidate at any moment
20
- if (env_1.default.CACHE_AUTO_PURGE === true)
21
- return 'no-cache';
22
18
  const noCacheRequested = ((_a = req.headers['cache-control']) === null || _a === void 0 ? void 0 : _a.includes('no-store')) || ((_b = req.headers['Cache-Control']) === null || _b === void 0 ? void 0 : _b.includes('no-store'));
23
19
  // When the user explicitly asked to skip the cache
24
20
  if (noCacheRequested)
25
21
  return 'no-store';
22
+ // When the resource / current request shouldn't be cached
23
+ if (ttl === undefined || ttl < 0)
24
+ return 'no-cache';
25
+ // When the API cache can invalidate at any moment
26
+ if (globalCacheSettings && env_1.default.CACHE_AUTO_PURGE === true)
27
+ return 'no-cache';
28
+ const headerValues = [];
29
+ // When caching depends on the authentication status of the users
30
+ if (personalized) {
31
+ // Allow response to be stored in shared cache (public) or local cache only (private)
32
+ const access = !!((_c = req.accountability) === null || _c === void 0 ? void 0 : _c.role) === false ? 'public' : 'private';
33
+ headerValues.push(access);
34
+ }
26
35
  // Cache control header uses seconds for everything
27
36
  const ttlSeconds = Math.round(ttl / 1000);
28
- const access = !!((_c = req.accountability) === null || _c === void 0 ? void 0 : _c.role) === false ? 'public' : 'private';
29
- let headerValue = `${access}, max-age=${ttlSeconds}`;
37
+ headerValues.push(`max-age=${ttlSeconds}`);
30
38
  // When the s-maxage flag should be included
31
- if (env_1.default.CACHE_CONTROL_S_MAXAGE !== false) {
32
- // Default to regular max-age flag when true
33
- if (env_1.default.CACHE_CONTROL_S_MAXAGE === true) {
34
- headerValue += `, s-maxage=${ttlSeconds}`;
35
- }
36
- else {
37
- // Set to custom value
38
- headerValue += `, s-maxage=${env_1.default.CACHE_CONTROL_S_MAXAGE}`;
39
- }
39
+ if (globalCacheSettings && Number.isInteger(env_1.default.CACHE_CONTROL_S_MAXAGE) && env_1.default.CACHE_CONTROL_S_MAXAGE >= 0) {
40
+ headerValues.push(`s-maxage=${env_1.default.CACHE_CONTROL_S_MAXAGE}`);
40
41
  }
41
- return headerValue;
42
+ return headerValues.join(', ');
42
43
  }
43
44
  exports.getCacheControlHeader = getCacheControlHeader;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Safely parse human readable time format into milliseconds
3
+ */
4
+ export declare function getMilliseconds<T>(value: unknown, fallback?: T): number | T;