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 754dee5
Show file tree
Hide file tree
Showing 15 changed files with 290 additions and 106 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
46 changes: 46 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 @@ -478,6 +499,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',
},
},
],
},
{
Expand Down Expand Up @@ -967,6 +1000,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 +1023,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;
});
});
20 changes: 1 addition & 19 deletions spec/model/implementation/object-type.spec.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.`);
});
});
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
Loading

0 comments on commit 754dee5

Please sign in to comment.