@directus/api 24.0.0 → 25.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 (103) hide show
  1. package/dist/app.js +10 -4
  2. package/dist/auth/drivers/oauth2.js +2 -3
  3. package/dist/auth/drivers/openid.js +2 -3
  4. package/dist/cache.d.ts +2 -2
  5. package/dist/cache.js +20 -7
  6. package/dist/controllers/assets.js +2 -2
  7. package/dist/controllers/metrics.d.ts +2 -0
  8. package/dist/controllers/metrics.js +33 -0
  9. package/dist/controllers/server.js +1 -1
  10. package/dist/database/get-ast-from-query/lib/parse-fields.js +4 -3
  11. package/dist/database/helpers/index.d.ts +1 -3
  12. package/dist/database/helpers/index.js +1 -3
  13. package/dist/database/helpers/number/dialects/mssql.d.ts +2 -2
  14. package/dist/database/helpers/number/dialects/mssql.js +3 -3
  15. package/dist/database/helpers/number/dialects/oracle.d.ts +2 -2
  16. package/dist/database/helpers/number/dialects/oracle.js +2 -2
  17. package/dist/database/helpers/number/dialects/sqlite.d.ts +2 -2
  18. package/dist/database/helpers/number/dialects/sqlite.js +2 -2
  19. package/dist/database/helpers/number/types.d.ts +2 -2
  20. package/dist/database/helpers/number/types.js +2 -2
  21. package/dist/database/helpers/schema/dialects/oracle.d.ts +6 -1
  22. package/dist/database/helpers/schema/dialects/oracle.js +15 -0
  23. package/dist/database/helpers/schema/types.d.ts +3 -1
  24. package/dist/database/helpers/schema/types.js +9 -0
  25. package/dist/database/index.js +3 -0
  26. package/dist/metrics/index.d.ts +1 -0
  27. package/dist/metrics/index.js +1 -0
  28. package/dist/metrics/lib/create-metrics.d.ts +15 -0
  29. package/dist/metrics/lib/create-metrics.js +239 -0
  30. package/dist/metrics/lib/use-metrics.d.ts +17 -0
  31. package/dist/metrics/lib/use-metrics.js +15 -0
  32. package/dist/metrics/types/metric.d.ts +1 -0
  33. package/dist/metrics/types/metric.js +1 -0
  34. package/dist/middleware/respond.js +7 -1
  35. package/dist/operations/condition/index.js +7 -2
  36. package/dist/operations/mail/index.d.ts +6 -3
  37. package/dist/operations/mail/index.js +2 -2
  38. package/dist/schedules/metrics.d.ts +7 -0
  39. package/dist/schedules/metrics.js +44 -0
  40. package/dist/services/assets.d.ts +6 -1
  41. package/dist/services/assets.js +8 -6
  42. package/dist/services/fields.js +23 -39
  43. package/dist/services/files.js +6 -5
  44. package/dist/services/graphql/errors/format.d.ts +6 -0
  45. package/dist/services/graphql/errors/format.js +14 -0
  46. package/dist/services/graphql/index.d.ts +5 -53
  47. package/dist/services/graphql/index.js +5 -2720
  48. package/dist/services/graphql/resolvers/mutation.d.ts +4 -0
  49. package/dist/services/graphql/resolvers/mutation.js +74 -0
  50. package/dist/services/graphql/resolvers/query.d.ts +8 -0
  51. package/dist/services/graphql/resolvers/query.js +87 -0
  52. package/dist/services/graphql/resolvers/system-admin.d.ts +5 -0
  53. package/dist/services/graphql/resolvers/system-admin.js +236 -0
  54. package/dist/services/graphql/resolvers/system-global.d.ts +7 -0
  55. package/dist/services/graphql/resolvers/system-global.js +435 -0
  56. package/dist/services/graphql/resolvers/system.d.ts +11 -0
  57. package/dist/services/graphql/resolvers/system.js +554 -0
  58. package/dist/services/graphql/schema/get-types.d.ts +12 -0
  59. package/dist/services/graphql/schema/get-types.js +223 -0
  60. package/dist/services/graphql/schema/index.d.ts +32 -0
  61. package/dist/services/graphql/schema/index.js +190 -0
  62. package/dist/services/graphql/schema/parse-args.d.ts +9 -0
  63. package/dist/services/graphql/schema/parse-args.js +35 -0
  64. package/dist/services/graphql/schema/parse-query.d.ts +7 -0
  65. package/dist/services/graphql/schema/parse-query.js +98 -0
  66. package/dist/services/graphql/schema/read.d.ts +12 -0
  67. package/dist/services/graphql/schema/read.js +653 -0
  68. package/dist/services/graphql/schema/write.d.ts +9 -0
  69. package/dist/services/graphql/schema/write.js +142 -0
  70. package/dist/services/graphql/subscription.d.ts +1 -1
  71. package/dist/services/graphql/subscription.js +7 -6
  72. package/dist/services/graphql/utils/aggrgate-query.d.ts +6 -0
  73. package/dist/services/graphql/utils/aggrgate-query.js +32 -0
  74. package/dist/services/graphql/utils/replace-fragments.d.ts +6 -0
  75. package/dist/services/graphql/utils/replace-fragments.js +21 -0
  76. package/dist/services/graphql/utils/replace-funcs.d.ts +5 -0
  77. package/dist/services/graphql/utils/replace-funcs.js +21 -0
  78. package/dist/services/graphql/utils/sanitize-gql-schema.d.ts +1 -1
  79. package/dist/services/graphql/utils/sanitize-gql-schema.js +5 -5
  80. package/dist/services/items.js +0 -2
  81. package/dist/services/meta.js +25 -84
  82. package/dist/services/users.d.ts +4 -0
  83. package/dist/services/users.js +23 -1
  84. package/dist/utils/apply-query.d.ts +1 -1
  85. package/dist/utils/apply-query.js +58 -21
  86. package/dist/utils/freeze-schema.d.ts +3 -0
  87. package/dist/utils/freeze-schema.js +31 -0
  88. package/dist/utils/get-accountability-for-token.js +1 -0
  89. package/dist/utils/get-milliseconds.js +1 -1
  90. package/dist/utils/get-schema.js +11 -6
  91. package/dist/utils/permissions-cachable.d.ts +8 -0
  92. package/dist/utils/permissions-cachable.js +38 -0
  93. package/dist/utils/sanitize-schema.d.ts +1 -1
  94. package/dist/websocket/messages.d.ts +6 -6
  95. package/package.json +23 -20
  96. package/dist/database/helpers/nullable-update/dialects/default.d.ts +0 -3
  97. package/dist/database/helpers/nullable-update/dialects/default.js +0 -3
  98. package/dist/database/helpers/nullable-update/dialects/oracle.d.ts +0 -12
  99. package/dist/database/helpers/nullable-update/dialects/oracle.js +0 -16
  100. package/dist/database/helpers/nullable-update/index.d.ts +0 -7
  101. package/dist/database/helpers/nullable-update/index.js +0 -7
  102. package/dist/database/helpers/nullable-update/types.d.ts +0 -7
  103. package/dist/database/helpers/nullable-update/types.js +0 -12
@@ -0,0 +1,239 @@
1
+ import { useEnv } from '@directus/env';
2
+ import { toArray } from '@directus/utils';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { Readable } from 'node:stream';
5
+ import { promisify } from 'node:util';
6
+ import pm2 from 'pm2';
7
+ import { AggregatorRegistry, Counter, Histogram, register } from 'prom-client';
8
+ import { getCache } from '../../cache.js';
9
+ import { hasDatabaseConnection } from '../../database/index.js';
10
+ import { redisConfigAvailable, useRedis } from '../../redis/index.js';
11
+ import { getStorage } from '../../storage/index.js';
12
+ const isPM2 = 'PM2_HOME' in process.env;
13
+ const METRICS_SYNC_PACKET = 'directus:metrics---data-sync';
14
+ const listApps = promisify(pm2.list.bind(pm2));
15
+ const sendDataToProcessId = promisify(pm2.sendDataToProcessId.bind(pm2));
16
+ export function createMetrics() {
17
+ const env = useEnv();
18
+ const services = env['METRICS_SERVICES'] ?? [];
19
+ const aggregates = new Map();
20
+ /**
21
+ * Listen for PM2 metric data sync messages and add them to the aggregate
22
+ */
23
+ if (isPM2) {
24
+ process.on('message', (packet) => {
25
+ if (!packet.data || packet.topic !== METRICS_SYNC_PACKET)
26
+ return;
27
+ aggregate(packet.data);
28
+ });
29
+ }
30
+ async function generate() {
31
+ const checkId = randomUUID();
32
+ await Promise.all([
33
+ trackDatabaseMetric(),
34
+ trackCacheMetric(checkId),
35
+ trackRedisMetric(checkId),
36
+ trackStorageMetric(checkId),
37
+ ]);
38
+ /**
39
+ * Push generated metrics to all pm2 instances
40
+ */
41
+ if (isPM2) {
42
+ try {
43
+ const apps = await listApps();
44
+ const data = await register.getMetricsAsJSON();
45
+ const syncs = [];
46
+ for (const app of apps) {
47
+ if (app.pm_id === undefined || app.pid === 0 || app.name !== 'directus') {
48
+ continue;
49
+ }
50
+ syncs.push(sendDataToProcessId(app.pm_id, {
51
+ data: { pid: process.pid, metrics: data },
52
+ topic: METRICS_SYNC_PACKET,
53
+ }));
54
+ }
55
+ await Promise.allSettled(syncs);
56
+ }
57
+ catch {
58
+ // ignore
59
+ }
60
+ }
61
+ }
62
+ /**
63
+ * Add PM2 synced metric to the aggregate store.
64
+ * Subsequent syncs for the given instance will override previous value.
65
+ */
66
+ async function aggregate(data) {
67
+ aggregates.set(data.pid, data.metrics);
68
+ }
69
+ async function readAll() {
70
+ /**
71
+ * In a PM2 context we must aggregate the metrics across instances ensuring
72
+ * only currently active instances are added to the aggregate
73
+ */
74
+ if (isPM2 && aggregates.size !== 0) {
75
+ const apps = await listApps();
76
+ const aggregate = [];
77
+ for (const app of apps) {
78
+ if (aggregates.has(app.pid)) {
79
+ aggregate.push(aggregates.get(app.pid));
80
+ }
81
+ }
82
+ if (aggregate.length !== 0) {
83
+ return AggregatorRegistry.aggregate(aggregate).metrics();
84
+ }
85
+ }
86
+ return register.metrics();
87
+ }
88
+ function getDatabaseErrorMetric() {
89
+ if (services.includes('database') === false) {
90
+ return null;
91
+ }
92
+ const client = env['DB_CLIENT'];
93
+ let metric = register.getSingleMetric(`directus_db_${client}_connection_errors`);
94
+ if (!metric) {
95
+ metric = new Counter({
96
+ name: `directus_db_${client}_connection_errors`,
97
+ help: `${client} Database connection error count`,
98
+ });
99
+ }
100
+ return metric;
101
+ }
102
+ function getDatabaseResponseMetric() {
103
+ if (services.includes('database') === false) {
104
+ return null;
105
+ }
106
+ const client = env['DB_CLIENT'];
107
+ let metric = register.getSingleMetric(`directus_db_${client}_response_time_ms`);
108
+ if (!metric) {
109
+ metric = new Histogram({
110
+ name: `directus_db_${client}_response_time_ms`,
111
+ help: `${client} Database connection response time`,
112
+ buckets: [1, 10, 20, 40, 60, 80, 100, 200, 500, 750, 1000],
113
+ });
114
+ }
115
+ return metric;
116
+ }
117
+ function getCacheErrorMetric() {
118
+ if (services.includes('cache') === false || env['CACHE_ENABLED'] !== true) {
119
+ return null;
120
+ }
121
+ if (env['CACHE_STORE'] === 'redis' && redisConfigAvailable() !== true) {
122
+ return null;
123
+ }
124
+ let metric = register.getSingleMetric(`directus_cache_${env['CACHE_STORE']}_connection_errors`);
125
+ if (!metric) {
126
+ metric = new Counter({
127
+ name: `directus_cache_${env['CACHE_STORE']}_connection_errors`,
128
+ help: 'Cache connection error count',
129
+ });
130
+ }
131
+ return metric;
132
+ }
133
+ function getRedisErrorMetric() {
134
+ if (services.includes('redis') === false || redisConfigAvailable() !== true) {
135
+ return null;
136
+ }
137
+ let metric = register.getSingleMetric('directus_redis_connection_errors');
138
+ if (!metric) {
139
+ metric = new Counter({
140
+ name: `directus_redis_connection_errors`,
141
+ help: 'Redis connection error count',
142
+ });
143
+ }
144
+ return metric;
145
+ }
146
+ function getStorageErrorMetric(location) {
147
+ if (services.includes('storage') === false) {
148
+ return null;
149
+ }
150
+ let metric = register.getSingleMetric(`directus_storage_${location}_connection_errors`);
151
+ if (!metric) {
152
+ metric = new Counter({
153
+ name: `directus_storage_${location}_connection_errors`,
154
+ help: `${location} storage connection error count`,
155
+ });
156
+ }
157
+ return metric;
158
+ }
159
+ async function trackDatabaseMetric() {
160
+ const metric = getDatabaseErrorMetric();
161
+ if (metric === null) {
162
+ return;
163
+ }
164
+ try {
165
+ if (!(await hasDatabaseConnection())) {
166
+ metric.inc();
167
+ }
168
+ }
169
+ catch {
170
+ metric.inc();
171
+ }
172
+ }
173
+ async function trackCacheMetric(checkId) {
174
+ const metric = getCacheErrorMetric();
175
+ if (metric === null) {
176
+ return;
177
+ }
178
+ const { cache } = getCache();
179
+ if (!cache) {
180
+ return;
181
+ }
182
+ try {
183
+ await cache.set(`metrics-${checkId}`, '1', 5);
184
+ await cache.delete(`metrics-${checkId}`);
185
+ }
186
+ catch {
187
+ metric.inc();
188
+ }
189
+ }
190
+ async function trackRedisMetric(checkId) {
191
+ const metric = getRedisErrorMetric();
192
+ if (metric === null) {
193
+ return;
194
+ }
195
+ const redis = useRedis();
196
+ try {
197
+ await redis.set(`metrics-${checkId}`, '1');
198
+ await redis.del(`metrics-${checkId}`);
199
+ }
200
+ catch {
201
+ metric.inc();
202
+ }
203
+ }
204
+ async function trackStorageMetric(checkId) {
205
+ if (services.includes('storage') === false) {
206
+ return;
207
+ }
208
+ const storage = await getStorage();
209
+ for (const location of toArray(env['STORAGE_LOCATIONS'])) {
210
+ const disk = storage.location(location);
211
+ const metric = getStorageErrorMetric(location);
212
+ if (metric === null) {
213
+ continue;
214
+ }
215
+ try {
216
+ await disk.write(`metric-${checkId}`, Readable.from(['check']));
217
+ const fileStream = await disk.read(`metric-${checkId}`);
218
+ await new Promise((resolve) => fileStream.on('data', async () => {
219
+ fileStream.destroy();
220
+ await disk.delete(`metric-${checkId}`);
221
+ return resolve(null);
222
+ }));
223
+ }
224
+ catch {
225
+ metric.inc();
226
+ }
227
+ }
228
+ }
229
+ return {
230
+ getDatabaseErrorMetric,
231
+ getDatabaseResponseMetric,
232
+ getCacheErrorMetric,
233
+ getRedisErrorMetric,
234
+ getStorageErrorMetric,
235
+ aggregate,
236
+ generate,
237
+ readAll,
238
+ };
239
+ }
@@ -0,0 +1,17 @@
1
+ import { createMetrics } from './create-metrics.js';
2
+ export declare const _cache: {
3
+ metrics: ReturnType<typeof createMetrics> | undefined;
4
+ };
5
+ export declare const useMetrics: () => {
6
+ getDatabaseErrorMetric: () => import("prom-client").Counter | null;
7
+ getDatabaseResponseMetric: () => import("prom-client").Histogram | null;
8
+ getCacheErrorMetric: () => import("prom-client").Counter | null;
9
+ getRedisErrorMetric: () => import("prom-client").Counter | null;
10
+ getStorageErrorMetric: (location: string) => import("prom-client").Counter | null;
11
+ aggregate: (data: {
12
+ pid: number;
13
+ metrics: import("prom-client").MetricObjectWithValues<import("prom-client").MetricValue<string>>[];
14
+ }) => Promise<void>;
15
+ generate: () => Promise<void>;
16
+ readAll: () => Promise<string>;
17
+ } | undefined;
@@ -0,0 +1,15 @@
1
+ import { useEnv } from '@directus/env';
2
+ import { toBoolean } from '@directus/utils';
3
+ import { createMetrics } from './create-metrics.js';
4
+ export const _cache = { metrics: undefined };
5
+ export const useMetrics = () => {
6
+ const env = useEnv();
7
+ if (!toBoolean(env['METRICS_ENABLED'])) {
8
+ return;
9
+ }
10
+ if (_cache.metrics) {
11
+ return _cache.metrics;
12
+ }
13
+ _cache.metrics = createMetrics();
14
+ return _cache.metrics;
15
+ };
@@ -0,0 +1 @@
1
+ export type MetricService = 'database' | 'cache' | 'redis' | 'storage';
@@ -0,0 +1 @@
1
+ export {};
@@ -1,6 +1,7 @@
1
1
  import { useEnv } from '@directus/env';
