Skip to content

Commit

Permalink
feat(taxonomy): added metadata options endpoint (#3678)
Browse files Browse the repository at this point in the history
* feat(taxonomy): added metadata options endpoint

* feat(taxonomy): fixed flow errors

* feat(taxonomy): added unit test for getMetadataOptionsUrl

* feat(taxonomy): removed unused variable

* feat(taxonomy): added doc block

* feat(taxonomy): added marker in test

* feat(taxonomy): updated abort logic

* feat(taxonomy): changed abort class

* feat(taxonomy): improved abort handling

* feat(taxonomy): test enhancements

* feat(taxonomy): simplified function call

* feat(taxonomy): added level in query string params

* feat(taxonomy): updated test
  • Loading branch information
TylerGauntlett authored Oct 4, 2024
1 parent 5c54eb5 commit 47ba331
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 2 deletions.
77 changes: 77 additions & 0 deletions src/api/Metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import uniqueId from 'lodash/uniqueId';
import isEmpty from 'lodash/isEmpty';
import { getBadItemError, getBadPermissionsError, isUserCorrectableError } from '../utils/error';
import { getTypedFileId } from '../utils/file';
import { handleOnAbort } from './utils';
import File from './File';
import {
HEADER_CONTENT_TYPE,
Expand All @@ -28,6 +29,7 @@ import {
ERROR_CODE_FETCH_METADATA,
ERROR_CODE_FETCH_METADATA_TEMPLATES,
ERROR_CODE_FETCH_SKILLS,
ERROR_CODE_FETCH_METADATA_OPTIONS,
ERROR_CODE_FETCH_METADATA_SUGGESTIONS,
ERROR_CODE_EMPTY_METADATA_SUGGESTIONS,
TYPE_FILE,
Expand Down Expand Up @@ -1053,6 +1055,81 @@ class Metadata extends File {

return getProp(suggestionsResponse, 'data.suggestions', []);
}

/**
* Build URL for metadata options associated to a taxonomy field.
*
* @param scope
* @param templateKey
* @param fieldKey
* @returns {`${string}/metadata_templates/${string}/${string}/fields/${string}/options`}
*/
getMetadataOptionsUrl(scope: string, templateKey: string, fieldKey: string): string {
return `${this.getBaseApiUrl()}/metadata_templates/${scope}/${templateKey}/fields/${fieldKey}/options`;
}
/**
* Gets options associated with a taxonomy field.
*
* @param id
* @param scope
* @param templateKey
* @param fieldKey
* @param level
* @param options
* @returns {Promise<MetadataOptions>}
*/
async getMetadataOptions(
id: string,
scope: string,
templateKey: string,
fieldKey: string,
level: number,
options: { marker?: string, searchInput?: string, signal?: AbortSignal },
) {
this.errorCode = ERROR_CODE_FETCH_METADATA_OPTIONS;

if (!id) {
throw getBadItemError();
}

if (!scope) {
throw new Error('Missing scope');
}

if (!templateKey) {
throw new Error('Missing templateKey');
}

if (!fieldKey) {
throw new Error('Missing fieldKey');
}

// 0 is a valid level value
if (!level && level !== 0) {
throw new Error('Missing level');
}

const url = this.getMetadataOptionsUrl(scope, templateKey, fieldKey);
const { marker, searchInput, signal } = options;
const params = {
...(marker ? { marker } : {}),
...(searchInput ? { searchInput } : {}),
...(level || level === 0 ? { level } : {}),
};

if (signal) {
signal.onabort = () => handleOnAbort(this.xhr);
}

const metadataOptions = await this.xhr.get({
url,
id: getTypedFileId(id),
params,
});

return getProp(metadataOptions, 'data', {});
}
}

export default Metadata;
113 changes: 113 additions & 0 deletions src/api/__tests__/Metadata.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,16 @@ import {
METADATA_TEMPLATE_PROPERTIES,
TYPE_FILE,
ERROR_CODE_EMPTY_METADATA_SUGGESTIONS,
ERROR_CODE_FETCH_METADATA_OPTIONS,
} from '../../constants';
import { handleOnAbort } from '../utils';

let metadata: Metadata;

jest.mock('../utils', () => ({
handleOnAbort: jest.fn(),
}));

describe('api/Metadata', () => {
beforeEach(() => {
metadata = new Metadata({});
Expand Down Expand Up @@ -2878,4 +2884,111 @@ describe('api/Metadata', () => {
});
});
});

describe('getMetadataOptions()', () => {
test('should return metadata options when called with valid parameters', async () => {
const response = {
entries: [
{
id: '1',
display_name: 'Foo',
level: 0,
ancestors: [{ id: '2', display_name: 'Bar', level: 1 }],
deprecated: false,
selectable: true,
},
],
next_marker: 'next_marker',
result_count: 1,
};
const abortController = new AbortController();

metadata.getMetadataOptionsUrl = jest.fn().mockReturnValueOnce('options_url');
metadata.xhr.get = jest.fn().mockReturnValueOnce({ data: response });

const options = {
marker: 'current_marker',
signal: abortController.signal,
searchInput: 'search_term',
};

const metadataOptions = await metadata.getMetadataOptions(
'id',
'enterprise',
'templateKey',
'fieldKey',
0,
options,
);

expect(metadata.errorCode).toBe(ERROR_CODE_FETCH_METADATA_OPTIONS);
expect(metadataOptions).toEqual(response);
expect(metadata.getMetadataOptionsUrl).toHaveBeenCalled();
expect(metadata.xhr.get).toHaveBeenCalledWith({
url: 'options_url',
id: 'file_id',
params: {
marker: 'current_marker',
searchInput: 'search_term',
level: 0,
},
});
});

test('should build getMetadataOptionsUrl correctly', async () => {
const url = metadata.getMetadataOptionsUrl('enterprise', 'templateKey', 'fieldKey');

expect(url).toBe(
'https://api.box.com/2.0/metadata_templates/enterprise/templateKey/fields/fieldKey/options',
);
});

test('should throw an error if id is missing', async () => {
await expect(() =>
metadata.getMetadataOptions('', 'enterprise', 'templateKey', 'fieldKey', 'level', {}),
).rejects.toThrow(ErrorUtil.getBadItemError());
});

test('should throw an error if scope is missing', async () => {
await expect(() =>
metadata.getMetadataOptions('id', '', 'templateKey', 'fieldKey', 'level', {}),
).rejects.toThrow(new Error('Missing scope'));
});

test('should throw an error if templateKey is missing', async () => {
await expect(() =>
metadata.getMetadataOptions('id', 'enterprise', '', 'fieldKey', 'level', {}),
).rejects.toThrow(new Error('Missing templateKey'));
});

test('should throw an error if fieldKey is missing', async () => {
await expect(() =>
metadata.getMetadataOptions('id', 'enterprise', 'templateKey', '', 'level', {}),
).rejects.toThrow(new Error('Missing fieldKey'));
});

test('should throw an error if level is missing', async () => {
await expect(() =>
metadata.getMetadataOptions('id', 'enterprise', 'templateKey', 'fieldKey', '', {}),
).rejects.toThrow(new Error('Missing level'));
});

test('should abort when onabort is called', async () => {
const abortController = new AbortController();

const options = {
marker: null,
signal: abortController.signal,
searchInput: '',
};

metadata.xhr.get = jest.fn().mockReturnValueOnce(new Promise(() => {}));

metadata.getMetadataOptions('id', 'enterprise', 'templateKey', 'fieldKey', 0, options);

abortController.abort();

expect(handleOnAbort).toHaveBeenCalled();
});
});
});
20 changes: 19 additions & 1 deletion src/api/__tests__/utils.test.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,29 @@
import { formatComment } from '../utils';
import Xhr from '../../utils/Xhr';
import { getAbortError } from '../../utils/error';
import { formatComment, handleOnAbort } from '../utils';
import { threadedComments, threadedCommentsFormatted } from '../fixtures';

