@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
package/dist/flows.js CHANGED
@@ -33,7 +33,7 @@ const LAST_KEY = '$last';
33
33
  const ENV_KEY = '$env';
34
34
  class FlowManager {
35
35
  isLoaded = false;
36
- operations = {};
36
+ operations = new Map();
37
37
  triggerHandlers = [];
38
38
  operationFlowHandlers = {};
39
39
  webhookFlowHandlers = {};
@@ -67,10 +67,10 @@ class FlowManager {
67
67
  messenger.publish('flows', { type: 'reload' });
68
68
  }
69
69
  addOperation(id, operation) {
70
- this.operations[id] = operation;
70
+ this.operations.set(id, operation);
71
71
  }
72
- clearOperations() {
73
- this.operations = {};
72
+ removeOperation(id) {
73
+ this.operations.delete(id);
74
74
  }
75
75
  async runOperationFlow(id, data, context) {
76
76
  if (!(id in this.operationFlowHandlers)) {
@@ -305,11 +305,11 @@ class FlowManager {
305
305
  return undefined;
306
306
  }
307
307
  async executeOperation(operation, keyedData, context = {}) {
308
- if (!(operation.type in this.operations)) {
308
+ if (!this.operations.has(operation.type)) {
309
309
  logger.warn(`Couldn't find operation ${operation.type}`);
310
310
  return { successor: null, status: 'unknown', data: null, options: null };
311
311
  }
312
- const handler = this.operations[operation.type];
312
+ const handler = this.operations.get(operation.type);
313
313
  const options = applyOptionsData(operation.options, keyedData);
314
314
  try {
315
315
  let result = await handler(options, {
@@ -1,8 +1,10 @@
1
1
  import { parse as parseBytesConfiguration } from 'bytes';
2
+ import { assign } from 'lodash-es';
2
3
  import { getCache, setCacheValue } from '../cache.js';
3
4
  import env from '../env.js';
4
5
  import logger from '../logger.js';
5
6
  import { ExportService } from '../services/import-export/index.js';
7
+ import { VersionsService } from '../services/versions.js';
6
8
  import asyncHandler from '../utils/async-handler.js';
7
9
  import { getCacheControlHeader } from '../utils/get-cache-headers.js';
8
10
  import { getCacheKey } from '../utils/get-cache-key.js';
@@ -17,6 +19,16 @@ export const respond = asyncHandler(async (req, res) => {
17
19
  const maxSize = parseBytesConfiguration(env['CACHE_VALUE_MAX_SIZE']);
18
20
  exceedsMaxSize = valueSize > maxSize;
19
21
  }
22
+ if (req.sanitizedQuery.version &&
23
+ req.collection &&
24
+ (req.singleton || req.params['pk']) &&
25
+ 'data' in res.locals['payload']) {
26
+ const versionsService = new VersionsService({ accountability: req.accountability ?? null, schema: req.schema });
27
+ const saves = await versionsService.getVersionSaves(req.sanitizedQuery.version, req.collection, req.params['pk']);
28
+ if (saves) {
29
+ assign(res.locals['payload'].data, ...saves);
30
+ }
31
+ }
20
32
  if ((req.method.toLowerCase() === 'get' || req.originalUrl?.startsWith('/graphql')) &&
21
33
  env['CACHE_ENABLED'] === true &&
22
34
  cache &&
package/dist/server.js CHANGED
@@ -10,9 +10,9 @@ import emitter from './emitter.js';
10
10
  import env from './env.js';
11
11
  import logger from './logger.js';
12
12
  import { getConfigFromEnv } from './utils/get-config-from-env.js';
13
+ import { toBoolean } from './utils/to-boolean.js';
13
14
  import { createSubscriptionController, createWebSocketController, getSubscriptionController, getWebSocketController, } from './websocket/controllers/index.js';
14
15
  import { startWebSocketHandlers } from './websocket/handlers/index.js';
15
- import { toBoolean } from './utils/to-boolean.js';
16
16
  export let SERVER_ONLINE = true;
17
17
  export async function createServer() {
18
18
  const server = http.createServer(await createApp());
@@ -116,6 +116,7 @@ export async function startServer() {
116
116
  server
117
117
  .listen(port, host, () => {
118
118
  logger.info(`Server started at http://${host}:${port}`);
119
+ process.send?.('ready');
119
120
  emitter.emitAction('server.start', { server }, {
120
121
  database: getDatabase(),
121
122
  schema: null,
@@ -25,7 +25,7 @@ export class AssetsService {
25
25
  async getAsset(id, transformation, range) {
26
26
  const storage = await getStorage();
27
27
  const publicSettings = await this.knex
28
- .select('project_logo', 'public_background', 'public_foreground')
28
+ .select('project_logo', 'public_background', 'public_foreground', 'public_favicon')
29
29
  .from('directus_settings')
30
30
  .first();
31
31
  const systemPublicKeys = Object.values(publicSettings || {});
@@ -0,0 +1,31 @@
1
+ import type { ApiOutput, ExtensionSettings } from '@directus/extensions';
2
+ import type { SchemaInspector } from '@directus/schema';
3
+ import type { Accountability, DeepPartial, SchemaOverview } from '@directus/types';
4
+ import type Keyv from 'keyv';
5
+ import type { Knex } from 'knex';
6
+ import type { Helpers } from '../database/helpers/index.js';
7
+ import type { ExtensionManager } from '../extensions/manager.js';
8
+ import type { AbstractServiceOptions } from '../types/index.js';
9
+ import { ItemsService } from './items.js';
10
+ import { PermissionsService } from './permissions.js';
11
+ export declare class ExtensionsService {
12
+ knex: Knex;
13
+ permissionsService: PermissionsService;
14
+ schemaInspector: SchemaInspector;
15
+ accountability: Accountability | null;
16
+ schema: SchemaOverview;
17
+ extensionsItemService: ItemsService<ExtensionSettings>;
18
+ systemCache: Keyv<any>;
19
+ helpers: Helpers;
20
+ extensionsManager: ExtensionManager;
21
+ constructor(options: AbstractServiceOptions);
22
+ readAll(): Promise<ApiOutput[]>;
23
+ readOne(bundle: string | null, name: string): Promise<ApiOutput>;
24
+ updateOne(bundle: string | null, name: string, data: DeepPartial<ApiOutput>): Promise<void>;
25
+ private getKey;
26
+ /**
27
+ * Combine the settings stored in the database with the information available from the installed
28
+ * extensions into the standardized extensions api output
29
+ */
30
+ private stitch;
31
+ }
@@ -0,0 +1,136 @@
1
+ import { ForbiddenError, InvalidPayloadError } from '@directus/errors';
2
+ import { createInspector } from '@directus/schema';
3
+ import Joi from 'joi';
4
+ import { omit, pick } from 'lodash-es';
5
+ import { getCache } from '../cache.js';
6
+ import { getHelpers } from '../database/helpers/index.js';
7
+ import getDatabase, { getSchemaInspector } from '../database/index.js';
8
+ import { getExtensionManager } from '../extensions/index.js';
9
+ import { ItemsService } from './items.js';
10
+ import { PermissionsService } from './permissions.js';
11
+ export class ExtensionsService {
12
+ knex;
13
+ permissionsService;
14
+ schemaInspector;
15
+ accountability;
16
+ schema;
17
+ extensionsItemService;
18
+ systemCache;
19
+ helpers;
20
+ extensionsManager;
21
+ constructor(options) {
22
+ this.knex = options.knex || getDatabase();
23
+ this.permissionsService = new PermissionsService(options);
24
+ this.schemaInspector = options.knex ? createInspector(options.knex) : getSchemaInspector();
25
+ this.schema = options.schema;
26
+ this.accountability = options.accountability || null;
27
+ this.extensionsManager = getExtensionManager();
28
+ this.extensionsItemService = new ItemsService('directus_extensions', {
29
+ knex: this.knex,
30
+ schema: this.schema,
31
+ // No accountability here, as every other method is hardcoded to be admin only
32
+ });
33
+ this.systemCache = getCache().systemCache;
34
+ this.helpers = getHelpers(this.knex);
35
+ }
36
+ async readAll() {
37
+ if (this.accountability?.admin !== true) {
38
+ throw new ForbiddenError();
39
+ }
40
+ const installedExtensions = this.extensionsManager.getExtensions();
41
+ const configuredExtensions = await this.extensionsItemService.readByQuery({ limit: -1 });
42
+ return this.stitch(installedExtensions, configuredExtensions);
43
+ }
44
+ async readOne(bundle, name) {
45
+ if (this.accountability?.admin !== true) {
46
+ throw new ForbiddenError();
47
+ }
48
+ const key = this.getKey(bundle, name);
49
+ const schema = this.extensionsManager.getExtensions().find((extension) => extension.name === bundle ?? name);
50
+ const meta = await this.extensionsItemService.readOne(key);
51
+ const stitched = this.stitch(schema ? [schema] : [], [meta])[0];
52
+ if (stitched)
53
+ return stitched;
54
+ throw new ForbiddenError();
55
+ }
56
+ async updateOne(bundle, name, data) {
57
+ if (this.accountability?.admin !== true) {
58
+ throw new ForbiddenError();
59
+ }
60
+ const key = this.getKey(bundle, name);
61
+ const updateExtensionSchema = Joi.object({
62
+ meta: Joi.object({
63
+ enabled: Joi.boolean(),
64
+ }),
65
+ });
66
+ const { error } = updateExtensionSchema.validate(data);
67
+ if (error) {
68
+ throw new InvalidPayloadError({ reason: error.message });
69
+ }
70
+ if ('meta' in data && 'enabled' in data.meta) {
71
+ await this.knex('directus_extensions').update({ enabled: data.meta.enabled }).where({ name: key });
72
+ this.extensionsManager.reload();
73
+ }
74
+ }
75
+ getKey(bundle, name) {
76
+ return bundle ? `${bundle}/${name}` : name;
77
+ }
78
+ /**
79
+ * Combine the settings stored in the database with the information available from the installed
80
+ * extensions into the standardized extensions api output
81
+ */
82
+ stitch(installed, configured) {
83
+ /**
84
+ * On startup, the extensions manager will automatically create the rows for installed
85
+ * extensions that don't have configured settings yet, so there should always be equal or more
86
+ * settings rows than installed extensions.
87
+ */
88
+ return configured.map((meta) => {
89
+ let bundleName = null;
90
+ let name = meta.name;
91
+ if (name.includes('/')) {
92
+ const parts = name.split('/');
93
+ // NPM packages can have an optional organization scope in the format
94
+ // `@<org>/<package>`. This is limited to a single `/`.
95
+ //
96
+ // `foo` -> extension
97
+ // `foo/bar` -> bundle
98
+ // `@rijk/foo` -> extension
99
+ // `@rijk/foo/bar -> bundle
100
+ const hasOrg = parts.at(0).startsWith('@');
101
+ if (hasOrg && parts.length > 2) {
102
+ name = parts.pop();
103
+ bundleName = parts.join('/');
104
+ }
105
+ else if (hasOrg === false) {
106
+ [bundleName, name] = parts;
107
+ }
108
+ }
109
+ let schema;
110
+ if (bundleName) {
111
+ const bundle = installed.find((extension) => extension.name === bundleName);
112
+ if (bundle && 'entries' in bundle) {
113
+ const entry = bundle.entries.find((entry) => entry.name === name) ?? null;
114
+ if (entry) {
115
+ schema = {
116
+ type: entry.type,
117
+ local: bundle.local,
118
+ };
119
+ }
120
+ }
121
+ else {
122
+ schema = null;
123
+ }
124
+ }
125
+ else {
126
+ schema = installed.find((extension) => extension.name === name) ?? null;
127
+ }
128
+ return {
129
+ name,
130
+ bundle: bundleName,
131
+ schema: schema ? pick(schema, 'type', 'local') : null,
132
+ meta: omit(meta, 'name'),
133
+ };
134
+ });
135
+ }
136
+ }
@@ -1,4 +1,4 @@
1
- import type { DirectusError } from '@directus/errors';
1
+ import { type DirectusError } from '@directus/errors';
2
2
  import type { Accountability, Filter, Query, SchemaOverview } from '@directus/types';
3
3
  import type { ArgumentNode, FormattedExecutionResult, FragmentDefinitionNode, GraphQLResolveInfo, SelectionNode } from 'graphql';
4
4
  import { GraphQLError, GraphQLSchema } from 'graphql';
@@ -1,16 +1,14 @@
1
1
  import { Action, FUNCTIONS } from '@directus/constants';
2
- import { isDirectusError } from '@directus/errors';
2
+ import { ErrorCode, ForbiddenError, InvalidPayloadError, isDirectusError } from '@directus/errors';
3
3
  import { parseFilterFunctionPath } from '@directus/utils';
4
4
  import argon2 from 'argon2';
5
5
  import { GraphQLBoolean, GraphQLEnumType, GraphQLError, GraphQLFloat, GraphQLID, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLObjectType, GraphQLScalarType, GraphQLSchema, GraphQLString, GraphQLUnionType, NoSchemaIntrospectionCustomRule, execute, specifiedRules, validate, } from 'graphql';
6
6
  import { GraphQLJSON, InputTypeComposer, ObjectTypeComposer, SchemaComposer, toInputObjectType } from 'graphql-compose';
7
- import { flatten, get, mapKeys, merge, omit, pick, set, transform, uniq } from 'lodash-es';
7
+ import { assign, flatten, get, mapKeys, merge, omit, pick, set, transform, uniq } from 'lodash-es';
8
8
  import { clearSystemCache, getCache } from '../../cache.js';
9
9
  import { DEFAULT_AUTH_PROVIDER, GENERATE_SPECIAL } from '../../constants.js';
10
10
  import getDatabase from '../../database/index.js';
11
11
  import env from '../../env.js';
12
- import { ErrorCode, ForbiddenError, InvalidPayloadError } from '@directus/errors';
13
- import { getExtensionManager } from '../../extensions/index.js';
14
12
  import { generateHash } from '../../utils/generate-hash.js';
15
13
  import { getGraphQLType } from '../../utils/get-graphql-type.js';
16
14
  import { getMilliseconds } from '../../utils/get-milliseconds.js';
@@ -22,6 +20,7 @@ import { validateQuery } from '../../utils/validate-query.js';
22
20
  import { ActivityService } from '../activity.js';
23
21
  import { AuthenticationService } from '../authentication.js';
24
22
  import { CollectionsService } from '../collections.js';
23
+ import { ExtensionsService } from '../extensions.js';
25
24
  import { FieldsService } from '../fields.js';
26
25
  import { FilesService } from '../files.js';
27
26
  import { RelationsService } from '../relations.js';
@@ -31,6 +30,7 @@ import { SpecificationService } from '../specifications.js';
31
30
  import { TFAService } from '../tfa.js';
32
31
  import { UsersService } from '../users.js';
33
32
  import { UtilsService } from '../utils.js';
33
+ import { VersionsService } from '../versions.js';
34
34
  import { GraphQLExecutionError, GraphQLValidationError } from './errors/index.js';
35
35
  import { createSubscriptionGenerator } from './subscription.js';
36
36
  import { GraphQLBigInt } from './types/bigint.js';
@@ -54,6 +54,7 @@ const SYSTEM_DENY_LIST = [
54
54
  'directus_relations',
55
55
  'directus_migrations',
56
56
  'directus_sessions',
57
+ 'directus_extensions',
57
58
  ];
58
59
  const READ_ONLY = ['directus_activity', 'directus_revisions'];
59
60
  export class GraphQLService {
@@ -842,6 +843,13 @@ export class GraphQLService {
842
843
  },
843
844
  };
844
845
  }
846
+ else {
847
+ resolver.args = {
848
+ version: {
849
+ type: GraphQLString,
850
+ },
851
+ };
852
+ }
845
853
  ReadCollectionTypes[collection.collection].addResolver(resolver);
846
854
  ReadCollectionTypes[collection.collection].addResolver({
847
855
  name: `${collection.collection}_aggregated`,
@@ -877,6 +885,7 @@ export class GraphQLService {
877
885
  type: ReadCollectionTypes[collection.collection],
878
886
  args: {
879
887
  id: new GraphQLNonNull(GraphQLID),
888
+ version: GraphQLString,
880
889
  },
881
890
  resolve: async ({ info, context }) => {
882
891
  const result = await self.resolveQuery(info);
@@ -1147,6 +1156,20 @@ export class GraphQLService {
1147
1156
  }
1148
1157
  }
1149
1158
  const result = await this.read(collection, query);
1159
+ if (args['version']) {
1160
+ const versionsService = new VersionsService({ accountability: this.accountability, schema: this.schema });
1161
+ const saves = await versionsService.getVersionSaves(args['version'], collection, args['id']);
1162
+ if (saves) {
1163
+ if (this.schema.collections[collection].singleton) {
1164
+ return assign(result, ...saves);
1165
+ }
1166
+ else {
1167
+ if (result?.[0] === undefined)
1168
+ return null;
1169
+ return assign(result[0], ...saves);
1170
+ }
1171
+ }
1172
+ }
1150
1173
  if (args['id']) {
1151
1174
  return result?.[0] || null;
1152
1175
  }
@@ -1597,26 +1620,6 @@ export class GraphQLService {
1597
1620
  }
1598
1621
  /** Globally available query */
1599
1622
  schemaComposer.Query.addFields({
1600
- extensions: {
1601
- type: schemaComposer.createObjectTC({
1602
- name: 'extensions',
1603
- fields: {
1604
- interfaces: new GraphQLList(GraphQLString),
1605
- displays: new GraphQLList(GraphQLString),
1606
- layouts: new GraphQLList(GraphQLString),
1607
- modules: new GraphQLList(GraphQLString),
1608
- },
1609
- }),
1610
- resolve: async () => {
1611
- const extensionManager = getExtensionManager();
1612
- return {
1613
- interfaces: extensionManager.getExtensionsList('interface'),
1614
- displays: extensionManager.getExtensionsList('display'),
1615
- layouts: extensionManager.getExtensionsList('layout'),
1616
- modules: extensionManager.getExtensionsList('module'),
1617
- };
1618
- },
1619
- },
1620
1623
  server_specs_oas: {
1621
1624
  type: GraphQLJSON,
1622
1625
  resolve: async () => {
@@ -1678,6 +1681,9 @@ export class GraphQLService {
1678
1681
  const Relation = schemaComposer.createObjectTC({
1679
1682
  name: 'directus_relations',
1680
1683
  });
1684
+ const Extension = schemaComposer.createObjectTC({
1685
+ name: 'directus_extensions',
1686
+ });
1681
1687
  /**
1682
1688
  * Globally available mutations
1683
1689
  */
@@ -2361,6 +2367,63 @@ export class GraphQLService {
2361
2367
  },
2362
2368
  },
2363
2369
  });
2370
+ Extension.addFields({
2371
+ bundle: GraphQLString,
2372
+ name: new GraphQLNonNull(GraphQLString),
2373
+ schema: schemaComposer.createObjectTC({
2374
+ name: 'directus_extensions_schema',
2375
+ fields: {
2376
+ type: GraphQLString,
2377
+ local: GraphQLBoolean,
2378
+ },
2379
+ }),
2380
+ meta: schemaComposer.createObjectTC({
2381
+ name: 'directus_extensions_meta',
2382
+ fields: {
2383
+ enabled: GraphQLBoolean,
2384
+ },
2385
+ }),
2386
+ });
2387
+ schemaComposer.Query.addFields({
2388
+ extensions: {
2389
+ type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(Extension.getType()))),
2390
+ resolve: async () => {
2391
+ const service = new ExtensionsService({
2392
+ accountability: this.accountability,
2393
+ schema: this.schema,
2394
+ });
2395
+ return await service.readAll();
2396
+ },
2397
+ },
2398
+ });
2399
+ schemaComposer.Mutation.addFields({
2400
+ update_extensions_item: {
2401
+ type: Extension,
2402
+ args: {
2403
+ bundle: GraphQLString,
2404
+ name: new GraphQLNonNull(GraphQLString),
2405
+ data: toInputObjectType(schemaComposer.createObjectTC({
2406
+ name: 'update_directus_extensions_input',
2407
+ fields: {
2408
+ meta: schemaComposer.createObjectTC({
2409
+ name: 'update_directus_extensions_input_meta',
2410
+ fields: {
2411
+ enabled: GraphQLBoolean,
2412
+ },
2413
+ }),
2414
+ },
2415
+ })),
2416
+ },
2417
+ resolve: async (_, args) => {
2418
+ const extensionsService = new ExtensionsService({
2419
+ accountability: this.accountability,
2420
+ schema: this.schema,
2421
+ });
2422
+ await extensionsService.updateOne(args['bundle'], args['name'], args['data']);
2423
+ return await extensionsService.readOne(args['bundle'], args['name']);
2424
+ },
2425
+ },
2426
+ });
2364
2427
  }
2365
2428
  if ('directus_users' in schema.read.collections) {
2366
2429
  schemaComposer.Query.addFields({
@@ -4,6 +4,7 @@ export * from './authentication.js';
4
4
  export * from './authorization.js';
5
5
  export * from './collections.js';
6
6
  export * from './dashboards.js';
7
+ export * from './extensions.js';
7
8
  export * from './fields.js';
8
9
  export * from './files.js';
9
10
  export * from './flows.js';
@@ -31,5 +32,6 @@ export * from './tfa.js';
31
32
  export * from './translations.js';
32
33
  export * from './users.js';
33
34
  export * from './utils.js';
35
+ export * from './versions.js';
34
36
  export * from './webhooks.js';
35
37
  export * from './websocket.js';
@@ -4,6 +4,7 @@ export * from './authentication.js';
4
4
  export * from './authorization.js';
5
5
  export * from './collections.js';
6
6
  export * from './dashboards.js';
7
+ export * from './extensions.js';
7
8
  export * from './fields.js';
8
9
  export * from './files.js';
9
10
  export * from './flows.js';
@@ -31,5 +32,6 @@ export * from './tfa.js';
31
32
  export * from './translations.js';
32
33
  export * from './users.js';
33
34
  export * from './utils.js';
35
+ export * from './versions.js';
34
36
  export * from './webhooks.js';
35
37
  export * from './websocket.js';
@@ -12,8 +12,8 @@ import { rateLimiter } from '../middleware/rate-limiter-ip.js';
12
12
  import { SERVER_ONLINE } from '../server.js';
13
13
  import { getStorage } from '../storage/index.js';
14
14
  import { version } from '../utils/package.js';
15
- import { SettingsService } from './settings.js';
16
15
  import { toBoolean } from '../utils/to-boolean.js';
16
+ import { SettingsService } from './settings.js';
17
17
  export class ServerService {
18
18
  knex;
19
19
  accountability;
@@ -33,9 +33,11 @@ export class ServerService {
33
33
  'project_descriptor',
34
34
  'project_logo',
35
35
  'project_color',
36
+ 'default_appearance',
36
37
  'default_language',
37
38
  'public_foreground',
38
39
  'public_background',
40
+ 'public_favicon',
39
41
  'public_note',
40
42
  'custom_css',
41
43
  ],
@@ -274,7 +274,9 @@ export class UsersService extends ItemsService {
274
274
  catch (err) {
275
275
  (opts || (opts = {})).preMutationError = err;
276
276
  }
277
+ // Manual constraint, see https://github.com/directus/directus/pull/19912
277
278
  await this.knex('directus_notifications').update({ sender: null }).whereIn('sender', keys);
279
+ await this.knex('directus_versions').update({ user_updated: null }).whereIn('user_updated', keys);
278
280
  await super.deleteMany(keys, opts);
279
281
  return keys;
280
282
  }
@@ -0,0 +1,21 @@
1
+ import type { Item, PrimaryKey, Query } from '@directus/types';
2
+ import type { AbstractServiceOptions, MutationOptions } from '../types/index.js';
3
+ import { AuthorizationService } from './authorization.js';
4
+ import { ItemsService } from './items.js';
5
+ export declare class VersionsService extends ItemsService {
6
+ authorizationService: AuthorizationService;
7
+ constructor(options: AbstractServiceOptions);
8
+ private validateCreateData;
9
+ getMainItem(collection: string, item: PrimaryKey, query?: Query): Promise<Item>;
10
+ verifyHash(collection: string, item: PrimaryKey, hash: string): Promise<{
11
+ outdated: boolean;
12
+ mainHash: string;
13
+ }>;
14
+ getVersionSavesById(id: PrimaryKey): Promise<Partial<Item>[]>;
15
+ getVersionSaves(key: string, collection: string, item: string | undefined): Promise<Partial<Item>[] | null>;
16
+ createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
17
+ createMany(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]>;
18
+ updateMany(keys: PrimaryKey[], data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey[]>;
19
+ save(key: PrimaryKey, data: Partial<Item>): Promise<Partial<Item>>;
20
+ promote(version: PrimaryKey, mainHash: string, fields?: string[]): Promise<import("../types/items.js").PrimaryKey>;
21
+ }