@directus/api 21.0.0 → 22.1.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 (299) 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/commands/init/questions.d.ts +7 -6
  10. package/dist/cli/commands/init/questions.js +2 -2
  11. package/dist/cli/utils/create-env/index.d.ts +2 -2
  12. package/dist/cli/utils/create-env/index.js +3 -1
  13. package/dist/cli/utils/defaults.d.ts +4 -11
  14. package/dist/cli/utils/defaults.js +7 -1
  15. package/dist/cli/utils/drivers.js +1 -1
  16. package/dist/constants.d.ts +1 -1
  17. package/dist/controllers/access.d.ts +2 -0
  18. package/dist/controllers/access.js +148 -0
  19. package/dist/controllers/auth.js +5 -16
  20. package/dist/controllers/permissions.js +14 -2
  21. package/dist/controllers/policies.d.ts +2 -0
  22. package/dist/controllers/policies.js +169 -0
  23. package/dist/controllers/roles.js +22 -1
  24. package/dist/controllers/tus.js +14 -26
  25. package/dist/controllers/users.js +0 -55
  26. package/dist/database/get-ast-from-query/get-ast-from-query.d.ts +16 -0
  27. package/dist/database/get-ast-from-query/get-ast-from-query.js +82 -0
  28. package/dist/database/get-ast-from-query/lib/convert-wildcards.d.ts +13 -0
  29. package/dist/database/get-ast-from-query/lib/convert-wildcards.js +69 -0
  30. package/dist/database/get-ast-from-query/lib/parse-fields.d.ts +15 -0
  31. package/dist/database/get-ast-from-query/lib/parse-fields.js +200 -0
  32. package/dist/database/get-ast-from-query/utils/get-deep-query.d.ts +14 -0
  33. package/dist/database/get-ast-from-query/utils/get-deep-query.js +17 -0
  34. package/dist/database/get-ast-from-query/utils/get-related-collection.d.ts +2 -0
  35. package/dist/database/get-ast-from-query/utils/get-related-collection.js +13 -0
  36. package/dist/database/get-ast-from-query/utils/get-relation.d.ts +2 -0
  37. package/dist/database/get-ast-from-query/utils/get-relation.js +7 -0
  38. package/dist/database/helpers/fn/types.d.ts +2 -1
  39. package/dist/database/helpers/fn/types.js +1 -1
  40. package/dist/database/helpers/geometry/dialects/mssql.d.ts +1 -1
  41. package/dist/database/helpers/geometry/dialects/mssql.js +4 -2
  42. package/dist/database/helpers/geometry/dialects/mysql.js +1 -1
  43. package/dist/database/helpers/geometry/dialects/oracle.d.ts +1 -1
  44. package/dist/database/helpers/geometry/dialects/oracle.js +5 -3
  45. package/dist/database/helpers/geometry/types.d.ts +1 -1
  46. package/dist/database/helpers/geometry/types.js +4 -2
  47. package/dist/database/helpers/index.d.ts +3 -3
  48. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +2 -1
  49. package/dist/database/helpers/schema/dialects/cockroachdb.js +4 -0
  50. package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -1
  51. package/dist/database/helpers/schema/dialects/mssql.js +4 -0
  52. package/dist/database/helpers/schema/dialects/oracle.d.ts +2 -1
  53. package/dist/database/helpers/schema/dialects/oracle.js +4 -0
  54. package/dist/database/helpers/schema/dialects/postgres.d.ts +2 -1
  55. package/dist/database/helpers/schema/dialects/postgres.js +4 -0
  56. package/dist/database/helpers/schema/types.d.ts +5 -0
  57. package/dist/database/helpers/schema/types.js +3 -0
  58. package/dist/database/helpers/schema/utils/preprocess-bindings.d.ts +8 -0
  59. package/dist/database/helpers/schema/utils/preprocess-bindings.js +30 -0
  60. package/dist/database/index.js +6 -1
  61. package/dist/database/migrations/20210519A-add-system-fk-triggers.js +3 -2
  62. package/dist/database/migrations/20230721A-require-shares-fields.js +3 -5
  63. package/dist/database/migrations/20240716A-update-files-date-fields.js +3 -7
  64. package/dist/{utils/merge-permissions.d.ts → database/migrations/20240806A-permissions-policies.d.ts} +4 -1
  65. package/dist/database/migrations/20240806A-permissions-policies.js +352 -0
  66. package/dist/database/run-ast/lib/get-db-query.d.ts +4 -0
  67. package/dist/database/run-ast/lib/get-db-query.js +218 -0
  68. package/dist/database/run-ast/lib/parse-current-level.d.ts +7 -0
  69. package/dist/database/run-ast/lib/parse-current-level.js +41 -0
  70. package/dist/database/run-ast/run-ast.d.ts +7 -0
  71. package/dist/database/run-ast/run-ast.js +107 -0
  72. package/dist/database/{run-ast.d.ts → run-ast/types.d.ts} +3 -9
  73. package/dist/database/run-ast/types.js +1 -0
  74. package/dist/database/run-ast/utils/apply-case-when.d.ts +16 -0
  75. package/dist/database/run-ast/utils/apply-case-when.js +27 -0
  76. package/dist/database/run-ast/utils/apply-parent-filters.d.ts +3 -0
  77. package/dist/database/run-ast/utils/apply-parent-filters.js +55 -0
  78. package/dist/database/run-ast/utils/get-column-pre-processor.d.ts +10 -0
  79. package/dist/database/run-ast/utils/get-column-pre-processor.js +57 -0
  80. package/dist/database/run-ast/utils/get-field-alias.d.ts +2 -0
  81. package/dist/database/run-ast/utils/get-field-alias.js +4 -0
  82. package/dist/database/run-ast/utils/get-inner-query-column-pre-processor.d.ts +5 -0
  83. package/dist/database/run-ast/utils/get-inner-query-column-pre-processor.js +23 -0
  84. package/dist/database/run-ast/utils/merge-with-parent-items.d.ts +3 -0
  85. package/dist/database/run-ast/utils/merge-with-parent-items.js +87 -0
  86. package/dist/database/run-ast/utils/remove-temporary-fields.d.ts +3 -0
  87. package/dist/database/run-ast/utils/remove-temporary-fields.js +73 -0
  88. package/dist/database/run-ast/utils/with-preprocess-bindings.d.ts +2 -0
  89. package/dist/database/run-ast/utils/with-preprocess-bindings.js +14 -0
  90. package/dist/flows.js +3 -4
  91. package/dist/middleware/authenticate.js +2 -7
  92. package/dist/middleware/cache.js +1 -1
  93. package/dist/middleware/respond.js +1 -1
  94. package/dist/permissions/cache.d.ts +2 -0
  95. package/dist/permissions/cache.js +23 -0
  96. package/dist/permissions/lib/fetch-permissions.d.ts +11 -0
  97. package/dist/permissions/lib/fetch-permissions.js +56 -0
  98. package/dist/permissions/lib/fetch-policies.d.ts +14 -0
  99. package/dist/permissions/lib/fetch-policies.js +43 -0
  100. package/dist/permissions/lib/fetch-roles-tree.d.ts +3 -0
  101. package/dist/permissions/lib/fetch-roles-tree.js +28 -0
  102. package/dist/{services/permissions → permissions}/lib/with-app-minimal-permissions.d.ts +1 -1
  103. package/dist/permissions/lib/with-app-minimal-permissions.js +10 -0
  104. package/dist/permissions/modules/fetch-accountability-collection-access/fetch-accountability-collection-access.d.ts +7 -0
  105. package/dist/permissions/modules/fetch-accountability-collection-access/fetch-accountability-collection-access.js +56 -0
  106. package/dist/permissions/modules/fetch-accountability-policy-globals/fetch-accountability-policy-globals.d.ts +3 -0
  107. package/dist/permissions/modules/fetch-accountability-policy-globals/fetch-accountability-policy-globals.js +16 -0
  108. package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.d.ts +8 -0
  109. package/dist/permissions/modules/fetch-allowed-collections/fetch-allowed-collections.js +24 -0
  110. package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.d.ts +9 -0
  111. package/dist/permissions/modules/fetch-allowed-field-map/fetch-allowed-field-map.js +31 -0
  112. package/dist/permissions/modules/fetch-allowed-fields/fetch-allowed-fields.d.ts +16 -0
  113. package/dist/permissions/modules/fetch-allowed-fields/fetch-allowed-fields.js +27 -0
  114. package/dist/permissions/modules/fetch-global-access/fetch-global-access.d.ts +10 -0
  115. package/dist/permissions/modules/fetch-global-access/fetch-global-access.js +23 -0
  116. package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-roles.d.ts +5 -0
  117. package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-roles.js +7 -0
  118. package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-user.d.ts +5 -0
  119. package/dist/permissions/modules/fetch-global-access/lib/fetch-global-access-for-user.js +10 -0
  120. package/dist/permissions/modules/fetch-global-access/types.d.ts +4 -0
  121. package/dist/permissions/modules/fetch-global-access/types.js +1 -0
  122. package/dist/permissions/modules/fetch-global-access/utils/fetch-global-access-for-query.d.ts +4 -0
  123. package/dist/permissions/modules/fetch-global-access/utils/fetch-global-access-for-query.js +27 -0
  124. package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.d.ts +12 -0
  125. package/dist/permissions/modules/fetch-inconsistent-field-map/fetch-inconsistent-field-map.js +32 -0
  126. package/dist/permissions/modules/fetch-policies-ip-access/fetch-policies-ip-access.d.ts +4 -0
  127. package/dist/permissions/modules/fetch-policies-ip-access/fetch-policies-ip-access.js +29 -0
  128. package/dist/permissions/modules/process-ast/lib/extract-fields-from-children.d.ts +4 -0
  129. package/dist/permissions/modules/process-ast/lib/extract-fields-from-children.js +49 -0
  130. package/dist/permissions/modules/process-ast/lib/extract-fields-from-query.d.ts +3 -0
  131. package/dist/permissions/modules/process-ast/lib/extract-fields-from-query.js +56 -0
  132. package/dist/permissions/modules/process-ast/lib/field-map-from-ast.d.ts +4 -0
  133. package/dist/permissions/modules/process-ast/lib/field-map-from-ast.js +8 -0
  134. package/dist/permissions/modules/process-ast/lib/inject-cases.d.ts +9 -0
  135. package/dist/permissions/modules/process-ast/lib/inject-cases.js +93 -0
  136. package/dist/permissions/modules/process-ast/process-ast.d.ts +9 -0
  137. package/dist/permissions/modules/process-ast/process-ast.js +39 -0
  138. package/dist/permissions/modules/process-ast/types.d.ts +18 -0
  139. package/dist/permissions/modules/process-ast/types.js +1 -0
  140. package/dist/permissions/modules/process-ast/utils/collections-in-field-map.d.ts +2 -0
  141. package/dist/permissions/modules/process-ast/utils/collections-in-field-map.js +7 -0
  142. package/dist/permissions/modules/process-ast/utils/dedupe-access.d.ts +12 -0
  143. package/dist/permissions/modules/process-ast/utils/dedupe-access.js +30 -0
  144. package/dist/permissions/modules/process-ast/utils/extract-paths-from-query.d.ts +15 -0
  145. package/dist/permissions/modules/process-ast/utils/extract-paths-from-query.js +60 -0
  146. package/dist/permissions/modules/process-ast/utils/find-related-collection.d.ts +3 -0
  147. package/dist/permissions/modules/process-ast/utils/find-related-collection.js +9 -0
  148. package/dist/permissions/modules/process-ast/utils/flatten-filter.d.ts +3 -0
  149. package/dist/permissions/modules/process-ast/utils/flatten-filter.js +34 -0
  150. package/dist/permissions/modules/process-ast/utils/format-a2o-key.d.ts +1 -0
  151. package/dist/permissions/modules/process-ast/utils/format-a2o-key.js +3 -0
  152. package/dist/permissions/modules/process-ast/utils/get-info-for-path.d.ts +5 -0
  153. package/dist/permissions/modules/process-ast/utils/get-info-for-path.js +7 -0
  154. package/dist/permissions/modules/process-ast/utils/has-item-permissions.d.ts +2 -0
  155. package/dist/permissions/modules/process-ast/utils/has-item-permissions.js +3 -0
  156. package/dist/permissions/modules/process-ast/utils/stringify-query-path.d.ts +2 -0
  157. package/dist/permissions/modules/process-ast/utils/stringify-query-path.js +3 -0
  158. package/dist/permissions/modules/process-ast/utils/validate-path/create-error.d.ts +3 -0
  159. package/dist/permissions/modules/process-ast/utils/validate-path/create-error.js +16 -0
  160. package/dist/permissions/modules/process-ast/utils/validate-path/validate-path-existence.d.ts +2 -0
  161. package/dist/permissions/modules/process-ast/utils/validate-path/validate-path-existence.js +12 -0
  162. package/dist/permissions/modules/process-ast/utils/validate-path/validate-path-permissions.d.ts +2 -0
  163. package/dist/permissions/modules/process-ast/utils/validate-path/validate-path-permissions.js +28 -0
  164. package/dist/permissions/modules/process-payload/lib/is-field-nullable.d.ts +5 -0
  165. package/dist/permissions/modules/process-payload/lib/is-field-nullable.js +12 -0
  166. package/dist/permissions/modules/process-payload/process-payload.d.ts +13 -0
  167. package/dist/permissions/modules/process-payload/process-payload.js +77 -0
  168. package/dist/permissions/modules/validate-access/lib/validate-collection-access.d.ts +12 -0
  169. package/dist/permissions/modules/validate-access/lib/validate-collection-access.js +11 -0
  170. package/dist/permissions/modules/validate-access/lib/validate-item-access.d.ts +9 -0
  171. package/dist/permissions/modules/validate-access/lib/validate-item-access.js +33 -0
  172. package/dist/permissions/modules/validate-access/validate-access.d.ts +14 -0
  173. package/dist/permissions/modules/validate-access/validate-access.js +28 -0
  174. package/dist/permissions/modules/validate-remaining-admin/validate-remaining-admin-count.d.ts +1 -0
  175. package/dist/permissions/modules/validate-remaining-admin/validate-remaining-admin-count.js +8 -0
  176. package/dist/permissions/modules/validate-remaining-admin/validate-remaining-admin-users.d.ts +5 -0
  177. package/dist/permissions/modules/validate-remaining-admin/validate-remaining-admin-users.js +10 -0
  178. package/dist/permissions/types.d.ts +6 -0
  179. package/dist/permissions/types.js +1 -0
  180. package/dist/permissions/utils/create-default-accountability.d.ts +2 -0
  181. package/dist/permissions/utils/create-default-accountability.js +11 -0
  182. package/dist/permissions/utils/extract-required-dynamic-variable-context.d.ts +8 -0
  183. package/dist/permissions/utils/extract-required-dynamic-variable-context.js +27 -0
  184. package/dist/permissions/utils/fetch-dynamic-variable-context.d.ts +9 -0
  185. package/dist/permissions/utils/fetch-dynamic-variable-context.js +43 -0
  186. package/dist/permissions/utils/filter-policies-by-ip.d.ts +2 -0
  187. package/dist/permissions/utils/filter-policies-by-ip.js +15 -0
  188. package/dist/permissions/utils/get-unaliased-field-key.d.ts +5 -0
  189. package/dist/permissions/utils/get-unaliased-field-key.js +17 -0
  190. package/dist/permissions/utils/process-permissions.d.ts +7 -0
  191. package/dist/permissions/utils/process-permissions.js +9 -0
  192. package/dist/permissions/utils/with-cache.d.ts +10 -0
  193. package/dist/permissions/utils/with-cache.js +25 -0
  194. package/dist/server.js +17 -4
  195. package/dist/services/access.d.ts +10 -0
  196. package/dist/services/access.js +43 -0
  197. package/dist/services/activity.js +22 -10
  198. package/dist/services/assets.d.ts +2 -3
  199. package/dist/services/assets.js +10 -5
  200. package/dist/services/authentication.js +18 -18
  201. package/dist/services/collections.js +18 -17
  202. package/dist/services/fields.d.ts +0 -1
  203. package/dist/services/fields.js +54 -25
  204. package/dist/services/files.js +10 -3
  205. package/dist/services/graphql/index.d.ts +3 -3
  206. package/dist/services/graphql/index.js +126 -22
  207. package/dist/services/graphql/subscription.js +2 -4
  208. package/dist/services/import-export.d.ts +3 -1
  209. package/dist/services/import-export.js +67 -9
  210. package/dist/services/index.d.ts +3 -2
  211. package/dist/services/index.js +3 -2
  212. package/dist/services/items.js +115 -44
  213. package/dist/services/meta.js +60 -23
  214. package/dist/services/notifications.js +14 -6
  215. package/dist/services/payload.d.ts +9 -10
  216. package/dist/services/payload.js +18 -3
  217. package/dist/services/{permissions/index.d.ts → permissions.d.ts} +5 -7
  218. package/dist/services/{permissions/index.js → permissions.js} +30 -54
  219. package/dist/services/policies.d.ts +12 -0
  220. package/dist/services/policies.js +87 -0
  221. package/dist/services/relations.d.ts +0 -6
  222. package/dist/services/relations.js +27 -30
  223. package/dist/services/roles.d.ts +4 -12
  224. package/dist/services/roles.js +57 -424
  225. package/dist/services/shares.d.ts +0 -2
  226. package/dist/services/shares.js +12 -8
  227. package/dist/services/specifications.d.ts +2 -2
  228. package/dist/services/specifications.js +39 -27
  229. package/dist/services/users.d.ts +1 -5
  230. package/dist/services/users.js +78 -161
  231. package/dist/services/utils.js +11 -7
  232. package/dist/services/versions.d.ts +0 -2
  233. package/dist/services/versions.js +34 -10
  234. package/dist/telemetry/lib/get-report.js +2 -2
  235. package/dist/telemetry/utils/check-user-limits.d.ts +5 -0
  236. package/dist/telemetry/utils/check-user-limits.js +19 -0
  237. package/dist/types/ast.d.ts +43 -1
  238. package/dist/types/database.d.ts +1 -1
  239. package/dist/types/items.d.ts +11 -0
  240. package/dist/utils/apply-query.d.ts +11 -7
  241. package/dist/utils/apply-query.js +69 -11
  242. package/dist/utils/fetch-user-count/fetch-access-lookup.d.ts +19 -0
  243. package/dist/utils/fetch-user-count/fetch-access-lookup.js +23 -0
  244. package/dist/utils/fetch-user-count/fetch-access-roles.d.ts +16 -0
  245. package/dist/utils/fetch-user-count/fetch-access-roles.js +37 -0
  246. package/dist/utils/fetch-user-count/fetch-active-users.d.ts +6 -0
  247. package/dist/utils/fetch-user-count/fetch-active-users.js +3 -0
  248. package/dist/utils/fetch-user-count/fetch-user-count.d.ts +12 -0
  249. package/dist/utils/fetch-user-count/fetch-user-count.js +64 -0
  250. package/dist/utils/fetch-user-count/get-user-count-query.d.ts +20 -0
  251. package/dist/utils/fetch-user-count/get-user-count-query.js +17 -0
  252. package/dist/utils/get-accountability-for-role.js +16 -25
  253. package/dist/utils/get-accountability-for-token.js +17 -16
  254. package/dist/utils/get-address.d.ts +5 -0
  255. package/dist/utils/get-address.js +13 -0
  256. package/dist/utils/get-cache-key.d.ts +1 -1
  257. package/dist/utils/get-cache-key.js +12 -1
  258. package/dist/utils/get-column.d.ts +2 -1
  259. package/dist/utils/get-column.js +1 -0
  260. package/dist/utils/get-service.js +5 -1
  261. package/dist/utils/reduce-schema.d.ts +4 -6
  262. package/dist/utils/reduce-schema.js +16 -32
  263. package/dist/utils/sanitize-schema.d.ts +1 -1
  264. package/dist/utils/transaction.js +28 -11
  265. package/dist/utils/validate-user-count-integrity.d.ts +13 -0
  266. package/dist/utils/validate-user-count-integrity.js +29 -0
  267. package/dist/websocket/authenticate.d.ts +0 -2
  268. package/dist/websocket/authenticate.js +0 -12
  269. package/dist/websocket/controllers/graphql.js +3 -7
  270. package/dist/websocket/controllers/hooks.js +4 -0
  271. package/dist/websocket/controllers/rest.js +2 -5
  272. package/dist/websocket/handlers/subscribe.js +0 -2
  273. package/dist/websocket/utils/items.d.ts +1 -1
  274. package/package.json +31 -30
  275. package/dist/database/run-ast.js +0 -458
  276. package/dist/middleware/check-ip.d.ts +0 -2
  277. package/dist/middleware/check-ip.js +0 -37
  278. package/dist/middleware/get-permissions.d.ts +0 -3
  279. package/dist/middleware/get-permissions.js +0 -10
  280. package/dist/services/authorization.d.ts +0 -17
  281. package/dist/services/authorization.js +0 -456
  282. package/dist/services/permissions/lib/with-app-minimal-permissions.js +0 -13
  283. package/dist/telemetry/utils/check-increased-user-limits.d.ts +0 -7
  284. package/dist/telemetry/utils/check-increased-user-limits.js +0 -25
  285. package/dist/telemetry/utils/get-role-counts-by-roles.d.ts +0 -6
  286. package/dist/telemetry/utils/get-role-counts-by-roles.js +0 -27
  287. package/dist/telemetry/utils/get-role-counts-by-users.d.ts +0 -11
  288. package/dist/telemetry/utils/get-role-counts-by-users.js +0 -34
  289. package/dist/telemetry/utils/get-user-count.d.ts +0 -8
  290. package/dist/telemetry/utils/get-user-count.js +0 -33
  291. package/dist/telemetry/utils/get-user-counts-by-roles.d.ts +0 -7
  292. package/dist/telemetry/utils/get-user-counts-by-roles.js +0 -35
  293. package/dist/utils/get-ast-from-query.d.ts +0 -13
  294. package/dist/utils/get-ast-from-query.js +0 -297
  295. package/dist/utils/get-permissions.d.ts +0 -2
  296. package/dist/utils/get-permissions.js +0 -150
  297. package/dist/utils/merge-permissions-for-share.d.ts +0 -4
  298. package/dist/utils/merge-permissions-for-share.js +0 -109
  299. package/dist/utils/merge-permissions.js +0 -95
