directus 9.8.0 → 9.9.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 (52) hide show
  1. package/dist/__mocks__/cache.d.ts +5 -0
  2. package/dist/__mocks__/cache.js +7 -0
  3. package/dist/auth/drivers/ldap.js +10 -11
  4. package/dist/auth/drivers/oauth2.js +9 -4
  5. package/dist/auth/drivers/openid.js +7 -4
  6. package/dist/cli/commands/schema/apply.js +9 -3
  7. package/dist/controllers/assets.js +5 -0
  8. package/dist/controllers/files.d.ts +2 -0
  9. package/dist/controllers/files.js +10 -5
  10. package/dist/database/helpers/date/dialects/mssql.d.ts +4 -0
  11. package/dist/database/helpers/date/dialects/mssql.js +12 -0
  12. package/dist/database/helpers/date/dialects/mysql.d.ts +5 -0
  13. package/dist/database/helpers/date/dialects/mysql.js +16 -0
  14. package/dist/database/helpers/date/dialects/oracle.d.ts +4 -0
  15. package/dist/database/helpers/date/dialects/oracle.js +15 -0
  16. package/dist/database/helpers/date/dialects/sqlite.d.ts +1 -0
  17. package/dist/database/helpers/date/dialects/sqlite.js +8 -0
  18. package/dist/database/helpers/date/index.d.ts +3 -3
  19. package/dist/database/helpers/date/index.js +6 -6
  20. package/dist/database/helpers/date/types.d.ts +3 -0
  21. package/dist/database/helpers/date/types.js +10 -0
  22. package/dist/database/helpers/index.d.ts +1 -1
  23. package/dist/database/migrations/20220402A-remove-default-value-panel-icon.d.ts +3 -0
  24. package/dist/database/migrations/20220402A-remove-default-value-panel-icon.js +22 -0
  25. package/dist/database/system-data/fields/settings.yaml +0 -1
  26. package/dist/database/system-data/fields/users.yaml +3 -0
  27. package/dist/env.js +3 -1
  28. package/dist/exceptions/index.d.ts +1 -0
  29. package/dist/exceptions/index.js +1 -0
  30. package/dist/exceptions/token-expired.d.ts +4 -0
  31. package/dist/exceptions/token-expired.js +10 -0
  32. package/dist/middleware/cache.js +10 -0
  33. package/dist/services/authorization.js +72 -30
  34. package/dist/services/collections.d.ts +2 -0
  35. package/dist/services/collections.js +8 -0
  36. package/dist/services/fields.js +24 -1
  37. package/dist/services/files.d.ts +5 -1
  38. package/dist/services/files.js +59 -40
  39. package/dist/services/graphql.d.ts +2 -3
  40. package/dist/services/graphql.js +39 -9
  41. package/dist/services/payload.d.ts +1 -0
  42. package/dist/services/payload.js +19 -16
  43. package/dist/types/files.d.ts +8 -0
  44. package/dist/utils/apply-query.js +17 -7
  45. package/dist/utils/apply-snapshot.d.ts +3 -1
  46. package/dist/utils/apply-snapshot.js +34 -5
  47. package/dist/utils/get-graphql-type.js +1 -0
  48. package/dist/utils/get-local-type.js +5 -0
  49. package/dist/utils/jwt.js +1 -1
  50. package/dist/utils/validate-query.js +19 -15
  51. package/example.env +4 -0
  52. package/package.json +14 -13
@@ -89,14 +89,14 @@ class PayloadService {
89
89
  return (accountability === null || accountability === void 0 ? void 0 : accountability.role) || null;
90
90
  return value;
91
91
  },
92
- async 'date-created'({ action, value }) {
92
+ async 'date-created'({ action, value, helpers }) {
93
93
  if (action === 'create')
94
- return new Date();
94
+ return new Date(helpers.date.writeTimestamp(new Date().toISOString()));
95
95
  return value;
96
96
  },
97
- async 'date-updated'({ action, value }) {
97
+ async 'date-updated'({ action, value, helpers }) {
98
98
  if (action === 'update')
99
- return new Date();
99
+ return new Date(helpers.date.writeTimestamp(new Date().toISOString()));
100
100
  return value;
101
101
  },
102
102
  async 'cast-csv'({ action, value }) {
@@ -181,6 +181,7 @@ class PayloadService {
181
181
  payload,
182
182
  accountability,
183
183
  specials: fieldSpecials,
184
+ helpers: this.helpers,
184
185
  });
185
186
  }
186
187
  }
