@directus/api 18.2.1 → 19.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 (90) hide show
  1. package/dist/app.js +0 -3
  2. package/dist/auth/drivers/ldap.js +1 -1
  3. package/dist/auth/drivers/local.js +1 -1
  4. package/dist/auth/drivers/oauth2.js +1 -1
  5. package/dist/auth/drivers/openid.js +1 -1
  6. package/dist/cache.js +1 -1
  7. package/dist/cli/utils/create-env/env-stub.liquid +2 -2
  8. package/dist/controllers/activity.js +1 -1
  9. package/dist/controllers/assets.js +9 -12
  10. package/dist/controllers/auth.js +7 -6
  11. package/dist/controllers/collections.js +1 -2
  12. package/dist/controllers/dashboards.js +1 -2
  13. package/dist/controllers/extensions.js +30 -0
  14. package/dist/controllers/fields.js +1 -3
  15. package/dist/controllers/flows.js +1 -2
  16. package/dist/controllers/folders.js +1 -2
  17. package/dist/controllers/items.js +3 -4
  18. package/dist/controllers/notifications.js +1 -2
  19. package/dist/controllers/operations.js +1 -2
  20. package/dist/controllers/panels.js +1 -2
  21. package/dist/controllers/presets.js +1 -2
  22. package/dist/controllers/roles.js +1 -2
  23. package/dist/controllers/translations.js +1 -2
  24. package/dist/controllers/users.js +1 -2
  25. package/dist/controllers/webhooks.js +10 -74
  26. package/dist/database/migrations/20240122A-add-report-url-fields.d.ts +3 -0
  27. package/dist/database/migrations/20240122A-add-report-url-fields.js +14 -0
  28. package/dist/database/migrations/20240204A-marketplace.js +17 -5
  29. package/dist/database/migrations/20240305A-change-useragent-type.d.ts +3 -0
  30. package/dist/database/migrations/20240305A-change-useragent-type.js +19 -0
  31. package/dist/database/migrations/20240311A-deprecate-webhooks.d.ts +13 -0
  32. package/dist/database/migrations/20240311A-deprecate-webhooks.js +125 -0
  33. package/dist/database/run-ast.js +4 -3
  34. package/dist/extensions/manager.d.ts +1 -0
  35. package/dist/extensions/manager.js +4 -1
  36. package/dist/middleware/authenticate.js +1 -1
  37. package/dist/services/activity.d.ts +2 -1
  38. package/dist/services/authorization.d.ts +2 -2
  39. package/dist/services/collections.d.ts +1 -1
  40. package/dist/services/collections.js +8 -7
  41. package/dist/services/extensions.d.ts +3 -0
  42. package/dist/services/extensions.js +42 -10
  43. package/dist/services/fields.d.ts +2 -1
  44. package/dist/services/fields.js +37 -7
  45. package/dist/services/files.d.ts +2 -2
  46. package/dist/services/flows.d.ts +2 -2
  47. package/dist/services/graphql/index.d.ts +2 -2
  48. package/dist/services/graphql/index.js +5 -0
  49. package/dist/services/import-export.js +4 -3
  50. package/dist/services/items.d.ts +2 -2
  51. package/dist/services/items.js +9 -8
  52. package/dist/services/notifications.d.ts +2 -2
  53. package/dist/services/operations.d.ts +2 -2
  54. package/dist/services/payload.d.ts +2 -2
  55. package/dist/services/permissions/index.d.ts +2 -2
  56. package/dist/services/relations.js +10 -3
  57. package/dist/services/revisions.d.ts +2 -1
  58. package/dist/services/roles.d.ts +2 -2
  59. package/dist/services/roles.js +2 -1
  60. package/dist/services/shares.d.ts +2 -1
  61. package/dist/services/shares.js +1 -1
  62. package/dist/services/tfa.d.ts +2 -1
  63. package/dist/services/tfa.js +1 -1
  64. package/dist/services/users.d.ts +2 -2
  65. package/dist/services/users.js +3 -2
  66. package/dist/services/utils.d.ts +2 -2
  67. package/dist/services/utils.js +2 -2
  68. package/dist/services/versions.d.ts +1 -1
  69. package/dist/services/webhooks.d.ts +8 -4
  70. package/dist/services/webhooks.js +15 -12
  71. package/dist/types/items.d.ts +1 -8
  72. package/dist/types/services.d.ts +1 -2
  73. package/dist/utils/apply-diff.js +2 -1
  74. package/dist/utils/get-ast-from-query.js +1 -1
  75. package/dist/utils/get-auth-providers.d.ts +3 -1
  76. package/dist/utils/get-auth-providers.js +15 -4
  77. package/dist/utils/get-cache-headers.js +0 -3
  78. package/dist/utils/get-schema.d.ts +1 -1
  79. package/dist/utils/get-schema.js +52 -29
  80. package/dist/utils/merge-version-data.js +1 -1
  81. package/dist/utils/transaction.d.ts +9 -0
  82. package/dist/utils/transaction.js +15 -0
  83. package/dist/utils/validate-keys.d.ts +1 -2
  84. package/dist/websocket/controllers/base.d.ts +1 -3
  85. package/dist/websocket/controllers/base.js +12 -3
  86. package/dist/websocket/utils/items.d.ts +1 -1
  87. package/license +1 -1
  88. package/package.json +39 -37
  89. package/dist/webhooks.d.ts +0 -4
  90. package/dist/webhooks.js +0 -80
