@directus/api 17.0.1 → 18.0.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 (100) hide show
  1. package/dist/app.js +8 -2
  2. package/dist/auth/drivers/ldap.js +14 -16
  3. package/dist/auth/drivers/local.js +16 -10
  4. package/dist/auth/drivers/oauth2.js +16 -11
  5. package/dist/auth/drivers/openid.js +16 -11
  6. package/dist/auth/drivers/saml.js +27 -12
  7. package/dist/cli/commands/init/index.js +3 -3
  8. package/dist/cli/commands/security/key.js +2 -2
  9. package/dist/cli/utils/create-env/env-stub.liquid +19 -4
  10. package/dist/cli/utils/create-env/index.js +2 -2
  11. package/dist/constants.d.ts +2 -1
  12. package/dist/constants.js +11 -4
  13. package/dist/controllers/auth.js +54 -19
  14. package/dist/controllers/extensions.js +102 -5
  15. package/dist/controllers/fields.js +0 -3
  16. package/dist/controllers/items.js +3 -2
  17. package/dist/controllers/permissions.js +1 -1
  18. package/dist/controllers/shares.js +19 -4
  19. package/dist/database/migrations/20220429A-add-flows.js +3 -3
  20. package/dist/database/migrations/20230526A-migrate-translation-strings.js +2 -2
  21. package/dist/database/migrations/20240204A-marketplace.d.ts +3 -0
  22. package/dist/database/migrations/20240204A-marketplace.js +68 -0
  23. package/dist/database/migrations/run.js +3 -2
  24. package/dist/extensions/lib/get-extensions-settings.d.ts +6 -2
  25. package/dist/extensions/lib/get-extensions-settings.js +70 -22
  26. package/dist/extensions/lib/get-extensions.d.ts +5 -1
  27. package/dist/extensions/lib/get-extensions.js +7 -31
  28. package/dist/extensions/lib/installation/index.d.ts +2 -0
  29. package/dist/extensions/lib/installation/index.js +9 -0
  30. package/dist/extensions/lib/installation/manager.d.ts +5 -0
  31. package/dist/extensions/lib/installation/manager.js +90 -0
  32. package/dist/extensions/lib/sandbox/generate-api-extensions-sandbox-entrypoint.d.ts +1 -1
  33. package/dist/extensions/lib/sync-extensions.js +11 -10
  34. package/dist/extensions/manager.d.ts +27 -25
  35. package/dist/extensions/manager.js +214 -183
  36. package/dist/middleware/authenticate.d.ts +1 -0
  37. package/dist/middleware/error-handler.js +22 -18
  38. package/dist/middleware/extract-token.d.ts +6 -5
  39. package/dist/middleware/extract-token.js +27 -11
  40. package/dist/middleware/merge-content-versions.d.ts +2 -0
  41. package/dist/middleware/merge-content-versions.js +26 -0
  42. package/dist/middleware/respond.js +0 -12
  43. package/dist/middleware/validate-batch.d.ts +1 -0
  44. package/dist/operations/item-update/index.js +4 -1
  45. package/dist/request/agent-with-ip-validation.d.ts +1 -1
  46. package/dist/request/agent-with-ip-validation.js +5 -1
  47. package/dist/services/activity.js +3 -3
  48. package/dist/services/assets.js +2 -3
  49. package/dist/services/authentication.d.ts +7 -2
  50. package/dist/services/authentication.js +21 -13
  51. package/dist/services/extensions.d.ts +4 -8
  52. package/dist/services/extensions.js +110 -93
  53. package/dist/services/fields.js +34 -22
  54. package/dist/services/graphql/index.js +98 -42
  55. package/dist/services/import-export.js +61 -26
  56. package/dist/services/index.d.ts +1 -1
  57. package/dist/services/index.js +1 -1
  58. package/dist/services/mail/index.d.ts +1 -1
  59. package/dist/services/mail/index.js +4 -2
  60. package/dist/services/payload.js +2 -2
  61. package/dist/services/{permissions.d.ts → permissions/index.d.ts} +3 -4
  62. package/dist/services/{permissions.js → permissions/index.js} +6 -23
  63. package/dist/services/permissions/lib/with-app-minimal-permissions.d.ts +2 -0
  64. package/dist/services/permissions/lib/with-app-minimal-permissions.js +13 -0
  65. package/dist/services/relations.d.ts +2 -3
  66. package/dist/services/relations.js +2 -2
  67. package/dist/services/roles.d.ts +9 -4
  68. package/dist/services/roles.js +51 -3
  69. package/dist/services/server.js +3 -0
  70. package/dist/services/shares.d.ts +3 -1
  71. package/dist/services/shares.js +9 -5
  72. package/dist/storage/index.js +5 -4
  73. package/dist/types/auth.d.ts +6 -4
  74. package/dist/types/graphql.d.ts +1 -0
  75. package/dist/utils/apply-query.js +3 -3
  76. package/dist/utils/filter-items.d.ts +2 -2
  77. package/dist/utils/filter-items.js +1 -3
  78. package/dist/utils/get-cache-headers.d.ts +1 -0
  79. package/dist/utils/get-cache-key.d.ts +1 -0
  80. package/dist/utils/get-graphql-query-and-variables.d.ts +1 -0
  81. package/dist/utils/get-ip-from-req.d.ts +1 -0
  82. package/dist/utils/get-milliseconds.d.ts +1 -1
  83. package/dist/utils/get-milliseconds.js +4 -1
  84. package/dist/utils/is-login-redirect-allowed.d.ts +4 -0
  85. package/dist/utils/is-login-redirect-allowed.js +34 -0
  86. package/dist/utils/is-url-allowed.d.ts +1 -1
  87. package/dist/utils/is-url-allowed.js +5 -5
  88. package/dist/utils/is-valid-uuid.d.ts +3 -0
  89. package/dist/utils/is-valid-uuid.js +21 -0
  90. package/dist/utils/jwt.d.ts +1 -1
  91. package/dist/utils/jwt.js +3 -3
  92. package/dist/utils/merge-version-data.d.ts +3 -0
  93. package/dist/utils/merge-version-data.js +134 -0
  94. package/dist/utils/sanitize-query.js +2 -0
  95. package/dist/utils/should-skip-cache.d.ts +1 -0
  96. package/dist/utils/validate-keys.js +2 -2
  97. package/dist/utils/validate-query.js +1 -0
  98. package/dist/websocket/controllers/base.js +2 -2
  99. package/dist/websocket/controllers/hooks.js +1 -1
  100. package/package.json +50 -51
