@directus/api 23.1.2 → 23.2.0

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 (52) hide show
  1. package/dist/app.js +7 -4
  2. package/dist/auth/drivers/openid.js +1 -1
  3. package/dist/controllers/activity.js +2 -88
  4. package/dist/controllers/comments.js +0 -7
  5. package/dist/controllers/items.js +1 -1
  6. package/dist/controllers/tus.d.ts +0 -1
  7. package/dist/controllers/tus.js +0 -16
  8. package/dist/controllers/versions.js +1 -8
  9. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +1 -1
  10. package/dist/database/helpers/schema/dialects/cockroachdb.js +3 -3
  11. package/dist/database/helpers/schema/dialects/mssql.d.ts +1 -1
  12. package/dist/database/helpers/schema/dialects/mssql.js +3 -3
  13. package/dist/database/helpers/schema/dialects/oracle.d.ts +2 -1
  14. package/dist/database/helpers/schema/dialects/oracle.js +8 -3
  15. package/dist/database/helpers/schema/dialects/postgres.d.ts +1 -1
  16. package/dist/database/helpers/schema/dialects/postgres.js +3 -3
  17. package/dist/database/helpers/schema/types.d.ts +2 -1
  18. package/dist/database/helpers/schema/types.js +4 -1
  19. package/dist/database/helpers/schema/utils/{preprocess-bindings.d.ts → prep-query-params.d.ts} +2 -2
  20. package/dist/database/helpers/schema/utils/{preprocess-bindings.js → prep-query-params.js} +1 -1
  21. package/dist/database/index.js +8 -2
  22. package/dist/database/migrations/20240909A-separate-comments.js +1 -6
  23. package/dist/database/migrations/20240924A-migrate-legacy-comments.d.ts +3 -0
  24. package/dist/database/migrations/20240924A-migrate-legacy-comments.js +59 -0
  25. package/dist/database/migrations/20240924B-populate-versioning-deltas.d.ts +3 -0
  26. package/dist/database/migrations/20240924B-populate-versioning-deltas.js +32 -0
  27. package/dist/database/run-ast/utils/apply-parent-filters.js +4 -0
  28. package/dist/database/run-ast/utils/with-preprocess-bindings.js +4 -3
  29. package/dist/permissions/modules/validate-access/lib/validate-item-access.js +2 -1
  30. package/dist/schedules/retention.d.ts +14 -0
  31. package/dist/schedules/retention.js +96 -0
  32. package/dist/{telemetry/lib/init-telemetry.d.ts → schedules/telemetry.d.ts} +2 -2
  33. package/dist/{telemetry/lib/init-telemetry.js → schedules/telemetry.js} +6 -6
  34. package/dist/schedules/tus.d.ts +6 -0
  35. package/dist/schedules/tus.js +23 -0
  36. package/dist/services/assets.js +4 -3
  37. package/dist/services/comments.d.ts +4 -22
  38. package/dist/services/comments.js +16 -252
  39. package/dist/services/graphql/index.d.ts +1 -2
  40. package/dist/services/graphql/index.js +1 -75
  41. package/dist/services/users.js +1 -1
  42. package/dist/services/versions.d.ts +0 -1
  43. package/dist/services/versions.js +9 -29
  44. package/dist/storage/register-locations.js +1 -0
  45. package/dist/telemetry/index.d.ts +0 -1
  46. package/dist/telemetry/index.js +0 -1
  47. package/dist/utils/apply-diff.js +15 -3
  48. package/dist/utils/get-service.js +1 -1
  49. package/dist/utils/get-snapshot-diff.js +17 -1
  50. package/dist/websocket/controllers/base.js +2 -1
  51. package/dist/websocket/controllers/graphql.js +2 -1
  52. package/package.json +19 -19
@@ -29,7 +29,6 @@ import { sanitizeQuery } from '../../utils/sanitize-query.js';
29
29
  import { validateQuery } from '../../utils/validate-query.js';
30
30
  import { AuthenticationService } from '../authentication.js';
