Format epoch date strings to ensure consistent date values (#31865)

This PR fixes multiple issues around timestamp fields sometimes being returned as a number (`1551211325000`) instead of a UTC ISO8601 string (`2019-02-26T20:02:05.000Z`). The fix was introduced at the GraphQL layer so that any GraphQL datetime field could take advantage of the fix by simply using the new `Date` type.

- Added a new [GraphQLScalarType](https://www.apollographql.com/docs/graphql-tools/scalars.html#Date-as-a-scalar) for converting epoch date values to UTC ISO8601 before sending to the client.
- Updated all `timestamp` schema types to use the new `Date` type so that datetime fields will be returned in a consistent type/format.
- Created `KibanaConfigContext` React Context for accessing Kibana Advanced Settings from custom components
- Created HoC `PreferenceFormattedDate` for formatting dates according to Kibana Advanced Settings
- Updated Timeline to use new `PreferenceFormattedDate`
- Updated HostsTable's date to be `FormattedRelative` and added tool tip for viewing actual date value
- Added generated files to `.gitattributes` so that they're [collapsed by default](https://thoughtbot.com/blog/github-diff-supression) when reviewing in GitHub.

Includes fixes for:
https://github.com/elastic/ingest-dev/issues/199
https://github.com/elastic/ingest-dev/issues/280
https://github.com/elastic/ingest-dev/issues/282
https://github.com/elastic/ingest-dev/issues/303
This commit is contained in:
Garrett Spong 2019-02-27 14:22:17 -07:00 committed by GitHub
parent f63a8ca3df
commit e9c1360dab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 543 additions and 197 deletions

6
x-pack/plugins/secops/.gitattributes vendored Normal file
View file

@ -0,0 +1,6 @@
# Auto-collapse generated files in GitHub
# https://help.github.com/en/articles/customizing-how-changed-files-appear-on-github
x-pack/plugins/secops/public/graphql/types.ts linguist-generated=true
x-pack/plugins/secops/server/graphql/types.ts linguist-generated=true
x-pack/plugins/secops/public/graphql/introspection.json linguist-generated=true
*.test.tsx.snap linguist-generated=true

View file

@ -16,6 +16,7 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json';
import { I18nContext } from 'ui/i18n';
import { ErrorToast } from '../components/error_toast';
import { KibanaConfigContext } from '../components/formatted_date';
import { AppFrontendLibs } from '../lib/lib';
import { PageRouter } from '../routes';
import { store } from '../store';
@ -34,7 +35,9 @@ export const startApp = async (libs: AppFrontendLibs) => {
darkMode: libs.framework.darkMode,
})}
>
<PageRouter history={history} />
<KibanaConfigContext.Provider value={libs.framework}>
<PageRouter history={history} />
</KibanaConfigContext.Provider>
</ThemeProvider>
<ErrorToast />
</ApolloProvider>

View file

@ -0,0 +1,71 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { mount } from 'enzyme';
import moment from 'moment-timezone';
import * as React from 'react';
import { KibanaConfigContext, PreferenceFormattedDate } from '.';
import { AppTestingFrameworkAdapter } from '../../lib/adapters/framework/testing_framework_adapter';
import { mockFrameworks } from '../../mock';
describe('PreferenceFormattedDate', () => {
describe('rendering', () => {
const isoDateString = '2019-02-25T22:27:05.000Z';
const configFormattedDateString = (
dateString: string,
config: Partial<AppTestingFrameworkAdapter>
): string =>
moment
.tz(
dateString,
config.dateFormatTz! === 'Browser' ? config.timezone! : config.dateFormatTz!
)
.format(config.dateFormat);
test('it renders the UTC ISO8601 date string supplied when no configuration exists', () => {
const wrapper = mount(
<KibanaConfigContext.Provider value={{}}>
<PreferenceFormattedDate value={isoDateString} />
</KibanaConfigContext.Provider>
);
expect(wrapper.text()).toEqual(isoDateString);
});
test('it renders the UTC ISO8601 date supplied when the default configuration exists', () => {
const wrapper = mount(
<KibanaConfigContext.Provider value={mockFrameworks.default_UTC}>
<PreferenceFormattedDate value={isoDateString} />
</KibanaConfigContext.Provider>
);
expect(wrapper.text()).toEqual(
configFormattedDateString(isoDateString, mockFrameworks.default_UTC)
);
});
test('it renders the correct tz when the default browser configuration exists', () => {
const wrapper = mount(
<KibanaConfigContext.Provider value={mockFrameworks.default_browser}>
<PreferenceFormattedDate value={isoDateString} />
</KibanaConfigContext.Provider>
);
expect(wrapper.text()).toEqual(
configFormattedDateString(isoDateString, mockFrameworks.default_browser)
);
});
test('it renders the correct tz when a non-UTC configuration exists', () => {
const wrapper = mount(
<KibanaConfigContext.Provider value={mockFrameworks.default_MT}>
<PreferenceFormattedDate value={isoDateString} />
</KibanaConfigContext.Provider>
);
expect(wrapper.text()).toEqual(
configFormattedDateString(isoDateString, mockFrameworks.default_MT)
);
});
});
});

View file

