@directus/api 24.0.1 → 25.0.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 (85) hide show
  1. package/dist/app.js +10 -4
  2. package/dist/auth/drivers/oauth2.js +2 -3
  3. package/dist/auth/drivers/openid.js +2 -3
  4. package/dist/cache.d.ts +2 -2
  5. package/dist/cache.js +20 -7
  6. package/dist/controllers/assets.js +2 -2
  7. package/dist/controllers/metrics.d.ts +2 -0
  8. package/dist/controllers/metrics.js +33 -0
  9. package/dist/controllers/server.js +1 -1
  10. package/dist/database/helpers/number/dialects/mssql.d.ts +2 -2
  11. package/dist/database/helpers/number/dialects/mssql.js +3 -3
  12. package/dist/database/helpers/number/dialects/oracle.d.ts +2 -2
  13. package/dist/database/helpers/number/dialects/oracle.js +2 -2
  14. package/dist/database/helpers/number/dialects/sqlite.d.ts +2 -2
  15. package/dist/database/helpers/number/dialects/sqlite.js +2 -2
  16. package/dist/database/helpers/number/types.d.ts +2 -2
  17. package/dist/database/helpers/number/types.js +2 -2
  18. package/dist/database/index.js +3 -0
  19. package/dist/metrics/index.d.ts +1 -0
  20. package/dist/metrics/index.js +1 -0
  21. package/dist/metrics/lib/create-metrics.d.ts +15 -0
  22. package/dist/metrics/lib/create-metrics.js +239 -0
  23. package/dist/metrics/lib/use-metrics.d.ts +17 -0
  24. package/dist/metrics/lib/use-metrics.js +15 -0
  25. package/dist/metrics/types/metric.d.ts +1 -0
  26. package/dist/metrics/types/metric.js +1 -0
  27. package/dist/middleware/respond.js +7 -1
  28. package/dist/operations/condition/index.js +7 -2
  29. package/dist/schedules/metrics.d.ts +7 -0
  30. package/dist/schedules/metrics.js +44 -0
  31. package/dist/services/assets.d.ts +6 -1
  32. package/dist/services/assets.js +8 -6
  33. package/dist/services/fields.js +1 -1
  34. package/dist/services/graphql/errors/format.d.ts +6 -0
  35. package/dist/services/graphql/errors/format.js +14 -0
  36. package/dist/services/graphql/index.d.ts +5 -53
  37. package/dist/services/graphql/index.js +5 -2720
  38. package/dist/services/graphql/resolvers/mutation.d.ts +4 -0
  39. package/dist/services/graphql/resolvers/mutation.js +74 -0
  40. package/dist/services/graphql/resolvers/query.d.ts +8 -0
  41. package/dist/services/graphql/resolvers/query.js +87 -0
  42. package/dist/services/graphql/resolvers/system-admin.d.ts +5 -0
  43. package/dist/services/graphql/resolvers/system-admin.js +236 -0
  44. package/dist/services/graphql/resolvers/system-global.d.ts +7 -0
  45. package/dist/services/graphql/resolvers/system-global.js +435 -0
  46. package/dist/services/graphql/resolvers/system.d.ts +11 -0
  47. package/dist/services/graphql/resolvers/system.js +554 -0
  48. package/dist/services/graphql/schema/get-types.d.ts +12 -0
  49. package/dist/services/graphql/schema/get-types.js +223 -0
  50. package/dist/services/graphql/schema/index.d.ts +32 -0
  51. package/dist/services/graphql/schema/index.js +190 -0
  52. package/dist/services/graphql/schema/parse-args.d.ts +9 -0
  53. package/dist/services/graphql/schema/parse-args.js +35 -0
  54. package/dist/services/graphql/schema/parse-query.d.ts +7 -0
  55. package/dist/services/graphql/schema/parse-query.js +98 -0
  56. package/dist/services/graphql/schema/read.d.ts +12 -0
  57. package/dist/services/graphql/schema/read.js +653 -0
  58. package/dist/services/graphql/schema/write.d.ts +9 -0
  59. package/dist/services/graphql/schema/write.js +142 -0
  60. package/dist/services/graphql/subscription.d.ts +1 -1
  61. package/dist/services/graphql/subscription.js +7 -6
  62. package/dist/services/graphql/utils/aggrgate-query.d.ts +6 -0
  63. package/dist/services/graphql/utils/aggrgate-query.js +32 -0
  64. package/dist/services/graphql/utils/replace-fragments.d.ts +6 -0
  65. package/dist/services/graphql/utils/replace-fragments.js +21 -0
  66. package/dist/services/graphql/utils/replace-funcs.d.ts +5 -0
  67. package/dist/services/graphql/utils/replace-funcs.js +21 -0
  68. package/dist/services/graphql/utils/sanitize-gql-schema.d.ts +1 -1
  69. package/dist/services/graphql/utils/sanitize-gql-schema.js +5 -5
  70. package/dist/services/items.js +0 -2
  71. package/dist/services/meta.js +25 -84
  72. package/dist/services/users.d.ts +4 -0
  73. package/dist/services/users.js +23 -1
  74. package/dist/utils/apply-query.d.ts +1 -1
  75. package/dist/utils/apply-query.js +58 -21
  76. package/dist/utils/freeze-schema.d.ts +3 -0
  77. package/dist/utils/freeze-schema.js +31 -0
  78. package/dist/utils/get-accountability-for-token.js +1 -0
  79. package/dist/utils/get-milliseconds.js +1 -1
  80. package/dist/utils/get-schema.js +10 -5
  81. package/dist/utils/permissions-cachable.d.ts +8 -0
  82. package/dist/utils/permissions-cachable.js +38 -0
  83. package/dist/utils/sanitize-schema.d.ts +1 -1
  84. package/dist/websocket/messages.d.ts +6 -6
  85. package/package.json +22 -19
