@directus/api 22.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.
@@ -1,20 +1,21 @@
1
+ import type { Driver } from '../../../types/index.js';
1
2
  export declare const databaseQuestions: {
2
3
  sqlite3: (({ filepath }: {
3
4
  filepath: string;
4
5
  }) => Record<string, string>)[];
5
- mysql: (({ client }: {
6
- client: string;
6
+ mysql2: (({ client }: {
7
+ client: Exclude<Driver, 'sqlite3'>;
7
8
  }) => Record<string, any>)[];
8
9
  pg: (({ client }: {
9
- client: string;
10
+ client: Exclude<Driver, 'sqlite3'>;
10
11
  }) => Record<string, any>)[];
11
12
  cockroachdb: (({ client }: {
12
- client: string;
13
+ client: Exclude<Driver, 'sqlite3'>;
13
14
  }) => Record<string, any>)[];
14
15
  oracledb: (({ client }: {
15
- client: string;
16
+ client: Exclude<Driver, 'sqlite3'>;
16
17
  }) => Record<string, any>)[];
17
18
  mssql: (({ client }: {
18
- client: string;
19
+ client: Exclude<Driver, 'sqlite3'>;
19
20
  }) => Record<string, any>)[];
20
21
  };
@@ -19,7 +19,7 @@ const port = ({ client }) => ({
19
19
  const ports = {
20
20
  pg: 5432,
21
21
  cockroachdb: 26257,
22
- mysql: 3306,
22
+ mysql2: 3306,
23
23
  oracledb: 1521,
24
24
  mssql: 1433,
25
25
  };
@@ -57,7 +57,7 @@ const ssl = () => ({
57
57
  });
58
58
  export const databaseQuestions = {
59
59
  sqlite3: [filename],
60
- mysql: [host, port, database, user, password],
60
+ mysql2: [host, port, database, user, password],
61
61
  pg: [host, port, database, user, password, ssl],
62
62
  cockroachdb: [host, port, database, user, password, ssl],
63
63
  oracledb: [host, port, database, user, password],
@@ -1,3 +1,3 @@
1
+ import type { Driver } from '../../../types/index.js';
1
2
  import type { Credentials } from '../create-db-connection.js';
2
- import type { drivers } from '../drivers.js';
3
- export default function createEnv(client: keyof typeof drivers, credentials: Credentials, directory: string): Promise<void>;
3
+ export default function createEnv(client: Driver, credentials: Credentials, directory: string): Promise<void>;
@@ -14,12 +14,14 @@ const liquidEngine = new Liquid({
14
14
  });
15
15
  export default async function createEnv(client, credentials, directory) {
16
16
  const { nanoid } = await import('nanoid');
17
+ // For backwards-compatibility, DB_CLIENT is still 'mysql'
18
+ const dbClient = client === 'mysql2' ? 'mysql' : client;
17
19
  const config = {
18
20
  security: {
19
21
  SECRET: nanoid(32),
20
22
  },
21
23
  database: {
22
- DB_CLIENT: client,
24
+ DB_CLIENT: dbClient,
23
25
  },
24
26
  };
25
27
  for (const [key, value] of Object.entries(credentials)) {
@@ -1,7 +1,7 @@
1
1
  export const drivers = {
2
2
  pg: 'PostgreSQL / Redshift',
3
3
  cockroachdb: 'CockroachDB (Beta)',
4
- mysql: 'MySQL / MariaDB / Aurora',
4
+ mysql2: 'MySQL / MariaDB / Aurora',
5
5
  sqlite3: 'SQLite',
6
6
  mssql: 'Microsoft SQL Server',
7
7
  oracledb: 'Oracle Database',
@@ -8,10 +8,10 @@ import * as sequenceHelpers from './sequence/index.js';
8
8
  import * as numberHelpers from './number/index.js';
9
9
  export declare function getHelpers(database: Knex): {
10
10
  date: dateHelpers.postgres | dateHelpers.oracle | dateHelpers.mysql | dateHelpers.mssql | dateHelpers.sqlite;
11
- st: geometryHelpers.mysql | geometryHelpers.postgres | geometryHelpers.mssql | geometryHelpers.sqlite | geometryHelpers.oracle | geometryHelpers.redshift;
12
- schema: schemaHelpers.mysql | schemaHelpers.cockroachdb | schemaHelpers.mssql | schemaHelpers.postgres | schemaHelpers.sqlite | schemaHelpers.oracle | schemaHelpers.redshift;
11
+ st: geometryHelpers.postgres | geometryHelpers.mssql | geometryHelpers.mysql | geometryHelpers.sqlite | geometryHelpers.oracle | geometryHelpers.redshift;
12
+ schema: schemaHelpers.cockroachdb | schemaHelpers.mssql | schemaHelpers.mysql | schemaHelpers.postgres | schemaHelpers.sqlite | schemaHelpers.oracle | schemaHelpers.redshift;
13
13
  sequence: sequenceHelpers.mysql | sequenceHelpers.postgres;
14
14
  number: numberHelpers.cockroachdb | numberHelpers.mssql | numberHelpers.postgres | numberHelpers.sqlite | numberHelpers.oracle;
15
15
  };
16
- export declare function getFunctions(database: Knex, schema: SchemaOverview): fnHelpers.mysql | fnHelpers.postgres | fnHelpers.mssql | fnHelpers.sqlite | fnHelpers.oracle;
16
+ export declare function getFunctions(database: Knex, schema: SchemaOverview): fnHelpers.postgres | fnHelpers.mssql | fnHelpers.mysql | fnHelpers.sqlite | fnHelpers.oracle;
17
17
  export type Helpers = ReturnType<typeof getHelpers>;
@@ -1,5 +1,6 @@
1
1
  import { createInspector } from '@directus/schema';
2
2
  import { useLogger } from '../../logger/index.js';
3
+ import { getDatabaseClient } from '../index.js';
3
4
  /**
4
5
  * Things to keep in mind:
5
6
  *
@@ -99,7 +100,7 @@ export async function up(knex) {
99
100
  * MySQL won't delete the index when you drop the foreign key constraint. Gotta make
100
101
  * sure to clean those up as well
101
102
  */
102
- if (knex.client.constructor.name === 'Client_MySQL') {
103
+ if (getDatabaseClient(knex) === 'mysql') {
103
104
  try {
104
105
  await knex.schema.alterTable(update.table, (table) => {
105
106
  // Knex uses a default convention for index names: `table_column_type`
@@ -140,7 +141,7 @@ export async function down(knex) {
140
141
  * MySQL won't delete the index when you drop the foreign key constraint. Gotta make
141
142
  * sure to clean those up as well
142
143
  */
143
- if (knex.client.constructor.name === 'Client_MySQL') {
144
+ if (getDatabaseClient(knex) === 'mysql') {
144
145
  try {
145
146
  await knex.schema.alterTable(update.table, (table) => {
146
147
  // Knex uses a default convention for index names: `table_column_type`
@@ -1,9 +1,8 @@
1
1
  import { createInspector } from '@directus/schema';
2
2
  import { useLogger } from '../../logger/index.js';
3
- import { getHelpers } from '../helpers/index.js';
3
+ import { getDatabaseClient } from '../index.js';
4
4
  export async function up(knex) {
5
- const helper = getHelpers(knex).schema;
6
- const isMysql = helper.isOneOfClients(['mysql']);
5
+ const isMysql = getDatabaseClient(knex) === 'mysql';
7
6
  if (isMysql) {
8
7
  await dropConstraint(knex);
9
8
  }
@@ -16,8 +15,7 @@ export async function up(knex) {
16
15
  }
17
16
  }
18
17
  export async function down(knex) {
19
- const helper = getHelpers(knex).schema;
20
- const isMysql = helper.isOneOfClients(['mysql']);
18
+ const isMysql = getDatabaseClient(knex) === 'mysql';
21
19
  if (isMysql) {
22
20
  await dropConstraint(knex);
23
21
  }
@@ -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,10 +1,12 @@
1
1
  import { processChunk, toBoolean } from '@directus/utils';
2
2
  import { flatten, intersection, isEqual, merge, omit, uniq } from 'lodash-es';
3
3
  import { randomUUID } from 'node:crypto';
4
+ import { useLogger } from '../../logger/index.js';
4
5
  import { fetchPermissions } from '../../permissions/lib/fetch-permissions.js';
5
6
  import { fetchPolicies } from '../../permissions/lib/fetch-policies.js';
6
7
  import { fetchRolesTree } from '../../permissions/lib/fetch-roles-tree.js';
7
8
  import { getSchema } from '../../utils/get-schema.js';
9
+ import { getSchemaInspector } from '../index.js';
8
10
  // Adapted from https://github.com/directus/directus/blob/141b8adbf4dd8e06530a7929f34e3fc68a522053/api/src/utils/merge-permissions.ts#L4
9
11
  export function mergePermissions(strategy, ...permissions) {
10
12
  const allPermissions = flatten(permissions);
@@ -129,6 +131,7 @@ async function fetchRoleAccess(roles, context) {
129
131
  */
130
132
  const PUBLIC_POLICY_ID = 'abf8a154-5b1c-4a46-ac9c-7300570f4f17';
131
133
  export async function up(knex) {
134
+ const logger = useLogger();
132
135
  /////////////////////////////////////////////////////////////////////////////////////////////////
133
136
  // If the policies table already exists the migration has already run
134
137
  if (await knex.schema.hasTable('directus_policies')) {
@@ -184,9 +187,20 @@ export async function up(knex) {
184
187
  // Link permissions to policies instead of roles
185
188
  await knex.schema.alterTable('directus_permissions', (table) => {
186
189
  table.uuid('policy').references('directus_policies.id').onDelete('CASCADE');
187
- // Drop the foreign key constraint here in order to update `null` role to public policy ID
188
- table.dropForeign('role');
189
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
+ }
190
204
  await knex('directus_permissions')
191
205
  .update({
192
206
  role: PUBLIC_POLICY_ID,
package/dist/server.js CHANGED
@@ -13,6 +13,7 @@ import emitter from './emitter.js';
13
13
  import { useLogger } from './logger/index.js';
14
14
  import { getConfigFromEnv } from './utils/get-config-from-env.js';
15
15
  import { getIPFromReq } from './utils/get-ip-from-req.js';
16
+ import { getAddress } from './utils/get-address.js';
16
17
  import { createSubscriptionController, createWebSocketController, getSubscriptionController, getWebSocketController, } from './websocket/controllers/index.js';
17
18
  import { startWebSocketHandlers } from './websocket/handlers/index.js';
18
19
  export let SERVER_ONLINE = true;
@@ -116,10 +117,22 @@ export async function createServer() {
116
117
  export async function startServer() {
117
118
  const server = await createServer();
118
119
  const host = env['HOST'];
119
- const port = parseInt(env['PORT']);
120
+ const path = env['UNIX_SOCKET_PATH'];
121
+ const port = env['PORT'];
122
+ let listenOptions;
123
+ if (path) {
124
+ listenOptions = { path };
125
+ }
126
+ else {
127
+ listenOptions = {
128
+ host,
129
+ port: parseInt(port || '8055'),
130
+ };
131
+ }
120
132
  server
121
- .listen(port, host, () => {
122
- logger.info(`Server started at http://${host}:${port}`);
133
+ .listen(listenOptions, () => {
134
+ const protocol = server instanceof https.Server ? 'https' : 'http';
135
+ logger.info(`Server started at ${listenOptions.port ? `${protocol}://${getAddress(server)}` : getAddress(server)}`);
123
136
  process.send?.('ready');
124
137
  emitter.emitAction('server.start', { server }, {
125
138
  database: getDatabase(),
@@ -129,7 +142,7 @@ export async function startServer() {
129
142
  })
130
143
  .once('error', (err) => {
131
144
  if (err?.code === 'EADDRINUSE') {
132
- logger.error(`Port ${port} is already in use`);
145
+ logger.error(`${listenOptions.port ? `Port ${listenOptions.port}` : getAddress(server)} is already in use`);
133
146
  process.exit(1);
134
147
  }
135
148
  else {
@@ -1,3 +1,3 @@
1
- export type Driver = 'mysql' | 'pg' | 'cockroachdb' | 'sqlite3' | 'oracledb' | 'mssql';
1
+ export type Driver = 'mysql2' | 'pg' | 'cockroachdb' | 'sqlite3' | 'oracledb' | 'mssql';
2
2
  export declare const DatabaseClients: readonly ["mysql", "postgres", "cockroachdb", "sqlite", "oracle", "mssql", "redshift"];
3
3
  export type DatabaseClient = (typeof DatabaseClients)[number];
@@ -0,0 +1,5 @@
1
+ /// <reference types="node" resolution-mode="require"/>
2
+ /// <reference types="node/http.js" />
3
+ /// <reference types="pino-http" />
4
+ import * as http from 'http';
5
+ export declare function getAddress(server: http.Server): string | undefined;
@@ -0,0 +1,13 @@
1
+ import * as http from 'http';
2
+ export function getAddress(server) {
3
+ const address = server.address();
4
+ if (address === null) {
5
+ // Before the 'listening' event has been emitted or after calling server.close()
6
+ return;
7
+ }
8
+ if (typeof address === 'string') {
9
+ // unix path
10
+ return address;
11
+ }
12
+ return `${address.address}:${address.port}`;
13
+ }
@@ -1,3 +1,4 @@
1
+ import { isObject } from '@directus/utils';
1
2
  import {} from 'knex';
2
3
  import { getDatabaseClient } from '../database/index.js';
3
4
  import { useLogger } from '../logger/index.js';
@@ -18,16 +19,7 @@ export const transaction = async (knex, handler) => {
18
19
  }
19
20
  catch (error) {
20
21
  const client = getDatabaseClient(knex);
21
- /**
22
- * This error code indicates that the transaction failed due to another
23
- * concurrent or recent transaction attempting to write to the same data.
24
- * This can usually be solved by restarting the transaction on client-side
25
- * after a short delay, so that it is executed against the latest state.
26
- *
27
- * @link https://www.cockroachlabs.com/docs/stable/transaction-retry-error-reference
28
- */
29
- const COCKROACH_RETRY_ERROR_CODE = '40001';
30
- if (client !== 'cockroachdb' || error?.code !== COCKROACH_RETRY_ERROR_CODE)
22
+ if (!shouldRetryTransaction(client, error))
31
23
  throw error;
32
24
  const MAX_ATTEMPTS = 3;
33
25
  const BASE_DELAY = 100;
@@ -40,7 +32,7 @@ export const transaction = async (knex, handler) => {
40
32
  return await knex.transaction((trx) => handler(trx));
41
33
  }
42
34
  catch (error) {
43
- if (error?.code !== COCKROACH_RETRY_ERROR_CODE)
35
+ if (!shouldRetryTransaction(client, error))
44
36
  throw error;
45
37
  }
46
38
  }
@@ -50,3 +42,28 @@ export const transaction = async (knex, handler) => {
50
42
  }
51
43
  }
52
44
  };
45
+ function shouldRetryTransaction(client, error) {
46
+ /**
47
+ * This error code indicates that the transaction failed due to another
48
+ * concurrent or recent transaction attempting to write to the same data.
49
+ * This can usually be solved by restarting the transaction on client-side
50
+ * after a short delay, so that it is executed against the latest state.
51
+ *
52
+ * @link https://www.cockroachlabs.com/docs/stable/transaction-retry-error-reference
53
+ */
54
+ const COCKROACH_RETRY_ERROR_CODE = '40001';
55
+ /**
56
+ * SQLITE_BUSY is an error code returned by SQLite when an operation can't be
57
+ * performed due to a locked database file. This often arises due to multiple
58
+ * processes trying to simultaneously access the database, causing potential
59
+ * data inconsistencies. There are a few mechanisms to handle this case,
60
+ * one of which is to retry the complete transaction again
61
+ * on client-side after a short delay.
62
+ *
63
+ * @link https://www.sqlite.org/rescode.html#busy
64
+ */
65
+ const SQLITE_BUSY_ERROR_CODE = 'SQLITE_BUSY';
66
+ return (isObject(error) &&
67
+ ((client === 'cockroachdb' && error['code'] === COCKROACH_RETRY_ERROR_CODE) ||
68
+ (client === 'sqlite' && error['code'] === SQLITE_BUSY_ERROR_CODE)));
69
+ }
@@ -1,9 +1,9 @@
1
- import { useEnv } from '@directus/env';
2
1
  import { CloseCode, MessageType, makeServer } from 'graphql-ws';
3
2
  import { useLogger } from '../../logger/index.js';
4
3
  import { bindPubSub } from '../../services/graphql/subscription.js';
5
4
  import { GraphQLService } from '../../services/index.js';
6
5
  import { getSchema } from '../../utils/get-schema.js';
6
+ import { getAddress } from '../../utils/get-address.js';
7
7
  import { authenticateConnection } from '../authenticate.js';
8
8
  import { handleWebSocketError } from '../errors.js';
9
9
  import { ConnectionParams, WebSocketMessage } from '../messages.js';
@@ -14,7 +14,6 @@ export class GraphQLSubscriptionController extends SocketController {
14
14
  gql;
15
15
  constructor(httpServer) {
16
16
  super(httpServer, 'WEBSOCKETS_GRAPHQL');
17
- const env = useEnv();
18
17
  this.server.on('connection', (ws, auth) => {
19
18
  this.bindEvents(this.createClient(ws, auth));
20
19
  });
@@ -31,7 +30,7 @@ export class GraphQLSubscriptionController extends SocketController {
31
30
  },
32
31
  });
33
32
  bindPubSub();
34
- logger.info(`GraphQL Subscriptions started at ws://${env['HOST']}:${env['PORT']}${this.endpoint}`);
33
+ logger.info(`GraphQL Subscriptions started at ws://${getAddress(httpServer)}${this.endpoint}`);
35
34
  }
36
35
  bindEvents(client) {
37
36
  const closedHandler = this.gql.opened({
@@ -1,7 +1,7 @@
1
- import { useEnv } from '@directus/env';
2
1
  import { parseJSON } from '@directus/utils';
3
2
  import emitter from '../../emitter.js';
4
3
  import { useLogger } from '../../logger/index.js';
4
+ import { getAddress } from '../../utils/get-address.js';
5
5
  import { WebSocketError, handleWebSocketError } from '../errors.js';
6
6
  import { WebSocketMessage } from '../messages.js';
7
7
  import SocketController from './base.js';
@@ -9,11 +9,10 @@ const logger = useLogger();
9
9
  export class WebSocketController extends SocketController {
10
10
  constructor(httpServer) {
11
11
  super(httpServer, 'WEBSOCKETS_REST');
12
- const env = useEnv();
13
12
  this.server.on('connection', (ws, auth) => {
14
13
  this.bindEvents(this.createClient(ws, auth));
15
14
  });
16
- logger.info(`WebSocket Server started at ws://${env['HOST']}:${env['PORT']}${this.endpoint}`);
15
+ logger.info(`WebSocket Server started at ws://${getAddress(httpServer)}${this.endpoint}`);
17
16
  }
18
17
  bindEvents(client) {
19
18
  client.on('parsed-message', async (message) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directus/api",
3
- "version": "22.0.0",
3
+ "version": "22.1.0",
4
4
  "description": "Directus is a real-time API and App dashboard for managing SQL database content",
5
5
  "keywords": [
6
6
  "directus",
@@ -148,30 +148,30 @@
148
148
  "wellknown": "0.5.0",
149
149
  "ws": "8.18.0",
150
150
  "zod": "3.23.8",
151
- "zod-validation-error": "3.3.0",
151
+ "zod-validation-error": "3.3.1",
152
+ "@directus/app": "13.0.1",
152
153
  "@directus/constants": "12.0.0",
153
- "@directus/app": "13.0.0",
154
- "@directus/env": "2.0.0",
154
+ "@directus/env": "3.0.0",
155
155
  "@directus/errors": "1.0.0",
156
- "@directus/format-title": "11.0.0",
157
156
  "@directus/extensions": "2.0.0",
158
- "@directus/extensions-sdk": "12.0.0",
159
157
  "@directus/extensions-registry": "2.0.0",
160
- "@directus/pressure": "2.0.0",
158
+ "@directus/format-title": "11.0.0",
161
159
  "@directus/memory": "2.0.0",
160
+ "@directus/pressure": "2.0.0",
162
161
  "@directus/schema": "12.0.0",
162
+ "@directus/extensions-sdk": "12.0.0",
163
163
  "@directus/specs": "11.0.0",
164
- "@directus/storage": "11.0.0",
165
164
  "@directus/storage-driver-azure": "11.0.0",
165
+ "@directus/storage": "11.0.0",
166
+ "@directus/storage-driver-cloudinary": "11.0.0",
166
167
  "@directus/storage-driver-gcs": "11.0.0",
167
168
  "@directus/storage-driver-local": "11.0.0",
168
- "@directus/storage-driver-cloudinary": "11.0.0",
169
- "@directus/storage-driver-s3": "11.0.0",
170
169
  "@directus/storage-driver-supabase": "2.0.0",
171
170
  "@directus/system-data": "2.0.0",
171
+ "@directus/storage-driver-s3": "11.0.0",
172
172
  "@directus/validation": "1.0.0",
173
- "@directus/utils": "12.0.0",
174
- "directus": "11.0.0"
173
+ "directus": "11.0.1",
174
+ "@directus/utils": "12.0.0"
175
175
  },
176
176
  "devDependencies": {
177
177
  "@ngneat/falso": "7.2.0",