@directus/api 10.1.0 → 11.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 +4 -3
- package/dist/auth/drivers/oauth2.js +1 -1
- package/dist/auth/drivers/openid.js +1 -1
- package/dist/cli/utils/create-env/env-stub.liquid +7 -0
- package/dist/constants.d.ts +0 -1
- package/dist/constants.js +0 -1
- package/dist/controllers/assets.js +6 -10
- package/dist/controllers/files.js +19 -1
- package/dist/controllers/permissions.js +7 -4
- package/dist/controllers/translations.d.ts +2 -0
- package/dist/controllers/translations.js +149 -0
- package/dist/controllers/users.js +1 -1
- package/dist/database/migrations/20230525A-add-preview-settings.d.ts +3 -0
- package/dist/database/migrations/20230525A-add-preview-settings.js +10 -0
- package/dist/database/migrations/20230526A-migrate-translation-strings.d.ts +3 -0
- package/dist/database/migrations/20230526A-migrate-translation-strings.js +54 -0
- package/dist/database/run-ast.js +3 -3
- package/dist/database/system-data/app-access-permissions/app-access-permissions.yaml +3 -0
- package/dist/database/system-data/collections/collections.yaml +23 -0
- package/dist/database/system-data/fields/collections.yaml +16 -0
- package/dist/database/system-data/fields/settings.yaml +0 -5
- package/dist/database/system-data/fields/translations.yaml +27 -0
- package/dist/env.js +17 -0
- package/dist/exceptions/content-too-large.d.ts +4 -0
- package/dist/exceptions/content-too-large.js +6 -0
- package/dist/extensions.js +13 -11
- package/dist/flows.d.ts +1 -1
- package/dist/flows.js +20 -19
- package/dist/logger.d.ts +1 -1
- package/dist/logger.js +6 -6
- package/dist/server.js +0 -11
- package/dist/services/assets.d.ts +2 -2
- package/dist/services/collections.js +8 -7
- package/dist/services/fields.js +7 -5
- package/dist/services/files.d.ts +2 -2
- package/dist/services/files.js +4 -9
- package/dist/services/graphql/index.js +4 -41
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/items.js +10 -9
- package/dist/services/revisions.d.ts +6 -1
- package/dist/services/revisions.js +24 -0
- package/dist/services/server.js +3 -17
- package/dist/services/specifications.d.ts +2 -2
- package/dist/services/specifications.js +6 -5
- package/dist/services/translations.d.ts +10 -0
- package/dist/services/translations.js +36 -0
- package/dist/synchronization.d.ts +7 -0
- package/dist/synchronization.js +120 -0
- package/dist/types/assets.d.ts +6 -1
- package/dist/types/events.d.ts +2 -2
- package/dist/utils/apply-query.d.ts +9 -2
- package/dist/utils/apply-query.js +43 -16
- package/dist/utils/md.js +1 -1
- package/dist/utils/redact.d.ts +11 -0
- package/dist/utils/redact.js +75 -0
- package/dist/utils/sanitize-query.js +10 -1
- package/dist/utils/schedule.d.ts +5 -0
- package/dist/utils/schedule.js +27 -0
- package/dist/utils/should-clear-cache.d.ts +10 -0
- package/dist/utils/should-clear-cache.js +18 -0
- package/dist/utils/should-skip-cache.js +18 -2
- package/dist/utils/transformations.d.ts +2 -2
- package/dist/utils/transformations.js +27 -10
- package/dist/utils/validate-query.js +3 -1
- package/package.json +49 -53
- package/dist/utils/get-os-info.d.ts +0 -9
- package/dist/utils/get-os-info.js +0 -40
package/dist/services/items.js
CHANGED
|
@@ -9,6 +9,7 @@ import env from '../env.js';
|
|
|
9
9
|
import { translateDatabaseError } from '../exceptions/database/translate.js';
|
|
10
10
|
import { ForbiddenException, InvalidPayloadException } from '../exceptions/index.js';
|
|
11
11
|
import getASTFromQuery from '../utils/get-ast-from-query.js';
|
|
12
|
+
import { shouldClearCache } from '../utils/should-clear-cache.js';
|
|
12
13
|
import { validateKeys } from '../utils/validate-keys.js';
|
|
13
14
|
import { AuthorizationService } from './authorization.js';
|
|
14
15
|
import { PayloadService } from './payload.js';
|
|
@@ -180,7 +181,7 @@ export class ItemsService {
|
|
|
180
181
|
// Make sure to set the parent field of the child-revision rows
|
|
181
182
|
const childrenRevisions = [...revisionsM2O, ...revisionsA2O, ...revisionsO2M];
|
|
182
183
|
if (childrenRevisions.length > 0) {
|
|
183
|
-
await revisionsService.updateMany(childrenRevisions, { parent: revision }
|
|
184
|
+
await revisionsService.updateMany(childrenRevisions, { parent: revision });
|
|
184
185
|
}
|
|
185
186
|
if (opts.onRevisionCreate) {
|
|
186
187
|
opts.onRevisionCreate(revision);
|
|
@@ -220,7 +221,7 @@ export class ItemsService {
|
|
|
220
221
|
}
|
|
221
222
|
}
|
|
222
223
|
}
|
|
223
|
-
if (this.cache
|
|
224
|
+
if (shouldClearCache(this.cache, opts, this.collection)) {
|
|
224
225
|
await this.cache.clear();
|
|
225
226
|
}
|
|
226
227
|
return primaryKey;
|
|
@@ -260,7 +261,7 @@ export class ItemsService {
|
|
|
260
261
|
}
|
|
261
262
|
}
|
|
262
263
|
}
|
|
263
|
-
if (this.cache
|
|
264
|
+
if (shouldClearCache(this.cache, opts, this.collection)) {
|
|
264
265
|
await this.cache.clear();
|
|
265
266
|
}
|
|
266
267
|
return primaryKeys;
|
|
@@ -401,7 +402,7 @@ export class ItemsService {
|
|
|
401
402
|
});
|
|
402
403
|
}
|
|
403
404
|
finally {
|
|
404
|
-
if (this.cache
|
|
405
|
+
if (shouldClearCache(this.cache, opts, this.collection)) {
|
|
405
406
|
await this.cache.clear();
|
|
406
407
|
}
|
|
407
408
|
}
|
|
@@ -514,7 +515,7 @@ export class ItemsService {
|
|
|
514
515
|
data: snapshots && Array.isArray(snapshots) ? JSON.stringify(snapshots[index]) : JSON.stringify(snapshots),
|
|
515
516
|
delta: await payloadService.prepareDelta(payloadWithTypeCasting),
|
|
516
517
|
})))).filter((revision) => revision.delta);
|
|
517
|
-
const revisionIDs = await revisionsService.createMany(revisions
|
|
518
|
+
const revisionIDs = await revisionsService.createMany(revisions);
|
|
518
519
|
for (let i = 0; i < revisionIDs.length; i++) {
|
|
519
520
|
const revisionID = revisionIDs[i];
|
|
520
521
|
if (opts.onRevisionCreate) {
|
|
@@ -526,14 +527,14 @@ export class ItemsService {
|
|
|
526
527
|
// with all other revisions on the current level as regular "flat" updates, and
|
|
527
528
|
// nested revisions as children of this first "root" item.
|
|
528
529
|
if (childrenRevisions.length > 0) {
|
|
529
|
-
await revisionsService.updateMany(childrenRevisions, { parent: revisionID }
|
|
530
|
+
await revisionsService.updateMany(childrenRevisions, { parent: revisionID });
|
|
530
531
|
}
|
|
531
532
|
}
|
|
532
533
|
}
|
|
533
534
|
}
|
|
534
535
|
}
|
|
535
536
|
});
|
|
536
|
-
if (this.cache
|
|
537
|
+
if (shouldClearCache(this.cache, opts, this.collection)) {
|
|
537
538
|
await this.cache.clear();
|
|
538
539
|
}
|
|
539
540
|
if (opts.emitEvents !== false) {
|
|
@@ -610,7 +611,7 @@ export class ItemsService {
|
|
|
610
611
|
}
|
|
611
612
|
return primaryKeys;
|
|
612
613
|
});
|
|
613
|
-
if (this.cache
|
|
614
|
+
if (shouldClearCache(this.cache, opts, this.collection)) {
|
|
614
615
|
await this.cache.clear();
|
|
615
616
|
}
|
|
616
617
|
return primaryKeys;
|
|
@@ -683,7 +684,7 @@ export class ItemsService {
|
|
|
683
684
|
})), { bypassLimits: true });
|
|
684
685
|
}
|
|
685
686
|
});
|
|
686
|
-
if (this.cache
|
|
687
|
+
if (shouldClearCache(this.cache, opts, this.collection)) {
|
|
687
688
|
await this.cache.clear();
|
|
688
689
|
}
|
|
689
690
|
if (opts.emitEvents !== false) {
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import type { AbstractServiceOptions, PrimaryKey } from '../types/index.js';
|
|
1
|
+
import type { AbstractServiceOptions, Item, MutationOptions, PrimaryKey } from '../types/index.js';
|
|
2
2
|
import { ItemsService } from './items.js';
|
|
3
3
|
export declare class RevisionsService extends ItemsService {
|
|
4
4
|
constructor(options: AbstractServiceOptions);
|
|
5
5
|
revert(pk: PrimaryKey): Promise<void>;
|
|
6
|
+
private setDefaultOptions;
|
|
7
|
+
createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
|
|
8
|
+
createMany(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]>;
|
|
9
|
+
updateOne(key: PrimaryKey, data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
|
|
10
|
+
updateMany(keys: PrimaryKey[], data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey[]>;
|
|
6
11
|
}
|
|
@@ -17,4 +17,28 @@ export class RevisionsService extends ItemsService {
|
|
|
17
17
|
});
|
|
18
18
|
await service.updateOne(revision['item'], revision['data']);
|
|
19
19
|
}
|
|
20
|
+
setDefaultOptions(opts) {
|
|
21
|
+
if (!opts) {
|
|
22
|
+
return { autoPurgeCache: false, bypassLimits: true };
|
|
23
|
+
}
|
|
24
|
+
if (!('autoPurgeCache' in opts)) {
|
|
25
|
+
opts.autoPurgeCache = false;
|
|
26
|
+
}
|
|
27
|
+
if (!('bypassLimits' in opts)) {
|
|
28
|
+
opts.bypassLimits = true;
|
|
29
|
+
}
|
|
30
|
+
return opts;
|
|
31
|
+
}
|
|
32
|
+
async createOne(data, opts) {
|
|
33
|
+
return super.createOne(data, this.setDefaultOptions(opts));
|
|
34
|
+
}
|
|
35
|
+
async createMany(data, opts) {
|
|
36
|
+
return super.createMany(data, this.setDefaultOptions(opts));
|
|
37
|
+
}
|
|
38
|
+
async updateOne(key, data, opts) {
|
|
39
|
+
return super.updateOne(key, data, this.setDefaultOptions(opts));
|
|
40
|
+
}
|
|
41
|
+
async updateMany(keys, data, opts) {
|
|
42
|
+
return super.updateMany(keys, data, this.setDefaultOptions(opts));
|
|
43
|
+
}
|
|
20
44
|
}
|
package/dist/services/server.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { toArray } from '@directus/utils';
|
|
2
2
|
import { merge } from 'lodash-es';
|
|
3
3
|
import { Readable } from 'node:stream';
|
|
4
|
-
import os from 'os';
|
|
5
4
|
import { performance } from 'perf_hooks';
|
|
6
5
|
import { getCache } from '../cache.js';
|
|
7
6
|
import getDatabase, { hasDatabaseConnection } from '../database/index.js';
|
|
@@ -12,7 +11,6 @@ import { rateLimiterGlobal } from '../middleware/rate-limiter-global.js';
|
|
|
12
11
|
import { rateLimiter } from '../middleware/rate-limiter-ip.js';
|
|
13
12
|
import { SERVER_ONLINE } from '../server.js';
|
|
14
13
|
import { getStorage } from '../storage/index.js';
|
|
15
|
-
import { getOSInfo } from '../utils/get-os-info.js';
|
|
16
14
|
import { version } from '../utils/package.js';
|
|
17
15
|
import { SettingsService } from './settings.js';
|
|
18
16
|
export class ServerService {
|
|
@@ -64,21 +62,9 @@ export class ServerService {
|
|
|
64
62
|
info['flows'] = {
|
|
65
63
|
execAllowedModules: env['FLOWS_EXEC_ALLOWED_MODULES'] ? toArray(env['FLOWS_EXEC_ALLOWED_MODULES']) : [],
|
|
66
64
|
};
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
info['directus'] = {
|
|
71
|
-
version,
|
|
72
|
-
};
|
|
73
|
-
info['node'] = {
|
|
74
|
-
version: process.versions.node,
|
|
75
|
-
uptime: Math.round(process.uptime()),
|
|
76
|
-
};
|
|
77
|
-
info['os'] = {
|
|
78
|
-
type: osType,
|
|
79
|
-
version: osVersion,
|
|
80
|
-
uptime: Math.round(os.uptime()),
|
|
81
|
-
totalmem: os.totalmem(),
|
|
65
|
+
info['queryLimit'] = {
|
|
66
|
+
default: env['QUERY_LIMIT_DEFAULT'],
|
|
67
|
+
max: Number.isFinite(env['QUERY_LIMIT_MAX']) ? env['QUERY_LIMIT_MAX'] : -1,
|
|
82
68
|
};
|
|
83
69
|
}
|
|
84
70
|
return info;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { Knex } from 'knex';
|
|
2
|
-
import type { OpenAPIObject } from 'openapi3-ts';
|
|
3
1
|
import type { Accountability, SchemaOverview } from '@directus/types';
|
|
2
|
+
import type { Knex } from 'knex';
|
|
3
|
+
import type { OpenAPIObject } from 'openapi3-ts/oas30';
|
|
4
4
|
import type { AbstractServiceOptions } from '../types/index.js';
|
|
5
5
|
import { CollectionsService } from './collections.js';
|
|
6
6
|
import { FieldsService } from './fields.js';
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
+
import formatTitle from '@directus/format-title';
|
|
1
2
|
import { spec } from '@directus/specs';
|
|
2
3
|
import { cloneDeep, mergeWith } from 'lodash-es';
|
|
3
|
-
import { version } from '../utils/package.js';
|
|
4
4
|
import { OAS_REQUIRED_SCHEMAS } from '../constants.js';
|
|
5
5
|
import getDatabase from '../database/index.js';
|
|
6
6
|
import env from '../env.js';
|
|
7
7
|
import { getRelationType } from '../utils/get-relation-type.js';
|
|
8
|
+
import { version } from '../utils/package.js';
|
|
8
9
|
import { CollectionsService } from './collections.js';
|
|
9
10
|
import { FieldsService } from './fields.js';
|
|
10
11
|
import { GraphQLService } from './graphql/index.js';
|
|
11
12
|
import { RelationsService } from './relations.js';
|
|
12
|
-
import formatTitle from '@directus/format-title';
|
|
13
13
|
export class SpecificationService {
|
|
14
14
|
accountability;
|
|
15
15
|
knex;
|
|
@@ -148,7 +148,8 @@ class OASSpecsService {
|
|
|
148
148
|
const listBase = cloneDeep(spec.paths['/items/{collection}']);
|
|
149
149
|
const detailBase = cloneDeep(spec.paths['/items/{collection}/{id}']);
|
|
150
150
|
const collection = tag['x-collection'];
|
|
151
|
-
|
|
151
|
+
const methods = ['post', 'get', 'patch', 'delete'];
|
|
152
|
+
for (const method of methods) {
|
|
152
153
|
const hasPermission = this.accountability?.admin === true ||
|
|
153
154
|
!!permissions.find((permission) => permission.collection === collection && permission.action === this.getActionForMethod(method));
|
|
154
155
|
if (hasPermission) {
|
|
@@ -156,7 +157,7 @@ class OASSpecsService {
|
|
|
156
157
|
paths[`/items/${collection}`] = {};
|
|
157
158
|
if (!paths[`/items/${collection}/{id}`])
|
|
158
159
|
paths[`/items/${collection}/{id}`] = {};
|
|
159
|
-
if (listBase[method]) {
|
|
160
|
+
if (listBase?.[method]) {
|
|
160
161
|
paths[`/items/${collection}`][method] = mergeWith(cloneDeep(listBase[method]), {
|
|
161
162
|
description: listBase[method].description.replace('item', collection + ' item'),
|
|
162
163
|
tags: [tag.name],
|
|
@@ -208,7 +209,7 @@ class OASSpecsService {
|
|
|
208
209
|
return undefined;
|
|
209
210
|
});
|
|
210
211
|
}
|
|
211
|
-
if (detailBase[method]) {
|
|
212
|
+
if (detailBase?.[method]) {
|
|
212
213
|
paths[`/items/${collection}/{id}`][method] = mergeWith(cloneDeep(detailBase[method]), {
|
|
213
214
|
description: detailBase[method].description.replace('item', collection + ' item'),
|
|
214
215
|
tags: [tag.name],
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Item, PrimaryKey } from '@directus/types';
|
|
2
|
+
import { ItemsService } from './items.js';
|
|
3
|
+
import type { AbstractServiceOptions } from '../types/services.js';
|
|
4
|
+
import type { MutationOptions } from '../types/items.js';
|
|
5
|
+
export declare class TranslationsService extends ItemsService {
|
|
6
|
+
constructor(options: AbstractServiceOptions);
|
|
7
|
+
private translationKeyExists;
|
|
8
|
+
createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
|
|
9
|
+
updateMany(keys: PrimaryKey[], data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey[]>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import getDatabase from '../database/index.js';
|
|
2
|
+
import { ItemsService } from './items.js';
|
|
3
|
+
import { InvalidPayloadException } from '../index.js';
|
|
4
|
+
export class TranslationsService extends ItemsService {
|
|
5
|
+
constructor(options) {
|
|
6
|
+
super('directus_translations', options);
|
|
7
|
+
this.knex = options.knex || getDatabase();
|
|
8
|
+
this.accountability = options.accountability || null;
|
|
9
|
+
this.schema = options.schema;
|
|
10
|
+
}
|
|
11
|
+
async translationKeyExists(key, language) {
|
|
12
|
+
const result = await this.knex.select('id').from(this.collection).where({ key, language });
|
|
13
|
+
return result.length > 0;
|
|
14
|
+
}
|
|
15
|
+
async createOne(data, opts) {
|
|
16
|
+
if (await this.translationKeyExists(data['key'], data['language'])) {
|
|
17
|
+
throw new InvalidPayloadException('Duplicate key and language combination.');
|
|
18
|
+
}
|
|
19
|
+
return await super.createOne(data, opts);
|
|
20
|
+
}
|
|
21
|
+
async updateMany(keys, data, opts) {
|
|
22
|
+
if (keys.length > 0 && 'key' in data && 'language' in data) {
|
|
23
|
+
throw new InvalidPayloadException('Duplicate key and language combination.');
|
|
24
|
+
}
|
|
25
|
+
else if ('key' in data || 'language' in data) {
|
|
26
|
+
const items = await this.readMany(keys);
|
|
27
|
+
for (const item of items) {
|
|
28
|
+
const updatedData = { ...item, ...data };
|
|
29
|
+
if (await this.translationKeyExists(updatedData['key'], updatedData['language'])) {
|
|
30
|
+
throw new InvalidPayloadException('Duplicate key and language combination.');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return await super.updateMany(keys, data, opts);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { Redis } from 'ioredis';
|
|
2
|
+
import env from './env.js';
|
|
3
|
+
import { getConfigFromEnv } from './utils/get-config-from-env.js';
|
|
4
|
+
let synchronizationManager;
|
|
5
|
+
function getSynchronizationManager() {
|
|
6
|
+
if (synchronizationManager)
|
|
7
|
+
return synchronizationManager;
|
|
8
|
+
if (env['SYNCHRONIZATION_STORE'] === 'redis') {
|
|
9
|
+
synchronizationManager = new SynchronizationManagerRedis();
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
synchronizationManager = new SynchronizationManagerMemory();
|
|
13
|
+
}
|
|
14
|
+
return synchronizationManager;
|
|
15
|
+
}
|
|
16
|
+
class SynchronizationManagerMemory {
|
|
17
|
+
store;
|
|
18
|
+
constructor() {
|
|
19
|
+
this.store = {};
|
|
20
|
+
}
|
|
21
|
+
async set(key, value) {
|
|
22
|
+
this.setSync(key, value);
|
|
23
|
+
}
|
|
24
|
+
async get(key) {
|
|
25
|
+
return this.getSync(key);
|
|
26
|
+
}
|
|
27
|
+
async delete(key) {
|
|
28
|
+
this.deleteSync(key);
|
|
29
|
+
}
|
|
30
|
+
async exists(key) {
|
|
31
|
+
return this.existsSync(key);
|
|
32
|
+
}
|
|
33
|
+
async setGreaterThan(key, value) {
|
|
34
|
+
if (this.existsSync(key)) {
|
|
35
|
+
const oldValue = Number(this.getSync(key));
|
|
36
|
+
if (value <= oldValue) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
this.setSync(key, value);
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
setSync(key, value) {
|
|
44
|
+
this.store[key] = String(value);
|
|
45
|
+
}
|
|
46
|
+
getSync(key) {
|
|
47
|
+
return this.store[key] ?? null;
|
|
48
|
+
}
|
|
49
|
+
deleteSync(key) {
|
|
50
|
+
delete this.store[key];
|
|
51
|
+
}
|
|
52
|
+
existsSync(key) {
|
|
53
|
+
return key in this.store;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const SET_GREATER_THAN_SCRIPT = `
|
|
57
|
+
local key = KEYS[1]
|
|
58
|
+
local value = tonumber(ARGV[1])
|
|
59
|
+
|
|
60
|
+
if redis.call("EXISTS", key) == 1 then
|
|
61
|
+
local oldValue = tonumber(redis.call('GET', key))
|
|
62
|
+
|
|
63
|
+
if value <= oldValue then
|
|
64
|
+
return false
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
redis.call('SET', key, value)
|
|
69
|
+
|
|
70
|
+
return true
|
|
71
|
+
`;
|
|
72
|
+
class SynchronizationManagerRedis {
|
|
73
|
+
namespace;
|
|
74
|
+
client;
|
|
75
|
+
constructor() {
|
|
76
|
+
const config = getConfigFromEnv('SYNCHRONIZATION_REDIS');
|
|
77
|
+
this.client = new Redis(env['SYNCHRONIZATION_REDIS'] ?? config);
|
|
78
|
+
this.namespace = env['SYNCHRONIZATION_NAMESPACE'] ?? 'directus';
|
|
79
|
+
this.client.defineCommand('setGreaterThan', {
|
|
80
|
+
numberOfKeys: 1,
|
|
81
|
+
lua: SET_GREATER_THAN_SCRIPT,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async set(key, value) {
|
|
85
|
+
await this.client.set(this.getNamespacedKey(key), value);
|
|
86
|
+
}
|
|
87
|
+
get(key) {
|
|
88
|
+
return this.client.get(this.getNamespacedKey(key));
|
|
89
|
+
}
|
|
90
|
+
async delete(key) {
|
|
91
|
+
await this.client.del(this.getNamespacedKey(key));
|
|
92
|
+
}
|
|
93
|
+
async exists(key) {
|
|
94
|
+
const doesExist = await this.client.exists(this.getNamespacedKey(key));
|
|
95
|
+
return doesExist === 1;
|
|
96
|
+
}
|
|
97
|
+
async setGreaterThan(key, value) {
|
|
98
|
+
const client = this.client;
|
|
99
|
+
const wasSet = await client.setGreaterThan(this.getNamespacedKey(key), value);
|
|
100
|
+
return wasSet === 1;
|
|
101
|
+
}
|
|
102
|
+
getNamespacedKey(key) {
|
|
103
|
+
return `${this.namespace}:${key}`;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
export class SynchronizedClock {
|
|
107
|
+
key;
|
|
108
|
+
synchronizationManager;
|
|
109
|
+
constructor(id) {
|
|
110
|
+
this.key = `clock:${id}`;
|
|
111
|
+
this.synchronizationManager = getSynchronizationManager();
|
|
112
|
+
}
|
|
113
|
+
async set(timestamp) {
|
|
114
|
+
const wasSet = await this.synchronizationManager.setGreaterThan(this.key, timestamp);
|
|
115
|
+
return wasSet;
|
|
116
|
+
}
|
|
117
|
+
async reset() {
|
|
118
|
+
await this.synchronizationManager.delete(this.key);
|
|
119
|
+
}
|
|
120
|
+
}
|
package/dist/types/assets.d.ts
CHANGED
|
@@ -6,10 +6,15 @@ export type TransformationMap = {
|
|
|
6
6
|
};
|
|
7
7
|
export type Transformation = TransformationMap[keyof TransformationMap];
|
|
8
8
|
export type TransformationResize = Pick<ResizeOptions, 'width' | 'height' | 'fit' | 'withoutEnlargement'>;
|
|
9
|
+
export type TransformationFormat = 'jpg' | 'jpeg' | 'png' | 'webp' | 'tiff' | 'avif';
|
|
9
10
|
export type TransformationParams = {
|
|
10
11
|
key?: string;
|
|
11
12
|
transforms?: Transformation[];
|
|
12
|
-
format?:
|
|
13
|
+
format?: TransformationFormat | 'auto';
|
|
13
14
|
quality?: number;
|
|
14
15
|
} & TransformationResize;
|
|
16
|
+
export type TransformationSet = {
|
|
17
|
+
transformationParams: TransformationParams;
|
|
18
|
+
acceptFormat?: TransformationFormat | undefined;
|
|
19
|
+
};
|
|
15
20
|
export {};
|
package/dist/types/events.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ActionHandler, FilterHandler, InitHandler } from '@directus/types';
|
|
2
|
-
import type {
|
|
2
|
+
import type { ScheduledJob } from '../utils/schedule.js';
|
|
3
3
|
export type EventHandler = {
|
|
4
4
|
type: 'filter';
|
|
5
5
|
name: string;
|
|
@@ -14,5 +14,5 @@ export type EventHandler = {
|
|
|
14
14
|
handler: InitHandler;
|
|
15
15
|
} | {
|
|
16
16
|
type: 'schedule';
|
|
17
|
-
|
|
17
|
+
job: ScheduledJob;
|
|
18
18
|
};
|
|
@@ -11,6 +11,7 @@ export default function applyQuery(knex: Knex, collection: string, dbQuery: Knex
|
|
|
11
11
|
hasMultiRelationalSort?: boolean | undefined;
|
|
12
12
|
}): {
|
|
13
13
|
query: Knex.QueryBuilder<any, any>;
|
|
14
|
+
hasJoins: boolean;
|
|
14
15
|
hasMultiRelationalFilter: boolean;
|
|
15
16
|
};
|
|
16
17
|
export type ColumnSortRecord = {
|
|
@@ -22,13 +23,19 @@ export declare function applySort(knex: Knex, schema: SchemaOverview, rootQuery:
|
|
|
22
23
|
order: "asc" | "desc";
|
|
23
24
|
column: any;
|
|
24
25
|
}[];
|
|
26
|
+
hasJoins: boolean;
|
|
25
27
|
hasMultiRelationalSort: boolean;
|
|
26
|
-
} |
|
|
28
|
+
} | {
|
|
29
|
+
hasJoins: boolean;
|
|
30
|
+
hasMultiRelationalSort: boolean;
|
|
31
|
+
sortRecords?: never;
|
|
32
|
+
};
|
|
27
33
|
export declare function applyLimit(knex: Knex, rootQuery: Knex.QueryBuilder, limit: any): void;
|
|
28
34
|
export declare function applyOffset(knex: Knex, rootQuery: Knex.QueryBuilder, offset: any): void;
|
|
29
35
|
export declare function applyFilter(knex: Knex, schema: SchemaOverview, rootQuery: Knex.QueryBuilder, rootFilter: Filter, collection: string, aliasMap: AliasMap): {
|
|
30
36
|
query: Knex.QueryBuilder<any, any>;
|
|
37
|
+
hasJoins: boolean;
|
|
31
38
|
hasMultiRelationalFilter: boolean;
|
|
32
39
|
};
|
|
33
40
|
export declare function applySearch(schema: SchemaOverview, dbQuery: Knex.QueryBuilder, searchQuery: string, collection: string): Promise<void>;
|
|
34
|
-
export declare function applyAggregate(dbQuery: Knex.QueryBuilder, aggregate: Aggregate, collection: string): void;
|
|
41
|
+
export declare function applyAggregate(schema: SchemaOverview, dbQuery: Knex.QueryBuilder, aggregate: Aggregate, collection: string, hasJoins: boolean): void;
|
|
@@ -15,6 +15,7 @@ export const generateAlias = customAlphabet('abcdefghijklmnopqrstuvwxyz', 5);
|
|
|
15
15
|
*/
|
|
16
16
|
export default function applyQuery(knex, collection, dbQuery, query, schema, options) {
|
|
17
17
|
const aliasMap = options?.aliasMap ?? Object.create(null);
|
|
18
|
+
let hasJoins = false;
|
|
18
19
|
let hasMultiRelationalFilter = false;
|
|
19
20
|
applyLimit(knex, dbQuery, query.limit);
|
|
20
21
|
if (query.offset) {
|
|
@@ -24,7 +25,10 @@ export default function applyQuery(knex, collection, dbQuery, query, schema, opt
|
|
|
24
25
|
applyOffset(knex, dbQuery, query.limit * (query.page - 1));
|
|
25
26
|
}
|
|
26
27
|
if (query.sort && !options?.isInnerQuery && !options?.hasMultiRelationalSort) {
|
|
27
|
-
applySort(knex, schema, dbQuery, query.sort, collection, aliasMap);
|
|
28
|
+
const sortResult = applySort(knex, schema, dbQuery, query.sort, collection, aliasMap);
|
|
29
|
+
if (!hasJoins) {
|
|
30
|
+
hasJoins = sortResult.hasJoins;
|
|
31
|
+
}
|
|
28
32
|
}
|
|
29
33
|
if (query.search) {
|
|
30
34
|
applySearch(schema, dbQuery, query.search, collection);
|
|
@@ -32,19 +36,24 @@ export default function applyQuery(knex, collection, dbQuery, query, schema, opt
|
|
|
32
36
|
if (query.group) {
|
|
33
37
|
dbQuery.groupBy(query.group.map((column) => getColumn(knex, collection, column, false, schema)));
|
|
34
38
|
}
|
|
35
|
-
if (query.aggregate) {
|
|
36
|
-
applyAggregate(dbQuery, query.aggregate, collection);
|
|
37
|
-
}
|
|
38
39
|
if (query.filter) {
|
|
39
|
-
|
|
40
|
+
const filterResult = applyFilter(knex, schema, dbQuery, query.filter, collection, aliasMap);
|
|
41
|
+
if (!hasJoins) {
|
|
42
|
+
hasJoins = filterResult.hasJoins;
|
|
43
|
+
}
|
|
44
|
+
hasMultiRelationalFilter = filterResult.hasMultiRelationalFilter;
|
|
45
|
+
}
|
|
46
|
+
if (query.aggregate) {
|
|
47
|
+
applyAggregate(schema, dbQuery, query.aggregate, collection, hasJoins);
|
|
40
48
|
}
|
|
41
|
-
return { query: dbQuery, hasMultiRelationalFilter };
|
|
49
|
+
return { query: dbQuery, hasJoins, hasMultiRelationalFilter };
|
|
42
50
|
}
|
|
43
51
|
function addJoin({ path, collection, aliasMap, rootQuery, schema, relations, knex }) {
|
|
44
52
|
let hasMultiRelational = false;
|
|
53
|
+
let isJoinAdded = false;
|
|
45
54
|
path = clone(path);
|
|
46
55
|
followRelation(path);
|
|
47
|
-
return hasMultiRelational;
|
|
56
|
+
return { hasMultiRelational, isJoinAdded };
|
|
48
57
|
function followRelation(pathParts, parentCollection = collection, parentFields) {
|
|
49
58
|
/**
|
|
50
59
|
* For A2M fields, the path can contain an optional collection scope <field>:<scope>
|
|
@@ -65,6 +74,7 @@ function addJoin({ path, collection, aliasMap, rootQuery, schema, relations, kne
|
|
|
65
74
|
if (relationType === 'm2o') {
|
|
66
75
|
rootQuery.leftJoin({ [alias]: relation.related_collection }, `${aliasedParentCollection}.${relation.field}`, `${alias}.${schema.collections[relation.related_collection].primary}`);
|
|
67
76
|
aliasMap[aliasKey].collection = relation.related_collection;
|
|
77
|
+
isJoinAdded = true;
|
|
68
78
|
}
|
|
69
79
|
else if (relationType === 'a2o') {
|
|
70
80
|
const pathScope = pathParts[0].split(':')[1];
|
|
@@ -77,6 +87,7 @@ function addJoin({ path, collection, aliasMap, rootQuery, schema, relations, kne
|
|
|
77
87
|
.andOn(`${aliasedParentCollection}.${relation.field}`, '=', knex.raw(getHelpers(knex).schema.castA2oPrimaryKey(), `${alias}.${schema.collections[pathScope].primary}`));
|
|
78
88
|
});
|
|
79
89
|
aliasMap[aliasKey].collection = pathScope;
|
|
90
|
+
isJoinAdded = true;
|
|
80
91
|
}
|
|
81
92
|
else if (relationType === 'o2a') {
|
|
82
93
|
rootQuery.leftJoin({ [alias]: relation.collection }, (joinClause) => {
|
|
@@ -86,11 +97,13 @@ function addJoin({ path, collection, aliasMap, rootQuery, schema, relations, kne
|
|
|
86
97
|
});
|
|
87
98
|
aliasMap[aliasKey].collection = relation.collection;
|
|
88
99
|
hasMultiRelational = true;
|
|
100
|
+
isJoinAdded = true;
|
|
89
101
|
}
|
|
90
102
|
else if (relationType === 'o2m') {
|
|
91
103
|
rootQuery.leftJoin({ [alias]: relation.collection }, `${aliasedParentCollection}.${schema.collections[relation.related_collection].primary}`, `${alias}.${relation.field}`);
|
|
92
104
|
aliasMap[aliasKey].collection = relation.collection;
|
|
93
105
|
hasMultiRelational = true;
|
|
106
|
+
isJoinAdded = true;
|
|
94
107
|
}
|
|
95
108
|
}
|
|
96
109
|
let parent;
|
|
@@ -114,6 +127,7 @@ function addJoin({ path, collection, aliasMap, rootQuery, schema, relations, kne
|
|
|
114
127
|
}
|
|
115
128
|
export function applySort(knex, schema, rootQuery, rootSort, collection, aliasMap, returnRecords = false) {
|
|
116
129
|
const relations = schema.relations;
|
|
130
|
+
let hasJoins = false;
|
|
117
131
|
let hasMultiRelationalSort = false;
|
|
118
132
|
const sortRecords = rootSort.map((sortField) => {
|
|
119
133
|
const column = sortField.split('.');
|
|
@@ -134,7 +148,7 @@ export function applySort(knex, schema, rootQuery, rootSort, collection, aliasMa
|
|
|
134
148
|
};
|
|
135
149
|
}
|
|
136
150
|
}
|
|
137
|
-
const hasMultiRelational = addJoin({
|
|
151
|
+
const { hasMultiRelational, isJoinAdded } = addJoin({
|
|
138
152
|
path: column,
|
|
139
153
|
collection,
|
|
140
154
|
aliasMap,
|
|
@@ -151,6 +165,9 @@ export function applySort(knex, schema, rootQuery, rootSort, collection, aliasMa
|
|
|
151
165
|
schema,
|
|
152
166
|
});
|
|
153
167
|
const [alias, field] = columnPath.split('.');
|
|
168
|
+
if (!hasJoins) {
|
|
169
|
+
hasJoins = isJoinAdded;
|
|
170
|
+
}
|
|
154
171
|
if (!hasMultiRelationalSort) {
|
|
155
172
|
hasMultiRelationalSort = hasMultiRelational;
|
|
156
173
|
}
|
|
@@ -160,11 +177,11 @@ export function applySort(knex, schema, rootQuery, rootSort, collection, aliasMa
|
|
|
160
177
|
};
|
|
161
178
|
});
|
|
162
179
|
if (returnRecords)
|
|
163
|
-
return { sortRecords, hasMultiRelationalSort };
|
|
180
|
+
return { sortRecords, hasJoins, hasMultiRelationalSort };
|
|
164
181
|
// Clears the order if any, eg: from MSSQL offset
|
|
165
182
|
rootQuery.clear('order');
|
|
166
183
|
rootQuery.orderBy(sortRecords);
|
|
167
|
-
return
|
|
184
|
+
return { hasJoins, hasMultiRelationalSort };
|
|
168
185
|
}
|
|
169
186
|
export function applyLimit(knex, rootQuery, limit) {
|
|
170
187
|
if (typeof limit === 'number') {
|
|
@@ -179,10 +196,11 @@ export function applyOffset(knex, rootQuery, offset) {
|
|
|
179
196
|
export function applyFilter(knex, schema, rootQuery, rootFilter, collection, aliasMap) {
|
|
180
197
|
const helpers = getHelpers(knex);
|
|
181
198
|
const relations = schema.relations;
|
|
199
|
+
let hasJoins = false;
|
|
182
200
|
let hasMultiRelationalFilter = false;
|
|
183
201
|
addJoins(rootQuery, rootFilter, collection);
|
|
184
202
|
addWhereClauses(knex, rootQuery, rootFilter, collection);
|
|
185
|
-
return { query: rootQuery, hasMultiRelationalFilter };
|
|
203
|
+
return { query: rootQuery, hasJoins, hasMultiRelationalFilter };
|
|
186
204
|
function addJoins(dbQuery, filter, collection) {
|
|
187
205
|
for (const [key, value] of Object.entries(filter)) {
|
|
188
206
|
if (key === '_or' || key === '_and') {
|
|
@@ -198,8 +216,8 @@ export function applyFilter(knex, schema, rootQuery, rootFilter, collection, ali
|
|
|
198
216
|
}
|
|
199
217
|
const filterPath = getFilterPath(key, value);
|
|
200
218
|
if (filterPath.length > 1 ||
|
|
201
|
-
(!(key.includes('(') && key.includes(')')) && schema.collections[collection]
|
|
202
|
-
const hasMultiRelational = addJoin({
|
|
219
|
+
(!(key.includes('(') && key.includes(')')) && schema.collections[collection]?.fields[key]?.type === 'alias')) {
|
|
220
|
+
const { hasMultiRelational, isJoinAdded } = addJoin({
|
|
203
221
|
path: filterPath,
|
|
204
222
|
collection,
|
|
205
223
|
knex,
|
|
@@ -208,6 +226,9 @@ export function applyFilter(knex, schema, rootQuery, rootFilter, collection, ali
|
|
|
208
226
|
rootQuery,
|
|
209
227
|
aliasMap,
|
|
210
228
|
});
|
|
229
|
+
if (!hasJoins) {
|
|
230
|
+
hasJoins = isJoinAdded;
|
|
231
|
+
}
|
|
211
232
|
if (!hasMultiRelationalFilter) {
|
|
212
233
|
hasMultiRelationalFilter = hasMultiRelational;
|
|
213
234
|
}
|
|
@@ -238,7 +259,7 @@ export function applyFilter(knex, schema, rootQuery, rootFilter, collection, ali
|
|
|
238
259
|
const { relation, relationType } = getRelationInfo(relations, collection, pathRoot);
|
|
239
260
|
const { operator: filterOperator, value: filterValue } = getOperation(key, value);
|
|
240
261
|
if (filterPath.length > 1 ||
|
|
241
|
-
(!(key.includes('(') && key.includes(')')) && schema.collections[collection]
|
|
262
|
+
(!(key.includes('(') && key.includes(')')) && schema.collections[collection]?.fields[key]?.type === 'alias')) {
|
|
242
263
|
if (!relation)
|
|
243
264
|
continue;
|
|
244
265
|
if (relationType === 'o2m' || relationType === 'o2a') {
|
|
@@ -503,7 +524,7 @@ function validateNumber(value, parsed) {
|
|
|
503
524
|
// (prevent unintended number parsing, e.g. String(7) !== "ob111")
|
|
504
525
|
return String(parsed) === value;
|
|
505
526
|
}
|
|
506
|
-
export function applyAggregate(dbQuery, aggregate, collection) {
|
|
527
|
+
export function applyAggregate(schema, dbQuery, aggregate, collection, hasJoins) {
|
|
507
528
|
for (const [operation, fields] of Object.entries(aggregate)) {
|
|
508
529
|
if (!fields)
|
|
509
530
|
continue;
|
|
@@ -526,7 +547,13 @@ export function applyAggregate(dbQuery, aggregate, collection) {
|
|
|
526
547
|
}
|
|
527
548
|
}
|
|
528
549
|
if (operation === 'countDistinct') {
|
|
529
|
-
|
|
550
|
+
if (!hasJoins && schema.collections[collection]?.primary === field) {
|
|
551
|
+
// Optimize to count as primary keys are unique
|
|
552
|
+
dbQuery.count(`${collection}.${field}`, { as: `countDistinct->${field}` });
|
|
553
|
+
}
|
|
554
|
+
else {
|
|
555
|
+
dbQuery.countDistinct(`${collection}.${field}`, { as: `countDistinct->${field}` });
|
|
556
|
+
}
|
|
530
557
|
}
|
|
531
558
|
if (operation === 'sum') {
|
|
532
559
|
dbQuery.sum(`${collection}.${field}`, { as: `sum->${field}` });
|
package/dist/utils/md.js
CHANGED