@directus/api 21.0.0 → 22.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 (285) hide show
  1. package/dist/app.js +4 -4
  2. package/dist/auth/drivers/ldap.js +4 -4
  3. package/dist/auth/drivers/local.js +4 -4
  4. package/dist/auth/drivers/oauth2.js +4 -4
  5. package/dist/auth/drivers/openid.js +2 -4
  6. package/dist/cache.js +3 -0
  7. package/dist/cli/commands/bootstrap/index.js +8 -2
  8. package/dist/cli/commands/init/index.js +9 -10
  9. package/dist/cli/utils/defaults.d.ts +4 -11
  10. package/dist/cli/utils/defaults.js +7 -1
  11. package/dist/constants.d.ts +1 -1
  12. package/dist/controllers/access.d.ts +2 -0
  13. package/dist/controllers/access.js +148 -0
  14. package/dist/controllers/auth.js +5 -16
  15. package/dist/controllers/permissions.js +14 -2
  16. package/dist/controllers/policies.d.ts +2 -0
  17. package/dist/controllers/policies.js +169 -0
  18. package/dist/controllers/roles.js +22 -1
  19. package/dist/controllers/tus.js +14 -26
  20. package/dist/controllers/users.js +0 -55
  21. package/dist/database/get-ast-from-query/get-ast-from-query.d.ts +16 -0
  22. package/dist/database/get-ast-from-query/get-ast-from-query.js +82 -0
  23. package/dist/database/get-ast-from-query/lib/convert-wildcards.d.ts +13 -0
  24. package/dist/database/get-ast-from-query/lib/convert-wildcards.js +69 -0
  25. package/dist/database/get-ast-from-query/lib/parse-fields.d.ts +15 -0
  26. package/dist/database/get-ast-from-query/lib/parse-fields.js +200 -0
  27. package/dist/database/get-ast-from-query/utils/get-deep-query.d.ts +14 -0
  28. package/dist/database/get-ast-from-query/utils/get-deep-query.js +17 -0
  29. package/dist/database/get-ast-from-query/utils/get-related-collection.d.ts +2 -0
  30. package/dist/database/get-ast-from-query/utils/get-related-collection.js +13 -0
  31. package/dist/database/get-ast-from-query/utils/get-relation.d.ts +2 -0
  32. package/dist/database/get-ast-from-query/utils/get-relation.js +7 -0
  33. package/dist/database/helpers/fn/types.d.ts +2 -1
  34. package/dist/database/helpers/fn/types.js +1 -1
  35. package/dist/database/helpers/geometry/dialects/mssql.d.ts +1 -1
  36. package/dist/database/helpers/geometry/dialects/mssql.js +4 -2
  37. package/dist/database/helpers/geometry/dialects/mysql.js +1 -1
  38. package/dist/database/helpers/geometry/dialects/oracle.d.ts +1 -1
  39. package/dist/database/helpers/geometry/dialects/oracle.js +5 -3
  40. package/dist/database/helpers/geometry/types.d.ts +1 -1
  41. package/dist/database/helpers/geometry/types.js +4 -2
  42. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +2 -1
  43. package/dist/database/helpers/schema/dialects/cockroachdb.js +4 -0
  44. package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -1
  45. package/dist/database/helpers/schema/dialects/mssql.js +4 -0
  46. package/dist/database/helpers/schema/dialects/oracle.d.ts +2 -1
  47. package/dist/database/helpers/schema/dialects/oracle.js +4 -0
  48. package/dist/database/helpers/schema/dialects/postgres.d.ts +2 -1
  49. package/dist/database/helpers/schema/dialects/postgres.js +4 -0
  50. package/dist/database/helpers/schema/types.d.ts +5 -0
  51. package/dist/database/helpers/schema/types.js +3 -0
  52. package/dist/database/helpers/schema/utils/preprocess-bindings.d.ts +8 -0
  53. package/dist/database/helpers/schema/utils/preprocess-bindings.js +30 -0
  54. package/dist/database/index.js +6 -1
  55. package/dist/{utils/merge-permissions.d.ts → database/migrations/20240806A-permissions-policies.d.ts} +4 -1
  56. package/dist/database/migrations/20240806A-permissions-policies.js +338 -0
  57. package/dist/database/run-ast/lib/get-db-query.d.ts +4 -0
  58. package/dist/database/run-ast/lib/get-db-query.js +218 -0
  59. package/dist/database/run-ast/lib/parse-current-level.d.ts +7 -0
  60. package/dist/database/run-ast/lib/parse-current-level.js +41 -0
  61. package/dist/database/run-ast/run-ast.d.ts +7 -0
  62. package/dist/database/run-ast/run-ast.js +107 -0
  63. package/dist/database/{run-ast.d.ts → run-ast/types.d.ts} +3 -9
  64. package/dist/database/run-ast/types.js +1 -0
  65. package/dist/database/run-ast/utils/apply-case-when.d.ts +16 -0
  66. package/dist/database/run-ast/utils/apply-case-when.js +27 -0
  67. package/dist/database/run-ast/utils/apply-parent-filters.d.ts +3 -0
  68. package/dist/database/run-ast/utils/apply-parent-filters.js +55 -0
  69. package/dist/database/run-ast/utils/get-column-pre-processor.d.ts +10 -0
  70. package/dist/database/run-ast/utils/get-column-pre-processor.js +57 -0
  71. package/dist/database/run-ast/utils/get-field-alias.d.ts +2 -0
  72. package/dist/database/run-ast/utils/get-field-alias.js +4 -0
  73. package/dist/database/run-ast/utils/get-inner-query-column-pre-processor.d.ts +5 -0
  74. package/dist/database/run-ast/utils/get-inner-query-column-pre-processor.js +23 -0
  75. package/dist/database/run-ast/utils/merge-with-parent-items.d.ts +3 -0
  76. package/dist/database/run-ast/utils/merge-with-parent-items.js +87 -0
  77. package/dist/database/run-ast/utils/remove-temporary-fields.d.ts +3 -0
  78. package/dist/database/run-ast/utils/remove-temporary-fields.js +73 -0
  79. package/dist/database/run-ast/utils/with-preprocess-bindings.d.ts +2 -0
  80. package/dist/database/run-ast/utils/with-preprocess-bindings.js +14 -0
  81. package/dist/flows.js +3 -4
  82. package/dist/middleware/authenticate.js +2 -7
  83. package/dist/middleware/cache.js +1 -1
  84. package/dist/middleware/respond.js +1 -1
  85. package/dist/permissions/cache.d.ts +2 -0
  86. package/dist/permissions/cache.js +23 -0
  87. package/dist/permissions/lib/fetch-permissions.d.ts +11 -0
  88. package/dist/permissions/lib/fetch-permissions.js +56 -0
  89. package/dist/permissions/lib/fetch-policies.d.ts +14 -0
  90. package/dist/permissions/lib/fetch-policies.js +43 -0
  91. package/dist/permissions/lib/fetch-roles-tree.d.ts +3 -0
  92. package/dist/permissions/lib/fetch-roles-tree.js +28 -0
  93. package/dist/{services/permissions → permissions}/lib/with-app-minimal-permissions.d.ts +1 -1
  94. package/dist/permissions/lib/with-app-minimal-permissions.js +10 -0
  95. package/dist/permissions/modules/fetch-accountability-collection-access/fetch-accountability-collection-access.d.ts +7 -0
  96. package/dist/permissions/modules/fetch-accountability-collection-access/fetch-accountability-collection-access.js +56 -0
  97. package/dist/permissions/modules/fetch-accountability-policy-globals/fetch-accountability-policy-globals.d.ts +3 -0
  98. package/dist/permissions/modules/fetch-accountability-policy-globals/fetch-accountability-policy-globals.js +16 -0
  99. package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.d.ts +8 -0
  100. package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.js +24 -0
  101. package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.d.ts +9 -0
  102. package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.js +31 -0
  103. package/dist/permissions/modules/fetch-allowed-fields/fetch-allowed-fields.d.ts +16 -0
  104. package/dist/permissions/modules/fetch-allowed-fields/fetch-allowed-fields.js +27 -0
  105. package/dist/permissions/modules/fetch-global-access/fetch-global-access.d.ts +10 -0
  106. package/dist/permissions/modules/fetch-global-access/fetch-global-access.js +23 -0
  107. package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-roles.d.ts +5 -0
  108. package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-roles.js +7 -0
  109. package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-user.d.ts +5 -0
  110. package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-user.js +10 -0
  111. package/dist/permissions/modules/fetch-global-access/types.d.ts +4 -0
  112. package/dist/permissions/modules/fetch-global-access/types.js +1 -0
  113. package/dist/permissions/modules/fetch-global-access/utils/fetch-global-access-for-query.d.ts +4 -0
  114. package/dist/permissions/modules/fetch-global-access/utils/fetch-global-access-for-query.js +27 -0
  115. package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.d.ts +12 -0
  116. package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.js +32 -0
  117. package/dist/permissions/modules/fetch-policies-ip-access/fetch-policies-ip-access.d.ts +4 -0
  118. package/dist/permissions/modules/fetch-policies-ip-access/fetch-policies-ip-access.js +29 -0
  119. package/dist/permissions/modules/process-ast/lib/extract-fields-from-children.d.ts +4 -0
  120. package/dist/permissions/modules/process-ast/lib/extract-fields-from-children.js +49 -0
  121. package/dist/permissions/modules/process-ast/lib/extract-fields-from-query.d.ts +3 -0
  122. package/dist/permissions/modules/process-ast/lib/extract-fields-from-query.js +56 -0
  123. package/dist/permissions/modules/process-ast/lib/field-map-from-ast.d.ts +4 -0
  124. package/dist/permissions/modules/process-ast/lib/field-map-from-ast.js +8 -0
  125. package/dist/permissions/modules/process-ast/lib/inject-cases.d.ts +9 -0
  126. package/dist/permissions/modules/process-ast/lib/inject-cases.js +93 -0
  127. package/dist/permissions/modules/process-ast/process-ast.d.ts +9 -0
  128. package/dist/permissions/modules/process-ast/process-ast.js +39 -0
  129. package/dist/permissions/modules/process-ast/types.d.ts +18 -0
  130. package/dist/permissions/modules/process-ast/types.js +1 -0
  131. package/dist/permissions/modules/process-ast/utils/collections-in-field-map.d.ts +2 -0
  132. package/dist/permissions/modules/process-ast/utils/collections-in-field-map.js +7 -0
  133. package/dist/permissions/modules/process-ast/utils/dedupe-access.d.ts +12 -0
  134. package/dist/permissions/modules/process-ast/utils/dedupe-access.js +30 -0
  135. package/dist/permissions/modules/process-ast/utils/extract-paths-from-query.d.ts +15 -0
  136. package/dist/permissions/modules/process-ast/utils/extract-paths-from-query.js +60 -0
  137. package/dist/permissions/modules/process-ast/utils/find-related-collection.d.ts +3 -0
  138. package/dist/permissions/modules/process-ast/utils/find-related-collection.js +9 -0
  139. package/dist/permissions/modules/process-ast/utils/flatten-filter.d.ts +3 -0
  140. package/dist/permissions/modules/process-ast/utils/flatten-filter.js +34 -0
  141. package/dist/permissions/modules/process-ast/utils/format-a2o-key.d.ts +1 -0
  142. package/dist/permissions/modules/process-ast/utils/format-a2o-key.js +3 -0
  143. package/dist/permissions/modules/process-ast/utils/get-info-for-path.d.ts +5 -0
  144. package/dist/permissions/modules/process-ast/utils/get-info-for-path.js +7 -0
  145. package/dist/permissions/modules/process-ast/utils/has-item-permissions.d.ts +2 -0
  146. package/dist/permissions/modules/process-ast/utils/has-item-permissions.js +3 -0
  147. package/dist/permissions/modules/process-ast/utils/stringify-query-path.d.ts +2 -0
  148. package/dist/permissions/modules/process-ast/utils/stringify-query-path.js +3 -0
  149. package/dist/permissions/modules/process-ast/utils/validate-path/create-error.d.ts +3 -0
  150. package/dist/permissions/modules/process-ast/utils/validate-path/create-error.js +16 -0
  151. package/dist/permissions/modules/process-ast/utils/validate-path/validate-path-existence.d.ts +2 -0
  152. package/dist/permissions/modules/process-ast/utils/validate-path/validate-path-existence.js +12 -0
  153. package/dist/permissions/modules/process-ast/utils/validate-path/validate-path-permissions.d.ts +2 -0
  154. package/dist/permissions/modules/process-ast/utils/validate-path/validate-path-permissions.js +28 -0
  155. package/dist/permissions/modules/process-payload/lib/is-field-nullable.d.ts +5 -0
  156. package/dist/permissions/modules/process-payload/lib/is-field-nullable.js +12 -0
  157. package/dist/permissions/modules/process-payload/process-payload.d.ts +13 -0
  158. package/dist/permissions/modules/process-payload/process-payload.js +77 -0
  159. package/dist/permissions/modules/validate-access/lib/validate-collection-access.d.ts +12 -0
  160. package/dist/permissions/modules/validate-access/lib/validate-collection-access.js +11 -0
  161. package/dist/permissions/modules/validate-access/lib/validate-item-access.d.ts +9 -0
  162. package/dist/permissions/modules/validate-access/lib/validate-item-access.js +33 -0
  163. package/dist/permissions/modules/validate-access/validate-access.d.ts +14 -0
  164. package/dist/permissions/modules/validate-access/validate-access.js +28 -0
  165. package/dist/permissions/modules/validate-remaining-admin/validate-remaining-admin-count.d.ts +1 -0
  166. package/dist/permissions/modules/validate-remaining-admin/validate-remaining-admin-count.js +8 -0
  167. package/dist/permissions/modules/validate-remaining-admin/validate-remaining-admin-users.d.ts +5 -0
  168. package/dist/permissions/modules/validate-remaining-admin/validate-remaining-admin-users.js +10 -0
  169. package/dist/permissions/types.d.ts +6 -0
  170. package/dist/permissions/types.js +1 -0
  171. package/dist/permissions/utils/create-default-accountability.d.ts +2 -0
  172. package/dist/permissions/utils/create-default-accountability.js +11 -0
  173. package/dist/permissions/utils/extract-required-dynamic-variable-context.d.ts +8 -0
  174. package/dist/permissions/utils/extract-required-dynamic-variable-context.js +27 -0
  175. package/dist/permissions/utils/fetch-dynamic-variable-context.d.ts +9 -0
  176. package/dist/permissions/utils/fetch-dynamic-variable-context.js +43 -0
  177. package/dist/permissions/utils/filter-policies-by-ip.d.ts +2 -0
  178. package/dist/permissions/utils/filter-policies-by-ip.js +15 -0
  179. package/dist/permissions/utils/get-unaliased-field-key.d.ts +5 -0
  180. package/dist/permissions/utils/get-unaliased-field-key.js +17 -0
  181. package/dist/permissions/utils/process-permissions.d.ts +7 -0
  182. package/dist/permissions/utils/process-permissions.js +9 -0
  183. package/dist/permissions/utils/with-cache.d.ts +10 -0
  184. package/dist/permissions/utils/with-cache.js +25 -0
  185. package/dist/services/access.d.ts +10 -0
  186. package/dist/services/access.js +43 -0
  187. package/dist/services/activity.js +22 -10
  188. package/dist/services/assets.d.ts +2 -3
  189. package/dist/services/assets.js +10 -5
  190. package/dist/services/authentication.js +18 -18
  191. package/dist/services/collections.js +18 -17
  192. package/dist/services/fields.d.ts +0 -1
  193. package/dist/services/fields.js +54 -25
  194. package/dist/services/files.js +10 -3
  195. package/dist/services/graphql/index.d.ts +3 -3
  196. package/dist/services/graphql/index.js +126 -22
  197. package/dist/services/graphql/subscription.js +2 -4
  198. package/dist/services/import-export.d.ts +3 -1
  199. package/dist/services/import-export.js +67 -9
  200. package/dist/services/index.d.ts +3 -2
  201. package/dist/services/index.js +3 -2
  202. package/dist/services/items.js +115 -44
  203. package/dist/services/meta.js +60 -23
  204. package/dist/services/notifications.js +14 -6
  205. package/dist/services/payload.d.ts +9 -10
  206. package/dist/services/payload.js +18 -3
  207. package/dist/services/{permissions/index.d.ts → permissions.d.ts} +5 -7
  208. package/dist/services/{permissions/index.js → permissions.js} +30 -54
  209. package/dist/services/policies.d.ts +12 -0
  210. package/dist/services/policies.js +87 -0
  211. package/dist/services/relations.d.ts +0 -6
  212. package/dist/services/relations.js +27 -30
  213. package/dist/services/roles.d.ts +4 -12
  214. package/dist/services/roles.js +57 -424
  215. package/dist/services/shares.d.ts +0 -2
  216. package/dist/services/shares.js +12 -8
  217. package/dist/services/specifications.d.ts +2 -2
  218. package/dist/services/specifications.js +39 -27
  219. package/dist/services/users.d.ts +1 -5
  220. package/dist/services/users.js +78 -161
  221. package/dist/services/utils.js +11 -7
  222. package/dist/services/versions.d.ts +0 -2
  223. package/dist/services/versions.js +34 -10
  224. package/dist/telemetry/lib/get-report.js +2 -2
  225. package/dist/telemetry/utils/check-user-limits.d.ts +5 -0
  226. package/dist/telemetry/utils/check-user-limits.js +19 -0
  227. package/dist/types/ast.d.ts +43 -1
  228. package/dist/types/items.d.ts +11 -0
  229. package/dist/utils/apply-query.d.ts +11 -7
  230. package/dist/utils/apply-query.js +69 -11
  231. package/dist/utils/fetch-user-count/fetch-access-lookup.d.ts +19 -0
  232. package/dist/utils/fetch-user-count/fetch-access-lookup.js +23 -0
  233. package/dist/utils/fetch-user-count/fetch-access-roles.d.ts +16 -0
  234. package/dist/utils/fetch-user-count/fetch-access-roles.js +37 -0
  235. package/dist/utils/fetch-user-count/fetch-active-users.d.ts +6 -0
  236. package/dist/utils/fetch-user-count/fetch-active-users.js +3 -0
  237. package/dist/utils/fetch-user-count/fetch-user-count.d.ts +12 -0
  238. package/dist/utils/fetch-user-count/fetch-user-count.js +64 -0
  239. package/dist/utils/fetch-user-count/get-user-count-query.d.ts +20 -0
  240. package/dist/utils/fetch-user-count/get-user-count-query.js +17 -0
  241. package/dist/utils/get-accountability-for-role.js +16 -25
  242. package/dist/utils/get-accountability-for-token.js +17 -16
  243. package/dist/utils/get-cache-key.d.ts +1 -1
  244. package/dist/utils/get-cache-key.js +12 -1
  245. package/dist/utils/get-column.d.ts +2 -1
  246. package/dist/utils/get-column.js +1 -0
  247. package/dist/utils/get-service.js +5 -1
  248. package/dist/utils/reduce-schema.d.ts +4 -6
  249. package/dist/utils/reduce-schema.js +16 -32
  250. package/dist/utils/sanitize-schema.d.ts +1 -1
  251. package/dist/utils/validate-user-count-integrity.d.ts +13 -0
  252. package/dist/utils/validate-user-count-integrity.js +29 -0
  253. package/dist/websocket/authenticate.d.ts +0 -2
  254. package/dist/websocket/authenticate.js +0 -12
  255. package/dist/websocket/controllers/graphql.js +1 -4
  256. package/dist/websocket/controllers/hooks.js +4 -0
  257. package/dist/websocket/controllers/rest.js +0 -2
  258. package/dist/websocket/handlers/subscribe.js +0 -2
  259. package/dist/websocket/utils/items.d.ts +1 -1
  260. package/package.json +30 -29
  261. package/dist/database/run-ast.js +0 -458
  262. package/dist/middleware/check-ip.d.ts +0 -2
  263. package/dist/middleware/check-ip.js +0 -37
  264. package/dist/middleware/get-permissions.d.ts +0 -3
  265. package/dist/middleware/get-permissions.js +0 -10
  266. package/dist/services/authorization.d.ts +0 -17
  267. package/dist/services/authorization.js +0 -456
  268. package/dist/services/permissions/lib/with-app-minimal-permissions.js +0 -13
  269. package/dist/telemetry/utils/check-increased-user-limits.d.ts +0 -7
  270. package/dist/telemetry/utils/check-increased-user-limits.js +0 -25
  271. package/dist/telemetry/utils/get-role-counts-by-roles.d.ts +0 -6
  272. package/dist/telemetry/utils/get-role-counts-by-roles.js +0 -27
  273. package/dist/telemetry/utils/get-role-counts-by-users.d.ts +0 -11
  274. package/dist/telemetry/utils/get-role-counts-by-users.js +0 -34
  275. package/dist/telemetry/utils/get-user-count.d.ts +0 -8
  276. package/dist/telemetry/utils/get-user-count.js +0 -33
  277. package/dist/telemetry/utils/get-user-counts-by-roles.d.ts +0 -7
  278. package/dist/telemetry/utils/get-user-counts-by-roles.js +0 -35
  279. package/dist/utils/get-ast-from-query.d.ts +0 -13
  280. package/dist/utils/get-ast-from-query.js +0 -297
  281. package/dist/utils/get-permissions.d.ts +0 -2
  282. package/dist/utils/get-permissions.js +0 -150
  283. package/dist/utils/merge-permissions-for-share.d.ts +0 -4
  284. package/dist/utils/merge-permissions-for-share.js +0 -109
  285. package/dist/utils/merge-permissions.js +0 -95
