@directus/api 14.1.0 → 14.1.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/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) => {
@@ -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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directus/api",
3
- "version": "14.1.0",
3
+ "version": "14.1.1",
4
4
  "description": "Directus is a real-time API and App dashboard for managing SQL database content",
5
5
  "keywords": [
6
6
  "directus",
@@ -145,23 +145,23 @@
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",
148
+ "@directus/app": "10.13.1",
149
149
  "@directus/constants": "11.0.1",
150
- "@directus/errors": "0.2.0",
150
+ "@directus/extensions": "0.2.0",
151
151
  "@directus/extensions-sdk": "10.2.0",
152
+ "@directus/errors": "0.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
155
  "@directus/storage": "10.0.7",
156
+ "@directus/specs": "10.2.3",
157
157
  "@directus/storage-driver-azure": "10.0.14",
158
- "@directus/storage-driver-gcs": "10.0.14",
159
- "@directus/storage-driver-cloudinary": "10.0.14",
160
158
  "@directus/storage-driver-local": "10.0.14",
159
+ "@directus/storage-driver-cloudinary": "10.0.14",
161
160
  "@directus/storage-driver-s3": "10.0.14",
161
+ "@directus/storage-driver-gcs": "10.0.14",
162
162
  "@directus/storage-driver-supabase": "1.0.6",
163
- "@directus/validation": "0.0.9",
164
- "@directus/utils": "11.0.2"
163
+ "@directus/utils": "11.0.2",
164
+ "@directus/validation": "0.0.9"
165
165
  },
166
166
  "devDependencies": {
167
167
  "@ngneat/falso": "6.4.0",