@@ -0,0 +1,142 @@
1
+ import { GraphQLBoolean, GraphQLID, GraphQLList, GraphQLNonNull } from 'graphql';
2
+ import { ObjectTypeComposer, toInputObjectType } from 'graphql-compose';
3
+ import { GraphQLService } from '../index.js';
4
+ import { resolveMutation } from '../resolvers/mutation.js';
5
+ import { getTypes } from './get-types.js';
6
+ import { SYSTEM_DENY_LIST } from './index.js';
7
+ export function getWritableTypes(gql, schemaComposer, schema, inconsistentFields, ReadCollectionTypes) {
8
+ const { CollectionTypes: CreateCollectionTypes } = getTypes(schemaComposer, gql.scope, schema, inconsistentFields, 'create');
9
+ const { CollectionTypes: UpdateCollectionTypes } = getTypes(schemaComposer, gql.scope, schema, inconsistentFields, 'update');
10
+ const DeleteCollectionTypes = {};
11
+ for (const collection of Object.values(schema.create.collections)) {
12
+ if (Object.keys(collection.fields).length === 0)
13
+ continue;
14
+ if (SYSTEM_DENY_LIST.includes(collection.collection))
15
+ continue;
16
+ if (collection.collection in CreateCollectionTypes === false)
17
+ continue;
18
+ const collectionIsReadable = collection.collection in ReadCollectionTypes;
19
+ const creatableFields = CreateCollectionTypes[collection.collection]?.getFields() || {};
20
+ if (Object.keys(creatableFields).length > 0) {
21
+ const resolverDefinition = {
22
+ name: `create_${collection.collection}_items`,
23
+ type: collectionIsReadable
24
+ ? new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(ReadCollectionTypes[collection.collection].getType())))
25
+ : GraphQLBoolean,
26
+ resolve: async ({ args, info }) => await resolveMutation(gql, args, info),
27
+ };
28
+ if (collectionIsReadable) {
29
+ resolverDefinition.args = ReadCollectionTypes[collection.collection].getResolver(collection.collection).getArgs();
30
+ }
31
+ CreateCollectionTypes[collection.collection].addResolver(resolverDefinition);
32
+ CreateCollectionTypes[collection.collection].addResolver({
33
+ name: `create_${collection.collection}_item`,
34
+ type: collectionIsReadable ? ReadCollectionTypes[collection.collection] : GraphQLBoolean,
35
+ resolve: async ({ args, info }) => await resolveMutation(gql, args, info),
36
+ });
37
+ CreateCollectionTypes[collection.collection].getResolver(`create_${collection.collection}_items`).addArgs({
38
+ ...CreateCollectionTypes[collection.collection].getResolver(`create_${collection.collection}_items`).getArgs(),
39
+ data: [
40
+ toInputObjectType(CreateCollectionTypes[collection.collection]).setTypeName(`create_${collection.collection}_input`).NonNull,
41
+ ],
42
+ });
43
+ CreateCollectionTypes[collection.collection].getResolver(`create_${collection.collection}_item`).addArgs({
44
+ ...CreateCollectionTypes[collection.collection].getResolver(`create_${collection.collection}_item`).getArgs(),
45
+ data: toInputObjectType(CreateCollectionTypes[collection.collection]).setTypeName(`create_${collection.collection}_input`).NonNull,
46
+ });
47
+ }
48
+ }
49
+ for (const collection of Object.values(schema.update.collections)) {
50
+ if (Object.keys(collection.fields).length === 0)
51
+ continue;
52
+ if (SYSTEM_DENY_LIST.includes(collection.collection))
53
+ continue;
54
+ if (collection.collection in UpdateCollectionTypes === false)
55
+ continue;
56
+ const collectionIsReadable = collection.collection in ReadCollectionTypes;
57
+ const updatableFields = UpdateCollectionTypes[collection.collection]?.getFields() || {};
58
+ if (Object.keys(updatableFields).length > 0) {
59
+ if (collection.singleton) {
60
+ UpdateCollectionTypes[collection.collection].addResolver({
61
+ name: `update_${collection.collection}`,
62
+ type: collectionIsReadable ? ReadCollectionTypes[collection.collection] : GraphQLBoolean,
63
+ args: {
64
+ data: toInputObjectType(UpdateCollectionTypes[collection.collection]).setTypeName(`update_${collection.collection}_input`).NonNull,
65
+ },
66
+ resolve: async ({ args, info }) => await resolveMutation(gql, args, info),
67
+ });
68
+ }
69
+ else {
70
+ UpdateCollectionTypes[collection.collection].addResolver({
71
+ name: `update_${collection.collection}_batch`,
72
+ type: collectionIsReadable
73
+ ? new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(ReadCollectionTypes[collection.collection].getType())))
74
+ : GraphQLBoolean,
75
+ args: {
76
+ ...(collectionIsReadable
77
+ ? ReadCollectionTypes[collection.collection].getResolver(collection.collection).getArgs()
78
+ : {}),
79
+ data: [
80
+ toInputObjectType(UpdateCollectionTypes[collection.collection]).setTypeName(`update_${collection.collection}_input`).NonNull,
81
+ ],
82
+ },
83
+ resolve: async ({ args, info }) => await resolveMutation(gql, args, info),
84
+ });
85
+ UpdateCollectionTypes[collection.collection].addResolver({
86
+ name: `update_${collection.collection}_items`,
87
+ type: collectionIsReadable
88
+ ? new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(ReadCollectionTypes[collection.collection].getType())))
89
+ : GraphQLBoolean,
90
+ args: {
91
+ ...(collectionIsReadable
92
+ ? ReadCollectionTypes[collection.collection].getResolver(collection.collection).getArgs()
93
+ : {}),
94
+ ids: new GraphQLNonNull(new GraphQLList(GraphQLID)),
95
+ data: toInputObjectType(UpdateCollectionTypes[collection.collection]).setTypeName(`update_${collection.collection}_input`).NonNull,
96
+ },
97
+ resolve: async ({ args, info }) => await resolveMutation(gql, args, info),
98
+ });
99
+ UpdateCollectionTypes[collection.collection].addResolver({
100
+ name: `update_${collection.collection}_item`,
101
+ type: collectionIsReadable ? ReadCollectionTypes[collection.collection] : GraphQLBoolean,
102
+ args: {
103
+ id: new GraphQLNonNull(GraphQLID),
104
+ data: toInputObjectType(UpdateCollectionTypes[collection.collection]).setTypeName(`update_${collection.collection}_input`).NonNull,
105
+ },
106
+ resolve: async ({ args, info }) => await resolveMutation(gql, args, info),
107
+ });
108
+ }
109
+ }
110
+ }
111
+ DeleteCollectionTypes['many'] = schemaComposer.createObjectTC({
112
+ name: `delete_many`,
113
+ fields: {
114
+ ids: new GraphQLNonNull(new GraphQLList(GraphQLID)),
115
+ },
116
+ });
117
+ DeleteCollectionTypes['one'] = schemaComposer.createObjectTC({
118
+ name: `delete_one`,
119
+ fields: {
120
+ id: new GraphQLNonNull(GraphQLID),
121
+ },
122
+ });
123
+ for (const collection of Object.values(schema.delete.collections)) {
124
+ DeleteCollectionTypes['many'].addResolver({
125
+ name: `delete_${collection.collection}_items`,
126
+ type: DeleteCollectionTypes['many'],
127
+ args: {
128
+ ids: new GraphQLNonNull(new GraphQLList(GraphQLID)),
129
+ },
130
+ resolve: async ({ args, info }) => await resolveMutation(gql, args, info),
131
+ });
132
+ DeleteCollectionTypes['one'].addResolver({
133
+ name: `delete_${collection.collection}_item`,
134
+ type: DeleteCollectionTypes['one'],
135
+ args: {
136
+ id: new GraphQLNonNull(GraphQLID),
137
+ },
138
+ resolve: async ({ args, info }) => await resolveMutation(gql, args, info),
139
+ });
140
+ }
141
+ return { CreateCollectionTypes, UpdateCollectionTypes, DeleteCollectionTypes };
142
+ }
@@ -1,7 +1,7 @@
1
1
  import type { GraphQLService } from './index.js';