@@ -15,6 +15,7 @@ import StreamArray from 'stream-json/streamers/StreamArray.js';
15
15
  import getDatabase from '../database/index.js';
16
16
  import emitter from '../emitter.js';
17
17
  import { useLogger } from '../logger/index.js';
18
+ import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
18
19
  import { getDateFormatted } from '../utils/get-date-formatted.js';
19
20
  import { getService } from '../utils/get-service.js';
20
21
  import { transaction } from '../utils/transaction.js';
@@ -23,6 +24,7 @@ import { userName } from '../utils/user-name.js';
23
24
  import { FilesService } from './files.js';
24
25
  import { NotificationsService } from './notifications.js';
25
26
  import { UsersService } from './users.js';
27
+ import { parseFields } from '../database/get-ast-from-query/lib/parse-fields.js';
26
28
  const env = useEnv();
27
29
  const logger = useLogger();
28
30
  export class ImportService {
@@ -37,10 +39,23 @@ export class ImportService {
37
39
  async import(collection, mimetype, stream) {
38
40
  if (this.accountability?.admin !== true && isSystemCollection(collection))
39
41
  throw new ForbiddenError();
40
- const createPermissions = this.accountability?.permissions?.find((permission) => permission.collection === collection && permission.action === 'create');
41
- const updatePermissions = this.accountability?.permissions?.find((permission) => permission.collection === collection && permission.action === 'update');
42
- if (this.accountability?.admin !== true && (!createPermissions || !updatePermissions)) {
43
- throw new ForbiddenError();
42
+ if (this.accountability) {
43
+ await validateAccess({
44
+ accountability: this.accountability,
45
+ action: 'create',
46
+ collection,
47
+ }, {
48
+ schema: this.schema,
49
+ knex: this.knex,
50
+ });
51
+ await validateAccess({
52
+ accountability: this.accountability,
53
+ action: 'update',
54
+ collection,
55
+ }, {
56
+ schema: this.schema,
57
+ knex: this.knex,
58
+ });
44
59
  }
45
60
  switch (mimetype) {
46
61
  case 'application/json':
@@ -248,9 +263,26 @@ export class ExportService {
248
263
  });
249
264
  readCount += result.length;
250
265
  if (result.length) {
266
+ let csvHeadings = null;
267
+ if (format === 'csv') {
268
+ if (!query.fields)
269
+ query.fields = ['*'];
270
+ // to ensure the all headings are included in the CSV file, all possible fields need to be determined.
271
+ const parsedFields = await parseFields({
272
+ parentCollection: collection,
273
+ fields: query.fields,
274
+ query: query,
275
+ accountability: this.accountability,
276
+ }, {
277
+ schema: this.schema,
278
+ knex: database,
279
+ });
280
+ csvHeadings = getHeadingsForCsvExport(parsedFields);
281
+ }
251
282
  await appendFile(tmpFile.path, this.transform(result, format, {
252
283
  includeHeader: batch === 0,
253
284
  includeFooter: batch + 1 === batchesRequired,
285
+ fields: csvHeadings,
254
286
  }));
255
287
  }
256
288
  }
@@ -345,11 +377,12 @@ Your export of ${collection} is ready. <a href="${href}">Click here to view.</a>
345
377
  if (format === 'csv') {
346
378
  if (input.length === 0)
347
379
  return '';
348
- const parser = new CSVParser({
349
- transforms: [CSVTransforms.flatten({ separator: '.' })],
350
- header: options?.includeHeader !== false,
351
- });
352
- let string = parser.parse(input);
380
+ const transforms = [CSVTransforms.flatten({ separator: '.' })];
381
+ const header = options?.includeHeader !== false;
382
+ const transformOptions = options?.fields
383
+ ? { transforms, header, fields: options?.fields }
384
+ : { transforms, header };
385
+ let string = new CSVParser(transformOptions).parse(input);
353
386
  if (options?.includeHeader === false) {
354
387
  string = '\n' + string;
355
388
  }
@@ -361,3 +394,28 @@ Your export of ${collection} is ready. <a href="${href}">Click here to view.</a>
361
394
  throw new ServiceUnavailableError({ service: 'export', reason: `Illegal export type used: "${format}"` });
362
395
  }
363
396
  }
397
+ /*
398
+ * Recursive function to traverse the field nodes, to determine the headings for the CSV export file.
399
+ *
400
+ * Relational nodes which target a single item get expanded, which means that their nested fields get their own column in the csv file.
401
+ * For relational nodes which target a multiple items, the nested field names are not going to be expanded.
402
+ * Instead they will be stored as a single value/cell of the CSV file.
403
+ */
404
+ export function getHeadingsForCsvExport(nodes, prefix = '') {
405
+ let fieldNames = [];
406
+ if (!nodes)
407
+ return fieldNames;
408
+ nodes.forEach((node) => {
409
+ switch (node.type) {
410
+ case 'field':
411
+ case 'functionField':
412
+ case 'o2m':
413
+ case 'a2o':
414
+ fieldNames.push(prefix ? `${prefix}.${node.fieldKey}` : node.fieldKey);
415
+ break;
416
+ case 'm2o':
417
+ fieldNames = fieldNames.concat(getHeadingsForCsvExport(node.children, prefix ? `${prefix}.${node.fieldKey}` : node.fieldKey));
418
+ }
419
+ });
420
+ return fieldNames;
421
+ }
@@ -1,7 +1,7 @@
1
+ export * from './access.js';
1
2
  export * from './activity.js';
2
3
  export * from './assets.js';
3
4
  export * from './authentication.js';
4
- export * from './authorization.js';
5
5
  export * from './collections.js';
6
6
  export * from './dashboards.js';
7
7
  export * from './extensions.js';
@@ -18,7 +18,8 @@ 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/index.js';
21
+ export * from './permissions.js';
22
+ export * from './policies.js';
22
23
  export * from './presets.js';
23
24
  export * from './relations.js';
24
25
  export * from './revisions.js';
@@ -1,7 +1,7 @@
1
+ export * from './access.js';
1
2
  export * from './activity.js';
2
3
  export * from './assets.js';
3
4
  export * from './authentication.js';
4
- export * from './authorization.js';
5
5
  export * from './collections.js';
6
6
  export * from './dashboards.js';
7
7
  export * from './extensions.js';
@@ -18,7 +18,8 @@ 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/index.js';
21
+ export * from './permissions.js';
22
+ export * from './policies.js';
22
23
  export * from './presets.js';
23
24
  export * from './relations.js';
24
25
  export * from './revisions.js';
@@ -5,15 +5,18 @@ import { isSystemCollection } from '@directus/system-data';
5
5
  import { assign, clone, cloneDeep, omit, pick, without } from 'lodash-es';
6
6
  import { getCache } from '../cache.js';
7
7
  import { translateDatabaseError } from '../database/errors/translate.js';
8
+ import { getAstFromQuery } from '../database/get-ast-from-query/get-ast-from-query.js';
8
9
  import { getHelpers } from '../database/helpers/index.js';
9
10
  import getDatabase from '../database/index.js';
10
- import runAST from '../database/run-ast.js';
11
+ import { runAst } from '../database/run-ast/run-ast.js';
11
12
  import emitter from '../emitter.js';
12
- import getASTFromQuery from '../utils/get-ast-from-query.js';
13
+ import { processAst } from '../permissions/modules/process-ast/process-ast.js';
14
+ import { processPayload } from '../permissions/modules/process-payload/process-payload.js';
15
+ import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
13
16
  import { shouldClearCache } from '../utils/should-clear-cache.js';
14
17
  import { transaction } from '../utils/transaction.js';
15
18
  import { validateKeys } from '../utils/validate-keys.js';
16
- import { AuthorizationService } from './authorization.js';
19
+ import { UserIntegrityCheckFlag, validateUserCountIntegrity } from '../utils/validate-user-count-integrity.js';
17
20
  import { PayloadService } from './payload.js';
18
21
  const env = useEnv();
19
22
  export class ItemsService {
@@ -98,17 +101,13 @@ export class ItemsService {
98
101
  // that any errors thrown in any nested relational changes will bubble up and cancel the whole
99
102
  // update tree
100
103
  const primaryKey = await transaction(this.knex, async (trx) => {
101
- // We're creating new services instances so they can use the transaction as their Knex interface
102
- const payloadService = new PayloadService(this.collection, {
103
- accountability: this.accountability,
104
- knex: trx,
105
- schema: this.schema,
106
- });
107
- const authorizationService = new AuthorizationService({
104
+ const serviceOptions = {
108
105
  accountability: this.accountability,
109
106
  knex: trx,
110
107
  schema: this.schema,
111
- });
108
+ };
109
+ // We're creating new services instances so they can use the transaction as their Knex interface
110
+ const payloadService = new PayloadService(this.collection, serviceOptions);
112
111
  // Run all hooks that are attached to this event so the end user has the chance to augment the
113
112
  // item that is about to be saved
114
113
  const payloadAfterHooks = opts.emitEvents !== false
@@ -123,13 +122,21 @@ export class ItemsService {
123
122
  })
124
123
  : payload;
125
124
  const payloadWithPresets = this.accountability
126
- ? authorizationService.validatePayload('create', this.collection, payloadAfterHooks)
125
+ ? await processPayload({
126
+ accountability: this.accountability,
127
+ action: 'create',
128
+ collection: this.collection,
129
+ payload: payloadAfterHooks,
130
+ }, {
131
+ knex: trx,
132
+ schema: this.schema,
133
+ })
127
134
  : payloadAfterHooks;
128
135
  if (opts.preMutationError) {
129
136
  throw opts.preMutationError;
130
137
  }
131
- const { payload: payloadWithM2O, revisions: revisionsM2O, nestedActionEvents: nestedActionEventsM2O, } = await payloadService.processM2O(payloadWithPresets, opts);
132
- const { payload: payloadWithA2O, revisions: revisionsA2O, nestedActionEvents: nestedActionEventsA2O, } = await payloadService.processA2O(payloadWithM2O, opts);
138
+ const { payload: payloadWithM2O, revisions: revisionsM2O, nestedActionEvents: nestedActionEventsM2O, userIntegrityCheckFlags: userIntegrityCheckFlagsM2O, } = await payloadService.processM2O(payloadWithPresets, opts);
139
+ const { payload: payloadWithA2O, revisions: revisionsA2O, nestedActionEvents: nestedActionEventsA2O, userIntegrityCheckFlags: userIntegrityCheckFlagsA2O, } = await payloadService.processA2O(payloadWithM2O, opts);
133
140
  const payloadWithoutAliases = pick(payloadWithA2O, without(fields, ...aliases));
134
141
  const payloadWithTypeCasting = await payloadService.processValues('create', payloadWithoutAliases);
135
142
  // The primary key can already exist in the payload.
@@ -187,10 +194,22 @@ export class ItemsService {
187
194
  }
188
195
  // At this point, the primary key is guaranteed to be set.
189
196
  primaryKey = primaryKey;
190
- const { revisions: revisionsO2M, nestedActionEvents: nestedActionEventsO2M } = await payloadService.processO2M(payloadWithPresets, primaryKey, opts);
197
+ const { revisions: revisionsO2M, nestedActionEvents: nestedActionEventsO2M, userIntegrityCheckFlags: userIntegrityCheckFlagsO2M, } = await payloadService.processO2M(payloadWithPresets, primaryKey, opts);
191
198
  nestedActionEvents.push(...nestedActionEventsM2O);
192
199
  nestedActionEvents.push(...nestedActionEventsA2O);
193
200
  nestedActionEvents.push(...nestedActionEventsO2M);
201
+ const userIntegrityCheckFlags = (opts.userIntegrityCheckFlags ?? UserIntegrityCheckFlag.None) |
202
+ userIntegrityCheckFlagsM2O |
203
+ userIntegrityCheckFlagsA2O |
204
+ userIntegrityCheckFlagsO2M;
205
+ if (userIntegrityCheckFlags) {
206
+ if (opts.onRequireUserIntegrityCheck) {
207
+ opts.onRequireUserIntegrityCheck(userIntegrityCheckFlags);
208
+ }
209
+ else {
210
+ await validateUserCountIntegrity({ flags: userIntegrityCheckFlags, knex: trx });
211
+ }
212
+ }
194
213
  // If this is an authenticated action, and accountability tracking is enabled, save activity row
195
214
  if (this.accountability && this.schema.collections[this.collection].accountability !== null) {
196
215
  const activityService = new ActivityService({
@@ -281,6 +300,7 @@ export class ItemsService {
281
300
  opts.mutationTracker = this.createMutationTracker();
282
301
  const { primaryKeys, nestedActionEvents } = await transaction(this.knex, async (knex) => {
283
302
  const service = this.fork({ knex });
303
+ let userIntegrityCheckFlags = opts.userIntegrityCheckFlags ?? UserIntegrityCheckFlag.None;
284
304
  const primaryKeys = [];
285
305
  const nestedActionEvents = [];
286
306
  const pkField = this.schema.collections[this.collection].primary;
@@ -294,12 +314,21 @@ export class ItemsService {
294
314
  const primaryKey = await service.createOne(payload, {
295
315
  ...(opts || {}),
296
316
  autoPurgeCache: false,
317
+ onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
297
318
  bypassEmitAction: (params) => nestedActionEvents.push(params),
298
319
  mutationTracker: opts.mutationTracker,
299
320
  bypassAutoIncrementSequenceReset,
300
321
  });
301
322
  primaryKeys.push(primaryKey);
302
323
  }
324
+ if (userIntegrityCheckFlags) {
325
+ if (opts.onRequireUserIntegrityCheck) {
326
+ opts.onRequireUserIntegrityCheck(userIntegrityCheckFlags);
327
+ }
328
+ else {
329
+ await validateUserCountIntegrity({ flags: userIntegrityCheckFlags, knex });
330
+ }
331
+ }
303
332
  return { primaryKeys, nestedActionEvents };
304
333
  });
305
334
  if (opts.emitEvents !== false) {
@@ -332,27 +361,21 @@ export class ItemsService {
332
361
  accountability: this.accountability,
333
362
  })
334
363
  : query;
335
- let ast = await getASTFromQuery(this.collection, updatedQuery, this.schema, {
364
+ let ast = await getAstFromQuery({
365
+ collection: this.collection,
366
+ query: updatedQuery,
336
367
  accountability: this.accountability,
337
- // By setting the permissions action, you can read items using the permissions for another
338
- // operation's permissions. This is used to dynamically check if you have update/delete
339
- // access to (a) certain item(s)
340
- action: opts?.permissionsAction || 'read',
368
+ }, {
369
+ schema: this.schema,
341
370
  knex: this.knex,
342
371
  });
343
- if (this.accountability && this.accountability.admin !== true) {
344
- const authorizationService = new AuthorizationService({
345
- accountability: this.accountability,
346
- knex: this.knex,
347
- schema: this.schema,
348
- });
349
- ast = await authorizationService.processAST(ast, opts?.permissionsAction);
350
- }
351
- const records = await runAST(ast, this.schema, {
372
+ ast = await processAst({ ast, action: 'read', accountability: this.accountability }, { knex: this.knex, schema: this.schema });
373
+ const records = await runAst(ast, this.schema, {
352
374
  knex: this.knex,
353
375
  // GraphQL requires relational keys to be returned regardless
354
376
  stripNonRequested: opts?.stripNonRequested !== undefined ? opts.stripNonRequested : true,
355
377
  });
378
+ // TODO when would this happen?
356
379
  if (records === null) {
357
380
  throw new ForbiddenError();
358
381
  }
@@ -450,13 +473,26 @@ export class ItemsService {
450
473
  try {
451
474
  await transaction(this.knex, async (knex) => {
452
475
  const service = this.fork({ knex });
476
+ let userIntegrityCheckFlags = opts.userIntegrityCheckFlags ?? UserIntegrityCheckFlag.None;
453
477
  for (const item of data) {
454
478
  const primaryKey = item[primaryKeyField];
455
479
  if (!primaryKey)
456
480
  throw new InvalidPayloadError({ reason: `Item in update misses primary key` });
457
- const combinedOpts = Object.assign({ autoPurgeCache: false }, opts);
481
+ const combinedOpts = {
482
+ autoPurgeCache: false,
483
+ ...opts,
484
+ onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
485
+ };
458
486
  keys.push(await service.updateOne(primaryKey, omit(item, primaryKeyField), combinedOpts));
459
487
  }
488
+ if (userIntegrityCheckFlags) {
489
+ if (opts.onRequireUserIntegrityCheck) {
490
+ opts.onRequireUserIntegrityCheck(userIntegrityCheckFlags);
491
+ }
492
+ else {
493
+ await validateUserCountIntegrity({ flags: userIntegrityCheckFlags, knex });
494
+ }
495
+ }
460
496
  });
461
497
  }
462
498
  finally {
@@ -485,11 +521,6 @@ export class ItemsService {
485
521
  .map((field) => field.field);
486
522
  const payload = cloneDeep(data);
487
523
  const nestedActionEvents = [];
488
- const authorizationService = new AuthorizationService({
489
- accountability: this.accountability,
490
- knex: this.knex,
491
- schema: this.schema,
492
- });
493
524
  // Run all hooks that are attached to this event so the end user has the chance to augment the
494
525
  // item that is about to be saved
495
526
  const payloadAfterHooks = opts.emitEvents !== false
@@ -507,10 +538,26 @@ export class ItemsService {
507
538
  // Sort keys to ensure that the order is maintained
508
539
  keys.sort();
509
540
  if (this.accountability) {
510
- await authorizationService.checkAccess('update', this.collection, keys);
541
+ await validateAccess({
542
+ accountability: this.accountability,
543
+ action: 'update',
544
+ collection: this.collection,
545
+ primaryKeys: keys,
546
+ }, {
547
+ schema: this.schema,
548
+ knex: this.knex,
549
+ });
511
550
  }
512
551
  const payloadWithPresets = this.accountability
513
- ? authorizationService.validatePayload('update', this.collection, payloadAfterHooks)
552
+ ? await processPayload({
553
+ accountability: this.accountability,
554
+ action: 'update',
555
+ collection: this.collection,
556
+ payload: payloadAfterHooks,
557
+ }, {
558
+ knex: this.knex,
559
+ schema: this.schema,
560
+ })
514
561
  : payloadAfterHooks;
515
562
  if (opts.preMutationError) {
516
563
  throw opts.preMutationError;
@@ -521,8 +568,8 @@ export class ItemsService {
521
568
  knex: trx,
522
569
  schema: this.schema,
523
570
  });
524
- const { payload: payloadWithM2O, revisions: revisionsM2O, nestedActionEvents: nestedActionEventsM2O, } = await payloadService.processM2O(payloadWithPresets, opts);
525
- const { payload: payloadWithA2O, revisions: revisionsA2O, nestedActionEvents: nestedActionEventsA2O, } = await payloadService.processA2O(payloadWithM2O, opts);
571
+ const { payload: payloadWithM2O, revisions: revisionsM2O, nestedActionEvents: nestedActionEventsM2O, userIntegrityCheckFlags: userIntegrityCheckFlagsM2O, } = await payloadService.processM2O(payloadWithPresets, opts);
572
+ const { payload: payloadWithA2O, revisions: revisionsA2O, nestedActionEvents: nestedActionEventsA2O, userIntegrityCheckFlags: userIntegrityCheckFlagsA2O, } = await payloadService.processA2O(payloadWithM2O, opts);
526
573
  const payloadWithoutAliasAndPK = pick(payloadWithA2O, without(fields, primaryKeyField, ...aliases));
527
574
  const payloadWithTypeCasting = await payloadService.processValues('update', payloadWithoutAliasAndPK);
528
575
  if (Object.keys(payloadWithTypeCasting).length > 0) {
@@ -534,12 +581,25 @@ export class ItemsService {
534
581
  }
535
582
  }
536
583
  const childrenRevisions = [...revisionsM2O, ...revisionsA2O];
584
+ let userIntegrityCheckFlags = opts.userIntegrityCheckFlags ??
585
+ UserIntegrityCheckFlag.None | userIntegrityCheckFlagsM2O | userIntegrityCheckFlagsA2O;
537
586
  nestedActionEvents.push(...nestedActionEventsM2O);
538
587
  nestedActionEvents.push(...nestedActionEventsA2O);
539
588
  for (const key of keys) {
540
- const { revisions, nestedActionEvents: nestedActionEventsO2M } = await payloadService.processO2M(payloadWithA2O, key, opts);
589
+ const { revisions, nestedActionEvents: nestedActionEventsO2M, userIntegrityCheckFlags: userIntegrityCheckFlagsO2M, } = await payloadService.processO2M(payloadWithA2O, key, opts);
541
590
  childrenRevisions.push(...revisions);
542
591
  nestedActionEvents.push(...nestedActionEventsO2M);
592
+ userIntegrityCheckFlags |= userIntegrityCheckFlagsO2M;
593
+ }
594
+ if (userIntegrityCheckFlags) {
595
+ if (opts?.onRequireUserIntegrityCheck) {
596
+ opts.onRequireUserIntegrityCheck(userIntegrityCheckFlags);
597
+ }
598
+ else {
599
+ // Having no onRequireUserIntegrityCheck callback indicates that
600
+ // this is the top level invocation of the nested updates, so perform the user integrity check
601
+ await validateUserCountIntegrity({ flags: userIntegrityCheckFlags, knex: trx });
602
+ }
543
603
  }
544
604
  // If this is an authenticated action, and accountability tracking is enabled, save activity row
545
605
  if (this.accountability && this.schema.collections[this.collection].accountability !== null) {
@@ -708,13 +768,16 @@ export class ItemsService {
708
768
  const { ActivityService } = await import('./activity.js');
709
769
  const primaryKeyField = this.schema.collections[this.collection].primary;
710
770
  validateKeys(this.schema, this.collection, primaryKeyField, keys);
711
- if (this.accountability && this.accountability.admin !== true) {
712
- const authorizationService = new AuthorizationService({
771
+ if (this.accountability) {
772
+ await validateAccess({
713
773
  accountability: this.accountability,
714
- schema: this.schema,
774
+ action: 'delete',
775
+ collection: this.collection,
776
+ primaryKeys: keys,
777
+ }, {
715
778
  knex: this.knex,
779
+ schema: this.schema,
716
780
  });
717
- await authorizationService.checkAccess('delete', this.collection, keys);
718
781
  }
719
782
  if (opts.preMutationError) {
720
783
  throw opts.preMutationError;
@@ -730,6 +793,14 @@ export class ItemsService {
730
793
  }
731
794
  await transaction(this.knex, async (trx) => {
732
795
  await trx(this.collection).whereIn(primaryKeyField, keys).delete();
796
+ if (opts.userIntegrityCheckFlags) {
797
+ if (opts.onRequireUserIntegrityCheck) {
798
+ opts.onRequireUserIntegrityCheck(opts.userIntegrityCheckFlags);
799
+ }
800
+ else {
801
+ await validateUserCountIntegrity({ flags: opts.userIntegrityCheckFlags, knex: trx });
802
+ }
803
+ }
733
804
  if (this.accountability && this.schema.collections[this.collection].accountability !== null) {
734
805
  const activityService = new ActivityService({
735
806
  knex: trx,
@@ -1,5 +1,8 @@
1
1
  import getDatabase from '../database/index.js';
2
- import { ForbiddenError } from '@directus/errors';
2
+ import { fetchPermissions } from '../permissions/lib/fetch-permissions.js';
3
+ import { fetchPolicies } from '../permissions/lib/fetch-policies.js';
4
+ import { dedupeAccess } from '../permissions/modules/process-ast/utils/dedupe-access.js';
5
+ import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
3
6
  import { applyFilter, applySearch } from '../utils/apply-query.js';
4
7
  export class MetaService {
5
8
  knex;
@@ -28,39 +31,73 @@ export class MetaService {
28
31
  }, {});
29
32
  }
30
33
  async totalCount(collection) {
31
- const dbQuery = this.knex(collection).count('*', { as: 'count' }).first();
32
- if (this.accountability?.admin !== true) {
33
- const permissionsRecord = this.accountability?.permissions?.find((permission) => {
34
- return permission.action === 'read' && permission.collection === collection;
35
- });
36
- if (!permissionsRecord)
37
- throw new ForbiddenError();
38
- const permissions = permissionsRecord.permissions ?? {};
39
- applyFilter(this.knex, this.schema, dbQuery, permissions, collection, {});
34
+ const dbQuery = this.knex(collection);
35
+ let hasJoins = false;
36
+ if (this.accountability && this.accountability.admin === false) {
37
+ const context = { knex: this.knex, schema: this.schema };
38
+ await validateAccess({
39
+ accountability: this.accountability,
40
+ action: 'read',
41
+ collection,
42
+ }, context);
43
+ const policies = await fetchPolicies(this.accountability, context);
44
+ const permissions = await fetchPermissions({
45
+ action: 'read',
46
+ policies,
47
+ accountability: this.accountability,
48
+ ...(collection ? { collections: [collection] } : {}),
49
+ }, context);
50
+ const rules = dedupeAccess(permissions);
51
+ const cases = rules.map(({ rule }) => rule);
52
+ const filter = {
53
+ _or: cases,
54
+ };
55
+ const result = applyFilter(this.knex, this.schema, dbQuery, filter, collection, {}, cases);
56
+ hasJoins = result.hasJoins;
57
+ }
58
+ if (hasJoins) {
59
+ const primaryKeyName = this.schema.collections[collection].primary;
60
+ dbQuery.countDistinct({ count: [`${collection}.${primaryKeyName}`] });
61
+ }
62
+ else {
63
+ dbQuery.count('*', { as: 'count' });
40
64
  }
41
- const result = await dbQuery;
65
+ const result = await dbQuery.first();
42
66
  return Number(result?.count ?? 0);
43
67
  }
44
68
  async filterCount(collection, query) {
45
69
  const dbQuery = this.knex(collection);
46
70
  let filter = query.filter || {};
47
71
  let hasJoins = false;
48
- if (this.accountability?.admin !== true) {
49
- const permissionsRecord = this.accountability?.permissions?.find((permission) => {
50
- return permission.action === 'read' && permission.collection === collection;
51
- });
52
- if (!permissionsRecord)
53
- throw new ForbiddenError();
54
- const permissions = permissionsRecord.permissions ?? {};
72
+ let cases = [];
73
+ if (this.accountability && this.accountability.admin === false) {
74
+ const context = { knex: this.knex, schema: this.schema };
75
+ await validateAccess({
76
+ accountability: this.accountability,
77
+ action: 'read',
78
+ collection,
79
+ }, context);
80
+ const policies = await fetchPolicies(this.accountability, context);
81
+ const permissions = await fetchPermissions({
82
+ action: 'read',
83
+ policies,
84
+ accountability: this.accountability,
85
+ ...(collection ? { collections: [collection] } : {}),
86
+ }, context);
87
+ const rules = dedupeAccess(permissions);
88
+ cases = rules.map(({ rule }) => rule);
89
+ const permissionsFilter = {
90
+ _or: cases,
91
+ };
55
92
  if (Object.keys(filter).length > 0) {
56
- filter = { _and: [permissions, filter] };
93
+ filter = { _and: [permissionsFilter, filter] };
57
94
  }
58
95
  else {
59
- filter = permissions;
96
+ filter = permissionsFilter;
60
97
  }
61
98
  }
62
99
  if (Object.keys(filter).length > 0) {
63
- ({ hasJoins } = applyFilter(this.knex, this.schema, dbQuery, filter, collection, {}));
100
+ ({ hasJoins } = applyFilter(this.knex, this.schema, dbQuery, filter, collection, {}, cases));
64
101
  }
65
102
  if (query.search) {
66
103
  applySearch(this.knex, this.schema, dbQuery, query.search, collection);
@@ -72,7 +109,7 @@ export class MetaService {
72
109
  else {
73
110
  dbQuery.count('*', { as: 'count' });
74
111
  }
75
- const records = await dbQuery;
76
- return Number(records[0]['count']);
112
+ const result = await dbQuery.first();
113
+ return Number(result?.count ?? 0);
77
114
  }
78
115
  }
@@ -1,5 +1,7 @@
1
1
  import { useEnv } from '@directus/env';
2
2
  import { useLogger } from '../logger/index.js';
3
+ import { fetchRolesTree } from '../permissions/lib/fetch-roles-tree.js';
4
+ import { fetchGlobalAccess } from '../permissions/modules/fetch-global-access/fetch-global-access.js';
3
5
  import { md } from '../utils/md.js';
4
6
  import { Url } from '../utils/url.js';
5
7
  import { ItemsService } from './items.js';
@@ -23,18 +25,24 @@ export class NotificationsService extends ItemsService {
23
25
  async sendEmail(data) {
24
26
  if (data.recipient) {
25
27
  const user = await this.usersService.readOne(data.recipient, {
26
- fields: ['id', 'email', 'email_notifications', 'role.app_access'],
28
+ fields: ['id', 'email', 'email_notifications', 'role'],
27
29
  });
28
- const manageUserAccountUrl = new Url(env['PUBLIC_URL'])
29
- .addPath('admin', 'users', user['id'])
30
- .toString();
31
- const html = data.message ? md(data.message) : '';
32
30
  if (user['email'] && user['email_notifications'] === true) {
31
+ const manageUserAccountUrl = new Url(env['PUBLIC_URL'])
32
+ .addPath('admin', 'users', user['id'])
33
+ .toString();
34
+ const html = data.message ? md(data.message) : '';
35
+ const roles = await fetchRolesTree(user['role'], this.knex);
36
+ const { app: app_access } = await fetchGlobalAccess({
37
+ user: user['id'],
38
+ roles,
39
+ ip: null,
40
+ }, this.knex);
33
41
  this.mailService
34
42
  .send({
35
43
  template: {
36
44
  name: 'base',
37
- data: user['role']?.app_access ? { url: manageUserAccountUrl, html } : { html },
45
+ data: app_access ? { url: manageUserAccountUrl, html } : { html },
38
46
  },
39
47
  to: user['email'],
40
48
  subject: data.subject,
@@ -2,6 +2,7 @@ import type { Accountability, Item, PrimaryKey, SchemaOverview } from '@directus
2
2
  import type { Knex } from 'knex';
3
3
  import type { Helpers } from '../database/helpers/index.js';
4
4
  import type { AbstractServiceOptions, ActionEventParams, MutationOptions } from '../types/index.js';
5
+ import { UserIntegrityCheckFlag } from '../utils/validate-user-count-integrity.js';
5
6
  type Action = 'create' | 'read' | 'update';
6
7
  type Transformers = {
7
8
  [type: string]: (context: {
@@ -13,6 +14,11 @@ type Transformers = {
13
14
  helpers: Helpers;
14
15
  }) => Promise<any>;
15
16
  };
17
+ type PayloadServiceProcessRelationResult = {
18
+ revisions: PrimaryKey[];
19
+ nestedActionEvents: ActionEventParams[];
20
+ userIntegrityCheckFlags: UserIntegrityCheckFlag;
21
+ };
16
22
  /**
17
23
  * Process a given payload for a collection to ensure the special fields (hash, uuid, date etc) are
18
24
  * handled correctly.
@@ -46,26 +52,19 @@ export declare class PayloadService {
46
52
  /**
47
53
  * Recursively save/update all nested related Any-to-One items
48
54
  */
49
- processA2O(data: Partial<Item>, opts?: MutationOptions): Promise<{
55
+ processA2O(data: Partial<Item>, opts?: MutationOptions): Promise<PayloadServiceProcessRelationResult & {
50
56
  payload: Partial<Item>;
51
- revisions: PrimaryKey[];
52
- nestedActionEvents: ActionEventParams[];
53
57
  }>;
54
58
  /**
55
59
  * Save/update all nested related m2o items inside the payload
56
60
  */
57
- processM2O(data: Partial<Item>, opts?: MutationOptions): Promise<{
61
+ processM2O(data: Partial<Item>, opts?: MutationOptions): Promise<PayloadServiceProcessRelationResult & {
58
62
  payload: Partial<Item>;
59
- revisions: PrimaryKey[];
60
- nestedActionEvents: ActionEventParams[];
61
63
  }>;
62
64
  /**
63
65
  * Recursively save/update all nested related o2m items
64
66
  */
65
- processO2M(data: Partial<Item>, parent: PrimaryKey, opts?: MutationOptions): Promise<{
66
- revisions: PrimaryKey[];
67
- nestedActionEvents: ActionEventParams[];
68
- }>;
67
+ processO2M(data: Partial<Item>, parent: PrimaryKey, opts?: MutationOptions): Promise<PayloadServiceProcessRelationResult>;
69
68
  /**
70
69
  * Transforms the input partial payload to match the output structure, to have consistency
71
70
  * between delta and data