Skip to content

Commit 86ce3ca

Browse files
feat: add @hidden directive on fields
1 parent d619fbc commit 86ce3ca

File tree

16 files changed

+324
-111
lines changed

16 files changed

+324
-111
lines changed

spec/dev/model/simple.graphqls

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,15 @@ type Hero
99
]
1010
indices: [{ fields: "missions.date" }]
1111
) {
12+
createdAt: DateTime @hidden
1213
"The hero's screen name"
1314
name: String @unique @flexSearch @flexSearchFulltext(includeInSearch: true) @accessField
1415
knownName: I18nString @flexSearch @flexSearchFulltext
1516
age: Int @defaultValue(value: 42) @flexSearch
1617
nickNames: [String] @flexSearch(caseSensitive: false)
1718
movies: [Movie] @relation(inverseOf: "heroes")
1819
skills: [Skill] @flexSearch
19-
suit: Suit @flexSearch
20+
suit: Suit @flexSearch @hidden
2021
morality: Morality
2122
countryISOCode: String
2223
country: Country @reference(keyField: "countryISOCode")

spec/dev/server.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { ArangoDBAdapter, Project } from '../..';
77
import { globalContext } from '../../src/config/global';
88
import { InMemoryAdapter } from '../../src/database/inmemory';
99
import { getMetaSchema } from '../../src/meta-schema/meta-schema';
10-
import { Model } from '../../src/model';
1110
import { loadProjectFromDir } from '../../src/project/project-from-fs';
1211
import { Log4jsLoggerProvider } from '../helpers/log4js-logger-provider';
1312
import { createFastApp } from './fast-server';

spec/meta-schema/meta-schema.spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,17 @@ describe('Meta schema API', () => {
173173
}
174174
`;
175175

176+
const hiddenFieldsQuery = gql`
177+
{
178+
rootEntityType(name: "Country") {
179+
fields {
180+
name
181+
isHidden
182+
}
183+
}
184+
}
185+
`;
186+
176187
const permissionsQuery = gql`
177188
{
178189
rootEntityType(name: "Shipment") {
@@ -227,6 +238,16 @@ describe('Meta schema API', () => {
227238
{
228239
name: 'isoCode',
229240
typeName: 'String',
241+
isHidden: true,
242+
},
243+
{
244+
name: 'id',
245+
typeName: 'ID',
246+
isHidden: true,
247+
},
248+
{
249+
name: 'dummy',
250+
typeName: 'String',
230251
},
231252
],
232253
namespacePath: ['generic'],
@@ -572,6 +593,18 @@ describe('Meta schema API', () => {
572593
collectFieldConfig: null,
573594
type: { __typename: 'ScalarType' },
574595
},
596+
{
597+
collectFieldConfig: null,
598+
isCollectField: false,
599+
isList: false,
600+
isReference: false,
601+
isRelation: false,
602+
name: 'dummy',
603+
referenceKeyField: null,
604+
type: {
605+
__typename: 'ScalarType',
606+
},
607+
},
575608
],
576609
},
577610
{
@@ -1159,6 +1192,21 @@ describe('Meta schema API', () => {
11591192
expect(actualVersion).to.deep.equal(expectedVersion);
11601193
});
11611194

1195+
it('can query read whether fields are hidden', async () => {
1196+
const result = (await execute(hiddenFieldsQuery)) as any;
1197+
const rootEntityTypeFields = result.rootEntityType.fields;
1198+
const isoCodeField = rootEntityTypeFields.find((field: any) => field.name === 'isoCode');
1199+
const idField = rootEntityTypeFields.find((field: any) => field.name === 'id');
1200+
const dummyField = rootEntityTypeFields.find((field: any) => field.name === 'dummy');
1201+
const updatedAtField = rootEntityTypeFields.find(
1202+
(field: any) => field.name === 'updatedAt',
1203+
);
1204+
expect(isoCodeField.isHidden).to.be.true;
1205+
expect(idField.isHidden).to.be.true;
1206+
expect(dummyField.isHidden).to.be.false;
1207+
expect(updatedAtField.isHidden).to.be.false;
1208+
});
1209+
11621210
it('can query read the cruddl version from meta description', async () => {
11631211
const expectedVersion = CRUDDL_VERSION;
11641212
const result = (await execute(cruddlVersionIntrospectionQuery)) as any;

spec/model/create-model.spec.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { expect } from 'chai';
22
import { DocumentNode } from 'graphql';
33
import gql from 'graphql-tag';
4-
import { createModel } from '../../src/model';
54
import { createSimpleModel } from './model-spec.helper';
65

76
describe('createModel', () => {
@@ -353,4 +352,28 @@ describe('createModel', () => {
353352
en: 'Delivery via ship',
354353
});
355354
});
355+
356+
it('it allows to apply the hidden directive on regular and system fields', () => {
357+
const document: DocumentNode = gql`
358+
type Test @rootEntity {
359+
id: ID @hidden
360+
updatedAt: DateTime @hidden
361+
regularField: String!
362+
test2: Test2 @relation @hidden
363+
}
364+
365+
type Test2 @rootEntity {
366+
dummy: String
367+
}
368+
`;
369+
370+
const model = createSimpleModel(document);
371+
expect(model.validate().getErrors(), model.validate().toString()).to.deep.equal([]);
372+
373+
const type = model.getRootEntityTypeOrThrow('Test');
374+
expect(type.getFieldOrThrow('id').isHidden).to.be.true;
375+
expect(type.getFieldOrThrow('updatedAt').isHidden).to.be.true;
376+
expect(type.getFieldOrThrow('createdAt').isHidden).to.be.false;
377+
expect(type.getFieldOrThrow('test2').isHidden).to.be.true;
378+
});
356379
});

spec/model/implementation/object-type.spec.ts

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ChildEntityType, Model, Severity, TypeKind } from '../../../src/model';
2-
import { expectSingleErrorToInclude, expectToBeValid, validate } from './validation-utils';
2+
import { expectToBeValid, validate } from './validation-utils';
33
import { expect } from 'chai';
44

55
// This test uses a ChildEntityType because that is a concrete class without much addition to ObjectType, but it
@@ -57,22 +57,4 @@ describe('ObjectType', () => {
5757
expect(message.message).to.equal(`Duplicate field name: "deliveryNumber".`);
5858
}
5959
});
60-
61-
it('rejects type with reserved field names', () => {
62-
const type = new ChildEntityType(
63-
{
64-
kind: TypeKind.CHILD_ENTITY,
65-
name: 'Delivery',
66-
fields: [
67-
{
68-
name: 'updatedAt',
69-
typeName: 'String',
70-
},
71-
],
72-
},
73-
model,
74-
);
75-
76-
expectSingleErrorToInclude(type, `Field name "updatedAt" is reserved by a system field.`);
77-
});
7860
});

spec/schema/ast-validation-modules/key-field-validator.spec.ts

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -108,18 +108,6 @@ describe('key field validator', () => {
108108
`);
109109
});
110110

111-
it('warns about id: ID (without @key)', () => {
112-
assertValidatorWarns(
113-
`
114-
type Stuff @rootEntity {
115-
id: ID
116-
test: String
117-
}
118-
`,
119-
'The field "id" is redundant and should only be explicitly added when used with @key.',
120-
);
121-
});
122-
123111
it('warns about _key: String (without @key)', () => {
124112
assertValidatorWarns(
125113
`
@@ -132,30 +120,6 @@ describe('key field validator', () => {
132120
);
133121
});
134122

135-
it('rejects id: String @key (wrong type)', () => {
136-
assertValidatorRejects(
137-
`
138-
type Stuff @rootEntity {
139-
id: String @key
140-
test: String
141-
}
142-
`,
143-
'The field "id" must be of type "ID".',
144-
);
145-
});
146-
147-
it('rejects id: String (wrong type, without @key)', () => {
148-
assertValidatorRejects(
149-
`
150-
type Stuff @rootEntity {
151-
id: String
152-
test: String
153-
}
154-
`,
155-
'The field "id" must be of type "ID".',
156-
);
157-
});
158-
159123
it('rejects _key: String (without @key)', () => {
160124
assertValidatorRejects(
161125
`
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { print } from 'graphql';
2+
import gql from 'graphql-tag';
3+
import {
4+
assertValidatorAcceptsAndDoesNotWarn,
5+
assertValidatorRejects,
6+
assertValidatorWarns,
7+
} from './helpers';
8+
9+
describe('system field override validation', () => {
10+
it('is valid on non redundant system fields', () => {
11+
assertValidatorAcceptsAndDoesNotWarn(
12+
print(gql`
13+
type Root @rootEntity {
14+
id: ID @key
15+
createdAt: DateTime @hidden
16+
dummy: String
17+
}
18+
19+
type Root2 @rootEntity {
20+
id: ID @hidden
21+
updatedAt: DateTime @hidden
22+
dummy: String
23+
}
24+
`),
25+
);
26+
});
27+
28+
it('warns on redundant system field "id"', () => {
29+
assertValidatorWarns(
30+
print(gql`
31+
type Root @rootEntity {
32+
id: ID
33+
dummy: String
34+
}
35+
`),
36+
'Manually declaring system field "id" is redundant. Either add a suitable directive or consider removing the field',
37+
);
38+
});
39+
40+
it('warns on redundant system field "createdAt"', () => {
41+
assertValidatorWarns(
42+
print(gql`
43+
type Root @rootEntity {
44+
createdAt: DateTime
45+
dummy: String
46+
}
47+
`),
48+
'Manually declaring system field "createdAt" is redundant. Either add a suitable directive or consider removing the field',
49+
);
50+
});
51+
52+
it('warns on redundant system field "updatedAt"', () => {
53+
assertValidatorWarns(
54+
print(gql`
55+
type Root @rootEntity {
56+
updatedAt: DateTime
57+
dummy: String
58+
}
59+
`),
60+
'Manually declaring system field "updatedAt" is redundant. Either add a suitable directive or consider removing the field',
61+
);
62+
});
63+
64+
it('errors on system field "id" type mismatch', () => {
65+
assertValidatorRejects(
66+
print(gql`
67+
type Root @rootEntity {
68+
id: String
69+
dummy: String
70+
}
71+
`),
72+
'System field "id" must be of type "ID"',
73+
);
74+
});
75+
76+
it('errors on system field "createdAt" type mismatch', () => {
77+
assertValidatorRejects(
78+
print(gql`
79+
type Root @rootEntity {
80+
createdAt: String
81+
dummy: String
82+
}
83+
`),
84+
'System field "createdAt" must be of type "DateTime"',
85+
);
86+
});
87+
88+
it('errors on system field "updatedAt" type mismatch', () => {
89+
assertValidatorRejects(
90+
print(gql`
91+
type Root @rootEntity {
92+
updatedAt: String
93+
dummy: String
94+
}
95+
`),
96+
'System field "updatedAt" must be of type "DateTime"',
97+
);
98+
});
99+
100+
it('errors on not allowed directives on system fields', () => {
101+
assertValidatorRejects(
102+
print(gql`
103+
type Root @rootEntity {
104+
id: ID @relation
105+
dummy: String
106+
}
107+
`),
108+
'Directive "@relation" is not allowed on system field "id" and will be discarded',
109+
);
110+
});
111+
});

src/meta-schema/meta-schema.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,14 @@ import {
88
getPermissionDescriptorOfField,
99
getPermissionDescriptorOfRootEntityType,
1010
} from '../authorization/permission-descriptors-in-model';
11+
import { CRUDDL_VERSION } from '../cruddl-version';
1112
import { ExecutionOptionsCallbackArgs } from '../execution/execution-options';
1213
import { EnumValue, Field, RootEntityType, Type, TypeKind } from '../model';
1314
import { OrderDirection } from '../model/implementation/order';
1415
import { Project } from '../project/project';
16+
import { GraphQLI18nString } from '../schema/scalars/string-map';
1517
import { compact, flatMap } from '../utils/utils';
1618
import { I18N_GENERIC, I18N_LOCALE } from './constants';
17-
import { CRUDDL_VERSION } from '../cruddl-version';
18-
import { mapValues } from 'lodash';
19-
import { GraphQLI18nString } from '../schema/scalars/string-map';
2019

2120
const resolutionOrderDescription = JSON.stringify(
2221
'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`
9392
isIncludedInSearch: Boolean!
9493
isFlexSearchFulltextIndexed: Boolean!
9594
isFulltextIncludedInSearch: Boolean!
95+
isHidden: Boolean!
9696
flexSearchLanguage: FlexSearchLanguage
9797
9898
permissions: FieldPermissions

src/model/config/field.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,14 @@ export interface FieldConfig {
6666
readonly isAccessField?: boolean;
6767

6868
readonly accessFieldDirectiveASTNode?: DirectiveNode;
69+
70+
/**
71+
* Whether a field is marked as "hidden". This information can later be used,
72+
* via the fields meta information, to decide whether the field should be shown in UIs
73+
* or not.
74+
*/
75+
readonly isHidden?: boolean;
76+
readonly isHiddenASTNode?: DirectiveNode;
6977
}
7078

7179
export enum RelationDeleteAction {

0 commit comments

Comments
 (0)