Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 38 additions & 3 deletions src/api/Metadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
formatMetadataFieldValue,
handleOnAbort,
mapDetailedFieldToConfidenceScore,
mergeDetailedAndHydratedInstances,
parseTargetLocation,
} from './utils';
import File from './File';
Expand Down Expand Up @@ -391,6 +392,35 @@ class Metadata extends File {
return response;
}

/**
* Fetches both detailed and hydrated metadata views and merges them so that
* the result is in detailed format but with hydrated taxonomy values.
*
* @param {string} baseUrl - metadata API base URL
* @param {string} requestId - typed file id
* @return {Array} merged metadata instances
*/
async getDetailedInstancesWithHydratedTaxonomy(
baseUrl: string,
requestId: string,
): Promise<Array<MetadataInstanceV2>> {
try {
const [detailedResponse, hydratedResponse] = await Promise.all([
this.xhr.get({ url: `${baseUrl}?view=detailed`, id: requestId }),
this.xhr.get({ url: `${baseUrl}?view=hydrated`, id: requestId }),
]);
const detailedEntries = getProp(detailedResponse, 'data.entries', []);
const hydratedEntries = getProp(hydratedResponse, 'data.entries', []);
return mergeDetailedAndHydratedInstances(detailedEntries, hydratedEntries);
} catch (e) {
const { status } = e;
if (isUserCorrectableError(status)) {
throw e;
}
Comment thread
Lindar90 marked this conversation as resolved.
}
return [];
Comment thread
dlasecki-box marked this conversation as resolved.
}

/**
* Gets metadata instances for a Box file
*
Expand All @@ -407,14 +437,19 @@ class Metadata extends File {
this.errorCode = ERROR_CODE_FETCH_METADATA;

const baseUrl = this.getMetadataUrl(id);
const view = isConfidenceScoreEnabled ? 'detailed' : 'hydrated';
const url = isMetadataRedesign ? `${baseUrl}?view=${view}` : baseUrl;
const requestId = getTypedFileId(id);

if (isMetadataRedesign && isConfidenceScoreEnabled) {
return this.getDetailedInstancesWithHydratedTaxonomy(baseUrl, requestId);
}

const url = isMetadataRedesign ? `${baseUrl}?view=hydrated` : baseUrl;

let instances = {};
try {
instances = await this.xhr.get({
url,
id: getTypedFileId(id),
id: requestId,
});
} catch (e) {
const { status } = e;
Expand Down
48 changes: 41 additions & 7 deletions src/api/__tests__/Metadata.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -713,18 +713,52 @@ describe('api/Metadata', () => {
});
});

test('should apply detailed view query string param when isMetadataRedesign and isConfidenceScoreEnabled are true', async () => {
test('should make both detailed and hydrated calls when isMetadataRedesign and isConfidenceScoreEnabled are true', async () => {
metadata.getMetadataUrl = jest.fn().mockReturnValueOnce('metadata_url');
metadata.xhr.get = jest.fn().mockReturnValueOnce({
data: {
entries: [],
},
});
await metadata.getInstances('id', true, true);
metadata.xhr.get = jest
.fn()
.mockResolvedValueOnce({
data: {
entries: [
{
$id: 'inst-1',
region: {
values: ['uuid-1'],
details: { updatedAt: 1000, updatedBy: 'user1', updatedAppId: 'app1' },
},
},
],
},
})
.mockResolvedValueOnce({
data: {
entries: [
{
$id: 'inst-1',
region: [{ id: 'uuid-1', displayName: 'Japan', level: '1', nodePath: [] }],
},
],
},
});
const result = await metadata.getInstances('id', true, true);
expect(metadata.xhr.get).toHaveBeenCalledTimes(2);
expect(metadata.xhr.get).toHaveBeenCalledWith({
url: 'metadata_url?view=detailed',
id: 'file_id',
});
expect(metadata.xhr.get).toHaveBeenCalledWith({
url: 'metadata_url?view=hydrated',
id: 'file_id',
});
expect(result).toEqual([
{
$id: 'inst-1',
region: {
values: [{ id: 'uuid-1', displayName: 'Japan', level: '1', nodePath: [] }],
details: { updatedAt: 1000, updatedBy: 'user1', updatedAppId: 'app1' },
},
},
]);
});

test('should not apply view query string param when isMetadataRedesign is false even if isConfidenceScoreEnabled is true', async () => {
Expand Down
139 changes: 139 additions & 0 deletions src/api/__tests__/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
handleOnAbort,
isDetailedFieldValue,
mapDetailedFieldToConfidenceScore,
mergeDetailedAndHydratedInstances,
parseTargetLocation,
} from '../utils';
import { threadedComments, threadedCommentsFormatted } from '../fixtures';
Expand Down Expand Up @@ -251,4 +252,142 @@ describe('api/utils', () => {
expect(formatMetadataFieldValue(taxonomyField, [{ id, displayName }])).toEqual(expectedValue);
});
});

describe('mergeDetailedAndHydratedInstances()', () => {
test('should replace detailed values with hydrated values for taxonomy fields', () => {
const detailedEntries = [
{
$id: 'inst-1',
$template: 'template1',
$scope: 'enterprise',
region: {
values: ['uuid-1'],
details: { updatedAt: 1000, updatedBy: 'user1', updatedAppId: 'app1' },
},
name: {
values: 'Test Name',
details: { updatedAt: 2000, updatedBy: 'user2', updatedAppId: 'app2' },
},
},
];
const hydratedEntries = [
{
$id: 'inst-1',
$template: 'template1',
$scope: 'enterprise',
region: [{ id: 'uuid-1', displayName: 'Japan', level: '1', nodePath: [] }],
name: 'Test Name',
},
];

const result = mergeDetailedAndHydratedInstances(detailedEntries, hydratedEntries);

expect(result).toEqual([
{
$id: 'inst-1',
$template: 'template1',
$scope: 'enterprise',
region: {
values: [{ id: 'uuid-1', displayName: 'Japan', level: '1', nodePath: [] }],
details: { updatedAt: 1000, updatedBy: 'user1', updatedAppId: 'app1' },
},
name: {
values: 'Test Name',
details: { updatedAt: 2000, updatedBy: 'user2', updatedAppId: 'app2' },
},
},
]);
});

test('should return detailed entries unchanged when no matching hydrated entry exists', () => {
const detailedEntries = [
{
$id: 'inst-1',
region: { values: ['uuid-1'], details: {} },
},
];
const hydratedEntries = [];

const result = mergeDetailedAndHydratedInstances(detailedEntries, hydratedEntries);

expect(result).toEqual(detailedEntries);
});

test('should not modify $-prefixed system fields', () => {
const detailedEntries = [
{
$id: 'inst-1',
$scope: 'enterprise',
$template: 'tmpl',
field1: { values: 'val1', details: {} },
},
];
const hydratedEntries = [
{
$id: 'inst-1',
$scope: 'enterprise',
$template: 'tmpl',
field1: 'val1',
},
];

const result = mergeDetailedAndHydratedInstances(detailedEntries, hydratedEntries);

expect(result[0].$id).toBe('inst-1');
expect(result[0].$scope).toBe('enterprise');
expect(result[0].$template).toBe('tmpl');
});

test('should handle multiple instances', () => {
const detailedEntries = [
{
$id: 'inst-1',
country: { values: ['id-1'], details: { updatedAt: 100 } },
},
{
$id: 'inst-2',
city: { values: ['id-2'], details: { updatedAt: 200 } },
},
];
const hydratedEntries = [
{
$id: 'inst-2',
city: [{ id: 'id-2', displayName: 'Tokyo' }],
},
{
$id: 'inst-1',
country: [{ id: 'id-1', displayName: 'Japan' }],
},
];

const result = mergeDetailedAndHydratedInstances(detailedEntries, hydratedEntries);

expect(result[0].country.values).toEqual([{ id: 'id-1', displayName: 'Japan' }]);
expect(result[0].country.details).toEqual({ updatedAt: 100 });
expect(result[1].city.values).toEqual([{ id: 'id-2', displayName: 'Tokyo' }]);
expect(result[1].city.details).toEqual({ updatedAt: 200 });
});

test('should skip non-detailed fields during merge', () => {
const detailedEntries = [
{
$id: 'inst-1',
plainField: 'just a string',
detailedField: { values: 'val', details: {} },
},
];
const hydratedEntries = [
{
$id: 'inst-1',
plainField: 'just a string',
detailedField: 'val',
},
];

const result = mergeDetailedAndHydratedInstances(detailedEntries, hydratedEntries);

expect(result[0].plainField).toBe('just a string');
expect(result[0].detailedField).toEqual({ values: 'val', details: {} });
});
});
});
35 changes: 35 additions & 0 deletions src/api/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
MetadataConfidenceScoreData,
MetadataDetailedFieldValue,
MetadataFieldValue,
MetadataInstanceV2,
MetadataTargetLocationEntry,
MetadataTemplateField,
} from '../common/types/metadata';
Expand Down Expand Up @@ -94,6 +95,39 @@ const parseTargetLocation = (fieldValue: any): ?Array<MetadataTargetLocationEntr
}
};

const mergeDetailedAndHydratedInstances = (
detailedEntries: Array<MetadataInstanceV2>,
hydratedEntries: Array<MetadataInstanceV2>,
): Array<MetadataInstanceV2> => {
const hydratedById: { [string]: MetadataInstanceV2 } = {};
hydratedEntries.forEach(entry => {
hydratedById[entry.$id] = entry;
});

return detailedEntries.map(detailedEntry => {
const hydratedEntry = hydratedById[detailedEntry.$id];
if (!hydratedEntry) {
return detailedEntry;
}

const merged = { ...detailedEntry };
Object.keys(merged).forEach(key => {
if (key.startsWith('$')) {
return;
}

if (isDetailedFieldValue(merged[key]) && key in hydratedEntry) {
merged[key] = {
...merged[key],
values: hydratedEntry[key],
};
Comment thread
dlasecki-box marked this conversation as resolved.
}
});

return merged;
});
};

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

Expand All @@ -107,5 +141,6 @@ export {
handleOnAbort,
isDetailedFieldValue,
mapDetailedFieldToConfidenceScore,
mergeDetailedAndHydratedInstances,
parseTargetLocation,
};
Loading