From 27765ae22869dc507e4059ecbda67a08c4507fff Mon Sep 17 00:00:00 2001 From: ivaylokostov Date: Thu, 14 Nov 2024 12:44:37 +0200 Subject: [PATCH 1/4] Kadanza DAM media connector v0.0.1 * Add a connector with query, detail, filtering, metadata capabilities * Add support only for oAuth2ResourceOwnerPassword auth at this stage --- src/connectors/kadanza/connector.ts | 358 +++++++++++++++++++++++++++ src/connectors/kadanza/package.json | 31 +++ src/connectors/kadanza/tests.json | 27 ++ src/connectors/kadanza/tsconfig.json | 15 ++ 4 files changed, 431 insertions(+) create mode 100644 src/connectors/kadanza/connector.ts create mode 100644 src/connectors/kadanza/package.json create mode 100644 src/connectors/kadanza/tests.json create mode 100644 src/connectors/kadanza/tsconfig.json diff --git a/src/connectors/kadanza/connector.ts b/src/connectors/kadanza/connector.ts new file mode 100644 index 0000000..0d6cf1c --- /dev/null +++ b/src/connectors/kadanza/connector.ts @@ -0,0 +1,358 @@ +import { Connector, Media } from '@chili-publish/studio-connectors'; + +interface DamMedia { + id: number; + name: string; + thumbnail: string; + format: string; + width: number; + height: number; + size: number; + assetHash: string; + tenantHash: string; + fileName: string; + title: string; + createdBy: { + id: number; + userName: string; + firstName: string; + lastName: string; + } +} + +interface DamMediaPage { + 'hydra:totalItems': number; + 'hydra:itemsPerPage': number; + 'hydra:currentPage': number; + 'hydra:totalPages': number; + 'hydra:member': Array; +} + +interface AssetId { + id: string; + name: string; + assetHash: string; + tenantHash: string; + thumbnail: string; +} + +interface DAMCustomMetadata { + id: number; + name: string; + label: string; + type: string; + defaultValue: string; + required: boolean; + default: boolean; + readOnly: boolean; + visible: boolean; + sorting: boolean; + sort: number; + dropdownOptions: object; + title: string; + text: string; + deletedAt: string; + filterable: boolean; + sortable: boolean; + attributeName: string; +} + +interface DAMCustomMetadataPage { + 'hydra:totalItems': number; + 'hydra:itemsPerPage': number; + 'hydra:currentPage': number; + 'hydra:totalPages': number; + 'hydra:member': Array; +} + +export default class DamConnector implements Media.MediaConnector { + runtime: Connector.ConnectorRuntimeContext; + + constructor(runtime: Connector.ConnectorRuntimeContext) { + this.runtime = runtime; + } + + async detail( + id: string, + context: Connector.Dictionary + ): Promise { + const assetId: AssetId = JSON.parse(id); + const damMedia = await this._getDamMediaById(assetId.id); + const metadata = await this._getCustomMetadata(); + + return this._getMediaDetailFromDamMedia(damMedia, metadata); + } + + async query( + options: Connector.QueryOptions, + context: Connector.Dictionary + ): Promise { + this._logError( + `query options: sortOrder ${options?.sortOrder} sortBy ${options?.sortBy} collection ${options?.collection} filter ${options?.filter} pageToken ${options?.pageToken} pageSize ${options?.pageSize}` + ); + + this._logError( + `context: categoryGroup ${context?.categoryGroup} category ${context?.category} searchQuery ${context?.searchQuery}` + ); + + const currentPage = Number(options.pageToken) || 1; + const pageSize = Number(options.pageSize) || 15; + this._logError( + `currentPage: ${currentPage} pageSize: ${pageSize}` + ); + let queryEndpoint = `${this._getBaseMediaUrl()}/api/assets?page=${currentPage}&pageSize=${pageSize}`; + + // Check if a specific categoryGroup is provided in the settings (context) + if (context?.categoryGroup) { + queryEndpoint += `&categoryGroup=${context.categoryGroup}`; + } + + // Check if a specific category is provided in the settings (context) + if (context?.category) { + queryEndpoint += `&category=${context.category}`; + } + + const filter = options?.filter + ? options?.filter + : undefined; + + if (filter && filter.length > 0) { + const stringifiedFilter = filter.toString().trim(); + + let id; + try { + id = JSON.parse(stringifiedFilter).id; + this._logError( + `ID ${id}` + ); + } catch (e) { + // filter is not JSON + } + + if (id) { + this._logError( + `Filtering query by _id: ${id}` + ); + + queryEndpoint += `&search=_id: ${id}`; + } else if (stringifiedFilter && context?.searchQuery) { + this._logError( + `Filtering query by ${stringifiedFilter} in ${context.searchQuery}` + ); + + const search = context.searchQuery.toString().replace('', stringifiedFilter); + queryEndpoint += `&search=${search}`; + } + } + + this._logError(`Query: endpoint ${encodeURI(queryEndpoint)}`); + + const result = await this.runtime.fetch(encodeURI(queryEndpoint), { + method: 'GET', + headers: this._getHeaders(), + }); + + if (result.status / 200 != 1) { + this._logError(`Query fetch failed.`); + throw new Error(`Query failed ${result.status} ${result.statusText}`); + } + + const assetsPage: DamMediaPage = JSON.parse(result.text); + const page = assetsPage['hydra:currentPage']; + + const metadata = await this._getCustomMetadata(); + + const nextPage = Number(assetsPage['hydra:currentPage']) < Number(assetsPage['hydra:totalPages']) ? Number(assetsPage['hydra:currentPage']) + 1 : ''; + this._logError(`nextPage: ${nextPage}`); + + return { + pageSize: pageSize, + data: assetsPage['hydra:member'].map((a: DamMedia) => + this._getMediaDetailFromDamMedia(a, metadata) + ), + links: { + nextPage: nextPage.toString(), + }, + }; + } + + async download( + id: string, + previewType: Media.DownloadType, + intent: Media.DownloadIntent, + context: Connector.Dictionary + ): Promise { + this._logError(`Download: id ${id}, previewType ${previewType}`); + + // Temporary commented until issue with >= 1 await statements is resolved + // const detail = await this._getDamMediaById(id); + + // Extract all details from stringified id + const detail: AssetId = JSON.parse(id); + + const baseUrl = this._getBaseMediaUrl(); + let downloadEndpoint = `${baseUrl}`; + + switch (previewType) { + // TODO -> handle medium/highres with conversion profiles? + + case 'fullres': + case 'highres': + case 'original': + downloadEndpoint += `/${detail.tenantHash}/${detail.assetHash}/${encodeURIComponent( + detail.name + )}/original`; + break; + + case 'thumbnail': + case 'mediumres': + downloadEndpoint += detail.thumbnail ? detail.thumbnail : `/${detail.tenantHash}/${detail.assetHash}/${encodeURIComponent( + detail.name + )}/original`; + break; + + default: + downloadEndpoint += `/${detail.tenantHash}/${detail.assetHash}/${encodeURIComponent( + detail.name + )}/original`; + break; + } + + this._logError(`Download: endpoint ${downloadEndpoint}`); + + const result = await this.runtime.fetch(downloadEndpoint, { + method: 'GET', + headers: this._getHeaders(), + }); + + this._logError( + `Download: result status ${result.status} ${result.statusText}.` + ); + + if (result.status / 200 != 1) { + this._logError( + `Download: fetch failed for media with id ${id} and previewType ${previewType}` + ); + throw new Error(`Download failed ${result.status} ${result.statusText}`); + } + + this._logError( + `Download: result array buffer id, bytes: ${ + (result.arrayBuffer.id, result.arrayBuffer.bytes) + }` + ); + + return result.arrayBuffer; + } + + getConfigurationOptions(): Connector.ConnectorConfigValue[] | null { + return [ + { + name: 'categoryGroup', + displayName: 'Category group (entrypoint) ID', + type: 'text', + }, + { + name: 'category', + displayName: 'Category ID', + type: 'text', + }, + { + name: 'searchQuery', + displayName: 'Search query', + type: 'text', + }, + ]; + } + + getCapabilities(): Media.MediaConnectorCapabilities { + return { + query: true, + detail: true, + filtering: true, + metadata: true, + }; + } + + _getBaseMediaUrl() { + return this.runtime.options['BASE_URL']; + } + + _getDebug() { + return this.runtime.options['DEBUG']; + } + + _getHeaders() { + return { + 'Accept': 'application/json', + }; + } + + _getMediaDetailFromDamMedia(damMedia: DamMedia, customMetadata: DAMCustomMetadataPage): Media.MediaDetail { + const assetId: AssetId = { + id: damMedia.id.toString(), + name: damMedia.name, + assetHash: damMedia.assetHash, + tenantHash: damMedia.tenantHash, + thumbnail: damMedia.thumbnail, + }; + + return { + // We save all information required for 'download` under id to avoid details call + id: JSON.stringify(assetId), + name: damMedia.name, + relativePath: 'Media', + type: 0, + metaData: this._getMetadataFromDamMedia(damMedia, customMetadata), + extension: damMedia.format, + width: damMedia.width, + height: damMedia.height, + }; + } + + _getMetadataFromDamMedia(damMedia: DamMedia, customMetadata: DAMCustomMetadataPage): Connector.Dictionary { + const attributeNames: Array = customMetadata['hydra:member'].map((m) => m.attributeName); + + return Object.fromEntries(attributeNames.filter((a) => typeof damMedia[a] === 'string').map((a) => [a, damMedia[a]] )); + } + + _logError(err: string) { + if (this._getDebug()) { + this.runtime.logError(err); + } + } + + private async _getDamMediaById(id: string) { + const detailEndpoint = `${this._getBaseMediaUrl()}/api/assets/${id}`; + + const result = await this.runtime.fetch(detailEndpoint, { + method: 'GET', + headers: this._getHeaders(), + }); + + if (result.status / 200 != 1) { + this._logError(`Detail fetch failed for media with id ${id}`); + throw new Error(`Detail failed ${result.status} ${result.statusText}`); + } + + const damMedia: DamMedia = JSON.parse(result.text); + return damMedia; + } + + private async _getCustomMetadata() { + const customMetadataEndpoint = `${this._getBaseMediaUrl()}/api/custom-metadata`; + + const result = await this.runtime.fetch(customMetadataEndpoint, { + method: 'GET', + headers: this._getHeaders(), + }); + + if (result.status / 200 != 1) { + this._logError(`Custom metadata fetch failed.`); + throw new Error(`Custom metadata fetch failed: ${result.status} ${result.statusText}`); + } + + const customMetadata: DAMCustomMetadataPage = JSON.parse(result.text); + return customMetadata; + } +} diff --git a/src/connectors/kadanza/package.json b/src/connectors/kadanza/package.json new file mode 100644 index 0000000..f04cd99 --- /dev/null +++ b/src/connectors/kadanza/package.json @@ -0,0 +1,31 @@ +{ + "name": "kadanza", + "description": "Kadanza DAM", + "version": "0.0.1", + "author": { + "name": "CHILI publish", + "email": "info@chili-publish.com", + "url": "https://github.com/chili-publish" + }, + "config": { + "connectorName": "Kadanza DAM", + "type": "media", + "options": {"BASE_URL": null, "DEBUG": false}, + "mappings": {}, + "supportedAuth": ["oAuth2ResourceOwnerPassword"] + }, + "private": true, + "license": "MIT", + "main": "out/connector.js", + "dependencies": { + "typescript": "^5.2.2", + "@chili-publish/studio-connectors": "^1" + }, + "scripts": { + "build": "yarn connector-cli build", + "test": "yarn connector-cli test -t tests.json && yarn connector-cli stress" + }, + "devDependencies": { + "@chili-publish/connector-cli": "^1" + } +} \ No newline at end of file diff --git a/src/connectors/kadanza/tests.json b/src/connectors/kadanza/tests.json new file mode 100644 index 0000000..d77dc55 --- /dev/null +++ b/src/connectors/kadanza/tests.json @@ -0,0 +1,27 @@ +{ + "setup": { + "runtime_options": { + "BASE_URL": "https://localhost:3000" + } + }, + "tests": [ + { + "name": "test1", + "method": "download", + "arguments": { + "id": "id", + "url": "url", + "options": {} + }, + "asserts": { + "fetch": [ + { + "url": "url", + "method": "GET", + "count": 1 + } + ] + } + } + ] +} \ No newline at end of file diff --git a/src/connectors/kadanza/tsconfig.json b/src/connectors/kadanza/tsconfig.json new file mode 100644 index 0000000..45031db --- /dev/null +++ b/src/connectors/kadanza/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "lib": ["ES2020"], + "noEmitHelpers": true, + "module": "ES2020", + "outDir": "out", + "target": "ES2020", + "moduleResolution": "Node", + "preserveConstEnums": false, + "esModuleInterop": false, + "removeComments": true, + "declaration": false + }, + "include": ["connector.ts"] +} From a3ba93426407e0c494d08f891b2a01a6d81d42ed Mon Sep 17 00:00:00 2001 From: ivaylokostov Date: Wed, 20 Nov 2024 13:15:57 +0200 Subject: [PATCH 2/4] Kadanza DAM media connector v0.0.1 - add readme * Describe publish, runtime options and authentication --- src/connectors/kadanza/readme.md | 55 ++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/connectors/kadanza/readme.md diff --git a/src/connectors/kadanza/readme.md b/src/connectors/kadanza/readme.md new file mode 100644 index 0000000..e003b63 --- /dev/null +++ b/src/connectors/kadanza/readme.md @@ -0,0 +1,55 @@ +## Publish +``` +connector-cli publish \ + -e {ENVIRONMENT} \ + -b https://{ENVIRONMENT}.chili-publish.online/grafx \ + -n "Kadanza DAM Media Connector v0.0.1" \ + --proxyOption.allowedDomains "*.kadanza.io" \ + -ro BASE_URL="https://dam2.kadanza.io" \ + --connectorId={CONNECTOR_ID} +``` + +Enable debug messages +``` +connector-cli publish \ + -e {ENVIRONMENT} \ + -b https://{ENVIRONMENT}.chili-publish.online/grafx \ + -n "Kadanza DAM Media Connector v0.0.1" \ + --proxyOption.allowedDomains "*.kadanza.io" \ + -ro BASE_URL="https://dam2.kadanza.io" \ + -ro DEBUG=true \ + --connectorId={CONNECTOR_ID} +``` + +## Set authenticator + +``` +connector-cli set-auth \ + --connectorId {CONNECTOR_ID} \ + -e {ENVIRONMENT} \ + -b https://{ENVIRONMENT}.chili-publish.online/grafx \ + -au server \ + -at oAuth2ResourceOwnerPassword \ + --auth-data-file oauth-resource-owner.json +``` + +## Authorization setup + +### Create the authentication json files +https://docs.chiligrafx.com/GraFx-Developers/connectors/authorization-for-connectors/ + +The ones needed +- oAuth2ResourceOwnerPassword + +### Example + +oauth-resource-owner.json +``` +{ + "clientId": "{CLIENT_ID}", + "clientSecret": "{CLIENT_SECRET}", + "username": "{FUSIONAUTH_USERNAME}", + "password": "{FUSIONAUTH_PASSWORD}", + "tokenEndpoint": "https://idp.kadanza.io/oauth2/token" +} +``` \ No newline at end of file From 14757cdd981c0975bfbed8c5599333609f7eb6d4 Mon Sep 17 00:00:00 2001 From: ivaylokostov Date: Thu, 21 Nov 2024 10:23:37 +0200 Subject: [PATCH 3/4] Kadanza DAM media connector v0.0.1 - update readme and package.json * Set author to Kadanza * Add icon url * Add oAuth2AuthorizationCode to supportedAuth * Set studio-connectors and connector-cli deps versions to the ones that were used while building and testing * Add oAuth2AuthorizationCode example in the readme --- src/connectors/kadanza/package.json | 13 ++++++------ src/connectors/kadanza/readme.md | 32 +++++++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/connectors/kadanza/package.json b/src/connectors/kadanza/package.json index f04cd99..97d1510 100644 --- a/src/connectors/kadanza/package.json +++ b/src/connectors/kadanza/package.json @@ -3,29 +3,30 @@ "description": "Kadanza DAM", "version": "0.0.1", "author": { - "name": "CHILI publish", - "email": "info@chili-publish.com", - "url": "https://github.com/chili-publish" + "name": "Kadanza", + "email": "hello@kadanza.com", + "url": "https://www.kadanza.com/" }, "config": { "connectorName": "Kadanza DAM", + "iconUrl": "https://s3.eu-central-1.amazonaws.com/static.kadanza.io/logo/Logo_Kadanza_Icon.svg", "type": "media", "options": {"BASE_URL": null, "DEBUG": false}, "mappings": {}, - "supportedAuth": ["oAuth2ResourceOwnerPassword"] + "supportedAuth": ["oAuth2ResourceOwnerPassword", "oAuth2AuthorizationCode"] }, "private": true, "license": "MIT", "main": "out/connector.js", "dependencies": { "typescript": "^5.2.2", - "@chili-publish/studio-connectors": "^1" + "@chili-publish/studio-connectors": "^1.15" }, "scripts": { "build": "yarn connector-cli build", "test": "yarn connector-cli test -t tests.json && yarn connector-cli stress" }, "devDependencies": { - "@chili-publish/connector-cli": "^1" + "@chili-publish/connector-cli": "^1.6" } } \ No newline at end of file diff --git a/src/connectors/kadanza/readme.md b/src/connectors/kadanza/readme.md index e003b63..00cd8b8 100644 --- a/src/connectors/kadanza/readme.md +++ b/src/connectors/kadanza/readme.md @@ -23,6 +23,7 @@ connector-cli publish \ ## Set authenticator +###Server ``` connector-cli set-auth \ --connectorId {CONNECTOR_ID} \ @@ -33,6 +34,17 @@ connector-cli set-auth \ --auth-data-file oauth-resource-owner.json ``` +### Browser +``` +connector-cli set-auth \ + --connectorId {CONNECTOR_ID} \ + -e {ENVIRONMENT} \ + -b https://{ENVIRONMENT}.chili-publish.online/grafx \ + -au browser \ + -at oAuth2AuthorizationCode \ + --auth-data-file oauth-authorization-code.json +``` + ## Authorization setup ### Create the authentication json files @@ -40,8 +52,9 @@ https://docs.chiligrafx.com/GraFx-Developers/connectors/authorization-for-connec The ones needed - oAuth2ResourceOwnerPassword +- oAuth2AuthorizationCode -### Example +### Examples oauth-resource-owner.json ``` @@ -52,4 +65,19 @@ oauth-resource-owner.json "password": "{FUSIONAUTH_PASSWORD}", "tokenEndpoint": "https://idp.kadanza.io/oauth2/token" } -``` \ No newline at end of file +``` + +oauth-authorization-code.json +``` +{ + "name": "oAuth2AuthorizationCode", + "clientId": "{CLIENT_ID}", + "clientSecret": "{CLIENT_SECRET}", + "scope": "", + "authorizationServerMetadata": { + "authorization_endpoint": "https://idp.kadanza.io/oauth2/authorize", + "token_endpoint": "https://idp.kadanza.io/oauth2/token", + "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post"] + } +} +``` From 1121975d22fb739d9b544f1b107ec405de22c372 Mon Sep 17 00:00:00 2001 From: ivaylokostov Date: Thu, 21 Nov 2024 13:54:29 +0200 Subject: [PATCH 4/4] Kadanza DAM media connector v0.0.1 - update readme * Fix formatting * Remove name from auth example, no longer used --- src/connectors/kadanza/readme.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/connectors/kadanza/readme.md b/src/connectors/kadanza/readme.md index 00cd8b8..5eec8ba 100644 --- a/src/connectors/kadanza/readme.md +++ b/src/connectors/kadanza/readme.md @@ -23,7 +23,7 @@ connector-cli publish \ ## Set authenticator -###Server +### Server ``` connector-cli set-auth \ --connectorId {CONNECTOR_ID} \ @@ -70,7 +70,6 @@ oauth-resource-owner.json oauth-authorization-code.json ``` { - "name": "oAuth2AuthorizationCode", "clientId": "{CLIENT_ID}", "clientSecret": "{CLIENT_SECRET}", "scope": "",