@directus/api 22.1.1 → 23.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 (119) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/auth/drivers/ldap.js +14 -3
  3. package/dist/auth/drivers/oauth2.js +13 -2
  4. package/dist/auth/drivers/openid.js +13 -2
  5. package/dist/cache.d.ts +2 -2
  6. package/dist/cache.js +6 -6
  7. package/dist/cli/commands/init/questions.d.ts +5 -5
  8. package/dist/cli/commands/schema/apply.d.ts +1 -0
  9. package/dist/cli/commands/schema/apply.js +20 -1
  10. package/dist/cli/index.js +1 -0
  11. package/dist/cli/utils/create-env/env-stub.liquid +1 -4
  12. package/dist/constants.d.ts +1 -0
  13. package/dist/constants.js +1 -0
  14. package/dist/database/helpers/index.d.ts +2 -0
  15. package/dist/database/helpers/index.js +2 -0
  16. package/dist/database/helpers/nullable-update/dialects/default.d.ts +3 -0
  17. package/dist/database/helpers/nullable-update/dialects/default.js +3 -0
  18. package/dist/database/helpers/nullable-update/dialects/oracle.d.ts +12 -0
  19. package/dist/database/helpers/nullable-update/dialects/oracle.js +16 -0
  20. package/dist/database/helpers/nullable-update/index.d.ts +7 -0
  21. package/dist/database/helpers/nullable-update/index.js +7 -0
  22. package/dist/database/helpers/nullable-update/types.d.ts +7 -0
  23. package/dist/database/helpers/nullable-update/types.js +12 -0
  24. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +3 -1
  25. package/dist/database/helpers/schema/dialects/cockroachdb.js +17 -0
  26. package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -1
  27. package/dist/database/helpers/schema/dialects/mssql.js +20 -0
  28. package/dist/database/helpers/schema/dialects/mysql.d.ts +2 -1
  29. package/dist/database/helpers/schema/dialects/mysql.js +33 -0
  30. package/dist/database/helpers/schema/dialects/oracle.d.ts +3 -1
  31. package/dist/database/helpers/schema/dialects/oracle.js +21 -0
  32. package/dist/database/helpers/schema/dialects/postgres.d.ts +3 -1
  33. package/dist/database/helpers/schema/dialects/postgres.js +23 -0
  34. package/dist/database/helpers/schema/dialects/sqlite.d.ts +1 -0
  35. package/dist/database/helpers/schema/dialects/sqlite.js +3 -0
  36. package/dist/database/helpers/schema/types.d.ts +5 -0
  37. package/dist/database/helpers/schema/types.js +3 -0
  38. package/dist/database/helpers/schema/utils/preprocess-bindings.d.ts +5 -1
  39. package/dist/database/helpers/schema/utils/preprocess-bindings.js +23 -17
  40. package/dist/database/migrations/20210518A-add-foreign-key-constraints.js +1 -1
  41. package/dist/database/migrations/20240806A-permissions-policies.js +1 -1
  42. package/dist/database/migrations/20240817A-update-icon-fields-length.d.ts +3 -0
  43. package/dist/database/migrations/20240817A-update-icon-fields-length.js +55 -0
  44. package/dist/database/run-ast/lib/get-db-query.js +14 -8
  45. package/dist/extensions/lib/sandbox/generate-api-extensions-sandbox-entrypoint.d.ts +0 -3
  46. package/dist/extensions/lib/sandbox/register/route.d.ts +1 -2
  47. package/dist/extensions/manager.js +2 -2
  48. package/dist/logger/index.d.ts +8 -3
  49. package/dist/logger/index.js +79 -28
  50. package/dist/logger/logs-stream.d.ts +10 -0
  51. package/dist/logger/logs-stream.js +41 -0
  52. package/dist/mailer.js +0 -6
  53. package/dist/middleware/authenticate.d.ts +1 -3
  54. package/dist/middleware/error-handler.d.ts +0 -1
  55. package/dist/middleware/respond.js +1 -0
  56. package/dist/middleware/validate-batch.d.ts +1 -4
  57. package/dist/permissions/lib/fetch-permissions.d.ts +11 -1
  58. package/dist/permissions/modules/process-ast/utils/get-info-for-path.d.ts +2 -2
  59. package/dist/permissions/modules/validate-remaining-admin/validate-remaining-admin-users.d.ts +1 -2
  60. package/dist/permissions/utils/fetch-dynamic-variable-context.js +14 -6
  61. package/dist/permissions/utils/process-permissions.d.ts +11 -1
  62. package/dist/permissions/utils/process-permissions.js +6 -4
  63. package/dist/request/agent-with-ip-validation.d.ts +0 -1
  64. package/dist/request/is-denied-ip.js +7 -1
  65. package/dist/server.d.ts +0 -3
  66. package/dist/server.js +4 -2
  67. package/dist/services/assets.d.ts +0 -1
  68. package/dist/services/fields.js +52 -20
  69. package/dist/services/files/utils/get-metadata.d.ts +0 -1
  70. package/dist/services/files/utils/parse-image-metadata.d.ts +0 -1
  71. package/dist/services/files.d.ts +0 -1
  72. package/dist/services/import-export.d.ts +0 -1
  73. package/dist/services/mail/index.js +1 -5
  74. package/dist/services/notifications.d.ts +0 -4
  75. package/dist/services/notifications.js +8 -6
  76. package/dist/services/server.js +8 -1
  77. package/dist/services/specifications.js +7 -7
  78. package/dist/services/tus/data-store.d.ts +0 -1
  79. package/dist/services/users.js +6 -3
  80. package/dist/types/graphql.d.ts +0 -1
  81. package/dist/utils/apply-query.d.ts +1 -1
  82. package/dist/utils/compress.d.ts +0 -1
  83. package/dist/utils/delete-from-require-cache.js +1 -1
  84. package/dist/utils/fetch-user-count/fetch-user-count.d.ts +1 -2
  85. package/dist/utils/generate-hash.js +2 -2
  86. package/dist/utils/get-address.d.ts +1 -4
  87. package/dist/utils/get-address.js +6 -1
  88. package/dist/utils/get-allowed-log-levels.d.ts +3 -0
  89. package/dist/utils/get-allowed-log-levels.js +11 -0
  90. package/dist/utils/get-cache-headers.d.ts +0 -1
  91. package/dist/utils/get-cache-key.d.ts +0 -1
  92. package/dist/utils/get-column.d.ts +1 -1
  93. package/dist/utils/get-graphql-query-and-variables.d.ts +0 -1
  94. package/dist/utils/get-ip-from-req.d.ts +0 -1
  95. package/dist/utils/get-schema.js +19 -24
  96. package/dist/utils/get-snapshot.js +1 -1
  97. package/dist/utils/parse-filter-key.js +1 -5
  98. package/dist/utils/sanitize-query.js +1 -1
  99. package/dist/utils/sanitize-schema.d.ts +1 -1
  100. package/dist/utils/should-skip-cache.d.ts +0 -1
  101. package/dist/websocket/authenticate.js +1 -1
  102. package/dist/websocket/controllers/base.d.ts +6 -15
  103. package/dist/websocket/controllers/base.js +17 -4
  104. package/dist/websocket/controllers/graphql.d.ts +0 -3
  105. package/dist/websocket/controllers/graphql.js +3 -1
  106. package/dist/websocket/controllers/index.d.ts +4 -3
  107. package/dist/websocket/controllers/index.js +12 -0
  108. package/dist/websocket/controllers/logs.d.ts +17 -0
  109. package/dist/websocket/controllers/logs.js +54 -0
  110. package/dist/websocket/controllers/rest.d.ts +0 -3
  111. package/dist/websocket/controllers/rest.js +4 -2
  112. package/dist/websocket/handlers/index.d.ts +1 -0
  113. package/dist/websocket/handlers/index.js +21 -3
  114. package/dist/websocket/handlers/logs.d.ts +31 -0
  115. package/dist/websocket/handlers/logs.js +121 -0
  116. package/dist/websocket/messages.d.ts +26 -0
  117. package/dist/websocket/messages.js +9 -0
  118. package/dist/websocket/types.d.ts +6 -5
  119. package/package.json +48 -49
