@directus/api 13.2.0 → 14.0.1

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 (109) hide show
  1. package/dist/__utils__/snapshots.js +9 -0
  2. package/dist/app.js +2 -0
  3. package/dist/cli/index.js +3 -4
  4. package/dist/cli/load-extensions.d.ts +1 -0
  5. package/dist/cli/load-extensions.js +19 -0
  6. package/dist/controllers/extensions.js +28 -19
  7. package/dist/controllers/versions.d.ts +2 -0
  8. package/dist/controllers/versions.js +188 -0
  9. package/dist/database/migrations/20230823A-add-content-versioning.d.ts +3 -0
  10. package/dist/database/migrations/20230823A-add-content-versioning.js +36 -0
  11. package/dist/database/migrations/20230927A-themes.d.ts +3 -0
  12. package/dist/database/migrations/20230927A-themes.js +49 -0
  13. package/dist/database/migrations/20231009B-update-panel-options.d.ts +3 -0
  14. package/dist/database/migrations/20231009B-update-panel-options.js +77 -0
  15. package/dist/database/migrations/20231010A-add-extensions.d.ts +3 -0
  16. package/dist/database/migrations/20231010A-add-extensions.js +9 -0
  17. package/dist/database/run-ast.js +1 -1
  18. package/dist/database/system-data/app-access-permissions/app-access-permissions.yaml +5 -1
  19. package/dist/database/system-data/collections/collections.yaml +6 -0
  20. package/dist/database/system-data/fields/activity.yaml +4 -4
  21. package/dist/database/system-data/fields/collections.yaml +19 -0
  22. package/dist/database/system-data/fields/extensions.yaml +10 -0
  23. package/dist/database/system-data/fields/revisions.yaml +3 -0
  24. package/dist/database/system-data/fields/settings.yaml +73 -17
  25. package/dist/database/system-data/fields/users.yaml +50 -12
  26. package/dist/database/system-data/fields/versions.yaml +38 -0
  27. package/dist/database/system-data/fields/webhooks.yaml +9 -9
  28. package/dist/database/system-data/relations/relations.yaml +88 -20
  29. package/dist/emitter.d.ts +2 -2
  30. package/dist/env.js +4 -0
  31. package/dist/extensions/lib/get-extensions-settings.d.ts +7 -0
  32. package/dist/extensions/lib/get-extensions-settings.js +39 -0
  33. package/dist/extensions/lib/get-extensions.d.ts +1 -0
  34. package/dist/extensions/lib/get-extensions.js +11 -0
  35. package/dist/extensions/{get-shared-deps-mapping.js → lib/get-shared-deps-mapping.js} +3 -3
  36. package/dist/extensions/lib/sandbox/generate-api-extensions-sandbox-entrypoint.d.ts +31 -0
  37. package/dist/extensions/lib/sandbox/generate-api-extensions-sandbox-entrypoint.js +80 -0
  38. package/dist/extensions/lib/sandbox/generate-host-function-reference.d.ts +11 -0
  39. package/dist/extensions/lib/sandbox/generate-host-function-reference.js +28 -0
  40. package/dist/extensions/lib/sandbox/register/action.d.ts +6 -0
  41. package/dist/extensions/lib/sandbox/register/action.js +18 -0
  42. package/dist/extensions/lib/sandbox/register/call-reference.d.ts +5 -0
  43. package/dist/extensions/lib/sandbox/register/call-reference.js +20 -0
  44. package/dist/extensions/lib/sandbox/register/filter.d.ts +6 -0
  45. package/dist/extensions/lib/sandbox/register/filter.js +21 -0
  46. package/dist/extensions/lib/sandbox/register/index.d.ts +5 -0
  47. package/dist/extensions/lib/sandbox/register/index.js +5 -0
  48. package/dist/extensions/lib/sandbox/register/operation.d.ts +6 -0
  49. package/dist/extensions/lib/sandbox/register/operation.js +19 -0
  50. package/dist/extensions/lib/sandbox/register/route.d.ts +17 -0
  51. package/dist/extensions/lib/sandbox/register/route.js +44 -0
  52. package/dist/extensions/lib/sandbox/sdk/generators/index.d.ts +3 -0
  53. package/dist/extensions/lib/sandbox/sdk/generators/index.js +3 -0
  54. package/dist/extensions/lib/sandbox/sdk/generators/log.d.ts +3 -0
  55. package/dist/extensions/lib/sandbox/sdk/generators/log.js +11 -0
  56. package/dist/extensions/lib/sandbox/sdk/generators/request.d.ts +12 -0
  57. package/dist/extensions/lib/sandbox/sdk/generators/request.js +49 -0
  58. package/dist/extensions/lib/sandbox/sdk/generators/sleep.d.ts +3 -0
  59. package/dist/extensions/lib/sandbox/sdk/generators/sleep.js +11 -0
  60. package/dist/extensions/lib/sandbox/sdk/index.d.ts +2 -0
  61. package/dist/extensions/lib/sandbox/sdk/index.js +2 -0
  62. package/dist/extensions/lib/sandbox/sdk/instantiate.d.ts +11 -0
  63. package/dist/extensions/lib/sandbox/sdk/instantiate.js +28 -0
  64. package/dist/extensions/lib/sandbox/sdk/sdk.d.ts +20 -0
  65. package/dist/extensions/lib/sandbox/sdk/sdk.js +11 -0
  66. package/dist/extensions/lib/sandbox/sdk/utils/index.d.ts +1 -0
  67. package/dist/extensions/lib/sandbox/sdk/utils/index.js +1 -0
  68. package/dist/extensions/lib/sandbox/sdk/utils/wrap.d.ts +11 -0
  69. package/dist/extensions/lib/sandbox/sdk/utils/wrap.js +17 -0
  70. package/dist/extensions/manager.d.ts +128 -14
  71. package/dist/extensions/manager.js +310 -136
  72. package/dist/extensions/types.d.ts +1 -5
  73. package/dist/flows.d.ts +1 -1
  74. package/dist/flows.js +6 -6
  75. package/dist/middleware/respond.js +12 -0
  76. package/dist/server.js +2 -1
  77. package/dist/services/assets.js +1 -1
  78. package/dist/services/extensions.d.ts +31 -0
  79. package/dist/services/extensions.js +136 -0
  80. package/dist/services/graphql/index.d.ts +1 -1
  81. package/dist/services/graphql/index.js +87 -24
  82. package/dist/services/index.d.ts +2 -0
  83. package/dist/services/index.js +2 -0
  84. package/dist/services/server.js +3 -1
  85. package/dist/services/users.js +2 -0
  86. package/dist/services/versions.d.ts +21 -0
  87. package/dist/services/versions.js +238 -0
  88. package/dist/types/collection.d.ts +1 -0
  89. package/dist/utils/apply-query.d.ts +1 -1
  90. package/dist/utils/apply-query.js +30 -2
  91. package/dist/utils/delete-from-require-cache.d.ts +1 -0
  92. package/dist/utils/delete-from-require-cache.js +5 -0
  93. package/dist/utils/get-service.js +3 -1
  94. package/dist/utils/import-file-url.d.ts +5 -0
  95. package/dist/utils/import-file-url.js +6 -0
  96. package/dist/utils/job-queue.d.ts +2 -3
  97. package/dist/utils/redact-object.d.ts +1 -1
  98. package/dist/utils/redact-object.js +37 -24
  99. package/dist/utils/sanitize-query.js +3 -0
  100. package/dist/utils/validate-query.js +1 -0
  101. package/dist/worker-pool.js +8 -0
  102. package/package.json +28 -27
  103. package/dist/extensions/get-extensions.d.ts +0 -47
  104. package/dist/extensions/get-extensions.js +0 -9
  105. package/dist/extensions/normalize-extension-info.d.ts +0 -5
  106. package/dist/extensions/normalize-extension-info.js +0 -30
  107. /package/dist/extensions/{get-shared-deps-mapping.d.ts → lib/get-shared-deps-mapping.d.ts} +0 -0
  108. /package/dist/extensions/{wrap-embeds.d.ts → lib/wrap-embeds.d.ts} +0 -0
  109. /package/dist/extensions/{wrap-embeds.js → lib/wrap-embeds.js} +0 -0