2
2
  import type { GraphQLResolveInfo } from 'graphql';
3
3
  export declare function bindPubSub(): void;
4
- export declare function createSubscriptionGenerator(self: GraphQLService, event: string): (_x: unknown, _y: unknown, _z: unknown, request: GraphQLResolveInfo) => AsyncGenerator<{
4
+ export declare function createSubscriptionGenerator(gql: GraphQLService, event: string): (_x: unknown, _y: unknown, _z: unknown, request: GraphQLResolveInfo) => AsyncGenerator<{
5
5
  [x: string]: {
6
6
  key: string | number;
7
7
  data: null;
@@ -2,6 +2,7 @@ import { EventEmitter, on } from 'events';
2
2
  import { useBus } from '../../bus/index.js';
3
3
  import { getSchema } from '../../utils/get-schema.js';
4
4
  import { getPayload } from '../../websocket/utils/items.js';
5
+ import { getQuery } from './schema/parse-query.js';
5
6
  const messages = createPubSub(new EventEmitter());
6
7
  export function bindPubSub() {
7
8
  const messenger = useBus();
@@ -9,9 +10,9 @@ export function bindPubSub() {
9
10
  messages.publish(`${message['collection']}_mutated`, message);
10
11
  });
11
12
  }
12
- export function createSubscriptionGenerator(self, event) {
13
+ export function createSubscriptionGenerator(gql, event) {
13
14
  return async function* (_x, _y, _z, request) {
14
- const fields = parseFields(self, request);
15
+ const fields = parseFields(gql, request);
15
16
  const args = parseArguments(request);
16
17
  for await (const payload of messages.subscribe(event)) {
17
18
  const eventData = payload;
@@ -33,7 +34,7 @@ export function createSubscriptionGenerator(self, event) {
33
34
  if (eventData['action'] === 'create') {
34
35
  try {
35
36
  subscription.item = eventData['key'];
36
- const result = await getPayload(subscription, self.accountability, schema, eventData);
37
+ const result = await getPayload(subscription, gql.accountability, schema, eventData);
37
38
  yield {
38
39
  [event]: {
39
40
  key: eventData['key'],
@@ -50,7 +51,7 @@ export function createSubscriptionGenerator(self, event) {
50
51
  for (const key of eventData['keys']) {
51
52
  try {
52
53
  subscription.item = key;
53
- const result = await getPayload(subscription, self.accountability, schema, eventData);
54
+ const result = await getPayload(subscription, gql.accountability, schema, eventData);
54
55
  yield {
55
56
  [event]: {
56
57
  key,
@@ -78,7 +79,7 @@ function createPubSub(emitter) {
78
79
  },
79
80
  };
80
81
  }
81
- function parseFields(service, request) {
82
+ function parseFields(gql, request) {
82
83
  const selections = request.fieldNodes[0]?.selectionSet?.selections ?? [];
83
84
  const dataSelections = selections.reduce((result, selection) => {
84
85
  if (selection.kind === 'Field' &&
@@ -88,7 +89,7 @@ function parseFields(service, request) {
88
89
  }
89
90
  return result;
90
91
  }, []);
91
- const { fields } = service.getQuery({}, dataSelections, request.variableValues);
92
+ const { fields } = getQuery({}, dataSelections, request.variableValues, gql.accountability);
92
93
  return fields ?? [];
93
94
  }
94
95
  function parseArguments(request) {
@@ -0,0 +1,6 @@
1
+ import type { Accountability, Query } from '@directus/types';
2
+ import type { SelectionNode } from 'graphql';
3
+ /**
4
+ * Resolve the aggregation query based on the requested aggregated fields
5
+ */
6
+ export declare function getAggregateQuery(rawQuery: Query, selections: readonly SelectionNode[], accountability?: Accountability | null): Query;
@@ -0,0 +1,32 @@
1
+ import { replaceFuncs } from './replace-funcs.js';
2
+ import { sanitizeQuery } from '../../../utils/sanitize-query.js';
3
+ import { validateQuery } from '../../../utils/validate-query.js';
4
+ /**
5
+ * Resolve the aggregation query based on the requested aggregated fields
6
+ */
7
+ export function getAggregateQuery(rawQuery, selections, accountability) {
8
+ const query = sanitizeQuery(rawQuery, accountability);
9
+ query.aggregate = {};
10
+ for (let aggregationGroup of selections) {
11
+ if ((aggregationGroup.kind === 'Field') !== true)
12
+ continue;
13
+ aggregationGroup = aggregationGroup;
14
+ // filter out graphql pointers, like __typename
15
+ if (aggregationGroup.name.value.startsWith('__'))
16
+ continue;
17
+ const aggregateProperty = aggregationGroup.name.value;
18
+ query.aggregate[aggregateProperty] =
19
+ aggregationGroup.selectionSet?.selections
20
+ // filter out graphql pointers, like __typename
21
+ .filter((selectionNode) => !selectionNode?.name.value.startsWith('__'))
22
+ .map((selectionNode) => {
23
+ selectionNode = selectionNode;
24
+ return selectionNode.name.value;
25
+ }) ?? [];
26
+ }
27
+ if (query.filter) {
28
+ query.filter = replaceFuncs(query.filter);
29
+ }
30
+ validateQuery(query);
31
+ return query;
32
+ }
@@ -0,0 +1,6 @@
1
+ import type { FragmentDefinitionNode, SelectionNode } from 'graphql';
2
+ /**
3
+ * Replace all fragments in a selectionset for the actual selection set as defined in the fragment
4
+ * Effectively merges the selections with the fragments used in those selections
5
+ */
6
+ export declare function replaceFragmentsInSelections(selections: readonly SelectionNode[] | undefined, fragments: Record<string, FragmentDefinitionNode>): readonly SelectionNode[] | null;
@@ -0,0 +1,21 @@
1
+ import { flatten } from 'lodash-es';
2
+ /**
3
+ * Replace all fragments in a selectionset for the actual selection set as defined in the fragment
4
+ * Effectively merges the selections with the fragments used in those selections
5
+ */
6
+ export function replaceFragmentsInSelections(selections, fragments) {
7
+ if (!selections)
8
+ return null;
9
+ const result = flatten(selections.map((selection) => {
10
+ // Fragments can contains fragments themselves. This allows for nested fragments
11
+ if (selection.kind === 'FragmentSpread') {
12
+ return replaceFragmentsInSelections(fragments[selection.name.value].selectionSet.selections, fragments);
13
+ }
14
+ // Nested relational fields can also contain fragments
15
+ if ((selection.kind === 'Field' || selection.kind === 'InlineFragment') && selection.selectionSet) {
16
+ selection.selectionSet.selections = replaceFragmentsInSelections(selection.selectionSet.selections, fragments);
17
+ }
18
+ return selection;
19
+ })).filter((s) => s);
20
+ return result;
21
+ }
@@ -0,0 +1,5 @@
1
+ import type { Filter } from '@directus/types';
2
+ /**
3
+ * Replace functions from GraphQL format to Directus-Filter format
4
+ */
5
+ export declare function replaceFuncs(filter: Filter): Filter;
@@ -0,0 +1,21 @@
1
+ import { FUNCTIONS } from '@directus/constants';
2
+ import { transform } from 'lodash-es';
3
+ /**
4
+ * Replace functions from GraphQL format to Directus-Filter format
5
+ */
6
+ export function replaceFuncs(filter) {
7
+ return replaceFuncDeep(filter);
8
+ function replaceFuncDeep(filter) {
9
+ return transform(filter, (result, value, key) => {
10
+ const isFunctionKey = typeof key === 'string' && key.endsWith('_func') && FUNCTIONS.includes(Object.keys(value)[0]);
11
+ if (isFunctionKey) {
12
+ const functionName = Object.keys(value)[0];
13
+ const fieldName = key.slice(0, -5);
14
+ result[`${functionName}(${fieldName})`] = Object.values(value)[0];
15
+ }
16
+ else {
17
+ result[key] = value?.constructor === Object || value?.constructor === Array ? replaceFuncDeep(value) : value;
18
+ }
19
+ });
20
+ }
21
+ }
@@ -5,4 +5,4 @@ import type { SchemaOverview } from '@directus/types';
5
5
  * @param schema
6
6
  * @returns sanitized schema
7
7
  */
8
- export declare function sanitizeGraphqlSchema(schema: SchemaOverview): SchemaOverview;
8
+ export declare function sanitizeGraphqlSchema(schema: Readonly<SchemaOverview>): SchemaOverview;
@@ -36,7 +36,7 @@ const GRAPHQL_RESERVED_NAMES = [
36
36
  */
37
37
  export function sanitizeGraphqlSchema(schema) {
38
38
  const logger = useLogger();
39
- const collections = Object.entries(schema.collections).filter(([collectionName, _data]) => {
39
+ const collectionEntries = Object.entries(schema.collections).filter(([collectionName, _data]) => {
40
40
  // double underscore __ is reserved for GraphQL introspection
41
41
  if (collectionName.startsWith('__') || !collectionName.match(GRAPHQL_NAME_REGEX)) {
42
42
  logger.warn(`GraphQL skipping collection "${collectionName}" because it is not a valid name matching /^[_A-Za-z][_0-9A-Za-z]*$/ or starts with __`);
@@ -48,14 +48,14 @@ export function sanitizeGraphqlSchema(schema) {
48
48
  }
49
49
  return true;
50
50
  });
51
- schema.collections = Object.fromEntries(collections);
52
- const collectionExists = (collection) => Boolean(schema.collections[collection]);
51
+ const collections = Object.fromEntries(collectionEntries);
52
+ const collectionExists = (collection) => Boolean(collections[collection]);
53
53
  const skipRelation = (relation) => {
54
54
  const relationName = relation.schema?.constraint_name ?? `${relation.collection}.${relation.field}`;
55
55
  logger.warn(`GraphQL skipping relation "${relationName}" because it links to a non-existent or invalid collection.`);
56
56
  return false;
57
57
  };
58
- schema.relations = schema.relations.filter((relation) => {
58
+ const relations = schema.relations.filter((relation) => {
59
59
  if (relation.collection && !collectionExists(relation.collection)) {
60
60
  return skipRelation(relation);
61
61
  }
@@ -76,5 +76,5 @@ export function sanitizeGraphqlSchema(schema) {
76
76
  }
77
77
  return true;
78
78
  });
79
- return schema;
79
+ return { collections, relations };
80
80
  }
@@ -442,8 +442,6 @@ export class ItemsService {
442
442
  */
443
443
  async updateByQuery(query, data, opts) {
444
444
  const keys = await this.getKeysByQuery(query);
445
- const primaryKeyField = this.schema.collections[this.collection].primary;
446
- validateKeys(this.schema, this.collection, primaryKeyField, keys);
447
445
  return keys.length ? await this.updateMany(keys, data, opts) : [];
448
446
  }
449
447
  /**
@@ -1,9 +1,8 @@
1
+ import { isArray } from 'lodash-es';
2
+ import { getAstFromQuery } from '../database/get-ast-from-query/get-ast-from-query.js';
1
3
  import getDatabase from '../database/index.js';
2
- import { fetchPermissions } from '../permissions/lib/fetch-permissions.js';
3
- import { fetchPolicies } from '../permissions/lib/fetch-policies.js';
4
- import { dedupeAccess } from '../permissions/modules/process-ast/utils/dedupe-access.js';
5
- import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
6
- import { applyFilter, applySearch } from '../utils/apply-query.js';
4
+ import { runAst } from '../database/run-ast/run-ast.js';
5
+ import { processAst } from '../permissions/modules/process-ast/process-ast.js';
7
6
  export class MetaService {
8
7
  knex;
9
8
  accountability;
@@ -31,86 +30,28 @@ export class MetaService {
31
30
  }, {});
32
31
  }
33
32
  async totalCount(collection) {
34
- const dbQuery = this.knex(collection);
35
- let hasJoins = false;
36
- if (this.accountability && this.accountability.admin === false) {
37
- const context = { knex: this.knex, schema: this.schema };
38
- await validateAccess({
39
- accountability: this.accountability,
40
- action: 'read',
41
- collection,
42
- }, context);
43
- const policies = await fetchPolicies(this.accountability, context);
44
- const permissions = await fetchPermissions({
45
- action: 'read',
46
- policies,
47
- accountability: this.accountability,
48
- }, context);
49
- const collectionPermissions = permissions.filter((permission) => permission.collection === collection);
50
- const rules = dedupeAccess(collectionPermissions);
51
- const cases = rules.map(({ rule }) => rule);
52
- const filter = {
53
- _or: cases,
54
- };
55
- const result = applyFilter(this.knex, this.schema, dbQuery, filter, collection, {}, cases, permissions);
56
- hasJoins = result.hasJoins;
57
- }
58
- if (hasJoins) {
59
- const primaryKeyName = this.schema.collections[collection].primary;
60
- dbQuery.countDistinct({ count: [`${collection}.${primaryKeyName}`] });
61
- }
62
- else {
63
- dbQuery.count('*', { as: 'count' });
64
- }
65
- const result = await dbQuery.first();
66
- return Number(result?.count ?? 0);
33
+ return this.filterCount(collection, {});
67
34
  }
68
35
  async filterCount(collection, query) {
69
- const dbQuery = this.knex(collection);
70
- let filter = query.filter || {};
71
- let hasJoins = false;
72
- let cases = [];
73
- let permissions = [];
74
- if (this.accountability && this.accountability.admin === false) {
75
- const context = { knex: this.knex, schema: this.schema };
76
- await validateAccess({
77
- accountability: this.accountability,
78
- action: 'read',
79
- collection,
80
- }, context);
81
- const policies = await fetchPolicies(this.accountability, context);
82
- permissions = await fetchPermissions({
83
- action: 'read',
84
- policies,
85
- accountability: this.accountability,
86
- }, context);
87
- const collectionPermissions = permissions.filter((permission) => permission.collection === collection);
88
- const rules = dedupeAccess(collectionPermissions);
89
- cases = rules.map(({ rule }) => rule);
90
- const permissionsFilter = {
91
- _or: cases,
92
- };
93
- if (Object.keys(filter).length > 0) {
94
- filter = { _and: [permissionsFilter, filter] };
95
- }
96
- else {
97
- filter = permissionsFilter;
98
- }
99
- }
100
- if (Object.keys(filter).length > 0) {
101
- ({ hasJoins } = applyFilter(this.knex, this.schema, dbQuery, filter, collection, {}, cases, permissions));
102
- }
103
- if (query.search) {
104
- applySearch(this.knex, this.schema, dbQuery, query.search, collection);
105
- }
106
- if (hasJoins) {
107
- const primaryKeyName = this.schema.collections[collection].primary;
108
- dbQuery.countDistinct({ count: [`${collection}.${primaryKeyName}`] });
109
- }
110
- else {
111
- dbQuery.count('*', { as: 'count' });
112
- }
113
- const result = await dbQuery.first();
114
- return Number(result?.count ?? 0);
36
+ const aggregateQuery = {
37
+ aggregate: {
38
+ count: ['*'],
39
+ },
40
+ search: query.search ?? null,
41
+ filter: query.filter ?? null,
42
+ };
43
+ let ast = await getAstFromQuery({
44
+ collection,
45
+ query: aggregateQuery,
46
+ accountability: this.accountability,
47
+ }, {
48
+ schema: this.schema,
49
+ knex: this.knex,
50
+ });
51
+ ast = await processAst({ ast, action: 'read', accountability: this.accountability }, { knex: this.knex, schema: this.schema });
52
+ const records = await runAst(ast, this.schema, this.accountability, {
53
+ knex: this.knex,
54
+ });
55
+ return Number((isArray(records) ? records[0]?.['count'] : records?.['count']) ?? 0);
115
56
  }
116
57
  }
@@ -13,6 +13,10 @@ export declare class UsersService extends ItemsService {
13
13
  * directus_settings.auth_password_policy
14
14
  */
15
15
  private checkPasswordPolicy;
16
+ /**
17
+ * Clear users' sessions to log them out
18
+ */
19
+ private clearUserSessions;
16
20
  /**
17
21
  * Get basic information of user identified by email
18
22
  */
@@ -87,6 +87,21 @@ export class UsersService extends ItemsService {
87
87
  }
88
88
  }
89
89
  }
90
+ /**
91
+ * Clear users' sessions to log them out
92
+ */
93
+ async clearUserSessions(userKeys, excludeSession) {
94
+ if (excludeSession) {
95
+ await this.knex
96
+ .from('directus_sessions')
97
+ .whereIn('user', userKeys)
98
+ .andWhereNot('token', '=', excludeSession)
99
+ .delete();
100
+ }
101
+ else {
102
+ await this.knex.from('directus_sessions').whereIn('user', userKeys).delete();
103
+ }
104
+ }
90
105
  /**
91
106
  * Get basic information of user identified by email
92
107
  */
@@ -238,6 +253,12 @@ export class UsersService extends ItemsService {
238
253
  opts.onRequireUserIntegrityCheck?.(opts.userIntegrityCheckFlags);
239
254
  }
240
255
  const result = await super.updateMany(keys, data, opts);
256
+ if (data['status'] !== undefined && data['status'] !== 'active') {
257
+ await this.clearUserSessions(keys);
258
+ }
259
+ else if (data['password'] !== undefined || data['email'] !== undefined) {
260
+ await this.clearUserSessions(keys, this.accountability?.session);
261
+ }
241
262
  // Only clear the caches if the role has been updated
242
263
  if ('role' in data) {
243
264
  await this.clearCaches(opts);
@@ -264,6 +285,7 @@ export class UsersService extends ItemsService {
264
285
  await this.knex('directus_notifications').update({ sender: null }).whereIn('sender', keys);
265
286
  await this.knex('directus_versions').update({ user_updated: null }).whereIn('user_updated', keys);
266
287
  await super.deleteMany(keys, opts);
288
+ await this.clearUserSessions(keys);
267
289
  return keys;
268
290
  }
269
291
  async inviteUser(email, role, url, subject) {
@@ -462,7 +484,7 @@ export class UsersService extends ItemsService {
462
484
  await stall(STALL_TIME, timeStart);
463
485
  }
464
486
  async resetPassword(token, password) {
465
- const { email, scope, hash } = jwt.verify(token, getSecret(), { issuer: 'directus' });
487
+ const { email, scope, hash } = verifyJWT(token, getSecret());
466
488
  if (scope !== 'password-reset' || !hash)
467
489
  throw new ForbiddenError();
468
490
  const opts = {};
@@ -39,7 +39,7 @@ export declare function applyFilter(knex: Knex, schema: SchemaOverview, rootQuer
39
39
  hasJoins: boolean;
40
40
  hasMultiRelationalFilter: boolean;
41
41
  };
42
- export declare function applySearch(knex: Knex, schema: SchemaOverview, dbQuery: Knex.QueryBuilder, searchQuery: string, collection: string): void;
42
+ export declare function applySearch(knex: Knex, schema: SchemaOverview, dbQuery: Knex.QueryBuilder, searchQuery: string, collection: string, aliasMap: AliasMap, permissions: Permission[]): void;
43
43
  export declare function applyAggregate(schema: SchemaOverview, dbQuery: Knex.QueryBuilder, aggregate: Aggregate, collection: string, hasJoins: boolean): void;
44
44
  export declare function joinFilterWithCases(filter: Filter | null | undefined, cases: Filter[]): Filter | null;
45
45
  export {};