jest.mock('../../utils/Xhr', () => {
return jest.fn().mockImplementation(() => ({
abort: jest.fn(),
}));
});

describe('api/utils', () => {
describe('formatComment()', () => {
test('should return a comment and its replies with tagged_message property equal to message', () => {
expect(formatComment(threadedComments[0])).toMatchObject(threadedCommentsFormatted[0]);
expect(formatComment(threadedComments[1])).toMatchObject(threadedCommentsFormatted[1]);
});
});

describe('handleOnAbort()', () => {
test('should abort and throw when called', async () => {
const xhr = new Xhr();

await expect(() => handleOnAbort(xhr)).toThrow(getAbortError());

expect(xhr.abort).toHaveBeenCalled();
});
});
});
8 changes: 8 additions & 0 deletions src/api/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
* @author Box
*/

import type Xhr from '../utils/Xhr';
import type { Comment } from '../common/types/feed';
import { getAbortError } from '../utils/error';

/**
* Formats comment data (including replies) for use in components.
Expand All @@ -25,6 +27,12 @@ export const formatComment = (comment: Comment): Comment => {
return formattedComment;
};

export const handleOnAbort = (xhr: Xhr) => {
xhr.abort();

throw getAbortError();
};

export default {
formatComment,
};
22 changes: 22 additions & 0 deletions src/common/types/metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,27 @@ type MetadataSuggestion = {
suggestions: { [key: string]: string | number | string[] },
};

type MetadataOptionEntryAncestor = {
id: string,
display_name: string,
level: string,
};

type MetadataOptionEntry = {
id: string,
display_name: string,
level: string,
ancestors: MetadataOptionEntryAncestor[],
deprecated: boolean,
selectable: boolean,
};

type MetadataOptions = {
entries: MetadataOptionEntry[],
next_marker: string | null,
result_count: number,
};

type MetadataTemplateInstanceField = {
description?: string,
displayName?: string,
Expand Down Expand Up @@ -156,4 +177,5 @@ export type {
MetadataInstanceV2,
MetadataEditor,
MetadataSuggestion,
MetadataOptions,
};
1 change: 1 addition & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ export const ERROR_CODE_FETCH_ACCESS_STATS = 'fetch_access_stats_error';
export const ERROR_CODE_FETCH_SKILLS = 'fetch_skills_error';
export const ERROR_CODE_FETCH_RECENTS = 'fetch_recents_error';
export const ERROR_CODE_FETCH_METADATA_SUGGESTIONS = 'fetch_metadata_suggestions_error';
export const ERROR_CODE_FETCH_METADATA_OPTIONS = 'fetch_metadata_options_error';
export const ERROR_CODE_EMPTY_METADATA_SUGGESTIONS = 'empty_metadata_suggestions_error';
export const ERROR_CODE_EXECUTE_INTEGRATION = 'execute_integrations_error';
export const ERROR_CODE_EXTRACT_STRUCTURED = 'extract_structured_error';
Expand Down
20 changes: 19 additions & 1 deletion src/utils/error.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,22 @@ function isUserCorrectableError(status: number) {
);
}

export { getBadItemError, getBadPermissionsError, getBadUserError, getMissingItemTextOrStatus, isUserCorrectableError };
function getAbortError() {
class AbortError extends Error {
constructor(message: string) {
super(message);
this.name = 'AbortError';
}
}

return new AbortError('Aborted');
}

export {
getAbortError,
getBadItemError,
getBadPermissionsError,
getBadUserError,
getMissingItemTextOrStatus,
isUserCorrectableError,
};

0 comments on commit 47ba331

Please sign in to comment.