@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.
- package/dist/app.js +0 -3
- package/dist/auth/drivers/ldap.js +1 -1
- package/dist/auth/drivers/local.js +1 -1
- package/dist/auth/drivers/oauth2.js +1 -1
- package/dist/auth/drivers/openid.js +1 -1
- package/dist/cache.js +1 -1
- package/dist/cli/utils/create-env/env-stub.liquid +2 -2
- package/dist/controllers/activity.js +1 -1
- package/dist/controllers/assets.js +9 -12
- package/dist/controllers/auth.js +7 -6
- package/dist/controllers/collections.js +1 -2
- package/dist/controllers/dashboards.js +1 -2
- package/dist/controllers/extensions.js +30 -0
- package/dist/controllers/fields.js +1 -3
- package/dist/controllers/flows.js +1 -2
- package/dist/controllers/folders.js +1 -2
- package/dist/controllers/items.js +3 -4
- package/dist/controllers/notifications.js +1 -2
- package/dist/controllers/operations.js +1 -2
- package/dist/controllers/panels.js +1 -2
- package/dist/controllers/presets.js +1 -2
- package/dist/controllers/roles.js +1 -2
- package/dist/controllers/translations.js +1 -2
- package/dist/controllers/users.js +1 -2
- package/dist/controllers/webhooks.js +10 -74
- package/dist/database/migrations/20240122A-add-report-url-fields.d.ts +3 -0
- package/dist/database/migrations/20240122A-add-report-url-fields.js +14 -0
- package/dist/database/migrations/20240204A-marketplace.js +17 -5
- package/dist/database/migrations/20240305A-change-useragent-type.d.ts +3 -0
- package/dist/database/migrations/20240305A-change-useragent-type.js +19 -0
- package/dist/database/migrations/20240311A-deprecate-webhooks.d.ts +13 -0
- package/dist/database/migrations/20240311A-deprecate-webhooks.js +125 -0
- package/dist/database/run-ast.js +4 -3
- package/dist/extensions/manager.d.ts +1 -0
- package/dist/extensions/manager.js +4 -1
- package/dist/middleware/authenticate.js +1 -1
- package/dist/services/activity.d.ts +2 -1
- package/dist/services/authorization.d.ts +2 -2
- package/dist/services/collections.d.ts +1 -1
- package/dist/services/collections.js +8 -7
- package/dist/services/extensions.d.ts +3 -0
- package/dist/services/extensions.js +42 -10
- package/dist/services/fields.d.ts +2 -1
- package/dist/services/fields.js +37 -7
- package/dist/services/files.d.ts +2 -2
- package/dist/services/flows.d.ts +2 -2
- package/dist/services/graphql/index.d.ts +2 -2
- package/dist/services/graphql/index.js +5 -0
- package/dist/services/import-export.js +4 -3
- package/dist/services/items.d.ts +2 -2
- package/dist/services/items.js +9 -8
- package/dist/services/notifications.d.ts +2 -2
- package/dist/services/operations.d.ts +2 -2
- package/dist/services/payload.d.ts +2 -2
- package/dist/services/permissions/index.d.ts +2 -2
- package/dist/services/relations.js +10 -3
- package/dist/services/revisions.d.ts +2 -1
- package/dist/services/roles.d.ts +2 -2
- package/dist/services/roles.js +2 -1
- package/dist/services/shares.d.ts +2 -1
- package/dist/services/shares.js +1 -1
- package/dist/services/tfa.d.ts +2 -1
- package/dist/services/tfa.js +1 -1
- package/dist/services/users.d.ts +2 -2
- package/dist/services/users.js +3 -2
- package/dist/services/utils.d.ts +2 -2
- package/dist/services/utils.js +2 -2
- package/dist/services/versions.d.ts +1 -1
- package/dist/services/webhooks.d.ts +8 -4
- package/dist/services/webhooks.js +15 -12
- package/dist/types/items.d.ts +1 -8
- package/dist/types/services.d.ts +1 -2
- package/dist/utils/apply-diff.js +2 -1
- package/dist/utils/get-ast-from-query.js +1 -1
- package/dist/utils/get-auth-providers.d.ts +3 -1
- package/dist/utils/get-auth-providers.js +15 -4
- package/dist/utils/get-cache-headers.js +0 -3
- package/dist/utils/get-schema.d.ts +1 -1
- package/dist/utils/get-schema.js +52 -29
- package/dist/utils/merge-version-data.js +1 -1
- package/dist/utils/transaction.d.ts +9 -0
- package/dist/utils/transaction.js +15 -0
- package/dist/utils/validate-keys.d.ts +1 -2
- package/dist/websocket/controllers/base.d.ts +1 -3
- package/dist/websocket/controllers/base.js +12 -3
- package/dist/websocket/utils/items.d.ts +1 -1
- package/license +1 -1
- package/package.json +39 -37
- package/dist/webhooks.d.ts +0 -4
- 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
|
+
}
|
package/dist/database/run-ast.js
CHANGED
|
@@ -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(
|
|
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 (
|
|
334
|
-
parentItem[nestedNode.fieldKey] = parentItem[nestedNode.fieldKey].slice(0,
|
|
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.
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|
package/dist/services/fields.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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));
|
package/dist/services/files.d.ts
CHANGED
|
@@ -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
|
|
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 {
|
package/dist/services/flows.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { FlowRaw } from '@directus/types';
|
|
2
|
-
import type { AbstractServiceOptions,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
215
|
+
await transaction(database, async (trx) => {
|
|
215
216
|
const service = new ItemsService(collection, {
|
|
216
217
|
accountability: this.accountability,
|
|
217
218
|
schema: this.schema,
|
package/dist/services/items.d.ts
CHANGED
|
@@ -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,
|
|
4
|
+
import type { AbstractService, AbstractServiceOptions, MutationOptions } from '../types/index.js';
|
|
5
5
|
export type QueryOptions = {
|
|
6
6
|
stripNonRequested?: boolean;
|
|
7
7
|
permissionsAction?: PermissionsAction;
|