Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add @hidden directive on fields #315

Merged
merged 1 commit into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading