@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.
- package/dist/app.js +10 -4
- package/dist/auth/drivers/oauth2.js +2 -3
- package/dist/auth/drivers/openid.js +2 -3
- package/dist/cache.d.ts +2 -2
- package/dist/cache.js +20 -7
- package/dist/controllers/assets.js +2 -2
- package/dist/controllers/metrics.d.ts +2 -0
- package/dist/controllers/metrics.js +33 -0
- package/dist/controllers/server.js +1 -1
- package/dist/database/get-ast-from-query/lib/parse-fields.js +4 -3
- package/dist/database/helpers/index.d.ts +1 -3
- package/dist/database/helpers/index.js +1 -3
- package/dist/database/helpers/number/dialects/mssql.d.ts +2 -2
- package/dist/database/helpers/number/dialects/mssql.js +3 -3
- package/dist/database/helpers/number/dialects/oracle.d.ts +2 -2
- package/dist/database/helpers/number/dialects/oracle.js +2 -2
- package/dist/database/helpers/number/dialects/sqlite.d.ts +2 -2
- package/dist/database/helpers/number/dialects/sqlite.js +2 -2
- package/dist/database/helpers/number/types.d.ts +2 -2
- package/dist/database/helpers/number/types.js +2 -2
- package/dist/database/helpers/schema/dialects/oracle.d.ts +6 -1
- package/dist/database/helpers/schema/dialects/oracle.js +15 -0
- package/dist/database/helpers/schema/types.d.ts +3 -1
- package/dist/database/helpers/schema/types.js +9 -0
- package/dist/database/index.js +3 -0
- package/dist/metrics/index.d.ts +1 -0
- package/dist/metrics/index.js +1 -0
- package/dist/metrics/lib/create-metrics.d.ts +15 -0
- package/dist/metrics/lib/create-metrics.js +239 -0
- package/dist/metrics/lib/use-metrics.d.ts +17 -0
- package/dist/metrics/lib/use-metrics.js +15 -0
- package/dist/metrics/types/metric.d.ts +1 -0
- package/dist/metrics/types/metric.js +1 -0
- package/dist/middleware/respond.js +7 -1
- package/dist/operations/condition/index.js +7 -2
- package/dist/operations/mail/index.d.ts +6 -3
- package/dist/operations/mail/index.js +2 -2
- package/dist/schedules/metrics.d.ts +7 -0
- package/dist/schedules/metrics.js +44 -0
- package/dist/services/assets.d.ts +6 -1
- package/dist/services/assets.js +8 -6
- package/dist/services/fields.js +23 -39
- package/dist/services/files.js +6 -5
- package/dist/services/graphql/errors/format.d.ts +6 -0
- package/dist/services/graphql/errors/format.js +14 -0
- package/dist/services/graphql/index.d.ts +5 -53
- package/dist/services/graphql/index.js +5 -2720
- package/dist/services/graphql/resolvers/mutation.d.ts +4 -0
- package/dist/services/graphql/resolvers/mutation.js +74 -0
- package/dist/services/graphql/resolvers/query.d.ts +8 -0
- package/dist/services/graphql/resolvers/query.js +87 -0
- package/dist/services/graphql/resolvers/system-admin.d.ts +5 -0
- package/dist/services/graphql/resolvers/system-admin.js +236 -0
- package/dist/services/graphql/resolvers/system-global.d.ts +7 -0
- package/dist/services/graphql/resolvers/system-global.js +435 -0
- package/dist/services/graphql/resolvers/system.d.ts +11 -0
- package/dist/services/graphql/resolvers/system.js +554 -0
- package/dist/services/graphql/schema/get-types.d.ts +12 -0
- package/dist/services/graphql/schema/get-types.js +223 -0
- package/dist/services/graphql/schema/index.d.ts +32 -0
- package/dist/services/graphql/schema/index.js +190 -0
- package/dist/services/graphql/schema/parse-args.d.ts +9 -0
- package/dist/services/graphql/schema/parse-args.js +35 -0
- package/dist/services/graphql/schema/parse-query.d.ts +7 -0
- package/dist/services/graphql/schema/parse-query.js +98 -0
- package/dist/services/graphql/schema/read.d.ts +12 -0
- package/dist/services/graphql/schema/read.js +653 -0
- package/dist/services/graphql/schema/write.d.ts +9 -0
- package/dist/services/graphql/schema/write.js +142 -0
- package/dist/services/graphql/subscription.d.ts +1 -1
- package/dist/services/graphql/subscription.js +7 -6
- package/dist/services/graphql/utils/aggrgate-query.d.ts +6 -0
- package/dist/services/graphql/utils/aggrgate-query.js +32 -0
- package/dist/services/graphql/utils/replace-fragments.d.ts +6 -0
- package/dist/services/graphql/utils/replace-fragments.js +21 -0
- package/dist/services/graphql/utils/replace-funcs.d.ts +5 -0
- package/dist/services/graphql/utils/replace-funcs.js +21 -0
- package/dist/services/graphql/utils/sanitize-gql-schema.d.ts +1 -1
- package/dist/services/graphql/utils/sanitize-gql-schema.js +5 -5
- package/dist/services/items.js +0 -2
- package/dist/services/meta.js +25 -84
- package/dist/services/users.d.ts +4 -0
- package/dist/services/users.js +23 -1
- package/dist/utils/apply-query.d.ts +1 -1
- package/dist/utils/apply-query.js +58 -21
- package/dist/utils/freeze-schema.d.ts +3 -0
- package/dist/utils/freeze-schema.js +31 -0
- package/dist/utils/get-accountability-for-token.js +1 -0
- package/dist/utils/get-milliseconds.js +1 -1
- package/dist/utils/get-schema.js +11 -6
- package/dist/utils/permissions-cachable.d.ts +8 -0
- package/dist/utils/permissions-cachable.js +38 -0
- package/dist/utils/sanitize-schema.d.ts +1 -1
- package/dist/websocket/messages.d.ts +6 -6
- package/package.json +23 -20
- package/dist/database/helpers/nullable-update/dialects/default.d.ts +0 -3
- package/dist/database/helpers/nullable-update/dialects/default.js +0 -3
- package/dist/database/helpers/nullable-update/dialects/oracle.d.ts +0 -12
- package/dist/database/helpers/nullable-update/dialects/oracle.js +0 -16
- package/dist/database/helpers/nullable-update/index.d.ts +0 -7
- package/dist/database/helpers/nullable-update/index.js +0 -7
- package/dist/database/helpers/nullable-update/types.d.ts +0 -7
- 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
|
-
|
|
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,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
|
}
|
package/dist/services/assets.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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
|
|
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:
|
|
166
|
+
return { stream: deferStream ? assetStream : await assetStream(), file, stat };
|
|
165
167
|
}
|
|
166
168
|
}
|
|
167
169
|
}
|
package/dist/services/fields.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
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
|
|
739
|
-
table.dropUnique([field.field],
|
|
719
|
+
if (existing?.is_unique === true) {
|
|
720
|
+
table.dropUnique([field.field], this.helpers.schema.generateIndexName('unique', collection, field.field));
|
|
740
721
|
}
|
|
741
722
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
|
747
|
-
|
|
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) {
|
package/dist/services/files.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useEnv } from '@directus/env';
|
|
2
|
-
import { ContentTooLargeError,
|
|
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
|
|
215
|
-
|
|
216
|
-
|
|
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
|
+
}
|