@@ -1,8 +1,6 @@
1
- import { getHelpers } from '../helpers/index.js';
1
+ import { getDatabaseClient } from '../index.js';
2
2
  export async function up(knex) {
3
- const helper = getHelpers(knex).schema;
4
- const isMysql = helper.isOneOfClients(['mysql']);
5
- if (isMysql) {
3
+ if (getDatabaseClient(knex) === 'mysql') {
6
4
  // Knex creates invalid statement on MySQL, see https://github.com/knex/knex/issues/1888
7
5
  await knex.schema.raw('ALTER TABLE `directus_files` CHANGE `uploaded_on` `created_on` TIMESTAMP NOT NULL DEFAULT current_timestamp();');
8
6
  }
@@ -20,9 +18,7 @@ export async function down(knex) {
20
18
  await knex.schema.alterTable('directus_files', (table) => {
21
19
  table.dropColumn('uploaded_on');
22
20
  });
23
- const helper = getHelpers(knex).schema;
24
- const isMysql = helper.isOneOfClients(['mysql']);
25
- if (isMysql) {
21
+ if (getDatabaseClient(knex) === 'mysql') {
26
22
  await knex.schema.raw('ALTER TABLE `directus_files` CHANGE `created_on` `uploaded_on` TIMESTAMP NOT NULL DEFAULT current_timestamp();');
27
23
  }
28
24
  else {
@@ -1,3 +1,6 @@
1
+ import type { Knex } from 'knex';
1
2
  import type { Permission } from '@directus/types';
2
- export declare function mergePermissions(strategy: 'and' | 'or', ...permissions: Permission[][]): Permission[];
3
+ export declare function mergePermissions(strategy: 'and' | 'or', ...permissions: Permission[][]): any[];
3
4
  export declare function mergePermission(strategy: 'and' | 'or', currentPerm: Permission, newPerm: Permission): Omit<Permission, 'id' | 'system'>;
5
+ export declare function up(knex: Knex): Promise<void>;
6
+ export declare function down(knex: Knex): Promise<void>;
@@ -0,0 +1,352 @@
1
+ import { processChunk, toBoolean } from '@directus/utils';
2
+ import { flatten, intersection, isEqual, merge, omit, uniq } from 'lodash-es';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { useLogger } from '../../logger/index.js';
5
+ import { fetchPermissions } from '../../permissions/lib/fetch-permissions.js';
6
+ import { fetchPolicies } from '../../permissions/lib/fetch-policies.js';
7
+ import { fetchRolesTree } from '../../permissions/lib/fetch-roles-tree.js';
8
+ import { getSchema } from '../../utils/get-schema.js';
9
+ import { getSchemaInspector } from '../index.js';
10
+ // Adapted from https://github.com/directus/directus/blob/141b8adbf4dd8e06530a7929f34e3fc68a522053/api/src/utils/merge-permissions.ts#L4
11
+ export function mergePermissions(strategy, ...permissions) {
12
+ const allPermissions = flatten(permissions);
13
+ const mergedPermissions = allPermissions
14
+ .reduce((acc, val) => {
15
+ const key = `${val.collection}__${val.action}`;
16
+ const current = acc.get(key);
17
+ acc.set(key, current ? mergePermission(strategy, current, val) : val);
18
+ return acc;
19
+ }, new Map())
20
+ .values();
21
+ return Array.from(mergedPermissions);
22
+ }
23
+ export function mergePermission(strategy, currentPerm, newPerm) {
24
+ const logicalKey = `_${strategy}`;
25
+ let { permissions, validation, fields, presets } = currentPerm;
26
+ if (newPerm.permissions) {
27
+ if (currentPerm.permissions && Object.keys(currentPerm.permissions)[0] === logicalKey) {
28
+ permissions = {
29
+ [logicalKey]: [
30
+ ...currentPerm.permissions[logicalKey],
31
+ newPerm.permissions,
32
+ ],
33
+ };
34
+ }
35
+ else if (currentPerm.permissions) {
36
+ // Empty {} supersedes other permissions in _OR merge
37
+ if (strategy === 'or' && (isEqual(currentPerm.permissions, {}) || isEqual(newPerm.permissions, {}))) {
38
+ permissions = {};
39
+ }
40
+ else {
41
+ permissions = {
42
+ [logicalKey]: [currentPerm.permissions, newPerm.permissions],
43
+ };
44
+ }
45
+ }
46
+ else {
47
+ permissions = {
48
+ [logicalKey]: [newPerm.permissions],
49
+ };
50
+ }
51
+ }
52
+ if (newPerm.validation) {
53
+ if (currentPerm.validation && Object.keys(currentPerm.validation)[0] === logicalKey) {
54
+ validation = {
55
+ [logicalKey]: [
56
+ ...currentPerm.validation[logicalKey],
57
+ newPerm.validation,
58
+ ],
59
+ };
60
+ }
61
+ else if (currentPerm.validation) {
62
+ // Empty {} supersedes other validations in _OR merge
63
+ if (strategy === 'or' && (isEqual(currentPerm.validation, {}) || isEqual(newPerm.validation, {}))) {
64
+ validation = {};
65
+ }
66
+ else {
67
+ validation = {
68
+ [logicalKey]: [currentPerm.validation, newPerm.validation],
69
+ };
70
+ }
71
+ }
72
+ else {
73
+ validation = {
74
+ [logicalKey]: [newPerm.validation],
75
+ };
76
+ }
77
+ }
78
+ if (newPerm.fields) {
79
+ if (Array.isArray(currentPerm.fields) && strategy === 'or') {
80
+ fields = uniq([...currentPerm.fields, ...newPerm.fields]);
81
+ }
82
+ else if (Array.isArray(currentPerm.fields) && strategy === 'and') {
83
+ fields = intersection(currentPerm.fields, newPerm.fields);
84
+ }
85
+ else {
86
+ fields = newPerm.fields;
87
+ }
88
+ if (fields.includes('*'))
89
+ fields = ['*'];
90
+ }
91
+ if (newPerm.presets) {
92
+ presets = merge({}, presets, newPerm.presets);
93
+ }
94
+ return omit({
95
+ ...currentPerm,
96
+ permissions,
97
+ validation,
98
+ fields,
99
+ presets,
100
+ }, ['id', 'system']);
101
+ }
102
+ async function fetchRoleAccess(roles, context) {
103
+ const roleAccess = {
104
+ admin_access: false,
105
+ app_access: false,
106
+ ip_access: null,
107
+ enforce_tfa: false,
108
+ };
109
+ const accessRows = await context
110
+ .knex('directus_access')
111
+ .select('directus_policies.id', 'directus_policies.admin_access', 'directus_policies.app_access', 'directus_policies.ip_access', 'directus_policies.enforce_tfa')
112
+ .where('role', 'in', roles)
113
+ .leftJoin('directus_policies', 'directus_policies.id', 'directus_access.policy');
114
+ const ipAccess = new Set();
115
+ for (const { admin_access, app_access, ip_access, enforce_tfa } of accessRows) {
116
+ roleAccess.admin_access ||= toBoolean(admin_access);
117
+ roleAccess.app_access ||= toBoolean(app_access);
118
+ roleAccess.enforce_tfa ||= toBoolean(enforce_tfa);
119
+ if (ip_access && ip_access.length) {
120
+ ip_access.split(',').forEach((ip) => ipAccess.add(ip));
121
+ }
122
+ }
123
+ if (ipAccess.size > 0) {
124
+ roleAccess.ip_access = Array.from(ipAccess).join(',');
125
+ }
126
+ return roleAccess;
127
+ }
128
+ /**
129
+ * The public role used to be `null`, we gotta create a single new policy for the permissions
130
+ * previously attached to the public role (marked through `role = null`).
131
+ */
132
+ const PUBLIC_POLICY_ID = 'abf8a154-5b1c-4a46-ac9c-7300570f4f17';
133
+ export async function up(knex) {
134
+ const logger = useLogger();
135
+ /////////////////////////////////////////////////////////////////////////////////////////////////
136
+ // If the policies table already exists the migration has already run
137
+ if (await knex.schema.hasTable('directus_policies')) {
138
+ return;
139
+ }
140
+ /////////////////////////////////////////////////////////////////////////////////////////////////
141
+ // Create new policies table that mirrors previous Roles
142
+ await knex.schema.createTable('directus_policies', (table) => {
143
+ table.uuid('id').primary();
144
+ table.string('name', 100).notNullable();
145
+ table.string('icon', 64).notNullable().defaultTo('badge');
146
+ table.text('description');
147
+ table.text('ip_access');
148
+ table.boolean('enforce_tfa').defaultTo(false).notNullable();
149
+ table.boolean('admin_access').defaultTo(false).notNullable();
150
+ table.boolean('app_access').defaultTo(false).notNullable();
151
+ });
152
+ /////////////////////////////////////////////////////////////////////////////////////////////////
153
+ // Copy over all existing roles into new policies
154
+ const roles = await knex
155
+ .select('id', 'name', 'icon', 'description', 'ip_access', 'enforce_tfa', 'admin_access', 'app_access')
156
+ .from('directus_roles');
157
+ if (roles.length > 0) {
158
+ await processChunk(roles, 100, async (chunk) => {
159
+ await knex('directus_policies').insert(chunk);
160
+ });
161
+ }
162
+ await knex
163
+ .insert({
164
+ id: PUBLIC_POLICY_ID,
165
+ name: '$t:public_label',
166
+ icon: 'public',
167
+ description: '$t:public_description',
168
+ app_access: false,
169
+ })
170
+ .into('directus_policies');
171
+ // Change the admin policy description to $t:admin_policy_description
172
+ await knex('directus_policies')
173
+ .update({
174
+ description: '$t:admin_policy_description',
175
+ })
176
+ .where('description', 'LIKE', '$t:admin_description');
177
+ /////////////////////////////////////////////////////////////////////////////////////////////////
178
+ // Remove access control + add nesting to roles
179
+ await knex.schema.alterTable('directus_roles', (table) => {
180
+ table.dropColumn('ip_access');
181
+ table.dropColumn('enforce_tfa');
182
+ table.dropColumn('admin_access');
183
+ table.dropColumn('app_access');
184
+ table.uuid('parent').references('directus_roles.id');
185
+ });
186
+ /////////////////////////////////////////////////////////////////////////////////////////////////
187
+ // Link permissions to policies instead of roles
188
+ await knex.schema.alterTable('directus_permissions', (table) => {
189
+ table.uuid('policy').references('directus_policies.id').onDelete('CASCADE');
190
+ });
191
+ try {
192
+ const inspector = await getSchemaInspector();
193
+ const foreignKeys = await inspector.foreignKeys('directus_permissions');
194
+ const foreignConstraint = foreignKeys.find((foreign) => foreign.foreign_key_table === 'directus_roles' && foreign.column === 'role')
195
+ ?.constraint_name || undefined;
196
+ await knex.schema.alterTable('directus_permissions', (table) => {
197
+ // Drop the foreign key constraint here in order to update `null` role to public policy ID
198
+ table.dropForeign('role', foreignConstraint);
199
+ });
200
+ }
201
+ catch (err) {
202
+ logger.warn('Failed to drop foreign key constraint on `role` column in `directus_permissions` table');
203
+ }
204
+ await knex('directus_permissions')
205
+ .update({
206
+ role: PUBLIC_POLICY_ID,
207
+ })
208
+ .whereNull('role');
209
+ await knex('directus_permissions').update({
210
+ policy: knex.ref('role'),
211
+ });
212
+ await knex.schema.alterTable('directus_permissions', (table) => {
213
+ table.dropColumns('role');
214
+ table.dropNullable('policy');
215
+ });
216
+ /////////////////////////////////////////////////////////////////////////////////////////////////
217
+ // Setup junction table between roles/users and policies
218
+ // This could be a A2O style setup with a collection/item field rather than individual foreign
219
+ // keys, but we want to be able to show the reverse-relationship on the individual policies as
220
+ // well, which would require the O2A type to exist in Directus which currently doesn't.
221
+ // Shouldn't be the end of the world here, as we know we're only attaching policies to two other
222
+ // collections.
223
+ await knex.schema.createTable('directus_access', (table) => {
224
+ table.uuid('id').primary();
225
+ table.uuid('role').references('directus_roles.id').nullable().onDelete('CASCADE');
226
+ table.uuid('user').references('directus_users.id').nullable().onDelete('CASCADE');
227
+ table.uuid('policy').references('directus_policies.id').notNullable().onDelete('CASCADE');
228
+ table.integer('sort');
229
+ });
230
+ /////////////////////////////////////////////////////////////////////////////////////////////////
231
+ // Attach policies to existing roles for backwards compatibility
232
+ const policyAttachments = roles.map((role) => ({
233
+ id: randomUUID(),
234
+ role: role.id,
235
+ user: null,
236
+ policy: role.id,
237
+ sort: 1,
238
+ }));
239
+ await processChunk(policyAttachments, 100, async (chunk) => {
240
+ await knex('directus_access').insert(chunk);
241
+ });
242
+ await knex('directus_access').insert({
243
+ id: randomUUID(),
244
+ role: null,
245
+ user: null,
246
+ policy: PUBLIC_POLICY_ID,
247
+ sort: 1,
248
+ });
249
+ }
250
+ export async function down(knex) {
251
+ /////////////////////////////////////////////////////////////////////////////////////////////////
252
+ // Reinstate access control fields on directus roles
253
+ await knex.schema.alterTable('directus_roles', (table) => {
254
+ table.text('ip_access');
255
+ table.boolean('enforce_tfa').defaultTo(false).notNullable();
256
+ table.boolean('admin_access').defaultTo(false).notNullable();
257
+ table.boolean('app_access').defaultTo(true).notNullable();
258
+ });
259
+ /////////////////////////////////////////////////////////////////////////////////////////////////
260
+ // Copy policy access control rules back to roles
261
+ const originalPermissions = await knex
262
+ .select('id')
263
+ .from('directus_permissions')
264
+ .whereNot({ policy: PUBLIC_POLICY_ID });
265
+ await knex.schema.alterTable('directus_permissions', (table) => {
266
+ table.uuid('role').nullable();
267
+ table.setNullable('policy');
268
+ });
269
+ const context = { knex, schema: await getSchema() };
270
+ // fetch all roles
271
+ const roles = await knex.select('id').from('directus_roles');
272
+ // simulate Public Role
273
+ roles.push({ id: null });
274
+ // role permissions to be inserted once all processing is completed
275
+ const rolePermissions = [];
276
+ for (const role of roles) {
277
+ const roleTree = await fetchRolesTree(role.id, knex);
278
+ let roleAccess = null;
279
+ if (role.id !== null) {
280
+ roleAccess = await fetchRoleAccess(roleTree, context);
281
+ await knex('directus_roles').update(roleAccess).where({ id: role.id });
282
+ }
283
+ if (roleAccess === null || !roleAccess.admin_access) {
284
+ // fetch all of the roles policies
285
+ const policies = await fetchPolicies({ roles: roleTree, user: null, ip: null }, context);
286
+ // fetch all of the policies permissions
287
+ const rawPermissions = await fetchPermissions({
288
+ accountability: { role: null, roles: roleTree, user: null, app: roleAccess?.app_access || false },
289
+ policies,
290
+ bypassDynamicVariableProcessing: true,
291
+ }, context);
292
+ // merge all permissions to single version (v10) and save for later use
293
+ mergePermissions('or', rawPermissions).forEach((permission) => {
294
+ // System permissions are automatically populated
295
+ if (permission.system) {
296
+ return;
297
+ }
298
+ // convert merged permissions to storage ready format
299
+ if (Array.isArray(permission.fields)) {
300
+ permission.fields = permission.fields.join(',');
301
+ }
302
+ if (permission.permissions) {
303
+ permission.permissions = JSON.stringify(permission.permissions);
304
+ }
305
+ if (permission.validation) {
306
+ permission.validation = JSON.stringify(permission.validation);
307
+ }
308
+ if (permission.presets) {
309
+ permission.presets = JSON.stringify(permission.presets);
310
+ }
311
+ rolePermissions.push({ role: role.id, ...omit(permission, ['id', 'policy']) });
312
+ });
313
+ }
314
+ }
315
+ /////////////////////////////////////////////////////////////////////////////////////////////////
316
+ // Remove role nesting support
317
+ await knex.schema.alterTable('directus_roles', (table) => {
318
+ table.dropForeign('parent');
319
+ table.dropColumn('parent');
320
+ });
321
+ /////////////////////////////////////////////////////////////////////////////////////////////////
322
+ // Drop all permissions that are only attached to a user
323
+ // TODO query all policies that are attached to a user and delete their permissions,
324
+ // since we don't know were to put them now and it'll cause a foreign key problem
325
+ // as soon as we reference directus_roles in directus_permissions again
326
+ /////////////////////////////////////////////////////////////////////////////////////////////////
327
+ // Drop policy attachments
328
+ await knex.schema.dropTable('directus_access');
329
+ /////////////////////////////////////////////////////////////////////////////////////////////////
330
+ // Reattach permissions to roles instead of policies
331
+ await knex('directus_permissions')
332
+ .update({
333
+ role: null,
334
+ })
335
+ .where({ role: PUBLIC_POLICY_ID });
336
+ // remove all v11 permissions
337
+ await processChunk(originalPermissions, 100, async (chunk) => {
338
+ await knex('directus_permissions').delete(chunk);
339
+ });
340
+ // insert all v10 permissions
341
+ await processChunk(rolePermissions, 100, async (chunk) => {
342
+ await knex('directus_permissions').insert(chunk);
343
+ });
344
+ await knex.schema.alterTable('directus_permissions', (table) => {
345
+ table.uuid('role').references('directus_roles.id').alter();
346
+ table.dropForeign('policy');
347
+ table.dropColumn('policy');
348
+ });
349
+ /////////////////////////////////////////////////////////////////////////////////////////////////
350
+ // Drop policies table
351
+ await knex.schema.dropTable('directus_policies');
352
+ }
@@ -0,0 +1,4 @@
1
+ import type { Filter, Query, SchemaOverview } from '@directus/types';
2
+ import type { Knex } from 'knex';
3
+ import type { FieldNode, FunctionFieldNode, O2MNode } from '../../../types/ast.js';
4
+ export declare function getDBQuery(schema: SchemaOverview, knex: Knex, table: string, fieldNodes: (FieldNode | FunctionFieldNode)[], o2mNodes: O2MNode[], query: Query, cases: Filter[]): Knex.QueryBuilder;
@@ -0,0 +1,218 @@
1
+ import { useEnv } from '@directus/env';
2
+ import { cloneDeep } from 'lodash-es';
3
+ import applyQuery, { applyLimit, applySort, generateAlias } from '../../../utils/apply-query.js';
4
+ import { getCollectionFromAlias } from '../../../utils/get-collection-from-alias.js';
5
+ import { getColumn } from '../../../utils/get-column.js';
6
+ import { getHelpers } from '../../helpers/index.js';
7
+ import { applyCaseWhen } from '../utils/apply-case-when.js';
8
+ import { getColumnPreprocessor } from '../utils/get-column-pre-processor.js';
9
+ import { getNodeAlias } from '../utils/get-field-alias.js';
10
+ import { getInnerQueryColumnPreProcessor } from '../utils/get-inner-query-column-pre-processor.js';
11
+ import { withPreprocessBindings } from '../utils/with-preprocess-bindings.js';
12
+ export function getDBQuery(schema, knex, table, fieldNodes, o2mNodes, query, cases) {
13
+ const aliasMap = Object.create(null);
14
+ const env = useEnv();
15
+ const preProcess = getColumnPreprocessor(knex, schema, table, cases, aliasMap);
16
+ const queryCopy = cloneDeep(query);
17
+ const helpers = getHelpers(knex);
18
+ const hasCaseWhen = o2mNodes.some((node) => node.whenCase && node.whenCase.length > 0) ||
19
+ fieldNodes.some((node) => node.whenCase && node.whenCase.length > 0);
20
+ queryCopy.limit = typeof queryCopy.limit === 'number' ? queryCopy.limit : Number(env['QUERY_LIMIT_DEFAULT']);
21
+ // Queries with aggregates and groupBy will not have duplicate result
22
+ if (queryCopy.aggregate || queryCopy.group) {
23
+ const flatQuery = knex.from(table);
24
+ // Map the group fields to their respective field nodes
25
+ const groupWhenCases = hasCaseWhen
26
+ ? queryCopy.group?.map((field) => fieldNodes.find(({ fieldKey }) => fieldKey === field)?.whenCase ?? [])
27
+ : undefined;
28
+ const dbQuery = applyQuery(knex, table, flatQuery, queryCopy, schema, cases, { aliasMap, groupWhenCases }).query;
29
+ flatQuery.select(fieldNodes.map((node) => preProcess(node)));
30
+ withPreprocessBindings(knex, dbQuery);
31
+ return dbQuery;
32
+ }
33
+ const primaryKey = schema.collections[table].primary;
34
+ let dbQuery = knex.from(table);
35
+ let sortRecords;
36
+ const innerQuerySortRecords = [];
37
+ let hasMultiRelationalSort;
38
+ if (queryCopy.sort) {
39
+ const sortResult = applySort(knex, schema, dbQuery, queryCopy, table, aliasMap, true);
40
+ if (sortResult) {
41
+ sortRecords = sortResult.sortRecords;
42
+ hasMultiRelationalSort = sortResult.hasMultiRelationalSort;
43
+ }
44
+ }
45
+ const { hasMultiRelationalFilter } = applyQuery(knex, table, dbQuery, queryCopy, schema, cases, {
46
+ aliasMap,
47
+ isInnerQuery: true,
48
+ hasMultiRelationalSort,
49
+ });
50
+ const needsInnerQuery = hasMultiRelationalSort || hasMultiRelationalFilter;
51
+ if (needsInnerQuery) {
52
+ dbQuery.select(`${table}.${primaryKey}`);
53
+ // Only add distinct if there are no case/when constructs, since otherwise we rely on group by
54
+ if (!hasCaseWhen)
55
+ dbQuery.distinct();
56
+ }
57
+ else {
58
+ dbQuery.select(fieldNodes.map((node) => preProcess(node)));
59
+ // Add flags for o2m fields with case/when to the let the DB to the partial item permissions
60
+ dbQuery.select(o2mNodes
61
+ .filter((node) => node.whenCase && node.whenCase.length > 0)
62
+ .map((node) => {
63
+ const columnCases = node.whenCase.map((index) => cases[index]);
64
+ return applyCaseWhen({
65
+ column: knex.raw(1),
66
+ columnCases,
67
+ aliasMap,
68
+ cases,
69
+ table,
70
+ alias: node.fieldKey,
71
+ }, { knex, schema });
72
+ }));
73
+ }
74
+ if (sortRecords) {
75
+ // Clears the order if any, eg: from MSSQL offset
76
+ dbQuery.clear('order');
77
+ if (needsInnerQuery) {
78
+ let orderByString = '';
79
+ const orderByFields = [];
80
+ sortRecords.map((sortRecord) => {
81
+ if (orderByString.length !== 0) {
82
+ orderByString += ', ';
83
+ }
84
+ const sortAlias = `sort_${generateAlias()}`;
85
+ if (sortRecord.column.includes('.')) {
86
+ const [alias, field] = sortRecord.column.split('.');
87
+ const originalCollectionName = getCollectionFromAlias(alias, aliasMap);
88
+ dbQuery.select(getColumn(knex, alias, field, sortAlias, schema, { originalCollectionName }));
89
+ orderByString += `?? ${sortRecord.order}`;
90
+ orderByFields.push(getColumn(knex, alias, field, false, schema, { originalCollectionName }));
91
+ }
92
+ else {
93
+ dbQuery.select(getColumn(knex, table, sortRecord.column, sortAlias, schema));
94
+ orderByString += `?? ${sortRecord.order}`;
95
+ orderByFields.push(getColumn(knex, table, sortRecord.column, false, schema));
96
+ }
97
+ innerQuerySortRecords.push({ alias: sortAlias, order: sortRecord.order });
98
+ });
99
+ if (hasMultiRelationalSort) {
100
+ dbQuery = helpers.schema.applyMultiRelationalSort(knex, dbQuery, table, primaryKey, orderByString, orderByFields);
101
+ // Start order by with directus_row_number. The directus_row_number is derived from a window function that
102
+ // is ordered by the sort fields within every primary key partition. That ensures that the result with the
103
+ // row number = 1 is the top-most row of every partition, according to the selected sort fields.
104
+ // Since the only relevant result is the first row of this partition, adding the directus_row_number to the
105
+ // order by here ensures that all rows with a directus_row_number = 1 show up first in the inner query result,
106
+ // and are correctly truncated by the limit, but not earlier.
107
+ orderByString = `?? asc, ${orderByString}`;
108
+ orderByFields.unshift(knex.ref('directus_row_number'));
109
+ }
110
+ dbQuery.orderByRaw(orderByString, orderByFields);
111
+ }
112
+ else {
113
+ sortRecords.map((sortRecord) => {
114
+ if (sortRecord.column.includes('.')) {
115
+ const [alias, field] = sortRecord.column.split('.');
116
+ sortRecord.column = getColumn(knex, alias, field, false, schema, {
117
+ originalCollectionName: getCollectionFromAlias(alias, aliasMap),
118
+ });
119
+ }
120
+ else {
121
+ sortRecord.column = getColumn(knex, table, sortRecord.column, false, schema);
122
+ }
123
+ });
124
+ dbQuery.orderBy(sortRecords);
125
+ }
126
+ }
127
+ if (!needsInnerQuery)
128
+ return dbQuery;
129
+ const innerCaseWhenAliasPrefix = generateAlias();
130
+ if (hasCaseWhen) {
131
+ /* If there are cases, we need to employ a trick in order to evaluate the case/when structure in the inner query,
132
+ while passing the result of the evaluation to the outer query. The case/when needs to be evaluated in the inner
133
+ query since only there all joined in tables, that might be required for the case/when, are available.
134
+
135
+ The problem is, that the resulting columns can not be directly selected in the inner query,
136
+ as a `SELECT DISTINCT` does not work for all datatypes in all vendors.
137
+
138
+ So instead of having an inner query which might look like this:
139
+
140
+ SELECT DISTINCT ...,
141
+ CASE WHEN <condition> THEN <actual-column> END AS <alias>
142
+
143
+ a group-by query is generated.
144
+
145
+ Another problem is that all not all rows with the same primary key are guaranteed to have the same value for
146
+ the columns with the case/when, so we to `or` those together, but counting the number of flags in a group by
147
+ operation. This way the flag is set to > 0 if any of the rows in the group allows access to the column.
148
+
149
+ The inner query only evaluates the condition and passes up or-ed flag, that is used in the wrapper query to select
150
+ the actual column:
151
+
152
+ SELECT ...,
153
+ COUNT (CASE WHEN <condition> THEN 1 END) AS <random-prefix>_<alias>
154
+ ...
155
+ GROUP BY <primary-key>
156
+
157
+ Then, in the wrapper query there is no need to evaluate the condition again, but instead rely on the flag:
158
+
159
+ SELECT ...,
160
+ CASE WHEN `inner`.<random-prefix>_<alias> > 0 THEN <actual-column> END AS <alias>
161
+ */
162
+ const innerPreprocess = getInnerQueryColumnPreProcessor(knex, schema, table, cases, aliasMap, innerCaseWhenAliasPrefix);
163
+ // To optimize the query we avoid having unnecessary columns in the inner query, that don't have a caseWhen, since
164
+ // they are selected in the outer query directly
165
+ dbQuery.select(fieldNodes.map(innerPreprocess).filter((x) => x !== null));
166
+ // In addition to the regular columns select a flag that indicates if a user has access to o2m related field
167
+ // based on the case/when of that field.
168
+ dbQuery.select(o2mNodes.map(innerPreprocess).filter((x) => x !== null));
169
+ const groupByFields = [knex.raw('??.??', [table, primaryKey])];
170
+ if (hasMultiRelationalSort) {
171
+ // Sort fields that are not directly in the table the primary key is from need to be included in the group
172
+ // by clause, otherwise this causes problems on some DBs
173
+ groupByFields.push(...innerQuerySortRecords.map(({ alias }) => knex.raw('??', alias)));
174
+ }
175
+ dbQuery.groupBy(groupByFields);
176
+ }
177
+ const wrapperQuery = knex
178
+ .from(table)
179
+ .innerJoin(knex.raw('??', dbQuery.as('inner')), `${table}.${primaryKey}`, `inner.${primaryKey}`);
180
+ if (!hasCaseWhen) {
181
+ // No need for case/when in the wrapper query, just select the preprocessed columns
182
+ wrapperQuery.select(fieldNodes.map((node) => preProcess(node)));
183
+ }
184
+ else {
185
+ // This applies a simplified case/when construct in the wrapper query, that only looks at flag > 1
186
+ // Distinguish between column with and without case/when and handle them differently
187
+ const plainColumns = fieldNodes.filter((fieldNode) => !fieldNode.whenCase || fieldNode.whenCase.length === 0);
188
+ const whenCaseColumns = fieldNodes.filter((fieldNode) => fieldNode.whenCase && fieldNode.whenCase.length > 0);
189
+ // Select the plain columns
190
+ wrapperQuery.select(plainColumns.map((node) => preProcess(node)));
191
+ // Select the case/when columns based on the flag from the inner query
192
+ wrapperQuery.select(whenCaseColumns.map((fieldNode) => {
193
+ const alias = getNodeAlias(fieldNode);
194
+ const innerAlias = `${innerCaseWhenAliasPrefix}_${alias}`;
195
+ // Preprocess the column without the case/when, since that is applied in a simpler fashion in the select
196
+ const column = preProcess({ ...fieldNode, whenCase: [] }, { noAlias: true });
197
+ return knex.raw(`CASE WHEN ??.?? > 0 THEN ?? END as ??`, ['inner', innerAlias, column, alias]);
198
+ }));
199
+ // Pass the flags of o2m fields up through the wrapper query
200
+ wrapperQuery.select(o2mNodes
201
+ .filter((node) => node.whenCase && node.whenCase.length > 0)
202
+ .map((node) => {
203
+ const alias = node.fieldKey;
204
+ const innerAlias = `${innerCaseWhenAliasPrefix}_${alias}`;
205
+ return knex.raw(`CASE WHEN ??.?? > 0 THEN 1 END as ??`, ['inner', innerAlias, alias]);
206
+ }));
207
+ }
208
+ if (sortRecords) {
209
+ innerQuerySortRecords.map((innerQuerySortRecord) => {
210
+ wrapperQuery.orderBy(`inner.${innerQuerySortRecord.alias}`, innerQuerySortRecord.order);
211
+ });
212
+ if (hasMultiRelationalSort) {
213
+ wrapperQuery.where('inner.directus_row_number', '=', 1);
214
+ applyLimit(knex, wrapperQuery, queryCopy.limit);
215
+ }
216
+ }
217
+ return wrapperQuery;
218
+ }
@@ -0,0 +1,7 @@
1
+ import type { Query, SchemaOverview } from '@directus/types';
2
+ import type { FieldNode, FunctionFieldNode, NestedCollectionNode } from '../../../types/ast.js';
3
+ export declare function parseCurrentLevel(schema: SchemaOverview, collection: string, children: (NestedCollectionNode | FieldNode | FunctionFieldNode)[], query: Query): Promise<{
4
+ fieldNodes: FieldNode[];
5
+ nestedCollectionNodes: NestedCollectionNode[];
6
+ primaryKeyField: string;
7
+ }>;