@@ -233,21 +234,23 @@ class PayloadService {
233
234
  value = new Date(value);
234
235
  }
235
236
  if (dateColumn.type === 'timestamp') {
236
- const newValue = value.toISOString();
237
+ const newValue = this.helpers.date.readTimestampString(value.toISOString());
237
238
  payload[name] = newValue;
238
239
  }
239
240
  if (dateColumn.type === 'dateTime') {
240
- const year = String(value.getUTCFullYear());
241
- const month = String(value.getUTCMonth() + 1).padStart(2, '0');
242
- const date = String(value.getUTCDate()).padStart(2, '0');
243
- const hours = String(value.getUTCHours()).padStart(2, '0');
244
- const minutes = String(value.getUTCMinutes()).padStart(2, '0');
245
- const seconds = String(value.getUTCSeconds()).padStart(2, '0');
246
- const newValue = `${year}-${month}-${date}T${hours}:${minutes}:${seconds}`;
241
+ const year = String(value.getFullYear());
242
+ const month = String(value.getMonth() + 1).padStart(2, '0');
243
+ const day = String(value.getDate()).padStart(2, '0');
244
+ const hours = String(value.getHours()).padStart(2, '0');
245
+ const minutes = String(value.getMinutes()).padStart(2, '0');
246
+ const seconds = String(value.getSeconds()).padStart(2, '0');
247
+ const newValue = `${year}-${month}-${day}T${hours}:${minutes}:${seconds}`;
247
248
  payload[name] = newValue;
248
249
  }
249
250
  if (dateColumn.type === 'date') {
250
- const [year, month, day] = value.toISOString().slice(0, 10).split('-');
251
+ const year = String(value.getFullYear());
252
+ const month = String(value.getMonth() + 1).padStart(2, '0');
253
+ const day = String(value.getDate()).padStart(2, '0');
251
254
  // Strip off the time / timezone information from a date-only value
252
255
  const newValue = `${year}-${month}-${day}`;
253
256
  payload[name] = newValue;
@@ -258,16 +261,16 @@ class PayloadService {
258
261
  if (dateColumn.type === 'date') {
259
262
  const [date] = value.split('T');
260
263
  const [year, month, day] = date.split('-');
261
- payload[name] = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day)));
264
+ payload[name] = new Date(Number(year), Number(month) - 1, Number(day));
262
265
  }
263
266
  if (dateColumn.type === 'dateTime') {
264
267
  const [date, time] = value.split('T');
265
268
  const [year, month, day] = date.split('-');
266
269
  const [hours, minutes, seconds] = time.substring(0, 8).split(':');
267
- payload[name] = new Date(Date.UTC(Number(year), Number(month) - 1, Number(day), Number(hours), Number(minutes), Number(seconds)));
270
+ payload[name] = new Date(Number(year), Number(month) - 1, Number(day), Number(hours), Number(minutes), Number(seconds));
268
271
  }
269
272
  if (dateColumn.type === 'timestamp') {
270
- const newValue = (0, date_fns_1.parseISO)(value);
273
+ const newValue = this.helpers.date.writeTimestamp(value);
271
274
  payload[name] = newValue;
272
275
  }
273
276
  }
@@ -19,3 +19,11 @@ export declare type File = {
19
19
  tags: string | null;
20
20
  metadata: Record<string, any> | null;
21
21
  };
22
+ export declare type Metadata = {
23
+ height?: number | undefined;
24
+ width?: number | undefined;
25
+ description?: string | undefined;
26
+ title?: string | undefined;
27
+ tags?: any | undefined;
28
+ metadata?: any | undefined;
29
+ };
@@ -180,6 +180,7 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
180
180
  }
181
181
  }