@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import moment from 'moment-timezone';
import * as React from 'react';
import { pure } from 'recompose';
import { AppKibanaFrameworkAdapter } from '../../lib/adapters/framework/kibana_framework_adapter';
export const KibanaConfigContext = React.createContext<Partial<AppKibanaFrameworkAdapter>>({});
export const PreferenceFormattedDate = pure<{ value: Date | string }>(({ value }) => (
<KibanaConfigContext.Consumer>
{(config: Partial<AppKibanaFrameworkAdapter>) => {
return config && config.dateFormat && config.dateFormatTz && config.timezone
? moment
.tz(value, config.dateFormatTz === 'Browser' ? config.timezone : config.dateFormatTz)
.format(config.dateFormat)
: moment.utc(value).toISOString();
}}
</KibanaConfigContext.Consumer>
));

View file

@ -4,7 +4,7 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiBadge, EuiLink } from '@elastic/eui';
import { EuiBadge, EuiLink, EuiToolTip } from '@elastic/eui';
import { FormattedRelative } from '@kbn/i18n/react';
import { get, has, isNil } from 'lodash/fp';
import React from 'react';
@ -165,8 +165,10 @@ const getAuthenticationColumns = (startDate: number): Array<Columns<Authenticati
truncateText: false,
hideForMobile: false,
render: ({ node }) =>
has('lastFailure.timestamp', node) ? (
<FormattedRelative value={new Date(node.lastFailure!.timestamp!)} />
has('lastFailure.timestamp', node) && node.lastFailure!.timestamp != null ? (
<EuiToolTip position="bottom" content={node.lastFailure!.timestamp!}>
<FormattedRelative value={new Date(node.lastFailure!.timestamp!)} />
</EuiToolTip>
) : (
getEmptyTagValue()
),
@ -273,7 +275,9 @@ const getAuthenticationColumns = (startDate: number): Array<Columns<Authenticati
hideForMobile: false,
render: ({ node }) =>
has('lastSuccess.timestamp', node) ? (
<FormattedRelative value={new Date(node.lastSuccess!.timestamp!)} />
<EuiToolTip position="bottom" content={node.lastSuccess!.timestamp!}>
<FormattedRelative value={new Date(node.lastSuccess!.timestamp!)} />
</EuiToolTip>
) : (
getEmptyTagValue()
),

View file

@ -1,48 +1,58 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Load More Table Component rendering it renders the default Hosts table 1`] = `
<Connect(pure(Component))
data={
Array [
Object {
"cursor": Object {
"value": "98966fa2013c396155c460d35c0902be",
},
"node": Object {
"_id": "cPsuhGcB0WOhS6qyTKC0",
"firstSeen": "2018-12-06T15:40:53.319Z",
"host": Object {
"name": "elrond.elstc.co",
"os": Object {
"name": "Ubuntu",
"version": "18.04.1 LTS (Bionic Beaver)",
},
},
},
},
Object {
"cursor": Object {
"value": "aa7ca589f1b8220002f2fc61c64cfbf1",
},
"node": Object {
"_id": "KwQDiWcB0WOhS6qyXmrW",
"firstSeen": "2018-12-07T14:12:38.560Z",
"host": Object {
"name": "siem-kibana",
"os": Object {
"name": "Debian GNU/Linux",
"version": "9 (stretch)",
},
},
},
},
]
<ContextProvider
value={
Object {
"dateFormat": "MMM D, YYYY @ HH:mm:ss.SSS",
"dateFormatTz": "UTC",
"timezone": "UTC",
}
}
hasNextPage={true}
loadMore={[MockFunction]}
loading={false}
nextCursor="aa7ca589f1b8220002f2fc61c64cfbf1"
totalCount={4}
type="page"
/>
>
<Connect(pure(Component))
data={
Array [
Object {
"cursor": Object {
"value": "98966fa2013c396155c460d35c0902be",
},
"node": Object {
"_id": "cPsuhGcB0WOhS6qyTKC0",
"firstSeen": "2018-12-06T15:40:53.319Z",
"host": Object {
"name": "elrond.elstc.co",
"os": Object {
"name": "Ubuntu",
"version": "18.04.1 LTS (Bionic Beaver)",
},
},
},
},
Object {
"cursor": Object {
"value": "aa7ca589f1b8220002f2fc61c64cfbf1",
},
"node": Object {
"_id": "KwQDiWcB0WOhS6qyXmrW",
"firstSeen": "2018-12-07T14:12:38.560Z",
"host": Object {
"name": "siem-kibana",
"os": Object {
"name": "Debian GNU/Linux",
"version": "9 (stretch)",
},
},
},
},
]
}
hasNextPage={true}
loadMore={[MockFunction]}
loading={false}
nextCursor="aa7ca589f1b8220002f2fc61c64cfbf1"
totalCount={4}
type="page"
/>
</ContextProvider>
`;

View file

@ -11,8 +11,9 @@ import { getOr } from 'lodash/fp';
import * as React from 'react';
import { Provider as ReduxStoreProvider } from 'react-redux';
import { mockGlobalState } from '../../../../mock';
import { mockFrameworks, mockGlobalState } from '../../../../mock';
import { createStore, hostsModel, State } from '../../../../store';
import { KibanaConfigContext } from '../../../formatted_date';
import { HostsTable } from './index';
import { mockData } from './mock';
@ -30,15 +31,17 @@ describe('Load More Table Component', () => {
test('it renders the default Hosts table', () => {
const wrapper = shallow(
<ReduxStoreProvider store={store}>
<HostsTable
loading={false}
data={mockData.Hosts.edges}
totalCount={mockData.Hosts.totalCount}
hasNextPage={getOr(false, 'hasNextPage', mockData.Hosts.pageInfo)!}
nextCursor={getOr(null, 'endCursor.value', mockData.Hosts.pageInfo)!}
loadMore={loadMore}
type={hostsModel.HostsType.page}
/>
<KibanaConfigContext.Provider value={mockFrameworks.default_UTC}>
<HostsTable
loading={false}
data={mockData.Hosts.edges}
totalCount={mockData.Hosts.totalCount}
hasNextPage={getOr(false, 'hasNextPage', mockData.Hosts.pageInfo)!}
nextCursor={getOr(null, 'endCursor.value', mockData.Hosts.pageInfo)!}
loadMore={loadMore}
type={hostsModel.HostsType.page}
/>
</KibanaConfigContext.Provider>
</ReduxStoreProvider>
);

View file

@ -4,13 +4,14 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { EuiBadge, EuiLink } from '@elastic/eui';
import { EuiBadge, EuiLink, EuiToolTip } from '@elastic/eui';
import { get, isNil } from 'lodash/fp';
import React from 'react';
import { connect } from 'react-redux';
import { pure } from 'recompose';
import { ActionCreator } from 'typescript-fsa';
import { FormattedRelative } from '@kbn/i18n/react';
import { HostsEdges } from '../../../../graphql/types';
import { hostsActions, hostsModel, hostsSelectors, State } from '../../../../store';
import { DragEffects, DraggableWrapper } from '../../../drag_and_drop/draggable_wrapper';
@ -163,7 +164,14 @@ const getHostsColumns = (): Array<Columns<HostsEdges>> => [
name: i18n.FIRST_SEEN,
truncateText: false,
hideForMobile: false,
render: ({ node }) => defaultToEmptyTag(node.firstSeen),
render: ({ node }) =>
node.firstSeen && node.firstSeen !== '' ? (
<EuiToolTip position="bottom" content={node.firstSeen}>
<FormattedRelative value={node.firstSeen} />
</EuiToolTip>
) : (
defaultToEmptyTag(node.firstSeen)
),
},
{
name: i18n.OS,

View file

@ -10,6 +10,7 @@ import { pure } from 'recompose';
import { Ecs } from '../../../../../server/graphql/types';
import { getMappedEcsValue } from '../../../../lib/ecs';
import { getOrEmptyTag } from '../../../empty_value';
import { PreferenceFormattedDate } from '../../../formatted_date';
import { LocalizedDateTooltip } from '../../../localized_date_tooltip';
export const FormattedField = pure<{ data: Ecs; fieldName: string; fieldType: string }>(
@ -18,7 +19,9 @@ export const FormattedField = pure<{ data: Ecs; fieldName: string; fieldType: st
const maybeDate = moment(new Date(value!));
return fieldType === 'date' && value != null && maybeDate.isValid() ? (
<LocalizedDateTooltip date={maybeDate.toDate()}>{value}</LocalizedDateTooltip>
<LocalizedDateTooltip date={maybeDate.toDate()}>
<PreferenceFormattedDate value={value} />
</LocalizedDateTooltip>
) : (
getOrEmptyTag(fieldName, data)
);

View file

@ -12,14 +12,17 @@ import { DragDropContext } from 'react-beautiful-dnd';
import { Provider as ReduxStoreProvider } from 'react-redux';
import { ThemeProvider } from 'styled-components';
import moment from 'moment-timezone';
import { plainColumnRenderer } from '.';
import { Ecs } from '../../../../graphql/types';
import { getAllFieldsInSchemaByMappedName, virtualEcsSchema } from '../../../../lib/ecs';
import { mockEcsData } from '../../../../mock';
import { mockEcsData, mockFrameworks } from '../../../../mock';
import { createStore } from '../../../../store';
import { getEmptyValue } from '../../../empty_value';
import { KibanaConfigContext } from '../../../formatted_date';
const allFieldsInSchemaByName = getAllFieldsInSchemaByMappedName(virtualEcsSchema);
const mockFramework = mockFrameworks.default_UTC;
describe('plain_column_renderer', () => {
let mockDatum: Ecs;
@ -55,11 +58,13 @@ describe('plain_column_renderer', () => {
);
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{column}</span>
</DragDropContext>
</ReduxStoreProvider>
<KibanaConfigContext.Provider value={mockFramework}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{column}</span>
</DragDropContext>
</ReduxStoreProvider>
</KibanaConfigContext.Provider>
</ThemeProvider>
);
expect(wrapper.text()).toEqual('Access');
@ -73,11 +78,13 @@ describe('plain_column_renderer', () => {
);
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{column}</span>
</DragDropContext>
</ReduxStoreProvider>
<KibanaConfigContext.Provider value={mockFramework}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{column}</span>
</DragDropContext>
</ReduxStoreProvider>
</KibanaConfigContext.Provider>
</ThemeProvider>
);
expect(wrapper.text()).toEqual('192.168.0.3');
@ -91,11 +98,13 @@ describe('plain_column_renderer', () => {
);
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{column}</span>
</DragDropContext>
</ReduxStoreProvider>
<KibanaConfigContext.Provider value={mockFramework}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{column}</span>
</DragDropContext>
</ReduxStoreProvider>
</KibanaConfigContext.Provider>
</ThemeProvider>
);
expect(wrapper.text()).toEqual('1');
@ -109,11 +118,13 @@ describe('plain_column_renderer', () => {
);
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{column}</span>
</DragDropContext>
</ReduxStoreProvider>
<KibanaConfigContext.Provider value={mockFramework}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{column}</span>
</DragDropContext>
</ReduxStoreProvider>
</KibanaConfigContext.Provider>
</ThemeProvider>
);
expect(wrapper.text()).toEqual('xx');
@ -127,11 +138,13 @@ describe('plain_column_renderer', () => {
);
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{column}</span>
</DragDropContext>
</ReduxStoreProvider>
<KibanaConfigContext.Provider value={mockFramework}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{column}</span>
</DragDropContext>
</ReduxStoreProvider>
</KibanaConfigContext.Provider>
</ThemeProvider>
);
expect(wrapper.text()).toEqual('3');
@ -145,17 +158,19 @@ describe('plain_column_renderer', () => {
);
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{column}</span>
</DragDropContext>
</ReduxStoreProvider>
<KibanaConfigContext.Provider value={mockFramework}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{column}</span>
</DragDropContext>
</ReduxStoreProvider>
</KibanaConfigContext.Provider>
</ThemeProvider>
);
expect(wrapper.text()).toEqual('192.168.0.1');
});
test('should return the (unformatted) time if timestamp has a valid value', () => {
test('should return the time formatted as per Kibana advanced settings if timestamp has a valid value', () => {
const column = plainColumnRenderer.renderColumn(
'timestamp',
mockDatum,
@ -163,14 +178,18 @@ describe('plain_column_renderer', () => {
);
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{column}</span>
</DragDropContext>
</ReduxStoreProvider>
<KibanaConfigContext.Provider value={mockFramework}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{column}</span>
</DragDropContext>
</ReduxStoreProvider>
</KibanaConfigContext.Provider>
</ThemeProvider>
);
expect(wrapper.text()).toEqual(mockDatum.timestamp!);
expect(wrapper.text()).toEqual(
moment.tz(mockDatum.timestamp!, mockFramework.dateFormatTz!).format(mockFramework.dateFormat)
);
});
test('should return the value of event.action if event.action has a valid value', () => {
@ -181,11 +200,13 @@ describe('plain_column_renderer', () => {
);
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{column}</span>
</DragDropContext>
</ReduxStoreProvider>
<KibanaConfigContext.Provider value={mockFramework}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{column}</span>
</DragDropContext>
</ReduxStoreProvider>
</KibanaConfigContext.Provider>
</ThemeProvider>
);
expect(wrapper.text()).toEqual('Action');
@ -199,11 +220,13 @@ describe('plain_column_renderer', () => {
);
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{column}</span>
</DragDropContext>
</ReduxStoreProvider>
<KibanaConfigContext.Provider value={mockFramework}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{column}</span>
</DragDropContext>
</ReduxStoreProvider>
</KibanaConfigContext.Provider>
</ThemeProvider>
);
expect(wrapper.text()).toEqual('john.dee');
@ -218,11 +241,13 @@ describe('plain_column_renderer', () => {
);
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{emptyColumn}</span>
</DragDropContext>
</ReduxStoreProvider>
<KibanaConfigContext.Provider value={mockFramework}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{emptyColumn}</span>
</DragDropContext>
</ReduxStoreProvider>
</KibanaConfigContext.Provider>
</ThemeProvider>
);
expect(wrapper.text()).toEqual(getEmptyValue());
@ -237,11 +262,13 @@ describe('plain_column_renderer', () => {
);
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{emptyColumn}</span>
</DragDropContext>
</ReduxStoreProvider>
<KibanaConfigContext.Provider value={mockFramework}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{emptyColumn}</span>
</DragDropContext>
</ReduxStoreProvider>
</KibanaConfigContext.Provider>
</ThemeProvider>
);
expect(wrapper.text()).toEqual(getEmptyValue());
@ -256,11 +283,13 @@ describe('plain_column_renderer', () => {
);
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{emptyColumn}</span>
</DragDropContext>
</ReduxStoreProvider>
<KibanaConfigContext.Provider value={mockFramework}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{emptyColumn}</span>
</DragDropContext>
</ReduxStoreProvider>
</KibanaConfigContext.Provider>
</ThemeProvider>
);
expect(wrapper.text()).toEqual(getEmptyValue());
@ -275,11 +304,13 @@ describe('plain_column_renderer', () => {
);
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{emptyColumn}</span>
</DragDropContext>
</ReduxStoreProvider>
<KibanaConfigContext.Provider value={mockFramework}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{emptyColumn}</span>
</DragDropContext>
</ReduxStoreProvider>
</KibanaConfigContext.Provider>
</ThemeProvider>
);
expect(wrapper.text()).toEqual(getEmptyValue());
@ -294,11 +325,13 @@ describe('plain_column_renderer', () => {
);
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{emptyColumn}</span>
</DragDropContext>
</ReduxStoreProvider>
<KibanaConfigContext.Provider value={mockFramework}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{emptyColumn}</span>
</DragDropContext>
</ReduxStoreProvider>
</KibanaConfigContext.Provider>
</ThemeProvider>
);
expect(wrapper.text()).toEqual(getEmptyValue());
@ -313,11 +346,13 @@ describe('plain_column_renderer', () => {
);
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{emptyColumn}</span>
</DragDropContext>
</ReduxStoreProvider>
<KibanaConfigContext.Provider value={mockFramework}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{emptyColumn}</span>
</DragDropContext>
</ReduxStoreProvider>
</KibanaConfigContext.Provider>
</ThemeProvider>
);
expect(wrapper.text()).toEqual(getEmptyValue());
@ -332,11 +367,13 @@ describe('plain_column_renderer', () => {
);
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{emptyColumn}</span>
</DragDropContext>
</ReduxStoreProvider>
<KibanaConfigContext.Provider value={mockFramework}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{emptyColumn}</span>
</DragDropContext>
</ReduxStoreProvider>
</KibanaConfigContext.Provider>
</ThemeProvider>
);
expect(wrapper.text()).toEqual(getEmptyValue());
@ -351,11 +388,13 @@ describe('plain_column_renderer', () => {
);
const wrapper = mount(
<ThemeProvider theme={() => ({ eui: euiDarkVars, darkMode: true })}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{emptyColumn}</span>
</DragDropContext>
</ReduxStoreProvider>
<KibanaConfigContext.Provider value={mockFramework}>
<ReduxStoreProvider store={store}>
<DragDropContext onDragEnd={noop}>
<span>{emptyColumn}</span>
</DragDropContext>
</ReduxStoreProvider>
</KibanaConfigContext.Provider>
</ThemeProvider>
);
expect(wrapper.text()).toEqual(getEmptyValue());

View file

@ -1110,7 +1110,7 @@
"name": "timestamp",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"type": { "kind": "SCALAR", "name": "Date", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
@ -1136,6 +1136,16 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "Date",
"description": "",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "SourceEcsFields",
@ -1635,7 +1645,7 @@
"name": "timestamp",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"type": { "kind": "SCALAR", "name": "Date", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
@ -1978,7 +1988,7 @@
"name": "firstSeen",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"type": { "kind": "SCALAR", "name": "Date", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},
@ -1994,7 +2004,7 @@
"name": "lastBeat",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"type": { "kind": "SCALAR", "name": "Date", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
}
@ -2154,7 +2164,7 @@
"name": "timestamp",
"description": "",
"args": [],
"type": { "kind": "SCALAR", "name": "String", "ofType": null },
"type": { "kind": "SCALAR", "name": "Date", "ofType": null },
"isDeprecated": false,
"deprecationReason": null
},

View file

@ -9,6 +9,12 @@
// START: Typescript template
// ====================================================
// ====================================================
// Scalars
// ====================================================
export type Date = any;
// ====================================================
// Types
// ====================================================
@ -144,7 +150,7 @@ export interface UserEcsFields {
}
export interface LastSourceHost {
timestamp?: string | null;
timestamp?: Date | null;
source?: SourceEcsFields | null;
@ -240,7 +246,7 @@ export interface Ecs {
suricata?: SuricataEcsFields | null;
timestamp?: string | null;
timestamp?: Date | null;
user?: UserEcsFields | null;
}
@ -310,11 +316,11 @@ export interface HostsEdges {
export interface HostItem {
_id?: string | null;
firstSeen?: string | null;
firstSeen?: Date | null;
host?: HostEcsFields | null;
lastBeat?: string | null;
lastBeat?: Date | null;
}
export interface NetworkTopNFlowData {
@ -334,7 +340,7 @@ export interface NetworkTopNFlowEdges {
export interface NetworkTopNFlowItem {
_id?: string | null;
timestamp?: string | null;
timestamp?: Date | null;
source?: TopNFlowItem | null;
@ -612,7 +618,7 @@ export namespace GetAuthenticationsQuery {
export type LastSuccess = {
__typename?: 'LastSourceHost';
timestamp?: string | null;
timestamp?: Date | null;
source?: _Source | null;
@ -636,7 +642,7 @@ export namespace GetAuthenticationsQuery {
export type LastFailure = {
__typename?: 'LastSourceHost';
timestamp?: string | null;
timestamp?: Date | null;
source?: __Source | null;
@ -740,7 +746,7 @@ export namespace GetEventsQuery {
_index?: string | null;
timestamp?: string | null;
timestamp?: Date | null;
event?: Event | null;
@ -873,9 +879,9 @@ export namespace GetHostSummaryQuery {
_id?: string | null;
firstSeen?: string | null;
firstSeen?: Date | null;
lastBeat?: string | null;
lastBeat?: Date | null;
host?: Host | null;
};
@ -976,9 +982,9 @@ export namespace GetHostsTableQuery {
_id?: string | null;
firstSeen?: string | null;
firstSeen?: Date | null;
lastBeat?: string | null;
lastBeat?: Date | null;
host?: Host | null;
};
@ -1311,7 +1317,7 @@ export namespace GetTimelineQuery {
_index?: string | null;
timestamp?: string | null;
timestamp?: Date | null;
event?: Event | null;

View file

@ -24,6 +24,7 @@ const BREADCRUMBS_ELEMENT_ID = 'react-secops-breadcrumbs';
export class AppKibanaFrameworkAdapter implements AppFrameworkAdapter {
public dateFormat?: string;
public dateFormatTz?: string;
public darkMode?: boolean;
public kbnVersion?: string;
public scaledDateFormat?: string;
@ -130,6 +131,7 @@ export class AppKibanaFrameworkAdapter implements AppFrameworkAdapter {
this.timezone = Private(this.timezoneProvider)();
this.kbnVersion = kbnVersion;
this.dateFormat = config.get('dateFormat');
this.dateFormatTz = config.get('dateFormat:tz');
try {
this.darkMode = config.get('theme:darkMode');
} catch (e) {

View file

@ -9,6 +9,7 @@ import { AppFrameworkAdapter } from '../../lib';
export class AppTestingFrameworkAdapter implements AppFrameworkAdapter {
public appState?: object;
public dateFormat?: string;
public dateFormatTz?: string;
public kbnVersion?: string;
public scaledDateFormat?: string;
public timezone?: string;

View file

@ -23,6 +23,7 @@ export type AppApolloClient = ApolloClient<NormalizedCacheObject>;
export interface AppFrameworkAdapter {
appState?: object;
dateFormat?: string;
dateFormatTz?: string;
darkMode?: boolean;
kbnVersion?: string;
scaledDateFormat?: string;

View file

@ -5,5 +5,6 @@
*/
export * from './global_state';
export * from './mock_ecs';
export * from './index_pattern';
export * from './kibana_config';
export * from './mock_ecs';

View file

@ -0,0 +1,30 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { AppTestingFrameworkAdapter } from '../lib/adapters/framework/testing_framework_adapter';
export const mockFrameworks: Readonly<Record<string, Partial<AppTestingFrameworkAdapter>>> = {
default_browser: {
dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS',
dateFormatTz: 'Browser',
timezone: 'America/Denver',
},
default_ET: {
dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS',
dateFormatTz: 'America/New_York',
timezone: 'America/New_York',
},
default_MT: {
dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS',
dateFormatTz: 'America/Denver',
timezone: 'America/Denver',
},
default_UTC: {
dateFormat: 'MMM D, YYYY @ HH:mm:ss.SSS',
dateFormatTz: 'UTC',
timezone: 'UTC',
},
};

View file

@ -8,7 +8,7 @@ import gql from 'graphql-tag';
export const authenticationsSchema = gql`
type LastSourceHost {
timestamp: String
timestamp: Date
source: SourceEcsFields
host: HostEcsFields
}

View file

@ -11,6 +11,7 @@ import { rootSchema } from '../../../common/graphql/root/schema.gql';
import { sharedSchema } from '../../../common/graphql/shared';
import { Logger } from '../../utils/logger';
import { ecsSchema } from '../ecs';
import { dateSchema } from '../scalar_date';
import { sourceStatusSchema } from '../source_status/schema.gql';
import { sourcesSchema } from '../sources/schema.gql';
import { getAuthenticationsQueryMock, mockAuthenticationsData } from './authentications.mock';
@ -103,6 +104,7 @@ describe('Test Source Schema', () => {
sourceStatusSchema,
ecsSchema,
authenticationsSchema,
dateSchema,
];
const mockSchema = makeExecutableSchema({ typeDefs });

View file

@ -116,7 +116,7 @@ export const ecsSchema = gql`
host: HostEcsFields
source: SourceEcsFields
suricata: SuricataEcsFields
timestamp: String
timestamp: Date
user: UserEcsFields
}

View file

@ -11,6 +11,7 @@ import { rootSchema } from '../../../common/graphql/root/schema.gql';
import { sharedSchema } from '../../../common/graphql/shared';
import { Logger } from '../../utils/logger';
import { ecsSchema } from '../ecs';
import { dateSchema } from '../scalar_date';
import { sourceStatusSchema } from '../source_status/schema.gql';
import { sourcesSchema } from '../sources/schema.gql';
import { getEventsQueryMock, mockEventsData } from './events.mock';
@ -133,6 +134,7 @@ describe('Test Source Schema', () => {
sourceStatusSchema,
ecsSchema,
eventsSchema,
dateSchema,
];
const mockSchema = makeExecutableSchema({ typeDefs });

View file

@ -9,9 +9,9 @@ import gql from 'graphql-tag';
export const hostsSchema = gql`
type HostItem {
_id: String
firstSeen: String
firstSeen: Date
host: HostEcsFields
lastBeat: String
lastBeat: Date
}
type HostsEdges {

View file

@ -11,6 +11,7 @@ import { rootSchema } from '../../../common/graphql/root/schema.gql';
import { sharedSchema } from '../../../common/graphql/shared';
import { Logger } from '../../utils/logger';
import { ecsSchema } from '../ecs';
import { dateSchema } from '../scalar_date';
import { sourceStatusSchema } from '../source_status/schema.gql';
import { sourcesSchema } from '../sources/schema.gql';
import { getHostsQueryMock, mockHostsData } from './hosts.mock';
@ -87,6 +88,7 @@ describe('Test Source Schema', () => {
sourceStatusSchema,
ecsSchema,
hostsSchema,
dateSchema,
];
const mockSchema = makeExecutableSchema({ typeDefs });

View file

@ -14,15 +14,17 @@ import { ecsSchema } from './ecs';
import { eventsSchema } from './events';
import { hostsSchema } from './hosts';
import { networkTopNFlowSchema } from './network_top_n_flow';
import { dateSchema } from './scalar_date';
import { sourceStatusSchema } from './source_status';
import { sourcesSchema } from './sources';
import { uncommonProcessesSchema } from './uncommon_processes';
import { whoAmISchema } from './who_am_i';
export const schemas = [
ecsSchema,
authenticationsSchema,
ecsSchema,
eventsSchema,
dateSchema,
hostsSchema,
networkTopNFlowSchema,
rootSchema,

View file

@ -15,7 +15,7 @@ export const networkTopNFlowSchema = gql`
type NetworkTopNFlowItem {
_id: String
timestamp: String
timestamp: Date
source: TopNFlowItem
destination: TopNFlowItem
client: TopNFlowItem

View file

@ -11,6 +11,7 @@ import { rootSchema } from '../../../common/graphql/root/schema.gql';
import { sharedSchema } from '../../../common/graphql/shared';
import { Logger } from '../../utils/logger';
import { ecsSchema } from '../ecs';
import { dateSchema } from '../scalar_date';
import { sourceStatusSchema } from '../source_status/schema.gql';
import { sourcesSchema } from '../sources/schema.gql';
import { NetworkTopNFlowDirection, NetworkTopNFlowType } from '../types';
@ -107,6 +108,7 @@ describe('Test Source Schema', () => {
sourceStatusSchema,
ecsSchema,
networkTopNFlowSchema,
dateSchema,
];
const mockSchema = makeExecutableSchema({ typeDefs });

View file

@ -0,0 +1,8 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { createScalarDateResolvers } from './resolvers';
export { dateSchema } from './schema.gql';

View file

@ -0,0 +1,53 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { IntValueNode, StringValueNode } from 'graphql';
import { dateScalar } from './resolvers';
describe('Test ScalarDate Resolver', () => {
describe('#serialize', () => {
test('Make sure that an epoch date number is serialized', () => {
const date = dateScalar.serialize(1514782800000);
expect(date).toEqual('2018-01-01T05:00:00.000Z');
});
test('Make sure that a date string is serialized', () => {
const date = dateScalar.serialize('2018-01-01T05:00:00.000Z');
expect(date).toEqual('2018-01-01T05:00:00.000Z');
});
});
describe('#parseValue', () => {
test('Make sure that an epoch date number passes through parseValue', () => {
const date = dateScalar.parseValue(1514782800000);
expect(date).toEqual(1514782800000);
});
test('Make sure that a date string passes through parseValue', () => {
const date = dateScalar.parseValue('2018-01-01T05:00:00.000Z');
expect(date).toEqual('2018-01-01T05:00:00.000Z');
});
});
describe('#parseLiteral', () => {
test('Make sure that an epoch date string passes through parseLiteral', () => {
const valueNode: IntValueNode = {
kind: 'IntValue',
value: '1514782800000',
};
const date = dateScalar.parseLiteral(valueNode);
expect(date).toEqual(1514782800000);
});
test('Make sure that a date string passes through parseLiteral', () => {
const valueNode: StringValueNode = {
kind: 'StringValue',
value: '2018-01-01T05:00:00.000Z',
};
const date = dateScalar.parseLiteral(valueNode);
expect(date).toEqual('2018-01-01T05:00:00.000Z');
});
});
});

View file

@ -0,0 +1,32 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { GraphQLScalarType, Kind } from 'graphql';
export const dateScalar = new GraphQLScalarType({
name: 'Date',
description:
'Represents a Date for either an ES formatted date string or epoch string ISO8601 formatted',
serialize(value): string {
return Number.isNaN(Date.parse(value)) ? new Date(value).toISOString() : value;
},
parseValue(value) {
return value;
},
parseLiteral(ast) {
switch (ast.kind) {
case Kind.INT:
return parseInt(ast.value, 10);
case Kind.STRING:
return ast.value;
}
return null;
},
});
export const createScalarDateResolvers = () => ({
Date: dateScalar,
});

View file

@ -0,0 +1,11 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import gql from 'graphql-tag';
export const dateSchema = gql`
scalar Date
`;

View file

@ -38,6 +38,12 @@ export type SubscriptionResolver<Result, Parent = any, Context = any, Args = nev
// START: Typescript template
// ====================================================
// ====================================================
// Scalars
// ====================================================
export type Date = any;
// ====================================================
// Types
// ====================================================
@ -173,7 +179,7 @@ export interface UserEcsFields {
}
export interface LastSourceHost {
timestamp?: string | null;
timestamp?: Date | null;
source?: SourceEcsFields | null;
@ -269,7 +275,7 @@ export interface Ecs {
suricata?: SuricataEcsFields | null;
timestamp?: string | null;
timestamp?: Date | null;
user?: UserEcsFields | null;
}
@ -339,11 +345,11 @@ export interface HostsEdges {
export interface HostItem {
_id?: string | null;
firstSeen?: string | null;
firstSeen?: Date | null;
host?: HostEcsFields | null;
lastBeat?: string | null;
lastBeat?: Date | null;
}
export interface NetworkTopNFlowData {
@ -363,7 +369,7 @@ export interface NetworkTopNFlowEdges {
export interface NetworkTopNFlowItem {
_id?: string | null;
timestamp?: string | null;
timestamp?: Date | null;
source?: TopNFlowItem | null;
@ -1059,7 +1065,7 @@ export namespace UserEcsFieldsResolvers {
export namespace LastSourceHostResolvers {
export interface Resolvers<Context = SecOpsContext, TypeParent = LastSourceHost> {
timestamp?: TimestampResolver<string | null, TypeParent, Context>;
timestamp?: TimestampResolver<Date | null, TypeParent, Context>;
source?: SourceResolver<SourceEcsFields | null, TypeParent, Context>;
@ -1067,7 +1073,7 @@ export namespace LastSourceHostResolvers {
}
export type TimestampResolver<
R = string | null,
R = Date | null,
Parent = LastSourceHost,
Context = SecOpsContext
> = Resolver<R, Parent, Context>;
@ -1337,7 +1343,7 @@ export namespace EcsResolvers {
suricata?: SuricataResolver<SuricataEcsFields | null, TypeParent, Context>;
timestamp?: TimestampResolver<string | null, TypeParent, Context>;
timestamp?: TimestampResolver<Date | null, TypeParent, Context>;
user?: UserResolver<UserEcsFields | null, TypeParent, Context>;
}
@ -1382,11 +1388,11 @@ export namespace EcsResolvers {
Parent = Ecs,
Context = SecOpsContext
> = Resolver<R, Parent, Context>;
export type TimestampResolver<
R = string | null,
Parent = Ecs,
Context = SecOpsContext
> = Resolver<R, Parent, Context>;
export type TimestampResolver<R = Date | null, Parent = Ecs, Context = SecOpsContext> = Resolver<
R,
Parent,
Context
>;
export type UserResolver<
R = UserEcsFields | null,
Parent = Ecs,
@ -1599,11 +1605,11 @@ export namespace HostItemResolvers {
export interface Resolvers<Context = SecOpsContext, TypeParent = HostItem> {
_id?: IdResolver<string | null, TypeParent, Context>;
firstSeen?: FirstSeenResolver<string | null, TypeParent, Context>;
firstSeen?: FirstSeenResolver<Date | null, TypeParent, Context>;
host?: HostResolver<HostEcsFields | null, TypeParent, Context>;
lastBeat?: LastBeatResolver<string | null, TypeParent, Context>;
lastBeat?: LastBeatResolver<Date | null, TypeParent, Context>;
}
export type IdResolver<R = string | null, Parent = HostItem, Context = SecOpsContext> = Resolver<
@ -1612,7 +1618,7 @@ export namespace HostItemResolvers {
Context
>;
export type FirstSeenResolver<
R = string | null,
R = Date | null,
Parent = HostItem,
Context = SecOpsContext
> = Resolver<R, Parent, Context>;
@ -1622,7 +1628,7 @@ export namespace HostItemResolvers {
Context = SecOpsContext
> = Resolver<R, Parent, Context>;
export type LastBeatResolver<
R = string | null,
R = Date | null,
Parent = HostItem,
Context = SecOpsContext
> = Resolver<R, Parent, Context>;
@ -1677,7 +1683,7 @@ export namespace NetworkTopNFlowItemResolvers {
export interface Resolvers<Context = SecOpsContext, TypeParent = NetworkTopNFlowItem> {
_id?: IdResolver<string | null, TypeParent, Context>;
timestamp?: TimestampResolver<string | null, TypeParent, Context>;
timestamp?: TimestampResolver<Date | null, TypeParent, Context>;
source?: SourceResolver<TopNFlowItem | null, TypeParent, Context>;
@ -1696,7 +1702,7 @@ export namespace NetworkTopNFlowItemResolvers {
Context = SecOpsContext
> = Resolver<R, Parent, Context>;
export type TimestampResolver<
R = string | null,
R = Date | null,
Parent = NetworkTopNFlowItem,
Context = SecOpsContext
> = Resolver<R, Parent, Context>;

View file

@ -11,6 +11,7 @@ import { rootSchema } from '../../../common/graphql/root/schema.gql';
import { sharedSchema } from '../../../common/graphql/shared';
import { Logger } from '../../utils/logger';
import { ecsSchema } from '../ecs';
import { dateSchema } from '../scalar_date';
import { sourceStatusSchema } from '../source_status/schema.gql';
import { sourcesSchema } from '../sources/schema.gql';
import { uncommonProcessesSchema } from './schema.gql';
@ -94,6 +95,7 @@ describe('Test Source Schema', () => {
sourceStatusSchema,
ecsSchema,
uncommonProcessesSchema,
dateSchema,
];
const mockSchema = makeExecutableSchema({ typeDefs });

View file

@ -11,6 +11,7 @@ import { createAuthenticationsResolvers } from './graphql/authentications';
import { createEventsResolvers } from './graphql/events';
import { createHostsResolvers } from './graphql/hosts';
import { createNetworkTopNFlowResolvers } from './graphql/network_top_n_flow';
import { createScalarDateResolvers } from './graphql/scalar_date';
import { createSourceStatusResolvers } from './graphql/source_status';
import { createSourcesResolvers } from './graphql/sources';
import { createUncommonProcessesResolvers } from './graphql/uncommon_processes';
@ -27,14 +28,14 @@ export const initServer = (libs: AppBackendLibs, config: Config) => {
const { logger, mocking } = config;
const schema = makeExecutableSchema({
resolvers: [
createUncommonProcessesResolvers(libs) as IResolvers,
createSourceStatusResolvers(libs) as IResolvers,
createSourcesResolvers(libs) as IResolvers,
createAuthenticationsResolvers(libs) as IResolvers,
createEventsResolvers(libs) as IResolvers,
createHostsResolvers(libs) as IResolvers,
createSourcesResolvers(libs) as IResolvers,
createNetworkTopNFlowResolvers(libs) as IResolvers,
createScalarDateResolvers() as IResolvers,
createSourcesResolvers(libs) as IResolvers,
createSourceStatusResolvers(libs) as IResolvers,
createUncommonProcessesResolvers(libs) as IResolvers,
createWhoAmIResolvers() as IResolvers,
],
typeDefs: schemas,