Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kadanza DAM media connector v0.0.1 #81

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
358 changes: 358 additions & 0 deletions src/connectors/kadanza/connector.ts
Original file line number Diff line number Diff line change
@@ -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<DamMedia>;
}

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<DAMCustomMetadata>;
}

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<Media.MediaDetail> {
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<Media.MediaPage> {
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('<search_input>', 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<Connector.ArrayBufferPointer> {
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':
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these types seems like didn't return any image or in a wrong format. At least for 'highres' it should image type as for thumbnail and mediums (although can be with higher resolution)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@psamusev could you please add more details and specifics on the assets you tested with ? Additionally, what's an image in a wrong format?

Generally speaking, the current implementation returns the original asset and that does not guarantee that it will always be an image (this is expected as the DAM supports non-image file types as well).

Copy link
Collaborator

@psamusev psamusev Nov 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ivaylo-kostov-kad you can find supported formats under "Supported Formats" section https://docs.chiligrafx.com/GraFx-Developers/connectors/media-connector/media-connector-fundamentals/#download-method. In addition this PR can be used as a reference chili-publish/studio-sdk#371

In short, we're working only with JPEG, PNG or PDF (for 'fullres' and 'original') only

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@psamusev OK, thanks - that's clear. We don't expect GraFx to support other formats. Our expectation is that if an asset download is requested and the original is a non-supported format, loading (or rendering) the asset in the document will fail - that's fine for us.
That's why you also see a TODO on line 197 - we will eventually decide whether we need to add custom logic for other formats when we actually start using the connector in real-life use-cases.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ivaylo-kostov-kad this is exact behaviour of the Canvas - you won't see the asset if it's not recognised by the Engine that leads us to partially working connector behaviour. You would need to fix it as part of the PR to let us proceed with further testing and merge

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<string> = 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;
}
}
32 changes: 32 additions & 0 deletions src/connectors/kadanza/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "kadanza",
"description": "Kadanza DAM",
"version": "0.0.1",
"author": {
psamusev marked this conversation as resolved.
Show resolved Hide resolved
"name": "Kadanza",
"email": "[email protected]",
"url": "https://www.kadanza.com/"
},
"config": {
"connectorName": "Kadanza DAM",
psamusev marked this conversation as resolved.
Show resolved Hide resolved
"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", "oAuth2AuthorizationCode"]
},
"private": true,
"license": "MIT",
"main": "out/connector.js",
"dependencies": {
"typescript": "^5.2.2",
"@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.6"
}
}
Loading