@@ -0,0 +1,125 @@
1
+ import { parseJSON, toArray } from '@directus/utils';
2
+ import { randomUUID } from 'node:crypto';
3
+ // To avoid typos
4
+ const TABLE_WEBHOOKS = 'directus_webhooks';
5
+ const TABLE_FLOWS = 'directus_flows';
6
+ const TABLE_OPERATIONS = 'directus_operations';
7
+ const NEW_COLUMN_WAS_ACTIVE = 'was_active_before_deprecation';
8
+ const NEW_COLUMN_FLOW = 'migrated_flow';
9
+ /**
10
+ * 0. Identify and persist which webhooks were active before deprecation
11
+ * 1. Migrate existing webhooks over to identically behaving Flows
12
+ * 2. Disable existing webhooks
13
+ * 3. Dont drop webhooks yet
14
+ */
15
+ export async function up(knex) {
16
+ // 0. Identify and persist which webhooks were active before deprecation
17
+ await knex.schema.alterTable(TABLE_WEBHOOKS, (table) => {
18
+ table.boolean(NEW_COLUMN_WAS_ACTIVE).notNullable().defaultTo(false);
19
+ table.uuid(NEW_COLUMN_FLOW).nullable();
20
+ table.foreign(NEW_COLUMN_FLOW).references(`${TABLE_FLOWS}.id`).onDelete('SET NULL');
21
+ });
22
+ await knex(TABLE_WEBHOOKS)
23
+ .update({ [NEW_COLUMN_WAS_ACTIVE]: true })
24
+ .where({ status: 'active' });
25
+ const webhooks = await knex.select('*').from(TABLE_WEBHOOKS);
26
+ // 1. Migrate existing webhooks over to identically behaving Flows
27
+ await knex.transaction(async (trx) => {
28
+ for (const webhook of webhooks) {
29
+ const flowId = randomUUID();
30
+ const operationIdRunScript = randomUUID();
31
+ const operationIdWebhook = randomUUID();
32
+ const newFlow = {
33
+ id: flowId,
34
+ name: webhook.name,
35
+ icon: 'webhook',
36
+ // color: null,
37
+ description: 'Auto-generated by Directus Migration\n\nWebhooks were deprecated and have been moved into Flows for you automatically.',
38
+ status: webhook.status, // Only activate already active webhooks!
39
+ trigger: 'event',
40
+ // accountability: "all",
41
+ options: {
42
+ type: 'action',
43
+ scope: toArray(webhook.actions)
44
+ .filter((action) => action.trim() !== '')
45
+ .map((scope) => `items.${scope}`),
46
+ collections: toArray(webhook.collections).filter((collection) => collection.trim() !== ''),
47
+ },
48
+ operation: null, // Fill this in later --> `operationIdRunScript`
49
+ // date_created: Default Date,
50
+ // user_created: null,
51
+ };
52
+ const operationRunScript = {
53
+ id: operationIdRunScript,
54
+ name: 'Transforming Payload for Webhook',
55
+ key: 'payload-transform',
56
+ type: 'exec',
57
+ position_x: 19,
58
+ position_y: 1,
59
+ options: {
60
+ code: '/**\n * Webhook data slightly differs from Flow trigger data.\n * This flow converts the new Flow format into the older Webhook shape\n * in order to not break existing logic of consumers.\n */ \nmodule.exports = async function(data) {\n const crudOperation = data.$trigger.event.split(".").at(-1);\n const keyOrKeys = crudOperation === "create" ? "key" : "keys";\n return {\n event: `items.${crudOperation}`,\n accountability: { user: data.$accountability.user, role: data.$accountability.role },\n payload: data.$trigger.payload,\n [keyOrKeys]: data.$trigger?.[keyOrKeys],\n collection: data.$trigger.collection,\n };\n}',
61
+ },
62
+ resolve: operationIdWebhook,
63
+ reject: null,
64
+ flow: flowId,
65
+ // date_created: "",
66
+ // user_created: "",
67
+ };
68
+ const operationWebhook = {
69
+ id: operationIdWebhook,
70
+ name: 'Webhook',
71
+ key: 'webhook',
72
+ type: 'request',
73
+ position_x: 37,
74
+ position_y: 1,
75
+ options: {
76
+ url: webhook.url,
77
+ body: webhook.data ? '{{$last}}' : undefined,
78
+ method: webhook.method,
79
+ headers: typeof webhook.headers === 'string' ? parseJSON(webhook.headers) : webhook.headers,
80
+ },
81
+ resolve: null,
82
+ reject: null,
83
+ flow: flowId,
84
+ // date_created: "",
85
+ // user_created: "",
86
+ };
87
+ await trx(TABLE_FLOWS).insert(newFlow);
88
+ // Only need to transform the payload if the webhook enabled transmitting it
89
+ if (webhook.data && webhook.method !== 'GET') {
90
+ // Order is important due to IDs
91
+ await trx(TABLE_OPERATIONS).insert(operationWebhook);
92
+ await trx(TABLE_OPERATIONS).insert(operationRunScript);
93
+ await trx(TABLE_FLOWS).update({ operation: operationIdRunScript }).where({ id: flowId });
94
+ }
95
+ else {
96
+ operationWebhook.position_x = 19; // Reset it because the Run-Script will be missing
97
+ await trx(TABLE_OPERATIONS).insert(operationWebhook);
98
+ await trx(TABLE_FLOWS).update({ operation: operationIdWebhook }).where({ id: flowId });
99
+ }
100
+ // Persist new Flow/s so that we can retroactively remove them on potential down-migrations
101
+ await trx(TABLE_WEBHOOKS)
102
+ .update({ [NEW_COLUMN_FLOW]: flowId })
103
+ .where({ id: webhook.id });
104
+ // 2. Disable existing webhooks
105
+ await trx(TABLE_WEBHOOKS).update({ status: 'inactive' });
106
+ }
107
+ });
108
+ }
109
+ /**
110
+ * Dont completely drop Webhooks yet.
111
+ * Lets preserve the data and drop them in the next release to be extra safe with users data.
112
+ */
113
+ export async function down(knex) {
114
+ // Remove Flows created by the up-migration
115
+ const migratedFlowIds = (await knex(TABLE_WEBHOOKS).select(NEW_COLUMN_FLOW).whereNotNull(NEW_COLUMN_FLOW)).map((col) => col[NEW_COLUMN_FLOW]);
116
+ await knex(TABLE_FLOWS).delete().whereIn('id', migratedFlowIds);
117
+ // Restore previously activated webhooks
118
+ await knex(TABLE_WEBHOOKS)
119
+ .update({ status: 'active' })
120
+ .where({ [NEW_COLUMN_WAS_ACTIVE]: true });
121
+ // Cleanup of up-migration
122
+ await knex.schema.alterTable(TABLE_WEBHOOKS, (table) => {
123
+ table.dropColumns(NEW_COLUMN_WAS_ACTIVE, NEW_COLUMN_FLOW);
124
+ });
125
+ }
@@ -324,14 +324,15 @@ function mergeWithParentItems(schema, nestedItem, parentItem, nestedNode) {
324
324
  nestedItem[nestedNode.relation.field]?.[schema.collections[nestedNode.relation.related_collection].primary] == parentItem[schema.collections[nestedNode.relation.related_collection].primary]);