@@ -224,13 +224,15 @@ export class FieldsService {
224
224
  accountability: this.accountability,
225
225
  schema: this.schema,
226
226
  });
227
- const hookAdjustedField = await emitter.emitFilter(`fields.create`, field, {
228
- collection: collection,
229
- }, {
230
- database: trx,
231
- schema: this.schema,
232
- accountability: this.accountability,
233
- });
227
+ const hookAdjustedField = opts?.emitEvents !== false
228
+ ? await emitter.emitFilter(`fields.create`, field, {
229
+ collection: collection,
230
+ }, {
231
+ database: trx,
232
+ schema: this.schema,
233
+ accountability: this.accountability,
234
+ })
235
+ : field;
234
236
  if (hookAdjustedField.type && ALIAS_TYPES.includes(hookAdjustedField.type) === false) {
235
237
  if (table) {
236
238
  this.addColumnToTable(table, hookAdjustedField);
@@ -300,15 +302,23 @@ export class FieldsService {
300
302
  }
301
303
  const runPostColumnChange = await this.helpers.schema.preColumnChange();
302
304
  const nestedActionEvents = [];
305
+ // 'type' is required for further checks on schema update
306
+ if (field.schema && !field.type) {
307
+ const existingType = this.schema.collections[collection]?.fields[field.field]?.type;
308
+ if (existingType)
309
+ field.type = existingType;
310
+ }
303
311
  try {
304
- const hookAdjustedField = await emitter.emitFilter(`fields.update`, field, {
305
- keys: [field.field],
306
- collection: collection,
307
- }, {
308
- database: this.knex,
309
- schema: this.schema,
310
- accountability: this.accountability,
311
- });
312
+ const hookAdjustedField = opts?.emitEvents !== false
313
+ ? await emitter.emitFilter(`fields.update`, field, {
314
+ keys: [field.field],
315
+ collection: collection,
316
+ }, {
317
+ database: this.knex,
318
+ schema: this.schema,
319
+ accountability: this.accountability,
320
+ })
321
+ : field;
312
322
  const record = field.meta
313
323
  ? await this.knex.select('id').from('directus_fields').where({ collection, field: field.field }).first()
314
324
  : null;
@@ -403,13 +413,15 @@ export class FieldsService {
403
413
  const runPostColumnChange = await this.helpers.schema.preColumnChange();
404
414
  const nestedActionEvents = [];
405
415
  try {
406
- await emitter.emitFilter('fields.delete', [field], {
407
- collection: collection,
408
- }, {
409
- database: this.knex,
410
- schema: this.schema,
411
- accountability: this.accountability,
412
- });
416
+ if (opts?.emitEvents !== false) {
417
+ await emitter.emitFilter('fields.delete', [field], {
418
+ collection: collection,
419
+ }, {
420
+ database: this.knex,
421
+ schema: this.schema,
422
+ accountability: this.accountability,
423
+ });
424
+ }
413
425
  await this.knex.transaction(async (trx) => {
414
426
  const relations = this.schema.relations.filter((relation) => {
415
427
  return ((relation.collection === collection && relation.field === field) ||
@@ -5,14 +5,14 @@ import { parseFilterFunctionPath, toBoolean } from '@directus/utils';
5
5
  import argon2 from 'argon2';
6
6
  import { GraphQLBoolean, GraphQLEnumType, GraphQLError, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLSchema, GraphQLString, GraphQLUnionType, NoSchemaIntrospectionCustomRule, execute, specifiedRules, validate, } from 'graphql';
7
7
  import { GraphQLJSON, InputTypeComposer, ObjectTypeComposer, SchemaComposer, toInputObjectType } from 'graphql-compose';
8
- import { assign, flatten, get, mapKeys, merge, omit, pick, set, transform, uniq } from 'lodash-es';
8
+ import { flatten, get, mapKeys, merge, omit, pick, set, transform, uniq } from 'lodash-es';
9
9
  import { clearSystemCache, getCache } from '../../cache.js';
10
- import { DEFAULT_AUTH_PROVIDER, GENERATE_SPECIAL } from '../../constants.js';
10
+ import { DEFAULT_AUTH_PROVIDER, GENERATE_SPECIAL, REFRESH_COOKIE_OPTIONS, SESSION_COOKIE_OPTIONS, } from '../../constants.js';
11
11
  import getDatabase from '../../database/index.js';
12
12
  import { generateHash } from '../../utils/generate-hash.js';
13
13
  import { getGraphQLType } from '../../utils/get-graphql-type.js';
14
- import { getMilliseconds } from '../../utils/get-milliseconds.js';
15
14
  import { getService } from '../../utils/get-service.js';
15
+ import { mergeVersionsRaw, mergeVersionsRecursive } from '../../utils/merge-version-data.js';
16
16
  import { reduceSchema } from '../../utils/reduce-schema.js';
17
17
  import { sanitizeQuery } from '../../utils/sanitize-query.js';
18
18
  import { validateQuery } from '../../utils/validate-query.js';
@@ -41,6 +41,8 @@ import { GraphQLVoid } from './types/void.js';
41
41
  import { addPathToValidationError } from './utils/add-path-to-validation-error.js';
42
42
  import processError from './utils/process-error.js';
43
43
  import { isSystemCollection } from '@directus/system-data';
44
+ import isDirectusJWT from '../../utils/is-directus-jwt.js';
45
+ import { verifyAccessJWT } from '../../utils/jwt.js';
44
46
  const env = useEnv();
45
47
  const validationRules = Array.from(specifiedRules);
46
48
  if (env['GRAPHQL_INTROSPECTION'] === false) {
@@ -372,6 +374,14 @@ export class GraphQLService {
372
374
  },
373
375
  },
374
376
  });
377
+ VersionTypes[relation.collection]?.addFields({
378
+ [relation.field]: {
379
+ type: GraphQLJSON,
380
+ resolve: (obj, _, __, info) => {
381
+ return obj[info?.path?.key ?? relation.field];
382
+ },
383
+ },
384
+ });
375
385
  if (relation.meta?.one_field) {
376
386
  CollectionTypes[relation.related_collection]?.addFields({
377
387
  [relation.meta.one_field]: {
@@ -945,6 +955,7 @@ export class GraphQLService {
945
955
  type: ReadCollectionTypes[collection.collection],
946
956
  args: {
947
957
  id: new GraphQLNonNull(GraphQLID),
958
+ version: GraphQLString,
948
959
  },
949
960
  resolve: async ({ info, context }) => {
950
961
  const result = await self.resolveQuery(info);
@@ -1201,6 +1212,7 @@ export class GraphQLService {
1201
1212
  return null;
1202
1213
  const args = this.parseArgs(info.fieldNodes[0].arguments || [], info.variableValues);
1203
1214
  let query;
1215
+ let versionRaw = false;
1204
1216
  const isAggregate = collection.endsWith('_aggregated') && collection in this.schema.collections === false;
1205
1217
  if (isAggregate) {
1206
1218
  query = this.getAggregateQuery(args, selections);
@@ -1213,6 +1225,7 @@ export class GraphQLService {
1213
1225
  }
1214
1226
  if (collection.endsWith('_by_version') && collection in this.schema.collections === false) {
1215
1227
  collection = collection.slice(0, -11);
1228
+ versionRaw = true;
1216
1229
  }
1217
1230
  }
1218
1231
  if (args['id']) {
@@ -1240,12 +1253,16 @@ export class GraphQLService {
1240
1253
  const saves = await versionsService.getVersionSaves(args['version'], collection, args['id']);
1241
1254
  if (saves) {
1242
1255
  if (this.schema.collections[collection].singleton) {
1243
- return assign(result, ...saves);
1256
+ return versionRaw
1257
+ ? mergeVersionsRaw(result, saves)
1258
+ : mergeVersionsRecursive(result, saves, collection, this.schema);
1244
1259
  }
1245
1260
  else {
1246
1261
  if (result?.[0] === undefined)
1247
1262
  return null;
1248
- return assign(result[0], ...saves);
1263
+ return versionRaw
1264
+ ? mergeVersionsRaw(result[0], saves)
1265
+ : mergeVersionsRecursive(result[0], saves, collection, this.schema);
1249
1266
  }
1250
1267
  }
1251
1268
  }
@@ -1587,6 +1604,7 @@ export class GraphQLService {
1587
1604
  values: {
1588
1605
  json: { value: 'json' },
1589
1606
  cookie: { value: 'cookie' },
1607
+ session: { value: 'session' },
1590
1608
  },
1591
1609
  });
1592
1610
  const ServerInfo = schemaComposer.createObjectTC({
@@ -1789,21 +1807,24 @@ export class GraphQLService {
1789
1807
  accountability: accountability,
1790
1808
  schema: this.schema,
1791
1809
  });
1792
- const result = await authenticationService.login(DEFAULT_AUTH_PROVIDER, args, args?.otp);
1793
- if (args['mode'] === 'cookie') {
1794
- res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], result['refreshToken'], {
1795
- httpOnly: true,
1796
- domain: env['REFRESH_TOKEN_COOKIE_DOMAIN'],
1797
- maxAge: getMilliseconds(env['REFRESH_TOKEN_TTL']),
1798
- secure: env['REFRESH_TOKEN_COOKIE_SECURE'] ?? false,
1799
- sameSite: env['REFRESH_TOKEN_COOKIE_SAME_SITE'] || 'strict',
1800
- });
1810
+ const mode = args['mode'] ?? 'json';
1811
+ const { accessToken, refreshToken, expires } = await authenticationService.login(DEFAULT_AUTH_PROVIDER, args, {
1812
+ session: mode === 'session',
1813
+ otp: args?.otp,
1814
+ });
1815
+ const payload = { expires };
1816
+ if (mode === 'json') {
1817
+ payload.refresh_token = refreshToken;
1818
+ payload.access_token = accessToken;
1801
1819
  }
1802
- return {
1803
- access_token: result['accessToken'],
1804
- expires: result['expires'],
1805
- refresh_token: result['refreshToken'],
1806
- };
1820
+ if (mode === 'cookie') {
1821
+ res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, REFRESH_COOKIE_OPTIONS);
1822
+ payload.access_token = accessToken;
1823
+ }
1824
+ if (mode === 'session') {
1825
+ res?.cookie(env['SESSION_COOKIE_NAME'], accessToken, SESSION_COOKIE_OPTIONS);
1826
+ }
1827
+ return payload;
1807
1828
  },
1808
1829
  },
1809
1830
  auth_refresh: {
@@ -1826,35 +1847,51 @@ export class GraphQLService {
1826
1847
  accountability: accountability,
1827
1848
  schema: this.schema,
1828
1849
  });
1829
- const currentRefreshToken = args['refresh_token'] || req?.cookies[env['REFRESH_TOKEN_COOKIE_NAME']];
1850
+ const mode = args['mode'] ?? 'json';
1851
+ let currentRefreshToken;
1852
+ if (mode === 'json') {
1853
+ currentRefreshToken = args['refresh_token'];
1854
+ }
1855
+ else if (mode === 'cookie') {
1856
+ currentRefreshToken = req?.cookies[env['REFRESH_TOKEN_COOKIE_NAME']];
1857
+ }
1858
+ else if (mode === 'session') {
1859
+ const token = req?.cookies[env['SESSION_COOKIE_NAME']];
1860
+ if (isDirectusJWT(token)) {
1861
+ const payload = verifyAccessJWT(token, env['SECRET']);
1862
+ currentRefreshToken = payload.session;
1863
+ }
1864
+ }
1830
1865
  if (!currentRefreshToken) {
1831
1866
  throw new InvalidPayloadError({
1832
- reason: `"refresh_token" is required in either the JSON payload or Cookie`,
1867
+ reason: `The refresh token is required in either the payload or cookie`,
1833
1868
  });
1834
1869
  }
1835
- const result = await authenticationService.refresh(currentRefreshToken);
1836
- if (args['mode'] === 'cookie') {
1837
- res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], result['refreshToken'], {
1838
- httpOnly: true,
1839
- domain: env['REFRESH_TOKEN_COOKIE_DOMAIN'],
1840
- maxAge: getMilliseconds(env['REFRESH_TOKEN_TTL']),
1841
- secure: env['REFRESH_TOKEN_COOKIE_SECURE'] ?? false,
1842
- sameSite: env['REFRESH_TOKEN_COOKIE_SAME_SITE'] || 'strict',
1843
- });
1870
+ const { accessToken, refreshToken, expires } = await authenticationService.refresh(currentRefreshToken, {
1871
+ session: mode === 'session',
1872
+ });
1873
+ const payload = { expires };
1874
+ if (mode === 'json') {
1875
+ payload.refresh_token = refreshToken;
1876
+ payload.access_token = accessToken;
1844
1877
  }
1845
- return {
1846
- access_token: result['accessToken'],
1847
- expires: result['expires'],
1848
- refresh_token: result['refreshToken'],
1849
- };
1878
+ if (mode === 'cookie') {
1879
+ res?.cookie(env['REFRESH_TOKEN_COOKIE_NAME'], refreshToken, REFRESH_COOKIE_OPTIONS);
1880
+ payload.access_token = accessToken;
1881
+ }
1882
+ if (mode === 'session') {
1883
+ res?.cookie(env['SESSION_COOKIE_NAME'], accessToken, SESSION_COOKIE_OPTIONS);
1884
+ }
1885
+ return payload;
1850
1886
  },
1851
1887
  },
1852
1888
  auth_logout: {
1853
1889
  type: GraphQLBoolean,
1854
1890
  args: {
1855
1891
  refresh_token: GraphQLString,
1892
+ mode: AuthMode,
1856
1893
  },
1857
- resolve: async (_, args, { req }) => {
1894
+ resolve: async (_, args, { req, res }) => {
1858
1895
  const accountability = { role: null };
1859
1896
  if (req?.ip)
1860
1897
  accountability.ip = req.ip;
@@ -1868,13 +1905,33 @@ export class GraphQLService {
1868
1905
  accountability: accountability,
1869
1906
  schema: this.schema,
1870
1907
  });
1871
- const currentRefreshToken = args['refresh_token'] || req?.cookies[env['REFRESH_TOKEN_COOKIE_NAME']];
1908
+ const mode = args['mode'] ?? 'json';
1909
+ let currentRefreshToken;
1910
+ if (mode === 'json') {
1911
+ currentRefreshToken = args['refresh_token'];
1912
+ }
1913
+ else if (mode === 'cookie') {
1914
+ currentRefreshToken = req?.cookies[env['REFRESH_TOKEN_COOKIE_NAME']];
1915
+ }
1916
+ else if (mode === 'session') {
1917
+ const token = req?.cookies[env['SESSION_COOKIE_NAME']];
1918
+ if (isDirectusJWT(token)) {
1919
+ const payload = verifyAccessJWT(token, env['SECRET']);
1920
+ currentRefreshToken = payload.session;
1921
+ }
1922
+ }
1872
1923
  if (!currentRefreshToken) {
1873
1924
  throw new InvalidPayloadError({
1874
- reason: `"refresh_token" is required in either the JSON payload or Cookie`,
1925
+ reason: `The refresh token is required in either the payload or cookie`,
1875
1926
  });
1876
1927
  }
1877
1928
  await authenticationService.logout(currentRefreshToken);
1929
+ if (req?.cookies[env['REFRESH_TOKEN_COOKIE_NAME']]) {
1930
+ res?.clearCookie(env['REFRESH_TOKEN_COOKIE_NAME'], REFRESH_COOKIE_OPTIONS);
1931
+ }
1932
+ if (req?.cookies[env['SESSION_COOKIE_NAME']]) {
1933
+ res?.clearCookie(env['SESSION_COOKIE_NAME'], SESSION_COOKIE_OPTIONS);
1934
+ }
1878
1935
  return true;
1879
1936
  },
1880
1937
  },
@@ -2479,8 +2536,7 @@ export class GraphQLService {
2479
2536
  update_extensions_item: {
2480
2537
  type: Extension,
2481
2538
  args: {
2482
- bundle: GraphQLString,
2483
- name: new GraphQLNonNull(GraphQLString),
2539
+ id: GraphQLID,
2484
2540
  data: toInputObjectType(schemaComposer.createObjectTC({
2485
2541
  name: 'update_directus_extensions_input',
2486
2542
  fields: {
@@ -2498,8 +2554,8 @@ export class GraphQLService {
2498
2554
  accountability: this.accountability,
2499
2555
  schema: this.schema,
2500
2556
  });
2501
- await extensionsService.updateOne(args['bundle'], args['name'], args['data']);
2502
- return await extensionsService.readOne(args['bundle'], args['name']);
2557
+ await extensionsService.updateOne(args['id'], args['data']);
2558
+ return await extensionsService.readOne(args['id']);
2503
2559
  },
2504
2560
  },
2505
2561
  });
@@ -1,12 +1,14 @@
1
1
  import { useEnv } from '@directus/env';
2
2
  import { ForbiddenError, InvalidPayloadError, ServiceUnavailableError, UnsupportedMediaTypeError, } from '@directus/errors';
3
+ import { isSystemCollection } from '@directus/system-data';
3
4
  import { parseJSON, toArray } from '@directus/utils';
5
+ import { createTmpFile } from '@directus/utils/node';
4
6
  import { queue } from 'async';
5
7
  import destroyStream from 'destroy';
6
8
  import { dump as toYAML } from 'js-yaml';
7
9
  import { parse as toXML } from 'js2xmlparser';
8
10
  import { Parser as CSVParser, transforms as CSVTransforms } from 'json2csv';
9
- import { createReadStream } from 'node:fs';
11
+ import { createReadStream, createWriteStream } from 'node:fs';
10
12
  import { appendFile } from 'node:fs/promises';
11
13
  import Papa from 'papaparse';
12
14
  import StreamArray from 'stream-json/streamers/StreamArray.js';
@@ -20,7 +22,6 @@ import { FilesService } from './files.js';
20
22
  import { ItemsService } from './items.js';
21
23
  import { NotificationsService } from './notifications.js';
22
24
  import { UsersService } from './users.js';
23
- import { isSystemCollection } from '@directus/system-data';
24
25
  const env = useEnv();
25
26
  const logger = useLogger();
26
27
  export class ImportService {
@@ -86,7 +87,10 @@ export class ImportService {
86
87
  });
87
88
  });
88
89
  }
89
- importCSV(collection, stream) {
90
+ async importCSV(collection, stream) {
91
+ const tmpFile = await createTmpFile().catch(() => null);
92
+ if (!tmpFile)
93
+ throw new Error('Failed to create temporary file for import');
90
94
  const nestedActionEvents = [];
91
95
  return this.knex.transaction((trx) => {
92
96
  const service = new ItemsService(collection, {
@@ -116,35 +120,66 @@ export class ImportService {
116
120
  transform,
117
121
  };
118
122
  return new Promise((resolve, reject) => {
119
- stream
120
- .pipe(Papa.parse(Papa.NODE_STREAM_INPUT, PapaOptions))
121
- .on('data', (obj) => {
122
- // Filter out all undefined fields
123
- for (const field in obj) {
124
- if (obj[field] === undefined) {
125
- delete obj[field];
123
+ const streams = [stream];
124
+ const cleanup = (destroy = true) => {
125
+ if (destroy) {
126
+ for (const stream of streams) {
127
+ destroyStream(stream);
126
128
  }
127
129
  }
128
- saveQueue.push(obj);
129
- })
130
- .on('error', (err) => {
131
- destroyStream(stream);
132
- reject(new InvalidPayloadError({ reason: err.message }));
130
+ tmpFile.cleanup().catch(() => {
131
+ logger.warn(`Failed to cleanup temporary import file (${tmpFile.path})`);
132
+ });
133
+ };
134
+ saveQueue.error((error) => {
135
+ reject(error);
136
+ });
137
+ const fileWriteStream = createWriteStream(tmpFile.path)
138
+ .on('error', (error) => {
139
+ cleanup();
140
+ reject(new Error('Error while writing import data to temporary file', { cause: error }));
133
141
  })
134
- .on('end', () => {
135
- // In case of empty CSV file
136
- if (!saveQueue.started)
137
- return resolve();
138
- saveQueue.drain(() => {
139
- for (const nestedActionEvent of nestedActionEvents) {
140
- emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
142
+ .on('finish', () => {
143
+ const fileReadStream = createReadStream(tmpFile.path).on('error', (error) => {
144
+ cleanup();
145
+ reject(new Error('Error while reading import data from temporary file', { cause: error }));
146
+ });
147
+ streams.push(fileReadStream);
148
+ fileReadStream
149
+ .pipe(Papa.parse(Papa.NODE_STREAM_INPUT, PapaOptions))
150
+ .on('data', (obj) => {
151
+ // Filter out all undefined fields
152
+ for (const field in obj) {
153
+ if (obj[field] === undefined) {
154
+ delete obj[field];
155
+ }
141
156
  }
142
- return resolve();
157
+ saveQueue.push(obj);
158
+ })
159
+ .on('error', (error) => {
160
+ cleanup();
161
+ reject(new InvalidPayloadError({ reason: error.message }));
162
+ })
163
+ .on('end', () => {
164
+ cleanup(false);
165
+ // In case of empty CSV file
166
+ if (!saveQueue.started)
167
+ return resolve();
168
+ saveQueue.drain(() => {
169
+ for (const nestedActionEvent of nestedActionEvents) {
170
+ emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
171
+ }
172
+ return resolve();
173
+ });
143
174
  });
144
175
  });
145
- saveQueue.error((err) => {
146
- reject(err);
147
- });
176
+ streams.push(fileWriteStream);
177
+ stream
178
+ .on('error', (error) => {
179
+ cleanup();
180
+ reject(new Error('Error while retrieving import data', { cause: error }));
181
+ })
182
+ .pipe(fileWriteStream);
148
183
  });
149
184
  });
150
185
  }
@@ -18,7 +18,7 @@ export * from './notifications.js';
18
18
  export * from './operations.js';
19
19
  export * from './panels.js';
20
20
  export * from './payload.js';
21
- export * from './permissions.js';
21
+ export * from './permissions/index.js';
22
22
  export * from './presets.js';
23
23
  export * from './relations.js';
24
24
  export * from './revisions.js';
@@ -18,7 +18,7 @@ export * from './notifications.js';
18
18
  export * from './operations.js';
19
19
  export * from './panels.js';
20
20
  export * from './payload.js';
21
- export * from './permissions.js';
21
+ export * from './permissions/index.js';
22
22
  export * from './presets.js';
23
23
  export * from './relations.js';
24
24
  export * from './revisions.js';
@@ -14,7 +14,7 @@ export declare class MailService {
14
14
  knex: Knex;
15
15
  mailer: Transporter;
16
16
  constructor(opts: AbstractServiceOptions);
17
- send<T>(options: EmailOptions): Promise<T>;
17
+ send(options: EmailOptions): Promise<void>;
18
18
  private renderTemplate;
19
19
  private getDefaultTemplateData;
20
20
  }
@@ -55,8 +55,10 @@ export class MailService {
55
55
  .map((line) => line.trim())
56
56
  .join('\n');
57
57
  }
58
- const info = await this.mailer.sendMail({ ...emailOptions, from, html });
59
- return info;
58
+ this.mailer.sendMail({ ...emailOptions, from, html }).catch((error) => {
59
+ logger.warn(`Email send failed:`);
60
+ logger.warn(error);
61
+ });
60
62
  }
61
63
  async renderTemplate(template, variables) {
62
64
  const customTemplatePath = path.resolve(getExtensionsPath(), 'templates', template + '.liquid');
@@ -4,7 +4,7 @@ import { format, isValid, parseISO } from 'date-fns';
4
4
  import { unflatten } from 'flat';
5
5
  import Joi from 'joi';
6
6
  import { clone, cloneDeep, isNil, isObject, isPlainObject, omit, pick } from 'lodash-es';
7
- import { v4 as uuid } from 'uuid';
7
+ import { randomUUID } from 'node:crypto';
8
8
  import { parse as wktToGeoJSON } from 'wellknown';
9
9
  import { getHelpers } from '../database/helpers/index.js';
10
10
  import getDatabase from '../database/index.js';
@@ -39,7 +39,7 @@ export class PayloadService {
39
39
  },
40
40
  async uuid({ action, value }) {
41
41
  if (action === 'create' && !value) {
42
- return uuid();
42
+ return randomUUID();
43
43
  }
44
44
  return value;
45
45
  },
@@ -1,14 +1,13 @@
1
1
  import type { ItemPermissions, PermissionsAction, Query } from '@directus/types';
2
2
  import type Keyv from 'keyv';
3
- import type { AbstractServiceOptions, Item, MutationOptions, PrimaryKey } from '../types/index.js';
4
- import type { QueryOptions } from './items.js';
5
- import { ItemsService } from './items.js';
3
+ import type { AbstractServiceOptions, Item, MutationOptions, PrimaryKey } from '../../types/index.js';
4
+ import type { QueryOptions } from '../items.js';
5
+ import { ItemsService } from '../items.js';
6
6
  export declare class PermissionsService extends ItemsService {
7
7
  systemCache: Keyv<any>;
8
8
  constructor(options: AbstractServiceOptions);
9
9
  getAllowedFields(action: PermissionsAction, collection?: string): Record<string, string[]>;
10
10
  readByQuery(query: Query, opts?: QueryOptions): Promise<Partial<Item>[]>;
11
- readMany(keys: PrimaryKey[], query?: Query, opts?: QueryOptions): Promise<Partial<Item>[]>;
12
11
  createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
13
12
  createMany(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]>;
14
13
  updateBatch(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]>;
@@ -1,9 +1,8 @@
1
1
  import { ForbiddenError } from '@directus/errors';
2
- import { clearSystemCache, getCache } from '../cache.js';
3
- import { appAccessMinimalPermissions } from '@directus/system-data';
4
- import { filterItems } from '../utils/filter-items.js';
5
- import { AuthorizationService } from './authorization.js';
6
- import { ItemsService } from './items.js';
2
+ import { clearSystemCache, getCache } from '../../cache.js';
3
+ import { AuthorizationService } from '../authorization.js';
4
+ import { ItemsService } from '../items.js';
5
+ import { withAppMinimalPermissions } from './lib/with-app-minimal-permissions.js';
7
6
  export class PermissionsService extends ItemsService {
8
7
  systemCache;
9
8
  constructor(options) {
@@ -30,24 +29,8 @@ export class PermissionsService extends ItemsService {
30
29
  return fieldsPerCollection;
31
30
  }
32
31
  async readByQuery(query, opts) {
33
- const result = await super.readByQuery(query, opts);
34
- if (Array.isArray(result) && this.accountability && this.accountability.app === true) {
35
- result.push(...filterItems(appAccessMinimalPermissions.map((permission) => ({
36
- ...permission,
37
- role: this.accountability.role,
38
- })), query.filter));
39
- }
40
- return result;
41
- }
42
- async readMany(keys, query = {}, opts) {
43
- const result = await super.readMany(keys, query, opts);
44
- if (this.accountability && this.accountability.app === true) {
45
- result.push(...filterItems(appAccessMinimalPermissions.map((permission) => ({
46
- ...permission,
47
- role: this.accountability.role,
48
- })), query.filter));
49
- }
50
- return result;
32
+ const result = (await super.readByQuery(query, opts));
33
+ return withAppMinimalPermissions(this.accountability, result, query.filter);
51
34
  }
52
35
  async createOne(data, opts) {
53
36
  const res = await super.createOne(data, opts);
@@ -0,0 +1,2 @@
1
+ import type { Accountability, Permission, Query } from '@directus/types';
2
+ export declare function withAppMinimalPermissions(accountability: Accountability | null, permissions: Permission[], filter: Query['filter']): Permission[];
@@ -0,0 +1,13 @@
1
+ import { appAccessMinimalPermissions } from '@directus/system-data';
2
+ import { filterItems } from '../../../utils/filter-items.js';
3
+ import { mergePermissions } from '../../../utils/merge-permissions.js';
4
+ export function withAppMinimalPermissions(accountability, permissions, filter) {
5
+ if (accountability?.app === true) {
6
+ const filteredAppMinimalPermissions = filterItems(appAccessMinimalPermissions.map((permission) => ({
7
+ ...permission,
8
+ role: accountability.role,
9
+ })), filter);
10
+ return mergePermissions('or', permissions, filteredAppMinimalPermissions);
11
+ }
12
+ return permissions;
13
+ }
@@ -4,9 +4,8 @@ import type Keyv from 'keyv';
4
4
  import type { Knex } from 'knex';
5
5
  import type { Helpers } from '../database/helpers/index.js';
6
6
  import type { AbstractServiceOptions, MutationOptions } from '../types/index.js';
7
- import type { QueryOptions } from './items.js';
8
- import { ItemsService } from './items.js';
9
- import { PermissionsService } from './permissions.js';
7
+ import { ItemsService, type QueryOptions } from './items.js';
8
+ import { PermissionsService } from './permissions/index.js';
10
9
  export declare class RelationsService {
11
10
  knex: Knex;
12
11
  permissionsService: PermissionsService;
@@ -1,5 +1,6 @@
1
1
  import { ForbiddenError, InvalidPayloadError } from '@directus/errors';
2
2
  import { createInspector } from '@directus/schema';
3
+ import { systemRelationRows } from '@directus/system-data';
3
4
  import { toArray } from '@directus/utils';
4
5
  import { clearSystemCache, getCache } from '../cache.js';
5
6
  import { getHelpers } from '../database/helpers/index.js';
@@ -8,8 +9,7 @@ import emitter from '../emitter.js';
8
9
  import { getDefaultIndexName } from '../utils/get-default-index-name.js';
9
10
  import { getSchema } from '../utils/get-schema.js';
10
11
  import { ItemsService } from './items.js';
11
- import { PermissionsService } from './permissions.js';
12
- import { systemRelationRows } from '@directus/system-data';
12
+ import { PermissionsService } from './permissions/index.js';
13
13
  export class RelationsService {
14
14
  knex;
15
15
  permissionsService;