directus 9.9.1 → 9.11.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.
Files changed (67) hide show
  1. package/README.md +1 -1
  2. package/dist/app.js +3 -0
  3. package/dist/auth/drivers/oauth2.d.ts +1 -1
  4. package/dist/auth/drivers/oauth2.js +14 -11
  5. package/dist/auth/drivers/openid.d.ts +1 -1
  6. package/dist/auth/drivers/openid.js +14 -11
  7. package/dist/cli/commands/schema/apply.js +4 -3
  8. package/dist/controllers/assets.js +8 -9
  9. package/dist/database/helpers/date/dialects/sqlite.js +6 -2
  10. package/dist/database/index.js +5 -0
  11. package/dist/database/migrations/20210225A-add-relations-sort-field.js +2 -1
  12. package/dist/database/migrations/20210506A-rename-interfaces.js +2 -1
  13. package/dist/database/migrations/20210802A-replace-groups.js +2 -1
  14. package/dist/database/migrations/20210805A-update-groups.js +2 -1
  15. package/dist/database/migrations/20210805B-change-image-metadata-structure.js +3 -2
  16. package/dist/database/migrations/20211007A-update-presets.js +5 -4
  17. package/dist/database/run-ast.js +10 -14
  18. package/dist/database/system-data/fields/activity.yaml +3 -0
  19. package/dist/database/system-data/fields/dashboards.yaml +3 -1
  20. package/dist/database/system-data/fields/notifications.yaml +3 -1
  21. package/dist/database/system-data/fields/panels.yaml +3 -1
  22. package/dist/database/system-data/fields/shares.yaml +3 -1
  23. package/dist/env.js +193 -10
  24. package/dist/exceptions/index.d.ts +1 -0
  25. package/dist/exceptions/index.js +1 -0
  26. package/dist/exceptions/invalid-provider.d.ts +4 -0
  27. package/dist/exceptions/invalid-provider.js +10 -0
  28. package/dist/exceptions/range-not-satisfiable.d.ts +2 -2
  29. package/dist/exceptions/range-not-satisfiable.js +5 -1
  30. package/dist/middleware/graphql.js +2 -1
  31. package/dist/services/assets.js +27 -1
  32. package/dist/services/authentication.js +4 -1
  33. package/dist/services/fields.js +15 -8
  34. package/dist/services/graphql.js +61 -33
  35. package/dist/services/import-export.d.ts +1 -1
  36. package/dist/services/import-export.js +14 -11
  37. package/dist/services/items.d.ts +3 -3
  38. package/dist/services/items.js +31 -3
  39. package/dist/services/payload.d.ts +2 -2
  40. package/dist/services/payload.js +9 -8
  41. package/dist/services/users.d.ts +4 -0
  42. package/dist/services/users.js +20 -0
  43. package/dist/utils/apply-query.d.ts +2 -1
  44. package/dist/utils/apply-query.js +144 -149
  45. package/dist/utils/apply-snapshot.d.ts +3 -3
  46. package/dist/utils/apply-snapshot.js +64 -49
  47. package/dist/utils/get-ast-from-query.js +1 -7
  48. package/dist/utils/get-column-path.d.ts +16 -0
  49. package/dist/utils/get-column-path.js +46 -0
  50. package/dist/utils/get-default-value.js +4 -3
  51. package/dist/utils/get-permissions.d.ts +1 -1
  52. package/dist/utils/get-permissions.js +9 -8
  53. package/dist/utils/get-relation-info.d.ts +7 -0
  54. package/dist/utils/get-relation-info.js +45 -0
  55. package/dist/utils/get-relation-type.d.ts +1 -1
  56. package/dist/utils/get-schema.js +2 -1
  57. package/dist/utils/get-snapshot.js +22 -4
  58. package/dist/utils/merge-permissions-for-share.js +1 -1
  59. package/dist/utils/parse-json.d.ts +5 -0
  60. package/dist/utils/parse-json.js +19 -0
  61. package/dist/utils/reduce-schema.js +4 -5
  62. package/dist/utils/sanitize-query.d.ts +1 -2
  63. package/dist/utils/sanitize-query.js +6 -5
  64. package/dist/utils/validate-keys.d.ts +6 -0
  65. package/dist/utils/validate-keys.js +28 -0
  66. package/dist/utils/validate-query.js +1 -1
  67. package/package.json +16 -18