31
31
  import { CollectionsService } from '../collections.js';
32
- import { CommentsService } from '../comments.js';
33
32
  import { ExtensionsService } from '../extensions.js';
34
33
  import { FieldsService } from '../fields.js';
35
34
  import { FilesService } from '../files.js';
@@ -196,7 +195,6 @@ export class GraphQLService {
196
195
  CreateCollectionTypes,
197
196
  ReadCollectionTypes,
198
197
  UpdateCollectionTypes,
199
- DeleteCollectionTypes,
200
198
  }, schema);
201
199
  }
202
200
  const readableCollections = Object.values(schema.read.collections)
@@ -1650,7 +1648,7 @@ export class GraphQLService {
1650
1648
  })).filter((s) => s);
1651
1649
  return result;
1652
1650
  }
1653
- injectSystemResolvers(schemaComposer, { CreateCollectionTypes, ReadCollectionTypes, UpdateCollectionTypes, DeleteCollectionTypes, }, schema) {
1651
+ injectSystemResolvers(schemaComposer, { CreateCollectionTypes, ReadCollectionTypes, UpdateCollectionTypes, }, schema) {
1654
1652
  const AuthTokens = schemaComposer.createObjectTC({
1655
1653
  name: 'auth_tokens',
1656
1654
  fields: {
@@ -2764,78 +2762,6 @@ export class GraphQLService {
2764
2762
  },
2765
2763
  });
2766
2764
  }
2767
- if ('directus_activity' in schema.create.collections) {
2768
- schemaComposer.Mutation.addFields({
2769
- create_comment: {
2770
- type: ReadCollectionTypes['directus_activity'] ?? GraphQLBoolean,
2771
- args: {
2772
- collection: new GraphQLNonNull(GraphQLString),
2773
- item: new GraphQLNonNull(GraphQLID),
2774
- comment: new GraphQLNonNull(GraphQLString),
2775
- },
2776
- resolve: async (_, args, __, info) => {
2777
- const service = new CommentsService({
2778
- accountability: this.accountability,
2779
- schema: this.schema,
2780
- serviceOrigin: 'activity',
2781
- });
2782
- const primaryKey = await service.createOne({
2783
- ...args,
2784
- });
2785
- if ('directus_activity' in ReadCollectionTypes) {
2786
- const selections = this.replaceFragmentsInSelections(info.fieldNodes[0]?.selectionSet?.selections, info.fragments);
2787
- const query = this.getQuery(args, selections || [], info.variableValues);
2788
- return await service.readOne(primaryKey, query);
2789
- }
2790
- return true;
2791
- },
2792
- },
2793
- });
2794
- }
2795
- if ('directus_activity' in schema.update.collections) {
2796
- schemaComposer.Mutation.addFields({
2797
- update_comment: {
2798
- type: ReadCollectionTypes['directus_activity'] ?? GraphQLBoolean,
2799
- args: {
2800
- id: new GraphQLNonNull(GraphQLID),
2801
- comment: new GraphQLNonNull(GraphQLString),
2802
- },
2803
- resolve: async (_, args, __, info) => {
2804
- const commentsService = new CommentsService({
2805
- accountability: this.accountability,
2806
- schema: this.schema,
2807
- serviceOrigin: 'activity',
2808
- });
2809
- const primaryKey = await commentsService.updateOne(args['id'], { comment: args['comment'] });
2810
- if ('directus_activity' in ReadCollectionTypes) {
2811
- const selections = this.replaceFragmentsInSelections(info.fieldNodes[0]?.selectionSet?.selections, info.fragments);
2812
- const query = this.getQuery(args, selections || [], info.variableValues);
2813
- return { ...(await commentsService.readOne(primaryKey, query)), id: args['id'] };
2814
- }
2815
- return true;
2816
- },
2817
- },
2818
- });
2819
- }
2820
- if ('directus_activity' in schema.delete.collections) {
2821
- schemaComposer.Mutation.addFields({
2822
- delete_comment: {
2823
- type: DeleteCollectionTypes['one'],
2824
- args: {
2825
- id: new GraphQLNonNull(GraphQLID),
2826
- },
2827
- resolve: async (_, args) => {
2828
- const commentsService = new CommentsService({
2829
- accountability: this.accountability,
2830
- schema: this.schema,
2831
- serviceOrigin: 'activity',
2832
- });
2833
- await commentsService.deleteOne(args['id']);
2834
- return { id: args['id'] };
2835
- },
2836
- },
2837
- });
2838
- }
2839
2765
  if ('directus_files' in schema.create.collections) {
2840
2766
  schemaComposer.Mutation.addFields({
2841
2767
  import_file: {
@@ -131,7 +131,7 @@ export class UsersService extends ItemsService {
131
131
  */
132
132
  async createOne(data, opts = {}) {
133
133
  try {
134
- if ('email' in data) {
134
+ if ('email' in data && data['email'] !== undefined) {
135
135
  this.validateEmail(data['email']);
136
136
  await this.checkUniqueEmails([data['email']]);
137
137
  }
@@ -9,7 +9,6 @@ export declare class VersionsService extends ItemsService {
9
9
  outdated: boolean;
10
10
  mainHash: string;
11
11
  }>;
12
- getVersionSavesById(id: PrimaryKey): Promise<Partial<Item>[]>;
13
12
  getVersionSaves(key: string, collection: string, item: string | undefined): Promise<Partial<Item>[] | null>;
14
13
  createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
15
14
  createMany(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]>;
@@ -82,17 +82,6 @@ export class VersionsService extends ItemsService {
82
82
  const mainHash = objectHash(mainItem);
83
83
  return { outdated: hash !== mainHash, mainHash };
84
84
  }
85
- async getVersionSavesById(id) {
86
- const revisionsService = new RevisionsService({
87
- knex: this.knex,
88
- schema: this.schema,
89
- });
90
- const result = await revisionsService.readByQuery({
91
- filter: { version: { _eq: id } },
92
- });
93
- return result.map((revision) => revision['delta']);
94
- }
95
- // TODO: Remove legacy need to return a version array in subsequent release
96
85
  async getVersionSaves(key, collection, item) {
97
86
  const filter = {
98
87
  key: { _eq: key },
@@ -107,8 +96,7 @@ export class VersionsService extends ItemsService {
107
96
  if (versions[0]['delta']) {
108
97
  return [versions[0]['delta']];
109
98
  }
110
- const saves = await this.getVersionSavesById(versions[0]['id']);
111
- return saves;
99
+ return null;
112
100
  }
113
101
  async createOne(data, opts) {
114
102
  await this.validateCreateData(data);
@@ -202,12 +190,7 @@ export class VersionsService extends ItemsService {
202
190
  data: revisionDelta,
203
191
  delta: revisionDelta,
204
192
  });
205
- let existingDelta = version['delta'];
206
- if (!existingDelta) {
207
- const saves = await this.getVersionSavesById(key);
208
- existingDelta = assign({}, ...saves);
209
- }
210
- const finalVersionDelta = assign({}, existingDelta, revisionDelta ? JSON.parse(revisionDelta) : null);
193
+ const finalVersionDelta = assign({}, version['delta'], revisionDelta ? JSON.parse(revisionDelta) : null);
211
194
  const sudoService = new ItemsService(this.collection, {
212
195
  knex: this.knex,
213
196
  schema: this.schema,
@@ -220,7 +203,7 @@ export class VersionsService extends ItemsService {
220
203
  return finalVersionDelta;
221
204
  }
222
205
  async promote(version, mainHash, fields) {
223
- const { id, collection, item, delta } = (await this.readOne(version));
206
+ const { collection, item, delta } = (await this.readOne(version));
224
207
  // will throw an error if the accountability does not have permission to update the item
225
208
  if (this.accountability) {
226
209
  await validateAccess({
@@ -233,21 +216,18 @@ export class VersionsService extends ItemsService {
233
216
  knex: this.knex,
234
217
  });
235
218
  }
219
+ if (!delta) {
220
+ throw new UnprocessableContentError({
221
+ reason: `No changes to promote`,
222
+ });
223
+ }
236
224
  const { outdated } = await this.verifyHash(collection, item, mainHash);
237
225
  if (outdated) {
238
226
  throw new UnprocessableContentError({
239
227
  reason: `Main item has changed since this version was last updated`,
240
228
  });
241
229
  }
242
- let versionResult;
243
- if (delta) {
244
- versionResult = delta;
245
- }
246
- else {
247
- const saves = await this.getVersionSavesById(id);
248
- versionResult = assign({}, ...saves);
249
- }
250
- const payloadToUpdate = fields ? pick(versionResult, fields) : versionResult;
230
+ const payloadToUpdate = fields ? pick(delta, fields) : delta;
251
231
  const itemsService = new ItemsService(collection, {
252
232
  accountability: this.accountability,
253
233
  knex: this.knex,
@@ -6,6 +6,7 @@ export const registerLocations = async (storage) => {
6
6
  const env = useEnv();
7
7
  const locations = toArray(env['STORAGE_LOCATIONS']);
8
8
  const tus = {
9
+ enabled: RESUMABLE_UPLOADS.ENABLED,
9
10
  chunkSize: RESUMABLE_UPLOADS.CHUNK_SIZE,
10
11
  };
11
12
  locations.forEach((location) => {
@@ -1,4 +1,3 @@
1
1
  export * from './lib/get-report.js';
2
- export * from './lib/init-telemetry.js';
3
2
  export * from './lib/send-report.js';
4
3
  export * from './lib/track.js';
@@ -1,4 +1,3 @@
1
1
  export * from './lib/get-report.js';
2
- export * from './lib/init-telemetry.js';
3
2
  export * from './lib/send-report.js';
4
3
  export * from './lib/track.js';
@@ -148,7 +148,7 @@ export async function applyDiff(currentSnapshot, snapshotDiff, options) {
148
148
  }
149
149
  }
150
150
  }
151
- const fieldsService = new FieldsService({
151
+ let fieldsService = new FieldsService({
152
152
  knex: trx,
153
153
  schema: await getSchema({ database: trx, bypassCache: true }),
154
154
  });
@@ -156,6 +156,11 @@ export async function applyDiff(currentSnapshot, snapshotDiff, options) {
156
156
  if (diff?.[0]?.kind === DiffKind.NEW && !isNestedMetaUpdate(diff?.[0])) {
157
157
  try {
158
158
  await fieldsService.createField(collection, diff[0].rhs, undefined, mutationOptions);
159
+ // Refresh the schema
160
+ fieldsService = new FieldsService({
161
+ knex: trx,
162
+ schema: await getSchema({ database: trx, bypassCache: true }),
163
+ });
159
164
  }
160
165
  catch (err) {
161
166
  logger.error(`Failed to create field "${collection}.${field}"`);
@@ -183,14 +188,21 @@ export async function applyDiff(currentSnapshot, snapshotDiff, options) {
183
188
  if (diff?.[0]?.kind === DiffKind.DELETE && !isNestedMetaUpdate(diff?.[0])) {
184
189
  try {
185
190
  await fieldsService.deleteField(collection, field, mutationOptions);
191
+ // Refresh the schema
192
+ fieldsService = new FieldsService({
193
+ knex: trx,
194
+ schema: await getSchema({ database: trx, bypassCache: true }),
195
+ });
186
196
  }
187
197
  catch (err) {
188
198
  logger.error(`Failed to delete field "${collection}.${field}"`);
189
199
  throw err;
190
200
  }
191
201
  // Field deletion also cleans up the relationship. We should ignore any relationship
192
- // changes attached to this now non-existing field
193
- snapshotDiff.relations = snapshotDiff.relations.filter((relation) => (relation.collection === collection && relation.field === field) === false);
202
+ // changes attached to this now non-existing field except newly created relationship
203
+ snapshotDiff.relations = snapshotDiff.relations.filter((relation) => (relation.collection === collection &&
204
+ relation.field === field &&
205
+ !relation.diff.some((diff) => diff.kind === DiffKind.NEW)) === false);
194
206
  }
195
207
  }
196
208
  const relationsService = new RelationsService({
@@ -11,7 +11,7 @@ export function getService(collection, opts) {
11
11
  case 'directus_activity':
12
12
  return new ActivityService(opts);
13
13
  case 'directus_comments':
14
- return new CommentsService({ ...opts, serviceOrigin: 'comments' });
14
+ return new CommentsService(opts);
15
15
  case 'directus_dashboards':
16
16
  return new DashboardsService(opts);
17
17
  case 'directus_files':
@@ -25,6 +25,16 @@ export function getSnapshotDiff(current, after) {
25
25
  ...current.fields.map((currentField) => {
26
26
  const afterField = after.fields.find((afterField) => afterField.collection === currentField.collection && afterField.field === currentField.field);
27
27
  const isAutoIncrementPrimaryKey = !!currentField.schema?.is_primary_key && !!currentField.schema?.has_auto_increment;
28
+ // Changing to/from alias fields should delete the current field
29
+ if (afterField &&
30
+ currentField.type !== afterField.type &&
31
+ (currentField.type === 'alias' || afterField.type === 'alias')) {
32
+ return {
33
+ collection: currentField.collection,
34
+ field: currentField.field,
35
+ diff: deepDiff.diff(sanitizeField(currentField, isAutoIncrementPrimaryKey), sanitizeField(undefined, isAutoIncrementPrimaryKey)),
36
+ };
37
+ }
28
38
  return {
29
39
  collection: currentField.collection,
30
40
  field: currentField.field,
@@ -33,7 +43,13 @@ export function getSnapshotDiff(current, after) {
33
43
  }),
34
44
  ...after.fields
35
45
  .filter((afterField) => {
36
- const currentField = current.fields.find((currentField) => currentField.collection === afterField.collection && afterField.field === currentField.field);
46
+ let currentField = current.fields.find((currentField) => currentField.collection === afterField.collection && afterField.field === currentField.field);
47
+ // Changing to/from alias fields should create the new field
48
+ if (currentField &&
49
+ currentField.type !== afterField.type &&
50
+ (currentField.type === 'alias' || afterField.type === 'alias')) {
51
+ currentField = undefined;
52
+ }
37
53
  return !!currentField === false;
38
54
  })
39
55
  .map((afterField) => ({
@@ -16,6 +16,7 @@ import { AuthMode, WebSocketAuthMessage, WebSocketMessage } from '../messages.js
16
16
  import { getExpiresAtForToken } from '../utils/get-expires-at-for-token.js';
17
17
  import { getMessageType } from '../utils/message.js';
18
18
  import { waitForAnyMessage, waitForMessageType } from '../utils/wait-for-message.js';
19
+ import { createDefaultAccountability } from '../../permissions/utils/create-default-accountability.js';
19
20
  const TOKEN_CHECK_INTERVAL = 15 * 60 * 1000; // 15 minutes
20
21
  const logger = useLogger();
21
22
  export default class SocketController {
@@ -116,7 +117,7 @@ export default class SocketController {
116
117
  }
117
118
  this.server.handleUpgrade(request, socket, head, async (ws) => {
118
119
  this.catchInvalidMessages(ws);
119
- const state = { accountability: null, expires_at: null };
120
+ const state = { accountability: createDefaultAccountability(), expires_at: null };
120
121
  this.server.emit('connection', ws, state);
121
122
  });
122
123
  }
@@ -10,6 +10,7 @@ import { ConnectionParams, WebSocketMessage } from '../messages.js';
10
10
  import { getMessageType } from '../utils/message.js';
11
11
  import SocketController from './base.js';
12
12
  import { registerWebSocketEvents } from './hooks.js';
13
+ import { createDefaultAccountability } from '../../permissions/utils/create-default-accountability.js';
13
14
  const logger = useLogger();
14
15
  export class GraphQLSubscriptionController extends SocketController {
15
16
  gql;
@@ -95,7 +96,7 @@ export class GraphQLSubscriptionController extends SocketController {
95
96
  }
96
97
  async handleHandshakeUpgrade({ request, socket, head }) {
97
98
  this.server.handleUpgrade(request, socket, head, async (ws) => {
98
- this.server.emit('connection', ws, { accountability: null, expires_at: null });
99
+ this.server.emit('connection', ws, { accountability: createDefaultAccountability(), expires_at: null });
99
100
  // actual enforcement is handled by the setTokenExpireTimer function
100
101
  });
101
102
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@directus/api",
3
- "version": "23.1.2",
3
+ "version": "23.2.0",
4
4
  "description": "Directus is a real-time API and App dashboard for managing SQL database content",
5
5
  "keywords": [
6
6
  "directus",
@@ -149,29 +149,29 @@
149
149
  "ws": "8.18.0",
150
150
  "zod": "3.23.8",
151
151
  "zod-validation-error": "3.4.0",
152
- "@directus/app": "13.3.2",
153
- "@directus/constants": "12.0.0",
154
- "@directus/env": "3.1.3",
152
+ "@directus/app": "13.3.4",
153
+ "@directus/constants": "12.0.1",
154
+ "@directus/env": "4.1.0",
155
155
  "@directus/errors": "1.0.1",
156
- "@directus/extensions-registry": "2.0.4",
157
- "@directus/extensions": "2.0.4",
158
- "@directus/extensions-sdk": "12.1.2",
156
+ "@directus/extensions": "2.0.6",
157
+ "@directus/extensions-registry": "2.0.6",
158
+ "@directus/extensions-sdk": "12.1.4",
159
159
  "@directus/format-title": "11.0.0",
160
- "@directus/memory": "2.0.4",
161
- "@directus/pressure": "2.0.3",
160
+ "@directus/memory": "2.0.6",
161
+ "@directus/pressure": "2.0.5",
162
162
  "@directus/schema": "12.1.1",
163
163
  "@directus/specs": "11.1.0",
164
164
  "@directus/storage": "11.0.1",
165
- "@directus/storage-driver-azure": "11.1.0",
166
- "@directus/storage-driver-gcs": "11.1.0",
167
- "@directus/storage-driver-cloudinary": "11.1.0",
165
+ "@directus/storage-driver-azure": "11.1.2",
166
+ "@directus/storage-driver-cloudinary": "11.1.2",
167
+ "@directus/storage-driver-gcs": "11.1.2",
168
168
  "@directus/storage-driver-local": "11.0.1",
169
- "@directus/storage-driver-s3": "11.0.3",
170
- "@directus/storage-driver-supabase": "2.1.0",
171
- "@directus/system-data": "2.1.1",
172
- "@directus/validation": "1.0.3",
173
- "@directus/utils": "12.0.3",
174
- "directus": "11.2.1"
169
+ "@directus/storage-driver-s3": "11.0.5",
170
+ "@directus/storage-driver-supabase": "2.1.2",
171
+ "@directus/system-data": "2.1.2",
172
+ "@directus/utils": "12.0.5",
173
+ "@directus/validation": "1.0.5",
174
+ "directus": "11.3.0"
175
175
  },
176
176
  "devDependencies": {
177
177
  "@ngneat/falso": "7.2.0",
@@ -215,7 +215,7 @@
215
215
  "vitest": "2.1.2",
216
216
  "@directus/random": "1.0.0",
217
217
  "@directus/tsconfig": "2.0.0",
218
- "@directus/types": "12.2.1"
218
+ "@directus/types": "12.2.2"
219
219
  },
220
220
  "optionalDependencies": {
221
221
  "@keyv/redis": "3.0.1",