From 641fca146e4b6c79492d07835b6e0df768a31909 Mon Sep 17 00:00:00 2001 From: Rhys Date: Tue, 2 Feb 2021 09:20:19 +0000 Subject: [PATCH 01/10] test(content-item): add media rewriting tests, add --media flag for triggering rewrites --- package-lock.json | 13 + src/commands/content-item/copy.spec.ts | 11 +- src/commands/content-item/copy.ts | 13 +- src/commands/content-item/import.spec.ts | 36 +++ src/commands/content-item/import.ts | 30 +++ src/commands/content-item/move.spec.ts | 11 +- src/commands/content-item/move.ts | 7 + .../content-item/media-link-injector.spec.ts | 79 ++++++ .../media/__mocks__/dam-client-factory.ts | 9 + src/common/media/__mocks__/media-rewriter.ts | 14 ++ src/common/media/media-rewriter.spec.ts | 228 ++++++++++++++++++ src/common/media/media-rewriter.ts | 156 ++++++++++++ src/common/media/mock-dam.ts | 87 +++++++ .../copy-item-builder-options.interface.ts | 1 + .../import-item-builder-options.interface.ts | 1 + 15 files changed, 692 insertions(+), 4 deletions(-) create mode 100644 src/common/content-item/media-link-injector.spec.ts create mode 100644 src/common/media/__mocks__/dam-client-factory.ts create mode 100644 src/common/media/__mocks__/media-rewriter.ts create mode 100644 src/common/media/media-rewriter.spec.ts create mode 100644 src/common/media/media-rewriter.ts create mode 100644 src/common/media/mock-dam.ts diff --git a/package-lock.json b/package-lock.json index c01e3c49..6111b114 100644 --- a/package-lock.json +++ b/package-lock.json @@ -741,6 +741,14 @@ "chalk": "*" } }, + "@types/es6-promise": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@types/es6-promise/-/es6-promise-3.3.0.tgz", + "integrity": "sha512-ixCIAEkLUKv9movnHKCzx2rzAJgEnSALDXPrOSSwOjWwXFs0ssSZKan+O2e3FExPPCbX+DfA9NcKsbvLuyUlNA==", + "requires": { + "es6-promise": "*" + } + }, "@types/eslint-visitor-keys": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", @@ -2869,6 +2877,11 @@ "is-symbol": "^1.0.2" } }, + "es6-promise": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", + "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", diff --git a/src/commands/content-item/copy.spec.ts b/src/commands/content-item/copy.spec.ts index b988cefd..40530386 100644 --- a/src/commands/content-item/copy.spec.ts +++ b/src/commands/content-item/copy.spec.ts @@ -147,6 +147,13 @@ describe('content-item copy command', () => { describe: 'Exclude delivery keys when importing content items.' }); + expect(spyOption).toHaveBeenCalledWith('media', { + type: 'boolean', + boolean: true, + describe: + "Detect and rewrite media links to match assets in the target account's DAM. Your client must have DAM permissions configured." + }); + expect(spyOption).toHaveBeenCalledWith('logFile', { type: 'string', default: LOG_FILENAME, @@ -227,7 +234,8 @@ describe('content-item copy command', () => { publish: true, republish: true, - excludeKeys: true + excludeKeys: true, + media: true }; await handler(argv); @@ -256,6 +264,7 @@ describe('content-item copy command', () => { expect(importCalls[0].republish).toEqual(argv.republish); expect(importCalls[0].excludeKeys).toEqual(argv.excludeKeys); + expect(importCalls[0].media).toEqual(argv.media); }); it('should forward to import-revert when revertLog is present.', async () => { diff --git a/src/commands/content-item/copy.ts b/src/commands/content-item/copy.ts index 11900e31..f798432d 100644 --- a/src/commands/content-item/copy.ts +++ b/src/commands/content-item/copy.ts @@ -127,6 +127,13 @@ export const builder = (yargs: Argv): void => { describe: 'Exclude delivery keys when importing content items.' }) + .option('media', { + type: 'boolean', + boolean: true, + describe: + "Detect and rewrite media links to match assets in the target account's DAM. Your client must have DAM permissions configured." + }) + .option('logFile', { type: 'string', default: LOG_FILENAME, @@ -213,12 +220,14 @@ export const handler = async (argv: Arguments { describe: 'Exclude delivery keys when importing content items.' }); + expect(spyOption).toHaveBeenCalledWith('media', { + type: 'boolean', + boolean: true, + describe: + "Detect and rewrite media links to match assets in the target account's DAM. Your client must have DAM permissions configured." + }); + expect(spyOption).toHaveBeenCalledWith('logFile', { type: 'string', default: LOG_FILENAME, @@ -1304,5 +1312,33 @@ describe('content-item import command', () => { await rimraf('temp/import/abort2/'); }); + + it('should call the media rewriter when --media is passed', async () => { + const templates: ItemTemplate[] = [{ label: 'item1', repoId: 'repo', typeSchemaUri: 'http://type' }]; + + await createContent('temp/import/media1/', templates, false); + + const mockContent = new MockContent(dynamicContentClientFactory as jest.Mock); + mockContent.createMockRepository('targetRepo'); + mockContent.registerContentType('http://type', 'type', 'targetRepo'); + + const argv = { + ...yargArgs, + ...config, + dir: 'temp/import/media1/', + mapFile: 'temp/import/media1.json', + baseRepo: 'targetRepo', + media: true + }; + await handler(argv); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((MediaRewriter as any).rewrites).toEqual(1); + + expect(mockContent.metrics.itemsCreated).toEqual(1); + expect(mockContent.metrics.itemsUpdated).toEqual(0); + + await rimraf('temp/import/media1/'); + }); }); }); diff --git a/src/commands/content-item/import.ts b/src/commands/content-item/import.ts index 093e6112..170f2ad2 100644 --- a/src/commands/content-item/import.ts +++ b/src/commands/content-item/import.ts @@ -114,6 +114,13 @@ export const builder = (yargs: Argv): void => { describe: 'Exclude delivery keys when importing content items.' }) + .option('media', { + type: 'boolean', + boolean: true, + describe: + "Detect and rewrite media links to match assets in the target account's DAM. Your client must have DAM permissions configured." + }) + .option('logFile', { type: 'string', default: LOG_FILENAME, @@ -509,6 +516,29 @@ const prepareContentForImport = async ( } } + // Step 2.5: Detect media links and rewrite their IDs, endpoints etc. + if (argv.media) { + log.appendLine(`Detecting and rewriting media links...`); + const rewriter = new MediaRewriter(argv, contentItems); + + let missing: Set; + try { + missing = await rewriter.rewrite(); + } catch (e) { + log.error( + `Failed to rewrite media links. Make sure your client is properly configured, or remove the --media flag.`, + e + ); + return null; + } + + log.appendLine(`Finished rewriting media links.`); + + if (missing.size > 0) { + log.warn(`${missing.size} media items could not be found in the target asset store. Ignoring.`); + } + } + // Step 3: Track dependancies between content items and update them to match the new content ids. // To do this, we must insert content that is depended on before inserting the replacement. // Circular references cannot be resolved, so they should be handled by an insert with invalid id, then subsequent update. diff --git a/src/commands/content-item/move.spec.ts b/src/commands/content-item/move.spec.ts index ac24d319..829f7942 100644 --- a/src/commands/content-item/move.spec.ts +++ b/src/commands/content-item/move.spec.ts @@ -128,6 +128,13 @@ describe('content-item move command', () => { describe: 'Exclude delivery keys when importing content items.' }); + expect(spyOption).toHaveBeenCalledWith('media', { + type: 'boolean', + boolean: true, + describe: + "Detect and rewrite media links to match assets in the target account's DAM. Your client must have DAM permissions configured." + }); + expect(spyOption).toHaveBeenCalledWith('logFile', { type: 'string', default: LOG_FILENAME, @@ -210,7 +217,8 @@ describe('content-item move command', () => { mapFile: 'map.json', force: false, validate: false, - skipIncomplete: false + skipIncomplete: false, + media: true }; await handler(argv); @@ -229,6 +237,7 @@ describe('content-item move command', () => { expect(copyCalls[0].force).toEqual(argv.force); expect(copyCalls[0].validate).toEqual(argv.validate); expect(copyCalls[0].skipIncomplete).toEqual(argv.skipIncomplete); + expect(copyCalls[0].media).toEqual(argv.media); expect(argv.exportedIds).toEqual(exportIds); diff --git a/src/commands/content-item/move.ts b/src/commands/content-item/move.ts index 438c95d4..5e73884e 100644 --- a/src/commands/content-item/move.ts +++ b/src/commands/content-item/move.ts @@ -122,6 +122,13 @@ export const builder = (yargs: Argv): void => { describe: 'Exclude delivery keys when importing content items.' }) + .option('media', { + type: 'boolean', + boolean: true, + describe: + "Detect and rewrite media links to match assets in the target account's DAM. Your client must have DAM permissions configured." + }) + .option('logFile', { type: 'string', default: LOG_FILENAME, diff --git a/src/common/content-item/media-link-injector.spec.ts b/src/common/content-item/media-link-injector.spec.ts new file mode 100644 index 00000000..e0fa8205 --- /dev/null +++ b/src/common/content-item/media-link-injector.spec.ts @@ -0,0 +1,79 @@ +import { ContentItem, ContentRepository } from 'dc-management-sdk-js'; +import { MediaLinkInjector } from './media-link-injector'; + +describe('media-link-injector', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('Media Link Injector', () => { + it('should identify media links', async () => { + const repo = new ContentRepository(); + + const injector = new MediaLinkInjector([ + { + repo, + content: new ContentItem({ + body: { + _meta: { + schema: 'https://test-type-1.com' + }, + image: { + id: '299ba2ac-aa5b-4c18-81da-f12156ad9622', + name: 'imageProperty', + endpoint: 'old', + defaultHost: 'i1.adis.ws', + mediaType: 'image', + _meta: { + schema: 'http://bigcontent.io/cms/schema/v1/core#/definitions/image-link' + } + }, + nested: { + imageNested: { + id: '299ba2ac-aa5b-4c18-81da-f12156ad9622', + name: 'imageNested', + endpoint: 'old', + defaultHost: 'i1.adis.ws', + mediaType: 'image', + _meta: { + schema: 'http://bigcontent.io/cms/schema/v1/core#/definitions/image-link' + } + }, + imageArray: [ + { + id: '299ba2ac-aa5b-4c18-81da-f12156ad9622', + name: 'imageArray1', + endpoint: 'old', + defaultHost: 'i1.adis.ws', + mediaType: 'image', + _meta: { + schema: 'http://bigcontent.io/cms/schema/v1/core#/definitions/image-link' + } + }, + { + id: '299ba2ac-aa5b-4c18-81da-f12156ad9622', + name: 'imageArray2', + endpoint: 'old', + defaultHost: 'i1.adis.ws', + mediaType: 'image', + _meta: { + schema: 'http://bigcontent.io/cms/schema/v1/core#/definitions/image-link' + } + } + ] + } + } + }) + } + ]); + + expect(injector.all).not.toBeNull(); + expect(injector.all.length).toEqual(1); + expect(injector.all[0].links.length).toEqual(4); + + expect(injector.all.map(group => group.links.map(link => link.link.name))).toEqual([ + ['imageProperty', 'imageNested', 'imageArray1', 'imageArray2'] + ]); + }); + }); +}); diff --git a/src/common/media/__mocks__/dam-client-factory.ts b/src/common/media/__mocks__/dam-client-factory.ts new file mode 100644 index 00000000..cc863610 --- /dev/null +++ b/src/common/media/__mocks__/dam-client-factory.ts @@ -0,0 +1,9 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { DAM } from 'dam-management-sdk-js'; +import { ConfigurationParameters } from '../../../commands/configure'; +import { MockDAM } from '../mock-dam'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const damClientFactory = (_: ConfigurationParameters): DAM => (new MockDAM() as any) as DAM; + +export default damClientFactory; diff --git a/src/common/media/__mocks__/media-rewriter.ts b/src/common/media/__mocks__/media-rewriter.ts new file mode 100644 index 00000000..da2fdfd1 --- /dev/null +++ b/src/common/media/__mocks__/media-rewriter.ts @@ -0,0 +1,14 @@ +import { ConfigurationParameters } from '../../../commands/configure'; +import { RepositoryContentItem } from '../../content-item/content-dependancy-tree'; + +export class MediaRewriter { + static rewrites = 0; + + constructor(private config: ConfigurationParameters, private items: RepositoryContentItem[]) {} + + async rewrite(): Promise> { + MediaRewriter.rewrites++; + + return new Set(); + } +} diff --git a/src/common/media/media-rewriter.spec.ts b/src/common/media/media-rewriter.spec.ts new file mode 100644 index 00000000..7458a355 --- /dev/null +++ b/src/common/media/media-rewriter.spec.ts @@ -0,0 +1,228 @@ +import { ContentItem, ContentRepository } from 'dc-management-sdk-js'; +import damClientFactory from '../../services/dam-client-factory'; +import { RepositoryContentItem } from '../content-item/content-dependancy-tree'; +import { MediaLinkInjector } from '../content-item/media-link-injector'; +import { MediaRewriter } from './media-rewriter'; +import { MockDAM } from './mock-dam'; + +jest.mock('../../services/dam-client-factory'); + +let exampleLinks: RepositoryContentItem[] = []; + +describe('media-link-injector', () => { + beforeEach(() => { + jest.resetAllMocks(); + + MockDAM.missingAssetList = false; + MockDAM.throwOnGetSettings = false; + MockDAM.returnNullEndpoint = false; + MockDAM.throwOnAssetList = false; + MockDAM.requests = []; + + exampleLinks = [ + { + repo: new ContentRepository(), + content: new ContentItem({ + body: { + _meta: { + schema: 'https://test-type-1.com' + }, + image: { + id: '299ba2ac-aa5b-4c18-81da-f12156ad9622', + name: 'imageProperty', + endpoint: 'old', + defaultHost: 'i1.adis.ws', + mediaType: 'image', + _meta: { + schema: 'http://bigcontent.io/cms/schema/v1/core#/definitions/image-link' + } + }, + nested: { + imageNested: { + id: '299ba2ac-aa5b-4c18-81da-f12156ad9622', + name: 'imageNested', + endpoint: 'old', + defaultHost: 'i1.adis.ws', + mediaType: 'image', + _meta: { + schema: 'http://bigcontent.io/cms/schema/v1/core#/definitions/image-link' + } + }, + imageArray: [ + { + id: '299ba2ac-aa5b-4c18-81da-f12156ad9622', + name: 'imageArray1', + endpoint: 'old', + defaultHost: 'i1.adis.ws', + mediaType: 'image', + _meta: { + schema: 'http://bigcontent.io/cms/schema/v1/core#/definitions/image-link' + } + }, + { + id: '299ba2ac-aa5b-4c18-81da-f12156ad9622', + name: 'imageArray2', + endpoint: 'old', + defaultHost: 'i1.adis.ws', + mediaType: 'image', + _meta: { + schema: 'http://bigcontent.io/cms/schema/v1/core#/definitions/image-link' + } + } + ] + } + } + }) + } + ]; + + (damClientFactory as jest.Mock).mockReturnValue(new MockDAM()); + }); + + describe('Media Link Injector', () => { + it('should rewrite media links', async () => { + const rewriter = new MediaRewriter({ clientId: '', clientSecret: '', hubId: '' }, exampleLinks); + + const missing = await rewriter.rewrite(); + + expect(missing.size).toEqual(0); // 0 assets missing + + expect(MockDAM.requests).toMatchInlineSnapshot(` + Array [ + Object { + "n": 4, + "q": "(name:/imageProperty|imageNested|imageArray1|imageArray2/)", + }, + ] + `); + + // The mock DAM has all IDs equal to names, which lets us distinguish them easily. + // The IDs will be rewritten to these names if all went well. + + expect(exampleLinks[0].content.body.image.id).toBe('imageProperty'); + expect(exampleLinks[0].content.body.nested.imageNested.id).toBe('imageNested'); + expect(exampleLinks[0].content.body.nested.imageArray[0].id).toBe('imageArray1'); + expect(exampleLinks[0].content.body.nested.imageArray[1].id).toBe('imageArray2'); + }); + + it('should do nothing if no assets are provided', async () => { + const rewriter = new MediaRewriter({ clientId: '', clientSecret: '', hubId: '' }, []); + + const missing = await rewriter.rewrite(); + + expect(missing.size).toEqual(0); // 0 assets missing + + expect(MockDAM.requests.length).toEqual(0); + }); + + it('should ignore media links where content with a matching name does not exist on DAM', async () => { + MockDAM.missingAssetList = true; + const rewriter = new MediaRewriter({ clientId: '', clientSecret: '', hubId: '' }, exampleLinks); + + const results = await rewriter.rewrite(); + + expect(results.size).toEqual(4); // All 4 assets missing + + expect(MockDAM.requests).toMatchInlineSnapshot(` + Array [ + Object { + "n": 4, + "q": "(name:/imageProperty|imageNested|imageArray1|imageArray2/)", + }, + ] + `); + }); + + it('should fail when the settings endpoint throws', async () => { + MockDAM.throwOnGetSettings = true; + const rewriter = new MediaRewriter({ clientId: '', clientSecret: '', hubId: '' }, []); + + let throws = false; + try { + await rewriter.rewrite(); + } catch { + throws = true; + } + + expect(throws).toBeTruthy(); + }); + + it('should fail when the settings do not contain a default endpoint', async () => { + MockDAM.returnNullEndpoint = true; + const rewriter = new MediaRewriter({ clientId: '', clientSecret: '', hubId: '' }, []); + + let throws = false; + try { + await rewriter.rewrite(); + } catch { + throws = true; + } + + expect(throws).toBeTruthy(); + }); + + it('should fail when getting assets does not work a certain number of times in a row', async () => { + MockDAM.throwOnAssetList = true; + const rewriter = new MediaRewriter({ clientId: '', clientSecret: '', hubId: '' }, exampleLinks); + + let throws = false; + try { + await rewriter.rewrite(); + } catch { + throws = true; + } + + expect(throws).toBeTruthy(); + + expect(MockDAM.requests).toMatchInlineSnapshot(` + Array [ + Object { + "n": 4, + "q": "(name:/imageProperty|imageNested|imageArray1|imageArray2/)", + }, + Object { + "n": 4, + "q": "(name:/imageProperty|imageNested|imageArray1|imageArray2/)", + }, + Object { + "n": 4, + "q": "(name:/imageProperty|imageNested|imageArray1|imageArray2/)", + }, + ] + `); + }); + + it('should make multiple asset requests if the query gets too long (3000 chars)', async () => { + const expectedCharLimit = 3000; + const expectedRequests = 3; + + const injector = new MediaLinkInjector(exampleLinks); + + const names = injector.all[0].links.map(x => x.link.name + '0'); + const itemLength = names.join('|').length; + + const itemsNeeded = ((expectedRequests - 0.5) * expectedCharLimit) / itemLength; + + const newLinks: RepositoryContentItem[] = []; + const template = JSON.stringify(exampleLinks[0]); + + for (let i = 0; i < itemsNeeded; i++) { + const newLink: RepositoryContentItem = JSON.parse(template); + newLink.content.body.image.name += i; + newLink.content.body.nested.imageNested.name += i; + newLink.content.body.nested.imageArray[0].name += i; + newLink.content.body.nested.imageArray[1].name += i; + + newLinks[i] = newLink; + } + + const rewriter = new MediaRewriter({ clientId: '', clientSecret: '', hubId: '' }, newLinks); + + const missing = await rewriter.rewrite(); + + expect(missing.size).toEqual(0); // 0 assets missing + + expect(MockDAM.requests.length).toEqual(expectedRequests); // 3 requests + }); + }); +}); diff --git a/src/common/media/media-rewriter.ts b/src/common/media/media-rewriter.ts new file mode 100644 index 00000000..0011e9a6 --- /dev/null +++ b/src/common/media/media-rewriter.ts @@ -0,0 +1,156 @@ +import { Asset, DAM } from 'dam-management-sdk-js'; +import { DiSettingsEndpoint } from 'dam-management-sdk-js/build/main/lib/model/Settings'; +import { ConfigurationParameters } from '../../commands/configure'; +import damClientFactory from '../../services/dam-client-factory'; +import { RepositoryContentItem } from '../content-item/content-dependancy-tree'; +import { MediaLinkInjector } from '../content-item/media-link-injector'; + +/** + * Exports media related to given content items from an existing repository. + * Uses the account credentials to export the media. + */ +export class MediaRewriter { + private injector: MediaLinkInjector; + private dam: DAM; + + private endpoint: string; + private defaultHost: string; + + constructor(private config: ConfigurationParameters, private items: RepositoryContentItem[]) { + this.injector = new MediaLinkInjector(items); + } + + private connectDam(): void { + this.dam = damClientFactory(this.config); + } + + private async getEndpoint(): Promise { + let endpoint: DiSettingsEndpoint | undefined; + try { + const settings = await this.dam.settings.get(); + + endpoint = settings.di.endpoints.find(endpoint => { + return endpoint.id === settings.di.defaultEndpoint; + }); + } catch (e) { + throw new Error(`Could not obtain settings from DAM. Make sure you have the required permissions. ${e}`); + } + + if (endpoint == null) { + throw new Error('Could not find the default endpoint.'); + } + + this.endpoint = endpoint.path; + this.defaultHost = endpoint.dynamicHost; + } + + private async queryAndAdd(query: string, count: number, assets: Map): Promise { + const attempts = 3; + + for (let i = 0; i < attempts; i++) { + try { + const result = await this.dam.assets.list({ + q: '(' + query + ')', + n: count + }); + + const items = result.getItems(); + + items.forEach(asset => { + assets.set(asset.name as string, asset); + }); + + return items.length; + } catch (e) { + // Retry + } + } + + // Too many retries, fail the request. + throw new Error(`Request for assets failed after ${attempts} attempts.`); + } + + async rewrite(): Promise> { + this.connectDam(); + + await this.getEndpoint(); + + // Steps: + // identify existing assets by name (unique, case sensitive) + // - content item media dependancies, flush them all into a set + // - try do a few batch requests for assets with matching name (arbitrary limit: 3000 characters) + // - replace media link assets with ones that we found with matching names + // - return non-matching assets + + const allNames = new Set(); + + const itemLinks = this.injector.all; + + for (let i = 0; i < itemLinks.length; i++) { + const item = itemLinks[i]; + + const links = item.links; + for (let j = 0; j < links.length; j++) { + const link = links[j]; + + allNames.add(link.link.name); + } + } + + const missingAssets = new Set(); + + if (allNames.size == 0) { + return missingAssets; + } + + const assetsByName = new Map(); + const names = Array.from(allNames); + + let requestBuilder = 'name:/'; + let requestCount = 0; + let totalFound = 0; + + for (let i = 0; i < allNames.size; i++) { + const additionalRequest = `${names[i]}`; + + const lengthSoFar = requestBuilder.length; + if (lengthSoFar == 6) { + // First entry? + requestBuilder += additionalRequest; + requestCount++; + } else { + if (lengthSoFar + 4 + additionalRequest.length < 3000) { + // OR + requestBuilder += '|' + additionalRequest; + requestCount++; + } else { + // If the query is too big, batch out what we have and start over. + + totalFound += await this.queryAndAdd(requestBuilder + '/', requestCount, assetsByName); + requestBuilder = 'name:/' + additionalRequest; + } + } + } + + if (requestBuilder.length > 0) { + totalFound += await this.queryAndAdd(requestBuilder + '/', requestCount, assetsByName); + } + + // Replace media link assets with ones that we found with matching names. + this.injector.all.forEach(links => { + links.links.forEach(link => { + const asset = assetsByName.get(link.link.name); + if (asset != null) { + link.link.id = asset.id; + + link.link.defaultHost = this.defaultHost; + link.link.endpoint = this.endpoint; + } else { + missingAssets.add(link.link.name); + } + }); + }); + + return missingAssets; + } +} diff --git a/src/common/media/mock-dam.ts b/src/common/media/mock-dam.ts new file mode 100644 index 00000000..45112090 --- /dev/null +++ b/src/common/media/mock-dam.ts @@ -0,0 +1,87 @@ +import { DefaultApiClient, ApiClient, AssetListRequest } from 'dam-management-sdk-js'; +import { AssetsList } from 'dam-management-sdk-js/build/main/lib/model/Asset'; + +export class MockDAM { + static throwOnGetSettings = false; + static returnNullEndpoint = false; + static throwOnAssetList = false; + static missingAssetList = false; + + static requests: AssetListRequest[] = []; + + client: ApiClient; + + settings = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + get: (): Promise => { + if (MockDAM.throwOnGetSettings) { + throw new Error('Simulated settings error.'); + } + + if (MockDAM.returnNullEndpoint) { + return Promise.resolve({ + di: { + endpoints: [], + defaultEndpoint: null + } + }); + } else { + return Promise.resolve({ + di: { + endpoints: [ + { + id: 'test-endpoint', + path: 'test-endpoint-path', + dynamicHost: 'test-endpoint-dynamicHost' + } + ], + defaultEndpoint: 'test-endpoint' + } + }); + } + } + }; + + assets = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + list: (query: AssetListRequest): Promise => { + MockDAM.requests.push(query); + + if (MockDAM.throwOnAssetList) { + throw new Error('Simulated asset list error.'); + } + + let list: AssetsList; + + if (MockDAM.missingAssetList) { + list = new AssetsList({ + data: [], + count: 0 + }); + } else { + const nameStart = (query.q as string).indexOf('/'); + const nameEnd = (query.q as string).lastIndexOf('/'); + const name = (query.q as string).substring(nameStart + 1, nameEnd); + + const names = name.split('|'); + + list = new AssetsList({ + data: names.map(name => ({ + id: name, + name + })), + count: names.length + }); + } + + list.setClient(this.client); + + return Promise.resolve(list); + } + }; + + constructor() { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + this.client = new DefaultApiClient('', null as any, null as any); + } +} diff --git a/src/interfaces/copy-item-builder-options.interface.ts b/src/interfaces/copy-item-builder-options.interface.ts index 4c38d0c2..8c2ad947 100644 --- a/src/interfaces/copy-item-builder-options.interface.ts +++ b/src/interfaces/copy-item-builder-options.interface.ts @@ -19,6 +19,7 @@ export interface CopyItemBuilderOptions { force?: boolean; validate?: boolean; skipIncomplete?: boolean; + media?: boolean; logFile?: string | FileLog; copyConfig?: string | CopyConfig; diff --git a/src/interfaces/import-item-builder-options.interface.ts b/src/interfaces/import-item-builder-options.interface.ts index 6742b45d..bbbf35e7 100644 --- a/src/interfaces/import-item-builder-options.interface.ts +++ b/src/interfaces/import-item-builder-options.interface.ts @@ -11,6 +11,7 @@ export interface ImportItemBuilderOptions { validate?: boolean; skipIncomplete?: boolean; excludeKeys?: boolean; + media?: boolean; logFile?: string | FileLog; revertLog?: string; From 07dffedf27757d4a639680cfbc63df4f71278010 Mon Sep 17 00:00:00 2001 From: Rhys Date: Thu, 4 Feb 2021 17:41:10 +0000 Subject: [PATCH 02/10] fix(content-item): fix media link rewriting for media with special chars. Update dam-management-sdk --- package-lock.json | 30 ++++++++++++++++++++++++++++++ src/common/media/media-rewriter.ts | 6 +++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 6111b114..c3b152bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2576,6 +2576,36 @@ "word-wrap": "^1.0.3" } }, + "dam-management-sdk-js": { + "version": "git+https://github.com/rs-amp/dam-management-sdk-js.git#cecadd7b3887a0f5f94b645d29bbc5d6ee8ad001", + "from": "git+https://github.com/rs-amp/dam-management-sdk-js.git", + "requires": { + "@types/es6-promise": "^3.3.0", + "@types/node": "^14.14.5", + "axios": "^0.21.0", + "url-template": "^2.0.8" + }, + "dependencies": { + "@types/node": { + "version": "14.14.25", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.25.tgz", + "integrity": "sha512-EPpXLOVqDvisVxtlbvzfyqSsFeQxltFbluZNRndIb8tr9KiBnYNLzrc1N3pyKUCww2RNrfHDViqDWWE1LCJQtQ==" + }, + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, + "follow-redirects": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.2.tgz", + "integrity": "sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA==" + } + } + }, "dargs": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/dargs/-/dargs-4.1.0.tgz", diff --git a/src/common/media/media-rewriter.ts b/src/common/media/media-rewriter.ts index 0011e9a6..16761554 100644 --- a/src/common/media/media-rewriter.ts +++ b/src/common/media/media-rewriter.ts @@ -20,6 +20,10 @@ export class MediaRewriter { this.injector = new MediaLinkInjector(items); } + private escapeForRegex(url: string): string { + return url.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + } + private connectDam(): void { this.dam = damClientFactory(this.config); } @@ -111,7 +115,7 @@ export class MediaRewriter { let totalFound = 0; for (let i = 0; i < allNames.size; i++) { - const additionalRequest = `${names[i]}`; + const additionalRequest = `${this.escapeForRegex(names[i])}`; const lengthSoFar = requestBuilder.length; if (lengthSoFar == 6) { From 51a36da0f7798067c2366a74987f3f81cbee3dd2 Mon Sep 17 00:00:00 2001 From: Rhys Date: Tue, 16 Mar 2021 17:34:39 +0000 Subject: [PATCH 03/10] fix(content-item): include files that were lost during rebase --- package-lock.json | 14 ++-- package.json | 1 + src/commands/content-item/archive.spec.ts | 4 +- src/commands/content-item/import.spec.ts | 1 + src/commands/content-item/import.ts | 3 +- src/commands/content-item/unarchive.spec.ts | 4 +- .../content-item/media-link-injector.ts | 80 +++++++++++++++++++ src/services/dam-client-factory.ts | 17 ++++ 8 files changed, 112 insertions(+), 12 deletions(-) create mode 100644 src/common/content-item/media-link-injector.ts create mode 100644 src/services/dam-client-factory.ts diff --git a/package-lock.json b/package-lock.json index c3b152bf..495cb946 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2577,7 +2577,7 @@ } }, "dam-management-sdk-js": { - "version": "git+https://github.com/rs-amp/dam-management-sdk-js.git#cecadd7b3887a0f5f94b645d29bbc5d6ee8ad001", + "version": "git+https://github.com/rs-amp/dam-management-sdk-js.git#ffefa0da28c7723c0f627959362e94627f86ed6b", "from": "git+https://github.com/rs-amp/dam-management-sdk-js.git", "requires": { "@types/es6-promise": "^3.3.0", @@ -2587,9 +2587,9 @@ }, "dependencies": { "@types/node": { - "version": "14.14.25", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.25.tgz", - "integrity": "sha512-EPpXLOVqDvisVxtlbvzfyqSsFeQxltFbluZNRndIb8tr9KiBnYNLzrc1N3pyKUCww2RNrfHDViqDWWE1LCJQtQ==" + "version": "14.14.35", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.35.tgz", + "integrity": "sha512-Lt+wj8NVPx0zUmUwumiVXapmaLUcAk3yPuHCFVXras9k5VT9TdhJqKqGVUQCD60OTMCl0qxJ57OiTL0Mic3Iag==" }, "axios": { "version": "0.21.1", @@ -2600,9 +2600,9 @@ } }, "follow-redirects": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.2.tgz", - "integrity": "sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA==" + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz", + "integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==" } } }, diff --git a/package.json b/package.json index f4c6b306..c563d829 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,7 @@ "ajv": "^6.12.3", "axios": "^0.18.1", "chalk": "^2.4.2", + "dam-management-sdk-js": "git+https://github.com/rs-amp/dam-management-sdk-js.git", "dc-management-sdk-js": "^1.9.0", "lodash": "^4.17.15", "node-fetch": "^2.6.0", diff --git a/src/commands/content-item/archive.spec.ts b/src/commands/content-item/archive.spec.ts index 378dea01..5d0ba813 100644 --- a/src/commands/content-item/archive.spec.ts +++ b/src/commands/content-item/archive.spec.ts @@ -687,7 +687,7 @@ describe('content-item archive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - const logFileName = 'temp/content-item-unrachive.log'; + const logFileName = 'temp/content-item-unarchive.log'; const log = '// Type log test file\n' + 'UNARCHIVE 1\n' + 'UNARCHIVE 2\n' + 'UNARCHIVE idMissing'; const dir = dirname(logFileName); @@ -740,7 +740,7 @@ describe('content-item archive command', () => { await promisify(unlink)('temp/content-item-unarchive.log'); } - const logFileName = 'temp/content-item-unrachive.log'; + const logFileName = 'temp/content-item-unarchive.log'; const log = '// Type log test file\n' + 'UNARCHIVE 1\n' + 'UNARCHIVE 2\n' + 'UNARCHIVE idMissing'; const dir = dirname(logFileName); diff --git a/src/commands/content-item/import.spec.ts b/src/commands/content-item/import.spec.ts index f2b2f4dc..56b3b4f4 100644 --- a/src/commands/content-item/import.spec.ts +++ b/src/commands/content-item/import.spec.ts @@ -21,6 +21,7 @@ jest.mock('readline'); jest.mock('./import-revert'); jest.mock('../../services/dynamic-content-client-factory'); jest.mock('../../common/import/publish-queue'); +jest.mock('../../common/media/media-rewriter'); jest.mock('../../common/log-helpers'); function rimraf(dir: string): Promise { diff --git a/src/commands/content-item/import.ts b/src/commands/content-item/import.ts index 170f2ad2..3a9f5799 100644 --- a/src/commands/content-item/import.ts +++ b/src/commands/content-item/import.ts @@ -31,6 +31,7 @@ import { asyncQuestion } from '../../common/archive/archive-helpers'; import { AmplienceSchemaValidator, defaultSchemaLookup } from '../../common/content-item/amplience-schema-validator'; import { getDefaultLogPath } from '../../common/log-helpers'; import { PublishQueue } from '../../common/import/publish-queue'; +import { MediaRewriter } from '../../common/media/media-rewriter'; export function getDefaultMappingPath(name: string, platform: string = process.platform): string { return join( @@ -279,7 +280,7 @@ const prepareContentForImport = async ( folder: Folder | null, mapping: ContentMapping, log: FileLog, - argv: ImportItemBuilderOptions + argv: Arguments ): Promise => { // traverse folder structure and find content items // replicate relative path string in target repo/folder (create if does not exist) diff --git a/src/commands/content-item/unarchive.spec.ts b/src/commands/content-item/unarchive.spec.ts index cf2f75b8..19fd1842 100644 --- a/src/commands/content-item/unarchive.spec.ts +++ b/src/commands/content-item/unarchive.spec.ts @@ -695,7 +695,7 @@ describe('content-item unarchive command', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any (readline as any).setResponses(['y']); - const logFileName = 'temp/content-item-unrachive.log'; + const logFileName = 'temp/content-item-unarchive.log'; const log = '// Type log test file\n' + 'ARCHIVE 1\n' + 'ARCHIVE 2\n' + 'ARCHIVE idMissing'; const dir = dirname(logFileName); @@ -748,7 +748,7 @@ describe('content-item unarchive command', () => { await promisify(unlink)('temp/content-item-archive.log'); } - const logFileName = 'temp/content-item-unrachive.log'; + const logFileName = 'temp/content-item-unarchive.log'; const log = '// Type log test file\n' + 'ARCHIVE 1\n' + 'ARCHIVE 2\n' + 'ARCHIVE idMissing'; const dir = dirname(logFileName); diff --git a/src/common/content-item/media-link-injector.ts b/src/common/content-item/media-link-injector.ts new file mode 100644 index 00000000..23d5f301 --- /dev/null +++ b/src/common/content-item/media-link-injector.ts @@ -0,0 +1,80 @@ +import { RepositoryContentItem } from './content-dependancy-tree'; +import { Body } from './body'; + +export interface MediaLink { + id: string; + name: string; + endpoint: string; + defaultHost: string; + mediaType: string; + _meta: { + schema: + | 'http://bigcontent.io/cms/schema/v1/core#/definitions/image-link' + | 'http://bigcontent.io/cms/schema/v1/core#/definitions/video-link'; + }; +} + +export interface MediaLinkInfo { + link: MediaLink; + owner: RepositoryContentItem; +} + +export interface ItemMediaLinks { + owner: RepositoryContentItem; + links: MediaLinkInfo[]; +} + +export const linkTypes = [ + 'http://bigcontent.io/cms/schema/v1/core#/definitions/image-link', + 'http://bigcontent.io/cms/schema/v1/core#/definitions/video-link' +]; + +type RecursiveSearchStep = Body | MediaLink | Array; + +export class MediaLinkInjector { + all: ItemMediaLinks[]; + + constructor(items: RepositoryContentItem[]) { + // Identify all content dependancies. + this.all = this.identifyMediaLinks(items); + } + + private searchObjectForMediaLinks( + item: RepositoryContentItem, + body: RecursiveSearchStep, + result: MediaLinkInfo[] + ): void { + if (Array.isArray(body)) { + body.forEach(contained => { + this.searchObjectForMediaLinks(item, contained, result); + }); + } else { + const allPropertyNames = Object.getOwnPropertyNames(body); + // Does this object match the pattern expected for a content item or reference? + if ( + body._meta && + linkTypes.indexOf(body._meta.schema) !== -1 && + typeof body.name === 'string' && + typeof body.id === 'string' + ) { + result.push({ link: body as MediaLink, owner: item }); + return; + } + + allPropertyNames.forEach(propName => { + const prop = (body as Body)[propName]; + if (typeof prop === 'object') { + this.searchObjectForMediaLinks(item, prop, result); + } + }); + } + } + + private identifyMediaLinks(items: RepositoryContentItem[]): ItemMediaLinks[] { + return items.map(item => { + const result: MediaLinkInfo[] = []; + this.searchObjectForMediaLinks(item, item.content.body, result); + return { owner: item, links: result }; + }); + } +} diff --git a/src/services/dam-client-factory.ts b/src/services/dam-client-factory.ts new file mode 100644 index 00000000..5f849e2c --- /dev/null +++ b/src/services/dam-client-factory.ts @@ -0,0 +1,17 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import { DAM } from 'dam-management-sdk-js'; +import { ConfigurationParameters } from '../commands/configure'; + +const damClientFactory = (config: ConfigurationParameters): DAM => + new DAM( + { + client_id: config.clientId, + client_secret: config.clientSecret + }, + { + apiUrl: process.env.DAM_API_URL, + authUrl: process.env.AUTH_URL + } + ); + +export default damClientFactory; From f611e6fd154d30ba2f1cd3c164b5e0e68093fd37 Mon Sep 17 00:00:00 2001 From: Rhys Date: Wed, 31 Mar 2021 12:35:37 +0100 Subject: [PATCH 04/10] feat: move dam-sdk into this projcet, rename to ch-api --- package-lock.json | 67 ++-- package.json | 3 +- src/common/ch-api/ContentHub.mocks.ts | 66 ++++ src/common/ch-api/ContentHub.ts | 101 ++++++ .../_fixtures/ContentHubFixtures.mocks.ts | 142 ++++++++ .../ch-api/_fixtures/assets/DELETE-gfail.json | 4 + .../_fixtures/assets/DELETE-{id}-fail.json | 4 + .../ch-api/_fixtures/assets/DELETE-{id}.json | 4 + .../ch-api/_fixtures/assets/DELETE.json | 4 + .../_fixtures/assets/GET-{id}-select.json | 130 +++++++ .../ch-api/_fixtures/assets/GET-{id}.json | 29 ++ src/common/ch-api/_fixtures/assets/GET.json | 52 +++ .../ch-api/_fixtures/assets/PUT-gfail.json | 4 + src/common/ch-api/_fixtures/assets/PUT.json | 13 + .../assets/_localeGroups/GET-{ids}.json | 7 + .../ch-api/_fixtures/assets/badid/GET.json | 4 + .../ch-api/_fixtures/assets/publish/POST.json | 9 + .../assets/publish/validate/POST.json | 4 + .../_fixtures/assets/text/POST-external.json | 11 + .../ch-api/_fixtures/assets/text/POST.json | 11 + .../_fixtures/assets/unpublish/POST.json | 9 + .../_fixtures/assets/{id}/download/GET.json | 4 + .../_fixtures/assets/{id}/metadata/GET.json | 69 ++++ .../assets/{id}/revert/PUT-{version}.json | 9 + .../assets/{id}/thumbnails/GET-gfail.json | 4 + .../assets/{id}/transcodings/GET.json | 4 + .../assets/{id}/versions/GET-{version}.json | 36 ++ .../_fixtures/assets/{id}/versions/GET.json | 151 ++++++++ .../{id}/versions/{version}/download/GET.json | 4 + .../assets/{id}/video_profiles/GET.json | 7 + src/common/ch-api/_fixtures/settings/GET.json | 53 +++ src/common/ch-api/api/model/ApiResource.ts | 65 ++++ src/common/ch-api/api/services/ApiClient.ts | 197 +++++++++++ .../ch-api/api/services/ApiEndpoints.ts | 236 +++++++++++++ src/common/ch-api/api/services/CURIEs.spec.ts | 44 +++ src/common/ch-api/api/services/CURIEs.ts | 15 + .../ch-api/http/AxiosHttpClient.spec.ts | 107 ++++++ src/common/ch-api/http/AxiosHttpClient.ts | 41 +++ src/common/ch-api/http/HttpClient.ts | 9 + src/common/ch-api/http/HttpError.ts | 9 + src/common/ch-api/http/HttpRequest.ts | 20 ++ src/common/ch-api/http/HttpResponse.ts | 7 + src/common/ch-api/model/ActionPriority.ts | 6 + src/common/ch-api/model/Asset.spec.ts | 326 ++++++++++++++++++ src/common/ch-api/model/Asset.ts | 232 +++++++++++++ src/common/ch-api/model/AssetListRequest.ts | 92 +++++ src/common/ch-api/model/AssetMetadata.ts | 57 +++ src/common/ch-api/model/AssetPut.ts | 90 +++++ src/common/ch-api/model/AssetPutResult.ts | 15 + src/common/ch-api/model/AssetRequest.ts | 14 + src/common/ch-api/model/AssetText.ts | 33 ++ src/common/ch-api/model/Page.ts | 19 + src/common/ch-api/model/Pageable.ts | 14 + .../ch-api/model/PublishActivitySummary.ts | 13 + src/common/ch-api/model/PublishInfoPut.ts | 21 ++ src/common/ch-api/model/ResourceList.spec.ts | 22 ++ src/common/ch-api/model/ResourceList.ts | 30 ++ src/common/ch-api/model/Settings.spec.ts | 11 + src/common/ch-api/model/Settings.ts | 45 +++ src/common/ch-api/model/StringList.ts | 11 + src/common/ch-api/model/TagsPut.ts | 11 + src/common/ch-api/model/WorkflowSummary.ts | 4 + .../ch-api/oauth2/models/AccessToken.ts | 19 + .../oauth2/models/AccessTokenProvider.ts | 5 + .../oauth2/models/OAuth2ClientCredentials.ts | 14 + .../oauth2/services/OAuth2Client.spec.ts | 152 ++++++++ .../ch-api/oauth2/services/OAuth2Client.ts | 84 +++++ src/common/ch-api/utils/URL.spec.ts | 39 +++ src/common/ch-api/utils/URL.ts | 17 + .../media/__mocks__/ch-client-factory.ts | 9 + .../media/__mocks__/dam-client-factory.ts | 9 - src/common/media/media-rewriter.spec.ts | 36 +- src/common/media/media-rewriter.ts | 16 +- src/common/media/{mock-dam.ts => mock-ch.ts} | 17 +- ...client-factory.ts => ch-client-factory.ts} | 8 +- 75 files changed, 3169 insertions(+), 91 deletions(-) create mode 100644 src/common/ch-api/ContentHub.mocks.ts create mode 100644 src/common/ch-api/ContentHub.ts create mode 100644 src/common/ch-api/_fixtures/ContentHubFixtures.mocks.ts create mode 100644 src/common/ch-api/_fixtures/assets/DELETE-gfail.json create mode 100644 src/common/ch-api/_fixtures/assets/DELETE-{id}-fail.json create mode 100644 src/common/ch-api/_fixtures/assets/DELETE-{id}.json create mode 100644 src/common/ch-api/_fixtures/assets/DELETE.json create mode 100644 src/common/ch-api/_fixtures/assets/GET-{id}-select.json create mode 100644 src/common/ch-api/_fixtures/assets/GET-{id}.json create mode 100644 src/common/ch-api/_fixtures/assets/GET.json create mode 100644 src/common/ch-api/_fixtures/assets/PUT-gfail.json create mode 100644 src/common/ch-api/_fixtures/assets/PUT.json create mode 100644 src/common/ch-api/_fixtures/assets/_localeGroups/GET-{ids}.json create mode 100644 src/common/ch-api/_fixtures/assets/badid/GET.json create mode 100644 src/common/ch-api/_fixtures/assets/publish/POST.json create mode 100644 src/common/ch-api/_fixtures/assets/publish/validate/POST.json create mode 100644 src/common/ch-api/_fixtures/assets/text/POST-external.json create mode 100644 src/common/ch-api/_fixtures/assets/text/POST.json create mode 100644 src/common/ch-api/_fixtures/assets/unpublish/POST.json create mode 100644 src/common/ch-api/_fixtures/assets/{id}/download/GET.json create mode 100644 src/common/ch-api/_fixtures/assets/{id}/metadata/GET.json create mode 100644 src/common/ch-api/_fixtures/assets/{id}/revert/PUT-{version}.json create mode 100644 src/common/ch-api/_fixtures/assets/{id}/thumbnails/GET-gfail.json create mode 100644 src/common/ch-api/_fixtures/assets/{id}/transcodings/GET.json create mode 100644 src/common/ch-api/_fixtures/assets/{id}/versions/GET-{version}.json create mode 100644 src/common/ch-api/_fixtures/assets/{id}/versions/GET.json create mode 100644 src/common/ch-api/_fixtures/assets/{id}/versions/{version}/download/GET.json create mode 100644 src/common/ch-api/_fixtures/assets/{id}/video_profiles/GET.json create mode 100644 src/common/ch-api/_fixtures/settings/GET.json create mode 100644 src/common/ch-api/api/model/ApiResource.ts create mode 100644 src/common/ch-api/api/services/ApiClient.ts create mode 100644 src/common/ch-api/api/services/ApiEndpoints.ts create mode 100644 src/common/ch-api/api/services/CURIEs.spec.ts create mode 100644 src/common/ch-api/api/services/CURIEs.ts create mode 100644 src/common/ch-api/http/AxiosHttpClient.spec.ts create mode 100644 src/common/ch-api/http/AxiosHttpClient.ts create mode 100644 src/common/ch-api/http/HttpClient.ts create mode 100644 src/common/ch-api/http/HttpError.ts create mode 100644 src/common/ch-api/http/HttpRequest.ts create mode 100644 src/common/ch-api/http/HttpResponse.ts create mode 100644 src/common/ch-api/model/ActionPriority.ts create mode 100644 src/common/ch-api/model/Asset.spec.ts create mode 100644 src/common/ch-api/model/Asset.ts create mode 100644 src/common/ch-api/model/AssetListRequest.ts create mode 100644 src/common/ch-api/model/AssetMetadata.ts create mode 100644 src/common/ch-api/model/AssetPut.ts create mode 100644 src/common/ch-api/model/AssetPutResult.ts create mode 100644 src/common/ch-api/model/AssetRequest.ts create mode 100644 src/common/ch-api/model/AssetText.ts create mode 100644 src/common/ch-api/model/Page.ts create mode 100644 src/common/ch-api/model/Pageable.ts create mode 100644 src/common/ch-api/model/PublishActivitySummary.ts create mode 100644 src/common/ch-api/model/PublishInfoPut.ts create mode 100644 src/common/ch-api/model/ResourceList.spec.ts create mode 100644 src/common/ch-api/model/ResourceList.ts create mode 100644 src/common/ch-api/model/Settings.spec.ts create mode 100644 src/common/ch-api/model/Settings.ts create mode 100644 src/common/ch-api/model/StringList.ts create mode 100644 src/common/ch-api/model/TagsPut.ts create mode 100644 src/common/ch-api/model/WorkflowSummary.ts create mode 100644 src/common/ch-api/oauth2/models/AccessToken.ts create mode 100644 src/common/ch-api/oauth2/models/AccessTokenProvider.ts create mode 100644 src/common/ch-api/oauth2/models/OAuth2ClientCredentials.ts create mode 100644 src/common/ch-api/oauth2/services/OAuth2Client.spec.ts create mode 100644 src/common/ch-api/oauth2/services/OAuth2Client.ts create mode 100644 src/common/ch-api/utils/URL.spec.ts create mode 100644 src/common/ch-api/utils/URL.ts create mode 100644 src/common/media/__mocks__/ch-client-factory.ts delete mode 100644 src/common/media/__mocks__/dam-client-factory.ts rename src/common/media/{mock-dam.ts => mock-ch.ts} (80%) rename src/services/{dam-client-factory.ts => ch-client-factory.ts} (62%) diff --git a/package-lock.json b/package-lock.json index 495cb946..a215f293 100644 --- a/package-lock.json +++ b/package-lock.json @@ -741,14 +741,6 @@ "chalk": "*" } }, - "@types/es6-promise": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@types/es6-promise/-/es6-promise-3.3.0.tgz", - "integrity": "sha512-ixCIAEkLUKv9movnHKCzx2rzAJgEnSALDXPrOSSwOjWwXFs0ssSZKan+O2e3FExPPCbX+DfA9NcKsbvLuyUlNA==", - "requires": { - "es6-promise": "*" - } - }, "@types/eslint-visitor-keys": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", @@ -893,6 +885,12 @@ "integrity": "sha512-HKtXvBxU8U8evZCSlUi9HbfT/SFW7nSGCoiBEheB06jAhXeW6JbGh8biEAqIFG5rZo9f8xeJVdIn455sddmIcw==", "dev": true }, + "@types/url-template": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/@types/url-template/-/url-template-2.0.28.tgz", + "integrity": "sha512-1i/YtOhvlWDbMDTWhCfvhyUwBS9vNFs78sJOyahoruJCcDbwaSH73AlnuCp7luKPm6qqdCg4VKq/IHUncl6gZA==", + "dev": true + }, "@types/yargs": { "version": "13.0.2", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.2.tgz", @@ -1205,6 +1203,24 @@ } } }, + "axios-mock-adapter": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.19.0.tgz", + "integrity": "sha512-D+0U4LNPr7WroiBDvWilzTMYPYTuZlbo6BI8YHZtj7wYQS8NkARlP9KBt8IWWHTQJ0q/8oZ0ClPBtKCCkx8cQg==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3", + "is-buffer": "^2.0.3" + }, + "dependencies": { + "is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "dev": true + } + } + }, "babel-jest": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-24.9.0.tgz", @@ -2576,36 +2592,6 @@ "word-wrap": "^1.0.3" } }, - "dam-management-sdk-js": { - "version": "git+https://github.com/rs-amp/dam-management-sdk-js.git#ffefa0da28c7723c0f627959362e94627f86ed6b", - "from": "git+https://github.com/rs-amp/dam-management-sdk-js.git", - "requires": { - "@types/es6-promise": "^3.3.0", - "@types/node": "^14.14.5", - "axios": "^0.21.0", - "url-template": "^2.0.8" - }, - "dependencies": { - "@types/node": { - "version": "14.14.35", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.35.tgz", - "integrity": "sha512-Lt+wj8NVPx0zUmUwumiVXapmaLUcAk3yPuHCFVXras9k5VT9TdhJqKqGVUQCD60OTMCl0qxJ57OiTL0Mic3Iag==" - }, - "axios": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", - "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", - "requires": { - "follow-redirects": "^1.10.0" - } - }, - "follow-redirects": { - "version": "1.13.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.3.tgz", - "integrity": "sha512-DUgl6+HDzB0iEptNQEXLx/KhTmDb8tZUHSeLqpnjpknR70H0nC2t9N73BK6fN4hOvJ84pKlIQVQ4k5FFlBedKA==" - } - } - }, "dargs": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/dargs/-/dargs-4.1.0.tgz", @@ -2907,11 +2893,6 @@ "is-symbol": "^1.0.2" } }, - "es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" - }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", diff --git a/package.json b/package.json index c563d829..ec75f504 100644 --- a/package.json +++ b/package.json @@ -90,9 +90,11 @@ "@types/node-fetch": "^2.5.7", "@types/rimraf": "^3.0.0", "@types/table": "^4.0.7", + "@types/url-template": "^2.0.28", "@typescript-eslint/eslint-plugin": "^2.3.0", "@typescript-eslint/parser": "^2.3.0", "adm-zip": "^0.4.13", + "axios-mock-adapter": "^1.19.0", "commitizen": "^4.0.3", "cz-conventional-changelog": "^3.0.2", "eslint": "^6.4.0", @@ -112,7 +114,6 @@ "ajv": "^6.12.3", "axios": "^0.18.1", "chalk": "^2.4.2", - "dam-management-sdk-js": "git+https://github.com/rs-amp/dam-management-sdk-js.git", "dc-management-sdk-js": "^1.9.0", "lodash": "^4.17.15", "node-fetch": "^2.6.0", diff --git a/src/common/ch-api/ContentHub.mocks.ts b/src/common/ch-api/ContentHub.mocks.ts new file mode 100644 index 00000000..cc9b5a06 --- /dev/null +++ b/src/common/ch-api/ContentHub.mocks.ts @@ -0,0 +1,66 @@ +/* eslint-disable @typescript-eslint/camelcase */ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { AxiosRequestConfig } from 'axios'; +import { ContentHub, ContentHubConfig } from './ContentHub'; +import { ApiClient } from './api/services/ApiClient'; +import { AxiosHttpClient } from './http/AxiosHttpClient'; +import { HttpClient } from './http/HttpClient'; +import { AccessTokenProvider } from './oauth2/models/AccessTokenProvider'; +import { OAuth2ClientCredentials } from './oauth2/models/OAuth2ClientCredentials'; + +/** + * @hidden + */ +import MockAdapter from 'axios-mock-adapter'; +import { DAMFixtures } from './_fixtures/ContentHubFixtures.mocks'; +import { AccessToken } from 'dc-management-sdk-js'; + +/** + * @hidden + */ +export class MockContentHub extends ContentHub { + public mock: MockAdapter; + public mockClient: ApiClient; + + constructor( + clientCredentials?: OAuth2ClientCredentials, + damConfig?: ContentHubConfig, + httpClient?: AxiosRequestConfig + ) { + super( + clientCredentials || { + client_id: 'client_id', + client_secret: 'client_secret' + }, + damConfig, + httpClient + ); + } + + protected createTokenClient( + damConfig: ContentHubConfig, + clientCredentials: OAuth2ClientCredentials, + httpClient: HttpClient + ): AccessTokenProvider { + return { + getToken: (): Promise => + Promise.resolve({ + access_token: 'token', + expires_in: 60, + refresh_token: 'refresh' + }) + }; + } + + protected createResourceClient( + damConfig: ContentHubConfig, + tokenProvider: AccessTokenProvider, + httpClient: HttpClient + ): ApiClient { + const client = super.createResourceClient(damConfig, tokenProvider, httpClient); + this.mock = new MockAdapter((httpClient as AxiosHttpClient).client); + this.mockClient = client; + DAMFixtures.install(this.mock); + return client; + } +} diff --git a/src/common/ch-api/ContentHub.ts b/src/common/ch-api/ContentHub.ts new file mode 100644 index 00000000..d56c0d3b --- /dev/null +++ b/src/common/ch-api/ContentHub.ts @@ -0,0 +1,101 @@ +import { AxiosRequestConfig } from 'axios'; +import { ApiClient, DefaultApiClient } from './api/services/ApiClient'; +import { ApiEndpoints } from './api/services/ApiEndpoints'; +import { AxiosHttpClient } from './http/AxiosHttpClient'; +import { HttpClient } from './http/HttpClient'; +import { AccessTokenProvider } from './oauth2/models/AccessTokenProvider'; +import { OAuth2ClientCredentials } from './oauth2/models/OAuth2ClientCredentials'; +import { OAuth2Client } from './oauth2/services/OAuth2Client'; + +/** + * Configuration settings for DAM API client. You can optionally + * override these values with environment specific values. + */ +export interface ContentHubConfig { + /** + * URL used to connect to the Amplience DAM API. + * This property defaults to 'https://dam-api.amplience.net/v1.5.0' if not provided + */ + apiUrl?: string; + + /** + * URL used to connect to the Amplience OAuth API. + * This property defaults to 'https://auth.amplience.net' if not provided + */ + authUrl?: string; +} + +export class ContentHub { + /** + * Asset Resources + */ + public assets: ApiEndpoints['assets']; + + /** + * DAM Settings + */ + public settings: ApiEndpoints['settings']; + + /** + * @hidden + */ + private client: ApiClient; + + /** + * Creates a Dynamic Content API client instance. You must provide credentials that will + * be used to authenticate with the API. + * + * @param clientCredentials Api credentials used to generate an authentication token + * @param damConfig Optional configuration settings for Dynamic Content + * @param clientConfig Optional request settings, can be used to provide proxy settings, add interceptors etc + */ + constructor( + clientCredentials: OAuth2ClientCredentials, + damConfig?: ContentHubConfig, + httpClient?: AxiosRequestConfig | HttpClient + ) { + damConfig = damConfig || {}; + damConfig.apiUrl = damConfig.apiUrl || 'https://dam-api.amplience.net/v1.5.0'; + damConfig.authUrl = damConfig.authUrl || 'https://auth.amplience.net'; + + let httpClientInstance: HttpClient; + if (httpClient !== undefined && 'request' in httpClient) { + httpClientInstance = httpClient as HttpClient; + } else { + httpClientInstance = new AxiosHttpClient(httpClient === undefined ? {} : (httpClient as AxiosRequestConfig)); + } + + const tokenClient = this.createTokenClient(damConfig, clientCredentials, httpClientInstance); + + this.client = this.createResourceClient(damConfig, tokenClient, httpClientInstance); + + this.initEndpoints(); + } + + protected createTokenClient( + damConfig: ContentHubConfig, + clientCredentials: OAuth2ClientCredentials, + httpClient: HttpClient + ): AccessTokenProvider { + return new OAuth2Client( + clientCredentials, + { + authUrl: damConfig.authUrl + }, + httpClient + ); + } + + protected createResourceClient( + damConfig: ContentHubConfig, + tokenProvider: AccessTokenProvider, + httpClient: HttpClient + ): ApiClient { + return new DefaultApiClient(damConfig.apiUrl as string, httpClient, tokenProvider); + } + + private initEndpoints(): void { + this.assets = this.client.endpoints.assets; + this.settings = this.client.endpoints.settings; + } +} diff --git a/src/common/ch-api/_fixtures/ContentHubFixtures.mocks.ts b/src/common/ch-api/_fixtures/ContentHubFixtures.mocks.ts new file mode 100644 index 00000000..e8fceadf --- /dev/null +++ b/src/common/ch-api/_fixtures/ContentHubFixtures.mocks.ts @@ -0,0 +1,142 @@ +import MockAdapter from 'axios-mock-adapter'; +import { readFileSync, readdirSync, statSync } from 'fs'; +import { basename, dirname, join, relative } from 'path'; +import { resolve } from 'url'; + +interface ContentHubFixtureParameters { + selectors?: string[]; + [K: string]: string | string[] | undefined; +} + +function escapeForRegex(url: string): string { + return url.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); +} + +export class DAMFixtures { + static install(adapter: MockAdapter): void { + // Load fixtures. + + const fixturesBaseDir = __dirname; + + this.installSpecific(adapter, fixturesBaseDir + '/assets', { + id: '65d78690-bf4e-415d-a16c-ca4dadbb2717', + version: '1' + }); + this.installSpecific(adapter, fixturesBaseDir + '/assets', { + id: '1f7b5ac6-bb0c-4cd1-b5a1-190ede681b8f', + selectors: ['fail'] + }); + + this.installSpecific(adapter, fixturesBaseDir + '/settings', {}); + } + + private static installSpecific(adapter: MockAdapter, baseDir: string, params: ContentHubFixtureParameters): void { + // Scan the given folder for fixtures. + + // For each file, create register a response. Filenames follow a specific format: + // uri/path/from/base/-{}-.json + // Any {paramater}s will be replaced with those provided, and be treated as the final directory name. + // Parameters can also be on folder names, too. /assets/{id}/GET.json and /assets/GET-{id}.json are both valid. + // Responses with selectors will only be registered if the specified selector has been provided. + + const contents = readdirSync(baseDir); + + contents.forEach(name => { + const fullPath = join(baseDir, name); + + if (statSync(fullPath).isDirectory()) { + // Continue traversal + this.installSpecific(adapter, fullPath, params); + } else if (fullPath.endsWith(`.json`)) { + // Add this file as a response. + this.installFile(adapter, fullPath, params); + } + }); + } + + private static installFile(adapter: MockAdapter, path: string, params: ContentHubFixtureParameters): void { + const baseUri = 'https://dam-api.amplience.net/v1.5.0/'; + + const baseName = basename(path, '.json'); + let dirName = relative(__dirname, dirname(path)); + + const nameSplit = baseName.split('-'); + + if (nameSplit.length === 0) return; + + const method = nameSplit[0]; + let pathExtension = ''; + let selectorCount = 0; + + for (let i = 1; i < nameSplit.length; i++) { + const arg = nameSplit[i]; + + if (arg.startsWith('{') && arg.endsWith('}')) { + // Insert the given ID + const paramName = arg.substr(1, arg.length - 2); + + if (params[paramName] != null) { + pathExtension = params[paramName] as string; + } else { + return; // Skip this file. + } + } else { + // Is this selector present in the params? + selectorCount++; + if (params.selectors == null || params.selectors.indexOf(arg) === -1) { + return; // Skip this file. + } + } + } + + if (selectorCount === 0 && params.selectors != null && params.selectors.length > 0) { + return; // If selectors are present, only add responses that also have selectors. + } + + for (let i = 0; i < dirName.length; i++) { + if (dirName[i] === '{') { + for (let j = i + 1; j < dirName.length; j++) { + if (dirName[j] === '}') { + const paramName = dirName.substring(i + 1, j); + + if (params[paramName] != null) { + dirName = dirName.substring(0, i) + params[paramName] + dirName.substring(j + 1); + break; + } else { + return; // Skip this file. + } + } + } + } + } + + const response = JSON.parse(readFileSync(path, { encoding: 'utf8' })); + let fullUri = resolve(baseUri, dirName); + if (pathExtension !== '') { + fullUri = resolve(fullUri + '/', pathExtension); + } + + const responseCode = method === 'POST' ? 204 : 200; + + // Allow any url with query string + const regex = new RegExp('^' + escapeForRegex(fullUri) + '(\\?.*$)?$'); + + switch (method) { + case 'GET': + adapter.onGet(regex).reply(responseCode, response); + break; + case 'POST': + adapter.onPost(regex).reply(responseCode, response); + break; + case 'PATCH': + adapter.onPatch(regex).reply(responseCode, response); + break; + case 'PUT': + adapter.onPut(regex).reply(responseCode, response); + break; + case 'DELETE': + adapter.onDelete(regex).reply(responseCode, response); + break; + } + } +} diff --git a/src/common/ch-api/_fixtures/assets/DELETE-gfail.json b/src/common/ch-api/_fixtures/assets/DELETE-gfail.json new file mode 100644 index 00000000..e5d11636 --- /dev/null +++ b/src/common/ch-api/_fixtures/assets/DELETE-gfail.json @@ -0,0 +1,4 @@ +{ + "error": "Bad Request: Missing json array of asset IDs.", + "status": "failed" +} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/DELETE-{id}-fail.json b/src/common/ch-api/_fixtures/assets/DELETE-{id}-fail.json new file mode 100644 index 00000000..6f483ca5 --- /dev/null +++ b/src/common/ch-api/_fixtures/assets/DELETE-{id}-fail.json @@ -0,0 +1,4 @@ +{ + "error": "Bad Request: Asset ID missing.", + "status": "failed" +} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/DELETE-{id}.json b/src/common/ch-api/_fixtures/assets/DELETE-{id}.json new file mode 100644 index 00000000..3e33c43a --- /dev/null +++ b/src/common/ch-api/_fixtures/assets/DELETE-{id}.json @@ -0,0 +1,4 @@ +{ + "content": "65d78690-bf4e-415d-a16c-ca4dadbb2717", + "status": "success" +} diff --git a/src/common/ch-api/_fixtures/assets/DELETE.json b/src/common/ch-api/_fixtures/assets/DELETE.json new file mode 100644 index 00000000..3e33c43a --- /dev/null +++ b/src/common/ch-api/_fixtures/assets/DELETE.json @@ -0,0 +1,4 @@ +{ + "content": "65d78690-bf4e-415d-a16c-ca4dadbb2717", + "status": "success" +} diff --git a/src/common/ch-api/_fixtures/assets/GET-{id}-select.json b/src/common/ch-api/_fixtures/assets/GET-{id}-select.json new file mode 100644 index 00000000..84b5f87b --- /dev/null +++ b/src/common/ch-api/_fixtures/assets/GET-{id}-select.json @@ -0,0 +1,130 @@ +{ + "content": { + "data": [ + { + "srcName": "AlltheLook1.jpg", + "revisionNumber": 5, + "mimeType": "image/jpeg", + "type": "image", + "locale": "en-GB", + "userID": "57b98b7f-c6bf-43d2-9f24-a408d1c6d59d", + "thumbFile": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", + "folderID": "00000000-0000-0000-0000-000000000000", + "relationships": { + "containsImage": [ + { + "schema": "image", + "schemaID": "4e357e14-5141-4153-979e-125e4bdb9c97", + "variants": [ + { + "values": { + "colorSpace": "rgb", + "resolutionY": 0, + "valid": true, + "resolutionX": 0, + "depth": 8, + "alpha": false, + "format": "JPEG", + "width": 1289, + "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", + "height": 1400, + "resolutionUnits": 0 + }, + "variantID": "3e8a4d62-8058-46d9-a75d-bb735537e85c", + "variantName": "*" + } + ], + "PK": { + "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717" + } + } + ], + "containsEXIF": [ + { + "schema": "exif", + "schemaID": "cf9303dc-7b01-46e3-ba34-072390a4684d", + "variants": [ + { + "values": { + "software": "Adobe Photoshop CC 2014 (Macintosh)", + "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717" + }, + "variantID": "3e8a4d62-8058-46d9-a75d-bb735537e85c", + "variantName": "*" + } + ], + "PK": { + "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717" + } + } + ], + "fromFile": [ + { + "schema": "file", + "schemaID": "590ec5d8-d2f1-4df3-b52c-32df6e166d0f", + "variants": [ + { + "values": { + "size": 1109425, + "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", + "type": "JPEG" + }, + "variantID": "3e8a4d62-8058-46d9-a75d-bb735537e85c", + "variantName": "*" + } + ], + "PK": { + "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717" + } + } + ] + }, + "file": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", + "unpublish": { + "last": { + "jobID": "unpublish.8f0034fd-e0e7-4a55-a81b-a87054827a8f", + "revisionNumber": 18, + "timestamp": 1610382560948, + "status": "COMPLETED" + }, + "lastSuccess": { + "jobID": "unpublish.8f0034fd-e0e7-4a55-a81b-a87054827a8f", + "revisionNumber": 18, + "timestamp": 1610382560948, + "status": "COMPLETED" + } + }, + "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", + "thumbURL": "https://thumbs.amplience.net/r/74bef2ea-010b-4853-a5ee-f0b4daa59f0d", + "publishStatus": "PUBLISHED", + "timestamp": 1610384041283, + "workflow": {}, + "bucketID": "b0d42ebb-679b-4010-ae20-9ce989837481", + "label": "AlltheLook1.jpg", + "tags": [], + "createdDate": 1606739632488, + "size": 1109425, + "publish": { + "last": { + "jobID": "p.c78c904d-72c4-497b-be11-1d0a99c52627", + "revisionNumber": 21, + "timestamp": 1610384041207, + "status": "COMPLETED" + }, + "lastSuccess": { + "jobID": "p.c78c904d-72c4-497b-be11-1d0a99c52627", + "revisionNumber": 21, + "timestamp": 1610384041207, + "status": "COMPLETED" + } + }, + "name": "AlltheLook1", + "localeGroupAssets": [], + "subType": null, + "status": "active" + } + ], + "count": 1 + }, + "status": "success" +} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/GET-{id}.json b/src/common/ch-api/_fixtures/assets/GET-{id}.json new file mode 100644 index 00000000..faf9f6d2 --- /dev/null +++ b/src/common/ch-api/_fixtures/assets/GET-{id}.json @@ -0,0 +1,29 @@ +{ + "content": { + "data": [ + { + "srcName": "AlltheLook1.jpg", + "revisionNumber": 5, + "bucketID": "b0d42ebb-679b-4010-ae20-9ce989837481", + "label": "AlltheLook1.jpg", + "mimeType": "image/jpeg", + "type": "image", + "locale": "en-GB", + "userID": "57b98b7f-c6bf-43d2-9f24-a408d1c6d59d", + "thumbFile": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", + "folderID": "00000000-0000-0000-0000-000000000000", + "file": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", + "createdDate": 1606739632488, + "name": "AlltheLook1", + "subType": null, + "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", + "thumbURL": "https://thumbs.amplience.net/r/74bef2ea-010b-4853-a5ee-f0b4daa59f0d", + "publishStatus": "PUBLISHED", + "status": "active", + "timestamp": 1610384041283 + } + ], + "count": 1 + }, + "status": "success" +} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/GET.json b/src/common/ch-api/_fixtures/assets/GET.json new file mode 100644 index 00000000..946a7865 --- /dev/null +++ b/src/common/ch-api/_fixtures/assets/GET.json @@ -0,0 +1,52 @@ +{ + "content": { + "data": [ + { + "srcName": "AlltheLook1.jpg", + "revisionNumber": 5, + "bucketID": "b0d42ebb-679b-4010-ae20-9ce989837481", + "label": "AlltheLook1.jpg", + "mimeType": "image/jpeg", + "type": "image", + "locale": "en-GB", + "thumbFile": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", + "folderID": "00000000-0000-0000-0000-000000000000", + "file": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", + "createdDate": 1606739632488, + "name": "AlltheLook1", + "subType": null, + "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", + "thumbURL": "https://thumbs.amplience.net/r/74bef2ea-010b-4853-a5ee-f0b4daa59f0d", + "publishStatus": "PUBLISHED", + "status": "active", + "workflow": {}, + "timestamp": 1610120563111 + }, + { + "srcName": "1.png", + "revisionNumber": 3, + "bucketID": "b0d42ebb-679b-4010-ae20-9ce989837481", + "label": "1.png", + "mimeType": "image/png", + "type": "image", + "locale": "en-GB", + "thumbFile": "2af0bde1-473c-455a-afb5-17f90da7c2f9", + "folderID": "00000000-0000-0000-0000-000000000000", + "file": "2af0bde1-473c-455a-afb5-17f90da7c2f9", + "createdDate": 1414750113914, + "name": "1", + "subType": null, + "id": "4a0bcaed-ee57-481f-98d3-4b73aad49e68", + "thumbURL": "https://thumbs.amplience.net/r/2af0bde1-473c-455a-afb5-17f90da7c2f9", + "publishStatus": "NOT_PUBLISHED", + "status": "active", + "timestamp": 1607622717672 + } + ], + "numFound": 2, + "start": 0, + "count": 2, + "pageSize": 20 + }, + "status": "success" +} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/PUT-gfail.json b/src/common/ch-api/_fixtures/assets/PUT-gfail.json new file mode 100644 index 00000000..97675a6c --- /dev/null +++ b/src/common/ch-api/_fixtures/assets/PUT-gfail.json @@ -0,0 +1,4 @@ +{ + "error": "Bad Request: No assets found.", + "status": "failed" +} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/PUT.json b/src/common/ch-api/_fixtures/assets/PUT.json new file mode 100644 index 00000000..e0d731e6 --- /dev/null +++ b/src/common/ch-api/_fixtures/assets/PUT.json @@ -0,0 +1,13 @@ +{ + "content": [ + { + "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", + "status": "succeeded" + }, + { + "id": "4a0bcaed-ee57-481f-98d3-4b73aad49e68", + "status": "succeeded" + } + ], + "status": "success" +} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/_localeGroups/GET-{ids}.json b/src/common/ch-api/_fixtures/assets/_localeGroups/GET-{ids}.json new file mode 100644 index 00000000..d74ae83e --- /dev/null +++ b/src/common/ch-api/_fixtures/assets/_localeGroups/GET-{ids}.json @@ -0,0 +1,7 @@ +{ + "content": { + "data": [], + "count": 0 + }, + "status": "success" +} diff --git a/src/common/ch-api/_fixtures/assets/badid/GET.json b/src/common/ch-api/_fixtures/assets/badid/GET.json new file mode 100644 index 00000000..62a6f24e --- /dev/null +++ b/src/common/ch-api/_fixtures/assets/badid/GET.json @@ -0,0 +1,4 @@ +{ + "error": "Bad Request: Invalid Asset ID.", + "status": "failed" +} diff --git a/src/common/ch-api/_fixtures/assets/publish/POST.json b/src/common/ch-api/_fixtures/assets/publish/POST.json new file mode 100644 index 00000000..5cb1f95a --- /dev/null +++ b/src/common/ch-api/_fixtures/assets/publish/POST.json @@ -0,0 +1,9 @@ +{ + "content": { + "data": [ + "publish.8d1bb161-4de3-4cc7-a907-29636842032a" + ], + "count": 1 + }, + "status": "success" +} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/publish/validate/POST.json b/src/common/ch-api/_fixtures/assets/publish/validate/POST.json new file mode 100644 index 00000000..8815c5a2 --- /dev/null +++ b/src/common/ch-api/_fixtures/assets/publish/validate/POST.json @@ -0,0 +1,4 @@ +{ + "content": null, + "status": "success" +} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/text/POST-external.json b/src/common/ch-api/_fixtures/assets/text/POST-external.json new file mode 100644 index 00000000..05fd95af --- /dev/null +++ b/src/common/ch-api/_fixtures/assets/text/POST-external.json @@ -0,0 +1,11 @@ +{ + "content": [ + { + "data": "https://thumbs.amplience.net/r/74bef2ea-010b-4853-a5ee-f0b4daa59f0d", + "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", + "status": "EXTERNAL", + "info": "File contents too large" + } + ], + "status": "success" +} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/text/POST.json b/src/common/ch-api/_fixtures/assets/text/POST.json new file mode 100644 index 00000000..768d4d18 --- /dev/null +++ b/src/common/ch-api/_fixtures/assets/text/POST.json @@ -0,0 +1,11 @@ +{ + "content": [ + { + "data": "Text Content Example", + "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", + "status": "INTERNAL", + "info": "" + } + ], + "status": "success" +} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/unpublish/POST.json b/src/common/ch-api/_fixtures/assets/unpublish/POST.json new file mode 100644 index 00000000..dc82556a --- /dev/null +++ b/src/common/ch-api/_fixtures/assets/unpublish/POST.json @@ -0,0 +1,9 @@ +{ + "content": { + "data": [ + "unpublish.8f0034fd-e0e7-4a55-a81b-a87054827a8f" + ], + "count": 1 + }, + "status": "success" +} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/{id}/download/GET.json b/src/common/ch-api/_fixtures/assets/{id}/download/GET.json new file mode 100644 index 00000000..4330d073 --- /dev/null +++ b/src/common/ch-api/_fixtures/assets/{id}/download/GET.json @@ -0,0 +1,4 @@ +{ + "content": "/assets/65d78690-bf4e-415d-a16c-ca4dadbb2717/download/handle?auth=example", + "status": "success" +} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/{id}/metadata/GET.json b/src/common/ch-api/_fixtures/assets/{id}/metadata/GET.json new file mode 100644 index 00000000..c5046601 --- /dev/null +++ b/src/common/ch-api/_fixtures/assets/{id}/metadata/GET.json @@ -0,0 +1,69 @@ +{ + "content": { + "metadata": [ + { + "schema": "exif", + "schemaID": "cf9303dc-7b01-46e3-ba34-072390a4684d", + "variants": [ + { + "values": { + "software": "Adobe Photoshop CC 2014 (Macintosh)", + "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717" + }, + "variantID": "3e8a4d62-8058-46d9-a75d-bb735537e85c", + "variantName": "*" + } + ], + "PK": { + "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717" + } + }, + { + "schema": "file", + "schemaID": "590ec5d8-d2f1-4df3-b52c-32df6e166d0f", + "variants": [ + { + "values": { + "size": 1109425, + "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", + "type": "JPEG" + }, + "variantID": "3e8a4d62-8058-46d9-a75d-bb735537e85c", + "variantName": "*" + } + ], + "PK": { + "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717" + } + }, + { + "schema": "image", + "schemaID": "4e357e14-5141-4153-979e-125e4bdb9c97", + "variants": [ + { + "values": { + "colorSpace": "rgb", + "resolutionY": 0, + "valid": true, + "resolutionX": 0, + "depth": 8, + "alpha": false, + "format": "JPEG", + "width": 1289, + "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", + "height": 1400, + "resolutionUnits": 0 + }, + "variantID": "3e8a4d62-8058-46d9-a75d-bb735537e85c", + "variantName": "*" + } + ], + "PK": { + "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717" + } + } + ], + "assetId": "65d78690-bf4e-415d-a16c-ca4dadbb2717" + }, + "status": "success" +} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/{id}/revert/PUT-{version}.json b/src/common/ch-api/_fixtures/assets/{id}/revert/PUT-{version}.json new file mode 100644 index 00000000..e2669e0e --- /dev/null +++ b/src/common/ch-api/_fixtures/assets/{id}/revert/PUT-{version}.json @@ -0,0 +1,9 @@ +{ + "content": [ + { + "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", + "status": "succeeded" + } + ], + "status": "success" +} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/{id}/thumbnails/GET-gfail.json b/src/common/ch-api/_fixtures/assets/{id}/thumbnails/GET-gfail.json new file mode 100644 index 00000000..29c2059b --- /dev/null +++ b/src/common/ch-api/_fixtures/assets/{id}/thumbnails/GET-gfail.json @@ -0,0 +1,4 @@ +{ + "error": "Not Found: Unable to find video thumbnails for supplied asset id.", + "status": "failed" +} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/{id}/transcodings/GET.json b/src/common/ch-api/_fixtures/assets/{id}/transcodings/GET.json new file mode 100644 index 00000000..6e285a66 --- /dev/null +++ b/src/common/ch-api/_fixtures/assets/{id}/transcodings/GET.json @@ -0,0 +1,4 @@ +{ + "error": "Not Found: Unable to find video transcoding for supplied asset id.", + "status": "failed" +} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/{id}/versions/GET-{version}.json b/src/common/ch-api/_fixtures/assets/{id}/versions/GET-{version}.json new file mode 100644 index 00000000..f12c0ee1 --- /dev/null +++ b/src/common/ch-api/_fixtures/assets/{id}/versions/GET-{version}.json @@ -0,0 +1,36 @@ +{ + "content": { + "data": [ + { + "srcName": "AlltheLook1.jpg", + "workflow": {}, + "revisionNumber": 1, + "revertible": true, + "bucketID": "b0d42ebb-679b-4010-ae20-9ce989837481", + "label": "AlltheLook1.jpg", + "mimeType": "image/jpeg", + "type": "image", + "locale": "en-GB", + "userID": "526c39a9-f098-4ae7-8261-23821a4374cd", + "thumbFile": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", + "folderID": "00000000-0000-0000-0000-000000000000", + "file": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", + "createdDate": 1606739632488, + "contents": [], + "name": "AlltheLook1", + "subType": null, + "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", + "thumbURL": "https://thumbs.amplience.net/r/74bef2ea-010b-4853-a5ee-f0b4daa59f0d", + "user": "Username", + "changeSummary": [ + "Asset created" + ], + "publishStatus": "NOT_PUBLISHED", + "status": "active", + "timestamp": 1606739632488 + } + ], + "count": 1 + }, + "status": "success" +} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/{id}/versions/GET.json b/src/common/ch-api/_fixtures/assets/{id}/versions/GET.json new file mode 100644 index 00000000..1b79da93 --- /dev/null +++ b/src/common/ch-api/_fixtures/assets/{id}/versions/GET.json @@ -0,0 +1,151 @@ +{ + "content": { + "data": [ + { + "srcName": "AlltheLook1.jpg", + "workflow": {}, + "revisionNumber": 5, + "revertible": false, + "bucketID": "b0d42ebb-679b-4010-ae20-9ce989837481", + "label": "AlltheLook1.jpg", + "mimeType": "image/jpeg", + "type": "image", + "locale": "en-GB", + "userID": "57b98b7f-c6bf-43d2-9f24-a408d1c6d59d", + "thumbFile": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", + "folderID": "00000000-0000-0000-0000-000000000000", + "file": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", + "createdDate": 1606739632488, + "contents": [], + "name": "AlltheLook1", + "subType": null, + "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", + "thumbURL": "https://thumbs.amplience.net/r/74bef2ea-010b-4853-a5ee-f0b4daa59f0d", + "user": "restricted name", + "changeSummary": [ + "Asset published", + "Publish status changed to 'PUBLISHED' from 'PUBLISH_IN_PROGRESS'" + ], + "publishStatus": "PUBLISHED", + "status": "active", + "timestamp": 1606754846688 + }, + { + "srcName": "AlltheLook1.jpg", + "workflow": {}, + "revisionNumber": 4, + "revertible": false, + "bucketID": "b0d42ebb-679b-4010-ae20-9ce989837481", + "label": "AlltheLook1.jpg", + "mimeType": "image/jpeg", + "type": "image", + "locale": "en-GB", + "userID": "57b98b7f-c6bf-43d2-9f24-a408d1c6d59d", + "thumbFile": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", + "folderID": "00000000-0000-0000-0000-000000000000", + "file": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", + "createdDate": 1606739632488, + "contents": [], + "name": "AlltheLook1", + "subType": null, + "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", + "thumbURL": "https://thumbs.amplience.net/r/74bef2ea-010b-4853-a5ee-f0b4daa59f0d", + "user": "restricted name", + "changeSummary": [ + "System / other value updated" + ], + "publishStatus": "PUBLISH_IN_PROGRESS", + "status": "active", + "timestamp": 1606754845467 + }, + { + "srcName": "AlltheLook1.jpg", + "workflow": {}, + "revisionNumber": 3, + "revertible": false, + "bucketID": "b0d42ebb-679b-4010-ae20-9ce989837481", + "label": "AlltheLook1.jpg", + "mimeType": "image/jpeg", + "type": "image", + "locale": "en-GB", + "userID": "526c39a9-f098-4ae7-8261-23821a4374cd", + "thumbFile": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", + "folderID": "00000000-0000-0000-0000-000000000000", + "file": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", + "createdDate": 1606739632488, + "contents": [], + "name": "AlltheLook1", + "subType": null, + "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", + "thumbURL": "https://thumbs.amplience.net/r/74bef2ea-010b-4853-a5ee-f0b4daa59f0d", + "user": "Username", + "changeSummary": [ + "Publish status changed to 'PUBLISH_IN_PROGRESS' from 'NOT_PUBLISHED'", + "System / other value updated" + ], + "publishStatus": "PUBLISH_IN_PROGRESS", + "status": "active", + "timestamp": 1606754844819 + }, + { + "srcName": "AlltheLook1.jpg", + "workflow": {}, + "revisionNumber": 2, + "revertible": false, + "bucketID": "b0d42ebb-679b-4010-ae20-9ce989837481", + "label": "AlltheLook1.jpg", + "mimeType": "image/jpeg", + "type": "image", + "locale": "en-GB", + "userID": "57b98b7f-c6bf-43d2-9f24-a408d1c6d59d", + "thumbFile": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", + "folderID": "00000000-0000-0000-0000-000000000000", + "file": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", + "createdDate": 1606739632488, + "contents": [], + "name": "AlltheLook1", + "subType": null, + "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", + "thumbURL": "https://thumbs.amplience.net/r/74bef2ea-010b-4853-a5ee-f0b4daa59f0d", + "user": "restricted name", + "changeSummary": [ + "Asset inspected", + "System / other value updated" + ], + "publishStatus": "NOT_PUBLISHED", + "status": "active", + "timestamp": 1606739633484 + }, + { + "srcName": "AlltheLook1.jpg", + "workflow": {}, + "revisionNumber": 1, + "revertible": true, + "bucketID": "b0d42ebb-679b-4010-ae20-9ce989837481", + "label": "AlltheLook1.jpg", + "mimeType": "image/jpeg", + "type": "image", + "locale": "en-GB", + "userID": "526c39a9-f098-4ae7-8261-23821a4374cd", + "thumbFile": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", + "folderID": "00000000-0000-0000-0000-000000000000", + "file": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", + "createdDate": 1606739632488, + "contents": [], + "name": "AlltheLook1", + "subType": null, + "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", + "thumbURL": "https://thumbs.amplience.net/r/74bef2ea-010b-4853-a5ee-f0b4daa59f0d", + "user": "Username", + "changeSummary": [ + "Asset created" + ], + "publishStatus": "NOT_PUBLISHED", + "status": "active", + "timestamp": 1606739632488 + } + ], + "count": 21 + }, + "status": "success" + } \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/{id}/versions/{version}/download/GET.json b/src/common/ch-api/_fixtures/assets/{id}/versions/{version}/download/GET.json new file mode 100644 index 00000000..d1123f98 --- /dev/null +++ b/src/common/ch-api/_fixtures/assets/{id}/versions/{version}/download/GET.json @@ -0,0 +1,4 @@ +{ + "content": "/assets/65d78690-bf4e-415d-a16c-ca4dadbb2717/versions/1/download/handle?revisionNumber=1&auth=example", + "status": "success" +} diff --git a/src/common/ch-api/_fixtures/assets/{id}/video_profiles/GET.json b/src/common/ch-api/_fixtures/assets/{id}/video_profiles/GET.json new file mode 100644 index 00000000..5c7aff9b --- /dev/null +++ b/src/common/ch-api/_fixtures/assets/{id}/video_profiles/GET.json @@ -0,0 +1,7 @@ +{ + "content": { + "assetId": "65d78690-bf4e-415d-a16c-ca4dadbb2717", + "profiles": [] + }, + "status": "success" +} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/settings/GET.json b/src/common/ch-api/_fixtures/settings/GET.json new file mode 100644 index 00000000..8d1b72ab --- /dev/null +++ b/src/common/ch-api/_fixtures/settings/GET.json @@ -0,0 +1,53 @@ +{ + "content": { + "companyId": 1, + "companyClassificationType": "Demo", + "system": { + "auth-api-url": "https://auth.amplience.net/", + "publishing-service": "https://publishing-service.amplience.net/", + "im-api-url": "https://im-api.adis.ws/", + "virtual-staging-service": "https://virtual-staging.adis.ws/", + "dam-mixpanel-token": "", + "analytics-beacon-url": "https://a1.adis.ws/", + "analytics-query-api-url": "https://analytics.adis.ws/", + "dam-version": "jenkins-release-1.50.0/3", + "stream-api-url": "https://download-api.amplience.net/", + "ca-mixpanel-token": "", + "access-service": "https://access.adis.ws/", + "cms-api-url": "https://content-authoring-api.amplience.net/", + "provisioning-api": "https://provisioning-api.amplience.net/", + "identity-service": "https://identity.adis.ws/", + "media-graphics-host": "x1.adis.ws/v1/media/graphics" + }, + "di": { + "endpoints": [ + { + "path": "example", + "staticHost": "s1.adis.ws", + "textProtocols": [ + "http", + "https" + ], + "schemaIds": [ + "aaaaaaaa-bbbb-cccc-dddd-eeeeeeffffff" + ], + "dynamicHost": "i1.adis.ws", + "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeffffff", + "staticProtocols": [ + "http", + "https" + ], + "serviceId": "", + "textHost": "c1.adis.ws", + "dynamicProtocols": [ + "http", + "https" + ] + } + ], + "defaultEndpoint": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeffffff", + "enabled": true + } + }, + "status": "success" +} \ No newline at end of file diff --git a/src/common/ch-api/api/model/ApiResource.ts b/src/common/ch-api/api/model/ApiResource.ts new file mode 100644 index 00000000..fed84e1e --- /dev/null +++ b/src/common/ch-api/api/model/ApiResource.ts @@ -0,0 +1,65 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { ApiClient } from '../services/ApiClient'; + +/** + * @hidden + */ +export type ApiResourceConstructor = new (data?: any) => T; + +/** + * Base class for all Resources + */ +export class ApiResource { + /** + * @hidden + */ + // tslint:disable-next-line + protected _embedded: Map; + + /** + * @hidden + */ + protected client: ApiClient; + + /** + * Creates a new instance of the resource. + * If data is provided it will be parsed as if it had + * come from the remote api. + * @param data + */ + constructor(data?: any) { + if (data) { + this.parse(data); + } + } + + /** + * Parses the data returned by the API into the model class + * @hidden + */ + public parse(data: any): void { + Object.assign(this, data); + } + + /** + * Returns a copy of this resource's attributes excluding links and client references + */ + public toJSON(): any { + const result: any = Object.assign({}, this); + delete result.client; + delete result._links; + delete result.related; + return result; + } + + /** + * Set automatically by the HalClient when the resource is created. + * If this is not set the resource will be unable to resolve related + * resources and actions. + * + * @hidden + */ + public setClient(client: ApiClient): void { + this.client = client; + } +} diff --git a/src/common/ch-api/api/services/ApiClient.ts b/src/common/ch-api/api/services/ApiClient.ts new file mode 100644 index 00000000..97a85c02 --- /dev/null +++ b/src/common/ch-api/api/services/ApiClient.ts @@ -0,0 +1,197 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { HttpClient } from '../../http/HttpClient'; +import { HttpError } from '../../http/HttpError'; +import { HttpMethod, HttpRequest } from '../../http/HttpRequest'; +import { HttpResponse } from '../../http/HttpResponse'; +import { AccessTokenProvider } from '../../oauth2/models/AccessTokenProvider'; +import { combineURLs } from '../../utils/URL'; +import { ApiResource, ApiResourceConstructor } from '../model/ApiResource'; +import { ApiEndpoints } from './ApiEndpoints'; +import { CURIEs } from './CURIEs'; + +/** + * @hidden + */ +export interface ApiClient { + endpoints: ApiEndpoints; + + fetchRawResource(path: string, params: ApiParameters): Promise; + + fetchResource( + path: string, + params: ApiParameters, + resourceConstructor: ApiResourceConstructor + ): Promise; + + createResource( + path: string, + resource: T, + params: ApiParameters, + resourceConstructor: ApiResourceConstructor + ): Promise; + + updateResource( + path: string, + resource: T, + params: ApiParameters, + resourceConstructor: ApiResourceConstructor + ): Promise; + + genericRequest( + path: string, + method: HttpMethod, + body: any, + params: ApiParameters, + resourceConstructor?: ApiResourceConstructor + ): Promise; + + parse(data: any, resourceConstructor: ApiResourceConstructor): T; + + serialize(data: T): any; + + deleteResource(path: string, params: ApiParameters): Promise; +} + +/** + * Query and header parameters used in a resource request. + */ +export interface ApiParameters { + header?: any; + query?: any; +} + +/** + * @hidden + */ +export class DefaultApiClient implements ApiClient { + endpoints: ApiEndpoints; + + constructor(private baseUrl: string, private httpClient: HttpClient, private tokenProvider: AccessTokenProvider) { + this.endpoints = new ApiEndpoints(this); + } + + public async fetchRawResource(path: string, params: ApiParameters): Promise { + path = CURIEs.expand(path, params.query); + path = path.replace(/%20/g, '+'); // Convert space to + + + const response = await this.invoke({ + method: HttpMethod.GET, + url: path + }); + return (response.data as any) as T; + } + + public async fetchResource( + path: string, + params: ApiParameters, + resourceConstructor: ApiResourceConstructor + ): Promise { + const data = await this.fetchRawResource(path, params); + return this.parse(data, resourceConstructor); + } + + public async createResource( + path: string, + resource: T, + params: ApiParameters, + resourceConstructor: ApiResourceConstructor + ): Promise { + path = CURIEs.expand(path, params.query); + const response = await this.invoke({ + data: this.serialize(resource), + method: HttpMethod.POST, + url: path + }); + return this.parse(response.data, resourceConstructor); + } + + public async updateResource( + path: string, + resource: T, + params: ApiParameters, + resourceConstructor: ApiResourceConstructor + ): Promise { + path = CURIEs.expand(path, params.query); + const response = await this.invoke({ + data: this.serialize(resource), + method: HttpMethod.PATCH, + url: path + }); + return this.parse(response.data, resourceConstructor); + } + + public async genericRequest( + path: string, + method: HttpMethod, + body: any, + params: ApiParameters, + resourceConstructor: ApiResourceConstructor + ): Promise { + path = CURIEs.expand(path, params.query); + const response = await this.invoke({ + data: body, + method, + url: path + }); + return this.parse(response.data, resourceConstructor); + } + + public async deleteResource(path: string, params: ApiParameters): Promise { + path = CURIEs.expand(path, params.query); + await this.invoke({ + method: HttpMethod.DELETE, + url: path + }); + return Promise.resolve(); + } + + public parse(data: any, resourceConstructor: ApiResourceConstructor): T { + const instance: T = new resourceConstructor(data); + instance.setClient(this); + return instance; + } + + public serialize(data: T): any { + return JSON.parse(JSON.stringify(data)); + } + + protected transformDamResponse(data: any): any { + // Parse DAM response. All responses are in a common format. + + if (data.status !== 'success') { + throw new Error(`Request failed with status ${data.status}: ${JSON.stringify(data.content)}`); + } + + return data.content; + } + + public async invoke(request: HttpRequest): Promise { + const token = await this.tokenProvider.getToken(); + + const fullRequest: HttpRequest = { + data: request.data, + headers: { + 'X-Amp-Auth': token.access_token, + ...request.headers + }, + method: request.method, + url: combineURLs(this.baseUrl, request.url) + }; + + return this.httpClient.request(fullRequest).then(response => { + if (response.status >= 200 && response.status < 300) { + if (typeof response.data === 'string') { + response.data = JSON.parse(response.data); + } + response.data = this.transformDamResponse(response.data); + return response; + } else { + throw new HttpError( + `Request failed with status code ${response.status}: ${JSON.stringify(response.data)}`, + fullRequest, + response + ); + } + }); + } +} diff --git a/src/common/ch-api/api/services/ApiEndpoints.ts b/src/common/ch-api/api/services/ApiEndpoints.ts new file mode 100644 index 00000000..617d442f --- /dev/null +++ b/src/common/ch-api/api/services/ApiEndpoints.ts @@ -0,0 +1,236 @@ +import { HttpMethod } from '../../http/HttpRequest'; +import { Asset, AssetsList, AssetsPage } from '../../model/Asset'; +import { AssetListRequest } from '../../model/AssetListRequest'; +import { AssetMetadata } from '../../model/AssetMetadata'; +import { AssetPut } from '../../model/AssetPut'; +import { AssetPutResultList } from '../../model/AssetPutResult'; +import { AssetText, AssetTextList } from '../../model/AssetText'; +import { Settings } from '../../model/Settings'; +import { StringList } from '../../model/StringList'; +import { ApiClient } from './ApiClient'; + +/** + * Properties to strip from an asset before PUT. + * This allows updating an Asset with an existing Asset, rather than an AssetPut. + * NOTE: Tags are not supported as they must be added/removed as a delta, rather than replaced. + */ +const assetStrip = ['revisionNum', 'userId', 'file', 'createdDate', 'timestamp', 'tags']; + +export class ApiEndpoints { + constructor(private client: ApiClient) {} + + private assetsListToSingle(list: AssetsList, id: string): Asset { + const items = list.getItems(); + + if (items.length === 0) { + throw new Error(`Unable to find asset with id ${id}.`); + } + + return items[0]; + } + + /** + * Asset Resources + */ + public readonly assets = { + /** + * Retrieve an asset resource by id + * @param id asset id, previously generated on creation + */ + get: async (id: string): Promise => { + return this.assetsListToSingle(await this.client.fetchResource(`/assets/${id}`, {}, AssetsList), id); + }, + + /** + * Create or update an existing asset with new information. + * @param assets assets to upload to the DAM API. + * @returns A list of asset GUIDs. Throws on failure. + */ + put: (mode: 'overwrite' | 'renameUnique', assets: AssetPut[]): Promise => + this.client.genericRequest( + '/assets', + HttpMethod.PUT, + { mode, assets }, + {}, + AssetPutResultList + ), + + /** + * Create or update an existing asset, based on an existing asset. + * NOTE: Tags must be added/removed via the put() method. + * @param assets assets to upload to the DAM API. + * @returns A list of asset GUIDs. Throws on failure. + */ + putAsset: (mode: 'overwrite' | 'renameUnique', assets: Asset[]): Promise => { + const assetPuts = assets.map(asset => { + const copy = asset.toJSON(); + + assetStrip.forEach(key => { + delete copy[key]; + }); + + if (asset.workflow && asset.workflow.assignedTo == null) { + delete copy.workflow; + } + + return copy; + }); + + return this.client.genericRequest( + '/assets', + HttpMethod.PUT, + { mode, assets: assetPuts }, + {}, + AssetPutResultList + ); + }, + + /** + * Retrieve a list of asset resources shared with your client credentials. + * @param options Pagination options + */ + list: (options?: AssetListRequest): Promise => { + return this.client.fetchResource( + '/assets{?q,filter,c,n,s,f,bucket,select,variants,preferredLocales,snippetSize,hl.fl,hl.pre,hl.post,hl.max,localeGroups.collapse,localeGroups.preferredLocales,localeGroups.limit,sort}', + // + { query: options }, + AssetsPage + ); + }, + + /** + * Delete an asset resource by id. + * @param id asset id, previously generated on creation + */ + delete: (id: string): Promise => this.client.deleteResource(`/assets/${id}`, {}), + + /** + * Deletes an asset resources by id. + * @param id asset id, previously generated on creation + */ + deleteMany: (ids: string[]): Promise => + this.client.genericRequest('/assets', HttpMethod.DELETE, ids, {}, StringList), + + /** + * Retrieve a specific version of an asset resource by id. + * @param id asset id, previously generated on creation + * @param version asset version to request + */ + version: async (id: string, version: number): Promise => { + return this.assetsListToSingle( + await this.client.fetchResource(`/assets/${id}/versions/${version}`, {}, AssetsList), + id + ); + }, + + /** + * Retrieve a list of versions for a specific asset by id. + * @param options Pagination options + */ + versions: (id: string): Promise => this.client.fetchResource(`/assets/${id}/versions`, {}, AssetsList), + + /** + * Retrieves a download URL for an asset resource by id + * @param id asset id, previously generated on creation + */ + download: (id: string): Promise => this.client.fetchRawResource(`assets/${id}/download`, {}), + + /** + * Retrieves a download URL for an asset resource by id + * @param id asset id, previously generated on creation + * @param version the version of the asset to retrieve + */ + downloadVersion: (id: string, version: number): Promise => + this.client.fetchRawResource(`assets/${id}/versions/${version}/download`, {}), + + /** + * Retrieves all metadata for an asset. + * @param id asset id, previously generated on creation + */ + metadata: (id: string): Promise => + this.client.fetchResource(`assets/${id}/metadata`, {}, AssetMetadata), + + /** + * Publish a list of assets by id. + * @param ids a list of asset ids to publish + * @param mode the publish mode to use (default: UI) + */ + publish: (ids: string[], mode?: string): Promise => { + const header = { + 'X-Amp-Mode': mode || 'UI' + }; + + const body = { + assets: ids + }; + + return this.client.genericRequest('/assets/publish', HttpMethod.POST, body, { header }, StringList); + }, + + /** + * Publish a list of assets by id. + * @param ids a list of asset ids to publish + * @param mode the publish mode to use (default: UI) + */ + validatePublish: (ids: string[], mode?: string): Promise => { + const header = { + 'X-Amp-Mode': mode || 'UI' + }; + + const body = { + assets: ids + }; + + return this.client.genericRequest( + '/assets/publish/validate', + HttpMethod.POST, + body, + { header }, + StringList + ); + }, + + /** + * Unpublish a list of assets by id. + * @param ids a list of asset ids to publish + * @param mode the publish mode to use (default: UI) + */ + unpublish: (ids: string[], mode?: string): Promise => { + const header = { + 'X-Amp-Mode': mode || 'UI' + }; + + const body = { + assets: ids + }; + + return this.client.genericRequest('/assets/unpublish', HttpMethod.POST, body, { header }, StringList); + }, + + /** + * Retrieves text content for an asset. + * @param id asset id, previously generated on creation + */ + text: async (id: string): Promise => { + const list = await this.client.genericRequest( + `assets/text`, + HttpMethod.POST, + { assetIds: [id] }, + {}, + AssetTextList + ); + + return list[0]; + } + }; + + /** + * DAM Settings + */ + public readonly settings = { + /** + * Retrieve settings for the DAM account. + */ + get: (): Promise => this.client.fetchResource(`/settings`, {}, Settings) + }; +} diff --git a/src/common/ch-api/api/services/CURIEs.spec.ts b/src/common/ch-api/api/services/CURIEs.spec.ts new file mode 100644 index 00000000..d0416c05 --- /dev/null +++ b/src/common/ch-api/api/services/CURIEs.spec.ts @@ -0,0 +1,44 @@ +// tslint:disable:no-expression-statement +import { CURIEs } from './CURIEs'; + +describe('AxiosHttpClient tests', () => { + test('should ignore query parameters that are not provided', () => { + const href = CURIEs.expand('/resource{?page}', {}); + expect(href).toEqual('/resource'); + }); + + test('should replace provided query parameters', () => { + const href = CURIEs.expand('/resource{?page}', { page: 1 }); + expect(href).toEqual('/resource?page=1'); + }); + + test('should replace multiple provided query parameters', () => { + const href = CURIEs.expand('/resource{?page,size}', { page: 1, size: 10 }); + expect(href).toEqual('/resource?page=1&size=10'); + }); + + test('should only include provided query parameters', () => { + const href = CURIEs.expand('/resource{?page,size}', { page: 1 }); + expect(href).toEqual('/resource?page=1'); + }); + + test('should encode query string parameters', () => { + const href = CURIEs.expand('/resource{?page,size}', { page: '=' }); + expect(href).toEqual('/resource?page=%3D'); + }); + + test('should replace path parameters', () => { + const href = CURIEs.expand('/resource/{id}', { id: 1 }); + expect(href).toEqual('/resource/1'); + }); + + test('should replace with empty value if required path parameters missing', () => { + const href = CURIEs.expand('/resource/{id}', {}); + expect(href).toEqual('/resource/'); + }); + + test('should default parameters', () => { + const href = CURIEs.expand('/resource'); + expect(href).toEqual('/resource'); + }); +}); diff --git a/src/common/ch-api/api/services/CURIEs.ts b/src/common/ch-api/api/services/CURIEs.ts new file mode 100644 index 00000000..d620e273 --- /dev/null +++ b/src/common/ch-api/api/services/CURIEs.ts @@ -0,0 +1,15 @@ +/** + * @hidden + */ +import template from 'url-template'; + +/** + * @hidden + */ +export class CURIEs { + public static expand(href: string, variables?: unknown): string { + variables = variables || {}; + const compiledTemplate = template.parse(href); + return compiledTemplate.expand(variables); + } +} diff --git a/src/common/ch-api/http/AxiosHttpClient.spec.ts b/src/common/ch-api/http/AxiosHttpClient.spec.ts new file mode 100644 index 00000000..cf862021 --- /dev/null +++ b/src/common/ch-api/http/AxiosHttpClient.spec.ts @@ -0,0 +1,107 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import { AxiosHttpClient } from './AxiosHttpClient'; +import { HttpMethod } from './HttpRequest'; + +// axios-mock-adaptor's typedefs are wrong preventing calling onGet with 3 args, this is a workaround +/** + * @hidden + */ +import MockAdapter from 'axios-mock-adapter'; + +describe('AxiosHttpClient tests', () => { + test('client should use provided base url', async () => { + const client = new AxiosHttpClient({ + baseURL: 'http://mywebsite.com' + }); + + const mock = new MockAdapter(client.client); + mock.onGet('http://mywebsite.com/ping').reply(200, 'pong'); + + const response = await client.request({ + method: HttpMethod.GET, + url: 'http://mywebsite.com/ping' + }); + + expect(response.data).toEqual('pong'); + }); + + test('client should return status code', async () => { + const client = new AxiosHttpClient({}); + + const mock = new MockAdapter(client.client); + mock.onGet('/ping').reply(404); + + const response = await client.request({ + method: HttpMethod.GET, + url: '/ping' + }); + + expect(response.status).toEqual(404); + }); + + test('client should use provided method', async () => { + const client = new AxiosHttpClient({}); + + const mock = new MockAdapter(client.client); + mock.onDelete('/resource').reply(200); + + const response = await client.request({ + method: HttpMethod.DELETE, + url: '/resource' + }); + + expect(response.status).toEqual(200); + }); + + test('client should send form data', async () => { + const client = new AxiosHttpClient({}); + + const mock = new MockAdapter(client.client); + mock + .onPost('/oauth/token', 'grant_type=client_credentials&client_id=client_id&client_secret=client_secret') + .reply(200, { + access_token: 'token', + expires_in: 0, + refresh_token: 'refresh' + }); + + const response = await client.request({ + data: 'grant_type=client_credentials&client_id=client_id&client_secret=client_secret', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + method: HttpMethod.POST, + url: '/oauth/token' + }); + + expect(response.status).toEqual(200); + }); + + test('client should send JSON data', async () => { + const client = new AxiosHttpClient({}); + + const mock = new MockAdapter(client.client); + mock + .onPost('/resource/create', { + key: 'value' + }) + .reply(200, { + access_token: 'token', + expires_in: 0, + refresh_token: 'refresh' + }); + + const response = await client.request({ + data: { + key: 'value' + }, + headers: { + 'Content-Type': 'application/json' + }, + method: HttpMethod.POST, + url: '/resource/create' + }); + + expect(response.status).toEqual(200); + }); +}); diff --git a/src/common/ch-api/http/AxiosHttpClient.ts b/src/common/ch-api/http/AxiosHttpClient.ts new file mode 100644 index 00000000..303a616f --- /dev/null +++ b/src/common/ch-api/http/AxiosHttpClient.ts @@ -0,0 +1,41 @@ +import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; + +import { HttpClient } from './HttpClient'; +import { HttpRequest } from './HttpRequest'; +import { HttpResponse } from './HttpResponse'; + +/** + * @hidden + */ +export class AxiosHttpClient implements HttpClient { + public client: AxiosInstance; + + constructor(private config: AxiosRequestConfig) { + this.client = axios.create(config); + } + + public request(config: HttpRequest): Promise { + return this.client + .request({ + data: config.data, + headers: config.headers, + method: config.method, + url: config.url + }) + .then(response => { + return { + data: response.data, + status: response.status + }; + }) + .catch(error => { + if (error && error.response) { + return { + data: error.response.data, + status: error.response.status + }; + } + return error; + }); + } +} diff --git a/src/common/ch-api/http/HttpClient.ts b/src/common/ch-api/http/HttpClient.ts new file mode 100644 index 00000000..50174cf5 --- /dev/null +++ b/src/common/ch-api/http/HttpClient.ts @@ -0,0 +1,9 @@ +import { HttpRequest } from './HttpRequest'; +import { HttpResponse } from './HttpResponse'; + +/** + * @hidden + */ +export interface HttpClient { + request(config: HttpRequest): Promise; +} diff --git a/src/common/ch-api/http/HttpError.ts b/src/common/ch-api/http/HttpError.ts new file mode 100644 index 00000000..685c0565 --- /dev/null +++ b/src/common/ch-api/http/HttpError.ts @@ -0,0 +1,9 @@ +import { HttpRequest } from './HttpRequest'; +import { HttpResponse } from './HttpResponse'; + +export class HttpError extends Error { + constructor(message: string, public readonly request?: HttpRequest, public readonly response?: HttpResponse) { + super(message); + Object.setPrototypeOf(this, new.target.prototype); + } +} diff --git a/src/common/ch-api/http/HttpRequest.ts b/src/common/ch-api/http/HttpRequest.ts new file mode 100644 index 00000000..02970f00 --- /dev/null +++ b/src/common/ch-api/http/HttpRequest.ts @@ -0,0 +1,20 @@ +/** + * @hidden + */ +export enum HttpMethod { + GET = 'GET', + POST = 'POST', + PUT = 'PUT', + PATCH = 'PATCH', + DELETE = 'DELETE' +} + +/** + * @hidden + */ +export interface HttpRequest { + url: string; + method: HttpMethod; + data?: string | object; + headers?: { [key: string]: string }; +} diff --git a/src/common/ch-api/http/HttpResponse.ts b/src/common/ch-api/http/HttpResponse.ts new file mode 100644 index 00000000..adc1260e --- /dev/null +++ b/src/common/ch-api/http/HttpResponse.ts @@ -0,0 +1,7 @@ +/** + * @hidden + */ +export interface HttpResponse { + status: number; + data: string | object; +} diff --git a/src/common/ch-api/model/ActionPriority.ts b/src/common/ch-api/model/ActionPriority.ts new file mode 100644 index 00000000..0ebe2607 --- /dev/null +++ b/src/common/ch-api/model/ActionPriority.ts @@ -0,0 +1,6 @@ +/** + * Parameter for actions that have a priority level. + */ +export interface ActionPriority { + 'X-Amp-Mode'?: 'UI'; +} diff --git a/src/common/ch-api/model/Asset.spec.ts b/src/common/ch-api/model/Asset.spec.ts new file mode 100644 index 00000000..4edffabc --- /dev/null +++ b/src/common/ch-api/model/Asset.spec.ts @@ -0,0 +1,326 @@ +import { MockContentHub } from '../ContentHub.mocks'; +import { Asset } from './Asset'; +import { AssetPut } from './AssetPut'; +import { StringList } from './StringList'; + +describe('AxiosHttpClient tests', () => { + test('get asset by id', async () => { + const client = new MockContentHub(); + const result = await client.assets.get('65d78690-bf4e-415d-a16c-ca4dadbb2717'); + + expect(result.label).toEqual('AlltheLook1.jpg'); + }); + + test('delete asset (self)', async () => { + const client = new MockContentHub(); + + const result = await client.assets.get('65d78690-bf4e-415d-a16c-ca4dadbb2717'); + + await expect(result.related.delete()).resolves.toBe(undefined); + // Did not throw. + }); + + test('delete asset (by id)', async () => { + const client = new MockContentHub(); + + const del = client.assets.delete('65d78690-bf4e-415d-a16c-ca4dadbb2717'); + + await expect(del).resolves.toBe(undefined); + // Did not throw. + }); + + test('delete asset (by ids)', async () => { + const client = new MockContentHub(); + + const del = client.assets.deleteMany(['65d78690-bf4e-415d-a16c-ca4dadbb2717']); + + await expect(del).resolves.toEqual(expect.any(StringList)); + // Did not throw. + }); + + test('get versions (self)', async () => { + const client = new MockContentHub(); + + const result = await client.assets.get('65d78690-bf4e-415d-a16c-ca4dadbb2717'); + + const versions = await result.related.versions(); + + let versionId = 5; + versions.getItems().forEach(version => { + expect(version.label).toEqual('AlltheLook1.jpg'); + expect(version.revisionNumber).toEqual(versionId--); + expect(version.related).not.toBeUndefined(); + }); + }); + + test('get versions (by id)', async () => { + const client = new MockContentHub(); + + const versions = await client.assets.versions('65d78690-bf4e-415d-a16c-ca4dadbb2717'); + + let versionId = 5; + versions.getItems().forEach(version => { + expect(version.label).toEqual('AlltheLook1.jpg'); + expect(version.revisionNumber).toEqual(versionId--); + expect(version.related).not.toBeUndefined(); + }); + }); + + test('get version (self)', async () => { + const client = new MockContentHub(); + + const result = await client.assets.get('65d78690-bf4e-415d-a16c-ca4dadbb2717'); + + const version1 = await result.related.version(1); + + expect(version1.revisionNumber).toEqual(1); + }); + + test('get version (by id)', async () => { + const client = new MockContentHub(); + + const result = await client.assets.version('65d78690-bf4e-415d-a16c-ca4dadbb2717', 1); + + expect(result.label).toEqual('AlltheLook1.jpg'); + expect(result.revisionNumber).toEqual(1); + }); + + test('get download (self)', async () => { + const client = new MockContentHub(); + + const result = await client.assets.get('65d78690-bf4e-415d-a16c-ca4dadbb2717'); + + const downloadPath = await result.related.download(); + + expect(downloadPath).toEqual('/assets/65d78690-bf4e-415d-a16c-ca4dadbb2717/download/handle?auth=example'); + }); + + test('get download (by id)', async () => { + const client = new MockContentHub(); + + const downloadPath = await client.assets.download('65d78690-bf4e-415d-a16c-ca4dadbb2717'); + + expect(downloadPath).toEqual('/assets/65d78690-bf4e-415d-a16c-ca4dadbb2717/download/handle?auth=example'); + }); + + test('get version download (self)', async () => { + const client = new MockContentHub(); + + const result = await client.assets.get('65d78690-bf4e-415d-a16c-ca4dadbb2717'); + + const downloadPath = await result.related.downloadVersion(1); + + expect(downloadPath).toEqual( + '/assets/65d78690-bf4e-415d-a16c-ca4dadbb2717/versions/1/download/handle?revisionNumber=1&auth=example' + ); + }); + + test('get version download (by id)', async () => { + const client = new MockContentHub(); + + const downloadPath = await client.assets.downloadVersion('65d78690-bf4e-415d-a16c-ca4dadbb2717', 1); + + expect(downloadPath).toEqual( + '/assets/65d78690-bf4e-415d-a16c-ca4dadbb2717/versions/1/download/handle?revisionNumber=1&auth=example' + ); + }); + + test('publish (self)', async () => { + const client = new MockContentHub(); + + const result = await client.assets.get('65d78690-bf4e-415d-a16c-ca4dadbb2717'); + + const publishJobs = await result.related.publish('UI'); + + expect(publishJobs.getItems()).toEqual(['publish.8d1bb161-4de3-4cc7-a907-29636842032a']); + }); + + test('publish (by ids)', async () => { + const client = new MockContentHub(); + + const publishJobs = await client.assets.publish(['65d78690-bf4e-415d-a16c-ca4dadbb2717']); + + expect(publishJobs.getItems()).toEqual(['publish.8d1bb161-4de3-4cc7-a907-29636842032a']); + }); + + test('validate publish (self)', async () => { + const client = new MockContentHub(); + + const result = await client.assets.get('65d78690-bf4e-415d-a16c-ca4dadbb2717'); + + const publishJobs = await result.related.validatePublish('UI'); + + // Returns a null list, but does not throw. + expect(publishJobs.getItems()).toBeUndefined(); + }); + + test('validate publish (by ids)', async () => { + const client = new MockContentHub(); + + const publishJobs = await client.assets.validatePublish(['65d78690-bf4e-415d-a16c-ca4dadbb2717']); + + // Returns a null list, but does not throw. + expect(publishJobs.getItems()).toBeUndefined(); + }); + + test('unpublish (self)', async () => { + const client = new MockContentHub(); + + const result = await client.assets.get('65d78690-bf4e-415d-a16c-ca4dadbb2717'); + + const publishJobs = await result.related.unpublish('UI'); + + expect(publishJobs.getItems()).toEqual(['unpublish.8f0034fd-e0e7-4a55-a81b-a87054827a8f']); + }); + + test('unpublish (by ids)', async () => { + const client = new MockContentHub(); + + const publishJobs = await client.assets.unpublish(['65d78690-bf4e-415d-a16c-ca4dadbb2717']); + + expect(publishJobs.getItems()).toEqual(['unpublish.8f0034fd-e0e7-4a55-a81b-a87054827a8f']); + }); + + test('text (self)', async () => { + const client = new MockContentHub(); + + const result = await client.assets.get('65d78690-bf4e-415d-a16c-ca4dadbb2717'); + + const text = await result.related.text(); + + expect(text.id).toEqual('65d78690-bf4e-415d-a16c-ca4dadbb2717'); + expect(text.status).toEqual('INTERNAL'); + expect(text.data).toEqual('Text Content Example'); + }); + + test('text (by id)', async () => { + const client = new MockContentHub(); + + const text = await client.assets.text('65d78690-bf4e-415d-a16c-ca4dadbb2717'); + + expect(text.id).toEqual('65d78690-bf4e-415d-a16c-ca4dadbb2717'); + expect(text.status).toEqual('INTERNAL'); + expect(text.data).toEqual('Text Content Example'); + }); + + test('get metadata (self)', async () => { + const client = new MockContentHub(); + + const result = await client.assets.get('65d78690-bf4e-415d-a16c-ca4dadbb2717'); + + const metadata = await result.related.metadata(); + + expect(metadata.metadata.map(meta => meta.schema)).toEqual(['exif', 'file', 'image']); + }); + + test('get metadata (by id)', async () => { + const client = new MockContentHub(); + + const metadata = await client.assets.metadata('65d78690-bf4e-415d-a16c-ca4dadbb2717'); + + expect(metadata.metadata.map(meta => meta.schema)).toEqual(['exif', 'file', 'image']); + }); + + test('list assets', async () => { + const client = new MockContentHub(); + + const list = await client.assets.list({ q: 'example search query' }); + const items = list.getItems(); + + expect(items.map(item => item.label)).toEqual(['AlltheLook1.jpg', '1.png']); + + items.forEach(version => { + expect(version.related).not.toBeUndefined(); + }); + }); + + const assetStrip = ['revisionNum', 'userId', 'file', 'createdDate', 'timestamp', 'tags']; + + function sharedPut(client: MockContentHub, requestIds: string[]): void { + for (let i = 0; i < requestIds.length; i++) { + const request = client.mock.history.put[i]; + + expect(request.url).toEqual('https://dam-api.amplience.net/v1.5.0/assets'); + + // Must strip reserved fields from body. + + const data = JSON.parse(request.data); + expect(data.assets[0].id).toEqual(requestIds[i]); + data.assets.forEach((asset: Asset) => { + expect(asset.label).toEqual('Replacement label'); + + assetStrip.forEach(stripped => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((asset as any)[stripped]).toBeUndefined(); + }); + }); + } + } + + test('put assets (by ids)', async () => { + const client = new MockContentHub(); + + const list = await client.assets.list(); + const items = list.getItems(); + + items.forEach(item => (item.label = 'Replacement label')); + + const result = await client.assets.putAsset('overwrite', items); + + expect(result.results).toEqual(items.map(item => ({ id: item.id, status: 'succeeded' }))); + + sharedPut(client, ['65d78690-bf4e-415d-a16c-ca4dadbb2717']); + }); + + test('put assets (self)', async () => { + const client = new MockContentHub(); + + const list = await client.assets.list(); + const items = list.getItems(); + + items.forEach(item => (item.label = 'Replacement label')); + + const result = []; + + for (const item of items) { + item.label = 'Replacement label'; + result.push(await item.related.update()); + } + + sharedPut(client, ['65d78690-bf4e-415d-a16c-ca4dadbb2717', '4a0bcaed-ee57-481f-98d3-4b73aad49e68']); + }); + + test('put assets (using asset put interface)', async () => { + const client = new MockContentHub(); + + const assetPut: AssetPut = { + id: '65d78690-bf4e-415d-a16c-ca4dadbb2717', + tags: { add: [], remove: [] } + }; + + const result = await client.assets.put('overwrite', [assetPut]); + + expect(result.results[0]).toEqual({ + id: '65d78690-bf4e-415d-a16c-ca4dadbb2717', + status: 'succeeded' + }); + + const request = client.mock.history.put[0]; + + expect(request.url).toEqual('https://dam-api.amplience.net/v1.5.0/assets'); + + const data = JSON.parse(request.data); + expect(data.assets[0]).toEqual(assetPut); + }); + + test('get asset by id (failures)', async () => { + const client = new MockContentHub(); + const fail404 = client.assets.get('fail404'); + + await expect(fail404).rejects.toThrow(); + + const badId = client.assets.get('badId'); + + await expect(badId).rejects.toThrow(); + }); +}); diff --git a/src/common/ch-api/model/Asset.ts b/src/common/ch-api/model/Asset.ts new file mode 100644 index 00000000..1b8961dc --- /dev/null +++ b/src/common/ch-api/model/Asset.ts @@ -0,0 +1,232 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { ApiResource } from '../api/model/ApiResource'; +import { PublishActivitySummary } from './PublishActivitySummary'; +import { Page } from './Page'; +import { ResourceList } from './ResourceList'; +import { AssetMetadata, AssetRelationships } from './AssetMetadata'; +import { StringList } from './StringList'; +import { AssetText } from './AssetText'; +import { WorkflowSummary } from './WorkflowSummary'; +import { AssetPutResult } from './AssetPutResult'; + +export class Asset extends ApiResource { + /** + * UUID identifier for this asset. + */ + id: string; + + /** + * The original filename of the source file for this asset. + */ + srcName?: string; + + /** + * The latest revision number for this asset. Increments with each update. + */ + revisionNumber?: number; + + /** + * UUID of the bucket this asset is contained within. + */ + bucketID: string; + + /** + * Friendly label for the asset. + */ + label?: string; + + /** + * MIME type for this asset. + */ + mimeType: string; + + /** + * High level type for this asset, e.g. 'image'. + */ + type?: string; + + /** + * Locale name for this asset. + */ + locale: string; + + /** + * User UUID of the creator of this asset. + */ + userID?: string; + + /** + * Thumbnail file for this asset, if applicable. + */ + thumbFile?: string; + + /** + * The id of the containing folder for this asset. + * Value is an empty GUID if the asset is not contained in a folder. + */ + folderID?: string; + + /** + * UUID of the file backing this asset. + */ + file?: string; + + /** + * Created timestamp for this asset. + */ + createdDate: number; + + /** + * Filesize of the asset in bytes. Only available when 'file' is selected. + */ + size: number; + + /** + * Unique name for the asset. This is unique across the account so it can be flattenned for the public URL. + */ + name?: string; + + /** + * Sub type for this asset. + */ + subType?: any; + + /** + * Thumbnail URL for this asset, if applicable. + */ + thumbUrl?: string; + + /** + * Asset publish information. Only available when 'publish' is selected. + */ + publish?: PublishActivitySummary; + + /** + * Asset unpublish information. Only available when 'publish' is selected. + */ + unpublish?: PublishActivitySummary; + + /** + * Workflow summary for this asset. Only available when 'workflow' is selected. + */ + workflow?: WorkflowSummary; + + /** + * Tags for this asset. Only available when 'tags' is selected. + */ + tags?: string[]; + + /** + * Metadata for this asset. Only available when 'meta:' is selected. + */ + relationships?: AssetRelationships; + + /** + * The current publish status of this asset. + */ + publishStatus?: 'NOT_PUBLISHED' | 'PUBLISH_IN_PROGRESS' | 'UNPUBLISH_IN_PROGRESS' | 'PUBLISHED'; + + /** + * Last modified timestamp for this asset. + */ + timestamp?: string; + + /** + * The status of this asset. + */ + status?: 'deleted' | 'active'; + + /** + * UUID of the locale of the asset + */ + localeID?: string; + + /** + * Locale group that the asset belongs to. + */ + localeGroup?: string; + + /** + * Resources and actions related to a Content Item + */ + public readonly related = { + /** + * Delete this asset. + */ + delete: (): Promise => this.client.endpoints.assets.delete(this.id), + + /** + * Retrieves all versions for this asset. + */ + versions: (): Promise => this.client.endpoints.assets.versions(this.id), + + /** + * Retrieves a specific version for this asset. + */ + version: (version: number): Promise => this.client.endpoints.assets.version(this.id, version), + + /** + * Retrieves all metadata for this asset. + */ + metadata: (): Promise => this.client.endpoints.assets.metadata(this.id), + + /** + * Retrieves a download URL for this asset. + */ + download: (): Promise => this.client.endpoints.assets.download(this.id), + + /** + * Retrieves a download URL for this asset, with the given version. + */ + downloadVersion: (version: number): Promise => + this.client.endpoints.assets.downloadVersion(this.id, version), + + /** + * Retrieves a download URL for this asset, with the given version. + */ + text: (): Promise => this.client.endpoints.assets.text(this.id), + + /** + * Publishes only this asset. + */ + publish: (mode?: string): Promise => this.client.endpoints.assets.publish([this.id], mode), + + /** + * Validates publish for only this asset. Throws if not successful. + */ + validatePublish: (mode?: string): Promise => + this.client.endpoints.assets.validatePublish([this.id], mode), + + /** + * Unpublishes only this asset. + */ + unpublish: (mode?: string): Promise => this.client.endpoints.assets.unpublish([this.id], mode), + + /** + * Updates only this asset. + */ + update: async (): Promise => { + const results = await this.client.endpoints.assets.putAsset('overwrite', [this]); + + return results.results[0]; + } + }; +} + +/** + * @hidden + */ +export class AssetsList extends ResourceList { + constructor(data?: any) { + super(Asset, data); + } +} + +/** + * @hidden + */ +export class AssetsPage extends Page { + constructor(data?: any) { + super(Asset, data); + } +} diff --git a/src/common/ch-api/model/AssetListRequest.ts b/src/common/ch-api/model/AssetListRequest.ts new file mode 100644 index 00000000..2c57d6a2 --- /dev/null +++ b/src/common/ch-api/model/AssetListRequest.ts @@ -0,0 +1,92 @@ +import { AssetRequest } from './AssetRequest'; +import { Pageable } from './Pageable'; + +/** + * Parameters for asset list requests + */ +export interface AssetListRequest extends Pageable, AssetRequest { + /** + * Search terms. The search fields are Label, Name, and srcName. + */ + q?: string; + + /** + * A query to use to restrict the set of assets that will be searched over and returned + */ + filter?: string; + + /** + * Comma-separated list of fields to return for each asset. + */ + c?: string; + + /** + * The id of a folder to search in. + */ + f?: string; + + /** + * The id of a bucket to search in. + */ + bucket?: string; + + /** + * Comma seperated list locales in order of preference from left to right. + */ + preferredLocales?: string; + + /** + * The maximum length of the text snippet. (default is 200 characters) + */ + snippetSize?: string; + + /** + * Highlight parameters. + */ + highlight?: { + /** + * Field to highlight on (All Fields available as default (*)) + */ + fl?: string; + + /** + * Pre tag to wrap highlighted text (default is ) + */ + pre?: string; + + /** + * Post tag to wrap highlighted text (default is ) + */ + post?: string; + + /** + * The maximum number of highlighted snippets to generate per field. (default is 1) + */ + max?: number; + }; + + /** + * Locale group parameters. + */ + localeGroups?: { + /** + * Whether or not to retreat assets in a locale group as a single asset or as individual assets. (default is false) + */ + collapse?: boolean; + + /** + * Order of preference for the locale of the asset which will be returned for each group of assets + */ + preferredLocales?: string; + + /** + * The maximum number of siblings to return for a single asset. (default is 20) + */ + limit?: number; + }; + + /** + * field(s) on which the sorting of the results is based off ( default is on the update field) + */ + sort?: string; +} diff --git a/src/common/ch-api/model/AssetMetadata.ts b/src/common/ch-api/model/AssetMetadata.ts new file mode 100644 index 00000000..d446b622 --- /dev/null +++ b/src/common/ch-api/model/AssetMetadata.ts @@ -0,0 +1,57 @@ +import { ApiResource } from '../api/model/ApiResource'; + +export interface AssetMetadataVariant { + /** + * The name of this metadata variant. + */ + variantName: string; + + /** + * The unique identifier of this metadata variant. + */ + variantID: string; + + /** + * Values contained in this metadata variant. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + values: { [K: string]: any }; +} + +export interface AssetMetadataItem { + /** + * Schema name for this type of metadata. + */ + schema: string; + + /** + * Schema ID for this type of metadata. + */ + schemaID: string; + + /** + * Variants of this metadata that are present on the parent asset. + */ + variants: AssetMetadataVariant[]; + + /** + * Primary key identifying the parent asset. + */ + PK: { id: string }; +} + +export class AssetMetadata extends ApiResource { + /** + * The metadata attached to the asset. + */ + metadata: AssetMetadataItem[]; + + /** + * The ID of the related asset. + */ + assetId: string; +} + +export interface AssetRelationships { + [key: string]: AssetMetadataItem; +} diff --git a/src/common/ch-api/model/AssetPut.ts b/src/common/ch-api/model/AssetPut.ts new file mode 100644 index 00000000..3b79c575 --- /dev/null +++ b/src/common/ch-api/model/AssetPut.ts @@ -0,0 +1,90 @@ +import { PublishActivitySummary } from './PublishActivitySummary'; +import { TagsPut } from './TagsPut'; +import { WorkflowSummary } from './WorkflowSummary'; + +export interface AssetPut { + /** + * Path to the input file. E.g. http://url... s3://.... multipart://... + */ + src?: string; + + /** + * UUID identifier for the asset + */ + id?: string; + + /** + * ['image' or 'video' or 'set' or 'spin' or 'document' or 'other']: Sets the type of media to create + */ + type?: string; + + /** + * Unique name for the asset, this is unique across the account so we can flatten the it on the public URL + */ + name?: string; + + /** + * Friendly label for the asset, this is what a customer will see. If not specified this will default to srcName. + */ + label?: string; + + /** + * Name of the asset to use as a thumbnail. This should be a file ID not an asset ID + */ + thumbFile?: string; + + /** + * Original filename + */ + srcName?: string; + + /** + * ['active' or 'deleted' or 'expired']: Lifecycle Status, defaults to Active + */ + status?: string; + + /** + * UUID of the Folder this asset is contained within. Empty uuid means it is not in a specific folder + */ + folderID?: string; + + /** + * Add or Remove Tags when loading asset (Remove tags is for when you update an asset, no point when creating one) + */ + tags?: TagsPut; + + /** + * The publish status for the asset. One of: NOT_PUBLISHED, PUBLISH_IN_PROGRESS, UNPUBLISH_IN_PROGRESS, PUBLISHED + */ + publishStatus?: 'NOT_PUBLISHED' | 'PUBLISH_IN_PROGRESS' | 'UNPUBLISH_IN_PROGRESS' | 'PUBLISHED'; + + /** + * Information regarding publish activities. + */ + publish?: PublishActivitySummary; + + /** + * Information regarding unpublish activities. + */ + unpublish?: PublishActivitySummary; + + /** + * Information regarding assets workflow. + */ + workflow?: WorkflowSummary; + + /** + * If Asset type is a set (media\spin\2d...) then this will be the list of assetIds. + */ + contents?: string[]; + + /** + * UUID of the locale of the asset. + */ + localeID?: string; + + /** + * Name of the localeGroup the asset belongs to. Defaults to asset name. + */ + localeGroup?: string; +} diff --git a/src/common/ch-api/model/AssetPutResult.ts b/src/common/ch-api/model/AssetPutResult.ts new file mode 100644 index 00000000..97fe6a67 --- /dev/null +++ b/src/common/ch-api/model/AssetPutResult.ts @@ -0,0 +1,15 @@ +import { ApiResource } from '../api/model/ApiResource'; + +export interface AssetPutResult { + id: string; + status: string; +} + +export class AssetPutResultList extends ApiResource { + results: AssetPutResult[]; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public parse(data: any): void { + this.results = data; + } +} diff --git a/src/common/ch-api/model/AssetRequest.ts b/src/common/ch-api/model/AssetRequest.ts new file mode 100644 index 00000000..a9fd353d --- /dev/null +++ b/src/common/ch-api/model/AssetRequest.ts @@ -0,0 +1,14 @@ +/** + * Parameters for asset requests + */ +export interface AssetRequest { + /** + * Comma separated list to select subsections to include, e.g. tags,meta:{schema},meta:* + */ + select?: string; + + /** + * Comma seperated list of metadata variants to include in the result. + */ + variants?: string; +} diff --git a/src/common/ch-api/model/AssetText.ts b/src/common/ch-api/model/AssetText.ts new file mode 100644 index 00000000..5f58467b --- /dev/null +++ b/src/common/ch-api/model/AssetText.ts @@ -0,0 +1,33 @@ +import { ApiResource } from '../api/model/ApiResource'; + +/** + * Retrieved text content for an asset, or a link to externally hosted content. + */ +export class AssetText extends ApiResource { + /** + * UUID identifier for the asset + */ + id?: string; + + /** + * Status of the data. + * 'INTERNAL' (text data in the data field) + * 'EXTERNAL' (URL in the data field pointing to the text file) + * 'ERROR' (Error Message in the Info Field) + */ + status?: 'INTERNAL' | 'EXTERNAL' | 'ERROR'; + + /** + * Data for the requested asset. + */ + data?: string; + + /** + * Information regarding the given status. + */ + info?: string; +} + +export class AssetTextList extends ApiResource { + [index: number]: AssetText; +} diff --git a/src/common/ch-api/model/Page.ts b/src/common/ch-api/model/Page.ts new file mode 100644 index 00000000..6b9b66f9 --- /dev/null +++ b/src/common/ch-api/model/Page.ts @@ -0,0 +1,19 @@ +import { ApiResource } from '../api/model/ApiResource'; +import { ResourceList } from './ResourceList'; + +export class Page extends ResourceList { + /** + * The total number of resources found. + */ + numFound: number; + + /** + * The starting index of this page. + */ + start: number; + + /** + * The number of resources displayed on each page. + */ + pageSize: number; +} diff --git a/src/common/ch-api/model/Pageable.ts b/src/common/ch-api/model/Pageable.ts new file mode 100644 index 00000000..43d9281e --- /dev/null +++ b/src/common/ch-api/model/Pageable.ts @@ -0,0 +1,14 @@ +/** + * Parameters for paginated requests + */ +export interface Pageable { + /** + * Position of the first asset to return (1 based) + */ + s?: number; + + /** + * Maximum resources to return + */ + n?: number; +} diff --git a/src/common/ch-api/model/PublishActivitySummary.ts b/src/common/ch-api/model/PublishActivitySummary.ts new file mode 100644 index 00000000..b016fcc3 --- /dev/null +++ b/src/common/ch-api/model/PublishActivitySummary.ts @@ -0,0 +1,13 @@ +import { PublishInfoPut } from './PublishInfoPut'; + +export interface PublishActivitySummary { + /** + * Info on the last publish job, if present. + */ + last?: PublishInfoPut; + + /** + * Info on the last successful publish job, if present. + */ + lastsuccess?: PublishInfoPut; +} diff --git a/src/common/ch-api/model/PublishInfoPut.ts b/src/common/ch-api/model/PublishInfoPut.ts new file mode 100644 index 00000000..22cced05 --- /dev/null +++ b/src/common/ch-api/model/PublishInfoPut.ts @@ -0,0 +1,21 @@ +export interface PublishInfoPut { + /** + * The publish job ID + */ + jobID: string; + + /** + * The revision number of the published asset. + */ + revisionNumber: number; + + /** + * The timestamp of when the publish job status was last updated. + */ + timestamp: number; + + /** + * The status of this publish job. + */ + status: string; +} diff --git a/src/common/ch-api/model/ResourceList.spec.ts b/src/common/ch-api/model/ResourceList.spec.ts new file mode 100644 index 00000000..3ec5fded --- /dev/null +++ b/src/common/ch-api/model/ResourceList.spec.ts @@ -0,0 +1,22 @@ +import { ResourceList } from './ResourceList'; +import { Asset } from './Asset'; +import { MockContentHub } from '../ContentHub.mocks'; + +describe('ResourceList tests', () => { + test('creation and to JSON', async () => { + const list = { + data: [{ id: 'example1' }, { id: 'example2' }], + count: 2 + }; + + const resList = new ResourceList(Asset, list); + resList.setClient(new MockContentHub().mockClient); + + expect(resList.getItems().map(asset => asset.toJSON())).toEqual(list.data); + + // When converting to a list and back, we should get the same data. + const json = resList.toJSON(); + + expect(json).toEqual(list); + }); +}); diff --git a/src/common/ch-api/model/ResourceList.ts b/src/common/ch-api/model/ResourceList.ts new file mode 100644 index 00000000..2832f6cc --- /dev/null +++ b/src/common/ch-api/model/ResourceList.ts @@ -0,0 +1,30 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { ApiResource, ApiResourceConstructor } from '../api/model/ApiResource'; + +export class ResourceList extends ApiResource { + private resourceType: ApiResourceConstructor; + private items: T[]; + + private data: any[]; + private count: number; + + constructor(resourceType: ApiResourceConstructor, data?: any) { + super(data); + this.resourceType = resourceType; + } + + public getItems(): T[] { + if (!this.items) { + this.items = this.data.map(x => this.client.parse(x, this.resourceType)); + } + return this.items; + } + + public toJSON(): any { + const result = super.toJSON(); + result.data = this.getItems().map(item => item.toJSON()); + delete result.resourceType; + delete result.items; + return result; + } +} diff --git a/src/common/ch-api/model/Settings.spec.ts b/src/common/ch-api/model/Settings.spec.ts new file mode 100644 index 00000000..fcfc5010 --- /dev/null +++ b/src/common/ch-api/model/Settings.spec.ts @@ -0,0 +1,11 @@ +import { MockContentHub } from '../ContentHub.mocks'; + +describe('Settings tests', () => { + test('get settings', async () => { + const client = new MockContentHub(); + const result = await client.settings.get(); + + expect(result.companyClassificationType).toEqual('Demo'); + expect(result.di.defaultEndpoint).toEqual('aaaaaaaa-bbbb-cccc-dddd-eeeeeeffffff'); + }); +}); diff --git a/src/common/ch-api/model/Settings.ts b/src/common/ch-api/model/Settings.ts new file mode 100644 index 00000000..f62b90ae --- /dev/null +++ b/src/common/ch-api/model/Settings.ts @@ -0,0 +1,45 @@ +import { ApiResource } from '../api/model/ApiResource'; + +export interface SystemSettings { + 'auth-api-url': string; + 'publishing-service': string; + 'im-api-url': string; + 'virtual-staging-service': string; + 'dam-mixpanel-token': string; + 'analytics-beacon-url': string; + 'analytics-query-api-url': string; + 'dam-version': string; + 'stream-api-url': string; + 'ca-mixpanel-token': string; + 'access-service': string; + 'cms-api-url': string; + 'provisioning-api': string; + 'identity-service': string; + 'media-graphics-host': string; +} + +export interface DiSettingsEndpoint { + path: string; + staticHost: string; + textProtocols: string[]; + schemaIds: string[]; + dynamicHost: string; + id: string; + staticProtocols: string[]; + serviceId: string; + textHost: string; + dynamicProtocols: string[]; +} + +export interface DiSettings { + endpoints: DiSettingsEndpoint[]; + defaultEndpoint: string; + enabled: boolean; +} + +export class Settings extends ApiResource { + companyId: string; + companyClassificationType: string; + system: SystemSettings; + di: DiSettings; +} diff --git a/src/common/ch-api/model/StringList.ts b/src/common/ch-api/model/StringList.ts new file mode 100644 index 00000000..ec7bda0d --- /dev/null +++ b/src/common/ch-api/model/StringList.ts @@ -0,0 +1,11 @@ +import { ApiResource } from '../api/model/ApiResource'; + +export class StringList extends ApiResource { + private data?: string[]; + private content?: string[]; + private count: number; + + public getItems(): string[] { + return (this.data || this.content) as string[]; + } +} diff --git a/src/common/ch-api/model/TagsPut.ts b/src/common/ch-api/model/TagsPut.ts new file mode 100644 index 00000000..18ce52d1 --- /dev/null +++ b/src/common/ch-api/model/TagsPut.ts @@ -0,0 +1,11 @@ +export interface TagsPut { + /** + * Tags to remove from an asset. + */ + remove?: string[]; + + /** + * Tags to add to an asset. + */ + add?: string[]; +} diff --git a/src/common/ch-api/model/WorkflowSummary.ts b/src/common/ch-api/model/WorkflowSummary.ts new file mode 100644 index 00000000..e0c1e2df --- /dev/null +++ b/src/common/ch-api/model/WorkflowSummary.ts @@ -0,0 +1,4 @@ +export interface WorkflowSummary { + status?: string; + assignedTo?: string; +} diff --git a/src/common/ch-api/oauth2/models/AccessToken.ts b/src/common/ch-api/oauth2/models/AccessToken.ts new file mode 100644 index 00000000..ece94c76 --- /dev/null +++ b/src/common/ch-api/oauth2/models/AccessToken.ts @@ -0,0 +1,19 @@ +/** + * OAuth2 Access Token + */ +export interface AccessToken { + /** + * Token attached to requests to assert permissions + */ + access_token: string; + + /** + * Token used to refresh the access token after it has expired + */ + refresh_token: string; + + /** + * Period (in seconds) for how long the access token will remain valid + */ + expires_in: number; +} diff --git a/src/common/ch-api/oauth2/models/AccessTokenProvider.ts b/src/common/ch-api/oauth2/models/AccessTokenProvider.ts new file mode 100644 index 00000000..29f88b9f --- /dev/null +++ b/src/common/ch-api/oauth2/models/AccessTokenProvider.ts @@ -0,0 +1,5 @@ +import { AccessToken } from './AccessToken'; + +export interface AccessTokenProvider { + getToken(): Promise; +} diff --git a/src/common/ch-api/oauth2/models/OAuth2ClientCredentials.ts b/src/common/ch-api/oauth2/models/OAuth2ClientCredentials.ts new file mode 100644 index 00000000..23ad6652 --- /dev/null +++ b/src/common/ch-api/oauth2/models/OAuth2ClientCredentials.ts @@ -0,0 +1,14 @@ +/** + * OAuth2 Client Credentials + */ +export interface OAuth2ClientCredentials { + /** + * Client id + */ + client_id: string; + + /** + * Client secret + */ + client_secret: string; +} diff --git a/src/common/ch-api/oauth2/services/OAuth2Client.spec.ts b/src/common/ch-api/oauth2/services/OAuth2Client.spec.ts new file mode 100644 index 00000000..b92b5300 --- /dev/null +++ b/src/common/ch-api/oauth2/services/OAuth2Client.spec.ts @@ -0,0 +1,152 @@ +/* eslint-disable @typescript-eslint/camelcase */ +import { AxiosHttpClient } from '../../http/AxiosHttpClient'; +import { OAuth2Client } from './OAuth2Client'; + +// axios-mock-adaptor's typedefs are wrong preventing calling onGet with 3 args, this is a workaround +/** + * @hidden + */ +import MockAdapter from 'axios-mock-adapter'; + +describe('OAuth2Client tests', () => { + test('get token should request a token on the first invocation', async () => { + const httpClient = new AxiosHttpClient({}); + const client = new OAuth2Client( + { + client_id: 'client_id', + client_secret: 'client_secret' + }, + {}, + httpClient + ); + + const mock = new MockAdapter(httpClient.client); + mock + .onPost( + 'https://auth.amplience.net/oauth/token', + 'grant_type=client_credentials&client_id=client_id&client_secret=client_secret' + ) + .reply(200, { + access_token: 'token', + expires_in: 0, + refresh_token: 'refresh' + }); + + expect((await client.getToken()).access_token).toEqual('token'); + }); + + test('get token should cache tokens', async () => { + const httpClient = new AxiosHttpClient({}); + const client = new OAuth2Client( + { + client_id: 'client_id', + client_secret: 'client_secret' + }, + {}, + httpClient + ); + + const mock = new MockAdapter(httpClient.client); + mock + .onPost( + 'https://auth.amplience.net/oauth/token', + 'grant_type=client_credentials&client_id=client_id&client_secret=client_secret' + ) + .reply(200, { + access_token: 'token', + expires_in: 60, + refresh_token: 'refresh' + }); + + const token1 = await client.getToken(); + + mock + .onPost( + 'https://auth.amplience.net/oauth/token', + 'grant_type=client_credentials&client_id=client_id&client_secret=client_secret' + ) + .reply(200, { + access_token: 'token2', + expires_in: 60, + refresh_token: 'refresh' + }); + + const token2 = await client.getToken(); + + expect(token1.access_token).toEqual('token'); + expect(token2.access_token).toEqual('token'); + }); + + test('cached tokens should expire', async () => { + const httpClient = new AxiosHttpClient({}); + const client = new OAuth2Client( + { + client_id: 'client_id', + client_secret: 'client_secret' + }, + {}, + httpClient + ); + + const mock = new MockAdapter(httpClient.client); + mock + .onPost( + 'https://auth.amplience.net/oauth/token', + 'grant_type=client_credentials&client_id=client_id&client_secret=client_secret' + ) + .reply(200, { + access_token: 'token', + expires_in: -60, + refresh_token: 'refresh' + }); + + const token1 = await client.getToken(); + + mock + .onPost( + 'https://auth.amplience.net/oauth/token', + 'grant_type=client_credentials&client_id=client_id&client_secret=client_secret' + ) + .reply(200, { + access_token: 'token2', + expires_in: 0, + refresh_token: 'refresh' + }); + + const token2 = await client.getToken(); + + expect(token1.access_token).toEqual('token'); + expect(token2.access_token).toEqual('token2'); + }); + + test('only one token refresh should be in flight at once', async () => { + const httpClient = new AxiosHttpClient({}); + const client = new OAuth2Client( + { + client_id: 'client_id', + client_secret: 'client_secret' + }, + {}, + httpClient + ); + + const mock = new MockAdapter(httpClient.client, { delayResponse: 2000 }); + + mock + .onPost( + 'https://auth.amplience.net/oauth/token', + 'grant_type=client_credentials&client_id=client_id&client_secret=client_secret' + ) + .replyOnce(200, { + access_token: 'token', + expires_in: 0, + refresh_token: 'refresh' + }); + + const token1 = client.getToken(); + const token2 = client.getToken(); + + expect((await token1).access_token).toEqual('token'); + expect((await token2).access_token).toEqual('token'); + }); +}); diff --git a/src/common/ch-api/oauth2/services/OAuth2Client.ts b/src/common/ch-api/oauth2/services/OAuth2Client.ts new file mode 100644 index 00000000..16772d15 --- /dev/null +++ b/src/common/ch-api/oauth2/services/OAuth2Client.ts @@ -0,0 +1,84 @@ +import { HttpClient } from '../../http/HttpClient'; +import { HttpMethod } from '../../http/HttpRequest'; +import { combineURLs } from '../../utils/URL'; +import { AccessToken } from '../models/AccessToken'; +import { AccessTokenProvider } from '../models/AccessTokenProvider'; +import { OAuth2ClientCredentials } from '../models/OAuth2ClientCredentials'; + +/** + * @hidden + */ +export class OAuth2Client implements AccessTokenProvider { + public httpClient: HttpClient; + + private clientCredentials: OAuth2ClientCredentials; + private token: AccessToken; + private tokenExpires: number; + private inFlight?: Promise; + private authUrl: string; + + constructor( + clientCredentials: OAuth2ClientCredentials, + { authUrl = 'https://auth.amplience.net' }, + httpClient: HttpClient + ) { + this.authUrl = authUrl; + this.clientCredentials = clientCredentials; + this.httpClient = httpClient; + } + + /** + * Requests an authentication token that can be used + * to make requests to the Dynamic Content api. + * Tokens are reused until they expire. + * + * @returns {Promise} + */ + public async getToken(): Promise { + if (this.inFlight != null) { + return this.inFlight; + } + + if (this.token != null && this.tokenExpires > Date.now()) { + return this.token; + } + + const payload = + 'grant_type=client_credentials' + + '&client_id=' + + encodeURIComponent(this.clientCredentials.client_id) + + '&client_secret=' + + encodeURIComponent(this.clientCredentials.client_secret); + + const request = this.httpClient.request({ + data: payload, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + method: HttpMethod.POST, + url: combineURLs(this.authUrl, '/oauth/token') + }); + + this.inFlight = request.then( + (response): AccessToken => { + if (typeof response.data !== 'object') { + throw new Error('Unexpected response format from /oauth/token endpoint'); + } + + if (response.status !== 200) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const responseText = (response.data as any).error_description || JSON.stringify(response.data); + + throw new Error(`Authorization failed (${response.status}): ${responseText}`); + } + + this.token = (response.data as unknown) as AccessToken; + this.tokenExpires = Date.now() + this.token.expires_in * 1000; + this.inFlight = undefined; + return this.token; + } + ) as Promise; + + return this.inFlight; + } +} diff --git a/src/common/ch-api/utils/URL.spec.ts b/src/common/ch-api/utils/URL.spec.ts new file mode 100644 index 00000000..f68ee36b --- /dev/null +++ b/src/common/ch-api/utils/URL.spec.ts @@ -0,0 +1,39 @@ +import { combineURLs, isAbsoluteURL } from './URL'; + +describe('URL tests', () => { + test('combine url tests', () => { + const relativeX2 = combineURLs('test', 'test2'); + + expect(relativeX2).toEqual('test/test2'); + + const absoluteX2 = combineURLs('http://absoluteUrl', 'http://absoluteUrl2'); + + expect(absoluteX2).toEqual('http://absoluteUrl2'); + + const absoluteRelative = combineURLs('http://absoluteUrl', 'test2'); + + expect(absoluteRelative).toEqual('http://absoluteUrl/test2'); + + const relativeAbsolute = combineURLs('test', 'http://absoluteUrl2'); + + expect(relativeAbsolute).toEqual('http://absoluteUrl2'); + + const absoluteNone = combineURLs('http://absoluteUrl', null); + + expect(absoluteNone).toEqual('http://absoluteUrl'); + }); + + test('absolute url tests', () => { + expect(isAbsoluteURL('http://absolute/a/b')).toBeTruthy(); + expect(isAbsoluteURL('https://absolute/a/b/')).toBeTruthy(); + expect(isAbsoluteURL('//absolute/a/b/c')).toBeTruthy(); + expect(isAbsoluteURL('//absolute')).toBeTruthy(); + + expect(isAbsoluteURL('relative')).toBeFalsy(); + expect(isAbsoluteURL('relative.html')).toBeFalsy(); + expect(isAbsoluteURL('relative/a/b/c')).toBeFalsy(); + expect(isAbsoluteURL('relative/a/b/c/')).toBeFalsy(); + expect(isAbsoluteURL('/absolute/a/b/c')).toBeFalsy(); + expect(isAbsoluteURL('/absolute')).toBeFalsy(); + }); +}); diff --git a/src/common/ch-api/utils/URL.ts b/src/common/ch-api/utils/URL.ts new file mode 100644 index 00000000..b782b2a3 --- /dev/null +++ b/src/common/ch-api/utils/URL.ts @@ -0,0 +1,17 @@ +/** + * @hidden + */ +export function isAbsoluteURL(url: string): boolean { + return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url); +} + +/** + * @hidden + */ +export function combineURLs(baseURL: string, relativeURL: string | null): string { + if (relativeURL != null && isAbsoluteURL(relativeURL)) { + return relativeURL; + } else { + return relativeURL ? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '') : baseURL; + } +} diff --git a/src/common/media/__mocks__/ch-client-factory.ts b/src/common/media/__mocks__/ch-client-factory.ts new file mode 100644 index 00000000..86580440 --- /dev/null +++ b/src/common/media/__mocks__/ch-client-factory.ts @@ -0,0 +1,9 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { ContentHub } from '../../ch-api/ContentHub'; +import { ConfigurationParameters } from '../../../commands/configure'; +import { MockContentHub } from '../mock-ch'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const chClientFactory = (_: ConfigurationParameters): ContentHub => (new MockContentHub() as any) as ContentHub; + +export default chClientFactory; diff --git a/src/common/media/__mocks__/dam-client-factory.ts b/src/common/media/__mocks__/dam-client-factory.ts deleted file mode 100644 index cc863610..00000000 --- a/src/common/media/__mocks__/dam-client-factory.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -import { DAM } from 'dam-management-sdk-js'; -import { ConfigurationParameters } from '../../../commands/configure'; -import { MockDAM } from '../mock-dam'; - -// eslint-disable-next-line @typescript-eslint/no-unused-vars -const damClientFactory = (_: ConfigurationParameters): DAM => (new MockDAM() as any) as DAM; - -export default damClientFactory; diff --git a/src/common/media/media-rewriter.spec.ts b/src/common/media/media-rewriter.spec.ts index 7458a355..bc1f08a4 100644 --- a/src/common/media/media-rewriter.spec.ts +++ b/src/common/media/media-rewriter.spec.ts @@ -1,11 +1,11 @@ import { ContentItem, ContentRepository } from 'dc-management-sdk-js'; -import damClientFactory from '../../services/dam-client-factory'; +import chClientFactory from '../../services/ch-client-factory'; import { RepositoryContentItem } from '../content-item/content-dependancy-tree'; import { MediaLinkInjector } from '../content-item/media-link-injector'; import { MediaRewriter } from './media-rewriter'; -import { MockDAM } from './mock-dam'; +import { MockContentHub } from './mock-ch'; -jest.mock('../../services/dam-client-factory'); +jest.mock('../../services/ch-client-factory'); let exampleLinks: RepositoryContentItem[] = []; @@ -13,11 +13,11 @@ describe('media-link-injector', () => { beforeEach(() => { jest.resetAllMocks(); - MockDAM.missingAssetList = false; - MockDAM.throwOnGetSettings = false; - MockDAM.returnNullEndpoint = false; - MockDAM.throwOnAssetList = false; - MockDAM.requests = []; + MockContentHub.missingAssetList = false; + MockContentHub.throwOnGetSettings = false; + MockContentHub.returnNullEndpoint = false; + MockContentHub.throwOnAssetList = false; + MockContentHub.requests = []; exampleLinks = [ { @@ -76,7 +76,7 @@ describe('media-link-injector', () => { } ]; - (damClientFactory as jest.Mock).mockReturnValue(new MockDAM()); + (chClientFactory as jest.Mock).mockReturnValue(new MockContentHub()); }); describe('Media Link Injector', () => { @@ -87,7 +87,7 @@ describe('media-link-injector', () => { expect(missing.size).toEqual(0); // 0 assets missing - expect(MockDAM.requests).toMatchInlineSnapshot(` + expect(MockContentHub.requests).toMatchInlineSnapshot(` Array [ Object { "n": 4, @@ -112,18 +112,18 @@ describe('media-link-injector', () => { expect(missing.size).toEqual(0); // 0 assets missing - expect(MockDAM.requests.length).toEqual(0); + expect(MockContentHub.requests.length).toEqual(0); }); it('should ignore media links where content with a matching name does not exist on DAM', async () => { - MockDAM.missingAssetList = true; + MockContentHub.missingAssetList = true; const rewriter = new MediaRewriter({ clientId: '', clientSecret: '', hubId: '' }, exampleLinks); const results = await rewriter.rewrite(); expect(results.size).toEqual(4); // All 4 assets missing - expect(MockDAM.requests).toMatchInlineSnapshot(` + expect(MockContentHub.requests).toMatchInlineSnapshot(` Array [ Object { "n": 4, @@ -134,7 +134,7 @@ describe('media-link-injector', () => { }); it('should fail when the settings endpoint throws', async () => { - MockDAM.throwOnGetSettings = true; + MockContentHub.throwOnGetSettings = true; const rewriter = new MediaRewriter({ clientId: '', clientSecret: '', hubId: '' }, []); let throws = false; @@ -148,7 +148,7 @@ describe('media-link-injector', () => { }); it('should fail when the settings do not contain a default endpoint', async () => { - MockDAM.returnNullEndpoint = true; + MockContentHub.returnNullEndpoint = true; const rewriter = new MediaRewriter({ clientId: '', clientSecret: '', hubId: '' }, []); let throws = false; @@ -162,7 +162,7 @@ describe('media-link-injector', () => { }); it('should fail when getting assets does not work a certain number of times in a row', async () => { - MockDAM.throwOnAssetList = true; + MockContentHub.throwOnAssetList = true; const rewriter = new MediaRewriter({ clientId: '', clientSecret: '', hubId: '' }, exampleLinks); let throws = false; @@ -174,7 +174,7 @@ describe('media-link-injector', () => { expect(throws).toBeTruthy(); - expect(MockDAM.requests).toMatchInlineSnapshot(` + expect(MockContentHub.requests).toMatchInlineSnapshot(` Array [ Object { "n": 4, @@ -222,7 +222,7 @@ describe('media-link-injector', () => { expect(missing.size).toEqual(0); // 0 assets missing - expect(MockDAM.requests.length).toEqual(expectedRequests); // 3 requests + expect(MockContentHub.requests.length).toEqual(expectedRequests); // 3 requests }); }); }); diff --git a/src/common/media/media-rewriter.ts b/src/common/media/media-rewriter.ts index 16761554..c25ad1d1 100644 --- a/src/common/media/media-rewriter.ts +++ b/src/common/media/media-rewriter.ts @@ -1,7 +1,8 @@ -import { Asset, DAM } from 'dam-management-sdk-js'; -import { DiSettingsEndpoint } from 'dam-management-sdk-js/build/main/lib/model/Settings'; +import { ContentHub } from '../ch-api/ContentHub'; +import { Asset } from '../ch-api/model/Asset'; +import { DiSettingsEndpoint } from '../ch-api/model/Settings'; import { ConfigurationParameters } from '../../commands/configure'; -import damClientFactory from '../../services/dam-client-factory'; +import chClientFactory from '../../services/ch-client-factory'; import { RepositoryContentItem } from '../content-item/content-dependancy-tree'; import { MediaLinkInjector } from '../content-item/media-link-injector'; @@ -11,7 +12,7 @@ import { MediaLinkInjector } from '../content-item/media-link-injector'; */ export class MediaRewriter { private injector: MediaLinkInjector; - private dam: DAM; + private dam: ContentHub; private endpoint: string; private defaultHost: string; @@ -25,7 +26,7 @@ export class MediaRewriter { } private connectDam(): void { - this.dam = damClientFactory(this.config); + this.dam = chClientFactory(this.config); } private async getEndpoint(): Promise { @@ -112,7 +113,6 @@ export class MediaRewriter { let requestBuilder = 'name:/'; let requestCount = 0; - let totalFound = 0; for (let i = 0; i < allNames.size; i++) { const additionalRequest = `${this.escapeForRegex(names[i])}`; @@ -130,14 +130,14 @@ export class MediaRewriter { } else { // If the query is too big, batch out what we have and start over. - totalFound += await this.queryAndAdd(requestBuilder + '/', requestCount, assetsByName); + await this.queryAndAdd(requestBuilder + '/', requestCount, assetsByName); requestBuilder = 'name:/' + additionalRequest; } } } if (requestBuilder.length > 0) { - totalFound += await this.queryAndAdd(requestBuilder + '/', requestCount, assetsByName); + await this.queryAndAdd(requestBuilder + '/', requestCount, assetsByName); } // Replace media link assets with ones that we found with matching names. diff --git a/src/common/media/mock-dam.ts b/src/common/media/mock-ch.ts similarity index 80% rename from src/common/media/mock-dam.ts rename to src/common/media/mock-ch.ts index 45112090..4992cb58 100644 --- a/src/common/media/mock-dam.ts +++ b/src/common/media/mock-ch.ts @@ -1,7 +1,8 @@ -import { DefaultApiClient, ApiClient, AssetListRequest } from 'dam-management-sdk-js'; -import { AssetsList } from 'dam-management-sdk-js/build/main/lib/model/Asset'; +import { DefaultApiClient, ApiClient } from '../ch-api/api/services/ApiClient'; +import { AssetsList } from '../ch-api/model/Asset'; +import { AssetListRequest } from '../ch-api/model/AssetListRequest'; -export class MockDAM { +export class MockContentHub { static throwOnGetSettings = false; static returnNullEndpoint = false; static throwOnAssetList = false; @@ -14,11 +15,11 @@ export class MockDAM { settings = { // eslint-disable-next-line @typescript-eslint/no-explicit-any get: (): Promise => { - if (MockDAM.throwOnGetSettings) { + if (MockContentHub.throwOnGetSettings) { throw new Error('Simulated settings error.'); } - if (MockDAM.returnNullEndpoint) { + if (MockContentHub.returnNullEndpoint) { return Promise.resolve({ di: { endpoints: [], @@ -45,15 +46,15 @@ export class MockDAM { assets = { // eslint-disable-next-line @typescript-eslint/no-explicit-any list: (query: AssetListRequest): Promise => { - MockDAM.requests.push(query); + MockContentHub.requests.push(query); - if (MockDAM.throwOnAssetList) { + if (MockContentHub.throwOnAssetList) { throw new Error('Simulated asset list error.'); } let list: AssetsList; - if (MockDAM.missingAssetList) { + if (MockContentHub.missingAssetList) { list = new AssetsList({ data: [], count: 0 diff --git a/src/services/dam-client-factory.ts b/src/services/ch-client-factory.ts similarity index 62% rename from src/services/dam-client-factory.ts rename to src/services/ch-client-factory.ts index 5f849e2c..422ec12a 100644 --- a/src/services/dam-client-factory.ts +++ b/src/services/ch-client-factory.ts @@ -1,9 +1,9 @@ /* eslint-disable @typescript-eslint/camelcase */ -import { DAM } from 'dam-management-sdk-js'; +import { ContentHub } from '../common/ch-api/ContentHub'; import { ConfigurationParameters } from '../commands/configure'; -const damClientFactory = (config: ConfigurationParameters): DAM => - new DAM( +const chClientFactory = (config: ConfigurationParameters): ContentHub => + new ContentHub( { client_id: config.clientId, client_secret: config.clientSecret @@ -14,4 +14,4 @@ const damClientFactory = (config: ConfigurationParameters): DAM => } ); -export default damClientFactory; +export default chClientFactory; From b62b5da95a4ee32580fb9624a2dea13b41deb4bf Mon Sep 17 00:00:00 2001 From: Rhys Date: Wed, 31 Mar 2021 14:53:58 +0100 Subject: [PATCH 05/10] refactor: remove unused parts of ch-api --- .../ch-api/_fixtures/assets/DELETE-gfail.json | 4 - .../_fixtures/assets/DELETE-{id}-fail.json | 4 - .../ch-api/_fixtures/assets/DELETE-{id}.json | 4 - .../ch-api/_fixtures/assets/DELETE.json | 4 - .../ch-api/_fixtures/assets/PUT-gfail.json | 4 - src/common/ch-api/_fixtures/assets/PUT.json | 13 - .../assets/_localeGroups/GET-{ids}.json | 7 - .../ch-api/_fixtures/assets/publish/POST.json | 9 - .../assets/publish/validate/POST.json | 4 - .../_fixtures/assets/text/POST-external.json | 11 - .../ch-api/_fixtures/assets/text/POST.json | 11 - .../_fixtures/assets/unpublish/POST.json | 9 - .../_fixtures/assets/{id}/download/GET.json | 4 - .../_fixtures/assets/{id}/metadata/GET.json | 69 ----- .../assets/{id}/revert/PUT-{version}.json | 9 - .../assets/{id}/thumbnails/GET-gfail.json | 4 - .../assets/{id}/transcodings/GET.json | 4 - .../assets/{id}/versions/GET-{version}.json | 36 --- .../_fixtures/assets/{id}/versions/GET.json | 151 --------- .../{id}/versions/{version}/download/GET.json | 4 - .../assets/{id}/video_profiles/GET.json | 7 - .../ch-api/api/services/ApiEndpoints.ts | 182 ----------- src/common/ch-api/model/Asset.spec.ts | 292 ------------------ src/common/ch-api/model/Asset.ts | 68 +--- src/common/ch-api/model/AssetPut.ts | 90 ------ src/common/ch-api/model/AssetPutResult.ts | 15 - src/common/ch-api/model/AssetText.ts | 33 -- src/common/ch-api/model/StringList.ts | 11 - src/common/ch-api/model/TagsPut.ts | 11 - 29 files changed, 2 insertions(+), 1072 deletions(-) delete mode 100644 src/common/ch-api/_fixtures/assets/DELETE-gfail.json delete mode 100644 src/common/ch-api/_fixtures/assets/DELETE-{id}-fail.json delete mode 100644 src/common/ch-api/_fixtures/assets/DELETE-{id}.json delete mode 100644 src/common/ch-api/_fixtures/assets/DELETE.json delete mode 100644 src/common/ch-api/_fixtures/assets/PUT-gfail.json delete mode 100644 src/common/ch-api/_fixtures/assets/PUT.json delete mode 100644 src/common/ch-api/_fixtures/assets/_localeGroups/GET-{ids}.json delete mode 100644 src/common/ch-api/_fixtures/assets/publish/POST.json delete mode 100644 src/common/ch-api/_fixtures/assets/publish/validate/POST.json delete mode 100644 src/common/ch-api/_fixtures/assets/text/POST-external.json delete mode 100644 src/common/ch-api/_fixtures/assets/text/POST.json delete mode 100644 src/common/ch-api/_fixtures/assets/unpublish/POST.json delete mode 100644 src/common/ch-api/_fixtures/assets/{id}/download/GET.json delete mode 100644 src/common/ch-api/_fixtures/assets/{id}/metadata/GET.json delete mode 100644 src/common/ch-api/_fixtures/assets/{id}/revert/PUT-{version}.json delete mode 100644 src/common/ch-api/_fixtures/assets/{id}/thumbnails/GET-gfail.json delete mode 100644 src/common/ch-api/_fixtures/assets/{id}/transcodings/GET.json delete mode 100644 src/common/ch-api/_fixtures/assets/{id}/versions/GET-{version}.json delete mode 100644 src/common/ch-api/_fixtures/assets/{id}/versions/GET.json delete mode 100644 src/common/ch-api/_fixtures/assets/{id}/versions/{version}/download/GET.json delete mode 100644 src/common/ch-api/_fixtures/assets/{id}/video_profiles/GET.json delete mode 100644 src/common/ch-api/model/AssetPut.ts delete mode 100644 src/common/ch-api/model/AssetPutResult.ts delete mode 100644 src/common/ch-api/model/AssetText.ts delete mode 100644 src/common/ch-api/model/StringList.ts delete mode 100644 src/common/ch-api/model/TagsPut.ts diff --git a/src/common/ch-api/_fixtures/assets/DELETE-gfail.json b/src/common/ch-api/_fixtures/assets/DELETE-gfail.json deleted file mode 100644 index e5d11636..00000000 --- a/src/common/ch-api/_fixtures/assets/DELETE-gfail.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "error": "Bad Request: Missing json array of asset IDs.", - "status": "failed" -} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/DELETE-{id}-fail.json b/src/common/ch-api/_fixtures/assets/DELETE-{id}-fail.json deleted file mode 100644 index 6f483ca5..00000000 --- a/src/common/ch-api/_fixtures/assets/DELETE-{id}-fail.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "error": "Bad Request: Asset ID missing.", - "status": "failed" -} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/DELETE-{id}.json b/src/common/ch-api/_fixtures/assets/DELETE-{id}.json deleted file mode 100644 index 3e33c43a..00000000 --- a/src/common/ch-api/_fixtures/assets/DELETE-{id}.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "content": "65d78690-bf4e-415d-a16c-ca4dadbb2717", - "status": "success" -} diff --git a/src/common/ch-api/_fixtures/assets/DELETE.json b/src/common/ch-api/_fixtures/assets/DELETE.json deleted file mode 100644 index 3e33c43a..00000000 --- a/src/common/ch-api/_fixtures/assets/DELETE.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "content": "65d78690-bf4e-415d-a16c-ca4dadbb2717", - "status": "success" -} diff --git a/src/common/ch-api/_fixtures/assets/PUT-gfail.json b/src/common/ch-api/_fixtures/assets/PUT-gfail.json deleted file mode 100644 index 97675a6c..00000000 --- a/src/common/ch-api/_fixtures/assets/PUT-gfail.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "error": "Bad Request: No assets found.", - "status": "failed" -} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/PUT.json b/src/common/ch-api/_fixtures/assets/PUT.json deleted file mode 100644 index e0d731e6..00000000 --- a/src/common/ch-api/_fixtures/assets/PUT.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "content": [ - { - "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", - "status": "succeeded" - }, - { - "id": "4a0bcaed-ee57-481f-98d3-4b73aad49e68", - "status": "succeeded" - } - ], - "status": "success" -} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/_localeGroups/GET-{ids}.json b/src/common/ch-api/_fixtures/assets/_localeGroups/GET-{ids}.json deleted file mode 100644 index d74ae83e..00000000 --- a/src/common/ch-api/_fixtures/assets/_localeGroups/GET-{ids}.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "content": { - "data": [], - "count": 0 - }, - "status": "success" -} diff --git a/src/common/ch-api/_fixtures/assets/publish/POST.json b/src/common/ch-api/_fixtures/assets/publish/POST.json deleted file mode 100644 index 5cb1f95a..00000000 --- a/src/common/ch-api/_fixtures/assets/publish/POST.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "content": { - "data": [ - "publish.8d1bb161-4de3-4cc7-a907-29636842032a" - ], - "count": 1 - }, - "status": "success" -} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/publish/validate/POST.json b/src/common/ch-api/_fixtures/assets/publish/validate/POST.json deleted file mode 100644 index 8815c5a2..00000000 --- a/src/common/ch-api/_fixtures/assets/publish/validate/POST.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "content": null, - "status": "success" -} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/text/POST-external.json b/src/common/ch-api/_fixtures/assets/text/POST-external.json deleted file mode 100644 index 05fd95af..00000000 --- a/src/common/ch-api/_fixtures/assets/text/POST-external.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "content": [ - { - "data": "https://thumbs.amplience.net/r/74bef2ea-010b-4853-a5ee-f0b4daa59f0d", - "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", - "status": "EXTERNAL", - "info": "File contents too large" - } - ], - "status": "success" -} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/text/POST.json b/src/common/ch-api/_fixtures/assets/text/POST.json deleted file mode 100644 index 768d4d18..00000000 --- a/src/common/ch-api/_fixtures/assets/text/POST.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "content": [ - { - "data": "Text Content Example", - "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", - "status": "INTERNAL", - "info": "" - } - ], - "status": "success" -} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/unpublish/POST.json b/src/common/ch-api/_fixtures/assets/unpublish/POST.json deleted file mode 100644 index dc82556a..00000000 --- a/src/common/ch-api/_fixtures/assets/unpublish/POST.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "content": { - "data": [ - "unpublish.8f0034fd-e0e7-4a55-a81b-a87054827a8f" - ], - "count": 1 - }, - "status": "success" -} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/{id}/download/GET.json b/src/common/ch-api/_fixtures/assets/{id}/download/GET.json deleted file mode 100644 index 4330d073..00000000 --- a/src/common/ch-api/_fixtures/assets/{id}/download/GET.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "content": "/assets/65d78690-bf4e-415d-a16c-ca4dadbb2717/download/handle?auth=example", - "status": "success" -} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/{id}/metadata/GET.json b/src/common/ch-api/_fixtures/assets/{id}/metadata/GET.json deleted file mode 100644 index c5046601..00000000 --- a/src/common/ch-api/_fixtures/assets/{id}/metadata/GET.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "content": { - "metadata": [ - { - "schema": "exif", - "schemaID": "cf9303dc-7b01-46e3-ba34-072390a4684d", - "variants": [ - { - "values": { - "software": "Adobe Photoshop CC 2014 (Macintosh)", - "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717" - }, - "variantID": "3e8a4d62-8058-46d9-a75d-bb735537e85c", - "variantName": "*" - } - ], - "PK": { - "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717" - } - }, - { - "schema": "file", - "schemaID": "590ec5d8-d2f1-4df3-b52c-32df6e166d0f", - "variants": [ - { - "values": { - "size": 1109425, - "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", - "type": "JPEG" - }, - "variantID": "3e8a4d62-8058-46d9-a75d-bb735537e85c", - "variantName": "*" - } - ], - "PK": { - "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717" - } - }, - { - "schema": "image", - "schemaID": "4e357e14-5141-4153-979e-125e4bdb9c97", - "variants": [ - { - "values": { - "colorSpace": "rgb", - "resolutionY": 0, - "valid": true, - "resolutionX": 0, - "depth": 8, - "alpha": false, - "format": "JPEG", - "width": 1289, - "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", - "height": 1400, - "resolutionUnits": 0 - }, - "variantID": "3e8a4d62-8058-46d9-a75d-bb735537e85c", - "variantName": "*" - } - ], - "PK": { - "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717" - } - } - ], - "assetId": "65d78690-bf4e-415d-a16c-ca4dadbb2717" - }, - "status": "success" -} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/{id}/revert/PUT-{version}.json b/src/common/ch-api/_fixtures/assets/{id}/revert/PUT-{version}.json deleted file mode 100644 index e2669e0e..00000000 --- a/src/common/ch-api/_fixtures/assets/{id}/revert/PUT-{version}.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "content": [ - { - "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", - "status": "succeeded" - } - ], - "status": "success" -} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/{id}/thumbnails/GET-gfail.json b/src/common/ch-api/_fixtures/assets/{id}/thumbnails/GET-gfail.json deleted file mode 100644 index 29c2059b..00000000 --- a/src/common/ch-api/_fixtures/assets/{id}/thumbnails/GET-gfail.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "error": "Not Found: Unable to find video thumbnails for supplied asset id.", - "status": "failed" -} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/{id}/transcodings/GET.json b/src/common/ch-api/_fixtures/assets/{id}/transcodings/GET.json deleted file mode 100644 index 6e285a66..00000000 --- a/src/common/ch-api/_fixtures/assets/{id}/transcodings/GET.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "error": "Not Found: Unable to find video transcoding for supplied asset id.", - "status": "failed" -} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/{id}/versions/GET-{version}.json b/src/common/ch-api/_fixtures/assets/{id}/versions/GET-{version}.json deleted file mode 100644 index f12c0ee1..00000000 --- a/src/common/ch-api/_fixtures/assets/{id}/versions/GET-{version}.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "content": { - "data": [ - { - "srcName": "AlltheLook1.jpg", - "workflow": {}, - "revisionNumber": 1, - "revertible": true, - "bucketID": "b0d42ebb-679b-4010-ae20-9ce989837481", - "label": "AlltheLook1.jpg", - "mimeType": "image/jpeg", - "type": "image", - "locale": "en-GB", - "userID": "526c39a9-f098-4ae7-8261-23821a4374cd", - "thumbFile": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", - "folderID": "00000000-0000-0000-0000-000000000000", - "file": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", - "createdDate": 1606739632488, - "contents": [], - "name": "AlltheLook1", - "subType": null, - "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", - "thumbURL": "https://thumbs.amplience.net/r/74bef2ea-010b-4853-a5ee-f0b4daa59f0d", - "user": "Username", - "changeSummary": [ - "Asset created" - ], - "publishStatus": "NOT_PUBLISHED", - "status": "active", - "timestamp": 1606739632488 - } - ], - "count": 1 - }, - "status": "success" -} \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/{id}/versions/GET.json b/src/common/ch-api/_fixtures/assets/{id}/versions/GET.json deleted file mode 100644 index 1b79da93..00000000 --- a/src/common/ch-api/_fixtures/assets/{id}/versions/GET.json +++ /dev/null @@ -1,151 +0,0 @@ -{ - "content": { - "data": [ - { - "srcName": "AlltheLook1.jpg", - "workflow": {}, - "revisionNumber": 5, - "revertible": false, - "bucketID": "b0d42ebb-679b-4010-ae20-9ce989837481", - "label": "AlltheLook1.jpg", - "mimeType": "image/jpeg", - "type": "image", - "locale": "en-GB", - "userID": "57b98b7f-c6bf-43d2-9f24-a408d1c6d59d", - "thumbFile": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", - "folderID": "00000000-0000-0000-0000-000000000000", - "file": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", - "createdDate": 1606739632488, - "contents": [], - "name": "AlltheLook1", - "subType": null, - "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", - "thumbURL": "https://thumbs.amplience.net/r/74bef2ea-010b-4853-a5ee-f0b4daa59f0d", - "user": "restricted name", - "changeSummary": [ - "Asset published", - "Publish status changed to 'PUBLISHED' from 'PUBLISH_IN_PROGRESS'" - ], - "publishStatus": "PUBLISHED", - "status": "active", - "timestamp": 1606754846688 - }, - { - "srcName": "AlltheLook1.jpg", - "workflow": {}, - "revisionNumber": 4, - "revertible": false, - "bucketID": "b0d42ebb-679b-4010-ae20-9ce989837481", - "label": "AlltheLook1.jpg", - "mimeType": "image/jpeg", - "type": "image", - "locale": "en-GB", - "userID": "57b98b7f-c6bf-43d2-9f24-a408d1c6d59d", - "thumbFile": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", - "folderID": "00000000-0000-0000-0000-000000000000", - "file": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", - "createdDate": 1606739632488, - "contents": [], - "name": "AlltheLook1", - "subType": null, - "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", - "thumbURL": "https://thumbs.amplience.net/r/74bef2ea-010b-4853-a5ee-f0b4daa59f0d", - "user": "restricted name", - "changeSummary": [ - "System / other value updated" - ], - "publishStatus": "PUBLISH_IN_PROGRESS", - "status": "active", - "timestamp": 1606754845467 - }, - { - "srcName": "AlltheLook1.jpg", - "workflow": {}, - "revisionNumber": 3, - "revertible": false, - "bucketID": "b0d42ebb-679b-4010-ae20-9ce989837481", - "label": "AlltheLook1.jpg", - "mimeType": "image/jpeg", - "type": "image", - "locale": "en-GB", - "userID": "526c39a9-f098-4ae7-8261-23821a4374cd", - "thumbFile": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", - "folderID": "00000000-0000-0000-0000-000000000000", - "file": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", - "createdDate": 1606739632488, - "contents": [], - "name": "AlltheLook1", - "subType": null, - "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", - "thumbURL": "https://thumbs.amplience.net/r/74bef2ea-010b-4853-a5ee-f0b4daa59f0d", - "user": "Username", - "changeSummary": [ - "Publish status changed to 'PUBLISH_IN_PROGRESS' from 'NOT_PUBLISHED'", - "System / other value updated" - ], - "publishStatus": "PUBLISH_IN_PROGRESS", - "status": "active", - "timestamp": 1606754844819 - }, - { - "srcName": "AlltheLook1.jpg", - "workflow": {}, - "revisionNumber": 2, - "revertible": false, - "bucketID": "b0d42ebb-679b-4010-ae20-9ce989837481", - "label": "AlltheLook1.jpg", - "mimeType": "image/jpeg", - "type": "image", - "locale": "en-GB", - "userID": "57b98b7f-c6bf-43d2-9f24-a408d1c6d59d", - "thumbFile": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", - "folderID": "00000000-0000-0000-0000-000000000000", - "file": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", - "createdDate": 1606739632488, - "contents": [], - "name": "AlltheLook1", - "subType": null, - "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", - "thumbURL": "https://thumbs.amplience.net/r/74bef2ea-010b-4853-a5ee-f0b4daa59f0d", - "user": "restricted name", - "changeSummary": [ - "Asset inspected", - "System / other value updated" - ], - "publishStatus": "NOT_PUBLISHED", - "status": "active", - "timestamp": 1606739633484 - }, - { - "srcName": "AlltheLook1.jpg", - "workflow": {}, - "revisionNumber": 1, - "revertible": true, - "bucketID": "b0d42ebb-679b-4010-ae20-9ce989837481", - "label": "AlltheLook1.jpg", - "mimeType": "image/jpeg", - "type": "image", - "locale": "en-GB", - "userID": "526c39a9-f098-4ae7-8261-23821a4374cd", - "thumbFile": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", - "folderID": "00000000-0000-0000-0000-000000000000", - "file": "74bef2ea-010b-4853-a5ee-f0b4daa59f0d", - "createdDate": 1606739632488, - "contents": [], - "name": "AlltheLook1", - "subType": null, - "id": "65d78690-bf4e-415d-a16c-ca4dadbb2717", - "thumbURL": "https://thumbs.amplience.net/r/74bef2ea-010b-4853-a5ee-f0b4daa59f0d", - "user": "Username", - "changeSummary": [ - "Asset created" - ], - "publishStatus": "NOT_PUBLISHED", - "status": "active", - "timestamp": 1606739632488 - } - ], - "count": 21 - }, - "status": "success" - } \ No newline at end of file diff --git a/src/common/ch-api/_fixtures/assets/{id}/versions/{version}/download/GET.json b/src/common/ch-api/_fixtures/assets/{id}/versions/{version}/download/GET.json deleted file mode 100644 index d1123f98..00000000 --- a/src/common/ch-api/_fixtures/assets/{id}/versions/{version}/download/GET.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "content": "/assets/65d78690-bf4e-415d-a16c-ca4dadbb2717/versions/1/download/handle?revisionNumber=1&auth=example", - "status": "success" -} diff --git a/src/common/ch-api/_fixtures/assets/{id}/video_profiles/GET.json b/src/common/ch-api/_fixtures/assets/{id}/video_profiles/GET.json deleted file mode 100644 index 5c7aff9b..00000000 --- a/src/common/ch-api/_fixtures/assets/{id}/video_profiles/GET.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "content": { - "assetId": "65d78690-bf4e-415d-a16c-ca4dadbb2717", - "profiles": [] - }, - "status": "success" -} \ No newline at end of file diff --git a/src/common/ch-api/api/services/ApiEndpoints.ts b/src/common/ch-api/api/services/ApiEndpoints.ts index 617d442f..b398348e 100644 --- a/src/common/ch-api/api/services/ApiEndpoints.ts +++ b/src/common/ch-api/api/services/ApiEndpoints.ts @@ -1,21 +1,8 @@ -import { HttpMethod } from '../../http/HttpRequest'; import { Asset, AssetsList, AssetsPage } from '../../model/Asset'; import { AssetListRequest } from '../../model/AssetListRequest'; -import { AssetMetadata } from '../../model/AssetMetadata'; -import { AssetPut } from '../../model/AssetPut'; -import { AssetPutResultList } from '../../model/AssetPutResult'; -import { AssetText, AssetTextList } from '../../model/AssetText'; import { Settings } from '../../model/Settings'; -import { StringList } from '../../model/StringList'; import { ApiClient } from './ApiClient'; -/** - * Properties to strip from an asset before PUT. - * This allows updating an Asset with an existing Asset, rather than an AssetPut. - * NOTE: Tags are not supported as they must be added/removed as a delta, rather than replaced. - */ -const assetStrip = ['revisionNum', 'userId', 'file', 'createdDate', 'timestamp', 'tags']; - export class ApiEndpoints { constructor(private client: ApiClient) {} @@ -41,50 +28,6 @@ export class ApiEndpoints { return this.assetsListToSingle(await this.client.fetchResource(`/assets/${id}`, {}, AssetsList), id); }, - /** - * Create or update an existing asset with new information. - * @param assets assets to upload to the DAM API. - * @returns A list of asset GUIDs. Throws on failure. - */ - put: (mode: 'overwrite' | 'renameUnique', assets: AssetPut[]): Promise => - this.client.genericRequest( - '/assets', - HttpMethod.PUT, - { mode, assets }, - {}, - AssetPutResultList - ), - - /** - * Create or update an existing asset, based on an existing asset. - * NOTE: Tags must be added/removed via the put() method. - * @param assets assets to upload to the DAM API. - * @returns A list of asset GUIDs. Throws on failure. - */ - putAsset: (mode: 'overwrite' | 'renameUnique', assets: Asset[]): Promise => { - const assetPuts = assets.map(asset => { - const copy = asset.toJSON(); - - assetStrip.forEach(key => { - delete copy[key]; - }); - - if (asset.workflow && asset.workflow.assignedTo == null) { - delete copy.workflow; - } - - return copy; - }); - - return this.client.genericRequest( - '/assets', - HttpMethod.PUT, - { mode, assets: assetPuts }, - {}, - AssetPutResultList - ); - }, - /** * Retrieve a list of asset resources shared with your client credentials. * @param options Pagination options @@ -96,131 +39,6 @@ export class ApiEndpoints { { query: options }, AssetsPage ); - }, - - /** - * Delete an asset resource by id. - * @param id asset id, previously generated on creation - */ - delete: (id: string): Promise => this.client.deleteResource(`/assets/${id}`, {}), - - /** - * Deletes an asset resources by id. - * @param id asset id, previously generated on creation - */ - deleteMany: (ids: string[]): Promise => - this.client.genericRequest('/assets', HttpMethod.DELETE, ids, {}, StringList), - - /** - * Retrieve a specific version of an asset resource by id. - * @param id asset id, previously generated on creation - * @param version asset version to request - */ - version: async (id: string, version: number): Promise => { - return this.assetsListToSingle( - await this.client.fetchResource(`/assets/${id}/versions/${version}`, {}, AssetsList), - id - ); - }, - - /** - * Retrieve a list of versions for a specific asset by id. - * @param options Pagination options - */ - versions: (id: string): Promise => this.client.fetchResource(`/assets/${id}/versions`, {}, AssetsList), - - /** - * Retrieves a download URL for an asset resource by id - * @param id asset id, previously generated on creation - */ - download: (id: string): Promise => this.client.fetchRawResource(`assets/${id}/download`, {}), - - /** - * Retrieves a download URL for an asset resource by id - * @param id asset id, previously generated on creation - * @param version the version of the asset to retrieve - */ - downloadVersion: (id: string, version: number): Promise => - this.client.fetchRawResource(`assets/${id}/versions/${version}/download`, {}), - - /** - * Retrieves all metadata for an asset. - * @param id asset id, previously generated on creation - */ - metadata: (id: string): Promise => - this.client.fetchResource(`assets/${id}/metadata`, {}, AssetMetadata), - - /** - * Publish a list of assets by id. - * @param ids a list of asset ids to publish - * @param mode the publish mode to use (default: UI) - */ - publish: (ids: string[], mode?: string): Promise => { - const header = { - 'X-Amp-Mode': mode || 'UI' - }; - - const body = { - assets: ids - }; - - return this.client.genericRequest('/assets/publish', HttpMethod.POST, body, { header }, StringList); - }, - - /** - * Publish a list of assets by id. - * @param ids a list of asset ids to publish - * @param mode the publish mode to use (default: UI) - */ - validatePublish: (ids: string[], mode?: string): Promise => { - const header = { - 'X-Amp-Mode': mode || 'UI' - }; - - const body = { - assets: ids - }; - - return this.client.genericRequest( - '/assets/publish/validate', - HttpMethod.POST, - body, - { header }, - StringList - ); - }, - - /** - * Unpublish a list of assets by id. - * @param ids a list of asset ids to publish - * @param mode the publish mode to use (default: UI) - */ - unpublish: (ids: string[], mode?: string): Promise => { - const header = { - 'X-Amp-Mode': mode || 'UI' - }; - - const body = { - assets: ids - }; - - return this.client.genericRequest('/assets/unpublish', HttpMethod.POST, body, { header }, StringList); - }, - - /** - * Retrieves text content for an asset. - * @param id asset id, previously generated on creation - */ - text: async (id: string): Promise => { - const list = await this.client.genericRequest( - `assets/text`, - HttpMethod.POST, - { assetIds: [id] }, - {}, - AssetTextList - ); - - return list[0]; } }; diff --git a/src/common/ch-api/model/Asset.spec.ts b/src/common/ch-api/model/Asset.spec.ts index 4edffabc..c9212ec0 100644 --- a/src/common/ch-api/model/Asset.spec.ts +++ b/src/common/ch-api/model/Asset.spec.ts @@ -1,7 +1,4 @@ import { MockContentHub } from '../ContentHub.mocks'; -import { Asset } from './Asset'; -import { AssetPut } from './AssetPut'; -import { StringList } from './StringList'; describe('AxiosHttpClient tests', () => { test('get asset by id', async () => { @@ -11,216 +8,6 @@ describe('AxiosHttpClient tests', () => { expect(result.label).toEqual('AlltheLook1.jpg'); }); - test('delete asset (self)', async () => { - const client = new MockContentHub(); - - const result = await client.assets.get('65d78690-bf4e-415d-a16c-ca4dadbb2717'); - - await expect(result.related.delete()).resolves.toBe(undefined); - // Did not throw. - }); - - test('delete asset (by id)', async () => { - const client = new MockContentHub(); - - const del = client.assets.delete('65d78690-bf4e-415d-a16c-ca4dadbb2717'); - - await expect(del).resolves.toBe(undefined); - // Did not throw. - }); - - test('delete asset (by ids)', async () => { - const client = new MockContentHub(); - - const del = client.assets.deleteMany(['65d78690-bf4e-415d-a16c-ca4dadbb2717']); - - await expect(del).resolves.toEqual(expect.any(StringList)); - // Did not throw. - }); - - test('get versions (self)', async () => { - const client = new MockContentHub(); - - const result = await client.assets.get('65d78690-bf4e-415d-a16c-ca4dadbb2717'); - - const versions = await result.related.versions(); - - let versionId = 5; - versions.getItems().forEach(version => { - expect(version.label).toEqual('AlltheLook1.jpg'); - expect(version.revisionNumber).toEqual(versionId--); - expect(version.related).not.toBeUndefined(); - }); - }); - - test('get versions (by id)', async () => { - const client = new MockContentHub(); - - const versions = await client.assets.versions('65d78690-bf4e-415d-a16c-ca4dadbb2717'); - - let versionId = 5; - versions.getItems().forEach(version => { - expect(version.label).toEqual('AlltheLook1.jpg'); - expect(version.revisionNumber).toEqual(versionId--); - expect(version.related).not.toBeUndefined(); - }); - }); - - test('get version (self)', async () => { - const client = new MockContentHub(); - - const result = await client.assets.get('65d78690-bf4e-415d-a16c-ca4dadbb2717'); - - const version1 = await result.related.version(1); - - expect(version1.revisionNumber).toEqual(1); - }); - - test('get version (by id)', async () => { - const client = new MockContentHub(); - - const result = await client.assets.version('65d78690-bf4e-415d-a16c-ca4dadbb2717', 1); - - expect(result.label).toEqual('AlltheLook1.jpg'); - expect(result.revisionNumber).toEqual(1); - }); - - test('get download (self)', async () => { - const client = new MockContentHub(); - - const result = await client.assets.get('65d78690-bf4e-415d-a16c-ca4dadbb2717'); - - const downloadPath = await result.related.download(); - - expect(downloadPath).toEqual('/assets/65d78690-bf4e-415d-a16c-ca4dadbb2717/download/handle?auth=example'); - }); - - test('get download (by id)', async () => { - const client = new MockContentHub(); - - const downloadPath = await client.assets.download('65d78690-bf4e-415d-a16c-ca4dadbb2717'); - - expect(downloadPath).toEqual('/assets/65d78690-bf4e-415d-a16c-ca4dadbb2717/download/handle?auth=example'); - }); - - test('get version download (self)', async () => { - const client = new MockContentHub(); - - const result = await client.assets.get('65d78690-bf4e-415d-a16c-ca4dadbb2717'); - - const downloadPath = await result.related.downloadVersion(1); - - expect(downloadPath).toEqual( - '/assets/65d78690-bf4e-415d-a16c-ca4dadbb2717/versions/1/download/handle?revisionNumber=1&auth=example' - ); - }); - - test('get version download (by id)', async () => { - const client = new MockContentHub(); - - const downloadPath = await client.assets.downloadVersion('65d78690-bf4e-415d-a16c-ca4dadbb2717', 1); - - expect(downloadPath).toEqual( - '/assets/65d78690-bf4e-415d-a16c-ca4dadbb2717/versions/1/download/handle?revisionNumber=1&auth=example' - ); - }); - - test('publish (self)', async () => { - const client = new MockContentHub(); - - const result = await client.assets.get('65d78690-bf4e-415d-a16c-ca4dadbb2717'); - - const publishJobs = await result.related.publish('UI'); - - expect(publishJobs.getItems()).toEqual(['publish.8d1bb161-4de3-4cc7-a907-29636842032a']); - }); - - test('publish (by ids)', async () => { - const client = new MockContentHub(); - - const publishJobs = await client.assets.publish(['65d78690-bf4e-415d-a16c-ca4dadbb2717']); - - expect(publishJobs.getItems()).toEqual(['publish.8d1bb161-4de3-4cc7-a907-29636842032a']); - }); - - test('validate publish (self)', async () => { - const client = new MockContentHub(); - - const result = await client.assets.get('65d78690-bf4e-415d-a16c-ca4dadbb2717'); - - const publishJobs = await result.related.validatePublish('UI'); - - // Returns a null list, but does not throw. - expect(publishJobs.getItems()).toBeUndefined(); - }); - - test('validate publish (by ids)', async () => { - const client = new MockContentHub(); - - const publishJobs = await client.assets.validatePublish(['65d78690-bf4e-415d-a16c-ca4dadbb2717']); - - // Returns a null list, but does not throw. - expect(publishJobs.getItems()).toBeUndefined(); - }); - - test('unpublish (self)', async () => { - const client = new MockContentHub(); - - const result = await client.assets.get('65d78690-bf4e-415d-a16c-ca4dadbb2717'); - - const publishJobs = await result.related.unpublish('UI'); - - expect(publishJobs.getItems()).toEqual(['unpublish.8f0034fd-e0e7-4a55-a81b-a87054827a8f']); - }); - - test('unpublish (by ids)', async () => { - const client = new MockContentHub(); - - const publishJobs = await client.assets.unpublish(['65d78690-bf4e-415d-a16c-ca4dadbb2717']); - - expect(publishJobs.getItems()).toEqual(['unpublish.8f0034fd-e0e7-4a55-a81b-a87054827a8f']); - }); - - test('text (self)', async () => { - const client = new MockContentHub(); - - const result = await client.assets.get('65d78690-bf4e-415d-a16c-ca4dadbb2717'); - - const text = await result.related.text(); - - expect(text.id).toEqual('65d78690-bf4e-415d-a16c-ca4dadbb2717'); - expect(text.status).toEqual('INTERNAL'); - expect(text.data).toEqual('Text Content Example'); - }); - - test('text (by id)', async () => { - const client = new MockContentHub(); - - const text = await client.assets.text('65d78690-bf4e-415d-a16c-ca4dadbb2717'); - - expect(text.id).toEqual('65d78690-bf4e-415d-a16c-ca4dadbb2717'); - expect(text.status).toEqual('INTERNAL'); - expect(text.data).toEqual('Text Content Example'); - }); - - test('get metadata (self)', async () => { - const client = new MockContentHub(); - - const result = await client.assets.get('65d78690-bf4e-415d-a16c-ca4dadbb2717'); - - const metadata = await result.related.metadata(); - - expect(metadata.metadata.map(meta => meta.schema)).toEqual(['exif', 'file', 'image']); - }); - - test('get metadata (by id)', async () => { - const client = new MockContentHub(); - - const metadata = await client.assets.metadata('65d78690-bf4e-415d-a16c-ca4dadbb2717'); - - expect(metadata.metadata.map(meta => meta.schema)).toEqual(['exif', 'file', 'image']); - }); - test('list assets', async () => { const client = new MockContentHub(); @@ -234,85 +21,6 @@ describe('AxiosHttpClient tests', () => { }); }); - const assetStrip = ['revisionNum', 'userId', 'file', 'createdDate', 'timestamp', 'tags']; - - function sharedPut(client: MockContentHub, requestIds: string[]): void { - for (let i = 0; i < requestIds.length; i++) { - const request = client.mock.history.put[i]; - - expect(request.url).toEqual('https://dam-api.amplience.net/v1.5.0/assets'); - - // Must strip reserved fields from body. - - const data = JSON.parse(request.data); - expect(data.assets[0].id).toEqual(requestIds[i]); - data.assets.forEach((asset: Asset) => { - expect(asset.label).toEqual('Replacement label'); - - assetStrip.forEach(stripped => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((asset as any)[stripped]).toBeUndefined(); - }); - }); - } - } - - test('put assets (by ids)', async () => { - const client = new MockContentHub(); - - const list = await client.assets.list(); - const items = list.getItems(); - - items.forEach(item => (item.label = 'Replacement label')); - - const result = await client.assets.putAsset('overwrite', items); - - expect(result.results).toEqual(items.map(item => ({ id: item.id, status: 'succeeded' }))); - - sharedPut(client, ['65d78690-bf4e-415d-a16c-ca4dadbb2717']); - }); - - test('put assets (self)', async () => { - const client = new MockContentHub(); - - const list = await client.assets.list(); - const items = list.getItems(); - - items.forEach(item => (item.label = 'Replacement label')); - - const result = []; - - for (const item of items) { - item.label = 'Replacement label'; - result.push(await item.related.update()); - } - - sharedPut(client, ['65d78690-bf4e-415d-a16c-ca4dadbb2717', '4a0bcaed-ee57-481f-98d3-4b73aad49e68']); - }); - - test('put assets (using asset put interface)', async () => { - const client = new MockContentHub(); - - const assetPut: AssetPut = { - id: '65d78690-bf4e-415d-a16c-ca4dadbb2717', - tags: { add: [], remove: [] } - }; - - const result = await client.assets.put('overwrite', [assetPut]); - - expect(result.results[0]).toEqual({ - id: '65d78690-bf4e-415d-a16c-ca4dadbb2717', - status: 'succeeded' - }); - - const request = client.mock.history.put[0]; - - expect(request.url).toEqual('https://dam-api.amplience.net/v1.5.0/assets'); - - const data = JSON.parse(request.data); - expect(data.assets[0]).toEqual(assetPut); - }); - test('get asset by id (failures)', async () => { const client = new MockContentHub(); const fail404 = client.assets.get('fail404'); diff --git a/src/common/ch-api/model/Asset.ts b/src/common/ch-api/model/Asset.ts index 1b8961dc..6cde90e0 100644 --- a/src/common/ch-api/model/Asset.ts +++ b/src/common/ch-api/model/Asset.ts @@ -3,11 +3,8 @@ import { ApiResource } from '../api/model/ApiResource'; import { PublishActivitySummary } from './PublishActivitySummary'; import { Page } from './Page'; import { ResourceList } from './ResourceList'; -import { AssetMetadata, AssetRelationships } from './AssetMetadata'; -import { StringList } from './StringList'; -import { AssetText } from './AssetText'; +import { AssetRelationships } from './AssetMetadata'; import { WorkflowSummary } from './WorkflowSummary'; -import { AssetPutResult } from './AssetPutResult'; export class Asset extends ApiResource { /** @@ -149,68 +146,7 @@ export class Asset extends ApiResource { /** * Resources and actions related to a Content Item */ - public readonly related = { - /** - * Delete this asset. - */ - delete: (): Promise => this.client.endpoints.assets.delete(this.id), - - /** - * Retrieves all versions for this asset. - */ - versions: (): Promise => this.client.endpoints.assets.versions(this.id), - - /** - * Retrieves a specific version for this asset. - */ - version: (version: number): Promise => this.client.endpoints.assets.version(this.id, version), - - /** - * Retrieves all metadata for this asset. - */ - metadata: (): Promise => this.client.endpoints.assets.metadata(this.id), - - /** - * Retrieves a download URL for this asset. - */ - download: (): Promise => this.client.endpoints.assets.download(this.id), - - /** - * Retrieves a download URL for this asset, with the given version. - */ - downloadVersion: (version: number): Promise => - this.client.endpoints.assets.downloadVersion(this.id, version), - - /** - * Retrieves a download URL for this asset, with the given version. - */ - text: (): Promise => this.client.endpoints.assets.text(this.id), - - /** - * Publishes only this asset. - */ - publish: (mode?: string): Promise => this.client.endpoints.assets.publish([this.id], mode), - - /** - * Validates publish for only this asset. Throws if not successful. - */ - validatePublish: (mode?: string): Promise => - this.client.endpoints.assets.validatePublish([this.id], mode), - - /** - * Unpublishes only this asset. - */ - unpublish: (mode?: string): Promise => this.client.endpoints.assets.unpublish([this.id], mode), - - /** - * Updates only this asset. - */ - update: async (): Promise => { - const results = await this.client.endpoints.assets.putAsset('overwrite', [this]); - - return results.results[0]; - } - }; + public readonly related = {}; } /** diff --git a/src/common/ch-api/model/AssetPut.ts b/src/common/ch-api/model/AssetPut.ts deleted file mode 100644 index 3b79c575..00000000 --- a/src/common/ch-api/model/AssetPut.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { PublishActivitySummary } from './PublishActivitySummary'; -import { TagsPut } from './TagsPut'; -import { WorkflowSummary } from './WorkflowSummary'; - -export interface AssetPut { - /** - * Path to the input file. E.g. http://url... s3://.... multipart://... - */ - src?: string; - - /** - * UUID identifier for the asset - */ - id?: string; - - /** - * ['image' or 'video' or 'set' or 'spin' or 'document' or 'other']: Sets the type of media to create - */ - type?: string; - - /** - * Unique name for the asset, this is unique across the account so we can flatten the it on the public URL - */ - name?: string; - - /** - * Friendly label for the asset, this is what a customer will see. If not specified this will default to srcName. - */ - label?: string; - - /** - * Name of the asset to use as a thumbnail. This should be a file ID not an asset ID - */ - thumbFile?: string; - - /** - * Original filename - */ - srcName?: string; - - /** - * ['active' or 'deleted' or 'expired']: Lifecycle Status, defaults to Active - */ - status?: string; - - /** - * UUID of the Folder this asset is contained within. Empty uuid means it is not in a specific folder - */ - folderID?: string; - - /** - * Add or Remove Tags when loading asset (Remove tags is for when you update an asset, no point when creating one) - */ - tags?: TagsPut; - - /** - * The publish status for the asset. One of: NOT_PUBLISHED, PUBLISH_IN_PROGRESS, UNPUBLISH_IN_PROGRESS, PUBLISHED - */ - publishStatus?: 'NOT_PUBLISHED' | 'PUBLISH_IN_PROGRESS' | 'UNPUBLISH_IN_PROGRESS' | 'PUBLISHED'; - - /** - * Information regarding publish activities. - */ - publish?: PublishActivitySummary; - - /** - * Information regarding unpublish activities. - */ - unpublish?: PublishActivitySummary; - - /** - * Information regarding assets workflow. - */ - workflow?: WorkflowSummary; - - /** - * If Asset type is a set (media\spin\2d...) then this will be the list of assetIds. - */ - contents?: string[]; - - /** - * UUID of the locale of the asset. - */ - localeID?: string; - - /** - * Name of the localeGroup the asset belongs to. Defaults to asset name. - */ - localeGroup?: string; -} diff --git a/src/common/ch-api/model/AssetPutResult.ts b/src/common/ch-api/model/AssetPutResult.ts deleted file mode 100644 index 97fe6a67..00000000 --- a/src/common/ch-api/model/AssetPutResult.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { ApiResource } from '../api/model/ApiResource'; - -export interface AssetPutResult { - id: string; - status: string; -} - -export class AssetPutResultList extends ApiResource { - results: AssetPutResult[]; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public parse(data: any): void { - this.results = data; - } -} diff --git a/src/common/ch-api/model/AssetText.ts b/src/common/ch-api/model/AssetText.ts deleted file mode 100644 index 5f58467b..00000000 --- a/src/common/ch-api/model/AssetText.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { ApiResource } from '../api/model/ApiResource'; - -/** - * Retrieved text content for an asset, or a link to externally hosted content. - */ -export class AssetText extends ApiResource { - /** - * UUID identifier for the asset - */ - id?: string; - - /** - * Status of the data. - * 'INTERNAL' (text data in the data field) - * 'EXTERNAL' (URL in the data field pointing to the text file) - * 'ERROR' (Error Message in the Info Field) - */ - status?: 'INTERNAL' | 'EXTERNAL' | 'ERROR'; - - /** - * Data for the requested asset. - */ - data?: string; - - /** - * Information regarding the given status. - */ - info?: string; -} - -export class AssetTextList extends ApiResource { - [index: number]: AssetText; -} diff --git a/src/common/ch-api/model/StringList.ts b/src/common/ch-api/model/StringList.ts deleted file mode 100644 index ec7bda0d..00000000 --- a/src/common/ch-api/model/StringList.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ApiResource } from '../api/model/ApiResource'; - -export class StringList extends ApiResource { - private data?: string[]; - private content?: string[]; - private count: number; - - public getItems(): string[] { - return (this.data || this.content) as string[]; - } -} diff --git a/src/common/ch-api/model/TagsPut.ts b/src/common/ch-api/model/TagsPut.ts deleted file mode 100644 index 18ce52d1..00000000 --- a/src/common/ch-api/model/TagsPut.ts +++ /dev/null @@ -1,11 +0,0 @@ -export interface TagsPut { - /** - * Tags to remove from an asset. - */ - remove?: string[]; - - /** - * Tags to add to an asset. - */ - add?: string[]; -} From f4492c7fb4716ae71da42a7b13624ffe6a782392 Mon Sep 17 00:00:00 2001 From: Rhys Date: Thu, 1 Apr 2021 09:26:54 +0100 Subject: [PATCH 06/10] test: improve test coverage of ch-api --- jest.config.js | 5 +- src/common/ch-api/ContentHub.spec.ts | 14 ++++ src/common/ch-api/api/services/ApiClient.ts | 85 --------------------- 3 files changed, 18 insertions(+), 86 deletions(-) create mode 100644 src/common/ch-api/ContentHub.spec.ts diff --git a/jest.config.js b/jest.config.js index c6945a95..d2e405d3 100644 --- a/jest.config.js +++ b/jest.config.js @@ -4,5 +4,8 @@ module.exports = { transform: { '^.+\\.ts?$': 'ts-jest' }, - preset: 'ts-jest' + preset: 'ts-jest', + coveragePathIgnorePatterns: [ + '^.+\\.mocks\.ts?$' + ] }; diff --git a/src/common/ch-api/ContentHub.spec.ts b/src/common/ch-api/ContentHub.spec.ts new file mode 100644 index 00000000..b3ff0ee1 --- /dev/null +++ b/src/common/ch-api/ContentHub.spec.ts @@ -0,0 +1,14 @@ +import { AxiosHttpClient } from 'dc-management-sdk-js'; +import { ContentHub } from './ContentHub'; + +describe('ContentHub tests', () => { + it('should use the http client given to the constructor', async () => { + const httpClient = new AxiosHttpClient({}); + + // eslint-disable-next-line @typescript-eslint/camelcase + const ch = new ContentHub({ client_id: '', client_secret: '' }, undefined, httpClient); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((ch as any)['client']['httpClient']).toEqual(httpClient); + }); +}); diff --git a/src/common/ch-api/api/services/ApiClient.ts b/src/common/ch-api/api/services/ApiClient.ts index 97a85c02..dd07f420 100644 --- a/src/common/ch-api/api/services/ApiClient.ts +++ b/src/common/ch-api/api/services/ApiClient.ts @@ -23,33 +23,7 @@ export interface ApiClient { resourceConstructor: ApiResourceConstructor ): Promise; - createResource( - path: string, - resource: T, - params: ApiParameters, - resourceConstructor: ApiResourceConstructor - ): Promise; - - updateResource( - path: string, - resource: T, - params: ApiParameters, - resourceConstructor: ApiResourceConstructor - ): Promise; - - genericRequest( - path: string, - method: HttpMethod, - body: any, - params: ApiParameters, - resourceConstructor?: ApiResourceConstructor - ): Promise; - parse(data: any, resourceConstructor: ApiResourceConstructor): T; - - serialize(data: T): any; - - deleteResource(path: string, params: ApiParameters): Promise; } /** @@ -90,71 +64,12 @@ export class DefaultApiClient implements ApiClient { return this.parse(data, resourceConstructor); } - public async createResource( - path: string, - resource: T, - params: ApiParameters, - resourceConstructor: ApiResourceConstructor - ): Promise { - path = CURIEs.expand(path, params.query); - const response = await this.invoke({ - data: this.serialize(resource), - method: HttpMethod.POST, - url: path - }); - return this.parse(response.data, resourceConstructor); - } - - public async updateResource( - path: string, - resource: T, - params: ApiParameters, - resourceConstructor: ApiResourceConstructor - ): Promise { - path = CURIEs.expand(path, params.query); - const response = await this.invoke({ - data: this.serialize(resource), - method: HttpMethod.PATCH, - url: path - }); - return this.parse(response.data, resourceConstructor); - } - - public async genericRequest( - path: string, - method: HttpMethod, - body: any, - params: ApiParameters, - resourceConstructor: ApiResourceConstructor - ): Promise { - path = CURIEs.expand(path, params.query); - const response = await this.invoke({ - data: body, - method, - url: path - }); - return this.parse(response.data, resourceConstructor); - } - - public async deleteResource(path: string, params: ApiParameters): Promise { - path = CURIEs.expand(path, params.query); - await this.invoke({ - method: HttpMethod.DELETE, - url: path - }); - return Promise.resolve(); - } - public parse(data: any, resourceConstructor: ApiResourceConstructor): T { const instance: T = new resourceConstructor(data); instance.setClient(this); return instance; } - public serialize(data: T): any { - return JSON.parse(JSON.stringify(data)); - } - protected transformDamResponse(data: any): any { // Parse DAM response. All responses are in a common format. From b3af4126675343f404245f8407f8697a19f6cc2a Mon Sep 17 00:00:00 2001 From: Rhys Date: Tue, 13 Apr 2021 16:48:35 +0100 Subject: [PATCH 07/10] refactor(content-item): address feedback for media-link changes --- src/common/media/media-rewriter.spec.ts | 33 +++-------- src/common/media/media-rewriter.ts | 77 +++++++++++++++---------- 2 files changed, 54 insertions(+), 56 deletions(-) diff --git a/src/common/media/media-rewriter.spec.ts b/src/common/media/media-rewriter.spec.ts index bc1f08a4..e0ff5f68 100644 --- a/src/common/media/media-rewriter.spec.ts +++ b/src/common/media/media-rewriter.spec.ts @@ -137,42 +137,27 @@ describe('media-link-injector', () => { MockContentHub.throwOnGetSettings = true; const rewriter = new MediaRewriter({ clientId: '', clientSecret: '', hubId: '' }, []); - let throws = false; - try { - await rewriter.rewrite(); - } catch { - throws = true; - } - - expect(throws).toBeTruthy(); + await expect(rewriter.rewrite()).rejects.toThrowErrorMatchingInlineSnapshot( + `"Could not obtain settings from DAM. Make sure you have the required permissions. Error: Simulated settings error."` + ); }); it('should fail when the settings do not contain a default endpoint', async () => { MockContentHub.returnNullEndpoint = true; const rewriter = new MediaRewriter({ clientId: '', clientSecret: '', hubId: '' }, []); - let throws = false; - try { - await rewriter.rewrite(); - } catch { - throws = true; - } - - expect(throws).toBeTruthy(); + await expect(rewriter.rewrite()).rejects.toThrowErrorMatchingInlineSnapshot( + `"Could not find the default endpoint."` + ); }); it('should fail when getting assets does not work a certain number of times in a row', async () => { MockContentHub.throwOnAssetList = true; const rewriter = new MediaRewriter({ clientId: '', clientSecret: '', hubId: '' }, exampleLinks); - let throws = false; - try { - await rewriter.rewrite(); - } catch { - throws = true; - } - - expect(throws).toBeTruthy(); + await expect(rewriter.rewrite()).rejects.toThrowErrorMatchingInlineSnapshot( + `"Request for assets failed after 3 attempts."` + ); expect(MockContentHub.requests).toMatchInlineSnapshot(` Array [ diff --git a/src/common/media/media-rewriter.ts b/src/common/media/media-rewriter.ts index c25ad1d1..bc241beb 100644 --- a/src/common/media/media-rewriter.ts +++ b/src/common/media/media-rewriter.ts @@ -75,18 +75,7 @@ export class MediaRewriter { throw new Error(`Request for assets failed after ${attempts} attempts.`); } - async rewrite(): Promise> { - this.connectDam(); - - await this.getEndpoint(); - - // Steps: - // identify existing assets by name (unique, case sensitive) - // - content item media dependancies, flush them all into a set - // - try do a few batch requests for assets with matching name (arbitrary limit: 3000 characters) - // - replace media link assets with ones that we found with matching names - // - return non-matching assets - + private getLinkNames(): Set { const allNames = new Set(); const itemLinks = this.injector.all; @@ -102,29 +91,68 @@ export class MediaRewriter { } } + return allNames; + } + + private replaceLinks(assetsByName: Map): Set { const missingAssets = new Set(); + // Replace media link assets with ones that we found with matching names. + this.injector.all.forEach(links => { + links.links.forEach(link => { + const asset = assetsByName.get(link.link.name); + if (asset != null) { + link.link.id = asset.id; + + link.link.defaultHost = this.defaultHost; + link.link.endpoint = this.endpoint; + } else { + missingAssets.add(link.link.name); + } + }); + }); + + return missingAssets; + } + + async rewrite(): Promise> { + this.connectDam(); + + await this.getEndpoint(); + + // Steps: + // identify existing assets by name (unique, case sensitive) + // - content item media dependancies, flush them all into a set + // - try do a few batch requests for assets with matching name (arbitrary limit: 3000 characters) + // - replace media link assets with ones that we found with matching names + // - return non-matching assets + + const allNames = this.getLinkNames(); + if (allNames.size == 0) { - return missingAssets; + return new Set(); } + const maxQueryLength = 3000; const assetsByName = new Map(); const names = Array.from(allNames); let requestBuilder = 'name:/'; + let first = true; let requestCount = 0; for (let i = 0; i < allNames.size; i++) { const additionalRequest = `${this.escapeForRegex(names[i])}`; const lengthSoFar = requestBuilder.length; - if (lengthSoFar == 6) { + if (first) { // First entry? requestBuilder += additionalRequest; requestCount++; + first = false; } else { - if (lengthSoFar + 4 + additionalRequest.length < 3000) { - // OR + if (lengthSoFar + 1 + additionalRequest.length < maxQueryLength) { + // | requestBuilder += '|' + additionalRequest; requestCount++; } else { @@ -140,21 +168,6 @@ export class MediaRewriter { await this.queryAndAdd(requestBuilder + '/', requestCount, assetsByName); } - // Replace media link assets with ones that we found with matching names. - this.injector.all.forEach(links => { - links.links.forEach(link => { - const asset = assetsByName.get(link.link.name); - if (asset != null) { - link.link.id = asset.id; - - link.link.defaultHost = this.defaultHost; - link.link.endpoint = this.endpoint; - } else { - missingAssets.add(link.link.name); - } - }); - }); - - return missingAssets; + return this.replaceLinks(assetsByName); } } From e2c36803bb3e801a408622ae7b27c099d52216e0 Mon Sep 17 00:00:00 2001 From: Rhys Date: Mon, 19 Apr 2021 11:05:02 +0100 Subject: [PATCH 08/10] refactor: address media link feedback 2 --- package-lock.json | 34 +++++++ package.json | 2 + src/common/media/media-rewriter.spec.ts | 8 ++ src/common/media/media-rewriter.ts | 117 ++++++++++++------------ tsconfig.json | 2 +- 5 files changed, 101 insertions(+), 62 deletions(-) diff --git a/package-lock.json b/package-lock.json index a215f293..5b792482 100644 --- a/package-lock.json +++ b/package-lock.json @@ -857,6 +857,21 @@ "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", "dev": true }, + "@types/promise-retry": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@types/promise-retry/-/promise-retry-1.1.3.tgz", + "integrity": "sha512-LxIlEpEX6frE3co3vCO2EUJfHIta1IOmhDlcAsR4GMMv9hev1iTI9VwberVGkePJAuLZs5rMucrV8CziCfuJMw==", + "dev": true, + "requires": { + "@types/retry": "*" + } + }, + "@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true + }, "@types/rimraf": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.0.tgz", @@ -2855,6 +2870,11 @@ "once": "^1.4.0" } }, + "err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==" + }, "error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -7081,6 +7101,15 @@ "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", "dev": true }, + "promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "requires": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + } + }, "prompts": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.2.1.tgz", @@ -7413,6 +7442,11 @@ "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", "dev": true }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" + }, "right-pad": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/right-pad/-/right-pad-1.0.1.tgz", diff --git a/package.json b/package.json index ec75f504..56e45def 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "@types/jest": "^24.0.18", "@types/lodash": "^4.14.144", "@types/node-fetch": "^2.5.7", + "@types/promise-retry": "^1.1.3", "@types/rimraf": "^3.0.0", "@types/table": "^4.0.7", "@types/url-template": "^2.0.28", @@ -117,6 +118,7 @@ "dc-management-sdk-js": "^1.9.0", "lodash": "^4.17.15", "node-fetch": "^2.6.0", + "promise-retry": "^2.0.1", "rimraf": "^3.0.0", "sanitize-filename": "^1.6.3", "table": "^5.4.6", diff --git a/src/common/media/media-rewriter.spec.ts b/src/common/media/media-rewriter.spec.ts index e0ff5f68..c23d3586 100644 --- a/src/common/media/media-rewriter.spec.ts +++ b/src/common/media/media-rewriter.spec.ts @@ -7,6 +7,14 @@ import { MockContentHub } from './mock-ch'; jest.mock('../../services/ch-client-factory'); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +jest.mock('promise-retry', () => (fn: unknown, options: any): unknown => { + const retryActual = jest.requireActual('promise-retry'); + options.minTimeout = 0; + options.maxTimeout = 0; + return retryActual(fn, options); +}); + let exampleLinks: RepositoryContentItem[] = []; describe('media-link-injector', () => { diff --git a/src/common/media/media-rewriter.ts b/src/common/media/media-rewriter.ts index bc241beb..9b5313cb 100644 --- a/src/common/media/media-rewriter.ts +++ b/src/common/media/media-rewriter.ts @@ -5,6 +5,7 @@ import { ConfigurationParameters } from '../../commands/configure'; import chClientFactory from '../../services/ch-client-factory'; import { RepositoryContentItem } from '../content-item/content-dependancy-tree'; import { MediaLinkInjector } from '../content-item/media-link-injector'; +import promiseRetry from 'promise-retry'; /** * Exports media related to given content items from an existing repository. @@ -50,48 +51,40 @@ export class MediaRewriter { } private async queryAndAdd(query: string, count: number, assets: Map): Promise { - const attempts = 3; + let attemptCount = 0; - for (let i = 0; i < attempts; i++) { - try { - const result = await this.dam.assets.list({ - q: '(' + query + ')', - n: count - }); - - const items = result.getItems(); - - items.forEach(asset => { - assets.set(asset.name as string, asset); - }); - - return items.length; - } catch (e) { - // Retry - } + try { + return await promiseRetry( + async (retry, attempt) => { + try { + const result = await this.dam.assets.list({ + q: '(' + query + ')', + n: count + }); + + const items = result.getItems(); + + items.forEach(asset => { + assets.set(asset.name as string, asset); + }); + + return items.length; + } catch (e) { + attemptCount = attempt; + retry(e); + return 0; + } + }, + { retries: 2 } + ); + } catch (e) { + // Too many retries, fail the request. + throw new Error(`Request for assets failed after ${attemptCount} attempts.`); } - - // Too many retries, fail the request. - throw new Error(`Request for assets failed after ${attempts} attempts.`); } private getLinkNames(): Set { - const allNames = new Set(); - - const itemLinks = this.injector.all; - - for (let i = 0; i < itemLinks.length; i++) { - const item = itemLinks[i]; - - const links = item.links; - for (let j = 0; j < links.length; j++) { - const link = links[j]; - - allNames.add(link.link.name); - } - } - - return allNames; + return new Set(this.injector.all.flatMap(links => links.links.map(link => link.link.name))); } private replaceLinks(assetsByName: Map): Set { @@ -137,35 +130,37 @@ export class MediaRewriter { const assetsByName = new Map(); const names = Array.from(allNames); - let requestBuilder = 'name:/'; - let first = true; - let requestCount = 0; - - for (let i = 0; i < allNames.size; i++) { - const additionalRequest = `${this.escapeForRegex(names[i])}`; - - const lengthSoFar = requestBuilder.length; - if (first) { - // First entry? - requestBuilder += additionalRequest; - requestCount++; - first = false; - } else { - if (lengthSoFar + 1 + additionalRequest.length < maxQueryLength) { - // | - requestBuilder += '|' + additionalRequest; + if (names.length > 0) { + let requestBuilder = 'name:/'; + let first = true; + let requestCount = 0; + + for (const name of names) { + const additionalRequest = `${this.escapeForRegex(name)}`; + + const lengthSoFar = requestBuilder.length; + if (first) { + // First entry? + requestBuilder += additionalRequest; requestCount++; + first = false; } else { - // If the query is too big, batch out what we have and start over. - - await this.queryAndAdd(requestBuilder + '/', requestCount, assetsByName); - requestBuilder = 'name:/' + additionalRequest; + if (lengthSoFar + 1 + additionalRequest.length < maxQueryLength) { + // | + requestBuilder += '|' + additionalRequest; + requestCount++; + } else { + // If the query is too big, batch out what we have and start over. + + await this.queryAndAdd(requestBuilder + '/', requestCount, assetsByName); + requestBuilder = 'name:/' + additionalRequest; + } } } - } - if (requestBuilder.length > 0) { - await this.queryAndAdd(requestBuilder + '/', requestCount, assetsByName); + if (requestBuilder.length > 0) { + await this.queryAndAdd(requestBuilder + '/', requestCount, assetsByName); + } } return this.replaceLinks(assetsByName); diff --git a/tsconfig.json b/tsconfig.json index f19d5faf..23b5e977 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "typeRoots" : ["./node_modules/@types", "./definitions"], "module": "commonjs", - "target": "es2018", + "target": "es2019", "outDir": "dist", "noImplicitAny": true, "removeComments": true, From 46a9006176401ad977f8029066005a07b0f53f23 Mon Sep 17 00:00:00 2001 From: Rhys Date: Mon, 19 Apr 2021 12:08:21 +0100 Subject: [PATCH 09/10] refactor: revert to es2018, improve getLinkNames --- src/common/media/media-rewriter.ts | 10 +++++++++- tsconfig.json | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/common/media/media-rewriter.ts b/src/common/media/media-rewriter.ts index 9b5313cb..a436cdb8 100644 --- a/src/common/media/media-rewriter.ts +++ b/src/common/media/media-rewriter.ts @@ -84,7 +84,15 @@ export class MediaRewriter { } private getLinkNames(): Set { - return new Set(this.injector.all.flatMap(links => links.links.map(link => link.link.name))); + const allNames = new Set(); + + this.injector.all.forEach(item => { + item.links.forEach(link => { + allNames.add(link.link.name); + }); + }); + + return allNames; } private replaceLinks(assetsByName: Map): Set { diff --git a/tsconfig.json b/tsconfig.json index 23b5e977..f19d5faf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "typeRoots" : ["./node_modules/@types", "./definitions"], "module": "commonjs", - "target": "es2019", + "target": "es2018", "outDir": "dist", "noImplicitAny": true, "removeComments": true, From d0f0ddf19b8454fa1bf20d106f2b5f1c2366b9a3 Mon Sep 17 00:00:00 2001 From: Rhys Date: Tue, 15 Jun 2021 16:29:40 +0100 Subject: [PATCH 10/10] Revert "refactor: revert to es2018, improve getLinkNames" This reverts commit 46b2e68a60b090c852d47582b9a340c968e6e73a. --- src/common/media/media-rewriter.ts | 10 +--------- tsconfig.json | 2 +- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/src/common/media/media-rewriter.ts b/src/common/media/media-rewriter.ts index a436cdb8..9b5313cb 100644 --- a/src/common/media/media-rewriter.ts +++ b/src/common/media/media-rewriter.ts @@ -84,15 +84,7 @@ export class MediaRewriter { } private getLinkNames(): Set { - const allNames = new Set(); - - this.injector.all.forEach(item => { - item.links.forEach(link => { - allNames.add(link.link.name); - }); - }); - - return allNames; + return new Set(this.injector.all.flatMap(links => links.links.map(link => link.link.name))); } private replaceLinks(assetsByName: Map): Set { diff --git a/tsconfig.json b/tsconfig.json index f19d5faf..23b5e977 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "typeRoots" : ["./node_modules/@types", "./definitions"], "module": "commonjs", - "target": "es2018", + "target": "es2019", "outDir": "dist", "noImplicitAny": true, "removeComments": true,