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

[App Search] Schema: Set up server routes, grand foray into shared/connected Kea logic #99548

Merged
merged 11 commits into from
May 11, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { EngineDetails } from '../../../engine/types';

export const getConflictingEnginesFromConflictingField = (
conflictingField: SchemaConflictFieldTypes
): string[] => Object.values(conflictingField).flat();
) => Object.values(conflictingField).flat() as string[];

export const getConflictingEnginesFromSchemaConflicts = (
schemaConflicts: SchemaConflicts
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { LogicMounter, mockHttpValues } from '../../../__mocks__';

import { nextTick } from '@kbn/test/jest';

import { SchemaType } from '../../../shared/schema/types';

import { MetaEngineSchemaLogic } from './schema_meta_engine_logic';

describe('MetaEngineSchemaLogic', () => {
const { mount } = new LogicMounter(MetaEngineSchemaLogic);
const { http } = mockHttpValues;

const MOCK_RESPONSE = {
schema: {
some_text_field: SchemaType.Text,
some_number_field: SchemaType.Number,
},
fields: {
some_text_field: {
text: ['source-engine-a', 'source-engine-b'],
},
},
conflictingFields: {
some_number_field: {
number: ['source-engine-a'],
text: ['source-engine-b'],
},
},
};

const DEFAULT_VALUES = {
dataLoading: true,
schema: {},
fields: {},
conflictingFields: {},
conflictingFieldsCount: 0,
hasConflicts: false,
};

beforeEach(() => {
jest.clearAllMocks();
});

it('has expected default values', () => {
mount();
expect(MetaEngineSchemaLogic.values).toEqual(DEFAULT_VALUES);
});

describe('actions', () => {
describe('onMetaEngineSchemaLoad', () => {
it('stores the API response in state', () => {
mount();

MetaEngineSchemaLogic.actions.onMetaEngineSchemaLoad(MOCK_RESPONSE);

expect(MetaEngineSchemaLogic.values).toEqual({
...DEFAULT_VALUES,
fields: MOCK_RESPONSE.fields,
conflictingFields: MOCK_RESPONSE.conflictingFields,
hasConflicts: true,
conflictingFieldsCount: 1,
});
});
});
});

describe('selectors', () => {
describe('conflictingFieldsCount', () => {
it('returns the number of conflicting fields', () => {
mount({ conflictingFields: { field_a: {}, field_b: {} } });
expect(MetaEngineSchemaLogic.values.conflictingFieldsCount).toEqual(2);
});
});

describe('hasConflictingFields', () => {
it('returns true when the conflictingFields obj has items', () => {
mount({ conflictingFields: { field_c: {} } });
expect(MetaEngineSchemaLogic.values.hasConflicts).toEqual(true);
});

it('returns false when the conflictingFields obj is empty', () => {
mount({ conflictingFields: {} });
expect(MetaEngineSchemaLogic.values.hasConflicts).toEqual(false);
});
});
});

describe('listeners', () => {
describe('loadSourceEngineSchema', () => {
it('calls the base loadSchema listener and onSchemaLoad', async () => {
cee-chen marked this conversation as resolved.
Show resolved Hide resolved
http.get.mockReturnValueOnce(Promise.resolve(MOCK_RESPONSE));
mount();
jest.spyOn(MetaEngineSchemaLogic.actions, 'loadSchema');
jest.spyOn(MetaEngineSchemaLogic.actions, 'onMetaEngineSchemaLoad');

MetaEngineSchemaLogic.actions.loadMetaEngineSchema();
await nextTick();

expect(MetaEngineSchemaLogic.actions.loadSchema).toHaveBeenCalled();
expect(MetaEngineSchemaLogic.actions.onMetaEngineSchemaLoad).toHaveBeenCalledWith(
MOCK_RESPONSE
);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { kea, MakeLogicType } from 'kea';

import { SchemaBaseLogic, SchemaBaseValues, SchemaBaseActions } from './schema_base_logic';
import { MetaEngineSchemaApiResponse } from './types';

interface MetaEngineSchemaValues extends SchemaBaseValues {
fields: MetaEngineSchemaApiResponse['fields'];
conflictingFields: MetaEngineSchemaApiResponse['conflictingFields'];
conflictingFieldsCount: number;
hasConflicts: boolean;
}

interface MetaEngineSchemaActions extends SchemaBaseActions {
loadMetaEngineSchema(): void;
onMetaEngineSchemaLoad(response: MetaEngineSchemaApiResponse): MetaEngineSchemaApiResponse;
}

export const MetaEngineSchemaLogic = kea<
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hopefully comparing the two different logic files between source engines & meta engines helps illustrate why I chose to separate them - they're all in 1 file right now in the standalone UI and it's pretty hard to grok as a result. Hoping this is at least tidier.

It's honestly weird because the Meta Engine view, aside from the schema data, is almost completely different from the default/indexed schema view - it has no actual actions to take for example.

Some day I'll have a breakthrough genius idea on how to better organize our meta engine vs indexed engine logic/views/etc., but that day is not today 🙃

MakeLogicType<MetaEngineSchemaValues, MetaEngineSchemaActions>
>({
path: ['enterprise_search', 'app_search', 'meta_engine_schema_logic'],
connect: {
values: [SchemaBaseLogic, ['dataLoading', 'schema']],
actions: [SchemaBaseLogic, ['loadSchema', 'setSchema']],
},
actions: {
loadMetaEngineSchema: true,
onMetaEngineSchemaLoad: (response) => response,
cee-chen marked this conversation as resolved.
Show resolved Hide resolved
},
reducers: {
fields: [
{},
{
onMetaEngineSchemaLoad: (_, { fields }) => fields,
},
],
conflictingFields: [
{},
{
onMetaEngineSchemaLoad: (_, { conflictingFields }) => conflictingFields,
},
],
},
selectors: {
conflictingFieldsCount: [
(selectors) => [selectors.conflictingFields],
(conflictingFields) => Object.keys(conflictingFields).length,
],
hasConflicts: [
(selectors) => [selectors.conflictingFieldsCount],
(conflictingFieldsCount) => conflictingFieldsCount > 0,
],
cee-chen marked this conversation as resolved.
Show resolved Hide resolved
},
listeners: ({ actions }) => ({
loadMetaEngineSchema: async () => {
cee-chen marked this conversation as resolved.
Show resolved Hide resolved
await actions.loadSchema(actions.onMetaEngineSchemaLoad);
},
}),
});
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export type Schema = Record<string, SchemaType>;

// This is a mapping of schema field types ("text", "number", "geolocation", "date")
// to the names of source engines which utilize that type
export type SchemaConflictFieldTypes = Record<SchemaType, string[]>;
export type SchemaConflictFieldTypes = Partial<Record<SchemaType, string[]>>;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure how I didn't run into a type error with this before now, but without the Partial<> Typescript yells if you have just e.g. { text: ['some-engine'], number: ['another-engine'] } - it wants all the keys from SchemaType otherwise 🤷


export interface SchemaConflict {
fieldTypes: SchemaConflictFieldTypes;
Expand Down