[ContentManagement] Use natural number for version (#153239)

This commit is contained in:
Sébastien Loix 2023-03-16 20:28:15 +00:00 committed by GitHub
parent 15b1dd2e45
commit 92a6a1a7ea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 219 additions and 247 deletions

View file

@ -22,7 +22,7 @@ export default {
const todosClient = new TodosClient();
const contentTypeRegistry = new ContentTypeRegistry();
contentTypeRegistry.register({ id: 'todos', version: { latest: 'v1' } });
contentTypeRegistry.register({ id: 'todos', version: { latest: 1 } });
const contentClient = new ContentClient((contentType: string) => {
switch (contentType) {

View file

@ -34,7 +34,7 @@ export class ContentManagementExamplesPlugin
contentManagement.registry.register({
id: 'todos',
version: {
latest: 'v1',
latest: 1,
},
});

View file

@ -34,7 +34,7 @@ export const registerTodoContentType = ({
id: TODO_CONTENT_ID,
storage: new TodosStorage(),
version: {
latest: 'v1',
latest: 1,
},
});
};

View file

@ -7,6 +7,7 @@
*/
export { initTransform } from './object_transform';
export {
getTransforms as getContentManagmentServicesTransforms,
compile as compileServiceDefinitions,

View file

@ -12,6 +12,8 @@ describe('utils', () => {
describe('validateVersion()', () => {
[
{ input: '123', isValid: true, expected: 123 },
{ input: '1111111111111111111111111', isValid: true, expected: 1111111111111111111111111 },
{ input: '111111111111.1111111111111', isValid: false, expected: null },
{ input: 123, isValid: true, expected: 123 },
{ input: 1.23, isValid: false, expected: null },
{ input: '123a', isValid: false, expected: null },

View file

@ -29,7 +29,9 @@ export const validateObj = (obj: unknown, objSchema?: Type<any>): ValidationErro
}
};
export const validateVersion = (version: unknown): { result: boolean; value: Version | null } => {
export const validateVersion = (
version: unknown
): { result: true; value: Version } | { result: false; value: null } => {
if (typeof version === 'string') {
const isValid = /^\d+$/.test(version);
if (isValid) {
@ -42,9 +44,15 @@ export const validateVersion = (version: unknown): { result: boolean; value: Ver
return { result: false, value: null };
} else {
const isValid = Number.isInteger(version);
if (isValid) {
return {
result: true,
value: version as Version,
};
}
return {
result: isValid,
value: isValid ? (version as Version) : null,
result: false,
value: null,
};
}
};

View file

@ -18,5 +18,3 @@ export type {
DeleteIn,
SearchIn,
} from './rpc';
export type { Version } from './types';

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { schema } from '@kbn/config-schema';
import type { Version } from '../types';
import type { Version } from '@kbn/object-versioning';
import { versionSchema } from './constants';
import type { ProcedureSchemas } from './types';

View file

@ -6,19 +6,17 @@
* Side Public License, v 1.
*/
import { schema } from '@kbn/config-schema';
import { validateVersion } from '../utils';
import { validateVersion } from '@kbn/object-versioning/lib/utils';
export const procedureNames = ['get', 'bulkGet', 'create', 'update', 'delete', 'search'] as const;
export type ProcedureName = typeof procedureNames[number];
export const versionSchema = schema.string({
export const versionSchema = schema.number({
validate: (value) => {
try {
validateVersion(value);
} catch (e) {
return 'must follow the pattern [v${number}]';
const { result } = validateVersion(value);
if (!result) {
return 'must be an integer';
}
},
});

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { schema } from '@kbn/config-schema';
import type { Version } from '../types';
import type { Version } from '@kbn/object-versioning';
import { versionSchema } from './constants';
import type { ProcedureSchemas } from './types';

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { schema } from '@kbn/config-schema';
import type { Version } from '../types';
import type { Version } from '@kbn/object-versioning';
import { versionSchema } from './constants';
import type { ProcedureSchemas } from './types';

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { schema } from '@kbn/config-schema';
import type { Version } from '../types';
import type { Version } from '@kbn/object-versioning';
import { versionSchema } from './constants';
import type { ProcedureSchemas } from './types';

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { schema } from '@kbn/config-schema';
import type { Version } from '../types';
import type { Version } from '@kbn/object-versioning';
import { versionSchema } from './constants';
import type { ProcedureSchemas } from './types';

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { schema } from '@kbn/config-schema';
import type { Version } from '../types';
import type { Version } from '@kbn/object-versioning';
import { versionSchema } from './constants';
import type { ProcedureSchemas } from './types';

View file

@ -1,9 +0,0 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
export type Version = `v${number}`;

View file

@ -1,57 +0,0 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { validateVersion } from './utils';
describe('utils', () => {
describe('validateVersion', () => {
const isValid = (version: unknown): boolean => {
try {
validateVersion(version);
return true;
} catch (e) {
return false;
}
};
[
{
version: 'v1',
expected: true,
},
{
version: 'v689584563',
expected: true,
},
{
version: 'v0', // Invalid: must be >= 1
expected: false,
},
{
version: 'av0',
expected: false,
},
{
version: '1',
expected: false,
},
{
version: 'vv1',
expected: false,
},
].forEach(({ version, expected }) => {
test(`should validate [${version}] version`, () => {
expect(isValid(version)).toBe(expected);
});
});
test('should return the version number', () => {
expect(validateVersion('v7')).toBe(7);
});
});
});

View file

@ -1,26 +0,0 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
/** Utility to validate that a content version follows the pattern `v${number}` */
export const validateVersion = (version: unknown): number => {
if (typeof version !== 'string') {
throw new Error(`Invalid version [${version}]. Must follow the pattern [v$\{number\}]`);
}
if (/^v\d+$/.test(version) === false) {
throw new Error(`Invalid version [${version}]. Must follow the pattern [v$\{number\}]`);
}
const versionNumber = parseInt(version.substring(1), 10);
if (versionNumber < 1) {
throw new Error(`Version must be >= 1`);
}
return versionNumber;
};

View file

@ -18,7 +18,7 @@ const setup = () => {
const contentTypeRegistry = new ContentTypeRegistry();
contentTypeRegistry.register({
id: 'testType',
version: { latest: 'v3' },
version: { latest: 3 },
});
const contentClient = new ContentClient(() => crudClient, contentTypeRegistry);
return { crudClient, contentClient, contentTypeRegistry };
@ -31,27 +31,28 @@ describe('#get', () => {
const output = { test: 'test' };
crudClient.get.mockResolvedValueOnce(output);
expect(await contentClient.get(input)).toEqual(output);
expect(crudClient.get).toBeCalledWith({ ...input, version: 'v3' }); // latest version added
expect(crudClient.get).toBeCalledWith({ ...input, version: 3 }); // latest version added
});
it('does not add the latest version if one is passed', async () => {
const { crudClient, contentClient } = setup();
const input: GetIn = { id: 'test', contentTypeId: 'testType', version: 'v1' };
const input: GetIn = { id: 'test', contentTypeId: 'testType', version: 1 };
await contentClient.get(input);
expect(crudClient.get).toBeCalledWith(input);
});
it('throws if version is not valid', async () => {
const { contentClient } = setup();
let input = { id: 'test', contentTypeId: 'testType', version: 'vv' }; // Invalid format
let input = { id: 'test', contentTypeId: 'testType', version: 'foo' }; // Invalid format
await expect(async () => {
contentClient.get(input as any);
}).rejects.toThrowError('Invalid version [vv]. Must follow the pattern [v${number}]');
}).rejects.toThrowError('Invalid version [foo]. Must be an integer.');
input = { id: 'test', contentTypeId: 'testType', version: 'v4' }; // Latest version is v3
// @ts-expect-error
input = { id: 'test', contentTypeId: 'testType', version: 4 }; // Latest version is 3
await expect(async () => {
contentClient.get(input as any);
}).rejects.toThrowError('Invalid version [v4]. Latest version is [v3]');
}).rejects.toThrowError('Invalid version [4]. Latest version is [3]');
});
it('calls rpcClient.get$ with input and returns output', async () => {
@ -84,12 +85,12 @@ describe('#create', () => {
crudClient.create.mockResolvedValueOnce(output);
expect(await contentClient.create(input)).toEqual(output);
expect(crudClient.create).toBeCalledWith({ ...input, version: 'v3' }); // latest version added
expect(crudClient.create).toBeCalledWith({ ...input, version: 3 }); // latest version added
});
it('does not add the latest version if one is passed', async () => {
const { crudClient, contentClient } = setup();
const input: CreateIn = { contentTypeId: 'testType', data: { foo: 'bar' }, version: 'v1' };
const input: CreateIn = { contentTypeId: 'testType', data: { foo: 'bar' }, version: 1 };
await contentClient.create(input);
expect(crudClient.create).toBeCalledWith(input);
});
@ -107,7 +108,7 @@ describe('#update', () => {
crudClient.update.mockResolvedValueOnce(output);
expect(await contentClient.update(input)).toEqual(output);
expect(crudClient.update).toBeCalledWith({ ...input, version: 'v3' }); // latest version added
expect(crudClient.update).toBeCalledWith({ ...input, version: 3 }); // latest version added
});
it('does not add the latest version if one is passed', async () => {
@ -117,7 +118,7 @@ describe('#update', () => {
contentTypeId: 'testType',
id: 'test',
data: { foo: 'bar' },
version: 'v1',
version: 1,
};
await contentClient.update(input);
expect(crudClient.update).toBeCalledWith(input);
@ -132,12 +133,12 @@ describe('#delete', () => {
crudClient.delete.mockResolvedValueOnce(output);
expect(await contentClient.delete(input)).toEqual(output);
expect(crudClient.delete).toBeCalledWith({ ...input, version: 'v3' }); // latest version added
expect(crudClient.delete).toBeCalledWith({ ...input, version: 3 }); // latest version added
});
it('does not add the latest version if one is passed', async () => {
const { crudClient, contentClient } = setup();
const input: DeleteIn = { contentTypeId: 'testType', id: 'test', version: 'v1' };
const input: DeleteIn = { contentTypeId: 'testType', id: 'test', version: 1 };
await contentClient.delete(input);
expect(crudClient.delete).toBeCalledWith(input);
});
@ -150,12 +151,12 @@ describe('#search', () => {
const output = { hits: [{ id: 'test' }] };
crudClient.search.mockResolvedValueOnce(output);
expect(await contentClient.search(input)).toEqual(output);
expect(crudClient.search).toBeCalledWith({ ...input, version: 'v3' }); // latest version added
expect(crudClient.search).toBeCalledWith({ ...input, version: 3 }); // latest version added
});
it('does not add the latest version if one is passed', async () => {
const { crudClient, contentClient } = setup();
const input: SearchIn = { contentTypeId: 'testType', query: {}, version: 'v1' };
const input: SearchIn = { contentTypeId: 'testType', query: {}, version: 1 };
await contentClient.search(input);
expect(crudClient.search).toBeCalledWith(input);
});

View file

@ -7,10 +7,11 @@
*/
import { QueryClient } from '@tanstack/react-query';
import { validateVersion } from '@kbn/object-versioning/lib/utils';
import type { Version } from '@kbn/object-versioning';
import { createQueryObservable } from './query_observable';
import type { CrudClient } from '../crud_client';
import type { CreateIn, GetIn, UpdateIn, DeleteIn, SearchIn, Version } from '../../common';
import { validateVersion } from '../../common/utils';
import type { CreateIn, GetIn, UpdateIn, DeleteIn, SearchIn } from '../../common';
import type { ContentTypeRegistry } from '../registry';
export const queryKeyBuilder = {
@ -35,9 +36,13 @@ const addVersion = <I extends { contentTypeId: string; version?: Version }>(
const version = input.version ?? contentType.version.latest;
const versionNumber = validateVersion(version);
const { result, value } = validateVersion(version);
if (versionNumber > parseInt(contentType.version.latest.substring(1), 10)) {
if (!result) {
throw new Error(`Invalid version [${version}]. Must be an integer.`);
}
if (value > contentType.version.latest) {
throw new Error(
`Invalid version [${version}]. Latest version is [${contentType.version.latest}]`
);

View file

@ -24,7 +24,7 @@ const setup = () => {
const contentTypeRegistry = new ContentTypeRegistry();
contentTypeRegistry.register({
id: 'testType',
version: { latest: 'v3' },
version: { latest: 3 },
});
const contentClient = new ContentClient(() => crudClient, contentTypeRegistry);
@ -42,7 +42,7 @@ const setup = () => {
describe('useCreateContentMutation', () => {
test('should call rpcClient.create with input and resolve with output', async () => {
const { Wrapper, crudClient } = setup();
const input: CreateIn = { contentTypeId: 'testType', data: { foo: 'bar' }, version: 'v2' };
const input: CreateIn = { contentTypeId: 'testType', data: { foo: 'bar' }, version: 2 };
const output = { test: 'test' };
crudClient.create.mockResolvedValueOnce(output);
const { result, waitFor } = renderHook(() => useCreateContentMutation(), { wrapper: Wrapper });
@ -61,7 +61,7 @@ describe('useUpdateContentMutation', () => {
contentTypeId: 'testType',
id: 'test',
data: { foo: 'bar' },
version: 'v2',
version: 2,
};
const output = { test: 'test' };
crudClient.update.mockResolvedValueOnce(output);
@ -77,7 +77,7 @@ describe('useUpdateContentMutation', () => {
describe('useDeleteContentMutation', () => {
test('should call rpcClient.delete with input and resolve with output', async () => {
const { Wrapper, crudClient } = setup();
const input: DeleteIn = { contentTypeId: 'testType', id: 'test', version: 'v2' };
const input: DeleteIn = { contentTypeId: 'testType', id: 'test', version: 2 };
const output = { test: 'test' };
crudClient.delete.mockResolvedValueOnce(output);
const { result, waitFor } = renderHook(() => useDeleteContentMutation(), { wrapper: Wrapper });

View file

@ -20,7 +20,7 @@ const setup = () => {
const contentTypeRegistry = new ContentTypeRegistry();
contentTypeRegistry.register({
id: 'testType',
version: { latest: 'v2' },
version: { latest: 2 },
});
const contentClient = new ContentClient(() => crudClient, contentTypeRegistry);
@ -38,7 +38,7 @@ const setup = () => {
describe('useGetContentQuery', () => {
test('should call rpcClient.get with input and resolve with output', async () => {
const { crudClient, Wrapper } = setup();
const input: GetIn = { id: 'test', contentTypeId: 'testType', version: 'v2' };
const input: GetIn = { id: 'test', contentTypeId: 'testType', version: 2 };
const output = { test: 'test' };
crudClient.get.mockResolvedValueOnce(output);
const { result, waitFor } = renderHook(() => useGetContentQuery(input), { wrapper: Wrapper });
@ -50,7 +50,7 @@ describe('useGetContentQuery', () => {
describe('useSearchContentQuery', () => {
test('should call rpcClient.search with input and resolve with output', async () => {
const { crudClient, Wrapper } = setup();
const input: SearchIn = { contentTypeId: 'testType', query: {}, version: 'v2' };
const input: SearchIn = { contentTypeId: 'testType', query: {}, version: 2 };
const output = { hits: [{ id: 'test' }] };
crudClient.search.mockResolvedValueOnce(output);
const { result, waitFor } = renderHook(() => useSearchContentQuery(input), {

View file

@ -10,7 +10,7 @@ import { ContentType } from './content_type';
import type { ContentTypeDefinition } from './content_type_definition';
test('create a content type with just an id', () => {
const type = new ContentType({ id: 'test', version: { latest: 'v1' } });
const type = new ContentType({ id: 'test', version: { latest: 1 } });
expect(type.id).toBe('test');
expect(type.name).toBe('test');
@ -24,7 +24,7 @@ test('create a content type with all the full definition', () => {
name: 'Test',
icon: 'test',
description: 'Test description',
version: { latest: 'v1' },
version: { latest: 1 },
};
const type = new ContentType(definition);

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import type { Version } from '../../common';
import type { Version } from '@kbn/object-versioning';
import type { CrudClient } from '../crud_client';
/**

View file

@ -15,7 +15,7 @@ beforeEach(() => {
});
const versionInfo = {
latest: 'v2',
latest: 2,
} as const;
test('registering a content type', () => {
@ -45,12 +45,21 @@ test('registering already registered content type throws', () => {
).toThrowErrorMatchingInlineSnapshot(`"Content type with id \\"test\\" already registered."`);
});
test('registering string number version converts it to number', () => {
registry.register({
id: 'test',
version: { latest: '123' },
} as any);
expect(registry.get('test')?.version).toEqual({ latest: 123 });
});
test('registering without version throws', () => {
expect(() => {
registry.register({
id: 'test',
} as any);
}).toThrowError('Invalid version [undefined]. Must follow the pattern [v${number}]');
}).toThrowError('Invalid version [undefined]. Must be an integer.');
});
test('registering invalid version throws', () => {
@ -61,13 +70,13 @@ test('registering invalid version throws', () => {
latest: 'bad',
},
} as any);
}).toThrowError('Invalid version [bad]. Must follow the pattern [v${number}]');
}).toThrowError('Invalid version [bad]. Must be an integer.');
expect(() => {
registry.register({
id: 'test',
version: {
latest: 'v0',
latest: 0,
},
});
}).toThrowError('Version must be >= 1');

View file

@ -6,9 +6,10 @@
* Side Public License, v 1.
*/
import { validateVersion } from '@kbn/object-versioning/lib/utils';
import type { ContentTypeDefinition } from './content_type_definition';
import { ContentType } from './content_type';
import { validateVersion } from '../../common/utils';
export class ContentTypeRegistry {
private readonly types: Map<string, ContentType> = new Map();
@ -18,9 +19,19 @@ export class ContentTypeRegistry {
throw new Error(`Content type with id "${definition.id}" already registered.`);
}
validateVersion(definition.version?.latest);
const { result, value } = validateVersion(definition.version?.latest);
if (!result) {
throw new Error(`Invalid version [${definition.version?.latest}]. Must be an integer.`);
}
const type = new ContentType(definition);
if (value < 1) {
throw new Error(`Version must be >= 1`);
}
const type = new ContentType({
...definition,
version: { ...definition.version, latest: value },
});
this.types.set(type.id, type);
return type;

View file

@ -36,4 +36,8 @@ export class ContentType {
public get crud() {
return this.contentCrud;
}
public get version() {
return this._definition.version;
}
}

View file

@ -40,8 +40,8 @@ const setup = ({ registerFooType = false }: { registerFooType?: boolean } = {})
const ctx: StorageContext = {
requestHandlerContext: {} as any,
version: {
latest: 'v1',
request: 'v1',
latest: 1,
request: 1,
},
utils: {
getTransforms: jest.fn(),
@ -54,7 +54,7 @@ const setup = ({ registerFooType = false }: { registerFooType?: boolean } = {})
id: FOO_CONTENT_ID,
storage: createMemoryStorage(),
version: {
latest: 'v2',
latest: 2,
},
};
const cleanUp = () => {
@ -107,7 +107,24 @@ describe('Content Core', () => {
// Make sure the "register" exposed by the api is indeed registring
// the content into our "contentRegistry" instance
expect(contentRegistry.isContentRegistered(FOO_CONTENT_ID)).toBe(true);
expect(contentRegistry.getDefinition(FOO_CONTENT_ID)).toBe(contentDefinition);
expect(contentRegistry.getDefinition(FOO_CONTENT_ID)).toEqual(contentDefinition);
cleanUp();
});
test('should convert the latest version to number if string is passed', () => {
const { coreSetup, cleanUp, contentDefinition } = setup();
const {
contentRegistry,
api: { register },
} = coreSetup;
register({ ...contentDefinition, version: { latest: '123' } } as any);
expect(contentRegistry.getContentType(contentDefinition.id).version).toEqual({
latest: 123,
});
cleanUp();
});
@ -124,10 +141,10 @@ describe('Content Core', () => {
expect(() => {
register({ ...contentDefinition, version: undefined } as any);
}).toThrowError('Invalid version [undefined]. Must follow the pattern [v${number}]');
}).toThrowError('Invalid version [undefined]. Must be an integer.');
expect(() => {
register({ ...contentDefinition, version: { latest: 'v0' } });
register({ ...contentDefinition, version: { latest: 0 } });
}).toThrowError('Version must be >= 1');
cleanUp();

View file

@ -6,7 +6,7 @@
* Side Public License, v 1.
*/
import { validateVersion } from '../../common/utils';
import { validateVersion } from '@kbn/object-versioning/lib/utils';
import { ContentType } from './content_type';
import { EventBus } from './event_bus';
import type { ContentStorage, ContentTypeDefinition } from './types';
@ -27,9 +27,19 @@ export class ContentRegistry {
throw new Error(`Content [${definition.id}] is already registered`);
}
validateVersion(definition.version?.latest);
const { result, value } = validateVersion(definition.version?.latest);
if (!result) {
throw new Error(`Invalid version [${definition.version?.latest}]. Must be an integer.`);
}
const contentType = new ContentType(definition, this.eventBus);
if (value < 1) {
throw new Error(`Version must be >= 1`);
}
const contentType = new ContentType(
{ ...definition, version: { ...definition.version, latest: value } },
this.eventBus
);
this.types.set(contentType.id, contentType);
}

View file

@ -7,15 +7,14 @@
*/
import type { RequestHandlerContext } from '@kbn/core-http-request-handler-context-server';
import type { ContentManagementGetTransformsFn } from '@kbn/object-versioning';
import type { Version as LegacyVersion } from '../../common';
import type { ContentManagementGetTransformsFn, Version } from '@kbn/object-versioning';
/** Context that is sent to all storage instance methods */
export interface StorageContext {
requestHandlerContext: RequestHandlerContext;
version: {
request: LegacyVersion;
latest: LegacyVersion;
request: Version;
latest: Version;
};
utils: {
getTransforms: ContentManagementGetTransformsFn;
@ -48,6 +47,6 @@ export interface ContentTypeDefinition<S extends ContentStorage = ContentStorage
/** The storage layer for the content. It must implment the ContentStorage interface. */
storage: S;
version: {
latest: LegacyVersion;
latest: Version;
};
}

View file

@ -35,7 +35,7 @@ const FOO_CONTENT_ID = 'foo';
describe('RPC -> bulkGet()', () => {
describe('Input/Output validation', () => {
const ids = ['123', '456'];
const validInput = { contentTypeId: 'foo', ids, version: 'v1' };
const validInput = { contentTypeId: 'foo', ids, version: 1 };
/**
* These tests are for the procedure call itself. Every RPC needs to declare in/out schema
@ -50,11 +50,10 @@ describe('RPC -> bulkGet()', () => {
},
{
input: omit(validInput, 'version'),
expectedError: '[version]: expected value of type [string] but got [undefined]',
expectedError: '[version]: expected value of type [number] but got [undefined]',
},
{
input: { ...validInput, version: '1' }, // invalid version format
expectedError: '[version]: must follow the pattern [v${number}]',
input: { ...validInput, version: '1' }, // string number is OK
},
{
input: omit(validInput, 'ids'),
@ -96,7 +95,7 @@ describe('RPC -> bulkGet()', () => {
{
contentTypeId: 'foo',
ids: ['123'],
version: 'v1',
version: 1,
options: { any: 'object' },
},
inputSchema
@ -107,7 +106,7 @@ describe('RPC -> bulkGet()', () => {
error = validate(
{
contentTypeId: 'foo',
version: 'v1',
version: 1,
ids: ['123'],
options: 123, // Not an object
},
@ -157,7 +156,7 @@ describe('RPC -> bulkGet()', () => {
id: FOO_CONTENT_ID,
storage,
version: {
latest: 'v2',
latest: 2,
},
});
@ -179,7 +178,7 @@ describe('RPC -> bulkGet()', () => {
const result = await fn(ctx, {
contentTypeId: FOO_CONTENT_ID,
version: 'v1',
version: 1,
ids: ['123', '456'],
});
@ -192,8 +191,8 @@ describe('RPC -> bulkGet()', () => {
{
requestHandlerContext: ctx.requestHandlerContext,
version: {
request: 'v1',
latest: 'v2', // from the registry
request: 1,
latest: 2, // from the registry
},
utils: {
getTransforms: expect.any(Function),
@ -218,16 +217,16 @@ describe('RPC -> bulkGet()', () => {
fn(ctx, {
contentTypeId: FOO_CONTENT_ID,
ids: ['123', '456'],
version: 'v7',
version: 7,
})
).rejects.toEqual(new Error('Invalid version. Latest version is [v2].'));
).rejects.toEqual(new Error('Invalid version. Latest version is [2].'));
});
});
describe('object versioning', () => {
test('should expose a utility to transform and validate services objects', () => {
const { ctx, storage } = setup();
fn(ctx, { contentTypeId: FOO_CONTENT_ID, ids: ['1234'], version: 'v1' });
fn(ctx, { contentTypeId: FOO_CONTENT_ID, ids: ['1234'], version: 1 });
const [[storageContext]] = storage.bulkGet.mock.calls;
// getTransforms() utils should be available from context

View file

@ -34,7 +34,7 @@ const FOO_CONTENT_ID = 'foo';
describe('RPC -> create()', () => {
describe('Input/Output validation', () => {
const validInput = { contentTypeId: 'foo', version: 'v1', data: { title: 'hello' } };
const validInput = { contentTypeId: 'foo', version: 1, data: { title: 'hello' } };
test('should validate the input', () => {
[
@ -45,11 +45,10 @@ describe('RPC -> create()', () => {
},
{
input: omit(validInput, 'version'),
expectedError: '[version]: expected value of type [string] but got [undefined]',
expectedError: '[version]: expected value of type [number] but got [undefined]',
},
{
input: { ...validInput, version: '1' }, // invalid version format
expectedError: '[version]: must follow the pattern [v${number}]',
input: { ...validInput, version: '1' }, // string number is OK
},
{
input: omit(validInput, 'data'),
@ -83,7 +82,7 @@ describe('RPC -> create()', () => {
{
contentTypeId: 'foo',
data: { title: 'hello' },
version: 'v1',
version: 1,
options: { any: 'object' },
},
inputSchema
@ -95,7 +94,7 @@ describe('RPC -> create()', () => {
{
contentTypeId: 'foo',
data: { title: 'hello' },
version: 'v1',
version: 1,
options: 123, // Not an object
},
inputSchema
@ -130,7 +129,7 @@ describe('RPC -> create()', () => {
id: FOO_CONTENT_ID,
storage,
version: {
latest: 'v2',
latest: 2,
},
});
@ -152,7 +151,7 @@ describe('RPC -> create()', () => {
const result = await fn(ctx, {
contentTypeId: FOO_CONTENT_ID,
version: 'v1',
version: 1,
data: { title: 'Hello' },
});
@ -165,8 +164,8 @@ describe('RPC -> create()', () => {
{
requestHandlerContext: ctx.requestHandlerContext,
version: {
request: 'v1',
latest: 'v2', // from the registry
request: 1,
latest: 2, // from the registry
},
utils: {
getTransforms: expect.any(Function),
@ -191,16 +190,16 @@ describe('RPC -> create()', () => {
fn(ctx, {
contentTypeId: FOO_CONTENT_ID,
data: { title: 'Hello' },
version: 'v7',
version: 7,
})
).rejects.toEqual(new Error('Invalid version. Latest version is [v2].'));
).rejects.toEqual(new Error('Invalid version. Latest version is [2].'));
});
});
describe('object versioning', () => {
test('should expose a utility to transform and validate services objects', () => {
const { ctx, storage } = setup();
fn(ctx, { contentTypeId: FOO_CONTENT_ID, data: { title: 'Hello' }, version: 'v1' });
fn(ctx, { contentTypeId: FOO_CONTENT_ID, data: { title: 'Hello' }, version: 1 });
const [[storageContext]] = storage.create.mock.calls;
// getTransforms() utils should be available from context

View file

@ -34,7 +34,7 @@ const FOO_CONTENT_ID = 'foo';
describe('RPC -> delete()', () => {
describe('Input/Output validation', () => {
const validInput = { contentTypeId: 'foo', id: '123', version: 'v1' };
const validInput = { contentTypeId: 'foo', id: '123', version: 1 };
test('should validate that a contentTypeId and an id is passed', () => {
[
@ -53,11 +53,10 @@ describe('RPC -> delete()', () => {
},
{
input: omit(validInput, 'version'),
expectedError: '[version]: expected value of type [string] but got [undefined]',
expectedError: '[version]: expected value of type [number] but got [undefined]',
},
{
input: { ...validInput, version: '1' }, // invalid version format
expectedError: '[version]: must follow the pattern [v${number}]',
input: { ...validInput, version: '1' }, // string number is OK
},
].forEach(({ input, expectedError }) => {
const error = validate(input, inputSchema);
@ -79,7 +78,7 @@ describe('RPC -> delete()', () => {
{
contentTypeId: 'foo',
id: '123',
version: 'v1',
version: 1,
options: { any: 'object' },
},
inputSchema
@ -91,7 +90,7 @@ describe('RPC -> delete()', () => {
{
contentTypeId: 'foo',
id: '123',
version: 'v1',
version: 1,
options: 123, // Not an object
},
inputSchema
@ -126,7 +125,7 @@ describe('RPC -> delete()', () => {
id: FOO_CONTENT_ID,
storage,
version: {
latest: 'v2',
latest: 2,
},
});
@ -146,7 +145,7 @@ describe('RPC -> delete()', () => {
const expected = 'DeleteResult';
storage.delete.mockResolvedValueOnce(expected);
const result = await fn(ctx, { contentTypeId: FOO_CONTENT_ID, version: 'v1', id: '1234' });
const result = await fn(ctx, { contentTypeId: FOO_CONTENT_ID, version: 1, id: '1234' });
expect(result).toEqual({
contentTypeId: FOO_CONTENT_ID,
@ -157,8 +156,8 @@ describe('RPC -> delete()', () => {
{
requestHandlerContext: ctx.requestHandlerContext,
version: {
request: 'v1',
latest: 'v2', // from the registry
request: 1,
latest: 2, // from the registry
},
utils: {
getTransforms: expect.any(Function),
@ -183,16 +182,16 @@ describe('RPC -> delete()', () => {
fn(ctx, {
contentTypeId: FOO_CONTENT_ID,
id: '1234',
version: 'v7',
version: 7,
})
).rejects.toEqual(new Error('Invalid version. Latest version is [v2].'));
).rejects.toEqual(new Error('Invalid version. Latest version is [2].'));
});
});
describe('object versioning', () => {
test('should expose a utility to transform and validate services objects', () => {
const { ctx, storage } = setup();
fn(ctx, { contentTypeId: FOO_CONTENT_ID, id: '1234', version: 'v1' });
fn(ctx, { contentTypeId: FOO_CONTENT_ID, id: '1234', version: 1 });
const [[storageContext]] = storage.delete.mock.calls;
// getTransforms() utils should be available from context

View file

@ -34,7 +34,7 @@ const FOO_CONTENT_ID = 'foo';
describe('RPC -> get()', () => {
describe('Input/Output validation', () => {
const validInput = { contentTypeId: 'foo', id: '123', version: 'v1' };
const validInput = { contentTypeId: 'foo', id: '123', version: 1 };
test('should validate that a contentTypeId and an id is passed', () => {
[
@ -53,11 +53,10 @@ describe('RPC -> get()', () => {
},
{
input: omit(validInput, 'version'),
expectedError: '[version]: expected value of type [string] but got [undefined]',
expectedError: '[version]: expected value of type [number] but got [undefined]',
},
{
input: { ...validInput, version: '1' }, // invalid version format
expectedError: '[version]: must follow the pattern [v${number}]',
input: { ...validInput, version: '1' }, // string number is OK
},
].forEach(({ input, expectedError }) => {
const error = validate(input, inputSchema);
@ -79,7 +78,7 @@ describe('RPC -> get()', () => {
{
contentTypeId: 'foo',
id: '123',
version: 'v1',
version: 1,
options: { any: 'object' },
},
inputSchema
@ -91,7 +90,7 @@ describe('RPC -> get()', () => {
{
contentTypeId: 'foo',
id: '123',
version: 'v1',
version: 1,
options: 123, // Not an object
},
inputSchema
@ -126,7 +125,7 @@ describe('RPC -> get()', () => {
id: FOO_CONTENT_ID,
storage,
version: {
latest: 'v2',
latest: 2,
},
});
@ -146,7 +145,7 @@ describe('RPC -> get()', () => {
const expected = 'GetResult';
storage.get.mockResolvedValueOnce(expected);
const result = await fn(ctx, { contentTypeId: FOO_CONTENT_ID, id: '1234', version: 'v1' });
const result = await fn(ctx, { contentTypeId: FOO_CONTENT_ID, id: '1234', version: 1 });
expect(result).toEqual({
contentTypeId: FOO_CONTENT_ID,
@ -157,8 +156,8 @@ describe('RPC -> get()', () => {
{
requestHandlerContext: ctx.requestHandlerContext,
version: {
request: 'v1',
latest: 'v2', // from the registry
request: 1,
latest: 2, // from the registry
},
utils: {
getTransforms: expect.any(Function),
@ -183,16 +182,16 @@ describe('RPC -> get()', () => {
fn(ctx, {
contentTypeId: FOO_CONTENT_ID,
id: '1234',
version: 'v7',
version: 7,
})
).rejects.toEqual(new Error('Invalid version. Latest version is [v2].'));
).rejects.toEqual(new Error('Invalid version. Latest version is [2].'));
});
});
describe('object versioning', () => {
test('should expose a utility to transform and validate services objects', () => {
const { ctx, storage } = setup();
fn(ctx, { contentTypeId: FOO_CONTENT_ID, id: '1234', version: 'v1' });
fn(ctx, { contentTypeId: FOO_CONTENT_ID, id: '1234', version: 1 });
const [[storageContext]] = storage.get.mock.calls;
// getTransforms() utils should be available from context

View file

@ -35,7 +35,7 @@ const FOO_CONTENT_ID = 'foo';
describe('RPC -> search()', () => {
describe('Input/Output validation', () => {
const query = { title: 'hello' };
const validInput = { contentTypeId: 'foo', version: 'v1', query };
const validInput = { contentTypeId: 'foo', version: 1, query };
test('should validate that a contentTypeId and "query" object is passed', () => {
[
@ -46,11 +46,14 @@ describe('RPC -> search()', () => {
},
{
input: omit(validInput, 'version'),
expectedError: '[version]: expected value of type [string] but got [undefined]',
expectedError: '[version]: expected value of type [number] but got [undefined]',
},
{
input: { ...validInput, version: '1' }, // invalid version format
expectedError: '[version]: must follow the pattern [v${number}]',
input: { ...validInput, version: '1' }, // string number is OK
},
{
input: { ...validInput, version: 'foo' }, // invalid version format
expectedError: '[version]: expected value of type [number] but got [string]',
},
{
input: omit(validInput, 'query'),
@ -84,7 +87,7 @@ describe('RPC -> search()', () => {
{
contentTypeId: 'foo',
query: { title: 'hello' },
version: 'v1',
version: 1,
options: { any: 'object' },
},
inputSchema
@ -95,7 +98,7 @@ describe('RPC -> search()', () => {
error = validate(
{
contentTypeId: 'foo',
version: 'v1',
version: 1,
query: { title: 'hello' },
options: 123, // Not an object
},
@ -145,7 +148,7 @@ describe('RPC -> search()', () => {
id: FOO_CONTENT_ID,
storage,
version: {
latest: 'v2',
latest: 2,
},
});
@ -167,7 +170,7 @@ describe('RPC -> search()', () => {
const result = await fn(ctx, {
contentTypeId: FOO_CONTENT_ID,
version: 'v1', // version in request
version: 1, // version in request
query: { title: 'Hello' },
});
@ -180,8 +183,8 @@ describe('RPC -> search()', () => {
{
requestHandlerContext: ctx.requestHandlerContext,
version: {
request: 'v1',
latest: 'v2', // from the registry
request: 1,
latest: 2, // from the registry
},
utils: {
getTransforms: expect.any(Function),
@ -206,16 +209,16 @@ describe('RPC -> search()', () => {
fn(ctx, {
contentTypeId: FOO_CONTENT_ID,
query: { title: 'Hello' },
version: 'v7',
version: 7,
})
).rejects.toEqual(new Error('Invalid version. Latest version is [v2].'));
).rejects.toEqual(new Error('Invalid version. Latest version is [2].'));
});
});
describe('object versioning', () => {
test('should expose a utility to transform and validate services objects', () => {
const { ctx, storage } = setup();
fn(ctx, { contentTypeId: FOO_CONTENT_ID, query: { title: 'Hello' }, version: 'v1' });
fn(ctx, { contentTypeId: FOO_CONTENT_ID, query: { title: 'Hello' }, version: 1 });
const [[storageContext]] = storage.search.mock.calls;
// getTransforms() utils should be available from context

View file

@ -35,7 +35,7 @@ const FOO_CONTENT_ID = 'foo';
describe('RPC -> update()', () => {
describe('Input/Output validation', () => {
const data = { title: 'hello' };
const validInput = { contentTypeId: 'foo', id: '123', version: 'v1', data };
const validInput = { contentTypeId: 'foo', id: '123', version: 1, data };
test('should validate that a "contentTypeId", an "id" and "data" object is passed', () => {
[
@ -54,11 +54,10 @@ describe('RPC -> update()', () => {
},
{
input: omit(validInput, 'version'),
expectedError: '[version]: expected value of type [string] but got [undefined]',
expectedError: '[version]: expected value of type [number] but got [undefined]',
},
{
input: { ...validInput, version: '1' }, // invalid version format
expectedError: '[version]: must follow the pattern [v${number}]',
input: { ...validInput, version: '1' }, // string number is OK
},
{
input: omit(validInput, 'data'),
@ -88,7 +87,7 @@ describe('RPC -> update()', () => {
{
contentTypeId: 'foo',
id: '123',
version: 'v1',
version: 1,
data: { title: 'hello' },
options: { any: 'object' },
},
@ -102,7 +101,7 @@ describe('RPC -> update()', () => {
contentTypeId: 'foo',
data: { title: 'hello' },
id: '123',
version: 'v1',
version: 1,
options: 123, // Not an object
},
inputSchema
@ -137,7 +136,7 @@ describe('RPC -> update()', () => {
id: FOO_CONTENT_ID,
storage,
version: {
latest: 'v2',
latest: 2,
},
});
@ -160,7 +159,7 @@ describe('RPC -> update()', () => {
const result = await fn(ctx, {
contentTypeId: FOO_CONTENT_ID,
id: '123',
version: 'v1',
version: 1,
data: { title: 'Hello' },
});
@ -173,8 +172,8 @@ describe('RPC -> update()', () => {
{
requestHandlerContext: ctx.requestHandlerContext,
version: {
request: 'v1',
latest: 'v2', // from the registry
request: 1,
latest: 2, // from the registry
},
utils: {
getTransforms: expect.any(Function),
@ -201,9 +200,9 @@ describe('RPC -> update()', () => {
contentTypeId: FOO_CONTENT_ID,
id: '123',
data: { title: 'Hello' },
version: 'v7',
version: 7,
})
).rejects.toEqual(new Error('Invalid version. Latest version is [v2].'));
).rejects.toEqual(new Error('Invalid version. Latest version is [2].'));
});
});
@ -213,7 +212,7 @@ describe('RPC -> update()', () => {
fn(ctx, {
contentTypeId: FOO_CONTENT_ID,
id: '123',
version: 'v1',
version: 1,
data: { title: 'Hello' },
});
const [[storageContext]] = storage.update.mock.calls;

View file

@ -6,8 +6,8 @@
* Side Public License, v 1.
*/
import { validateVersion } from '../../../common/utils';
import type { Version } from '../../../common';
import { validateVersion } from '@kbn/object-versioning/lib/utils';
import type { Version } from '@kbn/object-versioning';
export const validateRequestVersion = (
requestVersion: Version | undefined,
@ -18,12 +18,15 @@ export const validateRequestVersion = (
throw new Error('Request version missing');
}
const requestVersionNumber = validateVersion(requestVersion);
const latestVersionNumber = parseInt(latestVersion.substring(1), 10);
const { result, value: requestVersionNumber } = validateVersion(requestVersion);
if (requestVersionNumber > latestVersionNumber) {
if (!result) {
throw new Error(`Invalid version [${requestVersion}]. Must be an integer.`);
}
if (requestVersionNumber > latestVersion) {
throw new Error(`Invalid version. Latest version is [${latestVersion}].`);
}
return requestVersion;
return requestVersionNumber;
};