@directus/api 25.0.0 → 26.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 (123) hide show
  1. package/dist/app.js +3 -3
  2. package/dist/auth/drivers/oauth2.d.ts +2 -0
  3. package/dist/auth/drivers/oauth2.js +40 -2
  4. package/dist/auth/drivers/openid.js +8 -1
  5. package/dist/controllers/access.js +2 -2
  6. package/dist/controllers/comments.js +2 -2
  7. package/dist/controllers/dashboards.js +2 -2
  8. package/dist/controllers/files.js +2 -2
  9. package/dist/controllers/flows.js +2 -2
  10. package/dist/controllers/folders.js +2 -2
  11. package/dist/controllers/items.js +2 -2
  12. package/dist/controllers/notifications.js +2 -2
  13. package/dist/controllers/operations.js +2 -2
  14. package/dist/controllers/panels.js +2 -2
  15. package/dist/controllers/permissions.js +2 -2
  16. package/dist/controllers/policies.js +2 -2
  17. package/dist/controllers/presets.js +2 -2
  18. package/dist/controllers/roles.js +2 -2
  19. package/dist/controllers/shares.js +2 -2
  20. package/dist/controllers/translations.js +2 -2
  21. package/dist/controllers/users.js +2 -2
  22. package/dist/controllers/utils.js +8 -3
  23. package/dist/controllers/versions.js +2 -2
  24. package/dist/controllers/webhooks.js +1 -1
  25. package/dist/database/helpers/capabilities/dialects/default.d.ts +3 -0
  26. package/dist/database/helpers/capabilities/dialects/default.js +3 -0
  27. package/dist/database/helpers/capabilities/dialects/mysql.d.ts +4 -0
  28. package/dist/database/helpers/capabilities/dialects/mysql.js +9 -0
  29. package/dist/database/helpers/capabilities/dialects/postgres.d.ts +5 -0
  30. package/dist/database/helpers/capabilities/dialects/postgres.js +14 -0
  31. package/dist/database/helpers/capabilities/index.d.ts +7 -0
  32. package/dist/database/helpers/capabilities/index.js +7 -0
  33. package/dist/database/helpers/capabilities/types.d.ts +11 -0
  34. package/dist/database/helpers/capabilities/types.js +15 -0
  35. package/dist/database/helpers/index.d.ts +2 -0
  36. package/dist/database/helpers/index.js +2 -0
  37. package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +1 -2
  38. package/dist/database/helpers/schema/dialects/cockroachdb.js +0 -4
  39. package/dist/database/helpers/schema/dialects/postgres.d.ts +1 -2
  40. package/dist/database/helpers/schema/dialects/postgres.js +0 -4
  41. package/dist/database/index.js +1 -1
  42. package/dist/database/migrations/20250224A-visual-editor.d.ts +3 -0
  43. package/dist/database/migrations/20250224A-visual-editor.js +35 -0
  44. package/dist/database/run-ast/lib/get-db-query.js +16 -4
  45. package/dist/logger/index.js +3 -3
  46. package/dist/middleware/sanitize-query.js +17 -7
  47. package/dist/middleware/validate-batch.js +1 -1
  48. package/dist/operations/item-delete/index.js +1 -1
  49. package/dist/operations/item-read/index.js +1 -1
  50. package/dist/operations/item-update/index.js +1 -1
  51. package/dist/permissions/lib/fetch-permissions.js +6 -4
  52. package/dist/permissions/modules/process-ast/utils/context-has-dynamic-variables.d.ts +2 -0
  53. package/dist/permissions/modules/process-ast/utils/context-has-dynamic-variables.js +3 -0
  54. package/dist/permissions/modules/process-payload/process-payload.d.ts +1 -0
  55. package/dist/permissions/modules/process-payload/process-payload.js +13 -4
  56. package/dist/permissions/types.d.ts +2 -1
  57. package/dist/permissions/utils/extract-required-dynamic-variable-context.d.ts +3 -2
  58. package/dist/permissions/utils/extract-required-dynamic-variable-context.js +24 -5
  59. package/dist/permissions/utils/fetch-dynamic-variable-data.d.ts +9 -0
  60. package/dist/permissions/utils/{fetch-dynamic-variable-context.js → fetch-dynamic-variable-data.js} +11 -12
  61. package/dist/rate-limiter.js +1 -1
  62. package/dist/services/assets.js +12 -2
  63. package/dist/services/authentication.js +2 -2
  64. package/dist/services/collections.js +39 -3
  65. package/dist/services/fields/build-collection-and-field-relations.d.ts +21 -0
  66. package/dist/services/fields/build-collection-and-field-relations.js +55 -0
  67. package/dist/services/fields/get-collection-meta-updates.d.ts +11 -0
  68. package/dist/services/fields/get-collection-meta-updates.js +72 -0
  69. package/dist/services/fields/get-collection-relation-list.d.ts +5 -0
  70. package/dist/services/fields/get-collection-relation-list.js +28 -0
  71. package/dist/services/fields.js +17 -12
  72. package/dist/services/graphql/resolvers/get-collection-type.d.ts +3 -0
  73. package/dist/services/graphql/resolvers/get-collection-type.js +34 -0
  74. package/dist/services/graphql/resolvers/get-field-type.d.ts +3 -0
  75. package/dist/services/graphql/resolvers/get-field-type.js +51 -0
  76. package/dist/services/graphql/resolvers/get-relation-type.d.ts +3 -0
  77. package/dist/services/graphql/resolvers/get-relation-type.js +39 -0
  78. package/dist/services/graphql/resolvers/mutation.js +1 -1
  79. package/dist/services/graphql/resolvers/query.js +4 -4
  80. package/dist/services/graphql/resolvers/system-admin.d.ts +2 -2
  81. package/dist/services/graphql/resolvers/system-admin.js +207 -199
  82. package/dist/services/graphql/resolvers/system.d.ts +1 -7
  83. package/dist/services/graphql/resolvers/system.js +12 -113
  84. package/dist/services/graphql/schema/index.js +1 -1
  85. package/dist/services/graphql/schema/parse-query.d.ts +2 -2
  86. package/dist/services/graphql/schema/parse-query.js +6 -6
  87. package/dist/services/graphql/schema/read.d.ts +2 -2
  88. package/dist/services/graphql/schema/read.js +86 -2
  89. package/dist/services/graphql/schema-cache.d.ts +2 -2
  90. package/dist/services/graphql/schema-cache.js +1 -3
  91. package/dist/services/graphql/subscription.d.ts +3 -3
  92. package/dist/services/graphql/subscription.js +3 -3
  93. package/dist/services/graphql/utils/{aggrgate-query.d.ts → aggregate-query.d.ts} +2 -2
  94. package/dist/services/graphql/utils/{aggrgate-query.js → aggregate-query.js} +3 -3
  95. package/dist/services/items.d.ts +1 -0
  96. package/dist/services/items.js +30 -16
  97. package/dist/services/meta.js +4 -2
  98. package/dist/services/payload.d.ts +1 -0
  99. package/dist/services/payload.js +32 -17
  100. package/dist/services/shares.js +1 -1
  101. package/dist/services/specifications.js +10 -5
  102. package/dist/services/tus/lockers.d.ts +1 -1
  103. package/dist/services/tus/lockers.js +6 -5
  104. package/dist/services/tus/server.js +24 -0
  105. package/dist/services/users.js +1 -0
  106. package/dist/types/services.d.ts +2 -0
  107. package/dist/utils/apply-query.d.ts +1 -0
  108. package/dist/utils/apply-query.js +42 -31
  109. package/dist/utils/generate-hash.js +1 -1
  110. package/dist/utils/get-config-from-env.d.ts +6 -1
  111. package/dist/utils/get-config-from-env.js +16 -11
  112. package/dist/utils/get-graphql-type.js +3 -1
  113. package/dist/utils/is-login-redirect-allowed.js +2 -0
  114. package/dist/utils/redact-object.js +5 -1
  115. package/dist/utils/sanitize-query.d.ts +5 -2
  116. package/dist/utils/sanitize-query.js +34 -9
  117. package/dist/websocket/controllers/base.d.ts +2 -2
  118. package/dist/websocket/handlers/items.js +4 -4
  119. package/dist/websocket/handlers/subscribe.js +2 -2
  120. package/dist/websocket/messages.d.ts +7 -7
  121. package/dist/websocket/messages.js +1 -1
  122. package/package.json +58 -58
  123. package/dist/permissions/utils/fetch-dynamic-variable-context.d.ts +0 -8
