diff --git a/spec/dev/model/simple.graphqls b/spec/dev/model/simple.graphqls index f95dd0a5..b06b23bc 100644 --- a/spec/dev/model/simple.graphqls +++ b/spec/dev/model/simple.graphqls @@ -9,6 +9,7 @@ type Hero ] indices: [{ fields: "missions.date" }] ) { + createdAt: DateTime @hidden "The hero's screen name" name: String @unique @flexSearch @flexSearchFulltext(includeInSearch: true) @accessField knownName: I18nString @flexSearch @flexSearchFulltext @@ -16,7 +17,7 @@ type Hero nickNames: [String] @flexSearch(caseSensitive: false) movies: [Movie] @relation(inverseOf: "heroes") skills: [Skill] @flexSearch - suit: Suit @flexSearch + suit: Suit @flexSearch @hidden morality: Morality countryISOCode: String country: Country @reference(keyField: "countryISOCode") diff --git a/spec/dev/server.ts b/spec/dev/server.ts index 8c8a95d9..94ef99ec 100644 --- a/spec/dev/server.ts +++ b/spec/dev/server.ts @@ -7,7 +7,6 @@ import { ArangoDBAdapter, Project } from '../..'; import { globalContext } from '../../src/config/global'; import { InMemoryAdapter } from '../../src/database/inmemory'; import { getMetaSchema } from '../../src/meta-schema/meta-schema'; -import { Model } from '../../src/model'; import { loadProjectFromDir } from '../../src/project/project-from-fs'; import { Log4jsLoggerProvider } from '../helpers/log4js-logger-provider'; import { createFastApp } from './fast-server'; diff --git a/spec/meta-schema/meta-schema.spec.ts b/spec/meta-schema/meta-schema.spec.ts index d381e118..6fb5b7b0 100644 --- a/spec/meta-schema/meta-schema.spec.ts +++ b/spec/meta-schema/meta-schema.spec.ts @@ -173,6 +173,17 @@ describe('Meta schema API', () => { } `; + const hiddenFieldsQuery = gql` + { + rootEntityType(name: "Country") { + fields { + name + isHidden + } + } + } + `; + const permissionsQuery = gql` { rootEntityType(name: "Shipment") { @@ -227,6 +238,16 @@ describe('Meta schema API', () => { { name: 'isoCode', typeName: 'String', + isHidden: true, + }, + { + name: 'id', + typeName: 'ID', + isHidden: true, + }, + { + name: 'dummy', + typeName: 'String', }, ], namespacePath: ['generic'], @@ -572,6 +593,18 @@ describe('Meta schema API', () => { collectFieldConfig: null, type: { __typename: 'ScalarType' }, }, + { + collectFieldConfig: null, + isCollectField: false, + isList: false, + isReference: false, + isRelation: false, + name: 'dummy', + referenceKeyField: null, + type: { + __typename: 'ScalarType', + }, + }, ], }, { @@ -1159,6 +1192,21 @@ describe('Meta schema API', () => { expect(actualVersion).to.deep.equal(expectedVersion); }); + it('can query read whether fields are hidden', async () => { + const result = (await execute(hiddenFieldsQuery)) as any; + const rootEntityTypeFields = result.rootEntityType.fields; + const isoCodeField = rootEntityTypeFields.find((field: any) => field.name === 'isoCode'); + const idField = rootEntityTypeFields.find((field: any) => field.name === 'id'); + const dummyField = rootEntityTypeFields.find((field: any) => field.name === 'dummy'); + const updatedAtField = rootEntityTypeFields.find( + (field: any) => field.name === 'updatedAt', + ); + expect(isoCodeField.isHidden).to.be.true; + expect(idField.isHidden).to.be.true; + expect(dummyField.isHidden).to.be.false; + expect(updatedAtField.isHidden).to.be.false; + }); + it('can query read the cruddl version from meta description', async () => { const expectedVersion = CRUDDL_VERSION; const result = (await execute(cruddlVersionIntrospectionQuery)) as any; diff --git a/spec/model/create-model.spec.ts b/spec/model/create-model.spec.ts index f41d8f83..f62c00fa 100644 --- a/spec/model/create-model.spec.ts +++ b/spec/model/create-model.spec.ts @@ -1,7 +1,6 @@ import { expect } from 'chai'; import { DocumentNode } from 'graphql'; import gql from 'graphql-tag'; -import { createModel } from '../../src/model'; import { createSimpleModel } from './model-spec.helper'; describe('createModel', () => { @@ -353,4 +352,28 @@ describe('createModel', () => { en: 'Delivery via ship', }); }); + + it('it allows to apply the hidden directive on regular and system fields', () => { + const document: DocumentNode = gql` + type Test @rootEntity { + id: ID @hidden + updatedAt: DateTime @hidden + regularField: String! + test2: Test2 @relation @hidden + } + + type Test2 @rootEntity { + dummy: String + } + `; + + const model = createSimpleModel(document); + expect(model.validate().getErrors(), model.validate().toString()).to.deep.equal([]); + + const type = model.getRootEntityTypeOrThrow('Test'); + expect(type.getFieldOrThrow('id').isHidden).to.be.true; + expect(type.getFieldOrThrow('updatedAt').isHidden).to.be.true; + expect(type.getFieldOrThrow('createdAt').isHidden).to.be.false; + expect(type.getFieldOrThrow('test2').isHidden).to.be.true; + }); }); diff --git a/spec/model/implementation/object-type.spec.ts b/spec/model/implementation/object-type.spec.ts index 7953a279..d4a566c3 100644 --- a/spec/model/implementation/object-type.spec.ts +++ b/spec/model/implementation/object-type.spec.ts @@ -1,5 +1,5 @@ import { ChildEntityType, Model, Severity, TypeKind } from '../../../src/model'; -import { expectSingleErrorToInclude, expectToBeValid, validate } from './validation-utils'; +import { expectToBeValid, validate } from './validation-utils'; import { expect } from 'chai'; // This test uses a ChildEntityType because that is a concrete class without much addition to ObjectType, but it @@ -57,22 +57,4 @@ describe('ObjectType', () => { expect(message.message).to.equal(`Duplicate field name: "deliveryNumber".`); } }); - - it('rejects type with reserved field names', () => { - const type = new ChildEntityType( - { - kind: TypeKind.CHILD_ENTITY, - name: 'Delivery', - fields: [ - { - name: 'updatedAt', - typeName: 'String', - }, - ], - }, - model, - ); - - expectSingleErrorToInclude(type, `Field name "updatedAt" is reserved by a system field.`); - }); }); diff --git a/spec/schema/ast-validation-modules/key-field-validator.spec.ts b/spec/schema/ast-validation-modules/key-field-validator.spec.ts index 49dfb801..658be1b7 100644 --- a/spec/schema/ast-validation-modules/key-field-validator.spec.ts +++ b/spec/schema/ast-validation-modules/key-field-validator.spec.ts @@ -108,18 +108,6 @@ describe('key field validator', () => { `); }); - it('warns about id: ID (without @key)', () => { - assertValidatorWarns( - ` - type Stuff @rootEntity { - id: ID - test: String - } - `, - 'The field "id" is redundant and should only be explicitly added when used with @key.', - ); - }); - it('warns about _key: String (without @key)', () => { assertValidatorWarns( ` @@ -132,30 +120,6 @@ describe('key field validator', () => { ); }); - it('rejects id: String @key (wrong type)', () => { - assertValidatorRejects( - ` - type Stuff @rootEntity { - id: String @key - test: String - } - `, - 'The field "id" must be of type "ID".', - ); - }); - - it('rejects id: String (wrong type, without @key)', () => { - assertValidatorRejects( - ` - type Stuff @rootEntity { - id: String - test: String - } - `, - 'The field "id" must be of type "ID".', - ); - }); - it('rejects _key: String (without @key)', () => { assertValidatorRejects( ` diff --git a/spec/schema/ast-validation-modules/system-field-override.spec.ts b/spec/schema/ast-validation-modules/system-field-override.spec.ts new file mode 100644 index 00000000..af61933f --- /dev/null +++ b/spec/schema/ast-validation-modules/system-field-override.spec.ts @@ -0,0 +1,111 @@ +import { print } from 'graphql'; +import gql from 'graphql-tag'; +import { + assertValidatorAcceptsAndDoesNotWarn, + assertValidatorRejects, + assertValidatorWarns, +} from './helpers'; + +describe('system field override validation', () => { + it('is valid on non redundant system fields', () => { + assertValidatorAcceptsAndDoesNotWarn( + print(gql` + type Root @rootEntity { + id: ID @key + createdAt: DateTime @hidden + dummy: String + } + + type Root2 @rootEntity { + id: ID @hidden + updatedAt: DateTime @hidden + dummy: String + } + `), + ); + }); + + it('warns on redundant system field "id"', () => { + assertValidatorWarns( + print(gql` + type Root @rootEntity { + id: ID + dummy: String + } + `), + 'Manually declaring system field "id" is redundant. Either add a suitable directive or consider removing the field', + ); + }); + + it('warns on redundant system field "createdAt"', () => { + assertValidatorWarns( + print(gql` + type Root @rootEntity { + createdAt: DateTime + dummy: String + } + `), + 'Manually declaring system field "createdAt" is redundant. Either add a suitable directive or consider removing the field', + ); + }); + + it('warns on redundant system field "updatedAt"', () => { + assertValidatorWarns( + print(gql` + type Root @rootEntity { + updatedAt: DateTime + dummy: String + } + `), + 'Manually declaring system field "updatedAt" is redundant. Either add a suitable directive or consider removing the field', + ); + }); + + it('errors on system field "id" type mismatch', () => { + assertValidatorRejects( + print(gql` + type Root @rootEntity { + id: String + dummy: String + } + `), + 'System field "id" must be of type "ID"', + ); + }); + + it('errors on system field "createdAt" type mismatch', () => { + assertValidatorRejects( + print(gql` + type Root @rootEntity { + createdAt: String + dummy: String + } + `), + 'System field "createdAt" must be of type "DateTime"', + ); + }); + + it('errors on system field "updatedAt" type mismatch', () => { + assertValidatorRejects( + print(gql` + type Root @rootEntity { + updatedAt: String + dummy: String + } + `), + 'System field "updatedAt" must be of type "DateTime"', + ); + }); + + it('errors on not allowed directives on system fields', () => { + assertValidatorRejects( + print(gql` + type Root @rootEntity { + id: ID @relation + dummy: String + } + `), + 'Directive "@relation" is not allowed on system field "id" and will be discarded', + ); + }); +}); diff --git a/src/meta-schema/meta-schema.ts b/src/meta-schema/meta-schema.ts index 5a47ebcf..9f5fa6db 100644 --- a/src/meta-schema/meta-schema.ts +++ b/src/meta-schema/meta-schema.ts @@ -8,15 +8,14 @@ import { getPermissionDescriptorOfField, getPermissionDescriptorOfRootEntityType, } from '../authorization/permission-descriptors-in-model'; +import { CRUDDL_VERSION } from '../cruddl-version'; import { ExecutionOptionsCallbackArgs } from '../execution/execution-options'; import { EnumValue, Field, RootEntityType, Type, TypeKind } from '../model'; import { OrderDirection } from '../model/implementation/order'; import { Project } from '../project/project'; +import { GraphQLI18nString } from '../schema/scalars/string-map'; import { compact, flatMap } from '../utils/utils'; import { I18N_GENERIC, I18N_LOCALE } from './constants'; -import { CRUDDL_VERSION } from '../cruddl-version'; -import { mapValues } from 'lodash'; -import { GraphQLI18nString } from '../schema/scalars/string-map'; const resolutionOrderDescription = JSON.stringify( 'The order in which languages and other localization providers are queried for a localization. You can specify languages as defined in the schema as well as the following special identifiers:\n\n- `_LOCALE`: The language defined by the GraphQL request (might be a list of languages, e.g. ["de_DE", "de", "en"])\n- `_GENERIC`: is auto-generated localization from field and type names (e. G. `orderDate` => `Order date`)\n\nThe default `resolutionOrder` is `["_LOCALE", "_GENERIC"]` (if not specified).', @@ -93,6 +92,7 @@ const typeDefs = gql` isIncludedInSearch: Boolean! isFlexSearchFulltextIndexed: Boolean! isFulltextIncludedInSearch: Boolean! + isHidden: Boolean! flexSearchLanguage: FlexSearchLanguage permissions: FieldPermissions diff --git a/src/model/config/field.ts b/src/model/config/field.ts index ca8052f2..d7612faf 100644 --- a/src/model/config/field.ts +++ b/src/model/config/field.ts @@ -66,6 +66,14 @@ export interface FieldConfig { readonly isAccessField?: boolean; readonly accessFieldDirectiveASTNode?: DirectiveNode; + + /** + * Whether a field is marked as "hidden". This information can later be used, + * via the fields meta information, to decide whether the field should be shown in UIs + * or not. + */ + readonly isHidden?: boolean; + readonly isHiddenASTNode?: DirectiveNode; } export enum RelationDeleteAction { diff --git a/src/model/create-model.ts b/src/model/create-model.ts index 96697d46..a59e21a7 100644 --- a/src/model/create-model.ts +++ b/src/model/create-model.ts @@ -6,13 +6,11 @@ import { FieldDefinitionNode, GraphQLBoolean, GraphQLEnumType, - GraphQLID, GraphQLInputObjectType, GraphQLInt, GraphQLList, GraphQLNonNull, GraphQLString, - IntValueNode, Kind, ObjectTypeDefinitionNode, ObjectValueNode, @@ -49,6 +47,7 @@ import { FLEX_SEARCH_INDEXED_LANGUAGE_ARG, FLEX_SEARCH_ORDER_ARGUMENT, FLEX_SEARCH_PERFORMANCE_PARAMS_ARGUMENT, + HIDDEN_DIRECTIVE, ID_FIELD, INDEX_DEFINITION_INPUT_TYPE, INDEX_DIRECTIVE, @@ -282,6 +281,7 @@ function processKeyField( if (underscoreKeyField) { fields = fields.filter((f) => f !== underscoreKeyField); if (keyFieldASTNode && keyFieldASTNode.name.value === underscoreKeyField.name) { + // keyFieldName needs to be "id" if the @key directive is set on the _key field keyFieldASTNode = underscoreKeyField.astNode; keyFieldName = ID_FIELD; context.addMessage( @@ -299,26 +299,7 @@ function processKeyField( ); } } - const idField = fields.find((field) => field.name == ID_FIELD); - if (idField) { - fields = fields.filter((f) => f !== idField); - if (keyFieldASTNode && keyFieldASTNode.name.value === idField.name) { - keyFieldASTNode = idField.astNode; - keyFieldName = ID_FIELD; - } else { - context.addMessage( - ValidationMessage.warn( - `The field "id" is redundant and should only be explicitly added when used with @key.`, - idField.astNode, - ), - ); - } - if (idField.typeName !== GraphQLID.name || idField.isList) { - context.addMessage( - ValidationMessage.error(`The field "id" must be of type "ID".`, idField.astNode), - ); - } - } + return { fields, keyFieldASTNode, keyFieldName }; } @@ -624,6 +605,7 @@ function createFieldInput( context, ); const accessFieldDirectiveASTNode = findDirectiveWithName(fieldNode, ACCESS_FIELD_DIRECTIVE); + const hiddenDirectiveASTNode = findDirectiveWithName(fieldNode, HIDDEN_DIRECTIVE); return { name: fieldNode.name.value, @@ -676,6 +658,8 @@ function createFieldInput( collect: getCollectConfig(fieldNode, context), isAccessField: !!accessFieldDirectiveASTNode, accessFieldDirectiveASTNode, + isHidden: !!hiddenDirectiveASTNode, + isHiddenASTNode: hiddenDirectiveASTNode, }; } diff --git a/src/model/implementation/child-entity-type.ts b/src/model/implementation/child-entity-type.ts index 73ac4166..68077386 100644 --- a/src/model/implementation/child-entity-type.ts +++ b/src/model/implementation/child-entity-type.ts @@ -1,9 +1,9 @@ import { ID_FIELD } from '../../schema/constants'; +import { ChildEntityTypeConfig, TypeKind } from '../config'; import { ValidationContext, ValidationMessage } from '../validation'; import { Field, SystemFieldConfig } from './field'; -import { ObjectTypeBase } from './object-type-base'; -import { ChildEntityTypeConfig, FieldConfig, TypeKind } from '../config'; import { Model } from './model'; +import { ObjectTypeBase } from './object-type-base'; export class ChildEntityType extends ObjectTypeBase { constructor(input: ChildEntityTypeConfig, model: Model) { @@ -50,6 +50,7 @@ const systemFieldInputs: ReadonlyArray = [ isFlexSearchIndexed: true, isFlexSearchFulltextIndexed: false, isIncludedInSearch: false, + allowedDirectiveNames: ['hidden'], }, { name: 'createdAt', @@ -59,6 +60,7 @@ const systemFieldInputs: ReadonlyArray = [ isFlexSearchIndexed: true, isFlexSearchFulltextIndexed: false, isIncludedInSearch: false, + allowedDirectiveNames: ['hidden'], }, { name: 'updatedAt', @@ -68,5 +70,6 @@ const systemFieldInputs: ReadonlyArray = [ isFlexSearchIndexed: true, isFlexSearchFulltextIndexed: false, isIncludedInSearch: false, + allowedDirectiveNames: ['hidden'], }, ]; diff --git a/src/model/implementation/field.ts b/src/model/implementation/field.ts index bafd71ea..c5279a74 100644 --- a/src/model/implementation/field.ts +++ b/src/model/implementation/field.ts @@ -46,6 +46,7 @@ import { ValueObjectType } from './value-object-type'; export interface SystemFieldConfig extends FieldConfig { readonly isSystemField?: boolean; readonly isNonNull?: boolean; + readonly allowedDirectiveNames?: ReadonlyArray; } export class Field implements ModelComponent { @@ -65,6 +66,7 @@ export class Field implements ModelComponent { readonly isParentField: boolean; readonly isRootField: boolean; readonly roles: RolesSpecifier | undefined; + readonly isHidden: boolean; private _type: Type | undefined; /** @@ -109,6 +111,7 @@ export class Field implements ModelComponent { : undefined; this.isSystemField = input.isSystemField || false; this.isAccessField = input.isAccessField ?? false; + this.isHidden = !!input.isHidden; } /** diff --git a/src/model/implementation/object-type-base.ts b/src/model/implementation/object-type-base.ts index 00b320a2..62c075cb 100644 --- a/src/model/implementation/object-type-base.ts +++ b/src/model/implementation/object-type-base.ts @@ -1,7 +1,7 @@ import { groupBy } from 'lodash'; import { objectValues } from '../../utils/utils'; -import { ObjectTypeConfig } from '../config'; -import { ValidationContext, ValidationMessage } from '../validation'; +import { FieldConfig, ObjectTypeConfig } from '../config'; +import { Severity, ValidationContext, ValidationMessage } from '../validation'; import { Field, SystemFieldConfig } from './field'; import { Model } from './model'; import { ObjectType } from './type'; @@ -10,6 +10,9 @@ import { TypeBase } from './type-base'; export abstract class ObjectTypeBase extends TypeBase { readonly fields: ReadonlyArray; private readonly fieldMap: ReadonlyMap; + readonly systemFieldOverrides: ReadonlyMap; + readonly systemFields: ReadonlyMap; + readonly systemFieldConfigs: ReadonlyMap; protected constructor( input: ObjectTypeConfig, @@ -18,12 +21,39 @@ export abstract class ObjectTypeBase extends TypeBase { ) { super(input, model); const thisAsObjectType: ObjectType = this as any; - const customFields = (input.fields || []).map( - (field) => new Field(field, thisAsObjectType), + + this.systemFieldConfigs = new Map( + systemFieldInputs.map((systemFieldInput) => [systemFieldInput.name, systemFieldInput]), + ); + + this.systemFieldOverrides = new Map( + (input.fields || []) + .filter((customField) => + systemFieldInputs.some((systemField) => systemField.name === customField.name), + ) + .map((field) => [field.name, field]), ); + + const customFields = (input.fields || []) + .filter((customField) => !this.systemFieldOverrides.has(customField.name)) + .map((field) => new Field(field, thisAsObjectType)); + const systemFields = (systemFieldInputs || []).map( - (input) => new Field({ ...input, isSystemField: true }, thisAsObjectType), + (input) => + new Field( + { + ...input, + isSystemField: true, + ...this.systemFieldOverrideToSystemFieldConfig(input), + }, + thisAsObjectType, + ), ); + + this.systemFields = new Map( + systemFields.map((systemField) => [systemField.name, systemField]), + ); + this.fields = [...systemFields, ...customFields]; this.fieldMap = new Map(this.fields.map((field): [string, Field] => [field.name, field])); } @@ -41,40 +71,71 @@ export abstract class ObjectTypeBase extends TypeBase { } this.validateDuplicateFields(context); + this.validateSystemFieldOverrides(context); for (const field of this.fields) { field.validate(context); } } + private validateSystemFieldOverrides(context: ValidationContext): void { + for (const systemFieldOverride of this.systemFieldOverrides.values()) { + const systemField = this.getSystemFieldOrThrow(systemFieldOverride.name); + if (systemField.type.name !== systemFieldOverride.typeName) { + context.addMessage( + new ValidationMessage( + Severity.Error, + `System field "${systemField.name}" must be of type "${systemField.type.name}"`, + systemField.astNode, + ), + ); + } + + if (!systemFieldOverride.astNode?.directives?.length) { + context.addMessage( + new ValidationMessage( + Severity.Warning, + `Manually declaring system field "${systemField.name}" is redundant. Either add a suitable directive or consider removing the field`, + systemField.astNode, + ), + ); + } + + const allowedSystemFieldDirectives = + this.systemFieldConfigs.get(systemFieldOverride.name)?.allowedDirectiveNames ?? []; + const forbiddenSystemFieldDirectives = + systemFieldOverride.astNode?.directives?.filter( + (directive) => !allowedSystemFieldDirectives.includes(directive.name.value), + ) ?? []; + for (const forbiddenDirective of forbiddenSystemFieldDirectives) { + context.addMessage( + new ValidationMessage( + Severity.Error, + `Directive "@${forbiddenDirective.name.value}" is not allowed on system field "${systemFieldOverride.name}" and will be discarded`, + forbiddenDirective, + ), + ); + } + } + } + private validateDuplicateFields(context: ValidationContext) { const duplicateFields = objectValues(groupBy(this.fields, (field) => field.name)).filter( (fields) => fields.length > 1, ); for (const fields of duplicateFields) { - const isSystemFieldCollision = fields.some((field) => field.isSystemField); for (const field of fields) { if (field.isSystemField) { // don't report errors for system fields the user didn't even write continue; } - if (isSystemFieldCollision) { - // user does not see duplicate field, so provide better message - context.addMessage( - ValidationMessage.error( - `Field name "${field.name}" is reserved by a system field.`, - field.astNode, - ), - ); - } else { - context.addMessage( - ValidationMessage.error( - `Duplicate field name: "${field.name}".`, - field.astNode, - ), - ); - } + context.addMessage( + ValidationMessage.error( + `Duplicate field name: "${field.name}".`, + field.astNode, + ), + ); } } } @@ -91,6 +152,25 @@ export abstract class ObjectTypeBase extends TypeBase { return field; } + getSystemFieldOrThrow(name: string): Field { + const field = this.systemFields.get(name); + if (!field) { + throw new Error(`System field ${name} not found`); + } + return field; + } + + private systemFieldOverrideToSystemFieldConfig(fieldConfig: FieldConfig): Partial { + const override = this.systemFieldOverrides.get(fieldConfig.name); + if (!override) { + return {}; + } + return { + isHidden: !!override.isHidden, + isHiddenASTNode: override.isHiddenASTNode, + }; + } + readonly isObjectType: true = true; readonly isScalarType: false = false; readonly isEnumType: false = false; diff --git a/src/model/implementation/root-entity-type.ts b/src/model/implementation/root-entity-type.ts index aefe266e..f46b141b 100644 --- a/src/model/implementation/root-entity-type.ts +++ b/src/model/implementation/root-entity-type.ts @@ -18,7 +18,7 @@ import { FlexSearchPrimarySortClauseConfig, PermissionsConfig, RootEntityTypeConfig, - TypeKind, + TypeKind } from '../config'; import { ValidationContext, ValidationMessage } from '../validation'; import { Field, SystemFieldConfig } from './field'; @@ -525,6 +525,7 @@ const systemFieldInputs: ReadonlyArray = [ isFlexSearchIndexed: true, isFlexSearchFulltextIndexed: false, isIncludedInSearch: false, + allowedDirectiveNames: ['key', 'hidden'], }, { name: 'createdAt', @@ -534,6 +535,7 @@ const systemFieldInputs: ReadonlyArray = [ isFlexSearchIndexed: true, isFlexSearchFulltextIndexed: false, isIncludedInSearch: false, + allowedDirectiveNames: ['hidden'], }, { name: 'updatedAt', @@ -544,5 +546,6 @@ const systemFieldInputs: ReadonlyArray = [ isFlexSearchIndexed: true, isFlexSearchFulltextIndexed: false, isIncludedInSearch: false, + allowedDirectiveNames: ['hidden'], }, ]; diff --git a/src/schema/constants.ts b/src/schema/constants.ts index fe856a26..5a06ae8f 100644 --- a/src/schema/constants.ts +++ b/src/schema/constants.ts @@ -1,7 +1,4 @@ -import { GraphQLInt } from 'graphql'; import { numberTypeNames } from '../model/implementation/built-in-types'; -import { GraphQLDecimal1, GraphQLDecimal2 } from './scalars/fixed-point-decimals'; -import { GraphQLInt53 } from './scalars/int53'; export const WILDCARD_CHARACTER = '*'; @@ -24,6 +21,8 @@ export const ACCESS_FIELD_DIRECTIVE = 'accessField'; export const FLEX_SEARCH_INDEXED_DIRECTIVE = 'flexSearch'; export const FLEX_SEARCH_FULLTEXT_INDEXED_DIRECTIVE = 'flexSearchFulltext'; +export const HIDDEN_DIRECTIVE = 'hidden'; + export const FLEX_SEARCH_INDEXED_ARGUMENT = 'flexSearch'; export const FLEX_SEARCH_ORDER_ARGUMENT = 'flexSearchOrder'; export const FLEX_SEARCH_PERFORMANCE_PARAMS_ARGUMENT = 'flexSearchPerformanceParams'; diff --git a/src/schema/graphql-base.ts b/src/schema/graphql-base.ts index 39c06159..54e34ceb 100644 --- a/src/schema/graphql-base.ts +++ b/src/schema/graphql-base.ts @@ -242,6 +242,11 @@ export const DIRECTIVES: DocumentNode = gql` "Annotates a field so it can be used within restrictions of a permission profile" directive @accessField on FIELD_DEFINITION + + """ + Marks this field as hidden in the meta schema so it will not be listed in generic UIs + """ + directive @hidden on FIELD_DEFINITION `; export const CORE_SCALARS: DocumentNode = gql`