@directus/api 31.0.0 → 32.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.
- package/dist/app.js +2 -0
- package/dist/auth/auth.d.ts +2 -1
- package/dist/auth/auth.js +7 -2
- package/dist/auth/drivers/ldap.d.ts +0 -2
- package/dist/auth/drivers/ldap.js +9 -7
- package/dist/auth/drivers/oauth2.d.ts +0 -2
- package/dist/auth/drivers/oauth2.js +11 -8
- package/dist/auth/drivers/openid.d.ts +0 -2
- package/dist/auth/drivers/openid.js +11 -8
- package/dist/auth/drivers/saml.d.ts +0 -2
- package/dist/auth/drivers/saml.js +5 -5
- package/dist/auth.js +1 -2
- package/dist/cli/commands/bootstrap/index.js +12 -33
- package/dist/cli/commands/init/index.js +1 -1
- package/dist/cli/commands/schema/apply.d.ts +4 -0
- package/dist/cli/commands/schema/apply.js +26 -3
- package/dist/controllers/collections.js +7 -2
- package/dist/controllers/fields.js +31 -8
- package/dist/controllers/server.js +26 -1
- package/dist/controllers/settings.js +9 -2
- package/dist/controllers/users.js +2 -2
- package/dist/database/helpers/fn/types.js +3 -3
- package/dist/database/helpers/schema/dialects/cockroachdb.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/cockroachdb.js +13 -0
- package/dist/database/helpers/schema/dialects/mssql.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/mssql.js +23 -0
- package/dist/database/helpers/schema/dialects/mysql.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/mysql.js +25 -0
- package/dist/database/helpers/schema/dialects/oracle.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/oracle.js +13 -0
- package/dist/database/helpers/schema/dialects/postgres.d.ts +2 -1
- package/dist/database/helpers/schema/dialects/postgres.js +13 -0
- package/dist/database/helpers/schema/types.d.ts +5 -0
- package/dist/database/helpers/schema/types.js +6 -0
- package/dist/database/migrations/20251012A-add-field-searchable.d.ts +3 -0
- package/dist/database/migrations/20251012A-add-field-searchable.js +10 -0
- package/dist/database/migrations/20251014A-add-project-owner.d.ts +3 -0
- package/dist/database/migrations/20251014A-add-project-owner.js +37 -0
- package/dist/database/migrations/20251028A-add-retention-indexes.d.ts +3 -0
- package/dist/database/migrations/20251028A-add-retention-indexes.js +42 -0
- package/dist/database/run-ast/lib/apply-query/add-join.js +2 -2
- package/dist/database/run-ast/lib/apply-query/filter/get-filter-type.d.ts +2 -2
- package/dist/database/run-ast/lib/apply-query/index.d.ts +0 -1
- package/dist/database/run-ast/lib/apply-query/index.js +4 -6
- package/dist/database/run-ast/lib/apply-query/search.js +2 -0
- package/dist/database/run-ast/lib/get-db-query.js +7 -6
- package/dist/database/run-ast/utils/generate-alias.d.ts +6 -0
- package/dist/database/run-ast/utils/generate-alias.js +57 -0
- package/dist/flows.js +1 -0
- package/dist/mcp/schema.d.ts +14 -14
- package/dist/mcp/schema.js +6 -6
- package/dist/mcp/server.d.ts +9 -3
- package/dist/mcp/server.js +1 -1
- package/dist/mcp/tools/collections.d.ts +1 -1
- package/dist/mcp/tools/fields.d.ts +1 -1
- package/dist/mcp/tools/files.d.ts +25 -25
- package/dist/mcp/tools/flows.d.ts +36 -36
- package/dist/mcp/tools/folders.d.ts +18 -18
- package/dist/mcp/tools/items.d.ts +18 -18
- package/dist/mcp/tools/operations.d.ts +19 -19
- package/dist/mcp/tools/prompts/items.md +1 -1
- package/dist/metrics/lib/create-metrics.js +16 -25
- package/dist/middleware/collection-exists.js +2 -2
- package/dist/operations/mail/index.js +3 -1
- package/dist/operations/mail/rate-limiter.d.ts +1 -0
- package/dist/operations/mail/rate-limiter.js +29 -0
- package/dist/permissions/modules/process-payload/process-payload.js +3 -10
- package/dist/permissions/modules/validate-access/validate-access.js +2 -3
- package/dist/schedules/metrics.js +6 -2
- package/dist/schedules/project.d.ts +4 -0
- package/dist/schedules/project.js +27 -0
- package/dist/services/collections.d.ts +3 -3
- package/dist/services/collections.js +16 -1
- package/dist/services/fields.d.ts +21 -5
- package/dist/services/fields.js +105 -28
- package/dist/services/graphql/resolvers/query.js +1 -1
- package/dist/services/graphql/resolvers/system-admin.js +49 -5
- package/dist/services/graphql/schema/parse-query.js +8 -8
- package/dist/services/graphql/utils/aggregate-query.d.ts +1 -1
- package/dist/services/graphql/utils/aggregate-query.js +5 -1
- package/dist/services/graphql/utils/filter-replace-m2a.js +2 -1
- package/dist/services/import-export.d.ts +9 -1
- package/dist/services/import-export.js +287 -101
- package/dist/services/items.d.ts +1 -1
- package/dist/services/items.js +36 -20
- package/dist/services/mail/index.js +2 -0
- package/dist/services/mail/rate-limiter.d.ts +1 -0
- package/dist/services/mail/rate-limiter.js +29 -0
- package/dist/services/meta.js +28 -24
- package/dist/services/schema.js +4 -1
- package/dist/services/server.d.ts +1 -0
- package/dist/services/server.js +14 -18
- package/dist/services/settings.d.ts +2 -1
- package/dist/services/settings.js +15 -0
- package/dist/services/tus/server.js +14 -9
- package/dist/telemetry/lib/get-report.js +4 -4
- package/dist/telemetry/lib/send-report.d.ts +6 -1
- package/dist/telemetry/lib/send-report.js +3 -1
- package/dist/telemetry/types/report.d.ts +17 -1
- package/dist/telemetry/utils/get-settings.d.ts +9 -0
- package/dist/telemetry/utils/get-settings.js +14 -0
- package/dist/test-utils/README.md +760 -0
- package/dist/test-utils/cache.d.ts +51 -0
- package/dist/test-utils/cache.js +59 -0
- package/dist/test-utils/database.d.ts +48 -0
- package/dist/test-utils/database.js +52 -0
- package/dist/test-utils/emitter.d.ts +35 -0
- package/dist/test-utils/emitter.js +38 -0
- package/dist/test-utils/fields-service.d.ts +28 -0
- package/dist/test-utils/fields-service.js +36 -0
- package/dist/test-utils/items-service.d.ts +23 -0
- package/dist/test-utils/items-service.js +37 -0
- package/dist/test-utils/knex.d.ts +164 -0
- package/dist/test-utils/knex.js +268 -0
- package/dist/test-utils/schema.d.ts +26 -0
- package/dist/test-utils/schema.js +35 -0
- package/dist/types/auth.d.ts +0 -2
- package/dist/utils/apply-diff.js +15 -0
- package/dist/utils/create-admin.d.ts +11 -0
- package/dist/utils/create-admin.js +50 -0
- package/dist/utils/get-schema.js +5 -3
- package/dist/utils/get-snapshot-diff.js +49 -5
- package/dist/utils/get-snapshot.js +13 -7
- package/dist/utils/sanitize-schema.d.ts +11 -4
- package/dist/utils/sanitize-schema.js +9 -6
- package/dist/utils/schedule.js +15 -19
- package/dist/utils/validate-diff.js +31 -0
- package/dist/utils/validate-snapshot.js +7 -0
- package/dist/websocket/controllers/hooks.js +12 -20
- package/dist/websocket/messages.d.ts +3 -3
- package/package.json +63 -65
- package/dist/cli/utils/defaults.d.ts +0 -4
- package/dist/cli/utils/defaults.js +0 -17
- package/dist/telemetry/utils/get-project-id.d.ts +0 -2
- package/dist/telemetry/utils/get-project-id.js +0 -4
package/dist/services/fields.js
CHANGED
|
@@ -2,8 +2,10 @@ import { DEFAULT_NUMERIC_PRECISION, DEFAULT_NUMERIC_SCALE, KNEX_TYPES, REGEX_BET
|
|
|
2
2
|
import { useEnv } from '@directus/env';
|
|
3
3
|
import { ForbiddenError, InvalidPayloadError } from '@directus/errors';
|
|
4
4
|
import { createInspector } from '@directus/schema';
|
|
5
|
+
import { isSystemField } from '@directus/system-data';
|
|
5
6
|
import { addFieldFlag, getRelations, toArray } from '@directus/utils';
|
|
6
7
|
import { isEqual, isNil, merge } from 'lodash-es';
|
|
8
|
+
import { z } from 'zod';
|
|
7
9
|
import { clearSystemCache, getCache, getCacheValue, setCacheValue } from '../cache.js';
|
|
8
10
|
import { ALIAS_TYPES, ALLOWED_DB_DEFAULT_FUNCTIONS } from '../constants.js';
|
|
9
11
|
import { translateDatabaseError } from '../database/errors/translate.js';
|
|
@@ -28,6 +30,17 @@ import { PayloadService } from './payload.js';
|
|
|
28
30
|
import { RelationsService } from './relations.js';
|
|
29
31
|
const systemFieldRows = getSystemFieldRowsWithAuthProviders();
|
|
30
32
|
const env = useEnv();
|
|
33
|
+
export const systemFieldUpdateSchema = z
|
|
34
|
+
.object({
|
|
35
|
+
collection: z.string().optional(),
|
|
36
|
+
field: z.string().optional(),
|
|
37
|
+
schema: z
|
|
38
|
+
.object({
|
|
39
|
+
is_indexed: z.boolean().optional(),
|
|
40
|
+
})
|
|
41
|
+
.strict(),
|
|
42
|
+
})
|
|
43
|
+
.strict();
|
|
31
44
|
export class FieldsService {
|
|
32
45
|
knex;
|
|
33
46
|
helpers;
|
|
@@ -89,14 +102,14 @@ export class FieldsService {
|
|
|
89
102
|
schema: this.schema,
|
|
90
103
|
});
|
|
91
104
|
if (collection) {
|
|
92
|
-
fields =
|
|
105
|
+
fields = await nonAuthorizedItemsService.readByQuery({
|
|
93
106
|
filter: { collection: { _eq: collection } },
|
|
94
107
|
limit: -1,
|
|
95
|
-
})
|
|
108
|
+
});
|
|
96
109
|
fields.push(...systemFieldRows.filter((fieldMeta) => fieldMeta.collection === collection));
|
|
97
110
|
}
|
|
98
111
|
else {
|
|
99
|
-
fields =
|
|
112
|
+
fields = await nonAuthorizedItemsService.readByQuery({ limit: -1 });
|
|
100
113
|
fields.push(...systemFieldRows);
|
|
101
114
|
}
|
|
102
115
|
const columns = (await this.columnInfo(collection)).map((column) => ({
|
|
@@ -269,28 +282,35 @@ export class FieldsService {
|
|
|
269
282
|
if (flagToAdd) {
|
|
270
283
|
addFieldFlag(field, flagToAdd);
|
|
271
284
|
}
|
|
285
|
+
let hookAdjustedField = field;
|
|
286
|
+
const attemptConcurrentIndex = Boolean(opts?.attemptConcurrentIndex);
|
|
272
287
|
await transaction(this.knex, async (trx) => {
|
|
273
288
|
const itemsService = new ItemsService('directus_fields', {
|
|
274
289
|
knex: trx,
|
|
275
290
|
accountability: this.accountability,
|
|
276
291
|
schema: this.schema,
|
|
277
292
|
});
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
293
|
+
hookAdjustedField =
|
|
294
|
+
opts?.emitEvents !== false
|
|
295
|
+
? await emitter.emitFilter(`fields.create`, field, {
|
|
296
|
+
collection: collection,
|
|
297
|
+
}, {
|
|
298
|
+
database: trx,
|
|
299
|
+
schema: this.schema,
|
|
300
|
+
accountability: this.accountability,
|
|
301
|
+
})
|
|
302
|
+
: field;
|
|
287
303
|
if (hookAdjustedField.type && ALIAS_TYPES.includes(hookAdjustedField.type) === false) {
|
|
288
304
|
if (table) {
|
|
289
|
-
this.addColumnToTable(table, collection, hookAdjustedField
|
|
305
|
+
this.addColumnToTable(table, collection, hookAdjustedField, {
|
|
306
|
+
attemptConcurrentIndex,
|
|
307
|
+
});
|
|
290
308
|
}
|
|
291
309
|
else {
|
|
292
310
|
await trx.schema.alterTable(collection, (table) => {
|
|
293
|
-
this.addColumnToTable(table, collection, hookAdjustedField
|
|
311
|
+
this.addColumnToTable(table, collection, hookAdjustedField, {
|
|
312
|
+
attemptConcurrentIndex,
|
|
313
|
+
});
|
|
294
314
|
});
|
|
295
315
|
}
|
|
296
316
|
}
|
|
@@ -327,6 +347,12 @@ export class FieldsService {
|
|
|
327
347
|
nestedActionEvents.push(actionEvent);
|
|
328
348
|
}
|
|
329
349
|
});
|
|
350
|
+
// concurrent index creation cannot be done inside the transaction
|
|
351
|
+
if (attemptConcurrentIndex && hookAdjustedField.type && ALIAS_TYPES.includes(hookAdjustedField.type) === false) {
|
|
352
|
+
await this.addColumnIndex(collection, hookAdjustedField, {
|
|
353
|
+
attemptConcurrentIndex,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
330
356
|
}
|
|
331
357
|
finally {
|
|
332
358
|
if (runPostColumnChange) {
|
|
@@ -390,20 +416,32 @@ export class FieldsService {
|
|
|
390
416
|
const columnToCompare = opts?.bypassLimits && opts.autoPurgeSystemCache === false ? sanitizeColumn(existingColumn) : existingColumn;
|
|
391
417
|
if (!isEqual(columnToCompare, hookAdjustedField.schema)) {
|
|
392
418
|
try {
|
|
419
|
+
const attemptConcurrentIndex = Boolean(opts?.attemptConcurrentIndex);
|
|
393
420
|
await transaction(this.knex, async (trx) => {
|
|
394
|
-
await trx.schema.alterTable(collection,
|
|
421
|
+
await trx.schema.alterTable(collection, (table) => {
|
|
395
422
|
if (!hookAdjustedField.schema)
|
|
396
423
|
return;
|
|
397
|
-
this.addColumnToTable(table, collection, field,
|
|
424
|
+
this.addColumnToTable(table, collection, field, {
|
|
425
|
+
existing: existingColumn,
|
|
426
|
+
attemptConcurrentIndex,
|
|
427
|
+
});
|
|
398
428
|
});
|
|
399
429
|
});
|
|
430
|
+
// concurrent index creation cannot be done inside the transaction
|
|
431
|
+
if (attemptConcurrentIndex) {
|
|
432
|
+
await this.addColumnIndex(collection, field, {
|
|
433
|
+
existing: existingColumn,
|
|
434
|
+
attemptConcurrentIndex,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
400
437
|
}
|
|
401
438
|
catch (err) {
|
|
402
439
|
throw await translateDatabaseError(err, field);
|
|
403
440
|
}
|
|
404
441
|
}
|
|
405
442
|
}
|
|
406
|
-
if
|
|
443
|
+
// Only create/update a database record if this is not a system field
|
|
444
|
+
if (hookAdjustedField.meta && !isSystemField(collection, hookAdjustedField.field)) {
|
|
407
445
|
if (record) {
|
|
408
446
|
await this.itemsService.updateOne(record.id, {
|
|
409
447
|
...hookAdjustedField.meta,
|
|
@@ -463,11 +501,13 @@ export class FieldsService {
|
|
|
463
501
|
const nestedActionEvents = [];
|
|
464
502
|
try {
|
|
465
503
|
const fieldNames = [];
|
|
504
|
+
const attemptConcurrentIndex = Boolean(opts?.attemptConcurrentIndex);
|
|
466
505
|
for (const field of fields) {
|
|
467
506
|
fieldNames.push(await this.updateField(collection, field, {
|
|
468
507
|
autoPurgeCache: false,
|
|
469
508
|
autoPurgeSystemCache: false,
|
|
470
509
|
bypassEmitAction: (params) => nestedActionEvents.push(params),
|
|
510
|
+
attemptConcurrentIndex,
|
|
471
511
|
}));
|
|
472
512
|
}
|
|
473
513
|
return fieldNames;
|
|
@@ -590,6 +630,22 @@ export class FieldsService {
|
|
|
590
630
|
field: { _eq: field },
|
|
591
631
|
},
|
|
592
632
|
}, { emitEvents: false });
|
|
633
|
+
// cleanup permissions for deleted field
|
|
634
|
+
const permissionRows = await trx
|
|
635
|
+
.select('id', 'collection', 'fields')
|
|
636
|
+
.from('directus_permissions')
|
|
637
|
+
.whereRaw('?? = ? AND ?? LIKE ?', ['collection', collection, 'fields', '%' + field + '%']);
|
|
638
|
+
if (permissionRows.length > 0) {
|
|
639
|
+
for (const permissionRow of permissionRows) {
|
|
640
|
+
const newFields = permissionRow['fields']
|
|
641
|
+
.split(',')
|
|
642
|
+
.filter((v) => v !== field)
|
|
643
|
+
.join(',');
|
|
644
|
+
await trx('directus_permissions')
|
|
645
|
+
.update('fields', newFields.length > 0 ? newFields : null)
|
|
646
|
+
.where('id', '=', permissionRow['id']);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
593
649
|
});
|
|
594
650
|
const actionEvent = {
|
|
595
651
|
event: 'fields.delete',
|
|
@@ -629,11 +685,12 @@ export class FieldsService {
|
|
|
629
685
|
}
|
|
630
686
|
}
|
|
631
687
|
}
|
|
632
|
-
addColumnToTable(table, collection, field,
|
|
688
|
+
addColumnToTable(table, collection, field, options) {
|
|
633
689
|
let column;
|
|
634
690
|
// Don't attempt to add a DB column for alias / corrupt fields
|
|
635
691
|
if (field.type === 'alias' || field.type === 'unknown')
|
|
636
692
|
return;
|
|
693
|
+
const existing = options?.existing ?? null;
|
|
637
694
|
if (field.schema?.has_auto_increment) {
|
|
638
695
|
if (field.type === 'bigInteger') {
|
|
639
696
|
column = table.bigIncrements(field.field);
|
|
@@ -707,28 +764,48 @@ export class FieldsService {
|
|
|
707
764
|
else if (!existing?.is_primary_key) {
|
|
708
765
|
// primary key will already have unique/index constraints
|
|
709
766
|
if (field.schema?.is_unique === true) {
|
|
710
|
-
if (!existing || existing.is_unique === false) {
|
|
767
|
+
if ((!existing || existing.is_unique === false) && !options?.attemptConcurrentIndex) {
|
|
711
768
|
column.unique({ indexName: this.helpers.schema.generateIndexName('unique', collection, field.field) });
|
|
712
769
|
}
|
|
713
770
|
}
|
|
714
|
-
else if (field.schema?.is_unique === false) {
|
|
715
|
-
|
|
716
|
-
table.dropUnique([field.field], this.helpers.schema.generateIndexName('unique', collection, field.field));
|
|
717
|
-
}
|
|
771
|
+
else if (field.schema?.is_unique === false && existing?.is_unique === true) {
|
|
772
|
+
table.dropUnique([field.field], this.helpers.schema.generateIndexName('unique', collection, field.field));
|
|
718
773
|
}
|
|
719
774
|
if (field.schema?.is_indexed === true) {
|
|
720
|
-
if (!existing || existing.is_indexed === false) {
|
|
775
|
+
if ((!existing || existing.is_indexed === false) && !options?.attemptConcurrentIndex) {
|
|
721
776
|
column.index(this.helpers.schema.generateIndexName('index', collection, field.field));
|
|
722
777
|
}
|
|
723
778
|
}
|
|
724
|
-
else if (field.schema?.is_indexed === false) {
|
|
725
|
-
|
|
726
|
-
table.dropIndex([field.field], this.helpers.schema.generateIndexName('index', collection, field.field));
|
|
727
|
-
}
|
|
779
|
+
else if (field.schema?.is_indexed === false && existing?.is_indexed === true) {
|
|
780
|
+
table.dropIndex([field.field], this.helpers.schema.generateIndexName('index', collection, field.field));
|
|
728
781
|
}
|
|
729
782
|
}
|
|
730
783
|
if (existing) {
|
|
731
784
|
column.alter();
|
|
732
785
|
}
|
|
733
786
|
}
|
|
787
|
+
async addColumnIndex(collection, field, options) {
|
|
788
|
+
const attemptConcurrentIndex = Boolean(options?.attemptConcurrentIndex);
|
|
789
|
+
const knex = options?.knex ?? this.knex;
|
|
790
|
+
const existing = options?.existing ?? null;
|
|
791
|
+
// Don't attempt to index a DB column for alias / corrupt fields
|
|
792
|
+
if (field.type === 'alias' || field.type === 'unknown')
|
|
793
|
+
return;
|
|
794
|
+
// primary key will already have unique/index constraints
|
|
795
|
+
if (field.schema?.is_primary_key || existing?.is_primary_key)
|
|
796
|
+
return;
|
|
797
|
+
const helpers = getHelpers(knex);
|
|
798
|
+
if (field.schema?.is_unique === true && (!existing || existing.is_unique == false)) {
|
|
799
|
+
await helpers.schema.createIndex(collection, field.field, {
|
|
800
|
+
unique: true,
|
|
801
|
+
attemptConcurrentIndex,
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
if (field.schema?.is_indexed === true && (!existing || existing.is_indexed === false)) {
|
|
805
|
+
await helpers.schema.createIndex(collection, field.field, {
|
|
806
|
+
unique: false,
|
|
807
|
+
attemptConcurrentIndex,
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
}
|
|
734
811
|
}
|
|
@@ -19,8 +19,8 @@ export async function resolveQuery(gql, info) {
|
|
|
19
19
|
let query;
|
|
20
20
|
const isAggregate = collection.endsWith('_aggregated') && collection in gql.schema.collections === false;
|
|
21
21
|
if (isAggregate) {
|
|
22
|
-
query = await getAggregateQuery(args, selections, gql.schema, gql.accountability);
|
|
23
22
|
collection = collection.slice(0, -11);
|
|
23
|
+
query = await getAggregateQuery(args, selections, gql.schema, gql.accountability, collection);
|
|
24
24
|
}
|
|
25
25
|
else {
|
|
26
26
|
query = await getQuery(args, gql.schema, selections, info.variableValues, gql.accountability, collection);
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import { InvalidPayloadError } from '@directus/errors';
|
|
2
|
+
import { isSystemField } from '@directus/system-data';
|
|
1
3
|
import { GraphQLBoolean, GraphQLID, GraphQLList, GraphQLNonNull, GraphQLString } from 'graphql';
|
|
2
4
|
import { SchemaComposer, toInputObjectType } from 'graphql-compose';
|
|
5
|
+
import { fromZodError } from 'zod-validation-error';
|
|
3
6
|
import { CollectionsService } from '../../collections.js';
|
|
4
7
|
import { ExtensionsService } from '../../extensions.js';
|
|
5
|
-
import { FieldsService } from '../../fields.js';
|
|
8
|
+
import { FieldsService, systemFieldUpdateSchema } from '../../fields.js';
|
|
6
9
|
import { RelationsService } from '../../relations.js';
|
|
7
10
|
import { GraphQLService } from '../index.js';
|
|
11
|
+
import { getCollectionType } from './get-collection-type.js';
|
|
8
12
|
import { getFieldType } from './get-field-type.js';
|
|
9
13
|
import { getRelationType } from './get-relation-type.js';
|
|
10
|
-
import { getCollectionType } from './get-collection-type.js';
|
|
11
14
|
export function resolveSystemAdmin(gql, schema, schemaComposer) {
|
|
12
15
|
if (!gql.accountability?.admin) {
|
|
13
16
|
return;
|
|
@@ -24,13 +27,16 @@ export function resolveSystemAdmin(gql, schema, schemaComposer) {
|
|
|
24
27
|
}).addFields({
|
|
25
28
|
fields: [toInputObjectType(Field, { postfix: '_input' }).NonNull],
|
|
26
29
|
}).NonNull,
|
|
30
|
+
concurrentIndexCreation: { type: GraphQLBoolean, defaultValue: false },
|
|
27
31
|
},
|
|
28
32
|
resolve: async (_, args) => {
|
|
29
33
|
const collectionsService = new CollectionsService({
|
|
30
34
|
accountability: gql.accountability,
|
|
31
35
|
schema: gql.schema,
|
|
32
36
|
});
|
|
33
|
-
const collectionKey = await collectionsService.createOne(args['data']
|
|
37
|
+
const collectionKey = await collectionsService.createOne(args['data'], {
|
|
38
|
+
attemptConcurrentIndex: Boolean(args['concurrentIndexCreation']),
|
|
39
|
+
});
|
|
34
40
|
return await collectionsService.readOne(collectionKey);
|
|
35
41
|
},
|
|
36
42
|
},
|
|
@@ -77,13 +83,16 @@ export function resolveSystemAdmin(gql, schema, schemaComposer) {
|
|
|
77
83
|
args: {
|
|
78
84
|
collection: new GraphQLNonNull(GraphQLString),
|
|
79
85
|
data: toInputObjectType(Field, { postfix: '_input' }).NonNull,
|
|
86
|
+
concurrentIndexCreation: { type: GraphQLBoolean, defaultValue: false },
|
|
80
87
|
},
|
|
81
88
|
resolve: async (_, args) => {
|
|
82
89
|
const service = new FieldsService({
|
|
83
90
|
accountability: gql.accountability,
|
|
84
91
|
schema: gql.schema,
|
|
85
92
|
});
|
|
86
|
-
await service.createField(args['collection'], args['data']
|
|
93
|
+
await service.createField(args['collection'], args['data'], undefined, {
|
|
94
|
+
attemptConcurrentIndex: Boolean(args['concurrentIndexCreation']),
|
|
95
|
+
});
|
|
87
96
|
return await service.readOne(args['collection'], args['data'].field);
|
|
88
97
|
},
|
|
89
98
|
},
|
|
@@ -93,17 +102,52 @@ export function resolveSystemAdmin(gql, schema, schemaComposer) {
|
|
|
93
102
|
collection: new GraphQLNonNull(GraphQLString),
|
|
94
103
|
field: new GraphQLNonNull(GraphQLString),
|
|
95
104
|
data: toInputObjectType(Field, { postfix: '_input' }).NonNull,
|
|
105
|
+
concurrentIndexCreation: { type: GraphQLBoolean, defaultValue: false },
|
|
96
106
|
},
|
|
97
107
|
resolve: async (_, args) => {
|
|
98
108
|
const service = new FieldsService({
|
|
99
109
|
accountability: gql.accountability,
|
|
100
110
|
schema: gql.schema,
|
|
101
111
|
});
|
|
112
|
+
if (isSystemField(args['collection'], args['field'])) {
|
|
113
|
+
const validationResult = systemFieldUpdateSchema.safeParse(args['data']);
|
|
114
|
+
if (!validationResult.success) {
|
|
115
|
+
throw new InvalidPayloadError({ reason: fromZodError(validationResult.error).message });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
102
118
|
await service.updateField(args['collection'], {
|
|
103
119
|
...args['data'],
|
|
104
120
|
field: args['field'],
|
|
121
|
+
}, {
|
|
122
|
+
attemptConcurrentIndex: Boolean(args['concurrentIndexCreation']),
|
|
105
123
|
});
|
|
106
|
-
return await service.readOne(args['collection'], args['
|
|
124
|
+
return await service.readOne(args['collection'], args['field']);
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
update_fields_items: {
|
|
128
|
+
type: Field,
|
|
129
|
+
args: {
|
|
130
|
+
collection: new GraphQLNonNull(GraphQLString),
|
|
131
|
+
data: [toInputObjectType(Field, { postfix: '_input' }).NonNull],
|
|
132
|
+
concurrentIndexCreation: { type: GraphQLBoolean, defaultValue: false },
|
|
133
|
+
},
|
|
134
|
+
resolve: async (_, args) => {
|
|
135
|
+
const service = new FieldsService({
|
|
136
|
+
accountability: gql.accountability,
|
|
137
|
+
schema: gql.schema,
|
|
138
|
+
});
|
|
139
|
+
for (const fieldData of args['data']) {
|
|
140
|
+
if (isSystemField(args['collection'], fieldData['field'])) {
|
|
141
|
+
const validationResult = systemFieldUpdateSchema.safeParse(fieldData);
|
|
142
|
+
if (!validationResult.success) {
|
|
143
|
+
throw new InvalidPayloadError({ reason: fromZodError(validationResult.error).message });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
await service.updateFields(args['collection'], args['data'], {
|
|
148
|
+
attemptConcurrentIndex: Boolean(args['concurrentIndexCreation']),
|
|
149
|
+
});
|
|
150
|
+
return await service.readOne(args['collection'], args['field']);
|
|
107
151
|
},
|
|
108
152
|
},
|
|
109
153
|
delete_fields_item: {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { get, mapKeys, merge, set, uniq } from 'lodash-es';
|
|
2
2
|
import { sanitizeQuery } from '../../../utils/sanitize-query.js';
|
|
3
3
|
import { validateQuery } from '../../../utils/validate-query.js';
|
|
4
|
+
import { filterReplaceM2A, filterReplaceM2ADeep } from '../utils/filter-replace-m2a.js';
|
|
4
5
|
import { replaceFuncs } from '../utils/replace-funcs.js';
|
|
5
6
|
import { parseArgs } from './parse-args.js';
|
|
6
|
-
import { filterReplaceM2A, filterReplaceM2ADeep } from '../utils/filter-replace-m2a.js';
|
|
7
7
|
/**
|
|
8
8
|
* Get a Directus Query object from the parsed arguments (rawQuery) and GraphQL AST selectionSet. Converts SelectionSet into
|
|
9
9
|
* Directus' `fields` query for use in the resolver. Also applies variables where appropriate.
|
|
@@ -52,7 +52,8 @@ export async function getQuery(rawQuery, schema, selections, variableValues, acc
|
|
|
52
52
|
if (selection.selectionSet) {
|
|
53
53
|
if (!query.deep)
|
|
54
54
|
query.deep = {};
|
|
55
|
-
|
|
55
|
+
const path = parent.replaceAll(':', '__');
|
|
56
|
+
set(query.deep, path, merge({}, get(query.deep, parent), { _alias: { [selection.alias.value]: selection.name.value } }));
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
}
|
|
@@ -79,12 +80,11 @@ export async function getQuery(rawQuery, schema, selections, variableValues, acc
|
|
|
79
80
|
fields.push(current);
|
|
80
81
|
}
|
|
81
82
|
if (selection.kind === 'Field' && selection.arguments && selection.arguments.length > 0) {
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
83
|
+
if (!query.deep)
|
|
84
|
+
query.deep = {};
|
|
85
|
+
const args = parseArgs(selection.arguments, variableValues);
|
|
86
|
+
const path = (currentAlias ?? current).replaceAll(':', '__');
|
|
87
|
+
set(query.deep, path, merge({}, get(query.deep, path), mapKeys(await sanitizeQuery(args, schema, accountability), (_value, key) => `_${key}`)));
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
return uniq(fields);
|
|
@@ -3,4 +3,4 @@ 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[], schema: SchemaOverview, accountability?: Accountability | null): Promise<Query>;
|
|
6
|
+
export declare function getAggregateQuery(rawQuery: Query, selections: readonly SelectionNode[], schema: SchemaOverview, accountability?: Accountability | null, collection?: string): Promise<Query>;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { sanitizeQuery } from '../../../utils/sanitize-query.js';
|
|
2
2
|
import { validateQuery } from '../../../utils/validate-query.js';
|
|
3
|
+
import { filterReplaceM2A } from './filter-replace-m2a.js';
|
|
3
4
|
import { replaceFuncs } from './replace-funcs.js';
|
|
4
5
|
/**
|
|
5
6
|
* Resolve the aggregation query based on the requested aggregated fields
|
|
6
7
|
*/
|
|
7
|
-
export async function getAggregateQuery(rawQuery, selections, schema, accountability) {
|
|
8
|
+
export async function getAggregateQuery(rawQuery, selections, schema, accountability, collection) {
|
|
8
9
|
const query = await sanitizeQuery(rawQuery, schema, accountability);
|
|
9
10
|
query.aggregate = {};
|
|
10
11
|
for (let aggregationGroup of selections) {
|
|
@@ -27,6 +28,9 @@ export async function getAggregateQuery(rawQuery, selections, schema, accountabi
|
|
|
27
28
|
if (query.filter) {
|
|
28
29
|
query.filter = replaceFuncs(query.filter);
|
|
29
30
|
}
|
|
31
|
+
if (collection && query.filter) {
|
|
32
|
+
query.filter = filterReplaceM2A(query.filter, collection, schema);
|
|
33
|
+
}
|
|
30
34
|
validateQuery(query);
|
|
31
35
|
return query;
|
|
32
36
|
}
|
|
@@ -48,7 +48,8 @@ export function filterReplaceM2ADeep(deep_arg, collection, schema) {
|
|
|
48
48
|
deep[key] = filterReplaceM2ADeep(deep[key], relation.related_collection, schema);
|
|
49
49
|
}
|
|
50
50
|
else if (type === 'a2o' && any_collection && relation.meta?.one_allowed_collections?.includes(any_collection)) {
|
|
51
|
-
deep[
|
|
51
|
+
deep[`${field}:${any_collection}`] = filterReplaceM2ADeep(deep[key], any_collection, schema);
|
|
52
|
+
delete deep[key];
|
|
52
53
|
}
|
|
53
54
|
}
|
|
54
55
|
if (key === '_filter') {
|
|
@@ -1,7 +1,15 @@
|
|
|
1
|
-
import type { AbstractServiceOptions, Accountability, ExportFormat, File, Query, SchemaOverview } from '@directus/types';
|
|
1
|
+
import type { AbstractServiceOptions, Accountability, DirectusError, ExportFormat, File, Query, SchemaOverview } from '@directus/types';
|
|
2
2
|
import type { Knex } from 'knex';
|
|
3
3
|
import type { Readable } from 'node:stream';
|
|
4
4
|
import type { FunctionFieldNode, FieldNode, NestedCollectionNode } from '../types/index.js';
|
|
5
|
+
export declare function createErrorTracker(): {
|
|
6
|
+
addCapturedError: (err: any, rowNumber: number) => void;
|
|
7
|
+
buildFinalErrors: () => DirectusError<any>[];
|
|
8
|
+
getCount: () => number;
|
|
9
|
+
hasErrors: () => boolean;
|
|
10
|
+
shouldStop: () => boolean;
|
|
11
|
+
hasGenericError: () => boolean;
|
|
12
|
+
};
|
|
5
13
|
export declare class ImportService {
|
|
6
14
|
knex: Knex;
|
|
7
15
|
accountability: Accountability | null;
|