@@ -4,13 +4,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.isNestedMetaUpdate = exports.applySnapshot = void 0;
7
- const get_snapshot_1 = require("./get-snapshot");
8
- const get_snapshot_diff_1 = require("./get-snapshot-diff");
9
- const database_1 = __importDefault(require("../database"));
10
- const get_schema_1 = require("./get-schema");
11
- const services_1 = require("../services");
12
7
  const lodash_1 = require("lodash");
8
+ const database_1 = __importDefault(require("../database"));
13
9
  const logger_1 = __importDefault(require("../logger"));
10
+ const services_1 = require("../services");
11
+ const get_schema_1 = require("./get-schema");
12
+ const get_snapshot_1 = require("./get-snapshot");
13
+ const get_snapshot_diff_1 = require("./get-snapshot-diff");
14
14
  async function applySnapshot(snapshot, options) {
15
15
  var _a, _b, _c, _d;
16
16
  const database = (_a = options === null || options === void 0 ? void 0 : options.database) !== null && _a !== void 0 ? _a : (0, database_1.default)();
@@ -19,55 +19,70 @@ async function applySnapshot(snapshot, options) {
19
19
  const snapshotDiff = (_d = options === null || options === void 0 ? void 0 : options.diff) !== null && _d !== void 0 ? _d : (0, get_snapshot_diff_1.getSnapshotDiff)(current, snapshot);
20
20
  await database.transaction(async (trx) => {
21
21
  const collectionsService = new services_1.CollectionsService({ knex: trx, schema });
22
- for (const { collection, diff } of snapshotDiff.collections) {
23
- if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'D') {
24
- try {
25
- await collectionsService.deleteOne(collection);
26
- }
27
- catch (err) {
28
- logger_1.default.error(`Failed to delete collection "${collection}"`);
29
- throw err;
22
+ const getNestedCollectionsToCreate = (currentLevelCollection) => snapshotDiff.collections.filter(({ diff }) => { var _a, _b; return ((_b = (_a = diff[0].rhs) === null || _a === void 0 ? void 0 : _a.meta) === null || _b === void 0 ? void 0 : _b.group) === currentLevelCollection; });
23
+ const getNestedCollectionsToDelete = (currentLevelCollection) => snapshotDiff.collections.filter(({ diff }) => { var _a, _b; return ((_b = (_a = diff[0].lhs) === null || _a === void 0 ? void 0 : _a.meta) === null || _b === void 0 ? void 0 : _b.group) === currentLevelCollection; });
24
+ const createCollections = async (collections) => {
25
+ for (const { collection, diff } of collections) {
26
+ if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'N' && diff[0].rhs) {
27
+ // We'll nest the to-be-created fields in the same collection creation, to prevent
28
+ // creating a collection without a primary key
29
+ const fields = snapshotDiff.fields
30
+ .filter((fieldDiff) => fieldDiff.collection === collection)
31
+ .map((fieldDiff) => fieldDiff.diff[0].rhs)
32
+ .map((fieldDiff) => {
33
+ // Casts field type to UUID when applying SQLite-based schema on other databases.
34
+ // This is needed because SQLite snapshots UUID fields as char with length 36, and
35
+ // it will fail when trying to create relation between char field to UUID field
36
+ if (!fieldDiff.schema ||
37
+ fieldDiff.schema.data_type !== 'char' ||
38
+ fieldDiff.schema.max_length !== 36 ||
39
+ !fieldDiff.schema.foreign_key_table ||
40
+ !fieldDiff.schema.foreign_key_column) {
41
+ return fieldDiff;
42
+ }
43
+ const matchingForeignKeyTable = schema.collections[fieldDiff.schema.foreign_key_table];
44
+ if (!matchingForeignKeyTable)
45
+ return fieldDiff;
46
+ const matchingForeignKeyField = matchingForeignKeyTable.fields[fieldDiff.schema.foreign_key_column];
47
+ if (!matchingForeignKeyField || matchingForeignKeyField.type !== 'uuid')
48
+ return fieldDiff;
49
+ return (0, lodash_1.merge)(fieldDiff, { type: 'uuid', schema: { data_type: 'uuid', max_length: null } });
50
+ });
51
+ try {
52
+ await collectionsService.createOne({
53
+ ...diff[0].rhs,
54
+ fields,
55
+ });
56
+ }
57
+ catch (err) {
58
+ logger_1.default.error(`Failed to create collection "${collection}"`);
59
+ throw err;
60
+ }
61
+ // Now that the fields are in for this collection, we can strip them from the field edits
62
+ snapshotDiff.fields = snapshotDiff.fields.filter((fieldDiff) => fieldDiff.collection !== collection);
63
+ await createCollections(getNestedCollectionsToCreate(collection));
30
64
  }
31
65
  }
32
- if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'N' && diff[0].rhs) {
33
- // We'll nest the to-be-created fields in the same collection creation, to prevent
34
- // creating a collection without a primary key
35
- const fields = snapshotDiff.fields
36
- .filter((fieldDiff) => fieldDiff.collection === collection)
37
- .map((fieldDiff) => fieldDiff.diff[0].rhs)
38
- .map((fieldDiff) => {
39
- // Casts field type to UUID when applying SQLite-based schema on other databases.
40
- // This is needed because SQLite snapshots UUID fields as char with length 36, and
41
- // it will fail when trying to create relation between char field to UUID field
42
- if (!fieldDiff.schema ||
43
- fieldDiff.schema.data_type !== 'char' ||
44
- fieldDiff.schema.max_length !== 36 ||
45
- !fieldDiff.schema.foreign_key_table ||
46
- !fieldDiff.schema.foreign_key_column) {
47
- return fieldDiff;
66
+ };
67
+ const deleteCollections = async (collections) => {
68
+ for (const { collection, diff } of collections) {
69
+ if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'D') {
70
+ await deleteCollections(getNestedCollectionsToDelete(collection));
71
+ try {
72
+ await collectionsService.deleteOne(collection);
73
+ }
74
+ catch (err) {
75
+ logger_1.default.error(`Failed to delete collection "${collection}"`);
76
+ throw err;
48
77
  }
49
- const matchingForeignKeyTable = schema.collections[fieldDiff.schema.foreign_key_table];
50
- if (!matchingForeignKeyTable)
51
- return fieldDiff;
52
- const matchingForeignKeyField = matchingForeignKeyTable.fields[fieldDiff.schema.foreign_key_column];
53
- if (!matchingForeignKeyField || matchingForeignKeyField.type !== 'uuid')
54
- return fieldDiff;
55
- return (0, lodash_1.merge)(fieldDiff, { type: 'uuid', schema: { data_type: 'uuid', max_length: null } });
56
- });
57
- try {
58
- await collectionsService.createOne({
59
- ...diff[0].rhs,
60
- fields,
61
- });
62
- }
63
- catch (err) {
64
- logger_1.default.error(`Failed to create collection "${collection}"`);
65
- throw err;
66
78
  }
67
- // Now that the fields are in for this collection, we can strip them from the field
68
- // edits
69
- snapshotDiff.fields = snapshotDiff.fields.filter((fieldDiff) => fieldDiff.collection !== collection);
70
79
  }
80
+ };
81
+ // create top level collections (no group) first, then continue with nested collections recursively
82
+ await createCollections(snapshotDiff.collections.filter(({ diff }) => { var _a; return diff[0].kind === 'N' && ((_a = diff[0].rhs.meta) === null || _a === void 0 ? void 0 : _a.group) === null; }));
83
+ // delete top level collections (no group) first, then continue with nested collections recursively
84
+ await deleteCollections(snapshotDiff.collections.filter(({ diff }) => { var _a; return diff[0].kind === 'D' && ((_a = diff[0].lhs.meta) === null || _a === void 0 ? void 0 : _a.group) === null; }));
85
+ for (const { collection, diff } of snapshotDiff.collections) {
71
86
  if ((diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'E' || (diff === null || diff === void 0 ? void 0 : diff[0].kind) === 'A') {
72
87
  const newValues = snapshot.collections.find((field) => {
73
88
  return field.collection === collection;
@@ -80,12 +80,6 @@ async function getASTFromQuery(collection, query, schema, options) {
80
80
  if (name in query.alias) {
81
81
  name = query.alias[fieldKey];
82
82
  }
83
- // check for junction alias (it is one of the value instead of the key)
84
- if (Object.values(query.alias).includes(name)) {
85
- const aliasKey = Object.keys(query.alias).find((key) => { var _a; return ((_a = query.alias) === null || _a === void 0 ? void 0 : _a[key]) === name; });
86
- if (aliasKey && fieldKey !== aliasKey)
87
- name = aliasKey;
88
- }
89
83
  }
90
84
  const isRelational = name.includes('.') ||
91
85
  // We'll always treat top level o2m fields as a related item. This is an alias field, otherwise it won't return
@@ -93,7 +87,7 @@ async function getASTFromQuery(collection, query, schema, options) {
93
87
  !!schema.relations.find((relation) => { var _a; return relation.related_collection === parentCollection && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === name; });
94
88
  if (isRelational) {
95
89
  // field is relational
96
- const parts = name.split('.');
90
+ const parts = fieldKey.split('.');
97
91
  let rootField = parts[0];
98
92
  let collectionScope = null;
99
93
  // a2o related collection scoped field selector `fields=sections.section_id:headings.title`
@@ -0,0 +1,16 @@
1
+ import { Relation } from '@directus/shared/types';
2
+ declare type AliasMap = string | {
3
+ [key: string]: AliasMap;
4
+ };
5
+ export declare type ColPathProps = {
6
+ path: string[];
7
+ collection: string;
8
+ aliasMap: AliasMap;
9
+ relations: Relation[];
10
+ };
11
+ /**
12
+ * Converts a Directus field list path to the correct SQL names based on the constructed alias map.
13
+ * For example: ['author', 'role', 'name'] -> 'ljnsv.name'
14
+ */
15
+ export declare function getColumnPath({ path, collection, aliasMap, relations }: ColPathProps): string | void;
16
+ export {};
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getColumnPath = void 0;
4
+ const get_relation_info_1 = require("./get-relation-info");
5
+ const exceptions_1 = require("../exceptions");
6
+ const lodash_1 = require("lodash");
7
+ /**
8
+ * Converts a Directus field list path to the correct SQL names based on the constructed alias map.
9
+ * For example: ['author', 'role', 'name'] -> 'ljnsv.name'
10
+ */
11
+ function getColumnPath({ path, collection, aliasMap, relations }) {
12
+ return followRelation(path);
13
+ function followRelation(pathParts, parentCollection = collection, parentAlias) {
14
+ /**
15
+ * For A2M fields, the path can contain an optional collection scope <field>:<scope>
16
+ */
17
+ const pathRoot = pathParts[0].split(':')[0];
18
+ const { relation, relationType } = (0, get_relation_info_1.getRelationInfo)(relations, parentCollection, pathRoot);
19
+ if (!relation) {
20
+ throw new exceptions_1.InvalidQueryException(`"${parentCollection}.${pathRoot}" is not a relational field`);
21
+ }
22
+ const alias = (0, lodash_1.get)(aliasMap, parentAlias ? [parentAlias, ...pathParts] : pathParts);
23
+ const remainingParts = pathParts.slice(1);
24
+ let parent;
25
+ if (relationType === 'a2o') {
26
+ const pathScope = pathParts[0].split(':')[1];
27
+ if (!pathScope) {
28
+ throw new exceptions_1.InvalidQueryException(`You have to provide a collection scope when sorting on a many-to-any item`);
29
+ }
30
+ parent = pathScope;
31
+ }
32
+ else if (relationType === 'm2o') {
33
+ parent = relation.related_collection;
34
+ }
35
+ else {
36
+ parent = relation.collection;
37
+ }
38
+ if (remainingParts.length === 1) {
39
+ return `${alias || parent}.${remainingParts[0]}`;
40
+ }
41
+ if (remainingParts.length) {
42
+ return followRelation(remainingParts, parent, alias);
43
+ }
44
+ }
45
+ }
46
+ exports.getColumnPath = getColumnPath;
@@ -3,9 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- const get_local_type_1 = __importDefault(require("./get-local-type"));
7
- const logger_1 = __importDefault(require("../logger"));
8
6
  const env_1 = __importDefault(require("../env"));
7
+ const logger_1 = __importDefault(require("../logger"));
8
+ const get_local_type_1 = __importDefault(require("./get-local-type"));
9
+ const parse_json_1 = require("./parse-json");
9
10
  function getDefaultValue(column) {
10
11
  var _a;
11
12
  const type = (0, get_local_type_1.default)(column);
@@ -59,7 +60,7 @@ function castToObject(value) {
59
60
  return value;
60
61
  if (typeof value === 'string') {
61
62
  try {
62
- return JSON.parse(value);
63
+ return (0, parse_json_1.parseJSON)(value);
63
64
  }
64
65
  catch (err) {
65
66
  if (env_1.default.NODE_ENV === 'development') {
@@ -1,2 +1,2 @@
1
- import { Permission, Accountability, SchemaOverview } from '@directus/shared/types';
1
+ import { Accountability, Permission, SchemaOverview } from '@directus/shared/types';
2
2
  export declare function getPermissions(accountability: Accountability, schema: SchemaOverview): Promise<Permission[]>;
@@ -6,15 +6,16 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getPermissions = void 0;
7
7
  const utils_1 = require("@directus/shared/utils");
8
8
  const lodash_1 = require("lodash");
9
+ const object_hash_1 = __importDefault(require("object-hash"));
10
+ const cache_1 = require("../cache");
9
11
  const database_1 = __importDefault(require("../database"));
10
12
  const app_access_permissions_1 = require("../database/system-data/app-access-permissions");
13
+ const env_1 = __importDefault(require("../env"));
14
+ const roles_1 = require("../services/roles");
15
+ const users_1 = require("../services/users");
11
16
  const merge_permissions_1 = require("../utils/merge-permissions");
12
17
  const merge_permissions_for_share_1 = require("./merge-permissions-for-share");
13
- const users_1 = require("../services/users");
14
- const roles_1 = require("../services/roles");
15
- const cache_1 = require("../cache");
16
- const object_hash_1 = __importDefault(require("object-hash"));
17
- const env_1 = __importDefault(require("../env"));
18
+ const parse_json_1 = require("./parse-json");
18
19
  async function getPermissions(accountability, schema) {
19
20
  const database = (0, database_1.default)();
20
21
  const { systemCache, cache } = (0, cache_1.getCache)();
@@ -84,19 +85,19 @@ function parsePermissions(permissions) {
84
85
  permissions = permissions.map((permissionRaw) => {
85
86
  const permission = (0, lodash_1.cloneDeep)(permissionRaw);
86
87
  if (permission.permissions && typeof permission.permissions === 'string') {
87
- permission.permissions = JSON.parse(permission.permissions);
88
+ permission.permissions = (0, parse_json_1.parseJSON)(permission.permissions);
88
89
  }
89
90
  else if (permission.permissions === null) {
90
91
  permission.permissions = {};
91
92
  }
92
93
  if (permission.validation && typeof permission.validation === 'string') {
93
- permission.validation = JSON.parse(permission.validation);
94
+ permission.validation = (0, parse_json_1.parseJSON)(permission.validation);
94
95
  }
95
96
  else if (permission.validation === null) {
96
97
  permission.validation = {};
97
98
  }
98
99
  if (permission.presets && typeof permission.presets === 'string') {
99
- permission.presets = JSON.parse(permission.presets);
100
+ permission.presets = (0, parse_json_1.parseJSON)(permission.presets);
100
101
  }
101
102
  else if (permission.presets === null) {
102
103
  permission.presets = {};
@@ -0,0 +1,7 @@
1
+ import { Relation } from '@directus/shared/types';
2
+ declare type RelationInfo = {
3
+ relation: Relation | null;
4
+ relationType: string | null;
5
+ };
6
+ export declare function getRelationInfo(relations: Relation[], collection: string, field: string): RelationInfo;
7
+ export {};
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getRelationInfo = void 0;
4
+ const get_relation_type_1 = require("./get-relation-type");
5
+ function getRelationInfo(relations, collection, field) {
6
+ var _a, _b;
7
+ if (field.startsWith('$FOLLOW') && field.length > 500) {
8
+ throw new Error(`Implicit $FOLLOW statement is too big to parse. Got: "${field.substring(500)}..."`);
9
+ }
10
+ const implicitRelation = (_a = field.match(/^\$FOLLOW\((.*?),(.*?)(?:,(.*?))?\)$/)) === null || _a === void 0 ? void 0 : _a.slice(1);
11
+ if (implicitRelation) {
12
+ if (implicitRelation[2] === undefined) {
13
+ const [m2oCollection, m2oField] = implicitRelation;
14
+ const relation = {
15
+ collection: m2oCollection.trim(),
16
+ field: m2oField.trim(),
17
+ related_collection: collection,
18
+ schema: null,
19
+ meta: null,
20
+ };
21
+ return { relation, relationType: 'o2m' };
22
+ }
23
+ else {
24
+ const [a2oCollection, a2oItemField, a2oCollectionField] = implicitRelation;
25
+ const relation = {
26
+ collection: a2oCollection.trim(),
27
+ field: a2oItemField.trim(),
28
+ related_collection: collection,
29
+ schema: null,
30
+ meta: {
31
+ one_collection_field: a2oCollectionField.trim(),
32
+ },
33
+ };
34
+ return { relation, relationType: 'o2a' };
35
+ }
36
+ }
37
+ const relation = (_b = relations.find((relation) => {
38
+ var _a;
39
+ return ((relation.collection === collection && relation.field === field) ||
40
+ (relation.related_collection === collection && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === field));
41
+ })) !== null && _b !== void 0 ? _b : null;
42
+ const relationType = relation ? (0, get_relation_type_1.getRelationType)({ relation, collection, field }) : null;
43
+ return { relation, relationType };
44
+ }
45
+ exports.getRelationInfo = getRelationInfo;
@@ -1,6 +1,6 @@
1
1
  import { Relation } from '@directus/shared/types';
2
2
  export declare function getRelationType(getRelationOptions: {
3
- relation: Relation;
3
+ relation?: Relation | null;
4
4
  collection: string | null;
5
5
  field: string;
6
6
  }): 'm2o' | 'o2m' | 'a2o' | null;
@@ -17,6 +17,7 @@ const logger_1 = __importDefault(require("../logger"));
17
17
  const services_1 = require("../services");
18
18
  const get_default_value_1 = __importDefault(require("./get-default-value"));
19
19
  const get_local_type_1 = __importDefault(require("./get-local-type"));
20
+ const parse_json_1 = require("./parse-json");
20
21
  async function getSchema(options) {
21
22
  const database = (options === null || options === void 0 ? void 0 : options.database) || (0, database_1.default)();
22
23
  const schemaInspector = (0, schema_1.default)(database);
@@ -119,7 +120,7 @@ async function getDatabaseSchema(database, schemaInspector) {
119
120
  const type = (existing && (0, get_local_type_1.default)(column, { special })) || 'alias';
120
121
  let validation = (_a = field.validation) !== null && _a !== void 0 ? _a : null;
121
122
  if (validation && typeof validation === 'string')
122
- validation = JSON.parse(validation);
123
+ validation = (0, parse_json_1.parseJSON)(validation);
123
124
  result.collections[field.collection].fields[field.field] = {
124
125
  field: field.field,
125
126
  defaultValue: (_b = existing === null || existing === void 0 ? void 0 : existing.defaultValue) !== null && _b !== void 0 ? _b : null,
@@ -16,17 +16,23 @@ async function getSnapshot(options) {
16
16
  const collectionsService = new services_1.CollectionsService({ knex: database, schema });
17
17
  const fieldsService = new services_1.FieldsService({ knex: database, schema });
18
18
  const relationsService = new services_1.RelationsService({ knex: database, schema });
19
- const [collections, fields, relations] = await Promise.all([
19
+ const [collectionsRaw, fieldsRaw, relationsRaw] = await Promise.all([
20
20
  collectionsService.readByQuery(),
21
21
  fieldsService.readAll(),
22
22
  relationsService.readAll(),
23
23
  ]);
24
+ const collectionsFiltered = collectionsRaw.filter((item) => excludeSystem(item));
25
+ const fieldsFiltered = fieldsRaw.filter((item) => excludeSystem(item)).map(omitID);
26
+ const relationsFiltered = relationsRaw.filter((item) => excludeSystem(item)).map(omitID);
27
+ const collectionsSorted = (0, lodash_1.sortBy)((0, lodash_1.mapValues)(collectionsFiltered, sortDeep), ['collection']);
28
+ const fieldsSorted = (0, lodash_1.sortBy)((0, lodash_1.mapValues)(fieldsFiltered, sortDeep), ['collection', 'field']);
29
+ const relationsSorted = (0, lodash_1.sortBy)((0, lodash_1.mapValues)(relationsFiltered, sortDeep), ['collection', 'field']);
24
30
  return {
25
31
  version: 1,
26
32
  directus: package_json_1.version,
27
- collections: collections.filter((item) => excludeSystem(item)),
28
- fields: fields.filter((item) => excludeSystem(item)).map(omitID),
29
- relations: relations.filter((item) => excludeSystem(item)).map(omitID),
33
+ collections: collectionsSorted,
34
+ fields: fieldsSorted,
35
+ relations: relationsSorted,
30
36
  };
31
37
  }
32
38
  exports.getSnapshot = getSnapshot;
@@ -39,3 +45,15 @@ function excludeSystem(item) {
39
45
  function omitID(item) {
40
46
  return (0, lodash_1.omit)(item, 'meta.id');
41
47
  }
48
+ function sortDeep(raw) {
49
+ if ((0, lodash_1.isPlainObject)(raw)) {
50
+ const mapped = (0, lodash_1.mapValues)(raw, sortDeep);
51
+ const pairs = (0, lodash_1.toPairs)(mapped);
52
+ const sorted = (0, lodash_1.sortBy)(pairs);
53
+ return (0, lodash_1.fromPairs)(sorted);
54
+ }
55
+ if ((0, lodash_1.isArray)(raw)) {
56
+ return (0, lodash_1.sortBy)(raw);
57
+ }
58
+ return raw;
59
+ }
@@ -48,7 +48,7 @@ function mergePermissionsForShare(currentPermissions, accountability, schema) {
48
48
  }
49
49
  }
50
50
  // Explicitly filter out permissions to collections unrelated to the root parent item.
51
- const limitedPermissions = currentPermissions.filter(({ collection }) => allowedCollections.includes(collection));
51
+ const limitedPermissions = currentPermissions.filter(({ action, collection }) => allowedCollections.includes(collection) && action === 'read');
52
52
  return (0, merge_permissions_1.mergePermissions)('and', limitedPermissions, generatedPermissions);
53
53
  }
54
54
  exports.mergePermissionsForShare = mergePermissionsForShare;
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Run JSON.parse, but ignore `__proto__` properties. This prevents prototype pollution attacks
3
+ */
4
+ export declare function parseJSON(input: string): any;
5
+ export declare function noproto<T>(key: string, value: T): T | void;
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.noproto = exports.parseJSON = void 0;
4
+ /**
5
+ * Run JSON.parse, but ignore `__proto__` properties. This prevents prototype pollution attacks
6
+ */
7
+ function parseJSON(input) {
8
+ if (String(input).includes('__proto__')) {
9
+ return JSON.parse(input, noproto);
10
+ }
11
+ return JSON.parse(input);
12
+ }
13
+ exports.parseJSON = parseJSON;
14
+ function noproto(key, value) {
15
+ if (key !== '__proto__') {
16
+ return value;
17
+ }
18
+ }
19
+ exports.noproto = noproto;
@@ -10,7 +10,7 @@ const lodash_1 = require("lodash");
10
10
  * @returns Reduced schema
11
11
  */
12
12
  function reduceSchema(schema, permissions, actions = ['create', 'read', 'update', 'delete']) {
13
- var _a, _b, _c, _d, _e;
13
+ var _a, _b, _c;
14
14
  const reduced = {
15
15
  collections: {},
16
16
  relations: [],
@@ -34,10 +34,9 @@ function reduceSchema(schema, permissions, actions = ['create', 'read', 'update'
34
34
  !((_c = allowedFieldsInCollection[collectionName]) === null || _c === void 0 ? void 0 : _c.includes(fieldName))) {
35
35
  continue;
36
36
  }
37
- const relatedCollection = ((_d = schema.relations.find((relation) => relation.collection === collectionName && relation.field === fieldName)) === null || _d === void 0 ? void 0 : _d.related_collection) ||
38
- ((_e = schema.relations.find((relation) => { var _a; return relation.related_collection === collectionName && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === fieldName; })) === null || _e === void 0 ? void 0 : _e.collection);
39
- if (relatedCollection &&
40
- !(permissions === null || permissions === void 0 ? void 0 : permissions.some((permission) => permission.collection === relatedCollection && actions.includes(permission.action)))) {
37
+ const o2mRelation = schema.relations.find((relation) => { var _a; return relation.related_collection === collectionName && ((_a = relation.meta) === null || _a === void 0 ? void 0 : _a.one_field) === fieldName; });
38
+ if (o2mRelation &&
39
+ !(permissions === null || permissions === void 0 ? void 0 : permissions.some((permission) => permission.collection === o2mRelation.collection && actions.includes(permission.action)))) {
41
40
  continue;
42
41
  }
43
42
  fields[fieldName] = field;
@@ -1,3 +1,2 @@
1
- import { Query } from '@directus/shared/types';
2
- import { Accountability } from '@directus/shared/types';
1
+ import { Accountability, Query } from '@directus/shared/types';
3
2
  export declare function sanitizeQuery(rawQuery: Record<string, any>, accountability?: Accountability | null): Query;
@@ -4,10 +4,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.sanitizeQuery = void 0;
7
+ const utils_1 = require("@directus/shared/utils");
7
8
  const lodash_1 = require("lodash");
8
9
  const logger_1 = __importDefault(require("../logger"));
9
10
  const types_1 = require("../types");
10
- const utils_1 = require("@directus/shared/utils");
11
+ const parse_json_1 = require("./parse-json");
11
12
  function sanitizeQuery(rawQuery, accountability) {
12
13
  const query = {};
13
14
  if (rawQuery.limit !== undefined) {
@@ -82,7 +83,7 @@ function sanitizeAggregate(rawAggregate) {
82
83
  let aggregate = rawAggregate;
83
84
  if (typeof rawAggregate === 'string') {
84
85
  try {
85
- aggregate = JSON.parse(rawAggregate);
86
+ aggregate = (0, parse_json_1.parseJSON)(rawAggregate);
86
87
  }
87
88
  catch {
88
89
  logger_1.default.warn('Invalid value passed for filter query parameter.');
@@ -100,7 +101,7 @@ function sanitizeFilter(rawFilter, accountability) {
100
101
  let filters = rawFilter;
101
102
  if (typeof rawFilter === 'string') {
102
103
  try {
103
- filters = JSON.parse(rawFilter);
104
+ filters = (0, parse_json_1.parseJSON)(rawFilter);
104
105
  }
105
106
  catch {
106
107
  logger_1.default.warn('Invalid value passed for filter query parameter.');
@@ -135,7 +136,7 @@ function sanitizeDeep(deep, accountability) {
135
136
  const result = {};
136
137
  if (typeof deep === 'string') {
137
138
  try {
138
- deep = JSON.parse(deep);
139
+ deep = (0, parse_json_1.parseJSON)(deep);
139
140
  }
140
141
  catch {
141
142
  logger_1.default.warn('Invalid value passed for deep query parameter.');
@@ -169,7 +170,7 @@ function sanitizeAlias(rawAlias) {
169
170
  let alias = rawAlias;
170
171
  if (typeof rawAlias === 'string') {
171
172
  try {
172
- alias = JSON.parse(rawAlias);
173
+ alias = (0, parse_json_1.parseJSON)(rawAlias);
173
174
  }
174
175
  catch (err) {
175
176
  logger_1.default.warn('Invalid value passed for alias query parameter.');
@@ -0,0 +1,6 @@
1
+ import { SchemaOverview } from '@directus/shared/types';
2
+ import { PrimaryKey } from '../types';
3
+ /**
4
+ * Validate keys based on its type
5
+ */
6
+ export declare function validateKeys(schema: SchemaOverview, collection: string, keyField: string, keys: PrimaryKey | PrimaryKey[]): void;
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.validateKeys = void 0;
7
+ const exceptions_1 = require("../exceptions");
8
+ const uuid_validate_1 = __importDefault(require("uuid-validate"));
9
+ /**
10
+ * Validate keys based on its type
11
+ */
12
+ function validateKeys(schema, collection, keyField, keys) {
13
+ if (Array.isArray(keys)) {
14
+ for (const key of keys) {
15
+ validateKeys(schema, collection, keyField, key);
16
+ }
17
+ }
18
+ else {
19
+ const primaryKeyFieldType = schema.collections[collection].fields[keyField].type;
20
+ if (primaryKeyFieldType === 'uuid' && !(0, uuid_validate_1.default)(String(keys))) {
21
+ throw new exceptions_1.ForbiddenException();
22
+ }
23
+ else if (primaryKeyFieldType === 'integer' && !Number.isInteger(Number(keys))) {
24
+ throw new exceptions_1.ForbiddenException();
25
+ }
26
+ }
27
+ }
28
+ exports.validateKeys = validateKeys;
@@ -104,7 +104,7 @@ function validateFilterPrimitive(value, key) {
104
104
  false) {
105
105
  throw new exceptions_1.InvalidQueryException(`The filter value for "${key}" has to be a string, number, or boolean`);
106
106
  }
107
- if (typeof value === 'number' && (Number.isNaN(value) || !Number.isSafeInteger(value))) {
107
+ if (typeof value === 'number' && Number.isNaN(value) && value <= Number.MAX_SAFE_INTEGER) {
108
108
  throw new exceptions_1.InvalidQueryException(`The filter value for "${key}" is not a valid number`);
109
109
  }
110
110
  if (typeof value === 'string' && value.length === 0) {