package/dist/app.js CHANGED
@@ -125,7 +125,7 @@ export default async function createApp() {
125
125
  'https://avatars.githubusercontent.com',
126
126
  ],
127
127
  mediaSrc: ["'self'"],
128
- connectSrc: ["'self'", 'https://*'],
128
+ connectSrc: ["'self'", 'https://*', 'wss://*'],
129
129
  },
130
130
  }, getConfigFromEnv('CONTENT_SECURITY_POLICY_'))));
131
131
  if (env['HSTS_ENABLED']) {
@@ -172,7 +172,7 @@ export class LDAPAuthDriver extends AuthDriver {
172
172
  }
173
173
  const logger = useLogger();
174
174
  await this.validateBindClient();
175
- const { userDn, userScope, userAttribute, groupDn, groupScope, groupAttribute, defaultRoleId } = this.config;
175
+ const { userDn, userScope, userAttribute, groupDn, groupScope, groupAttribute, defaultRoleId, syncUserInfo } = this.config;
176
176
  const userInfo = await this.fetchUserInfo(userDn, new ldap.EqualityFilter({
177
177
  attribute: userAttribute ?? 'cn',
178
178
  value: payload['identifier'],
@@ -201,11 +201,22 @@ export class LDAPAuthDriver extends AuthDriver {
201
201
  if (userId) {
202
202
  // Run hook so the end user has the chance to augment the
203
203
  // user that is about to be updated
204
- let updatedUserPayload = await emitter.emitFilter(`auth.update`, {}, { identifier: userInfo.dn, provider: this.config['provider'], providerPayload: { userInfo, userRole } }, { database: getDatabase(), schema: this.schema, accountability: null });
204
+ let emitPayload = {};
205
205
  // Only sync roles if the AD groups are configured
206
206
  if (groupDn) {
207
- updatedUserPayload = { role: userRole?.id ?? defaultRoleId ?? null, ...updatedUserPayload };
207
+ emitPayload = {
208
+ role: userRole?.id ?? defaultRoleId ?? null,
209
+ };
208
210
  }
211
+ if (syncUserInfo) {
212
+ emitPayload = {
213
+ ...emitPayload,
214
+ first_name: userInfo.firstName,
215
+ last_name: userInfo.lastName,
216
+ email: userInfo.email,
217
+ };
218
+ }
219
+ const updatedUserPayload = await emitter.emitFilter(`auth.update`, emitPayload, { identifier: userInfo.dn, provider: this.config['provider'], providerPayload: { userInfo, userRole } }, { database: getDatabase(), schema: this.schema, accountability: null });
209
220
  // Update user to update properties that might have changed
210
221
  await this.usersService.updateOne(userId, updatedUserPayload);
211
222
  return userId;
@@ -106,7 +106,7 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
106
106
  }
107
107
  // Flatten response to support dot indexes
108
108
  userInfo = flatten(userInfo);
109
- const { provider, emailKey, identifierKey, allowPublicRegistration } = this.config;
109
+ const { provider, emailKey, identifierKey, allowPublicRegistration, syncUserInfo } = this.config;
110
110
  const email = userInfo[emailKey ?? 'email'] ? String(userInfo[emailKey ?? 'email']) : undefined;
111
111
  // Fallback to email if explicit identifier not found
112
112
  const identifier = userInfo[identifierKey] ? String(userInfo[identifierKey]) : email;
@@ -127,7 +127,18 @@ export class OAuth2AuthDriver extends LocalAuthDriver {
127
127
  if (userId) {
128
128
  // Run hook so the end user has the chance to augment the
129
129
  // user that is about to be updated
130
- const updatedUserPayload = await emitter.emitFilter(`auth.update`, { auth_data: userPayload.auth_data }, {
130
+ let emitPayload = {
131
+ auth_data: userPayload.auth_data,
132
+ };
133
+ if (syncUserInfo) {
134
+ emitPayload = {
135
+ ...emitPayload,
136
+ first_name: userPayload.first_name,
137
+ last_name: userPayload.last_name,
138
+ email: userPayload.email,
139
+ };
140
+ }
141
+ const updatedUserPayload = await emitter.emitFilter(`auth.update`, emitPayload, {
131
142
  identifier,
132
143
  provider: this.config['provider'],
133
144
  providerPayload: { accessToken: tokenSet.access_token, userInfo },
@@ -125,7 +125,7 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
125
125
  }
126
126
  // Flatten response to support dot indexes
127
127
  userInfo = flatten(userInfo);
128
- const { provider, identifierKey, allowPublicRegistration, requireVerifiedEmail } = this.config;
128
+ const { provider, identifierKey, allowPublicRegistration, requireVerifiedEmail, syncUserInfo } = this.config;
129
129
  const email = userInfo['email'] ? String(userInfo['email']) : undefined;
130
130
  // Fallback to email if explicit identifier not found
131
131
  const identifier = userInfo[identifierKey ?? 'sub'] ? String(userInfo[identifierKey ?? 'sub']) : email;
@@ -146,7 +146,18 @@ export class OpenIDAuthDriver extends LocalAuthDriver {
146
146
  if (userId) {
147
147
  // Run hook so the end user has the chance to augment the
148
148
  // user that is about to be updated
149
- const updatedUserPayload = await emitter.emitFilter(`auth.update`, { auth_data: userPayload.auth_data }, {
149
+ let emitPayload = {
150
+ auth_data: userPayload.auth_data,
151
+ };
152
+ if (syncUserInfo) {
153
+ emitPayload = {
154
+ ...emitPayload,
155
+ first_name: userPayload.first_name,
156
+ last_name: userPayload.last_name,
157
+ email: userPayload.email,
158
+ };
159
+ }
160
+ const updatedUserPayload = await emitter.emitFilter(`auth.update`, emitPayload, {
150
161
  identifier,
151
162
  provider: this.config['provider'],
152
163
  providerPayload: { accessToken: tokenSet.access_token, userInfo },
package/dist/cache.d.ts CHANGED
@@ -13,7 +13,7 @@ export declare function clearSystemCache(opts?: {
13
13
  }): Promise<void>;
14
14
  export declare function setSystemCache(key: string, value: any, ttl?: number): Promise<void>;
15
15
  export declare function getSystemCache(key: string): Promise<Record<string, any>>;
16
- export declare function setSchemaCache(schema: SchemaOverview): Promise<void>;
17
- export declare function getSchemaCache(): Promise<SchemaOverview | undefined>;
16
+ export declare function setLocalSchemaCache(schema: SchemaOverview): Promise<void>;
17
+ export declare function getLocalSchemaCache(): Promise<SchemaOverview | undefined>;
18
18
  export declare function setCacheValue(cache: Keyv, key: string, value: Record<string, any> | Record<string, any>[], ttl?: number): Promise<void>;
19
19
  export declare function getCacheValue(cache: Keyv, key: string): Promise<any>;
package/dist/cache.js CHANGED
@@ -1,13 +1,13 @@
1
1
  import { useEnv } from '@directus/env';
2
- import Keyv from 'keyv';
2
+ import Keyv, {} from 'keyv';
3
3
  import { useBus } from './bus/index.js';
4
4
  import { useLogger } from './logger/index.js';
5
+ import { clearCache as clearPermissionCache } from './permissions/cache.js';
5
6
  import { redisConfigAvailable } from './redis/index.js';
6
7
  import { compress, decompress } from './utils/compress.js';
7
8
  import { getConfigFromEnv } from './utils/get-config-from-env.js';
8
9
  import { getMilliseconds } from './utils/get-milliseconds.js';
9
10
  import { validateEnv } from './utils/validate-env.js';
10
- import { clearCache as clearPermissionCache } from './permissions/cache.js';
11
11
  import { createRequire } from 'node:module';
12
12
  const logger = useLogger();
13
13
  const env = useEnv();
@@ -75,11 +75,11 @@ export async function getSystemCache(key) {
75
75
  const { systemCache } = getCache();
76
76
  return await getCacheValue(systemCache, key);
77
77
  }
78
- export async function setSchemaCache(schema) {
78
+ export async function setLocalSchemaCache(schema) {
79
79
  const { localSchemaCache } = getCache();
80
80
  await localSchemaCache.set('schema', schema);
81
81
  }
82
- export async function getSchemaCache() {
82
+ export async function getLocalSchemaCache() {
83
83
  const { localSchemaCache } = getCache();
84
84
  return await localSchemaCache.get('schema');
85
85
  }
@@ -106,10 +106,10 @@ function getKeyvInstance(store, ttl, namespaceSuffix) {
106
106
  function getConfig(store = 'memory', ttl, namespaceSuffix = '') {
107
107
  const config = {
108
108
  namespace: `${env['CACHE_NAMESPACE']}${namespaceSuffix}`,
109
- ttl,
109
+ ...(ttl && { ttl }),
110
110
  };
111
111
  if (store === 'redis') {
112
- const KeyvRedis = require('@keyv/redis');
112
+ const { default: KeyvRedis } = require('@keyv/redis');
113
113
  config.store = new KeyvRedis(env['REDIS'] || getConfigFromEnv('REDIS'), { useRedisSets: false });
114
114
  }
115
115
  return config;
@@ -4,18 +4,18 @@ export declare const databaseQuestions: {
4
4
  filepath: string;
5
5
  }) => Record<string, string>)[];
6
6
  mysql2: (({ client }: {
7
- client: Exclude<Driver, 'sqlite3'>;
7
+ client: Exclude<Driver, "sqlite3">;
8
8
  }) => Record<string, any>)[];
9
9
  pg: (({ client }: {
10
- client: Exclude<Driver, 'sqlite3'>;
10
+ client: Exclude<Driver, "sqlite3">;
11
11
  }) => Record<string, any>)[];
12
12
  cockroachdb: (({ client }: {
13
- client: Exclude<Driver, 'sqlite3'>;
13
+ client: Exclude<Driver, "sqlite3">;
14
14
  }) => Record<string, any>)[];
15
15
  oracledb: (({ client }: {
16
- client: Exclude<Driver, 'sqlite3'>;
16
+ client: Exclude<Driver, "sqlite3">;
17
17
  }) => Record<string, any>)[];
18
18
  mssql: (({ client }: {
19
- client: Exclude<Driver, 'sqlite3'>;
19
+ client: Exclude<Driver, "sqlite3">;
20
20
  }) => Record<string, any>)[];
21
21
  };
@@ -1,4 +1,5 @@
1
1
  export declare function apply(snapshotPath: string, options?: {
2
2
  yes: boolean;
3
3
  dryRun: boolean;
4
+ ignoreRules: string;
4
5
  }): Promise<void>;
@@ -11,6 +11,22 @@ import { isNestedMetaUpdate } from '../../../utils/apply-diff.js';
11
11
  import { applySnapshot } from '../../../utils/apply-snapshot.js';
12
12
  import { getSnapshotDiff } from '../../../utils/get-snapshot-diff.js';
13
13
  import { getSnapshot } from '../../../utils/get-snapshot.js';
14
+ function filterSnapshotDiff(snapshot, filters) {
15
+ const filterSet = new Set(filters);
16
+ function shouldKeep(item) {
17
+ if (filterSet.has(item.collection))
18
+ return false;
19
+ if (item.field && filterSet.has(`${item.collection}.${item.field}`))
20
+ return false;
21
+ return true;
22
+ }
23
+ const filteredDiff = {
24
+ collections: snapshot.collections.filter((item) => shouldKeep(item)),
25
+ fields: snapshot.fields.filter((item) => shouldKeep(item)),
26
+ relations: snapshot.relations.filter((item) => shouldKeep(item)),
27
+ };
28
+ return filteredDiff;
29
+ }
14
30
  export async function apply(snapshotPath, options) {
15
31
  const logger = useLogger();
16
32
  const filename = path.resolve(process.cwd(), snapshotPath);
@@ -31,7 +47,10 @@ export async function apply(snapshotPath, options) {
31
47
  snapshot = parseJSON(fileContents);
32
48
  }
33
49
  const currentSnapshot = await getSnapshot({ database });
34
- const snapshotDiff = getSnapshotDiff(currentSnapshot, snapshot);
50
+ let snapshotDiff = getSnapshotDiff(currentSnapshot, snapshot);
51
+ if (options?.ignoreRules) {
52
+ snapshotDiff = filterSnapshotDiff(snapshotDiff, options.ignoreRules.split(','));
53
+ }
35
54
  if (snapshotDiff.collections.length === 0 &&
36
55
  snapshotDiff.fields.length === 0 &&
37
56
  snapshotDiff.relations.length === 0) {
package/dist/cli/index.js CHANGED
@@ -81,6 +81,7 @@ export async function createCli() {
81
81
  .description('Apply a snapshot file to the current database')
82
82
  .option('-y, --yes', `Assume "yes" as answer to all prompts and run non-interactively`)
83
83
  .option('-d, --dry-run', 'Plan and log changes to be applied', false)
84
+ .option('--ignoreRules <value>', `Comma-separated list of collections and or fields to ignore. Format: "products.title,reviews" this will ignore applying changes to the title field in the products collection and the entire reviews collection`)
84
85
  .argument('<path>', 'Path to snapshot file')
85
86
  .action(apply);
86
87
  await emitter.emitInit('cli.after', { program });
@@ -313,7 +313,7 @@ EXTENSIONS_AUTO_RELOAD=false
313
313
  EMAIL_FROM="no-reply@example.com"
314
314
 
315
315
  # What to use to send emails. One of
316
- # sendmail, smtp, mailgun, sendgrid, ses.
316
+ # sendmail, smtp, mailgun, ses.
317
317
  EMAIL_TRANSPORT="sendmail"
318
318
  EMAIL_SENDMAIL_NEW_LINE="unix"
319
319
  EMAIL_SENDMAIL_PATH="/usr/sbin/sendmail"
@@ -340,6 +340,3 @@ EMAIL_SENDMAIL_PATH="/usr/sbin/sendmail"
340
340
  ## Email (Mailgun Transport)
341
341
  # EMAIL_MAILGUN_API_KEY="key-1234123412341234"
342
342
  # EMAIL_MAILGUN_DOMAIN="a domain name from https://app.mailgun.com/app/sending/domains"
343
-
344
- ## Email (SendGrid Transport)
345
- # EMAIL_SENDGRID_API_KEY="key-1234123412341234"
@@ -23,3 +23,4 @@ export declare const RESUMABLE_UPLOADS: {
23
23
  EXPIRATION_TIME: number;
24
24
  SCHEDULE: string;
25
25
  };
26
+ export declare const ALLOWED_DB_DEFAULT_FUNCTIONS: string[];
package/dist/constants.js CHANGED
@@ -87,3 +87,4 @@ export const RESUMABLE_UPLOADS = {
87
87
  EXPIRATION_TIME: getMilliseconds(env['TUS_UPLOAD_EXPIRATION'], 600_000 /* 10min */),
88
88
  SCHEDULE: String(env['TUS_CLEANUP_SCHEDULE']),
89
89
  };
90
+ export const ALLOWED_DB_DEFAULT_FUNCTIONS = ['gen_random_uuid()'];
@@ -6,12 +6,14 @@ import * as geometryHelpers from './geometry/index.js';
6
6
  import * as schemaHelpers from './schema/index.js';
7
7
  import * as sequenceHelpers from './sequence/index.js';
8
8
  import * as numberHelpers from './number/index.js';
9
+ import * as nullableUpdateHelper from './nullable-update/index.js';
9
10
  export declare function getHelpers(database: Knex): {
10
11
  date: dateHelpers.postgres | dateHelpers.oracle | dateHelpers.mysql | dateHelpers.mssql | dateHelpers.sqlite;
11
12
  st: geometryHelpers.postgres | geometryHelpers.mssql | geometryHelpers.mysql | geometryHelpers.sqlite | geometryHelpers.oracle | geometryHelpers.redshift;
12
13
  schema: schemaHelpers.cockroachdb | schemaHelpers.mssql | schemaHelpers.mysql | schemaHelpers.postgres | schemaHelpers.sqlite | schemaHelpers.oracle | schemaHelpers.redshift;
13
14
  sequence: sequenceHelpers.mysql | sequenceHelpers.postgres;
14
15
  number: numberHelpers.cockroachdb | numberHelpers.mssql | numberHelpers.postgres | numberHelpers.sqlite | numberHelpers.oracle;
16
+ nullableUpdate: nullableUpdateHelper.postgres | nullableUpdateHelper.oracle;
15
17
  };
16
18
  export declare function getFunctions(database: Knex, schema: SchemaOverview): fnHelpers.postgres | fnHelpers.mssql | fnHelpers.mysql | fnHelpers.sqlite | fnHelpers.oracle;
17
19
  export type Helpers = ReturnType<typeof getHelpers>;
@@ -5,6 +5,7 @@ import * as geometryHelpers from './geometry/index.js';
5
5
  import * as schemaHelpers from './schema/index.js';
6
6
  import * as sequenceHelpers from './sequence/index.js';
7
7
  import * as numberHelpers from './number/index.js';
8
+ import * as nullableUpdateHelper from './nullable-update/index.js';
8
9
  export function getHelpers(database) {
9
10
  const client = getDatabaseClient(database);
10
11
  return {
@@ -13,6 +14,7 @@ export function getHelpers(database) {
13
14
  schema: new schemaHelpers[client](database),
14
15
  sequence: new sequenceHelpers[client](database),
15
16
  number: new numberHelpers[client](database),
17
+ nullableUpdate: new nullableUpdateHelper[client](database),
16
18
  };
17
19
  }
18
20
  export function getFunctions(database, schema) {
@@ -0,0 +1,3 @@
1
+ import { NullableFieldUpdateHelper } from '../types.js';
2
+ export declare class NullableFieldUpdateHelperDefault extends NullableFieldUpdateHelper {
3
+ }
@@ -0,0 +1,3 @@
1
+ import { NullableFieldUpdateHelper } from '../types.js';
2
+ export class NullableFieldUpdateHelperDefault extends NullableFieldUpdateHelper {
3
+ }
@@ -0,0 +1,12 @@
1
+ import type { Column } from '@directus/schema';
2
+ import type { Field, RawField } from '@directus/types';
3
+ import type { Knex } from 'knex';
4
+ import { NullableFieldUpdateHelper } from '../types.js';
5
+ /**
6
+ * Oracle throws an error when overwriting the nullable option with same value.
7
+ * Therefore we need to check if the nullable option has changed and only then apply it.
8
+ * The default value can be set regardless of the previous value.
9
+ */
10
+ export declare class NullableFieldUpdateHelperOracle extends NullableFieldUpdateHelper {
11
+ updateNullableValue(column: Knex.ColumnBuilder, field: RawField | Field, existing: Column): void;
12
+ }
@@ -0,0 +1,16 @@
1
+ import { NullableFieldUpdateHelper } from '../types.js';
2
+ /**
3
+ * Oracle throws an error when overwriting the nullable option with same value.
4
+ * Therefore we need to check if the nullable option has changed and only then apply it.
5
+ * The default value can be set regardless of the previous value.
6
+ */
7
+ export class NullableFieldUpdateHelperOracle extends NullableFieldUpdateHelper {
8
+ updateNullableValue(column, field, existing) {
9
+ if (field.schema?.is_nullable === false && existing.is_nullable === true) {
10
+ column.notNullable();
11
+ }
12
+ else if (field.schema?.is_nullable === true && existing.is_nullable === false) {
13
+ column.nullable();
14
+ }
15
+ }
16
+ }
@@ -0,0 +1,7 @@
1
+ export { NullableFieldUpdateHelperOracle as oracle } from './dialects/oracle.js';
2
+ export { NullableFieldUpdateHelperDefault as postgres } from './dialects/default.js';
3
+ export { NullableFieldUpdateHelperDefault as mysql } from './dialects/default.js';
4
+ export { NullableFieldUpdateHelperDefault as cockroachdb } from './dialects/default.js';
5
+ export { NullableFieldUpdateHelperDefault as redshift } from './dialects/default.js';
6
+ export { NullableFieldUpdateHelperDefault as sqlite } from './dialects/default.js';
7
+ export { NullableFieldUpdateHelperDefault as mssql } from './dialects/default.js';
@@ -0,0 +1,7 @@
1
+ export { NullableFieldUpdateHelperOracle as oracle } from './dialects/oracle.js';
2
+ export { NullableFieldUpdateHelperDefault as postgres } from './dialects/default.js';
3
+ export { NullableFieldUpdateHelperDefault as mysql } from './dialects/default.js';
4
+ export { NullableFieldUpdateHelperDefault as cockroachdb } from './dialects/default.js';
5
+ export { NullableFieldUpdateHelperDefault as redshift } from './dialects/default.js';
6
+ export { NullableFieldUpdateHelperDefault as sqlite } from './dialects/default.js';
7
+ export { NullableFieldUpdateHelperDefault as mssql } from './dialects/default.js';
@@ -0,0 +1,7 @@
1
+ import type { Knex } from 'knex';
2
+ import { DatabaseHelper } from '../types.js';
3
+ import type { Column } from '@directus/schema';
4
+ import type { Field, RawField } from '@directus/types';
5
+ export declare class NullableFieldUpdateHelper extends DatabaseHelper {
6
+ updateNullableValue(column: Knex.ColumnBuilder, field: RawField | Field, existing: Column): void;
7
+ }
@@ -0,0 +1,12 @@
1
+ import { DatabaseHelper } from '../types.js';
2
+ export class NullableFieldUpdateHelper extends DatabaseHelper {
3
+ updateNullableValue(column, field, existing) {
4
+ const isNullable = field.schema?.is_nullable ?? existing?.is_nullable ?? true;
5
+ if (isNullable) {
6
+ column.nullable();
7
+ }
8
+ else {
9
+ column.notNullable();
10
+ }
11
+ }
12
+ }
@@ -1,9 +1,11 @@
1
1
  import type { KNEX_TYPES } from '@directus/constants';
2
- import type { Options, Sql } from '../types.js';
2
+ import { type Knex } from 'knex';
3
+ import type { Options, SortRecord, Sql } from '../types.js';
3
4
  import { SchemaHelper } from '../types.js';
4
5
  export declare class SchemaHelperCockroachDb extends SchemaHelper {
5
6
  changeToType(table: string, column: string, type: (typeof KNEX_TYPES)[number], options?: Options): Promise<void>;
6
7
  constraintName(existingName: string): string;
7
8
  getDatabaseSize(): Promise<number | null>;
8
9
  preprocessBindings(queryParams: Sql): Sql;
10
+ addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasMultiRelationalSort: boolean): void;
9
11
  }
@@ -1,3 +1,4 @@
1
+ import {} from 'knex';
1
2
  import { SchemaHelper } from '../types.js';
2
3
  import { useEnv } from '@directus/env';
3
4
  import { preprocessBindings } from '../utils/preprocess-bindings.js';
@@ -31,4 +32,20 @@ export class SchemaHelperCockroachDb extends SchemaHelper {
31
32
  preprocessBindings(queryParams) {
32
33
  return preprocessBindings(queryParams, { format: (index) => `$${index + 1}` });
33
34
  }
35
+ addInnerSortFieldsToGroupBy(groupByFields, sortRecords, hasMultiRelationalSort) {
36
+ if (hasMultiRelationalSort) {
37
+ /*
38
+ Cockroach allows aliases to be used in the GROUP BY clause and only needs columns in the GROUP BY clause that
39
+ are not functionally dependent on the primary key.
40
+
41
+ > You can group columns by an alias (i.e., a label assigned to the column with an AS clause) rather than the column name.
42
+
43
+ > If aggregate groups are created on a full primary key, any column in the table can be selected as a target_elem,
44
+ or specified in a HAVING clause.
45
+
46
+ https://www.cockroachlabs.com/docs/stable/select-clause#parameters
47
+ */
48
+ groupByFields.push(...sortRecords.map(({ alias }) => alias));
49
+ }
50
+ }
34
51
  }
@@ -1,9 +1,10 @@
1
1
  import type { Knex } from 'knex';
2
- import { SchemaHelper, type Sql } from '../types.js';
2
+ import { SchemaHelper, type SortRecord, type Sql } from '../types.js';
3
3
  export declare class SchemaHelperMSSQL extends SchemaHelper {
4
4
  applyLimit(rootQuery: Knex.QueryBuilder, limit: number): void;
5
5
  applyOffset(rootQuery: Knex.QueryBuilder, offset: number): void;
6
6
  formatUUID(uuid: string): string;
7
7
  getDatabaseSize(): Promise<number | null>;
8
8
  preprocessBindings(queryParams: Sql): Sql;
9
+ addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], _hasMultiRelationalSort: boolean): void;
9
10
  }
@@ -30,4 +30,24 @@ export class SchemaHelperMSSQL extends SchemaHelper {
30
30
  preprocessBindings(queryParams) {
31
31
  return preprocessBindings(queryParams, { format: (index) => `@p${index}` });
32
32
  }
33
+ addInnerSortFieldsToGroupBy(groupByFields, sortRecords, _hasMultiRelationalSort) {
34
+ /*
35
+ MSSQL requires all selected columns that are not aggregated over are to be present in the GROUP BY clause
36
+
37
+ > When the select list has no aggregations, each column in the select list must be included in the GROUP BY list.
38
+
39
+ https://learn.microsoft.com/en-us/sql/t-sql/queries/select-group-by-transact-sql?view=sql-server-ver16#g-syntax-variations-for-group-by
40
+
41
+ MSSQL does not support aliases in the GROUP BY clause
42
+
43
+ > The column expression cannot contain:
44
+ A column alias that is defined in the SELECT list. It can use a column alias for a derived table that is defined
45
+ in the FROM clause.
46
+
47
+ https://learn.microsoft.com/en-us/sql/t-sql/queries/select-group-by-transact-sql?view=sql-server-ver16
48
+ */
49
+ if (sortRecords.length > 0) {
50
+ groupByFields.push(...sortRecords.map(({ column }) => column));
51
+ }
52
+ }
33
53
  }
@@ -1,6 +1,7 @@
1
1
  import type { Knex } from 'knex';
2
- import { SchemaHelper } from '../types.js';
2
+ import { SchemaHelper, type SortRecord } from '../types.js';
3
3
  export declare class SchemaHelperMySQL extends SchemaHelper {
4
4
  applyMultiRelationalSort(knex: Knex, dbQuery: Knex.QueryBuilder, table: string, primaryKey: string, orderByString: string, orderByFields: Knex.Raw[]): Knex.QueryBuilder;
5
5
  getDatabaseSize(): Promise<number | null>;
6
+ addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasMultiRelationalSort: boolean): void;
6
7
  }
@@ -28,4 +28,37 @@ export class SchemaHelperMySQL extends SchemaHelper {
28
28
  return null;
29
29
  }
30
30
  }
31
+ addInnerSortFieldsToGroupBy(groupByFields, sortRecords, hasMultiRelationalSort) {
32
+ if (hasMultiRelationalSort) {
33
+ /*
34
+ ** MySQL **
35
+
36
+ MySQL only requires all selected sort columns that are not functionally dependent on the primary key to be included.
37
+
38
+ > If the ONLY_FULL_GROUP_BY SQL mode is enabled (which it is by default),
39
+ MySQL rejects queries for which the select list, HAVING condition, or ORDER BY list refer to
40
+ nonaggregated columns that are neither named in the GROUP BY clause nor are functionally dependent on them.
41
+
42
+ https://dev.mysql.com/doc/refman/8.4/en/group-by-handling.html
43
+
44
+ MySQL allows aliases to be used in the GROUP BY clause
45
+
46
+ > You can use the alias in GROUP BY, ORDER BY, or HAVING clauses to refer to the column:
47
+
48
+ https://dev.mysql.com/doc/refman/8.4/en/problems-with-alias.html
49
+
50
+ ** MariaDB **
51
+
52
+ MariaDB does not document how it supports functional dependent columns in GROUP BY clauses.
53
+ But testing shows that it does support the same features as MySQL in this area.
54
+
55
+ MariaDB allows aliases to be used in the GROUP BY clause
56
+
57
+ > The GROUP BY expression can be a computed value, and can refer back to an identifer specified with AS.
58
+
59
+ https://mariadb.com/kb/en/group-by/#group-by-examples
60
+ */
61
+ groupByFields.push(...sortRecords.map(({ alias }) => alias));
62
+ }
63
+ }
31
64
  }
@@ -1,6 +1,7 @@
1
1
  import type { KNEX_TYPES } from '@directus/constants';
2
2
  import type { Field, Relation, Type } from '@directus/types';
3
- import type { Options, Sql } from '../types.js';
3
+ import type { Knex } from 'knex';
4
+ import type { Options, SortRecord, Sql } from '../types.js';
4
5
  import { SchemaHelper } from '../types.js';
5
6
  export declare class SchemaHelperOracle extends SchemaHelper {
6
7
  changeToType(table: string, column: string, type: (typeof KNEX_TYPES)[number], options?: Options): Promise<void>;
@@ -9,4 +10,5 @@ export declare class SchemaHelperOracle extends SchemaHelper {
9
10
  processFieldType(field: Field): Type;
10
11
  getDatabaseSize(): Promise<number | null>;
11
12
  preprocessBindings(queryParams: Sql): Sql;
13
+ addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], _hasMultiRelationalSort: boolean): void;
12
14
  }
@@ -42,4 +42,25 @@ export class SchemaHelperOracle extends SchemaHelper {
42
42
  preprocessBindings(queryParams) {
43
43
  return preprocessBindings(queryParams, { format: (index) => `:${index + 1}` });
44
44
  }
45
+ addInnerSortFieldsToGroupBy(groupByFields, sortRecords, _hasMultiRelationalSort) {
46
+ /*
47
+ Oracle requires all selected columns that are not aggregated over to be present in the GROUP BY clause
48
+ aliases can not be used before version 23c.
49
+
50
+ > If you also specify a group_by_clause in this statement, then this select list can contain only the following
51
+ types of expressions:
52
+ * Constants
53
+ * Aggregate functions and the functions USER, UID, and SYSDATE
54
+ * Expressions identical to those in the group_by_clause. If the group_by_clause is in a subquery,
55
+ then all columns in the select list of the subquery must match the GROUP BY columns in the subquery.
56
+ If the select list and GROUP BY columns of a top-level query or of a subquery do not match,
57
+ then the statement results in ORA-00979.
58
+ * Expressions involving the preceding expressions that evaluate to the same value for all rows in a group
59
+
60
+ https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/SELECT.html
61
+ */
62
+ if (sortRecords.length > 0) {
63
+ groupByFields.push(...sortRecords.map(({ column }) => column));
64
+ }
65
+ }
45
66
  }
@@ -1,5 +1,7 @@
1
- import { SchemaHelper, type Sql } from '../types.js';
1
+ import type { Knex } from 'knex';
2
+ import { SchemaHelper, type SortRecord, type Sql } from '../types.js';
2
3
  export declare class SchemaHelperPostgres extends SchemaHelper {
3
4
  getDatabaseSize(): Promise<number | null>;
4
5
  preprocessBindings(queryParams: Sql): Sql;
6
+ addInnerSortFieldsToGroupBy(groupByFields: (string | Knex.Raw)[], sortRecords: SortRecord[], hasMultiRelationalSort: boolean): void;
5
7
  }
@@ -15,4 +15,27 @@ export class SchemaHelperPostgres extends SchemaHelper {
15
15
  preprocessBindings(queryParams) {
16
16
  return preprocessBindings(queryParams, { format: (index) => `$${index + 1}` });
17
17
  }
18
+ addInnerSortFieldsToGroupBy(groupByFields, sortRecords, hasMultiRelationalSort) {
19
+ if (hasMultiRelationalSort) {
20
+ /*
21
+ Postgres only requires selected columns that are not functionally dependent on the primary key to be
22
+ included in the GROUP BY clause. Since the results are already grouped by the primary key, we don't need to
23
+ always include the sort columns in the GROUP BY but only if there is a multi relational sort involved, eg.
24
+ a sort column that comes from a related M2O relation.
25
+
26
+ > When GROUP BY is present, or any aggregate functions are present, it is not valid for the SELECT list
27
+ expressions to refer to ungrouped columns except within aggregate functions or when the ungrouped column is
28
+ functionally dependent on the grouped columns, since there would otherwise be more than one possible value to
29
+ return for an ungrouped column.
30
+ https://www.postgresql.org/docs/current/sql-select.html
31
+
32
+ Postgres allows aliases to be used in the GROUP BY clause
33
+
34
+ > In strict SQL, GROUP BY can only group by columns of the source table but PostgreSQL extends this to also allow
35
+ GROUP BY to group by columns in the select list.
36
+ https://www.postgresql.org/docs/16/queries-table-expressions.html#QUERIES-GROUP
37
+ */
38
+ groupByFields.push(...sortRecords.map(({ alias }) => alias));
39
+ }
40
+ }
18
41
  }
@@ -3,4 +3,5 @@ export declare class SchemaHelperSQLite extends SchemaHelper {
3
3
  preColumnChange(): Promise<boolean>;
4
4
  postColumnChange(): Promise<void>;
5
5
  getDatabaseSize(): Promise<number | null>;
6
+ addInnerSortFieldsToGroupBy(): void;
6
7
  }
@@ -19,4 +19,7 @@ export class SchemaHelperSQLite extends SchemaHelper {
19
19
  return null;
20
20
  }
21
21
  }
22
+ addInnerSortFieldsToGroupBy() {
23
+ // SQLite does not need any special handling for inner query sort columns
24
+ }
22
25
  }
@@ -12,6 +12,10 @@ export type Sql = {
12
12
  sql: string;
13
13
  bindings: readonly Knex.Value[];
14
14
  };
15
+ export type SortRecord = {
16
+ alias: string;
17
+ column: Knex.Raw;
18
+ };
15
19
  export declare abstract class SchemaHelper extends DatabaseHelper {
16
20
  isOneOfClients(clients: DatabaseClient[]): boolean;
17
21
  changeNullable(table: string, column: string, nullable: boolean): Promise<void>;
@@ -32,4 +36,5 @@ export declare abstract class SchemaHelper extends DatabaseHelper {
32
36
  */
33
37
  getDatabaseSize(): Promise<number | null>;
34
38
  preprocessBindings(queryParams: Sql): Sql;
39
+ addInnerSortFieldsToGroupBy(_groupByFields: (string | Knex.Raw)[], _sortRecords: SortRecord[], _hasMultiRelationalSort: boolean): void;
35
40
  }
@@ -97,4 +97,7 @@ export class SchemaHelper extends DatabaseHelper {
97
97
  preprocessBindings(queryParams) {
98
98
  return queryParams;
99
99
  }
100
+ addInnerSortFieldsToGroupBy(_groupByFields, _sortRecords, _hasMultiRelationalSort) {
101
+ // no-op by default
102
+ }
100
103
  }