@@ -0,0 +1,238 @@
1
+ import { Action } from '@directus/constants';
2
+ import { InvalidPayloadError, UnprocessableContentError } from '@directus/errors';
3
+ import Joi from 'joi';
4
+ import { assign, pick } from 'lodash-es';
5
+ import objectHash from 'object-hash';
6
+ import { getCache } from '../cache.js';
7
+ import getDatabase from '../database/index.js';
8
+ import emitter from '../emitter.js';
9
+ import { shouldClearCache } from '../utils/should-clear-cache.js';
10
+ import { ActivityService } from './activity.js';
11
+ import { AuthorizationService } from './authorization.js';
12
+ import { ItemsService } from './items.js';
13
+ import { PayloadService } from './payload.js';
14
+ import { RevisionsService } from './revisions.js';
15
+ export class VersionsService extends ItemsService {
16
+ authorizationService;
17
+ constructor(options) {
18
+ super('directus_versions', options);
19
+ this.authorizationService = new AuthorizationService({
20
+ accountability: this.accountability,
21
+ knex: this.knex,
22
+ schema: this.schema,
23
+ });
24
+ }
25
+ async validateCreateData(data) {
26
+ if (!data['key'])
27
+ throw new InvalidPayloadError({ reason: `"key" is required` });
28
+ // Reserves the "main" version key for the version query parameter
29
+ if (data['key'] === 'main')
30
+ throw new InvalidPayloadError({ reason: `"main" is a reserved version key` });
31
+ if (!data['collection']) {
32
+ throw new InvalidPayloadError({ reason: `"collection" is required` });
33
+ }
34
+ if (!data['item'])
35
+ throw new InvalidPayloadError({ reason: `"item" is required` });
36
+ const { CollectionsService } = await import('./collections.js');
37
+ const collectionsService = new CollectionsService({
38
+ accountability: null,
39
+ knex: this.knex,
40
+ schema: this.schema,
41
+ });
42
+ const existingCollection = await collectionsService.readOne(data['collection']);
43
+ if (!existingCollection.meta?.versioning) {
44
+ throw new UnprocessableContentError({
45
+ reason: `Content Versioning is not enabled for collection "${data['collection']}"`,
46
+ });
47
+ }
48
+ const existingVersions = await super.readByQuery({
49
+ aggregate: { count: ['*'] },
50
+ filter: { key: { _eq: data['key'] }, collection: { _eq: data['collection'] }, item: { _eq: data['item'] } },
51
+ });
52
+ if (existingVersions[0]['count'] > 0) {
53
+ throw new UnprocessableContentError({
54
+ reason: `Version "${data['key']}" already exists for item "${data['item']}" in collection "${data['collection']}"`,
55
+ });
56
+ }
57
+ // will throw an error if the accountability does not have permission to read the item
58
+ await this.authorizationService.checkAccess('read', data['collection'], data['item']);
59
+ }
60
+ async getMainItem(collection, item, query) {
61
+ // will throw an error if the accountability does not have permission to read the item
62
+ await this.authorizationService.checkAccess('read', collection, item);
63
+ const itemsService = new ItemsService(collection, {
64
+ knex: this.knex,
65
+ accountability: this.accountability,
66
+ schema: this.schema,
67
+ });
68
+ return await itemsService.readOne(item, query);
69
+ }
70
+ async verifyHash(collection, item, hash) {
71
+ const mainItem = await this.getMainItem(collection, item);
72
+ const mainHash = objectHash(mainItem);
73
+ return { outdated: hash !== mainHash, mainHash };
74
+ }
75
+ async getVersionSavesById(id) {
76
+ const revisionsService = new RevisionsService({
77
+ knex: this.knex,
78
+ schema: this.schema,
79
+ });
80
+ const result = await revisionsService.readByQuery({
81
+ filter: { version: { _eq: id } },
82
+ });
83
+ return result.map((revision) => revision['delta']);
84
+ }
85
+ async getVersionSaves(key, collection, item) {
86
+ const filter = {
87
+ key: { _eq: key },
88
+ collection: { _eq: collection },
89
+ };
90
+ if (item) {
91
+ filter['item'] = { _eq: item };
92
+ }
93
+ const versions = await this.readByQuery({ filter });
94
+ if (!versions?.[0])
95
+ return null;
96
+ const saves = await this.getVersionSavesById(versions[0]['id']);
97
+ return saves;
98
+ }
99
+ async createOne(data, opts) {
100
+ await this.validateCreateData(data);
101
+ const mainItem = await this.getMainItem(data['collection'], data['item']);
102
+ data['hash'] = objectHash(mainItem);
103
+ return super.createOne(data, opts);
104
+ }
105
+ async createMany(data, opts) {
106
+ if (!Array.isArray(data)) {
107
+ throw new InvalidPayloadError({ reason: 'Input should be an array of items' });
108
+ }
109
+ const keyCombos = new Set();
110
+ for (const item of data) {
111
+ await this.validateCreateData(item);
112
+ const keyCombo = `${item['key']}-${item['collection']}-${item['item']}`;
113
+ if (keyCombos.has(keyCombo)) {
114
+ throw new UnprocessableContentError({
115
+ reason: `Cannot create multiple versions on "${item['item']}" in collection "${item['collection']}" with the same key "${item['key']}"`,
116
+ });
117
+ }
118
+ keyCombos.add(keyCombo);
119
+ const mainItem = await this.getMainItem(item['collection'], item['item']);
120
+ item['hash'] = objectHash(mainItem);
121
+ }
122
+ return super.createMany(data, opts);
123
+ }
124
+ async updateMany(keys, data, opts) {
125
+ // Only allow updates on "key" and "name" fields
126
+ const versionUpdateSchema = Joi.object({
127
+ key: Joi.string(),
128
+ name: Joi.string().allow(null).optional(),
129
+ });
130
+ const { error } = versionUpdateSchema.validate(data);
131
+ if (error)
132
+ throw new InvalidPayloadError({ reason: error.message });
133
+ if ('key' in data) {
134
+ // Reserves the "main" version key for the version query parameter
135
+ if (data['key'] === 'main')
136
+ throw new InvalidPayloadError({ reason: `"main" is a reserved version key` });
137
+ const keyCombos = new Set();
138
+ for (const pk of keys) {
139
+ const { collection, item } = await this.readOne(pk, { fields: ['collection', 'item'] });
140
+ const keyCombo = `${data['key']}-${collection}-${item}`;
141
+ if (keyCombos.has(keyCombo)) {
142
+ throw new UnprocessableContentError({
143
+ reason: `Cannot update multiple versions on "${item}" in collection "${collection}" to the same key "${data['key']}"`,
144
+ });
145
+ }
146
+ keyCombos.add(keyCombo);
147
+ const existingVersions = await super.readByQuery({
148
+ aggregate: { count: ['*'] },
149
+ filter: { id: { _neq: pk }, key: { _eq: data['key'] }, collection: { _eq: collection }, item: { _eq: item } },
150
+ });
151
+ if (existingVersions[0]['count'] > 0) {
152
+ throw new UnprocessableContentError({
153
+ reason: `Version "${data['key']}" already exists for item "${item}" in collection "${collection}"`,
154
+ });
155
+ }
156
+ }
157
+ }
158
+ return super.updateMany(keys, data, opts);
159
+ }
160
+ async save(key, data) {
161
+ const version = await super.readOne(key);
162
+ const payloadService = new PayloadService(this.collection, {
163
+ accountability: this.accountability,
164
+ knex: this.knex,
165
+ schema: this.schema,
166
+ });
167
+ const activityService = new ActivityService({
168
+ knex: this.knex,
169
+ schema: this.schema,
170
+ });
171
+ const revisionsService = new RevisionsService({
172
+ knex: this.knex,
173
+ schema: this.schema,
174
+ });
175
+ const activity = await activityService.createOne({
176
+ action: Action.VERSION_SAVE,
177
+ user: this.accountability?.user ?? null,
178
+ collection: version['collection'],
179
+ ip: this.accountability?.ip ?? null,
180
+ user_agent: this.accountability?.userAgent ?? null,
181
+ origin: this.accountability?.origin ?? null,
182
+ item: version['item'],
183
+ });
184
+ const revisionDelta = await payloadService.prepareDelta(data);
185
+ await revisionsService.createOne({
186
+ activity,
187
+ version: key,
188
+ collection: version['collection'],
189
+ item: version['item'],
190
+ data: revisionDelta,
191
+ delta: revisionDelta,
192
+ });
193
+ const { cache } = getCache();
194
+ if (shouldClearCache(cache, undefined, version['collection'])) {
195
+ cache.clear();
196
+ }
197
+ return data;
198
+ }
199
+ async promote(version, mainHash, fields) {
200
+ const { id, collection, item } = (await this.readOne(version));
201
+ // will throw an error if the accountability does not have permission to update the item
202
+ await this.authorizationService.checkAccess('update', collection, item);
203
+ const { outdated } = await this.verifyHash(collection, item, mainHash);
204
+ if (outdated) {
205
+ throw new UnprocessableContentError({
206
+ reason: `Main item has changed since this version was last updated`,
207
+ });
208
+ }
209
+ const saves = await this.getVersionSavesById(id);
210
+ const versionResult = assign({}, ...saves);
211
+ const payloadToUpdate = fields ? pick(versionResult, fields) : versionResult;
212
+ const itemsService = new ItemsService(collection, {
213
+ accountability: this.accountability,
214
+ schema: this.schema,
215
+ });
216
+ const payloadAfterHooks = await emitter.emitFilter(['items.promote', `${collection}.items.promote`], payloadToUpdate, {
217
+ collection,
218
+ item,
219
+ version,
220
+ }, {
221
+ database: getDatabase(),
222
+ schema: this.schema,
223
+ accountability: this.accountability,
224
+ });
225
+ const updatedItemKey = await itemsService.updateOne(item, payloadAfterHooks);
226
+ emitter.emitAction(['items.promote', `${collection}.items.promote`], {
227
+ payload: payloadAfterHooks,
228
+ collection,
229
+ item: updatedItemKey,
230
+ version,
231
+ }, {
232
+ database: getDatabase(),
233
+ schema: this.schema,
234
+ accountability: this.accountability,
235
+ });
236
+ return updatedItemKey;
237
+ }
238
+ }
@@ -7,6 +7,7 @@ export type CollectionMeta = {
7
7
  singleton: boolean;
8
8
  icon: string | null;
9
9
  translations: Record<string, string>;
10
+ versioning: boolean;
10
11
  item_duplication_fields: string[] | null;
11
12
  accountability: 'all' | 'accountability' | null;
12
13
  group: string | null;
@@ -18,7 +18,7 @@ export type ColumnSortRecord = {
18
18
  order: 'asc' | 'desc';
19
19
  column: string;
20
20
  };
21
- export declare function applySort(knex: Knex, schema: SchemaOverview, rootQuery: Knex.QueryBuilder, rootSort: string[], collection: string, aliasMap: AliasMap, returnRecords?: boolean): {
21
+ export declare function applySort(knex: Knex, schema: SchemaOverview, rootQuery: Knex.QueryBuilder, query: Query, collection: string, aliasMap: AliasMap, returnRecords?: boolean): {
22
22
  sortRecords: {
23
23
  order: "asc" | "desc";
24
24
  column: any;
@@ -24,7 +24,7 @@ export default function applyQuery(knex, collection, dbQuery, query, schema, opt
24
24
  applyOffset(knex, dbQuery, query.limit * (query.page - 1));
25
25
  }
26
26
  if (query.sort && !options?.isInnerQuery && !options?.hasMultiRelationalSort) {
27
- const sortResult = applySort(knex, schema, dbQuery, query.sort, collection, aliasMap);
27
+ const sortResult = applySort(knex, schema, dbQuery, query, collection, aliasMap);
28
28
  if (!hasJoins) {
29
29
  hasJoins = sortResult.hasJoins;
30
30
  }
@@ -128,7 +128,9 @@ function addJoin({ path, collection, aliasMap, rootQuery, schema, relations, kne
128
128
  }
129
129
  }
130
130
  }
131
- export function applySort(knex, schema, rootQuery, rootSort, collection, aliasMap, returnRecords = false) {
131
+ export function applySort(knex, schema, rootQuery, query, collection, aliasMap, returnRecords = false) {
132
+ const rootSort = query.sort;
133
+ const aggregate = query?.aggregate;
132
134
  const relations = schema.relations;
133
135
  let hasJoins = false;
134
136
  let hasMultiRelationalSort = false;
@@ -141,6 +143,32 @@ export function applySort(knex, schema, rootQuery, rootSort, collection, aliasMa
141
143
  if (column[0].startsWith('-')) {
142
144
  column[0] = column[0].substring(1);
143
145
  }
146
+ // Is the column name one of the aggregate functions used in the query if there is any?
147
+ if (Object.keys(aggregate ?? {}).includes(column[0])) {
148
+ // If so, return the column name without the order prefix
149
+ const operation = column[0];
150
+ // Get the field for the aggregate function
151
+ const field = column[1];
152
+ // If the operation is countAll there is no field.
153
+ if (operation === 'countAll') {
154
+ return {
155
+ order,
156
+ column: 'countAll',
157
+ };
158
+ }
159
+ // If the operation is a root count there is no field.
160
+ if (operation === 'count' && (field === '*' || !field)) {
161
+ return {
162
+ order,
163
+ column: 'count',
164
+ };
165
+ }
166
+ // Return the column name with the operation and field name
167
+ return {
168
+ order,
169
+ column: returnRecords ? column[0] : `${operation}->${field}`,
170
+ };
171
+ }
144
172
  if (column.length === 1) {
145
173
  const pathRoot = column[0].split(':')[0];
146
174
  const { relation, relationType } = getRelationInfo(relations, collection, pathRoot);
@@ -0,0 +1 @@
1
+ export declare function deleteFromRequireCache(modulePath: string): void;
@@ -0,0 +1,5 @@
1
+ import { createRequire } from 'node:module';
2
+ const require = createRequire(import.meta.url);
3
+ export function deleteFromRequireCache(modulePath) {
4
+ delete require.cache[require.resolve(modulePath)];
5
+ }
@@ -1,4 +1,4 @@
1
- import { ActivityService, DashboardsService, FilesService, FlowsService, FoldersService, ItemsService, NotificationsService, OperationsService, PanelsService, PermissionsService, PresetsService, RevisionsService, RolesService, SettingsService, SharesService, UsersService, WebhooksService, } from '../services/index.js';
1
+ import { ActivityService, DashboardsService, FilesService, FlowsService, FoldersService, ItemsService, NotificationsService, OperationsService, PanelsService, PermissionsService, PresetsService, RevisionsService, RolesService, SettingsService, SharesService, UsersService, VersionsService, WebhooksService, } from '../services/index.js';
2
2
  /**
3
3
  * Select the correct service for the given collection. This allows the individual services to run
4
4
  * their custom checks (f.e. it allows UsersService to prevent updating TFA secret from outside)
@@ -43,6 +43,8 @@ export function getService(collection, opts) {
43
43
  return new UsersService(opts);
44
44
  case 'directus_webhooks':
45
45
  return new WebhooksService(opts);
46
+ case 'directus_versions':
47
+ return new VersionsService(opts);
46
48
  default:
47
49
  return new ItemsService(collection, opts);
48
50
  }
@@ -0,0 +1,5 @@
1
+ interface ImportOptions {
2
+ fresh?: boolean;
3
+ }
4
+ export declare function importFileUrl(url: string, root: string, options?: ImportOptions): Promise<any>;
5
+ export {};
@@ -0,0 +1,6 @@
1
+ import { dirname } from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { pathToRelativeUrl } from '@directus/utils/node';
4
+ export function importFileUrl(url, root, options = {}) {
5
+ return import(`./${pathToRelativeUrl(url, dirname(fileURLToPath(root)))}${options.fresh ? `?t=${Date.now()}` : ''}`);
6
+ }
@@ -1,9 +1,8 @@
1
- type Job = () => Promise<void> | void;
1
+ import type { PromiseCallback } from '@directus/types';
2
2
  export declare class JobQueue {
3
3
  private running;
4
4
  private jobs;
5
5
  constructor();
6
- enqueue(job: Job): void;
6
+ enqueue(job: PromiseCallback): void;
7
7
  private run;
8
8
  }
9
- export {};
@@ -19,5 +19,5 @@ export declare function redactObject(input: UnknownObject, redact: {
19
19
  /**
20
20
  * Replace values and extract Error objects for use with JSON.stringify()
21
21
  */
22
- export declare function getReplacer(replacement: Replacement, values?: Values): (_key: string, value: unknown) => unknown;
22
+ export declare function getReplacer(replacement: Replacement, values?: Values): (_key: string, value: unknown) => any;
23
23
  export {};
@@ -84,31 +84,44 @@ export function getReplacer(replacement, values) {
84
84
  const filteredValues = values
85
85
  ? Object.entries(values).filter(([_k, v]) => typeof v === 'string' && v.length > 0)
86
86
  : [];
87
- const seen = new WeakSet();
88
- return (_key, value) => {
89
- // Skip circular values
90
- if (isObject(value)) {
91
- if (seen.has(value)) {
92
- return;
87
+ const replacer = (seen) => {
88
+ return function (_key, value) {
89
+ if (value instanceof Error) {
90
+ return {
91
+ name: value.name,
92
+ message: value.message,
93
+ stack: value.stack,
94
+ cause: value.cause,
95
+ };
93
96
  }
94
- seen.add(value);
95
- }
96
- if (value instanceof Error) {
97
- return {
98
- name: value.name,
99
- message: value.message,
100
- stack: value.stack,
101
- cause: value.cause,
102
- };
103
- }
104
- if (!values || filteredValues.length === 0 || typeof value !== 'string')
105
- return value;
106
- let finalValue = value;
107
- for (const [redactKey, valueToRedact] of filteredValues) {
108
- if (finalValue.includes(valueToRedact)) {
109
- finalValue = finalValue.replace(new RegExp(valueToRedact, 'g'), replacement(redactKey));
97
+ if (value !== null && typeof value === 'object') {
98
+ if (seen.has(value)) {
99
+ return '[Circular]';
100
+ }
101
+ seen.add(value);
102
+ const newValue = Array.isArray(value) ? [] : {};
103
+ for (const [key2, value2] of Object.entries(value)) {
104
+ if (typeof value2 === 'string') {
105
+ newValue[key2] = value2;
106
+ }
107
+ else {
108
+ newValue[key2] = replacer(seen)(key2, value2);
109
+ }
110
+ }
111
+ seen.delete(value);
112
+ return newValue;
110
113
  }
111
- }
112
- return finalValue;
114
+ if (!values || filteredValues.length === 0 || typeof value !== 'string')
115
+ return value;
116
+ let finalValue = value;
117
+ for (const [redactKey, valueToRedact] of filteredValues) {
118
+ if (finalValue.includes(valueToRedact)) {
119
+ finalValue = finalValue.replace(new RegExp(valueToRedact, 'g'), replacement(redactKey));
120
+ }
121
+ }
122
+ return finalValue;
123
+ };
113
124
  };
125
+ const seen = new WeakSet();
126
+ return replacer(seen);
114
127
  }
@@ -46,6 +46,9 @@ export function sanitizeQuery(rawQuery, accountability) {
46
46
  if (rawQuery['search'] && typeof rawQuery['search'] === 'string') {
47
47
  query.search = rawQuery['search'];
48
48
  }
49
+ if (rawQuery['version']) {
50
+ query.version = rawQuery['version'];
51
+ }
49
52
  if (rawQuery['export']) {
50
53
  query.export = rawQuery['export'];
51
54
  }
@@ -17,6 +17,7 @@ const querySchema = Joi.object({
17
17
  meta: Joi.array().items(Joi.string().valid('total_count', 'filter_count')),
18
18
  search: Joi.string(),
19
19
  export: Joi.string().valid('csv', 'json', 'xml', 'yaml'),
20
+ version: Joi.string(),
20
21
  aggregate: Joi.object(),
21
22
  deep: Joi.object(),
22
23
  alias: Joi.object(),
@@ -1,3 +1,4 @@
1
+ import os from 'node:os';
1
2
  import Tinypool from 'tinypool';
2
3
  let workerPool;
3
4
  export function getWorkerPool() {
@@ -6,6 +7,13 @@ export function getWorkerPool() {
6
7
  minThreads: 0,
7
8
  maxQueue: 'auto',
8
9
  });
10
+ // TODO Workaround currently required for failing CPU count on ARM in Tinypool,
11
+ // remove again once fixed upstream
12
+ if (workerPool.options.maxThreads === 0) {
13
+ const availableParallelism = os.availableParallelism();
14
+ workerPool.options.maxThreads = availableParallelism;
15
+ workerPool.options.maxQueue = availableParallelism ** 2;
16
+ }
9
17
  }
10
18
  return workerPool;
11
19
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directus/api",
3
- "version": "13.2.0",
3
+ "version": "14.0.1",
4
4
  "description": "Directus is a real-time API and App dashboard for managing SQL database content",
5
5
  "keywords": [
6
6
  "directus",
@@ -60,12 +60,12 @@
60
60
  "dependencies": {
61
61
  "@authenio/samlify-node-xmllint": "2.0.0",
62
62
  "@aws-sdk/client-ses": "3.332.0",
63
- "@directus/format-title": "10.0.0",
63
+ "@directus/format-title": "10.1.0",
64
64
  "@godaddy/terminus": "4.12.0",
65
65
  "@rollup/plugin-alias": "5.0.0",
66
66
  "@rollup/plugin-node-resolve": "15.0.2",
67
67
  "@rollup/plugin-virtual": "3.0.1",
68
- "argon2": "0.30.3",
68
+ "argon2": "0.31.1",
69
69
  "async": "3.2.4",
70
70
  "axios": "1.4.0",
71
71
  "busboy": "1.6.0",
@@ -89,9 +89,10 @@
89
89
  "express": "4.18.2",
90
90
  "flat": "5.0.2",
91
91
  "fs-extra": "11.1.1",
92
- "graphql": "16.6.0",
92
+ "glob-to-regexp": "0.4.1",
93
+ "graphql": "16.8.1",
93
94
  "graphql-compose": "9.0.10",
94
- "graphql-ws": "5.12.0",
95
+ "graphql-ws": "5.14.1",
95
96
  "helmet": "7.0.0",
96
97
  "icc": "3.0.0",
97
98
  "inquirer": "9.2.4",
@@ -131,7 +132,7 @@
131
132
  "rollup": "3.22.0",
132
133
  "samlify": "2.8.10",
133
134
  "sanitize-html": "2.10.0",
134
- "sharp": "0.32.5",
135
+ "sharp": "0.32.6",
135
136
  "snappy": "7.2.2",
136
137
  "stream-json": "1.7.5",
137
138
  "strip-bom-stream": "5.0.0",
@@ -140,26 +141,26 @@
140
141
  "uuid": "9.0.0",
141
142
  "uuid-validate": "0.0.3",
142
143
  "wellknown": "0.5.0",
143
- "ws": "8.12.1",
144
- "zod": "3.21.4",
144
+ "ws": "8.14.2",
145
+ "zod": "3.22.4",
145
146
  "zod-validation-error": "1.0.1",
146
- "@directus/app": "10.9.1",
147
- "@directus/constants": "11.0.0",
148
- "@directus/errors": "0.1.0",
149
- "@directus/extensions": "0.0.1",
150
- "@directus/extensions-sdk": "10.1.12",
151
- "@directus/pressure": "1.0.11",
147
+ "@directus/app": "10.11.0",
148
+ "@directus/constants": "11.0.1",
149
+ "@directus/errors": "0.2.0",
150
+ "@directus/extensions": "0.1.1",
151
+ "@directus/extensions-sdk": "10.1.14",
152
+ "@directus/pressure": "1.0.12",
152
153
  "@directus/schema": "11.0.0",
153
- "@directus/specs": "10.2.0",
154
- "@directus/storage": "10.0.6",
155
- "@directus/storage-driver-azure": "10.0.12",
156
- "@directus/storage-driver-cloudinary": "10.0.12",
157
- "@directus/storage-driver-gcs": "10.0.12",
158
- "@directus/storage-driver-local": "10.0.12",
159
- "@directus/storage-driver-s3": "10.0.12",
160
- "@directus/storage-driver-supabase": "1.0.4",
161
- "@directus/utils": "11.0.0",
162
- "@directus/validation": "0.0.7"
154
+ "@directus/specs": "10.2.1",
155
+ "@directus/storage": "10.0.7",
156
+ "@directus/storage-driver-azure": "10.0.13",
157
+ "@directus/storage-driver-cloudinary": "10.0.13",
158
+ "@directus/storage-driver-gcs": "10.0.13",
159
+ "@directus/storage-driver-local": "10.0.13",
160
+ "@directus/storage-driver-s3": "10.0.13",
161
+ "@directus/storage-driver-supabase": "1.0.5",
162
+ "@directus/utils": "11.0.1",
163
+ "@directus/validation": "0.0.8"
163
164
  },
164
165
  "devDependencies": {
165
166
  "@ngneat/falso": "6.4.0",
@@ -177,11 +178,11 @@
177
178
  "@types/express-serve-static-core": "4.17.35",
178
179
  "@types/flat": "5.0.2",
179
180
  "@types/fs-extra": "11.0.1",
181
+ "@types/glob-to-regexp": "0.4.3",
180
182
  "@types/inquirer": "9.0.3",
181
183
  "@types/js-yaml": "4.0.5",
182
184
  "@types/json2csv": "5.0.3",
183
185
  "@types/jsonwebtoken": "9.0.2",
184
- "@types/keyv": "4.2.0",
185
186
  "@types/ldapjs": "2.2.5",
186
187
  "@types/lodash-es": "4.17.7",
187
188
  "@types/marked": "4.3.0",
@@ -199,7 +200,7 @@
199
200
  "@types/uuid": "9.0.1",
200
201
  "@types/uuid-validate": "0.0.1",
201
202
  "@types/wellknown": "0.5.4",
202
- "@types/ws": "8.5.4",
203
+ "@types/ws": "8.5.8",
203
204
  "@vitest/coverage-c8": "0.31.1",
204
205
  "copyfiles": "2.4.1",
205
206
  "form-data": "4.0.0",
@@ -209,7 +210,7 @@
209
210
  "vitest": "0.31.1",
210
211
  "@directus/random": "0.2.3",
211
212
  "@directus/tsconfig": "1.0.1",
212
- "@directus/types": "11.0.0"
213
+ "@directus/types": "11.0.1"
213
214
  },
214
215
  "optionalDependencies": {
215
216
  "@keyv/redis": "2.5.8",
@@ -1,47 +0,0 @@
1
- export declare const getExtensions: () => Promise<(({
2
- path: string;
3
- name: string;
4
- version?: string;
5
- host?: string;
6
- local: boolean;
7
- } & {
8
- type: "interface" | "display" | "layout" | "module" | "panel";
9
- entrypoint: string;
10
- }) | ({
11
- path: string;
12
- name: string;
13
- version?: string;
14
- host?: string;
15
- local: boolean;
16
- } & {
17
- type: "endpoint" | "hook";
18
- entrypoint: string;
19
- }) | ({
20
- path: string;
21
- name: string;
22
- version?: string;
23
- host?: string;
24
- local: boolean;
25
- } & {
26
- type: "operation";
27
- entrypoint: {
28
- app: string;
29
- api: string;
30
- };
31
- }) | ({
32
- path: string;
33
- name: string;
34
- version?: string;
35
- host?: string;
36
- local: boolean;
37
- } & {
38
- type: "bundle";
39
- entrypoint: {
40
- app: string;
41
- api: string;
42
- };
43
- entries: {
44
- type: "operation" | "interface" | "display" | "endpoint" | "layout" | "module" | "panel" | "hook";
45
- name: string;
46
- }[];
47
- }))[]>;
@@ -1,9 +0,0 @@
1
- import { APP_EXTENSION_TYPES } from '@directus/extensions';
2
- import { getLocalExtensions, getPackageExtensions, resolvePackageExtensions } from '@directus/extensions/node';
3
- import env from '../env.js';
4
- export const getExtensions = async () => {
5
- const packageExtensions = await getPackageExtensions(env['PACKAGE_FILE_LOCATION']);
6
- const localPackageExtensions = await resolvePackageExtensions(env['EXTENSIONS_PATH']);
7
- const localExtensions = await getLocalExtensions(env['EXTENSIONS_PATH']);
8
- return [...packageExtensions, ...localPackageExtensions, ...localExtensions].filter((extension) => env['SERVE_APP'] || APP_EXTENSION_TYPES.includes(extension.type) === false);
9
- };