@directus/api 19.1.1 → 19.3.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.
- package/dist/controllers/users.js +1 -0
- package/dist/controllers/utils.js +7 -5
- package/dist/database/helpers/index.d.ts +1 -1
- package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +1 -0
- package/dist/database/helpers/schema/dialects/cockroachdb.js +13 -0
- package/dist/database/helpers/schema/dialects/mssql.d.ts +1 -0
- package/dist/database/helpers/schema/dialects/mssql.js +9 -0
- package/dist/database/helpers/schema/dialects/mysql.d.ts +1 -0
- package/dist/database/helpers/schema/dialects/mysql.js +17 -0
- package/dist/database/helpers/schema/dialects/oracle.d.ts +1 -0
- package/dist/database/helpers/schema/dialects/oracle.js +9 -0
- package/dist/database/helpers/schema/dialects/postgres.d.ts +4 -0
- package/dist/database/helpers/schema/dialects/postgres.js +14 -0
- package/dist/database/helpers/schema/dialects/sqlite.d.ts +1 -0
- package/dist/database/helpers/schema/dialects/sqlite.js +9 -0
- package/dist/database/helpers/schema/index.d.ts +3 -3
- package/dist/database/helpers/schema/index.js +3 -3
- package/dist/database/helpers/schema/types.d.ts +4 -0
- package/dist/database/helpers/schema/types.js +6 -0
- package/dist/middleware/graphql.js +5 -1
- package/dist/services/extensions.js +11 -8
- package/dist/services/graphql/index.js +15 -11
- package/dist/services/graphql/utils/sanitize-gql-schema.d.ts +8 -0
- package/dist/services/graphql/utils/sanitize-gql-schema.js +80 -0
- package/dist/services/roles.d.ts +1 -0
- package/dist/services/roles.js +200 -11
- package/dist/services/users.d.ts +1 -1
- package/dist/services/users.js +86 -17
- package/dist/telemetry/lib/get-report.js +22 -10
- package/dist/telemetry/types/report.d.ts +12 -0
- package/dist/telemetry/utils/check-increased-user-limits.d.ts +7 -0
- package/dist/telemetry/utils/check-increased-user-limits.js +22 -0
- package/dist/telemetry/utils/get-extension-count.d.ts +9 -0
- package/dist/telemetry/utils/get-extension-count.js +19 -0
- package/dist/telemetry/utils/get-field-count.d.ts +6 -0
- package/dist/telemetry/utils/get-field-count.js +12 -0
- package/dist/telemetry/utils/get-item-count.d.ts +10 -6
- package/dist/telemetry/utils/get-item-count.js +13 -9
- package/dist/telemetry/utils/get-role-counts-by-roles.d.ts +6 -0
- package/dist/telemetry/utils/get-role-counts-by-roles.js +27 -0
- package/dist/telemetry/utils/get-role-counts-by-users.d.ts +11 -0
- package/dist/telemetry/utils/get-role-counts-by-users.js +34 -0
- package/dist/telemetry/utils/get-user-count.d.ts +3 -2
- package/dist/telemetry/utils/get-user-count.js +7 -4
- package/dist/telemetry/utils/get-user-counts-by-roles.d.ts +7 -0
- package/dist/telemetry/utils/get-user-counts-by-roles.js +35 -0
- package/dist/telemetry/utils/get-user-item-count.js +4 -2
- package/package.json +28 -28
|
@@ -358,6 +358,7 @@ router.post('/:pk/tfa/disable', asyncHandler(async (req, _res, next) => {
|
|
|
358
358
|
const registerSchema = Joi.object({
|
|
359
359
|
email: Joi.string().email().required(),
|
|
360
360
|
password: Joi.string().required(),
|
|
361
|
+
verification_url: Joi.string().uri(),
|
|
361
362
|
first_name: Joi.string(),
|
|
362
363
|
last_name: Joi.string(),
|
|
363
364
|
});
|
|
@@ -12,13 +12,15 @@ import asyncHandler from '../utils/async-handler.js';
|
|
|
12
12
|
import { generateHash } from '../utils/generate-hash.js';
|
|
13
13
|
import { sanitizeQuery } from '../utils/sanitize-query.js';
|
|
14
14
|
const router = Router();
|
|
15
|
+
const randomStringSchema = Joi.object({
|
|
16
|
+
length: Joi.number().integer().min(1).max(500).default(32),
|
|
17
|
+
});
|
|
15
18
|
router.get('/random/string', asyncHandler(async (req, res) => {
|
|
16
19
|
const { nanoid } = await import('nanoid');
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return res.json({ data: string });
|
|
20
|
+
const { error, value } = randomStringSchema.validate(req.query, { allowUnknown: true });
|
|
21
|
+
if (error)
|
|
22
|
+
throw new InvalidQueryError({ reason: error.message });
|
|
23
|
+
return res.json({ data: nanoid(value.length) });
|
|
22
24
|
}));
|
|
23
25
|
router.post('/hash/generate', asyncHandler(async (req, res) => {
|
|
24
26
|
if (!req.body?.string) {
|
|
@@ -9,7 +9,7 @@ import * as numberHelpers from './number/index.js';
|
|
|
9
9
|
export declare function getHelpers(database: Knex): {
|
|
10
10
|
date: dateHelpers.postgres | dateHelpers.oracle | dateHelpers.mysql | dateHelpers.mssql | dateHelpers.sqlite;
|
|
11
11
|
st: geometryHelpers.mysql | geometryHelpers.postgres | geometryHelpers.mssql | geometryHelpers.sqlite | geometryHelpers.oracle | geometryHelpers.redshift;
|
|
12
|
-
schema: schemaHelpers.mysql | schemaHelpers.cockroachdb | schemaHelpers.mssql | schemaHelpers.postgres | schemaHelpers.sqlite | schemaHelpers.oracle;
|
|
12
|
+
schema: schemaHelpers.mysql | schemaHelpers.cockroachdb | schemaHelpers.mssql | schemaHelpers.postgres | schemaHelpers.sqlite | schemaHelpers.oracle | schemaHelpers.redshift;
|
|
13
13
|
sequence: sequenceHelpers.mysql | sequenceHelpers.postgres;
|
|
14
14
|
number: numberHelpers.cockroachdb | numberHelpers.mssql | numberHelpers.postgres | numberHelpers.sqlite | numberHelpers.oracle;
|
|
15
15
|
};
|
|
@@ -4,4 +4,5 @@ import { SchemaHelper } from '../types.js';
|
|
|
4
4
|
export declare class SchemaHelperCockroachDb extends SchemaHelper {
|
|
5
5
|
changeToType(table: string, column: string, type: (typeof KNEX_TYPES)[number], options?: Options): Promise<void>;
|
|
6
6
|
constraintName(existingName: string): string;
|
|
7
|
+
getDatabaseSize(): Promise<number | null>;
|
|
7
8
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { SchemaHelper } from '../types.js';
|
|
2
|
+
import { useEnv } from '@directus/env';
|
|
3
|
+
const env = useEnv();
|
|
2
4
|
export class SchemaHelperCockroachDb extends SchemaHelper {
|
|
3
5
|
async changeToType(table, column, type, options = {}) {
|
|
4
6
|
await this.changeToTypeByCopy(table, column, type, options);
|
|
@@ -14,4 +16,15 @@ export class SchemaHelperCockroachDb extends SchemaHelper {
|
|
|
14
16
|
return existingName + suffix;
|
|
15
17
|
}
|
|
16
18
|
}
|
|
19
|
+
async getDatabaseSize() {
|
|
20
|
+
try {
|
|
21
|
+
const result = await this.knex
|
|
22
|
+
.select(this.knex.raw('round(SUM(range_size_mb) * 1024 * 1024, 0) AS size'))
|
|
23
|
+
.from(this.knex.raw('[SHOW RANGES FROM database ??]', [env['DB_DATABASE']]));
|
|
24
|
+
return result[0]?.['size'] ? Number(result[0]?.['size']) : null;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
17
30
|
}
|
|
@@ -4,4 +4,5 @@ export declare class SchemaHelperMSSQL extends SchemaHelper {
|
|
|
4
4
|
applyLimit(rootQuery: Knex.QueryBuilder, limit: number): void;
|
|
5
5
|
applyOffset(rootQuery: Knex.QueryBuilder, offset: number): void;
|
|
6
6
|
formatUUID(uuid: string): string;
|
|
7
|
+
getDatabaseSize(): Promise<number | null>;
|
|
7
8
|
}
|
|
@@ -17,4 +17,13 @@ export class SchemaHelperMSSQL extends SchemaHelper {
|
|
|
17
17
|
formatUUID(uuid) {
|
|
18
18
|
return uuid.toUpperCase();
|
|
19
19
|
}
|
|
20
|
+
async getDatabaseSize() {
|
|
21
|
+
try {
|
|
22
|
+
const result = await this.knex.raw('SELECT SUM(size) * 8192 AS size FROM sys.database_files;');
|
|
23
|
+
return result[0]?.['size'] ? Number(result[0]?.['size']) : null;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
20
29
|
}
|
|
@@ -2,4 +2,5 @@ import type { Knex } from 'knex';
|
|
|
2
2
|
import { SchemaHelper } from '../types.js';
|
|
3
3
|
export declare class SchemaHelperMySQL extends SchemaHelper {
|
|
4
4
|
applyMultiRelationalSort(knex: Knex, dbQuery: Knex.QueryBuilder, table: string, primaryKey: string, orderByString: string, orderByFields: Knex.Raw[]): Knex.QueryBuilder;
|
|
5
|
+
getDatabaseSize(): Promise<number | null>;
|
|
5
6
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { useEnv } from '@directus/env';
|
|
1
2
|
import { getDatabaseVersion } from '../../../index.js';
|
|
2
3
|
import { SchemaHelper } from '../types.js';
|
|
4
|
+
const env = useEnv();
|
|
3
5
|
export class SchemaHelperMySQL extends SchemaHelper {
|
|
4
6
|
applyMultiRelationalSort(knex, dbQuery, table, primaryKey, orderByString, orderByFields) {
|
|
5
7
|
if (getDatabaseVersion()?.startsWith('5.7')) {
|
|
@@ -11,4 +13,19 @@ export class SchemaHelperMySQL extends SchemaHelper {
|
|
|
11
13
|
}
|
|
12
14
|
return super.applyMultiRelationalSort(knex, dbQuery, table, primaryKey, orderByString, orderByFields);
|
|
13
15
|
}
|
|
16
|
+
async getDatabaseSize() {
|
|
17
|
+
try {
|
|
18
|
+
const result = (await this.knex
|
|
19
|
+
.sum('size AS size')
|
|
20
|
+
.from(this.knex
|
|
21
|
+
.select(this.knex.raw('data_length + index_length AS size'))
|
|
22
|
+
.from('information_schema.TABLES')
|
|
23
|
+
.where('table_schema', '=', String(env['DB_DATABASE']))
|
|
24
|
+
.as('size')));
|
|
25
|
+
return result[0]?.['size'] ? Number(result[0]?.['size']) : null;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
14
31
|
}
|
|
@@ -29,4 +29,13 @@ export class SchemaHelperOracle extends SchemaHelper {
|
|
|
29
29
|
}
|
|
30
30
|
return field.type;
|
|
31
31
|
}
|
|
32
|
+
async getDatabaseSize() {
|
|
33
|
+
try {
|
|
34
|
+
const result = await this.knex.raw('select SUM(bytes) from dba_segments');
|
|
35
|
+
return result[0]?.['SUM(BYTES)'] ? Number(result[0]?.['SUM(BYTES)']) : null;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
32
41
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useEnv } from '@directus/env';
|
|
2
|
+
import { SchemaHelper } from '../types.js';
|
|
3
|
+
const env = useEnv();
|
|
4
|
+
export class SchemaHelperPostgres extends SchemaHelper {
|
|
5
|
+
async getDatabaseSize() {
|
|
6
|
+
try {
|
|
7
|
+
const result = await this.knex.select(this.knex.raw(`pg_database_size(?) as size;`, [env['DB_DATABASE']]));
|
|
8
|
+
return result[0]?.['size'] ? Number(result[0]?.['size']) : null;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -10,4 +10,13 @@ export class SchemaHelperSQLite extends SchemaHelper {
|
|
|
10
10
|
async postColumnChange() {
|
|
11
11
|
await this.knex.raw('PRAGMA foreign_keys = ON');
|
|
12
12
|
}
|
|
13
|
+
async getDatabaseSize() {
|
|
14
|
+
try {
|
|
15
|
+
const result = await this.knex.raw('SELECT page_count * page_size as "size" FROM pragma_page_count(), pragma_page_size();');
|
|
16
|
+
return result[0]?.['size'] ? Number(result[0]?.['size']) : null;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
13
22
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export { SchemaHelperDefault as postgres } from './dialects/default.js';
|
|
2
1
|
export { SchemaHelperCockroachDb as cockroachdb } from './dialects/cockroachdb.js';
|
|
3
2
|
export { SchemaHelperDefault as redshift } from './dialects/default.js';
|
|
3
|
+
export { SchemaHelperMSSQL as mssql } from './dialects/mssql.js';
|
|
4
|
+
export { SchemaHelperMySQL as mysql } from './dialects/mysql.js';
|
|
4
5
|
export { SchemaHelperOracle as oracle } from './dialects/oracle.js';
|
|
6
|
+
export { SchemaHelperPostgres as postgres } from './dialects/postgres.js';
|
|
5
7
|
export { SchemaHelperSQLite as sqlite } from './dialects/sqlite.js';
|
|
6
|
-
export { SchemaHelperMySQL as mysql } from './dialects/mysql.js';
|
|
7
|
-
export { SchemaHelperMSSQL as mssql } from './dialects/mssql.js';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
export { SchemaHelperDefault as postgres } from './dialects/default.js';
|
|
2
1
|
export { SchemaHelperCockroachDb as cockroachdb } from './dialects/cockroachdb.js';
|
|
3
2
|
export { SchemaHelperDefault as redshift } from './dialects/default.js';
|
|
3
|
+
export { SchemaHelperMSSQL as mssql } from './dialects/mssql.js';
|
|
4
|
+
export { SchemaHelperMySQL as mysql } from './dialects/mysql.js';
|
|
4
5
|
export { SchemaHelperOracle as oracle } from './dialects/oracle.js';
|
|
6
|
+
export { SchemaHelperPostgres as postgres } from './dialects/postgres.js';
|
|
5
7
|
export { SchemaHelperSQLite as sqlite } from './dialects/sqlite.js';
|
|
6
|
-
export { SchemaHelperMySQL as mysql } from './dialects/mysql.js';
|
|
7
|
-
export { SchemaHelperMSSQL as mssql } from './dialects/mssql.js';
|
|
@@ -23,4 +23,8 @@ export declare abstract class SchemaHelper extends DatabaseHelper {
|
|
|
23
23
|
castA2oPrimaryKey(): string;
|
|
24
24
|
applyMultiRelationalSort(knex: Knex, dbQuery: Knex.QueryBuilder, table: string, primaryKey: string, orderByString: string, orderByFields: Knex.Raw[]): Knex.QueryBuilder;
|
|
25
25
|
formatUUID(uuid: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* @returns Size of the database in bytes
|
|
28
|
+
*/
|
|
29
|
+
getDatabaseSize(): Promise<number | null>;
|
|
26
30
|
}
|
|
@@ -3,6 +3,7 @@ import { getOperationAST, parse, Source } from 'graphql';
|
|
|
3
3
|
import { InvalidPayloadError, InvalidQueryError, MethodNotAllowedError } from '@directus/errors';
|
|
4
4
|
import { GraphQLValidationError } from '../services/graphql/errors/validation.js';
|
|
5
5
|
import asyncHandler from '../utils/async-handler.js';
|
|
6
|
+
import { useEnv } from '@directus/env';
|
|
6
7
|
export const parseGraphQL = asyncHandler(async (req, res, next) => {
|
|
7
8
|
if (req.method !== 'GET' && req.method !== 'POST') {
|
|
8
9
|
throw new MethodNotAllowedError({ allowed: ['GET', 'POST'], current: req.method });
|
|
@@ -35,7 +36,10 @@ export const parseGraphQL = asyncHandler(async (req, res, next) => {
|
|
|
35
36
|
throw new InvalidPayloadError({ reason: 'Must provide query string' });
|
|
36
37
|
}
|
|
37
38
|
try {
|
|
38
|
-
|
|
39
|
+
const env = useEnv();
|
|
40
|
+
document = parse(new Source(query), {
|
|
41
|
+
maxTokens: Number(env['GRAPHQL_QUERY_TOKEN_LIMIT']),
|
|
42
|
+
});
|
|
39
43
|
}
|
|
40
44
|
catch (err) {
|
|
41
45
|
throw new GraphQLValidationError({
|
|
@@ -52,7 +52,7 @@ export class ExtensionsService {
|
|
|
52
52
|
const points = version.bundled.length ?? 1;
|
|
53
53
|
const afterInstallCount = currentlyInstalledCount + points;
|
|
54
54
|
if (afterInstallCount >= limit) {
|
|
55
|
-
throw new LimitExceededError();
|
|
55
|
+
throw new LimitExceededError({ category: 'Extensions' });
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
return { extension, version };
|
|
@@ -188,16 +188,19 @@ export class ExtensionsService {
|
|
|
188
188
|
* - Entry status change resulted in all children being disabled then the parent bundle is disabled
|
|
189
189
|
* - Entry status change resulted in at least one child being enabled then the parent bundle is enabled
|
|
190
190
|
*/
|
|
191
|
-
async checkBundleAndSyncStatus(trx,
|
|
191
|
+
async checkBundleAndSyncStatus(trx, extensionId, extension) {
|
|
192
192
|
if (extension.bundle === null && extension.schema?.type === 'bundle') {
|
|
193
193
|
// If extension is the parent bundle, set it and all nested extensions to enabled
|
|
194
194
|
await trx('directus_extensions')
|
|
195
195
|
.update({ enabled: extension.meta.enabled })
|
|
196
|
-
.where({ bundle:
|
|
197
|
-
.orWhere({ id:
|
|
196
|
+
.where({ bundle: extensionId })
|
|
197
|
+
.orWhere({ id: extensionId });
|
|
198
198
|
return;
|
|
199
199
|
}
|
|
200
|
-
const
|
|
200
|
+
const parentId = extension.bundle ?? extension.meta.bundle;
|
|
201
|
+
if (!parentId)
|
|
202
|
+
return;
|
|
203
|
+
const parent = await this.readOne(parentId);
|
|
201
204
|
if (parent.schema?.type !== 'bundle') {
|
|
202
205
|
return;
|
|
203
206
|
}
|
|
@@ -207,14 +210,14 @@ export class ExtensionsService {
|
|
|
207
210
|
});
|
|
208
211
|
}
|
|
209
212
|
const hasEnabledChildren = !!(await trx('directus_extensions')
|
|
210
|
-
.where({ bundle:
|
|
213
|
+
.where({ bundle: parentId })
|
|
211
214
|
.where({ enabled: true })
|
|
212
215
|
.first());
|
|
213
216
|
if (hasEnabledChildren) {
|
|
214
|
-
await trx('directus_extensions').update({ enabled: true }).where({ id:
|
|
217
|
+
await trx('directus_extensions').update({ enabled: true }).where({ id: parentId });
|
|
215
218
|
}
|
|
216
219
|
else {
|
|
217
|
-
await trx('directus_extensions').update({ enabled: false }).where({ id:
|
|
220
|
+
await trx('directus_extensions').update({ enabled: false }).where({ id: parentId });
|
|
218
221
|
}
|
|
219
222
|
}
|
|
220
223
|
}
|
|
@@ -47,6 +47,7 @@ import { GraphQLStringOrFloat } from './types/string-or-float.js';
|
|
|
47
47
|
import { GraphQLVoid } from './types/void.js';
|
|
48
48
|
import { addPathToValidationError } from './utils/add-path-to-validation-error.js';
|
|
49
49
|
import processError from './utils/process-error.js';
|
|
50
|
+
import { sanitizeGraphqlSchema } from './utils/sanitize-gql-schema.js';
|
|
50
51
|
const env = useEnv();
|
|
51
52
|
const validationRules = Array.from(specifiedRules);
|
|
52
53
|
if (env['GRAPHQL_INTROSPECTION'] === false) {
|
|
@@ -115,19 +116,20 @@ export class GraphQLService {
|
|
|
115
116
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
116
117
|
const self = this;
|
|
117
118
|
const schemaComposer = new SchemaComposer();
|
|
119
|
+
const sanitizedSchema = sanitizeGraphqlSchema(this.schema);
|
|
118
120
|
const schema = {
|
|
119
121
|
read: this.accountability?.admin === true
|
|
120
|
-
?
|
|
121
|
-
: reduceSchema(
|
|
122
|
+
? sanitizedSchema
|
|
123
|
+
: reduceSchema(sanitizedSchema, this.accountability?.permissions || null, ['read']),
|
|
122
124
|
create: this.accountability?.admin === true
|
|
123
|
-
?
|
|
124
|
-
: reduceSchema(
|
|
125
|
+
? sanitizedSchema
|
|
126
|
+
: reduceSchema(sanitizedSchema, this.accountability?.permissions || null, ['create']),
|
|
125
127
|
update: this.accountability?.admin === true
|
|
126
|
-
?
|
|
127
|
-
: reduceSchema(
|
|
128
|
+
? sanitizedSchema
|
|
129
|
+
: reduceSchema(sanitizedSchema, this.accountability?.permissions || null, ['update']),
|
|
128
130
|
delete: this.accountability?.admin === true
|
|
129
|
-
?
|
|
130
|
-
: reduceSchema(
|
|
131
|
+
? sanitizedSchema
|
|
132
|
+
: reduceSchema(sanitizedSchema, this.accountability?.permissions || null, ['delete']),
|
|
131
133
|
};
|
|
132
134
|
const subscriptionEventType = schemaComposer.createEnumTC({
|
|
133
135
|
name: 'EventEnum',
|
|
@@ -2074,10 +2076,10 @@ export class GraphQLService {
|
|
|
2074
2076
|
},
|
|
2075
2077
|
resolve: async (_, args) => {
|
|
2076
2078
|
const { nanoid } = await import('nanoid');
|
|
2077
|
-
if (args['length'] &&
|
|
2078
|
-
throw new InvalidPayloadError({ reason: `"length"
|
|
2079
|
+
if (args['length'] !== undefined && (args['length'] < 1 || args['length'] > 500)) {
|
|
2080
|
+
throw new InvalidPayloadError({ reason: `"length" must be between 1 and 500` });
|
|
2079
2081
|
}
|
|
2080
|
-
return nanoid(args['length'] ?
|
|
2082
|
+
return nanoid(args['length'] ? args['length'] : 32);
|
|
2081
2083
|
},
|
|
2082
2084
|
},
|
|
2083
2085
|
utils_hash_generate: {
|
|
@@ -2162,6 +2164,7 @@ export class GraphQLService {
|
|
|
2162
2164
|
args: {
|
|
2163
2165
|
email: new GraphQLNonNull(GraphQLString),
|
|
2164
2166
|
password: new GraphQLNonNull(GraphQLString),
|
|
2167
|
+
verification_url: GraphQLString,
|
|
2165
2168
|
first_name: GraphQLString,
|
|
2166
2169
|
last_name: GraphQLString,
|
|
2167
2170
|
},
|
|
@@ -2174,6 +2177,7 @@ export class GraphQLService {
|
|
|
2174
2177
|
await service.registerUser({
|
|
2175
2178
|
email: args.email,
|
|
2176
2179
|
password: args.password,
|
|
2180
|
+
verification_url: args.verification_url,
|
|
2177
2181
|
first_name: args.first_name,
|
|
2178
2182
|
last_name: args.last_name,
|
|
2179
2183
|
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SchemaOverview } from '@directus/types';
|
|
2
|
+
/**
|
|
3
|
+
* Filters out invalid collections to prevent graphql from errorring on schema generation
|
|
4
|
+
*
|
|
5
|
+
* @param schema
|
|
6
|
+
* @returns sanitized schema
|
|
7
|
+
*/
|
|
8
|
+
export declare function sanitizeGraphqlSchema(schema: SchemaOverview): SchemaOverview;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useLogger } from '../../../logger.js';
|
|
2
|
+
/**
|
|
3
|
+
* Regex was taken from the spec
|
|
4
|
+
* https://spec.graphql.org/June2018/#sec-Names
|
|
5
|
+
*/
|
|
6
|
+
const GRAPHQL_NAME_REGEX = /^[_A-Za-z][_0-9A-Za-z]*$/;
|
|
7
|
+
/**
|
|
8
|
+
* Manually curated list of GraphQL reserved names to cover the most likely naming footguns.
|
|
9
|
+
* This list is not exhaustive and does not cover generated type names.
|
|
10
|
+
*/
|
|
11
|
+
const GRAPHQL_RESERVED_NAMES = [
|
|
12
|
+
'Subscription',
|
|
13
|
+
'Query',
|
|
14
|
+
'Mutation',
|
|
15
|
+
'Int',
|
|
16
|
+
'Float',
|
|
17
|
+
'String',
|
|
18
|
+
'Boolean',
|
|
19
|
+
'DateTime',
|
|
20
|
+
'ID',
|
|
21
|
+
'uid',
|
|
22
|
+
'Point',
|
|
23
|
+
'PointList',
|
|
24
|
+
'Polygon',
|
|
25
|
+
'MultiPolygon',
|
|
26
|
+
'JSON',
|
|
27
|
+
'Hash',
|
|
28
|
+
'Date',
|
|
29
|
+
'Void',
|
|
30
|
+
];
|
|
31
|
+
/**
|
|
32
|
+
* Filters out invalid collections to prevent graphql from errorring on schema generation
|
|
33
|
+
*
|
|
34
|
+
* @param schema
|
|
35
|
+
* @returns sanitized schema
|
|
36
|
+
*/
|
|
37
|
+
export function sanitizeGraphqlSchema(schema) {
|
|
38
|
+
const logger = useLogger();
|
|
39
|
+
const collections = Object.entries(schema.collections).filter(([collectionName, _data]) => {
|
|
40
|
+
// double underscore __ is reserved for GraphQL introspection
|
|
41
|
+
if (collectionName.startsWith('__') || !collectionName.match(GRAPHQL_NAME_REGEX)) {
|
|
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 __`);
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (GRAPHQL_RESERVED_NAMES.includes(collectionName)) {
|
|
46
|
+
logger.warn(`GraphQL skipping collection "${collectionName}" because it is a reserved keyword`);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
});
|
|
51
|
+
schema.collections = Object.fromEntries(collections);
|
|
52
|
+
const collectionExists = (collection) => Boolean(schema.collections[collection]);
|
|
53
|
+
const skipRelation = (relation) => {
|
|
54
|
+
const relationName = relation.schema?.constraint_name ?? `${relation.collection}.${relation.field}`;
|
|
55
|
+
logger.warn(`GraphQL skipping relation "${relationName}" because it links to a non-existent or invalid collection.`);
|
|
56
|
+
return false;
|
|
57
|
+
};
|
|
58
|
+
schema.relations = schema.relations.filter((relation) => {
|
|
59
|
+
if (relation.collection && !collectionExists(relation.collection)) {
|
|
60
|
+
return skipRelation(relation);
|
|
61
|
+
}
|
|
62
|
+
if (relation.related_collection && !collectionExists(relation.related_collection)) {
|
|
63
|
+
return skipRelation(relation);
|
|
64
|
+
}
|
|
65
|
+
if (relation.meta) {
|
|
66
|
+
if (relation.meta.many_collection && !collectionExists(relation.meta.many_collection)) {
|
|
67
|
+
return skipRelation(relation);
|
|
68
|
+
}
|
|
69
|
+
if (relation.meta.one_collection && !collectionExists(relation.meta.one_collection)) {
|
|
70
|
+
return skipRelation(relation);
|
|
71
|
+
}
|
|
72
|
+
if (relation.meta.one_allowed_collections &&
|
|
73
|
+
relation.meta.one_allowed_collections.some((allowed_collection) => !collectionExists(allowed_collection))) {
|
|
74
|
+
return skipRelation(relation);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
78
|
+
});
|
|
79
|
+
return schema;
|
|
80
|
+
}
|
package/dist/services/roles.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export declare class RolesService extends ItemsService {
|
|
|
7
7
|
private checkForOtherAdminUsers;
|
|
8
8
|
private isIpAccessValid;
|
|
9
9
|
private assertValidIpAccess;
|
|
10
|
+
private getRoleAccessType;
|
|
10
11
|
createOne(data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
|
|
11
12
|
createMany(data: Partial<Item>[], opts?: MutationOptions): Promise<PrimaryKey[]>;
|
|
12
13
|
updateOne(key: PrimaryKey, data: Partial<Item>, opts?: MutationOptions): Promise<PrimaryKey>;
|