import { DataSettings, DatasourceModel } from '../datasources.js' import { Library } from '../libraries.js' import { Tenant } from './tenants.js' import { SDKModel } from '../../core/model.js' import type { SDK } from '../../sdk.js' export interface Field { name: string type: string } export interface XmTemplate { templateId?: string fullName?: string description?: string fields?: { nodes: Field[] } } export interface XmTemplateParams extends XmTemplate {} export interface XmTemplateModel extends XmTemplateParams {} export class XmTemplateModel extends SDKModel implements XmTemplate { static templateToDatasource(template: XmTemplate, libraryId: Library['id'], tenantName: Tenant['name']) { return new DatasourceModel({ libraryId, id: template.templateId, name: template.fullName.split('/').pop(), description: '', type: 'xmTemplate', settings: DataSettings({ jsonpath: '$' }), definition: template.fields.nodes, sample: {}, externalSourceId: tenantName }) } static generateSampleFromDefinition(definition: Field[]): DatasourceModel['sample'] { return definition?.reduce((obj, field: Field) => ({ ...obj, [field.name]: populateField(field) }), {}) } static async fetchAndFormatSample( datasource: DatasourceModel, tenantName: string, sitecoreUrlPostfix: string, authorization: string ) { const authoringApiUrl = `https://xmc-${tenantName}.${sitecoreUrlPostfix}/sitecore/api/authoring/graphql/v1` const body = { query: `query { search( query:{ index: "sitecore_master_index" paging: { pageSize: 10 } searchStatement: { criteria: [ { criteriaType: CONTAINS, field: "_templates", value: "${datasource.id}" } ] } } ) { results { innerItem { ${generateGraphQLFields((datasource.conflictDefinition || datasource.definition) as Field[])} } } } }` } const items = await datasource.sdk.fetchExternalJSON(authoringApiUrl, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: authorization }, body: JSON.stringify(body) }) if (items?.data?.search?.results?.length) { const item = pickMostContentfulItem(items.data.search.results) const result: Record = {} // format item await Promise.all( Object.keys(item).map(async (fieldName) => { const field = ((datasource.conflictDefinition || datasource.definition) as Field[]).find( (field) => field.name === fieldName ) // Skip fields that don't exist in the definition or have null values if (!field || !item[fieldName]) { return } // turn xml items (coming from authoring api) to json const json = xmlToJSON(item[fieldName].value) // enrich with media urls if (json['mediaid']) { const mediaUrl = await getMediaUrl( json['mediaid'], datasource.sdk, authorization, tenantName, sitecoreUrlPostfix ) if (mediaUrl) { json.src = mediaUrl } } // map fields to JSS equivalent const mappedFieldsJson = mapToJSSFields(json) // generate sample values for empty fields const generatedSampleValue = populateField(field) // add a mix of sample values and retrieved ones const fieldValue = // add an exception for rich text fields. We don't want rich text fields to be parsed, we want the raw xml value field.type?.toLowerCase() === 'rich text' && item[fieldName].value ? item[fieldName].value : // on the rest of the types, we use the parsed values Object.keys(mappedFieldsJson).length > 0 ? { ...(typeof generatedSampleValue === 'object' ? (generatedSampleValue as object) : {}), ...mappedFieldsJson } : // on no value, we use generated values item[fieldName].value || generatedSampleValue result[fieldName] = fieldValue }) ) return result } else { return null } } } const generateGraphQLFields = (definition: Field[]) => { return definition.reduce((prev, curr) => prev + `${curr.name}: field(name: "${curr.name}"){ value }\n`, '') } const pickMostContentfulItem = ( results: { innerItem: Record }[] ): Record => { let maxContentfulness = 0 let mostContentful = {} results.forEach((result) => { const item = result.innerItem as Record let contentfulness = Object.values(item).filter( (field) => field.value !== undefined && field.value !== null && field.value !== '' ).length if (contentfulness > maxContentfulness) { maxContentfulness = contentfulness mostContentful = item } }) return mostContentful } const xmlToJSON = (xml: string) => { const regex = /(\w+)="([^"]*)"/g const attributes: Record = {} let match while ((match = regex.exec(xml)) !== null) { const [_, attributeName, attributeValue] = match attributes[attributeName] = attributeValue } return attributes } const getMediaUrl = async ( mediaId: string, sdk: SDK, authorization: string, tenantName: string, sitecoreUrlPostfix: string ): Promise => { const media = await sdk.fetchJSON( `https://xmc-${tenantName}.${sitecoreUrlPostfix}/sitecore/api/authoring/graphql/v1`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: authorization }, body: JSON.stringify({ query: `query { mediaItem(where: { mediaItemId : "${mediaId}" }) { url } } ` }) } ) const mediaUrl = media?.data?.mediaItem?.url if (!mediaUrl) { return null } return `https://xmc-${tenantName}.${sitecoreUrlPostfix}` + mediaUrl } /** * Maps autoring api fields to JSS euqivalent ones */ const mapToJSSFields = (json: Record) => { const result: Record = {} const mappings: Record = { url: 'href' } for (const key in json) { if (json.hasOwnProperty(key) && mappings.hasOwnProperty(key)) { const newKey = mappings[key] result[newKey] = json[key] } else { result[key] = json[key] } } return result } const populateField = (field: Field) => { if (!field?.type) { return 'value' } const type = field.type.toLowerCase() switch (type) { case 'checkbox': return true case 'date': return '10-10-2011' case 'datetime': return '13:12:11 10-10-2010' case 'file': return { src: `https://feaasstatic.blob.core.windows.net/assets/sample/image.svg`, title: 'Sample Image', displayName: 'Sample Image' } case 'image': return { src: `https://feaasstatic.blob.core.windows.net/assets/sample/image.svg`, class: `scEmptyImage`, alt: ``, width: `580`, height: `386` } case 'integer': return 10 case 'multi-line text': return "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." case 'number': return 123 case 'password': return '***password***' case 'rich text': return ( '

Example of rich text from XMC data items.

' + '

This placeholder will be substituted in Pages

' ) case 'single-line text': return 'Single line text' case 'list': return [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }] case 'droplist': return [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }] case 'grouped Droplink': return [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }] case 'grouped Droplist': return [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }] case 'lookup Name Lookup Value List': return [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }] case 'multilist': return [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }] case 'multilist with Search': return [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }] case 'droplink': return { href: 'https://www.sitecore.com' } case 'droptree': return { href: 'https://www.sitecore.com' } case 'general link': return { className: 'link-class', class: 'link-class', title: 'Link Title', target: '_blank', anchor: '#', querystring: '?key=value', linktype: 'external', href: 'https://www.sitecore.com', text: 'description' } case 'general link with search': return { className: 'link-class', class: 'link-class', title: 'Link Title', target: '_blank', anchor: '#', querystring: '?key=value', linktype: 'internal', href: 'https://www.sitecore.com', text: 'description', search: 'Search somehting' } case 'version Identity': return { href: 'https://www.sitecore.com', text: 'description' } case 'version link': return { href: 'https://www.sitecore.com' } case 'internal link': return { className: 'link-class', class: 'link-class', title: 'Link Title', target: '_blank', anchor: '#', querystring: '?key=value', linktype: 'external', href: 'https://www.sitecore.com', text: 'description' } case 'icon': return { src: `https://feaasstatic.blob.core.windows.net/assets/sample/icon.svg`, class: `scEmptyImage`, alt: ``, width: `300`, height: `150` } case 'attachment': return { url: `https://feaasstatic.blob.core.windows.net/assets/sample/placeholder.svg` } case 'custom': return 'custom field' case 'datasource': return { fieldA: 'fieldA', fieldB: 'fieldB' } case 'thumbnail': return { src: `https://feaasstatic.blob.core.windows.net/assets/sample/icon.svg` } default: 'value' } }