Skip to content

Commit

Permalink
feat: add @hidden directive on fields
Browse files Browse the repository at this point in the history
  • Loading branch information
Etienne-Buschong committed Mar 20, 2024
1 parent 6b45cac commit e3e882a
Show file tree
Hide file tree
Showing 14 changed files with 277 additions and 87 deletions.
3 changes: 2 additions & 1 deletion spec/dev/model/simple.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ 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
age: Int @defaultValue(value: 42) @flexSearch
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")
Expand Down
1 change: 0 additions & 1 deletion spec/dev/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
34 changes: 34 additions & 0 deletions spec/meta-schema/meta-schema.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,17 @@ describe('Meta schema API', () => {
}
`;

const hiddenFieldsQuery = gql`
{
rootEntityType(name: "Country") {
fields {
name
isHidden
}
}
}
`;

const permissionsQuery = gql`
{
rootEntityType(name: "Shipment") {
Expand Down Expand Up @@ -197,6 +208,16 @@ describe('Meta schema API', () => {
{
name: 'isoCode',
typeName: 'String',
isHidden: true,
},
{
name: 'id',
typeName: 'ID',
isHidden: true,
},
{
name: 'dummy',
typeName: 'String',
},
],
namespacePath: ['generic'],
Expand Down Expand Up @@ -967,6 +988,17 @@ 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');
expect(isoCodeField.isHidden).to.be.true;
expect(idField.isHidden).to.be.true;
expect(dummyField.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;
Expand All @@ -979,4 +1011,6 @@ describe('Meta schema API', () => {
after(function () {
return stopMetaServer();
});

it;
});
25 changes: 24 additions & 1 deletion spec/model/create-model.spec.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -185,4 +184,28 @@ describe('createModel', () => {
expect(field.isFlexSearchIndexed).to.be.true;
expect(field.isIncludedInSearch).to.be.false;
});

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;
});
});
36 changes: 0 additions & 36 deletions spec/schema/ast-validation-modules/key-field-validator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
`
Expand All @@ -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(
`
Expand Down
99 changes: 99 additions & 0 deletions spec/schema/ast-validation-modules/system-field-override.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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"',
);
});
});
3 changes: 2 additions & 1 deletion src/meta-schema/meta-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { makeExecutableSchema } from '@graphql-tools/schema';
import { IResolvers } from '@graphql-tools/utils';
import { GraphQLResolveInfo, GraphQLSchema } from 'graphql';
import gql from 'graphql-tag';
import { AccessOperation, AuthContext } from '../authorization/auth-basics';
import { AccessOperation } from '../authorization/auth-basics';
import { PermissionResult } from '../authorization/permission-descriptors';
import {
getPermissionDescriptorOfField,
Expand Down Expand Up @@ -85,6 +85,7 @@ const typeDefs = gql`
isIncludedInSearch: Boolean!
isFlexSearchFulltextIndexed: Boolean!
isFulltextIncludedInSearch: Boolean!
isHidden: Boolean!
flexSearchLanguage: FlexSearchLanguage
permissions: FieldPermissions
Expand Down
8 changes: 8 additions & 0 deletions src/model/config/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
27 changes: 5 additions & 22 deletions src/model/create-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@ import {
FieldDefinitionNode,
GraphQLBoolean,
GraphQLEnumType,
GraphQLID,
GraphQLInputObjectType,
GraphQLInt,
GraphQLList,
GraphQLNonNull,
GraphQLString,
IntValueNode,
Kind,
ObjectTypeDefinitionNode,
ObjectValueNode,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -299,26 +298,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 };
}

Expand Down Expand Up @@ -624,6 +604,7 @@ function createFieldInput(
context,
);
const accessFieldDirectiveASTNode = findDirectiveWithName(fieldNode, ACCESS_FIELD_DIRECTIVE);
const hiddenDirectiveASTNode = findDirectiveWithName(fieldNode, HIDDEN_DIRECTIVE);

return {
name: fieldNode.name.value,
Expand Down Expand Up @@ -676,6 +657,8 @@ function createFieldInput(
collect: getCollectConfig(fieldNode, context),
isAccessField: !!accessFieldDirectiveASTNode,
accessFieldDirectiveASTNode,
isHidden: !!hiddenDirectiveASTNode,
isHiddenASTNode: hiddenDirectiveASTNode,
};
}

Expand Down
2 changes: 2 additions & 0 deletions src/model/implementation/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export class Field implements ModelComponent {
readonly isParentField: boolean;
readonly isRootField: boolean;
readonly roles: RolesSpecifier | undefined;
readonly isHidden: boolean;
private _type: Type | undefined;

/**
Expand Down Expand Up @@ -109,6 +110,7 @@ export class Field implements ModelComponent {
: undefined;
this.isSystemField = input.isSystemField || false;
this.isAccessField = input.isAccessField ?? false;
this.isHidden = !!input.isHidden;
}

/**
Expand Down
Loading

0 comments on commit e3e882a

Please sign in to comment.