2
2
  import { parse as parseBytesConfiguration } from 'bytes';
3
3
  import { getCache, setCacheValue } from '../cache.js';
4
+ import getDatabase from '../database/index.js';
4
5
  import { useLogger } from '../logger/index.js';
5
6
  import { ExportService } from '../services/import-export.js';
6
7
  import asyncHandler from '../utils/async-handler.js';
@@ -9,6 +10,7 @@ import { getCacheKey } from '../utils/get-cache-key.js';
9
10
  import { getDateFormatted } from '../utils/get-date-formatted.js';
10
11
  import { getMilliseconds } from '../utils/get-milliseconds.js';
11
12
  import { stringByteSize } from '../utils/get-string-byte-size.js';
13
+ import { permissionsCachable } from '../utils/permissions-cachable.js';
12
14
  export const respond = asyncHandler(async (req, res) => {
13
15
  const env = useEnv();
14
16
  const logger = useLogger();
@@ -26,7 +28,11 @@ export const respond = asyncHandler(async (req, res) => {
26
28
  cache &&
27
29
  !req.sanitizedQuery.export &&
28
30
  res.locals['cache'] !== false &&
29
- exceedsMaxSize === false) {
31
+ exceedsMaxSize === false &&
32
+ (await permissionsCachable(req.collection, {
33
+ knex: getDatabase(),
34
+ schema: req.schema,
35
+ }, req.accountability))) {
30
36
  const key = await getCacheKey(req);
31
37
  try {
32
38
  await setCacheValue(cache, key, res.locals['payload'], getMilliseconds(env['CACHE_TTL']));
@@ -1,11 +1,16 @@
1
- import { validatePayload } from '@directus/utils';
2
1
  import { defineOperationApi } from '@directus/extensions';
2
+ import { validatePayload } from '@directus/utils';
3
+ import { FailedValidationError, joiValidationErrorItemToErrorExtensions } from '@directus/validation';
3
4
  export default defineOperationApi({
4
5
  id: 'condition',
5
6
  handler: ({ filter }, { data }) => {
6
7
  const errors = validatePayload(filter, data, { requireAll: true });
7
8
  if (errors.length > 0) {
8
- throw errors;
9
+ // sanitize and format errors
10
+ const validationErrors = errors
11
+ .map((error) => error.details.map((details) => new FailedValidationError(joiValidationErrorItemToErrorExtensions(details))))
12
+ .flat();
13
+ throw validationErrors;
9
14
  }
10
15
  else {
11
16
  return null;
@@ -1,10 +1,13 @@
1
1
  export type Options = {
2
- body?: string;
3
- template?: string;
4
- data?: Record<string, any>;
5
2
  to: string;
6
3
  type: 'wysiwyg' | 'markdown' | 'template';
7
4
  subject: string;
5
+ body?: string;
6
+ template?: string;
7
+ data?: Record<string, any>;
8
+ cc?: string;
9
+ bcc?: string;
10
+ replyTo?: string;
8
11
  };
9
12
  declare const _default: import("@directus/extensions").OperationApiConfig<Options>;
10
13
  export default _default;
@@ -5,9 +5,9 @@ import { useLogger } from '../../logger/index.js';
5
5
  const logger = useLogger();
6
6
  export default defineOperationApi({
7
7
  id: 'mail',
8
- handler: async ({ body, template, data, to, type, subject }, { accountability, database, getSchema }) => {
8
+ handler: async ({ body, template, data, to, type, subject, cc, bcc, replyTo }, { accountability, database, getSchema }) => {
9
9
  const mailService = new MailService({ schema: await getSchema({ database }), accountability, knex: database });
10
- const mailObject = { to, subject };
10
+ const mailObject = { to, subject, cc, bcc, replyTo };
11
11
  const safeBody = typeof body !== 'string' ? JSON.stringify(body) : body;
12
12
  if (type === 'template') {
13
13
  mailObject.template = {
@@ -0,0 +1,7 @@
1
+ export declare function handleMetricsJob(): Promise<void>;
2
+ /**
3
+ * Schedule the metric generation
4
+ *
5
+ * @returns Whether or not metrics has been initialized
6
+ */
7
+ export default function schedule(): Promise<boolean>;
@@ -0,0 +1,44 @@
1
+ import { useEnv } from '@directus/env';
2
+ import { toBoolean } from '@directus/utils';
3
+ import { scheduleJob } from 'node-schedule';
4
+ import { useLogger } from '../logger/index.js';
5
+ import { useMetrics } from '../metrics/index.js';
6
+ import { validateCron } from '../utils/schedule.js';
7
+ const METRICS_LOCK_TIMEOUT = 10 * 60 * 1000; // 10 mins
8
+ let lockedAt = 0;
9
+ const logger = useLogger();
10
+ const metrics = useMetrics();
11
+ export async function handleMetricsJob() {
12
+ const now = Date.now();
13
+ if (lockedAt !== 0 && lockedAt > now - METRICS_LOCK_TIMEOUT) {
14
+ // ensure only generating metrics once per node
15
+ return;
16
+ }
17
+ lockedAt = Date.now();
18
+ try {
19
+ await metrics?.generate();
20
+ }
21
+ catch (err) {
22
+ logger.warn(`An error was thrown while attempting metric generation`);
23
+ logger.warn(err);
24
+ }
25
+ finally {
26
+ lockedAt = 0;
27
+ }
28
+ }
29
+ /**
30
+ * Schedule the metric generation
31
+ *
32
+ * @returns Whether or not metrics has been initialized
33
+ */
34
+ export default async function schedule() {
35
+ const env = useEnv();
36
+ if (!toBoolean(env['METRICS_ENABLED'])) {
37
+ return false;
38
+ }
39
+ if (!validateCron(String(env['METRICS_SCHEDULE']))) {
40
+ return false;
41
+ }
42
+ scheduleJob('metrics', String(env['METRICS_SCHEDULE']), handleMetricsJob);
43
+ return true;
44
+ }
@@ -10,9 +10,14 @@ export declare class AssetsService {
10
10
  schema: SchemaOverview;
11
11
  filesService: FilesService;
12
12
  constructor(options: AbstractServiceOptions);
13
- getAsset(id: string, transformation?: TransformationSet, range?: Range): Promise<{
13
+ getAsset(id: string, transformation?: TransformationSet, range?: Range, deferStream?: false): Promise<{
14
14
  stream: Readable;
15
15
  file: any;
16
16
  stat: Stat;
17
17
  }>;
18
+ getAsset(id: string, transformation?: TransformationSet, range?: Range, deferStream?: true): Promise<{
19
+ stream: () => Promise<Readable>;
20
+ file: any;
21
+ stat: Stat;
22
+ }>;
18
23
  }
@@ -28,7 +28,7 @@ export class AssetsService {
28
28
  this.schema = options.schema;
29
29
  this.filesService = new FilesService({ ...options, accountability: null });
30
30
  }
31
- async getAsset(id, transformation, range) {
31
+ async getAsset(id, transformation, range, deferStream = false) {
32
32
  const storage = await getStorage();
33
33
  const publicSettings = await this.knex
34
34
  .select('project_logo', 'public_background', 'public_foreground', 'public_favicon')
@@ -99,8 +99,9 @@ export class AssetsService {
99
99
  file.type = contentType(assetFilename) || null;
100
100
  }
101
101
  if (exists) {
102
+ const assetStream = () => storage.location(file.storage).read(assetFilename, { range });
102
103
  return {
103
- stream: await storage.location(file.storage).read(assetFilename, { range }),
104
+ stream: deferStream ? assetStream : await assetStream(),
104
105
  file,
105
106
  stat: await storage.location(file.storage).stat(assetFilename),
106
107
  };
@@ -123,7 +124,6 @@ export class AssetsService {
123
124
  reason: 'Server too busy',
124
125
  });
125
126
  }
126
- const readStream = await storage.location(file.storage).read(file.filename_disk, { range, version });
127
127
  const transformer = getSharpInstance();
128
128
  transformer.timeout({
129
129
  seconds: clamp(Math.round(getMilliseconds(env['ASSETS_TRANSFORM_TIMEOUT'], 0) / 1000), 1, 3600),
@@ -131,6 +131,7 @@ export class AssetsService {
131
131
  if (transforms.find((transform) => transform[0] === 'rotate') === undefined)
132
132
  transformer.rotate();
133
133
  transforms.forEach(([method, ...args]) => transformer[method].apply(transformer, args));
134
+ const readStream = await storage.location(file.storage).read(file.filename_disk, { range, version });
134
135
  readStream.on('error', (e) => {
135
136
  logger.error(e, `Couldn't transform file ${file.id}`);
136
137
  readStream.unpipe(transformer);
@@ -152,16 +153,17 @@ export class AssetsService {
152
153
  throw error;
153
154
  }
154
155
  }
156
+ const assetStream = () => storage.location(file.storage).read(assetFilename, { range, version });
155
157
  return {
156
- stream: await storage.location(file.storage).read(assetFilename, { range, version }),
158
+ stream: deferStream ? assetStream : await assetStream(),
157
159
  stat: await storage.location(file.storage).stat(assetFilename),
158
160
  file,
159
161
  };
160
162
  }
161
163
  else {
162
- const readStream = await storage.location(file.storage).read(file.filename_disk, { range, version });
164
+ const assetStream = () => storage.location(file.storage).read(file.filename_disk, { range, version });
163
165
  const stat = await storage.location(file.storage).stat(file.filename_disk);
164
- return { stream: readStream, file, stat };
166
+ return { stream: deferStream ? assetStream : await assetStream(), file, stat };
165
167
  }
166
168
  }
167
169
  }
@@ -58,7 +58,7 @@ export class FieldsService {
58
58
  if (!columnInfo) {
59
59
  columnInfo = await this.schemaInspector.columnInfo();
60
60
  if (schemaCacheIsEnabled) {
61
- setCacheValue(this.schemaCache, 'columnInfo', columnInfo);
61
+ await setCacheValue(this.schemaCache, 'columnInfo', columnInfo);
62
62
  }
63
63
  }
64
64
  if (collection) {
@@ -674,7 +674,17 @@ export class FieldsService {
674
674
  else {
675
675
  throw new InvalidPayloadError({ reason: `Illegal type passed: "${field.type}"` });
676
676
  }
677
- const setDefaultValue = (defaultValue) => {
677
+ /**
678
+ * The column nullability must be set on every alter or it will be dropped
679
+ * This is due to column.alter() not being incremental per https://knexjs.org/guide/schema-builder.html#alter
680
+ */
681
+ this.helpers.schema.setNullable(column, field, existing);
682
+ /**
683
+ * The default value must be set on every alter or it will be dropped
684
+ * This is due to column.alter() not being incremental per https://knexjs.org/guide/schema-builder.html#alter
685
+ */
686
+ const defaultValue = field.schema?.default_value !== undefined ? field.schema?.default_value : existing?.default_value;
687
+ if (defaultValue !== undefined) {
678
688
  const newDefaultValueIsString = typeof defaultValue === 'string';
679
689
  const newDefaultIsNowFunction = newDefaultValueIsString && defaultValue.toLowerCase() === 'now()';
680
690
  const newDefaultIsCurrentTimestamp = newDefaultValueIsString && defaultValue === 'CURRENT_TIMESTAMP';
@@ -694,57 +704,31 @@ export class FieldsService {
694
704
  else {
695
705
  column.defaultTo(defaultValue);
696
706
  }
697
- };
698
- // for a new item, set the default value and nullable as provided without any further considerations
699
- if (!existing) {
700
- if (field.schema?.default_value !== undefined) {
701
- setDefaultValue(field.schema.default_value);
702
- }
703
- if (field.schema?.is_nullable || field.schema?.is_nullable === undefined) {
704
- column.nullable();
705
- }
706
- else {
707
- column.notNullable();
708
- }
709
- }
710
- else {
711
- // for an existing item: if nullable option changed, we have to provide the default values as well and actually vice versa
712
- // see https://knexjs.org/guide/schema-builder.html#alter
713
- // To overwrite a nullable option with the same value this is not possible for Oracle though, hence the DB helper
714
- if (field.schema?.default_value !== undefined || field.schema?.is_nullable !== undefined) {
715
- this.helpers.nullableUpdate.updateNullableValue(column, field, existing);
716
- let defaultValue = null;
717
- if (field.schema?.default_value !== undefined) {
718
- defaultValue = field.schema.default_value;
719
- }
720
- else if (existing.default_value !== undefined) {
721
- defaultValue = existing.default_value;
722
- }
723
- setDefaultValue(defaultValue);
724
- }
725
707
  }
726
708
  if (field.schema?.is_primary_key) {
727
709
  column.primary().notNullable();
728
710
  }
729
711
  else if (!existing?.is_primary_key) {
730
712
  // primary key will already have unique/index constraints
731
- const uniqueIndexName = this.helpers.schema.generateIndexName('unique', collection, field.field);
732
713
  if (field.schema?.is_unique === true) {
733
714
  if (!existing || existing.is_unique === false) {
734
- column.unique({ indexName: uniqueIndexName });
715
+ column.unique({ indexName: this.helpers.schema.generateIndexName('unique', collection, field.field) });
735
716
  }
736
717
  }
737
718
  else if (field.schema?.is_unique === false) {
738
- if (existing && existing.is_unique === true) {
739
- table.dropUnique([field.field], uniqueIndexName);
719
+ if (existing?.is_unique === true) {
720
+ table.dropUnique([field.field], this.helpers.schema.generateIndexName('unique', collection, field.field));
740
721
  }
741
722
  }
742
- const indexName = this.helpers.schema.generateIndexName('index', collection, field.field);
743
- if (field.schema?.is_indexed === true && !existing?.is_indexed) {
744
- column.index(indexName);
723
+ if (field.schema?.is_indexed === true) {
724
+ if (!existing || existing.is_indexed === false) {
725
+ column.index(this.helpers.schema.generateIndexName('index', collection, field.field));
726
+ }
745
727
  }
746
- else if (field.schema?.is_indexed === false && existing?.is_indexed) {
747
- table.dropIndex([field.field], indexName);
728
+ else if (field.schema?.is_indexed === false) {
729
+ if (existing?.is_indexed === true) {
730
+ table.dropIndex([field.field], this.helpers.schema.generateIndexName('index', collection, field.field));
731
+ }
748
732
  }
749
733
  }
750
734
  if (existing) {
@@ -1,5 +1,5 @@
1
1
  import { useEnv } from '@directus/env';
2
- import { ContentTooLargeError, ForbiddenError, InvalidPayloadError, ServiceUnavailableError } from '@directus/errors';
2
+ import { ContentTooLargeError, InvalidPayloadError, ServiceUnavailableError } from '@directus/errors';
3
3
  import formatTitle from '@directus/format-title';
4
4
  import { toArray } from '@directus/utils';
5
5
  import encodeURL from 'encodeurl';
@@ -211,10 +211,11 @@ export class FilesService extends ItemsService {
211
211
  */
212
212
  async deleteMany(keys) {
213
213
  const storage = await getStorage();
214
- const files = await super.readMany(keys, { fields: ['id', 'storage', 'filename_disk'], limit: -1 });
215
- if (!files) {
216
- throw new ForbiddenError();
217
- }
214
+ const sudoFilesItemsService = new FilesService({
215
+ knex: this.knex,
216
+ schema: this.schema,
217
+ });
218
+ const files = await sudoFilesItemsService.readMany(keys, { fields: ['id', 'storage', 'filename_disk'], limit: -1 });
218
219
  await super.deleteMany(keys);
219
220
  for (const file of files) {
220
221
  const disk = storage.location(file['storage']);
@@ -0,0 +1,6 @@
1
+ import { type DirectusError } from '@directus/errors';
2
+ import { GraphQLError } from 'graphql';
3
+ /**
4
+ * Convert Directus-Exception into a GraphQL format, so it can be returned by GraphQL properly.
5
+ */
6
+ export declare function formatError(error: DirectusError | DirectusError[]): GraphQLError;
@@ -0,0 +1,14 @@
1
+ import {} from '@directus/errors';
2
+ import { GraphQLError } from 'graphql';
3
+ import { set } from 'lodash-es';
4
+ /**
5
+ * Convert Directus-Exception into a GraphQL format, so it can be returned by GraphQL properly.
6
+ */
7
+ export function formatError(error) {
8
+ if (Array.isArray(error)) {
9
+ set(error[0], 'extensions.code', error[0].code);
10
+ return new GraphQLError(error[0].message, undefined, undefined, undefined, undefined, error[0]);
11
+ }
12
+ set(error, 'extensions.code', error.code);
13
+ return new GraphQLError(error.message, undefined, undefined, undefined, undefined, error);
14
+ }