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 28, 2024
1 parent d619fbc commit f3aa37a
Show file tree
Hide file tree
Showing 16 changed files with 324 additions and 111 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
48 changes: 48 additions & 0 deletions spec/meta-schema/meta-schema.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,17 @@ describe('Meta schema API', () => {
}
`;

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

const permissionsQuery = gql`
{
rootEntityType(name: "Shipment") {
Expand Down Expand Up @@ -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'],
Expand Down Expand Up @@ -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',
},
},
],
},
{
Expand Down Expand Up @@ -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;
Expand Down
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 @@ -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;
});
});
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
111 changes: 111 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,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',
);
});
});
6 changes: 3 additions & 3 deletions src/meta-schema/meta-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).',
Expand Down Expand Up @@ -93,6 +92,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 f3aa37a

Please sign in to comment.