@directus/api 14.1.0 → 14.1.2

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/constants.js CHANGED
@@ -55,7 +55,7 @@ export const COOKIE_OPTIONS = {
55
55
  secure: env['REFRESH_TOKEN_COOKIE_SECURE'] ?? false,
56
56
  sameSite: env['REFRESH_TOKEN_COOKIE_SAME_SITE'] || 'strict',
57
57
  };
58
- export const OAS_REQUIRED_SCHEMAS = ['Diff', 'Schema', 'Query', 'x-metadata'];
58
+ export const OAS_REQUIRED_SCHEMAS = ['Query', 'x-metadata'];
59
59
  /** Formats from which transformation is supported */
60
60
  export const SUPPORTED_IMAGE_TRANSFORM_FORMATS = ['image/jpeg', 'image/png', 'image/webp', 'image/tiff', 'image/avif'];
61
61
  /** Formats where metadata extraction is supported */
@@ -11,7 +11,7 @@ router.get('/specs/oas', asyncHandler(async (req, res, next) => {
11
11
  accountability: req.accountability,
12
12
  schema: req.schema,
13
13
  });
14
- res.locals['payload'] = await service.oas.generate();
14
+ res.locals['payload'] = await service.oas.generate(req.headers.host);
15
15
  return next();
16
16
  }), respond);
