@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.
Files changed (68) hide show
  1. package/dist/app.js +4 -3
  2. package/dist/auth/drivers/oauth2.js +1 -1
  3. package/dist/auth/drivers/openid.js +1 -1
  4. package/dist/cli/utils/create-env/env-stub.liquid +7 -0
  5. package/dist/constants.d.ts +0 -1
  6. package/dist/constants.js +0 -1
  7. package/dist/controllers/assets.js +6 -10
  8. package/dist/controllers/files.js +19 -1
  9. package/dist/controllers/permissions.js +7 -4
  10. package/dist/controllers/translations.d.ts +2 -0
  11. package/dist/controllers/translations.js +149 -0
  12. package/dist/controllers/users.js +1 -1
  13. package/dist/database/migrations/20230525A-add-preview-settings.d.ts +3 -0
  14. package/dist/database/migrations/20230525A-add-preview-settings.js +10 -0
  15. package/dist/database/migrations/20230526A-migrate-translation-strings.d.ts +3 -0
  16. package/dist/database/migrations/20230526A-migrate-translation-strings.js +54 -0
  17. package/dist/database/run-ast.js +3 -3
  18. package/dist/database/system-data/app-access-permissions/app-access-permissions.yaml +3 -0
  19. package/dist/database/system-data/collections/collections.yaml +23 -0
  20. package/dist/database/system-data/fields/collections.yaml +16 -0
  21. package/dist/database/system-data/fields/settings.yaml +0 -5
  22. package/dist/database/system-data/fields/translations.yaml +27 -0
  23. package/dist/env.js +17 -0
  24. package/dist/exceptions/content-too-large.d.ts +4 -0
  25. package/dist/exceptions/content-too-large.js +6 -0
  26. package/dist/extensions.js +13 -11
  27. package/dist/flows.d.ts +1 -1
  28. package/dist/flows.js +20 -19
  29. package/dist/logger.d.ts +1 -1
  30. package/dist/logger.js +6 -6
  31. package/dist/server.js +0 -11
  32. package/dist/services/assets.d.ts +2 -2
  33. package/dist/services/collections.js +8 -7
  34. package/dist/services/fields.js +7 -5
  35. package/dist/services/files.d.ts +2 -2
  36. package/dist/services/files.js +4 -9
  37. package/dist/services/graphql/index.js +4 -41
  38. package/dist/services/index.d.ts +1 -0
  39. package/dist/services/index.js +1 -0
  40. package/dist/services/items.js +10 -9
  41. package/dist/services/revisions.d.ts +6 -1
  42. package/dist/services/revisions.js +24 -0
  43. package/dist/services/server.js +3 -17
  44. package/dist/services/specifications.d.ts +2 -2
  45. package/dist/services/specifications.js +6 -5
  46. package/dist/services/translations.d.ts +10 -0
  47. package/dist/services/translations.js +36 -0
  48. package/dist/synchronization.d.ts +7 -0
  49. package/dist/synchronization.js +120 -0
  50. package/dist/types/assets.d.ts +6 -1
  51. package/dist/types/events.d.ts +2 -2
  52. package/dist/utils/apply-query.d.ts +9 -2
  53. package/dist/utils/apply-query.js +43 -16
  54. package/dist/utils/md.js +1 -1
  55. package/dist/utils/redact.d.ts +11 -0
  56. package/dist/utils/redact.js +75 -0
  57. package/dist/utils/sanitize-query.js +10 -1
  58. package/dist/utils/schedule.d.ts +5 -0
  59. package/dist/utils/schedule.js +27 -0
  60. package/dist/utils/should-clear-cache.d.ts +10 -0
  61. package/dist/utils/should-clear-cache.js +18 -0
  62. package/dist/utils/should-skip-cache.js +18 -2
  63. package/dist/utils/transformations.d.ts +2 -2
  64. package/dist/utils/transformations.js +27 -10
  65. package/dist/utils/validate-query.js +3 -1
  66. package/package.json +49 -53
  67. package/dist/utils/get-os-info.d.ts +0 -9
  68. package/dist/utils/get-os-info.js +0 -40
@@ -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 }, { bypassLimits: true });
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 && env['CACHE_AUTO_PURGE'] && opts.autoPurgeCache !== false) {
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 && env['CACHE_AUTO_PURGE'] && opts.autoPurgeCache !== false) {
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 && env['CACHE_AUTO_PURGE'] && opts.autoPurgeCache !== false) {
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, { bypassLimits: true });
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 }, { bypassLimits: true });
530
+ await revisionsService.updateMany(childrenRevisions, { parent: revisionID });
530
531
  }
531
532
  }
532
533
  }
533
534
  }
534
535
  }
535
536
  });
536
- if (this.cache && env['CACHE_AUTO_PURGE'] && opts.autoPurgeCache !== false) {
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 && env['CACHE_AUTO_PURGE'] && opts.autoPurgeCache !== false) {
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 && env['CACHE_AUTO_PURGE'] && opts?.autoPurgeCache !== false) {
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
  }
@@ -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
- if (this.accountability?.admin === true) {
69
- const { osType, osVersion } = getOSInfo();
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
- for (const method of ['post', 'get', 'patch', 'delete']) {
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,7 @@
1
+ export declare class SynchronizedClock {
2
+ private key;
3
+ private synchronizationManager;
4
+ constructor(id: string);
5
+ set(timestamp: number): Promise<boolean>;
6
+ reset(): Promise<void>;
7
+ }
@@ -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
+ }
@@ -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?: 'auto' | 'jpg' | 'jpeg' | 'png' | 'webp' | 'tiff' | 'avif';
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 {};
@@ -1,5 +1,5 @@
1
1
  import type { ActionHandler, FilterHandler, InitHandler } from '@directus/types';
2
- import type { ScheduledTask } from 'node-cron';
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
- task: ScheduledTask;
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
- } | undefined;
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
- hasMultiRelationalFilter = applyFilter(knex, schema, dbQuery, query.filter, collection, aliasMap).hasMultiRelationalFilter;
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 undefined;
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].fields[key].type === 'alias')) {
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].fields[key].type === 'alias')) {
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
- dbQuery.countDistinct(`${collection}.${field}`, { as: `countDistinct->${field}` });
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
@@ -4,5 +4,5 @@ import sanitizeHTML from 'sanitize-html';
4
4
  * Render and sanitize a markdown string
5
5
  */
6
6
  export function md(str) {
7
- return sanitizeHTML(marked(str));
7
+ return sanitizeHTML(marked(str, { headerIds: false, mangle: false }));
8
8
  }