325
325
  });
326
326
  parentItem[nestedNode.fieldKey].push(...itemChildren);
327
+ const limit = nestedNode.query.limit ?? Number(env['QUERY_LIMIT_DEFAULT']);
327
328
  if (nestedNode.query.page && nestedNode.query.page > 1) {
328
- parentItem[nestedNode.fieldKey] = parentItem[nestedNode.fieldKey].slice((nestedNode.query.limit ?? Number(env['QUERY_LIMIT_DEFAULT'])) * (nestedNode.query.page - 1));
329
+ parentItem[nestedNode.fieldKey] = parentItem[nestedNode.fieldKey].slice(limit * (nestedNode.query.page - 1));
329
330
  }
330
331
  if (nestedNode.query.offset && nestedNode.query.offset >= 0) {
331
332
  parentItem[nestedNode.fieldKey] = parentItem[nestedNode.fieldKey].slice(nestedNode.query.offset);
332
333
  }
333
- if (nestedNode.query.limit !== -1) {
334
- parentItem[nestedNode.fieldKey] = parentItem[nestedNode.fieldKey].slice(0, nestedNode.query.limit ?? Number(env['QUERY_LIMIT_DEFAULT']));
334
+ if (limit !== -1) {
335
+ parentItem[nestedNode.fieldKey] = parentItem[nestedNode.fieldKey].slice(0, limit);
335
336
  }
336
337
  parentItem[nestedNode.fieldKey] = parentItem[nestedNode.fieldKey].sort((a, b) => {
337
338
  // This is pre-filled in get-ast-from-query
@@ -81,6 +81,7 @@ export declare class ExtensionManager {
81
81
  */
82
82
  install(versionId: string): Promise<void>;
83
83
  uninstall(folder: string): Promise<void>;
84
+ broadcastReloadNotification(): Promise<void>;
84
85
  /**
85
86
  * Load all extensions from disk and register them in their respective places
86
87
  */
@@ -171,11 +171,14 @@ export class ExtensionManager {
171
171
  async install(versionId) {
172
172
  await this.installationManager.install(versionId);
173
173
  await this.reload({ forceSync: true });
174
- await this.messenger.publish(this.reloadChannel, { origin: this.processId });
174
+ await this.broadcastReloadNotification();
175
175
  }
176
176
  async uninstall(folder) {
177
177
  await this.installationManager.uninstall(folder);
178
178
  await this.reload({ forceSync: true });
179
+ await this.broadcastReloadNotification();
180
+ }
181
+ async broadcastReloadNotification() {
179
182
  await this.messenger.publish(this.reloadChannel, { origin: this.processId });
180
183
  }
181
184
  /**
@@ -15,7 +15,7 @@ export const handler = async (req, _res, next) => {
15
15
  app: false,
16
16
  ip: getIPFromReq(req),
17
17
  };
18
- const userAgent = req.get('user-agent');
18
+ const userAgent = req.get('user-agent')?.substring(0, 1024);
19
19
  if (userAgent)
20
20
  defaultAccountability.userAgent = userAgent;
21
21
  const origin = req.get('origin');
@@ -1,4 +1,5 @@
1
- import type { AbstractServiceOptions, Item, MutationOptions, PrimaryKey } from '../types/index.js';
1
+ import type { Item, PrimaryKey } from '@directus/types';
2
+ import type { AbstractServiceOptions, MutationOptions } from '../types/index.js';
2
3
  import { ItemsService } from './items.js';
3
4
  import { NotificationsService } from './notifications.js';
4
5
  import { UsersService } from './users.js';
@@ -1,6 +1,6 @@
1
- import type { Accountability, PermissionsAction, SchemaOverview } from '@directus/types';
1
+ import type { Accountability, Item, PermissionsAction, PrimaryKey, SchemaOverview } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
- import type { AST, AbstractServiceOptions, Item, PrimaryKey } from '../types/index.js';
3
+ import type { AST, AbstractServiceOptions } from '../types/index.js';
4
4
  import { PayloadService } from './payload.js';
5
5
  export declare class AuthorizationService {
6
6
  knex: Knex;
@@ -1,10 +1,10 @@
1
1
  import type { SchemaInspector, Table } from '@directus/schema';
2
+ import { type BaseCollectionMeta } from '@directus/system-data';
2
3
  import type { Accountability, RawField, SchemaOverview } from '@directus/types';
3
4
  import type Keyv from 'keyv';
4
5
  import type { Knex } from 'knex';
5
6
  import type { Helpers } from '../database/helpers/index.js';
6
7
  import type { AbstractServiceOptions, Collection, MutationOptions } from '../types/index.js';
7
- import { type BaseCollectionMeta } from '@directus/system-data';
8
8
  export type RawCollection = {
9
9
  collection: string;
10
10
  fields?: RawField[];
@@ -1,6 +1,7 @@
1
1
  import { useEnv } from '@directus/env';
2
2
  import { ForbiddenError, InvalidPayloadError } from '@directus/errors';
3
3
  import { createInspector } from '@directus/schema';
4
+ import { systemCollectionRows } from '@directus/system-data';
4
5
  import { addFieldFlag } from '@directus/utils';
5
6
  import { chunk, groupBy, merge, omit } from 'lodash-es';
6
7
  import { clearSystemCache, getCache } from '../cache.js';
@@ -10,9 +11,9 @@ import getDatabase, { getSchemaInspector } from '../database/index.js';
10
11
  import emitter from '../emitter.js';
11
12
  import { getSchema } from '../utils/get-schema.js';
12
13
  import { shouldClearCache } from '../utils/should-clear-cache.js';
14
+ import { transaction } from '../utils/transaction.js';
13
15
  import { FieldsService } from './fields.js';
14
16
  import { ItemsService } from './items.js';
15
- import { systemCollectionRows } from '@directus/system-data';
16
17
  export class CollectionsService {
17
18
  knex;
18
19
  helpers;
@@ -56,7 +57,7 @@ export class CollectionsService {
56
57
  // Create the collection/fields in a transaction so it'll be reverted in case of errors or
57
58
  // permission problems. This might not work reliably in MySQL, as it doesn't support DDL in
58
59
  // transactions.
59
- await this.knex.transaction(async (trx) => {
60
+ await transaction(this.knex, async (trx) => {
60
61
  if (payload.schema) {
61
62
  // Directus heavily relies on the primary key of a collection, so we have to make sure that
62
63
  // every collection that is created has a primary key. If no primary key field is created
@@ -167,7 +168,7 @@ export class CollectionsService {
167
168
  async createMany(payloads, opts) {
168
169
  const nestedActionEvents = [];
169
170
  try {
170
- const collections = await this.knex.transaction(async (trx) => {
171
+ const collections = await transaction(this.knex, async (trx) => {
171
172
  const service = new CollectionsService({
172
173
  schema: this.schema,
173
174
  accountability: this.accountability,
@@ -361,7 +362,7 @@ export class CollectionsService {
361
362
  const collectionKeys = [];
362
363
  const nestedActionEvents = [];
363
364
  try {
364
- await this.knex.transaction(async (trx) => {
365
+ await transaction(this.knex, async (trx) => {
365
366
  const collectionItemsService = new CollectionsService({
366
367
  knex: trx,
367
368
  accountability: this.accountability,
@@ -406,7 +407,7 @@ export class CollectionsService {
406
407
  }
407
408
  const nestedActionEvents = [];
408
409
  try {
409
- await this.knex.transaction(async (trx) => {
410
+ await transaction(this.knex, async (trx) => {
410
411
  const service = new CollectionsService({
411
412
  schema: this.schema,
412
413
  accountability: this.accountability,
@@ -453,7 +454,7 @@ export class CollectionsService {
453
454
  if (!!collectionToBeDeleted === false) {
454
455
  throw new ForbiddenError();
455
456
  }
456
- await this.knex.transaction(async (trx) => {
457
+ await transaction(this.knex, async (trx) => {
457
458
  if (collectionToBeDeleted.schema) {
458
459
  await trx.schema.dropTable(collectionKey);
459
460
  }
@@ -552,7 +553,7 @@ export class CollectionsService {
552
553
  }
553
554
  const nestedActionEvents = [];
554
555
  try {
555
- await this.knex.transaction(async (trx) => {
556
+ await transaction(this.knex, async (trx) => {
556
557
  const service = new CollectionsService({
557
558
  schema: this.schema,
558
559
  accountability: this.accountability,
@@ -15,7 +15,10 @@ export declare class ExtensionsService {
15
15
  extensionsItemService: ItemsService<ExtensionSettings>;
16
16
  extensionsManager: ExtensionManager;
17
17
  constructor(options: AbstractServiceOptions);
18
+ private preInstall;
18
19
  install(extensionId: string, versionId: string): Promise<void>;
20
+ uninstall(id: string): Promise<void>;
21
+ reinstall(id: string): Promise<void>;
19
22
  readAll(): Promise<ApiOutput[]>;
20
23
  readOne(id: string): Promise<ApiOutput>;
21
24
  updateOne(id: string, data: DeepPartial<ApiOutput>): Promise<ApiOutput>;
@@ -4,6 +4,7 @@ import { describe } from '@directus/extensions-registry';
4
4
  import { isObject } from '@directus/utils';
5
5
  import getDatabase from '../database/index.js';
6
6
  import { getExtensionManager } from '../extensions/index.js';
7
+ import { transaction } from '../utils/transaction.js';
7
8
  import { ItemsService } from './items.js';
8
9
  export class ExtensionReadError extends Error {
9
10
  originalError;
@@ -29,7 +30,7 @@ export class ExtensionsService {
29
30
  accountability: this.accountability,
30
31
  });
31
32
  }
32
- async install(extensionId, versionId) {
33
+ async preInstall(extensionId, versionId) {
33
34
  const env = useEnv();
34
35
  const describeOptions = {};
35
36
  if (typeof env['MARKETPLACE_REGISTRY'] === 'string') {
@@ -54,6 +55,10 @@ export class ExtensionsService {
54
55
  throw new LimitExceededError();
55
56
  }
56
57
  }
58
+ return { extension, version };
59
+ }
60
+ async install(extensionId, versionId) {
61
+ const { extension, version } = await this.preInstall(extensionId, versionId);
57
62
  await this.extensionsItemService.createOne({
58
63
  id: extensionId,
59
64
  enabled: true,
@@ -71,6 +76,38 @@ export class ExtensionsService {
71
76
  }
72
77
  await this.extensionsManager.install(versionId);
73
78
  }
79
+ async uninstall(id) {
80
+ const settings = await this.extensionsItemService.readOne(id);
81
+ if (settings.source !== 'registry') {
82
+ throw new InvalidPayloadError({
83
+ reason: 'Cannot uninstall extensions that were not installed via marketplace',
84
+ });
85
+ }
86
+ if (settings.bundle !== null) {
87
+ throw new InvalidPayloadError({
88
+ reason: 'Cannot uninstall sub extensions of bundles separately',
89
+ });
90
+ }
91
+ await this.deleteOne(id);
92
+ await this.extensionsManager.uninstall(settings.folder);
93
+ }
94
+ async reinstall(id) {
95
+ const settings = await this.extensionsItemService.readOne(id);
96
+ if (settings.source !== 'registry') {
97
+ throw new InvalidPayloadError({
98
+ reason: 'Cannot reinstall extensions that were not installed via marketplace',
99
+ });
100
+ }
101
+ if (settings.bundle !== null) {
102
+ throw new InvalidPayloadError({
103
+ reason: 'Cannot reinstall sub extensions of bundles separately',
104
+ });
105
+ }
106
+ const extensionId = settings.id;
107
+ const versionId = settings.folder;
108
+ await this.preInstall(extensionId, versionId);
109
+ await this.extensionsManager.install(versionId);
110
+ }
74
111
  async readAll() {
75
112
  const settings = await this.extensionsItemService.readByQuery({ limit: -1 });
76
113
  const regular = settings.filter(({ bundle }) => bundle === null);
@@ -111,7 +148,7 @@ export class ExtensionsService {
111
148
  };
112
149
  }
113
150
  async updateOne(id, data) {
114
- const result = await this.knex.transaction(async (trx) => {
151
+ const result = await transaction(this.knex, async (trx) => {
115
152
  if (!isObject(data.meta)) {
116
153
  throw new InvalidPayloadError({ reason: `"meta" is required` });
117
154
  }
@@ -133,19 +170,14 @@ export class ExtensionsService {
133
170
  }
134
171
  return extension;
135
172
  });
136
- this.extensionsManager.reload();
173
+ this.extensionsManager.reload().then(() => {
174
+ this.extensionsManager.broadcastReloadNotification();
175
+ });
137
176
  return result;
138
177
  }
139
178
  async deleteOne(id) {
140
- const settings = await this.extensionsItemService.readOne(id);
141
- if (settings.source !== 'registry') {
142
- throw new InvalidPayloadError({
143
- reason: 'Cannot uninstall extensions that were not installed from the marketplace registry',
144
- });
145
- }
146
179
  await this.extensionsItemService.deleteOne(id);
147
180
  await this.extensionsItemService.deleteByQuery({ filter: { bundle: { _eq: id } } });
148
- await this.extensionsManager.uninstall(settings.folder);
149
181
  }
150
182
  /**
151
183
  * Sync a bundles enabled status
@@ -3,9 +3,9 @@ import type { Accountability, Field, RawField, SchemaOverview, Type } from '@dir
3
3
  import type Keyv from 'keyv';
4
4
  import type { Knex } from 'knex';
5
5
  import type { Helpers } from '../database/helpers/index.js';
6
+ import type { AbstractServiceOptions, MutationOptions } from '../types/index.js';
6
7
  import { ItemsService } from './items.js';
7
8
  import { PayloadService } from './payload.js';
8
- import type { AbstractServiceOptions, MutationOptions } from '../types/index.js';
9
9
  export declare class FieldsService {
10
10
  knex: Knex;
11
11
  helpers: Helpers;
@@ -26,6 +26,7 @@ export declare class FieldsService {
26
26
  }, table?: Knex.CreateTableBuilder, // allows collection creation to
27
27
  opts?: MutationOptions): Promise<void>;
28
28
  updateField(collection: string, field: RawField, opts?: MutationOptions): Promise<string>;
29
+ updateFields(collection: string, fields: RawField[], opts?: MutationOptions): Promise<string[]>;
29
30
  deleteField(collection: string, field: string, opts?: MutationOptions): Promise<void>;
30
31
  addColumnToTable(table: Knex.CreateTableBuilder, field: RawField | Field, alter?: Column | null): void;
31
32
  }
@@ -1,4 +1,5 @@
1
1
  import { KNEX_TYPES, REGEX_BETWEEN_PARENS } from '@directus/constants';
2
+ import { ForbiddenError, InvalidPayloadError } from '@directus/errors';
2
3
  import { createInspector } from '@directus/schema';
3
4
  import { addFieldFlag, toArray } from '@directus/utils';
4
5
  import { isEqual, isNil, merge } from 'lodash-es';
@@ -8,16 +9,16 @@ import { translateDatabaseError } from '../database/errors/translate.js';
8
9
  import { getHelpers } from '../database/helpers/index.js';
9
10
  import getDatabase, { getSchemaInspector } from '../database/index.js';
10
11
  import emitter from '../emitter.js';
11
- import { ForbiddenError, InvalidPayloadError } from '@directus/errors';
12
- import { ItemsService } from './items.js';
13
- import { PayloadService } from './payload.js';
14
12
  import getDefaultValue from '../utils/get-default-value.js';
13
+ import { getSystemFieldRowsWithAuthProviders } from '../utils/get-field-system-rows.js';
15
14
  import getLocalType from '../utils/get-local-type.js';
16
15
  import { getSchema } from '../utils/get-schema.js';
17
16
  import { sanitizeColumn } from '../utils/sanitize-schema.js';
18
17
  import { shouldClearCache } from '../utils/should-clear-cache.js';
18
+ import { transaction } from '../utils/transaction.js';
19
+ import { ItemsService } from './items.js';
20
+ import { PayloadService } from './payload.js';
19
21
  import { RelationsService } from './relations.js';
20
- import { getSystemFieldRowsWithAuthProviders } from '../utils/get-field-system-rows.js';
21
22
  const systemFieldRows = getSystemFieldRowsWithAuthProviders();
22
23
  export class FieldsService {
23
24
  knex;
@@ -218,7 +219,7 @@ export class FieldsService {
218
219
  if (flagToAdd) {
219
220
  addFieldFlag(field, flagToAdd);
220
221
  }
221
- await this.knex.transaction(async (trx) => {
222
+ await transaction(this.knex, async (trx) => {
222
223
  const itemsService = new ItemsService('directus_fields', {
223
224
  knex: trx,
224
225
  accountability: this.accountability,
@@ -337,7 +338,7 @@ export class FieldsService {
337
338
  const columnToCompare = opts?.bypassLimits && opts.autoPurgeSystemCache === false ? sanitizeColumn(existingColumn) : existingColumn;
338
339
  if (!isEqual(columnToCompare, hookAdjustedField.schema)) {
339
340
  try {
340
- await this.knex.transaction(async (trx) => {
341
+ await transaction(this.knex, async (trx) => {
341
342
  await trx.schema.alterTable(collection, async (table) => {
342
343
  if (!hookAdjustedField.schema)
343
344
  return;
@@ -406,6 +407,35 @@ export class FieldsService {
406
407
  }
407
408
  }
408
409
  }
410
+ async updateFields(collection, fields, opts) {
411
+ const nestedActionEvents = [];
412
+ try {
413
+ const fieldNames = [];
414
+ for (const field of fields) {
415
+ fieldNames.push(await this.updateField(collection, field, {
416
+ autoPurgeCache: false,
417
+ autoPurgeSystemCache: false,
418
+ bypassEmitAction: (params) => nestedActionEvents.push(params),
419
+ }));
420
+ }
421
+ return fieldNames;
422
+ }
423
+ finally {
424
+ if (shouldClearCache(this.cache, opts)) {
425
+ await this.cache.clear();
426
+ }
427
+ if (opts?.autoPurgeSystemCache !== false) {
428
+ await clearSystemCache({ autoPurgeCache: opts?.autoPurgeCache });
429
+ }
430
+ if (opts?.emitEvents !== false && nestedActionEvents.length > 0) {
431
+ const updatedSchema = await getSchema();
432
+ for (const nestedActionEvent of nestedActionEvents) {
433
+ nestedActionEvent.context.schema = updatedSchema;
434
+ emitter.emitAction(nestedActionEvent.event, nestedActionEvent.meta, nestedActionEvent.context);
435
+ }
436
+ }
437
+ }
438
+ }
409
439
  async deleteField(collection, field, opts) {
410
440
  if (this.accountability && this.accountability.admin !== true) {
411
441
  throw new ForbiddenError();
@@ -422,7 +452,7 @@ export class FieldsService {
422
452
  accountability: this.accountability,
423
453
  });
424
454
  }
425
- await this.knex.transaction(async (trx) => {
455
+ await transaction(this.knex, async (trx) => {
426
456
  const relations = this.schema.relations.filter((relation) => {
427
457
  return ((relation.collection === collection && relation.field === field) ||
428
458
  (relation.related_collection === collection && relation.meta?.one_field === field));
@@ -1,7 +1,7 @@
1
1
  /// <reference types="node" resolution-mode="require"/>
2
- import type { BusboyFileStream, File } from '@directus/types';
2
+ import type { BusboyFileStream, File, PrimaryKey } from '@directus/types';
3
3
  import type { Readable } from 'node:stream';
4
- import type { AbstractServiceOptions, MutationOptions, PrimaryKey } from '../types/index.js';
4
+ import type { AbstractServiceOptions, MutationOptions } from '../types/index.js';
5
5
  import { ItemsService } from './items.js';
6
6
  type Metadata = Partial<Pick<File, 'height' | 'width' | 'description' | 'title' | 'tags' | 'metadata'>>;
7
7
  export declare class FilesService extends ItemsService {
@@ -1,5 +1,5 @@
1
- import type { FlowRaw } from '@directus/types';
2
- import type { AbstractServiceOptions, Item, MutationOptions, PrimaryKey } from '../types/index.js';
1
+ import type { FlowRaw, Item, PrimaryKey } from '@directus/types';
2
+ import type { AbstractServiceOptions, MutationOptions } from '../types/index.js';
3
3
  import { ItemsService } from './items.js';
4
4
  export declare class FlowsService extends ItemsService<FlowRaw> {
5
5
  constructor(options: AbstractServiceOptions);
@@ -1,10 +1,10 @@
1
1
  import { type DirectusError } from '@directus/errors';
2
- import type { Accountability, Filter, Query, SchemaOverview } from '@directus/types';
2
+ import type { Accountability, Filter, Item, Query, SchemaOverview } from '@directus/types';
3
3
  import type { ArgumentNode, FormattedExecutionResult, FragmentDefinitionNode, GraphQLResolveInfo, SelectionNode } from 'graphql';
4
4
  import { GraphQLError, GraphQLSchema } from 'graphql';
5
5
  import { ObjectTypeComposer, SchemaComposer } from 'graphql-compose';
6
6
  import type { Knex } from 'knex';
7
- import type { AbstractServiceOptions, GraphQLParams, Item } from '../../types/index.js';
7
+ import type { AbstractServiceOptions, GraphQLParams } from '../../types/index.js';
8
8
  export declare class GraphQLService {
9
9
  accountability: Accountability | null;
10
10
  knex: Knex;
@@ -929,6 +929,11 @@ export class GraphQLService {
929
929
  },
930
930
  };
931
931
  }
932
+ else {
933
+ resolver.args = {
934
+ version: GraphQLString,
935
+ };
936
+ }
932
937
  ReadCollectionTypes[collection.collection].addResolver(resolver);
933
938
  ReadCollectionTypes[collection.collection].addResolver({
934
939
  name: `${collection.collection}_aggregated`,
@@ -16,6 +16,7 @@ import getDatabase from '../database/index.js';
16
16
  import emitter from '../emitter.js';
17
17
  import { useLogger } from '../logger.js';
18
18
  import { getDateFormatted } from '../utils/get-date-formatted.js';
19
+ import { transaction } from '../utils/transaction.js';
19
20
  import { Url } from '../utils/url.js';
20
21
  import { userName } from '../utils/user-name.js';
21
22
  import { FilesService } from './files.js';
@@ -54,7 +55,7 @@ export class ImportService {
54
55
  importJSON(collection, stream) {
55
56
  const extractJSON = StreamArray.withParser();
56
57
  const nestedActionEvents = [];
57
- return this.knex.transaction((trx) => {
58
+ return transaction(this.knex, (trx) => {
58
59
  const service = new ItemsService(collection, {
59
60
  knex: trx,
60
61
  schema: this.schema,
@@ -92,7 +93,7 @@ export class ImportService {
92
93
  if (!tmpFile)
93
94
  throw new Error('Failed to create temporary file for import');
94
95
  const nestedActionEvents = [];
95
- return this.knex.transaction((trx) => {
96
+ return transaction(this.knex, (trx) => {
96
97
  const service = new ItemsService(collection, {
97
98
  knex: trx,
98
99
  schema: this.schema,
@@ -211,7 +212,7 @@ export class ExportService {
211
212
  yaml: 'text/yaml',
212
213
  };
213
214
  const database = getDatabase();
214
- await database.transaction(async (trx) => {
215
+ await transaction(database, async (trx) => {
215
216
  const service = new ItemsService(collection, {
216
217
  accountability: this.accountability,
217
218
  schema: this.schema,
@@ -1,7 +1,7 @@
1
- import type { Accountability, PermissionsAction, Query, SchemaOverview } from '@directus/types';
1
+ import type { Accountability, Item as AnyItem, PermissionsAction, PrimaryKey, Query, SchemaOverview } from '@directus/types';
2
2
  import type Keyv from 'keyv';
3
3
  import type { Knex } from 'knex';
4
- import type { AbstractService, AbstractServiceOptions, Item as AnyItem, MutationOptions, PrimaryKey } from '../types/index.js';
4
+ import type { AbstractService, AbstractServiceOptions, MutationOptions } from '../types/index.js';
5
5
  export type QueryOptions = {
6
6
  stripNonRequested?: boolean;
7
7
  permissionsAction?: PermissionsAction;