17
17
  router.get('/specs/graphql/:scope?', asyncHandler(async (req, res) => {
@@ -120,6 +120,8 @@ fields:
120
120
  - field: theme_light_overrides
121
121
  width: full
122
122
  interface: system-theme-overrides
123
+ options:
124
+ appearance: light
123
125
  group: theming_group
124
126
  special:
125
127
  - cast-json
@@ -134,6 +136,8 @@ fields:
134
136
  - field: theme_dark_overrides
135
137
  width: full
136
138
  interface: system-theme-overrides
139
+ options:
140
+ appearance: dark
137
141
  group: theming_group
138
142
  special:
139
143
  - cast-json
@@ -124,6 +124,8 @@ fields:
124
124
  - field: theme_light_overrides
125
125
  width: full
126
126
  interface: system-theme-overrides
127
+ options:
128
+ appearance: light
127
129
  special:
128
130
  - cast-json
129
131
 
@@ -137,6 +139,8 @@ fields:
137
139
  - field: theme_dark_overrides
138
140
  width: full
139
141
  interface: system-theme-overrides
142
+ options:
143
+ appearance: dark
140
144
  special:
141
145
  - cast-json
142
146
 
@@ -2,20 +2,14 @@ import type { Accountability, SchemaOverview } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
3
  import type { OpenAPIObject } from 'openapi3-ts/oas30';
4
4
  import type { AbstractServiceOptions } from '../types/index.js';
5
- import { CollectionsService } from './collections.js';
6
- import { FieldsService } from './fields.js';
7
5
  import { GraphQLService } from './graphql/index.js';
8
- import { RelationsService } from './relations.js';
9
6
  export declare class SpecificationService {
10
7
  accountability: Accountability | null;
11
8
  knex: Knex;
12
9
  schema: SchemaOverview;
13
- fieldsService: FieldsService;
14
- collectionsService: CollectionsService;
15
- relationsService: RelationsService;
16
10
  oas: OASSpecsService;
17
11
  graphql: GraphQLSpecsService;
18
- constructor(options: AbstractServiceOptions);
12
+ constructor({ accountability, knex, schema }: AbstractServiceOptions);
19
13
  }
20
14
  interface SpecificationSubService {
21
15
  generate: (_?: any) => Promise<any>;
@@ -24,15 +18,8 @@ declare class OASSpecsService implements SpecificationSubService {
24
18
  accountability: Accountability | null;
25
19
  knex: Knex;
26
20
  schema: SchemaOverview;
27
- fieldsService: FieldsService;
28
- collectionsService: CollectionsService;
29
- relationsService: RelationsService;
30
- constructor(options: AbstractServiceOptions, { fieldsService, collectionsService, relationsService, }: {
31
- fieldsService: FieldsService;
32
- collectionsService: CollectionsService;
33
- relationsService: RelationsService;
34
- });
35
- generate(): Promise<OpenAPIObject>;
21
+ constructor({ knex, schema, accountability }: AbstractServiceOptions);
22
+ generate(host?: string): Promise<OpenAPIObject>;
36
23
  private generateTags;
37
24
  private generatePaths;
38
25
  private generateComponents;
@@ -6,57 +6,39 @@ import getDatabase from '../database/index.js';
6
6
  import env from '../env.js';
7
7
  import { getRelationType } from '../utils/get-relation-type.js';
8
8
  import { version } from '../utils/package.js';
9
- import { CollectionsService } from './collections.js';
10
- import { FieldsService } from './fields.js';
11
9
  import { GraphQLService } from './graphql/index.js';
12
- import { RelationsService } from './relations.js';
10
+ import { reduceSchema } from '../utils/reduce-schema.js';
13
11
  export class SpecificationService {
14
12
  accountability;
15
13
  knex;
16
14
  schema;
17
- fieldsService;
18
- collectionsService;
19
- relationsService;
20
15
  oas;
21
16
  graphql;
22
- constructor(options) {
23
- this.accountability = options.accountability || null;
24
- this.knex = options.knex || getDatabase();
25
- this.schema = options.schema;
26
- this.fieldsService = new FieldsService(options);
27
- this.collectionsService = new CollectionsService(options);
28
- this.relationsService = new RelationsService(options);
29
- this.oas = new OASSpecsService(options, {
30
- fieldsService: this.fieldsService,
31
- collectionsService: this.collectionsService,
32
- relationsService: this.relationsService,
33
- });
34
- this.graphql = new GraphQLSpecsService(options);
17
+ constructor({ accountability, knex, schema }) {
18
+ this.accountability = accountability || null;
19
+ this.knex = knex || getDatabase();
20
+ this.schema = schema;
21
+ this.oas = new OASSpecsService({ knex, schema, accountability });
22
+ this.graphql = new GraphQLSpecsService({ knex, schema });
35
23
  }
36
24
  }
37
25
  class OASSpecsService {
38
26
  accountability;
39
27
  knex;
40
28
  schema;
41
- fieldsService;
42
- collectionsService;
43
- relationsService;
44
- constructor(options, { fieldsService, collectionsService, relationsService, }) {
45
- this.accountability = options.accountability || null;
46
- this.knex = options.knex || getDatabase();
47
- this.schema = options.schema;
48
- this.fieldsService = fieldsService;
49
- this.collectionsService = collectionsService;
50
- this.relationsService = relationsService;
29
+ constructor({ knex, schema, accountability }) {
30
+ this.accountability = accountability || null;
31
+ this.knex = knex || getDatabase();
32
+ this.schema =
33
+ this.accountability?.admin === true ? schema : reduceSchema(schema, accountability?.permissions || null);
51
34
  }
52
- async generate() {
53
- const collections = await this.collectionsService.readByQuery();
54
- const fields = await this.fieldsService.readAll();
55
- const relations = (await this.relationsService.readAll());
35
+ async generate(host) {
56
36
  const permissions = this.accountability?.permissions ?? [];
57
- const tags = await this.generateTags(collections);
37
+ const tags = await this.generateTags();
58
38
  const paths = await this.generatePaths(permissions, tags);
59
- const components = await this.generateComponents(collections, fields, relations, tags);
39
+ const components = await this.generateComponents(tags);
40
+ const isDefaultPublicUrl = env['PUBLIC_URL'] === '/';
41
+ const url = isDefaultPublicUrl && host ? host : env['PUBLIC_URL'];
60
42
  const spec = {
61
43
  openapi: '3.0.1',
62
44
  info: {
@@ -66,7 +48,7 @@ class OASSpecsService {
66
48
  },
67
49
  servers: [
68
50
  {
69
- url: env['PUBLIC_URL'],
51
+ url,
70
52
  description: 'Your current Directus instance.',
71
53
  },
72
54
  ],
@@ -78,11 +60,17 @@ class OASSpecsService {
78
60
  spec.components = components;
79
61
  return spec;
80
62
  }
81
- async generateTags(collections) {
63
+ async generateTags() {
82
64
  const systemTags = cloneDeep(spec.tags);
65
+ const collections = Object.values(this.schema.collections);
83
66
  const tags = [];
84
- // System tags that don't have an associated collection are always readable to the user
85
67
  for (const systemTag of systemTags) {
68
+ // Check if necessary authentication level is given
69
+ if (systemTag['x-authentication'] === 'admin' && !this.accountability?.admin)
70
+ continue;
71
+ if (systemTag['x-authentication'] === 'user' && !this.accountability?.user)
72
+ continue;
73
+ // Remaining system tags that don't have an associated collection are publicly available
86
74
  if (!systemTag['x-collection']) {
87
75
  tags.push(systemTag);
88
76
  }
@@ -103,8 +91,8 @@ class OASSpecsService {
103
91
  name: 'Items' + formatTitle(collection.collection).replace(/ /g, ''),
104
92
  'x-collection': collection.collection,
105
93
  };
106
- if (collection.meta?.note) {
107
- tag.description = collection.meta.note;
94
+ if (collection.note) {
95
+ tag.description = collection.note;
108
96
  }
109
97
  tags.push(tag);
110
98
  }
@@ -256,33 +244,38 @@ class OASSpecsService {
256
244
  }
257
245
  return paths;
258
246
  }
259
- async generateComponents(collections, fields, relations, tags) {
247
+ async generateComponents(tags) {
248
+ if (!tags)
249
+ return;
260
250
  let components = cloneDeep(spec.components);
261
251
  if (!components)
262
252
  components = {};
263
253
  components.schemas = {};
264
- // Always includes the schemas with these names
265
- if (spec.components?.schemas !== null) {
266
- for (const schemaName of OAS_REQUIRED_SCHEMAS) {
267
- if (spec.components.schemas[schemaName] !== null) {
268
- components.schemas[schemaName] = cloneDeep(spec.components.schemas[schemaName]);
269
- }
254
+ const tagSchemas = tags.reduce((schemas, tag) => [...schemas, ...(tag['x-schemas'] ? tag['x-schemas'] : [])], []);
255
+ const requiredSchemas = [...OAS_REQUIRED_SCHEMAS, ...tagSchemas];
256
+ for (const [name, schema] of Object.entries(spec.components?.schemas ?? {})) {
257
+ if (requiredSchemas.includes(name)) {
258
+ const collection = spec.tags?.find((tag) => tag.name === name)?.['x-collection'];
259
+ components.schemas[name] = {
260
+ ...cloneDeep(schema),
261
+ ...(collection && { 'x-collection': collection }),
262
+ };
270
263
  }
271
264
  }
272
- if (!tags)
273
- return;
265
+ const collections = Object.values(this.schema.collections);
274
266
  for (const collection of collections) {
275
267
  const tag = tags.find((tag) => tag['x-collection'] === collection.collection);
276
268
  if (!tag)
277
269
  continue;
278
270
  const isSystem = collection.collection.startsWith('directus_');
279
- const fieldsInCollection = fields.filter((field) => field.collection === collection.collection);
271
+ const fieldsInCollection = Object.values(collection.fields);
280
272
  if (isSystem) {
281
273
  const schemaComponent = cloneDeep(spec.components.schemas[tag.name]);
282
274
  schemaComponent.properties = {};
275
+ schemaComponent['x-collection'] = collection.collection;
283
276
  for (const field of fieldsInCollection) {
284
277
  schemaComponent.properties[field.field] =
285
- cloneDeep(spec.components.schemas[tag.name].properties[field.field]) || this.generateField(field, relations, tags, fields);
278
+ cloneDeep(spec.components.schemas[tag.name].properties[field.field]) || this.generateField(collection.collection, field, tags);
286
279
  }
287
280
  components.schemas[tag.name] = schemaComponent;
288
281
  }
@@ -293,7 +286,7 @@ class OASSpecsService {
293
286
  'x-collection': collection.collection,
294
287
  };
295
288
  for (const field of fieldsInCollection) {
296
- schemaComponent.properties[field.field] = this.generateField(field, relations, tags, fields);
289
+ schemaComponent.properties[field.field] = this.generateField(collection.collection, field, tags);
297
290
  }
298
291
  components.schemas[tag.name] = schemaComponent;
299
292
  }
@@ -316,16 +309,14 @@ class OASSpecsService {
316
309
  return 'read';
317
310
  }
318
311
  }
319
- generateField(field, relations, tags, fields) {
312
+ generateField(collection, field, tags) {
320
313
  let propertyObject = {};
321
- if (field.schema && 'is_nullable' in field.schema) {
322
- propertyObject.nullable = field.schema.is_nullable;
314
+ propertyObject.nullable = field.nullable;
315
+ if (field.note) {
316
+ propertyObject.description = field.note;
323
317
  }
324
- if (field.meta?.note) {
325
- propertyObject.description = field.meta.note;
326
- }
327
- const relation = relations.find((relation) => (relation.collection === field.collection && relation.field === field.field) ||
328
- (relation.related_collection === field.collection && relation.meta?.one_field === field.field));
318
+ const relation = this.schema.relations.find((relation) => (relation.collection === collection && relation.field === field.field) ||
319
+ (relation.related_collection === collection && relation.meta?.one_field === field.field));
329
320
  if (!relation) {
330
321
  propertyObject = {
331
322
  ...propertyObject,
@@ -336,13 +327,17 @@ class OASSpecsService {
336
327
  const relationType = getRelationType({
337
328
  relation,
338
329
  field: field.field,
339
- collection: field.collection,
330
+ collection: collection,
340
331
  });
341
332
  if (relationType === 'm2o') {
342
333
  const relatedTag = tags.find((tag) => tag['x-collection'] === relation.related_collection);
343
- const relatedPrimaryKeyField = fields.find((field) => field.collection === relation.related_collection && field.schema?.is_primary_key);
344
- if (!relatedTag || !relatedPrimaryKeyField)
334
+ if (!relatedTag ||
335
+ !relation.related_collection ||
336
+ relation.related_collection in this.schema.collections === false) {
345
337
  return propertyObject;
338
+ }
339
+ const relatedCollection = this.schema.collections[relation.related_collection];
340
+ const relatedPrimaryKeyField = relatedCollection.fields[relatedCollection.primary];
346
341
  propertyObject.oneOf = [
347
342
  {
348
343
  ...this.fieldTypes[relatedPrimaryKeyField.type],
@@ -354,7 +349,11 @@ class OASSpecsService {
354
349
  }
355
350
  else if (relationType === 'o2m') {
356
351
  const relatedTag = tags.find((tag) => tag['x-collection'] === relation.collection);
357
- const relatedPrimaryKeyField = fields.find((field) => field.collection === relation.collection && field.schema?.is_primary_key);
352
+ if (!relatedTag || !relation.related_collection || relation.collection in this.schema.collections === false) {
353
+ return propertyObject;
354
+ }
355
+ const relatedCollection = this.schema.collections[relation.collection];
356
+ const relatedPrimaryKeyField = relatedCollection.fields[relatedCollection.primary];
358
357
  if (!relatedTag || !relatedPrimaryKeyField)
359
358
  return propertyObject;
360
359
  propertyObject.type = 'array';
@@ -2,7 +2,7 @@ import type { Accountability, Query, SchemaOverview } from '@directus/types';
2
2
  import type { Knex } from 'knex';
3
3
  import type { Item, PrimaryKey } from './items.js';
4
4
  export type AbstractServiceOptions = {
5
- knex?: Knex;
5
+ knex?: Knex | undefined;
6
6
  accountability?: Accountability | null | undefined;
7
7
  schema: SchemaOverview;
8
8
  };
@@ -86,9 +86,6 @@ function parsePermissions(permissions) {
86
86
  if (permission.permissions && typeof permission.permissions === 'string') {
87
87
  permission.permissions = parseJSON(permission.permissions);
88
88
  }
89
- else if (permission.permissions === null) {
90
- permission.permissions = {};
91
- }
92
89
  if (permission.validation && typeof permission.validation === 'string') {
93
90
  permission.validation = parseJSON(permission.validation);
94
91
  }
@@ -55,7 +55,8 @@ export function getSnapshotDiff(current, after) {
55
55
  }),
56
56
  ...after.relations
57
57
  .filter((afterRelation) => {
58
- const currentRelation = current.relations.find((currentRelation) => currentRelation.collection === afterRelation.collection && afterRelation.field === currentRelation.field);
58
+ const currentRelation = current.relations.find((currentRelation) => currentRelation.collection === afterRelation.collection &&
59
+ afterRelation.field === currentRelation.field);
59
60
  return !!currentRelation === false;
60
61
  })
61
62
  .map((afterRelation) => ({
@@ -1,4 +1,4 @@
1
- import { flatten, intersection, isEmpty, merge, omit } from 'lodash-es';
1
+ import { flatten, intersection, isEqual, merge, omit } from 'lodash-es';
2
2
  export function mergePermissions(strategy, ...permissions) {
3
3
  const allPermissions = flatten(permissions);
4
4
  const mergedPermissions = allPermissions
@@ -27,11 +27,15 @@ export function mergePermission(strategy, currentPerm, newPerm) {
27
27
  };
28
28
  }
29
29
  else if (currentPerm.permissions) {
30
- permissions = {
31
- [logicalKey]: strategy === 'or'
32
- ? [currentPerm.permissions, newPerm.permissions].filter((p) => !isEmpty(p))
33
- : [currentPerm.permissions, newPerm.permissions],
34
- };
30
+ // Empty {} supersedes other permissions in _OR merge
31
+ if (strategy === 'or' && (isEqual(currentPerm.permissions, {}) || isEqual(newPerm.permissions, {}))) {
32
+ permissions = {};
33
+ }
34
+ else {
35
+ permissions = {
36
+ [logicalKey]: [currentPerm.permissions, newPerm.permissions],
37
+ };
38
+ }
35
39
  }
36
40
  else {
37
41
  permissions = {
@@ -49,11 +53,15 @@ export function mergePermission(strategy, currentPerm, newPerm) {
49
53
  };
50
54
  }
51
55
  else if (currentPerm.validation) {
52
- validation = {
53
- [logicalKey]: strategy === 'or'
54
- ? [currentPerm.validation, newPerm.validation].filter((p) => !isEmpty(p))
55
- : [currentPerm.validation, newPerm.validation],
56
- };
56
+ // Empty {} supersedes other validations in _OR merge
57
+ if (strategy === 'or' && (isEqual(currentPerm.validation, {}) || isEqual(newPerm.validation, {}))) {
58
+ validation = {};
59
+ }
60
+ else {
61
+ validation = {
62
+ [logicalKey]: [currentPerm.validation, newPerm.validation],
63
+ };
64
+ }
57
65
  }
58
66
  else {
59
67
  validation = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directus/api",
3
- "version": "14.1.0",
3
+ "version": "14.1.2",
4
4
  "description": "Directus is a real-time API and App dashboard for managing SQL database content",
5
5
  "keywords": [
6
6
  "directus",
@@ -138,30 +138,30 @@
138
138
  "stream-json": "1.7.5",
139
139
  "strip-bom-stream": "5.0.0",
140
140
  "tinypool": "0.8.1",
141
- "tsx": "3.12.7",
141
+ "tsx": "4.1.4",
142
142
  "uuid": "9.0.0",
143
143
  "uuid-validate": "0.0.3",
144
144
  "wellknown": "0.5.0",
145
145
  "ws": "8.14.2",
146
146
  "zod": "3.22.4",
147
147
  "zod-validation-error": "1.0.1",
148
- "@directus/app": "10.13.0",
149
- "@directus/constants": "11.0.1",
148
+ "@directus/app": "10.13.2",
150
149
  "@directus/errors": "0.2.0",
150
+ "@directus/constants": "11.0.1",
151
+ "@directus/extensions": "0.2.0",
151
152
  "@directus/extensions-sdk": "10.2.0",
152
153
  "@directus/pressure": "1.0.13",
153
- "@directus/extensions": "0.2.0",
154
154
  "@directus/schema": "11.0.0",
155
- "@directus/specs": "10.2.2",
156
- "@directus/storage": "10.0.7",
155
+ "@directus/specs": "10.2.4",
157
156
  "@directus/storage-driver-azure": "10.0.14",
158
- "@directus/storage-driver-gcs": "10.0.14",
159
157
  "@directus/storage-driver-cloudinary": "10.0.14",
158
+ "@directus/storage-driver-gcs": "10.0.14",
159
+ "@directus/storage": "10.0.7",
160
160
  "@directus/storage-driver-local": "10.0.14",
161
- "@directus/storage-driver-s3": "10.0.14",
162
161
  "@directus/storage-driver-supabase": "1.0.6",
163
- "@directus/validation": "0.0.9",
164
- "@directus/utils": "11.0.2"
162
+ "@directus/storage-driver-s3": "10.0.14",
163
+ "@directus/utils": "11.0.2",
164
+ "@directus/validation": "0.0.9"
165
165
  },
166
166
  "devDependencies": {
167
167
  "@ngneat/falso": "6.4.0",
@@ -202,13 +202,13 @@
202
202
  "@types/uuid-validate": "0.0.1",
203
203
  "@types/wellknown": "0.5.4",
204
204
  "@types/ws": "8.5.8",
205
- "@vitest/coverage-c8": "0.31.1",
205
+ "@vitest/coverage-v8": "0.34.6",
206
206
  "copyfiles": "2.4.1",
207
207
  "form-data": "4.0.0",
208
208
  "knex-mock-client": "2.0.0",
209
209
  "supertest": "6.3.3",
210
210
  "typescript": "5.2.2",
211
- "vitest": "0.31.1",
211
+ "vitest": "0.34.6",
212
212
  "@directus/random": "0.2.3",
213
213
  "@directus/tsconfig": "1.0.1",
214
214
  "@directus/types": "11.0.2"