@@ -1,7 +1,7 @@
1
- import type { Accountability, Query } from '@directus/types';
1
+ import type { Accountability, Query, SchemaOverview } from '@directus/types';
2
2
  import type { GraphQLResolveInfo, SelectionNode } from 'graphql';
3
3
  /**
4
4
  * Get a Directus Query object from the parsed arguments (rawQuery) and GraphQL AST selectionSet. Converts SelectionSet into
5
5
  * Directus' `fields` query for use in the resolver. Also applies variables where appropriate.
6
6
  */
7
- export declare function getQuery(rawQuery: Query, selections: readonly SelectionNode[], variableValues: GraphQLResolveInfo['variableValues'], accountability?: Accountability | null): Query;
7
+ export declare function getQuery(rawQuery: Query, selections: readonly SelectionNode[], variableValues: GraphQLResolveInfo['variableValues'], schema: SchemaOverview, accountability?: Accountability | null): Promise<Query>;
@@ -7,8 +7,8 @@ import { parseArgs } from './parse-args.js';
7
7
  * Get a Directus Query object from the parsed arguments (rawQuery) and GraphQL AST selectionSet. Converts SelectionSet into
8
8
  * Directus' `fields` query for use in the resolver. Also applies variables where appropriate.
9
9
  */
10
- export function getQuery(rawQuery, selections, variableValues, accountability) {
11
- const query = sanitizeQuery(rawQuery, accountability);
10
+ export async function getQuery(rawQuery, selections, variableValues, schema, accountability) {
11
+ const query = await sanitizeQuery(rawQuery, schema, accountability);
12
12
  const parseAliases = (selections) => {
13
13
  const aliases = {};
14
14
  for (const selection of selections) {
@@ -20,7 +20,7 @@ export function getQuery(rawQuery, selections, variableValues, accountability) {
20
20
  }
21
21
  return aliases;
22
22
  };
23
- const parseFields = (selections, parent) => {
23
+ const parseFields = async (selections, parent) => {
24
24
  const fields = [];
25
25
  for (let selection of selections) {
26
26
  if ((selection.kind === 'Field' || selection.kind === 'InlineFragment') !== true)
@@ -70,7 +70,7 @@ export function getQuery(rawQuery, selections, variableValues, accountability) {
70
70
  }
71
71
  }
72
72
  else {
73
- children = parseFields(selection.selectionSet.selections, currentAlias ?? current);
73
+ children = await parseFields(selection.selectionSet.selections, currentAlias ?? current);
74
74
  }
75
75
  fields.push(...children);
76
76
  }
@@ -82,14 +82,14 @@ export function getQuery(rawQuery, selections, variableValues, accountability) {
82
82
  if (!query.deep)
83
83
  query.deep = {};
84
84
  const args = parseArgs(selection.arguments, variableValues);
85
- set(query.deep, currentAlias ?? current, merge({}, get(query.deep, currentAlias ?? current), mapKeys(sanitizeQuery(args, accountability), (_value, key) => `_${key}`)));
85
+ set(query.deep, currentAlias ?? current, merge({}, get(query.deep, currentAlias ?? current), mapKeys(await sanitizeQuery(args, schema, accountability), (_value, key) => `_${key}`)));
86
86
  }
87
87
  }
88
88
  }
89
89
  return uniq(fields);
90
90
  };
91
91
  query.alias = parseAliases(selections);
92
- query.fields = parseFields(selections);
92
+ query.fields = await parseFields(selections);
93
93
  if (query.filter)
94
94
  query.filter = replaceFuncs(query.filter);
95
95
  query.deep = replaceFuncs(query.deep);
@@ -5,8 +5,8 @@ import { type InconsistentFields, type Schema } from './index.js';
5
5
  /**
6
6
  * Create readable types and attach resolvers for each. Also prepares full filter argument structures
7
7
  */
8
- export declare function getReadableTypes(gql: GraphQLService, schemaComposer: SchemaComposer, schema: Schema, inconsistentFields: InconsistentFields): {
8
+ export declare function getReadableTypes(gql: GraphQLService, schemaComposer: SchemaComposer, schema: Schema, inconsistentFields: InconsistentFields): Promise<{
9
9
  ReadCollectionTypes: Record<string, ObjectTypeComposer<any, any>>;
10
10
  VersionCollectionTypes: Record<string, ObjectTypeComposer<any, any>>;
11
11
  ReadableCollectionFilterTypes: Record<string, InputTypeComposer<any>>;
12
- };
12
+ }>;
@@ -14,12 +14,75 @@ import { getTypes } from './get-types.js';
14
14
  /**
15
15
  * Create readable types and attach resolvers for each. Also prepares full filter argument structures
16
16
  */
17
- export function getReadableTypes(gql, schemaComposer, schema, inconsistentFields) {
17
+ export async function getReadableTypes(gql, schemaComposer, schema, inconsistentFields) {
18
18
  const { CollectionTypes: ReadCollectionTypes, VersionTypes: VersionCollectionTypes } = getTypes(schemaComposer, gql.scope, schema, inconsistentFields, 'read');
19
19
  const ReadableCollectionFilterTypes = {};
20
+ const ReadableCollectionQuantifierFilterTypes = {};
20
21
  const AggregatedFunctions = {};
21
22
  const AggregatedFields = {};
22
23
  const AggregateMethods = {};
24
+ const IDFilterOperators = schemaComposer.createInputTC({
25
+ name: 'id_filter_operators',
26
+ fields: {
27
+ _eq: {
28
+ type: GraphQLID,
29
+ },
30
+ _neq: {
31
+ type: GraphQLID,
32
+ },
33
+ _contains: {
34
+ type: GraphQLID,
35
+ },
36
+ _icontains: {
37
+ type: GraphQLID,
38
+ },
39
+ _ncontains: {
40
+ type: GraphQLID,
41
+ },
42
+ _starts_with: {
43
+ type: GraphQLID,
44
+ },
45
+ _nstarts_with: {
46
+ type: GraphQLID,
47
+ },
48
+ _istarts_with: {
49
+ type: GraphQLID,
50
+ },
51
+ _nistarts_with: {
52
+ type: GraphQLID,
53
+ },
54
+ _ends_with: {
55
+ type: GraphQLID,
56
+ },
57
+ _nends_with: {
58
+ type: GraphQLID,
59
+ },
60
+ _iends_with: {
61
+ type: GraphQLID,
62
+ },
63
+ _niends_with: {
64
+ type: GraphQLID,
65
+ },
66
+ _in: {
67
+ type: new GraphQLList(GraphQLID),
68
+ },
69
+ _nin: {
70
+ type: new GraphQLList(GraphQLID),
71
+ },
72
+ _null: {
73
+ type: GraphQLBoolean,
74
+ },
75
+ _nnull: {
76
+ type: GraphQLBoolean,
77
+ },
78
+ _empty: {
79
+ type: GraphQLBoolean,
80
+ },
81
+ _nempty: {
82
+ type: GraphQLBoolean,
83
+ },
84
+ },
85
+ });
23
86
  const StringFilterOperators = schemaComposer.createInputTC({
24
87
  name: 'string_filter_operators',
25
88
  fields: {
@@ -356,6 +419,9 @@ export function getReadableTypes(gql, schemaComposer, schema, inconsistentFields
356
419
  case GraphQLHash:
357
420
  filterOperatorType = HashFilterOperators;
358
421
  break;
422
+ case GraphQLID:
423
+ filterOperatorType = IDFilterOperators;
424
+ break;
359
425
  default:
360
426
  filterOperatorType = StringFilterOperators;
361
427
  }
@@ -591,10 +657,21 @@ export function getReadableTypes(gql, schemaComposer, schema, inconsistentFields
591
657
  });
592
658
  }
593
659
  }
660
+ for (const collection in ReadableCollectionFilterTypes) {
661
+ const quantifier_collection = ReadableCollectionFilterTypes[collection]?.clone(`${collection}_quantifier_filter`);
662
+ quantifier_collection?.addFields({
663
+ _some: ReadableCollectionFilterTypes[collection],
664
+ _none: ReadableCollectionFilterTypes[collection],
665
+ });
666
+ ReadableCollectionQuantifierFilterTypes[collection] = quantifier_collection;
667
+ }
594
668
  for (const relation of schema.read.relations) {
595
669
  if (relation.related_collection) {
596
670
  if (SYSTEM_DENY_LIST.includes(relation.related_collection))
597
671
  continue;
672
+ ReadableCollectionQuantifierFilterTypes[relation.collection]?.addFields({
673
+ [relation.field]: ReadableCollectionFilterTypes[relation.related_collection],
674
+ });
598
675
  ReadableCollectionFilterTypes[relation.collection]?.addFields({
599
676
  [relation.field]: ReadableCollectionFilterTypes[relation.related_collection],
600
677
  });
@@ -617,8 +694,11 @@ export function getReadableTypes(gql, schemaComposer, schema, inconsistentFields
617
694
  },
618
695
  });
619
696
  if (relation.meta?.one_field) {
697
+ ReadableCollectionQuantifierFilterTypes[relation.related_collection]?.addFields({
698
+ [relation.meta.one_field]: ReadableCollectionQuantifierFilterTypes[relation.collection],
699
+ });
620
700
  ReadableCollectionFilterTypes[relation.related_collection]?.addFields({
621
- [relation.meta.one_field]: ReadableCollectionFilterTypes[relation.collection],
701
+ [relation.meta.one_field]: ReadableCollectionQuantifierFilterTypes[relation.collection],
622
702
  });
623
703
  ReadCollectionTypes[relation.related_collection]?.addFieldArgs(relation.meta.one_field, {
624
704
  filter: ReadableCollectionFilterTypes[relation.collection],
@@ -641,8 +721,12 @@ export function getReadableTypes(gql, schemaComposer, schema, inconsistentFields
641
721
  }
642
722
  }
643
723
  else if (relation.meta?.one_allowed_collections) {
724
+ ReadableCollectionQuantifierFilterTypes[relation.collection]?.removeField('item');
644
725
  ReadableCollectionFilterTypes[relation.collection]?.removeField('item');
645
726
  for (const collection of relation.meta.one_allowed_collections) {
727
+ ReadableCollectionQuantifierFilterTypes[relation.collection]?.addFields({
728
+ [`item__${collection}`]: ReadableCollectionFilterTypes[collection],
729
+ });
646
730
  ReadableCollectionFilterTypes[relation.collection]?.addFields({
647
731
  [`item__${collection}`]: ReadableCollectionFilterTypes[collection],
648
732
  });
@@ -1,3 +1,3 @@
1
1
  import { GraphQLSchema } from 'graphql';
2
- import LRUMapDefault from 'mnemonist/lru-map.js';
3
- export declare const cache: LRUMapDefault.default<string, string | GraphQLSchema>;
2
+ import { LRUMap } from 'mnemonist';
3
+ export declare const cache: LRUMap<string, string | GraphQLSchema>;
@@ -1,9 +1,7 @@
1
1
  import { useEnv } from '@directus/env';
2
2
  import { GraphQLSchema } from 'graphql';
3
- import LRUMapDefault from 'mnemonist/lru-map.js';
3
+ import { LRUMap } from 'mnemonist';
4
4
  import { useBus } from '../../bus/index.js';
5
- // Workaround for misaligned types in mnemonist package exports
6
- const LRUMap = LRUMapDefault;
7
5
  const env = useEnv();
8
6
  const bus = useBus();
9
7
  export const cache = new LRUMap(Number(env['GRAPHQL_SCHEMA_CACHE_CAPACITY'] ?? 100));
@@ -2,19 +2,19 @@ import type { GraphQLService } from './index.js';
2
2
  import type { GraphQLResolveInfo } from 'graphql';
3
3
  export declare function bindPubSub(): void;
4
4
  export declare function createSubscriptionGenerator(gql: GraphQLService, event: string): (_x: unknown, _y: unknown, _z: unknown, request: GraphQLResolveInfo) => AsyncGenerator<{
5
- [x: string]: {
5
+ [event]: {
6
6
  key: string | number;
7
7
  data: null;
8
8
  event: "delete";
9
9
  };
10
10
  } | {
11
- [x: string]: {
11
+ [event]: {
12
12
  key: string | number;
13
13
  data: any;
14
14
  event: "create";
15
15
  };
16
16
  } | {
17
- [x: string]: {
17
+ [event]: {
18
18
  key: string | number;
19
19
  data: any;
20
20
  event: "update";
@@ -12,7 +12,7 @@ export function bindPubSub() {
12
12
  }
13
13
  export function createSubscriptionGenerator(gql, event) {
14
14
  return async function* (_x, _y, _z, request) {
15
- const fields = parseFields(gql, request);
15
+ const fields = await parseFields(gql, request);
16
16
  const args = parseArguments(request);
17
17
  for await (const payload of messages.subscribe(event)) {
18
18
  const eventData = payload;
@@ -79,7 +79,7 @@ function createPubSub(emitter) {
79
79
  },
80
80
  };
81
81
  }
82
- function parseFields(gql, request) {
82
+ async function parseFields(gql, request) {
83
83
  const selections = request.fieldNodes[0]?.selectionSet?.selections ?? [];
84
84
  const dataSelections = selections.reduce((result, selection) => {
85
85
  if (selection.kind === 'Field' &&
@@ -89,7 +89,7 @@ function parseFields(gql, request) {
89
89
  }
90
90
  return result;
91
91
  }, []);
92
- const { fields } = getQuery({}, dataSelections, request.variableValues, gql.accountability);
92
+ const { fields } = await getQuery({}, dataSelections, request.variableValues, gql.schema, gql.accountability);
93
93
  return fields ?? [];
94
94
  }
95
95
  function parseArguments(request) {
@@ -1,6 +1,6 @@
1
- import type { Accountability, Query } from '@directus/types';
1
+ import type { Accountability, Query, SchemaOverview } from '@directus/types';
2
2
  import type { SelectionNode } from 'graphql';
3
3
  /**
4
4
  * Resolve the aggregation query based on the requested aggregated fields
5
5
  */
6
- export declare function getAggregateQuery(rawQuery: Query, selections: readonly SelectionNode[], accountability?: Accountability | null): Query;
6
+ export declare function getAggregateQuery(rawQuery: Query, selections: readonly SelectionNode[], schema: SchemaOverview, accountability?: Accountability | null): Promise<Query>;
@@ -1,11 +1,11 @@
1
- import { replaceFuncs } from './replace-funcs.js';
2
1
  import { sanitizeQuery } from '../../../utils/sanitize-query.js';
3
2
  import { validateQuery } from '../../../utils/validate-query.js';
3
+ import { replaceFuncs } from './replace-funcs.js';
4
4
  /**
5
5
  * Resolve the aggregation query based on the requested aggregated fields
6
6
  */
7
- export function getAggregateQuery(rawQuery, selections, accountability) {
8
- const query = sanitizeQuery(rawQuery, accountability);
7
+ export async function getAggregateQuery(rawQuery, selections, schema, accountability) {
8
+ const query = await sanitizeQuery(rawQuery, schema, accountability);
9
9
  query.aggregate = {};
10
10
  for (let aggregationGroup of selections) {
11
11
  if ((aggregationGroup.kind === 'Field') !== true)
@@ -18,6 +18,7 @@ export declare class ItemsService<Item extends AnyItem = AnyItem, Collection ext
18
18
  eventScope: string;
19
19
  schema: SchemaOverview;
20
20
  cache: Keyv<any> | null;
21
+ nested: string[];
21
22
  constructor(collection: Collection, options: AbstractServiceOptions);
22
23
  /**
23
24
  * Create a fork of the current service, allowing instantiation with different options.
@@ -26,6 +26,7 @@ export class ItemsService {
26
26
  eventScope;
27
27
  schema;
28
28
  cache;
29
+ nested;
29
30
  constructor(collection, options) {
30
31
  this.collection = collection;
31
32
  this.knex = options.knex || getDatabase();
@@ -33,6 +34,7 @@ export class ItemsService {
33
34
  this.eventScope = isSystemCollection(this.collection) ? this.collection.substring(9) : 'items';
34
35
  this.schema = options.schema;
35
36
  this.cache = getCache().cache;
37
+ this.nested = options.nested ?? [];
36
38
  return this;
37
39
  }
38
40
  /**
@@ -43,7 +45,13 @@ export class ItemsService {
43
45
  // ItemsService expects `collection` and `options` as parameters,
44
46
  // while the other services only expect `options`
45
47
  const isItemsService = Service.length === 2;
46
- const newOptions = { knex: this.knex, accountability: this.accountability, schema: this.schema, ...options };
48
+ const newOptions = {
49
+ knex: this.knex,
50
+ accountability: this.accountability,
51
+ schema: this.schema,
52
+ nested: this.nested,
53
+ ...options,
54
+ };
47
55
  if (isItemsService) {
48
56
  return new ItemsService(this.collection, newOptions);
49
57
  }
@@ -95,19 +103,15 @@ export class ItemsService {
95
103
  .filter((field) => field.alias === true)
96
104
  .map((field) => field.field);
97
105
  const payload = cloneDeep(data);
106
+ let actionHookPayload = payload;
98
107
  const nestedActionEvents = [];
99
- // By wrapping the logic in a transaction, we make sure we automatically roll back all the
100
- // changes in the DB if any of the parts contained within throws an error. This also means
101
- // that any errors thrown in any nested relational changes will bubble up and cancel the whole
102
- // update tree
108
+ /**
109
+ * By wrapping the logic in a transaction, we make sure we automatically roll back all the
110
+ * changes in the DB if any of the parts contained within throws an error. This also means
111
+ * that any errors thrown in any nested relational changes will bubble up and cancel the whole
112
+ * update tree
113
+ */
103
114
  const primaryKey = await transaction(this.knex, async (trx) => {
104
- const serviceOptions = {
105
- accountability: this.accountability,
106
- knex: trx,
107
- schema: this.schema,
108
- };
109
- // We're creating new services instances so they can use the transaction as their Knex interface
110
- const payloadService = new PayloadService(this.collection, serviceOptions);
111
115
  // Run all hooks that are attached to this event so the end user has the chance to augment the
112
116
  // item that is about to be saved
113
117
  const payloadAfterHooks = opts.emitEvents !== false
@@ -127,6 +131,7 @@ export class ItemsService {
127
131
  action: 'create',
128
132
  collection: this.collection,
129
133
  payload: payloadAfterHooks,
134
+ nested: this.nested,
130
135
  }, {
131
136
  knex: trx,
132
137
  schema: this.schema,
@@ -135,6 +140,15 @@ export class ItemsService {
135
140
  if (opts.preMutationError) {
136
141
  throw opts.preMutationError;
137
142
  }
143
+ // Ensure the action hook payload has the post filter hook + preset changes
144
+ actionHookPayload = payloadWithPresets;
145
+ // We're creating new services instances so they can use the transaction as their Knex interface
146
+ const payloadService = new PayloadService(this.collection, {
147
+ accountability: this.accountability,
148
+ knex: trx,
149
+ schema: this.schema,
150
+ nested: this.nested,
151
+ });
138
152
  const { payload: payloadWithM2O, revisions: revisionsM2O, nestedActionEvents: nestedActionEventsM2O, userIntegrityCheckFlags: userIntegrityCheckFlagsM2O, } = await payloadService.processM2O(payloadWithPresets, opts);
139
153
  const { payload: payloadWithA2O, revisions: revisionsA2O, nestedActionEvents: nestedActionEventsA2O, userIntegrityCheckFlags: userIntegrityCheckFlagsA2O, } = await payloadService.processA2O(payloadWithM2O, opts);
140
154
  const payloadWithoutAliases = pick(payloadWithA2O, without(fields, ...aliases));
@@ -190,7 +204,7 @@ export class ItemsService {
190
204
  primaryKey = result.id;
191
205
  // Set the primary key on the input item, in order for the "after" event hook to be able
192
206
  // to read from it
193
- payload[primaryKeyField] = primaryKey;
207
+ actionHookPayload[primaryKeyField] = primaryKey;
194
208
  }
195
209
  // At this point, the primary key is guaranteed to be set.
196
210
  primaryKey = primaryKey;
@@ -260,7 +274,7 @@ export class ItemsService {
260
274
  ? ['items.create', `${this.collection}.items.create`]
261
275
  : `${this.eventScope}.create`,
262
276
  meta: {
263
- payload,
277
+ payload: actionHookPayload,
264
278
  key: primaryKey,
265
279
  collection: this.collection,
266
280
  },
@@ -450,8 +464,6 @@ export class ItemsService {
450
464
  * Uses `this.updateMany` under the hood.
451
465
  */
452
466
  async updateOne(key, data, opts) {
453
- const primaryKeyField = this.schema.collections[this.collection].primary;
454
- validateKeys(this.schema, this.collection, primaryKeyField, key);
455
467
  await this.updateMany([key], data, opts);
456
468
  return key;
457
469
  }
@@ -553,6 +565,7 @@ export class ItemsService {
553
565
  action: 'update',
554
566
  collection: this.collection,
555
567
  payload: payloadAfterHooks,
568
+ nested: this.nested,
556
569
  }, {
557
570
  knex: this.knex,
558
571
  schema: this.schema,
@@ -566,6 +579,7 @@ export class ItemsService {
566
579
  accountability: this.accountability,
567
580
  knex: trx,
568
581
  schema: this.schema,
582
+ nested: this.nested,
569
583
  });
570
584
  const { payload: payloadWithM2O, revisions: revisionsM2O, nestedActionEvents: nestedActionEventsM2O, userIntegrityCheckFlags: userIntegrityCheckFlagsM2O, } = await payloadService.processM2O(payloadWithPresets, opts);
571
585
  const { payload: payloadWithA2O, revisions: revisionsA2O, nestedActionEvents: nestedActionEventsA2O, userIntegrityCheckFlags: userIntegrityCheckFlagsA2O, } = await payloadService.processA2O(payloadWithM2O, opts);
@@ -33,9 +33,10 @@ export class MetaService {
33
33
  return this.filterCount(collection, {});
34
34
  }
35
35
  async filterCount(collection, query) {
36
+ const primaryKeyName = this.schema.collections[collection].primary;
36
37
  const aggregateQuery = {
37
38
  aggregate: {
38
- count: ['*'],
39
+ countDistinct: [primaryKeyName],
39
40
  },
40
41
  search: query.search ?? null,
41
42
  filter: query.filter ?? null,
@@ -52,6 +53,7 @@ export class MetaService {
52
53
  const records = await runAst(ast, this.schema, this.accountability, {
53
54
  knex: this.knex,
54
55
  });
55
- return Number((isArray(records) ? records[0]?.['count'] : records?.['count']) ?? 0);
56
+ return Number((isArray(records) ? records[0]?.['countDistinct'][primaryKeyName] : records?.['countDistinct'][primaryKeyName]) ??
57
+ 0);
56
58
  }
57
59
  }
@@ -29,6 +29,7 @@ export declare class PayloadService {
29
29
  helpers: Helpers;
30
30
  collection: string;
31
31
  schema: SchemaOverview;
32
+ nested: string[];
32
33
  constructor(collection: string, options: AbstractServiceOptions);
33
34
  transformers: Transformers;
34
35
  processValues(action: Action, payloads: Partial<Item>[]): Promise<Partial<Item>[]>;
@@ -20,12 +20,14 @@ export class PayloadService {
20
20
  helpers;
21
21
  collection;
22
22
  schema;
23
+ nested;
23
24
  constructor(collection, options) {
24
25
  this.accountability = options.accountability || null;
25
26
  this.knex = options.knex || getDatabase();
26
27
  this.helpers = getHelpers(this.knex);
27
28
  this.collection = collection;
28
29
  this.schema = options.schema;
30
+ this.nested = options.nested ?? [];
29
31
  return this;
30
32
  }
31
33
  transformers = {
@@ -346,6 +348,7 @@ export class PayloadService {
346
348
  accountability: this.accountability,
347
349
  knex: this.knex,
348
350
  schema: this.schema,
351
+ nested: [...this.nested, relation.field],
349
352
  });
350
353
  const relatedPrimaryKeyField = this.schema.collections[relatedCollection].primary;
351
354
  const relatedRecord = payload[relation.field];
@@ -412,6 +415,7 @@ export class PayloadService {
412
415
  accountability: this.accountability,
413
416
  knex: this.knex,
414
417
  schema: this.schema,
418
+ nested: [...this.nested, relation.field],
415
419
  });
416
420
  const relatedRecord = payload[relation.field];
417
421
  if (['string', 'number'].includes(typeof relatedRecord))
@@ -482,6 +486,7 @@ export class PayloadService {
482
486
  accountability: this.accountability,
483
487
  knex: this.knex,
484
488
  schema: this.schema,
489
+ nested: [...this.nested, relation.meta.one_field],
485
490
  });
486
491
  const recordsToUpsert = [];
487
492
  const savedPrimaryKeys = [];
@@ -490,15 +495,23 @@ export class PayloadService {
490
495
  if (!field || Array.isArray(field)) {
491
496
  const updates = field || []; // treat falsey values as removing all children
492
497
  for (let i = 0; i < updates.length; i++) {
498
+ const currentId = parent || payload[currentPrimaryKeyField];
493
499
  const relatedRecord = updates[i];
500
+ const relatedId = typeof relatedRecord === 'string' || typeof relatedRecord === 'number'
501
+ ? relatedRecord
502
+ : relatedRecord[relatedPrimaryKeyField];
494
503
  let record = cloneDeep(relatedRecord);
495
- if (typeof relatedRecord === 'string' || typeof relatedRecord === 'number') {
496
- const existingRecord = await this.knex
504
+ let existingRecord;
505
+ // No relatedId means it's a new record
506
+ if (relatedId) {
507
+ existingRecord = await this.knex
497
508
  .select(relatedPrimaryKeyField, relation.field)
498
509
  .from(relation.collection)
499
- .where({ [relatedPrimaryKeyField]: record })
510
+ .where({ [relatedPrimaryKeyField]: relatedId })
500
511
  .first();
501
- if (!!existingRecord === false) {
512
+ }
513
+ if (typeof relatedRecord === 'string' || typeof relatedRecord === 'number') {
514
+ if (!existingRecord) {
502
515
  throw new ForbiddenError();
503
516
  }
504
517
  // If the related item is already associated to the current item, and there's no
@@ -507,9 +520,7 @@ export class PayloadService {
507
520
  // for items that aren't actually being updated. NOTE: We use == here, as the
508
521
  // primary key might be reported as a string instead of number, coming from the
509
522
  // http route, and or a bigInteger in the DB
510
- if (isNil(existingRecord[relation.field]) === false &&
511
- (existingRecord[relation.field] == parent ||
512
- existingRecord[relation.field] == payload[currentPrimaryKeyField])) {
523
+ if (isNil(existingRecord[relation.field]) === false && existingRecord[relation.field] == currentId) {
513
524
  savedPrimaryKeys.push(existingRecord[relatedPrimaryKeyField]);
514
525
  continue;
515
526
  }
@@ -517,10 +528,10 @@ export class PayloadService {
517
528
  [relatedPrimaryKeyField]: relatedRecord,
518
529
  };
519
530
  }
520
- recordsToUpsert.push({
521
- ...record,
522
- [relation.field]: parent || payload[currentPrimaryKeyField],
523
- });
531
+ if (!existingRecord || existingRecord[relation.field] != parent) {
532
+ record[relation.field] = currentId;
533
+ }
534
+ recordsToUpsert.push(record);
524
535
  }
525
536
  savedPrimaryKeys.push(...(await service.upsertMany(recordsToUpsert, {
526
537
  onRevisionCreate: (pk) => revisions.push(pk),
@@ -608,13 +619,17 @@ export class PayloadService {
608
619
  });
609
620
  }
610
621
  if (alterations.update) {
611
- const primaryKeyField = this.schema.collections[relation.collection].primary;
612
622
  for (const item of alterations.update) {
613
- const { [primaryKeyField]: key, ...record } = item;
614
- await service.updateOne(key, {
615
- ...record,
616
- [relation.field]: parent || payload[currentPrimaryKeyField],
617
- }, {
623
+ const { [relatedPrimaryKeyField]: key, ...record } = item;
624
+ const existingRecord = await this.knex
625
+ .select(relatedPrimaryKeyField, relation.field)
626
+ .from(relation.collection)
627
+ .where({ [relatedPrimaryKeyField]: key })
628
+ .first();
629
+ if (!existingRecord || existingRecord[relation.field] != parent) {
630
+ record[relation.field] = parent || payload[currentPrimaryKeyField];
631
+ }
632
+ await service.updateOne(key, record, {
618
633
  onRevisionCreate: (pk) => revisions.push(pk),
619
634
  onRequireUserIntegrityCheck: (flags) => (userIntegrityCheckFlags |= flags),
620
635
  bypassEmitAction: (params) => opts?.bypassEmitAction ? opts.bypassEmitAction(params) : nestedActionEvents.push(params),
@@ -4,6 +4,7 @@ import argon2 from 'argon2';
4
4
  import jwt from 'jsonwebtoken';
5
5
  import { nanoid } from 'nanoid';
6
6
  import { useLogger } from '../logger/index.js';
7
+ import { clearCache as clearPermissionsCache } from '../permissions/cache.js';
7
8
  import { validateAccess } from '../permissions/modules/validate-access/validate-access.js';
8
9
  import { getMilliseconds } from '../utils/get-milliseconds.js';
9
10
  import { getSecret } from '../utils/get-secret.js';
@@ -13,7 +14,6 @@ import { userName } from '../utils/user-name.js';
13
14
  import { ItemsService } from './items.js';
14
15
  import { MailService } from './mail/index.js';
15
16
  import { UsersService } from './users.js';
16
- import { clearCache as clearPermissionsCache } from '../permissions/cache.js';
17
17
  const env = useEnv();
18
18
  const logger = useLogger();
19
19
  export class SharesService extends ItemsService {
@@ -49,7 +49,7 @@ class OASSpecsService {
49
49
  permissions = await fetchPermissions({ policies, accountability: this.accountability }, { schema: this.schema, knex: this.knex });
50
50
  }
51
51
  const tags = await this.generateTags(schemaForSpec);
52
- const paths = await this.generatePaths(permissions, tags);
52
+ const paths = await this.generatePaths(schemaForSpec, permissions, tags);
53
53
  const components = await this.generateComponents(schemaForSpec, tags);
54
54
  const isDefaultPublicUrl = env['PUBLIC_URL'] === '/';
55
55
  const url = isDefaultPublicUrl && host ? host : env['PUBLIC_URL'];
@@ -114,7 +114,7 @@ class OASSpecsService {
114
114
  // Filter out the generic Items information
115
115
  return tags.filter((tag) => tag.name !== 'Items');
116
116
  }
117
- async generatePaths(permissions, tags) {
117
+ async generatePaths(schema, permissions, tags) {
118
118
  const paths = {};
119
119
  if (!tags)
120
120
  return paths;
@@ -195,11 +195,16 @@ class OASSpecsService {
195
195
  'application/json': {
196
196
  schema: {
197
197
  properties: {
198
- data: {
199
- items: {
198
+ data: schema.collections[collection]?.singleton
199
+ ? {
200
200
  $ref: `#/components/schemas/${tag.name}`,
201
+ }
202
+ : {
203
+ type: 'array',
204
+ items: {
205
+ $ref: `#/components/schemas/${tag.name}`,
206
+ },
201
207
  },
202
- },
203
208
  },
204
209
  },
205
210
  },
@@ -29,7 +29,7 @@ export declare class KvLock implements Lock {
29
29
  private acquireTimeout;
30
30
  private kv;
31
31
  constructor(id: string, lockTimeout?: number, acquireTimeout?: number);
32
- lock(cancelReq: RequestRelease): Promise<void>;
32
+ lock(signal: AbortSignal, cancelReq: RequestRelease): Promise<void>;
33
33
  protected acquireLock(id: string, requestRelease: RequestRelease, signal: AbortSignal): Promise<boolean>;
34
34
  unlock(): Promise<void>;
35
35
  }