182
182
  function addWhereClauses(knex, dbQuery, filter, collection, logical = 'and') {
183
+ var _a, _b;
183
184
  for (const [key, value] of Object.entries(filter)) {
184
185
  if (key === '_or' || key === '_and') {
185
186
  // If the _or array contains an empty object (full permissions), we should short-circuit and ignore all other
@@ -220,16 +221,25 @@ function applyFilter(knex, schema, rootQuery, rootFilter, collection, subQuery =
220
221
  if (relationType === 'o2a') {
221
222
  pkField = knex.raw(`CAST(?? AS CHAR(255))`, [pkField]);
222
223
  }
223
- // Note: knex's types don't appreciate knex.raw in whereIn, even though it's officially supported
224
- dbQuery[logical].whereIn(pkField, (subQueryKnex) => {
224
+ const subQueryBuilder = (filter) => (subQueryKnex) => {
225
225
  const field = relation.field;
226
226
  const collection = relation.collection;
227
227
  const column = `${collection}.${field}`;
228
- subQueryKnex.select({ [field]: column }).from(collection);
229
- applyQuery(knex, relation.collection, subQueryKnex, {
230
- filter: value,
231
- }, schema, true);
232
- });
228
+ subQueryKnex
229
+ .select({ [field]: column })
230
+ .from(collection)
231
+ .whereNotNull(field);
232
+ applyQuery(knex, relation.collection, subQueryKnex, { filter }, schema, true);
233
+ };
234
+ if (((_a = Object.keys(value)) === null || _a === void 0 ? void 0 : _a[0]) === '_none') {
235
+ dbQuery[logical].whereNotIn(pkField, subQueryBuilder(Object.values(value)[0]));
236
+ }
237
+ else if (((_b = Object.keys(value)) === null || _b === void 0 ? void 0 : _b[0]) === '_some') {
238
+ dbQuery[logical].whereIn(pkField, subQueryBuilder(Object.values(value)[0]));
239
+ }
240
+ else {
241
+ dbQuery[logical].whereIn(pkField, subQueryBuilder(value));
242
+ }
233
243
  }
234
244
  }
235
245
  function applyFilterToQuery(key, operator, compareValue, logical = 'and') {
@@ -1,5 +1,6 @@
1
- import { Snapshot, SnapshotDiff } from '../types';
1
+ import { Snapshot, SnapshotDiff, SnapshotField } from '../types';
2
2
  import { Knex } from 'knex';
3
+ import { Diff } from 'deep-diff';
3
4
  import { SchemaOverview } from '@directus/shared/types';
4
5
  export declare function applySnapshot(snapshot: Snapshot, options?: {
5
6
  database?: Knex;
@@ -7,3 +8,4 @@ export declare function applySnapshot(snapshot: Snapshot, options?: {
7
8
  current?: Snapshot;
8
9
  diff?: SnapshotDiff;
9
10
  }): Promise<void>;
11
+ export declare function isNestedMetaUpdate(diff: Diff<SnapshotField | undefined>): boolean;
@@ -3,7 +3,7 @@ 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.applySnapshot = void 0;
6
+ exports.isNestedMetaUpdate = exports.applySnapshot = void 0;
7
7
  const get_snapshot_1 = require("./get-snapshot");
8
8
  const get_snapshot_diff_1 = require("./get-snapshot-diff");
9
9
  const database_1 = __importDefault(require("../database"));
@@ -34,7 +34,26 @@ async function applySnapshot(snapshot, options) {
34
34
  // creating a collection without a primary key
35
35
  const fields = snapshotDiff.fields
36
36
  .filter((fieldDiff) => fieldDiff.collection === collection)
37
- .map((fieldDiff) => fieldDiff.diff[0].rhs);
37
+ .map((fieldDiff) => fieldDiff.diff[0].rhs)
38
+ .map((fieldDiff) => {
39
+ // Casts field type to UUID when applying SQLite-based schema on other databases.
40
+ // This is needed because SQLite snapshots UUID fields as char with length 36, and
41
+ // it will fail when trying to create relation between char field to UUID field
42
+ if (!fieldDiff.schema ||
43
+ fieldDiff.schema.data_type !== 'char' ||
44
+ fieldDiff.schema.max_length !== 36 ||
45
+ !fieldDiff.schema.foreign_key_table ||
46
+ !fieldDiff.schema.foreign_key_column) {
47
+ return fieldDiff;
48
+ }
49
+ const matchingForeignKeyTable = schema.collections[fieldDiff.schema.foreign_key_table];
50
+ if (!matchingForeignKeyTable)
51
+ return fieldDiff;
52
+ const matchingForeignKeyField = matchingForeignKeyTable.fields[fieldDiff.schema.foreign_key_column];
53
+ if (!matchingForeignKeyField || matchingForeignKeyField.type !== 'uuid')
54
+ return fieldDiff;
55
+ return (0, lodash_1.merge)(fieldDiff, { type: 'uuid', schema: { data_type: 'uuid', max_length: null } });
56
+ });
38
57
  try {
39
58
  await collectionsService.createOne({
40
59
  ...diff[0].rhs,
@@ -66,7 +85,7 @@ async function applySnapshot(snapshot, options) {
66
85
  }
67
86
  const fieldsService = new services_1.FieldsService({ knex: trx, schema: await (0, get_schema_1.getSchema)({ database: trx }) });
68
87
  for (const { collection, field, diff } of snapshotDiff.fields) {
69
- if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'N') {
88
+ if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'N' && !isNestedMetaUpdate(diff === null || diff === void 0 ? void 0 : diff[0])) {
70
89
  try {
71
90
  await fieldsService.createField(collection, diff[0].rhs);
72
91
  }
@@ -75,7 +94,7 @@ async function applySnapshot(snapshot, options) {
75
94
  throw err;
76
95
  }
77
96
  }
78
- if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'E' || (diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'A') {
97
+ 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])) {
79
98
  const newValues = snapshot.fields.find((snapshotField) => {
80
99
  return snapshotField.collection === collection && snapshotField.field === field;
81
100
  });
@@ -91,7 +110,7 @@ async function applySnapshot(snapshot, options) {
91
110
  }
92
111
  }
93
112
  }
94
- if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'D') {
113
+ if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'D' && !isNestedMetaUpdate(diff === null || diff === void 0 ? void 0 : diff[0])) {
95
114
  try {
96
115
  await fieldsService.deleteField(collection, field);
97
116
  }
@@ -146,3 +165,13 @@ async function applySnapshot(snapshot, options) {
146
165
  });
147
166
  }
148
167
  exports.applySnapshot = applySnapshot;
168
+ function isNestedMetaUpdate(diff) {
169
+ if (!diff)
170
+ return false;
171
+ if (diff.kind !== 'N' && diff.kind !== 'D')
172
+ return false;
173
+ if (!diff.path || diff.path.length < 2 || diff.path[0] !== 'meta')
174
+ return false;
175
+ return true;
176
+ }
177
+ exports.isNestedMetaUpdate = isNestedMetaUpdate;
@@ -9,6 +9,7 @@ function getGraphQLType(localType) {
9
9
  case 'boolean':
10
10
  return graphql_1.GraphQLBoolean;
11
11
  case 'bigInteger':
12
+ return graphql_1.GraphQLString;
12
13
  case 'integer':
13
14
  return graphql_1.GraphQLInt;
14
15
  case 'decimal':
@@ -88,6 +88,7 @@ const localTypeMap = {
88
88
  'time without time zone': 'time',
89
89
  float4: 'float',
90
90
  float8: 'float',
91
+ citext: 'text',
91
92
  // Oracle
92
93
  number: 'integer',
93
94
  sdo_geometry: 'geometry',
@@ -109,6 +110,10 @@ function getLocalType(column, field) {
109
110
  return 'csv';
110
111
  if (special.includes('uuid') || special.includes('file'))
111
112
  return 'uuid';
113
+ if (special.includes('cast-timestamp'))
114
+ return 'timestamp';
115
+ if (special.includes('cast-datetime'))
116
+ return 'dateTime';
112
117
  if (type === null || type === void 0 ? void 0 : type.startsWith('geometry')) {
113
118
  return special[0] || 'geometry';
114
119
  }
package/dist/utils/jwt.js CHANGED
@@ -31,7 +31,7 @@ function verifyAccessJWT(token, secret) {
31
31
  }
32
32
  catch (err) {
33
33
  if (err instanceof jsonwebtoken_1.TokenExpiredError) {
34
- throw new exceptions_1.InvalidTokenException('Token expired.');
34
+ throw new exceptions_1.TokenExpiredException();
35
35
  }
36
36
  else if (err instanceof jsonwebtoken_1.JsonWebTokenError) {
37
37
  throw new exceptions_1.InvalidTokenException('Token invalid.');
@@ -47,21 +47,6 @@ function validateFilter(filter) {
47
47
  else if (key.startsWith('_')) {
48
48
  const value = nested;
49
49
  switch (key) {
50
- case '_eq':
51
- case '_neq':
52
- case '_contains':
53
- case '_ncontains':
54
- case '_starts_with':
55
- case '_nstarts_with':
56
- case '_ends_with':
57
- case '_nends_with':
58
- case '_gt':
59
- case '_gte':
60
- case '_lt':
61
- case '_lte':
62
- default:
63
- validateFilterPrimitive(value, key);
64
- break;
65
50
  case '_in':
66
51
  case '_nin':
67
52
  case '_between':
@@ -80,6 +65,25 @@ function validateFilter(filter) {
80
65
  case '_nintersects_bbox':
81
66
  validateGeometry(value, key);
82
67
  break;
68
+ case '_none':
69
+ case '_some':
70
+ validateFilter(nested);
71
+ break;
72
+ case '_eq':
73
+ case '_neq':
74
+ case '_contains':
75
+ case '_ncontains':
76
+ case '_starts_with':
77
+ case '_nstarts_with':
78
+ case '_ends_with':
79
+ case '_nends_with':
80
+ case '_gt':
81
+ case '_gte':
82
+ case '_lt':
83
+ case '_lte':
84
+ default:
85
+ validateFilterPrimitive(value, key);
86
+ break;
83
87
  }
84
88
  }
85
89
  else if ((0, lodash_1.isPlainObject)(nested)) {
package/example.env CHANGED
@@ -123,6 +123,10 @@ STORAGE_LOCAL_ROOT="./uploads"
123
123
  # STORAGE_GOOGLE_KEY_FILENAME="abcdef"
124
124
  # STORAGE_GOOGLE_BUCKET="my-files"
125
125
 
126
+
127
+ ## CSV of additional metadata keys
128
+ # FILE_METADATA_ALLOW_LIST=
129
+
126
130
  ####################################################################################################
127
131
  # Security
128
132
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "directus",
3
- "version": "9.8.0",
3
+ "version": "9.9.0",
4
4
  "license": "GPL-3.0-only",
5
5
  "homepage": "https://github.com/directus/directus#readme",
6
6
  "description": "Directus is a real-time API and App dashboard for managing SQL database content.",
@@ -78,16 +78,16 @@
78
78
  ],
79
79
  "dependencies": {
80
80
  "@aws-sdk/client-ses": "^3.40.0",
81
- "@directus/app": "9.8.0",
82
- "@directus/drive": "9.8.0",
83
- "@directus/drive-azure": "9.8.0",
84
- "@directus/drive-gcs": "9.8.0",
85
- "@directus/drive-s3": "9.8.0",
86
- "@directus/extensions-sdk": "9.8.0",
87
- "@directus/format-title": "9.8.0",
88
- "@directus/schema": "9.8.0",
89
- "@directus/shared": "9.8.0",
90
- "@directus/specs": "9.8.0",
81
+ "@directus/app": "9.9.0",
82
+ "@directus/drive": "9.9.0",
83
+ "@directus/drive-azure": "9.9.0",
84
+ "@directus/drive-gcs": "9.9.0",
85
+ "@directus/drive-s3": "9.9.0",
86
+ "@directus/extensions-sdk": "9.9.0",
87
+ "@directus/format-title": "9.9.0",
88
+ "@directus/schema": "9.9.0",
89
+ "@directus/shared": "9.9.0",
90
+ "@directus/specs": "9.9.0",
91
91
  "@godaddy/terminus": "^4.9.0",
92
92
  "@rollup/plugin-alias": "^3.1.9",
93
93
  "@rollup/plugin-virtual": "^2.0.3",
@@ -150,7 +150,7 @@
150
150
  "resolve-cwd": "^3.0.0",
151
151
  "rollup": "^2.67.3",
152
152
  "sanitize-html": "^2.6.0",
153
- "sharp": "^0.29.0",
153
+ "sharp": "^0.30.3",
154
154
  "stream-json": "^1.7.1",
155
155
  "supertest": "^6.1.6",
156
156
  "tmp-promise": "^3.0.3",
@@ -173,7 +173,7 @@
173
173
  "sqlite3": "^5.0.2",
174
174
  "tedious": "^13.0.0"
175
175
  },
176
- "gitHead": "2a6db01c42dd1d7524962e7153d1d7e1bd63fb2f",
176
+ "gitHead": "fb252ca4cef57de1aa8d1914db6bc85730902e81",
177
177
  "devDependencies": {
178
178
  "@types/async": "3.2.10",
179
179
  "@types/body-parser": "1.19.2",
@@ -214,6 +214,7 @@
214
214
  "@types/wellknown": "0.5.1",
215
215
  "copyfiles": "2.4.1",
216
216
  "cross-env": "7.0.3",
217
+ "form-data": "^4.0.0",
217
218
  "jest": "27.5.1",
218
219
  "knex-mock-client": "1.6.1",
219
220
  "ts-jest": "27.1.3",