import { Plugin, CallExpressionShape, type Record, type Shape } from '@servicenow/sdk-build-core' import { NowIdShape } from '../now-id-plugin' function convertToNumber(value: Shape, defaultValue: number = 0): number { if (!value || value.isUndefined() || value.ifString()?.isEmpty()) { return defaultValue } return value.toNumber()?.getValue() ?? defaultValue } export const ServicePortalPlugin = Plugin.create({ name: 'ServicePortalPlugin', records: { sp_portal: { relationships: { m2m_sp_portal_search_source: { descendant: true, via: 'sp_portal', }, m2m_sp_portal_knowledge_base: { descendant: true, via: 'sp_portal', }, m2m_sp_portal_catalog: { descendant: true, via: 'sp_portal', }, m2m_sp_portal_taxonomy: { descendant: true, via: 'sp_portal', }, }, toShape(record, { descendants }) { // Handle search sources const searchSources = descendants.query('m2m_sp_portal_search_source').map((m2m) => { return { searchSource: m2m.get('sp_search_source'), order: convertToNumber(m2m.get('order'), 100), } }) // Handle knowledge bases const knowledgeBases = descendants .query('m2m_sp_portal_knowledge_base') .filter((m2m) => !m2m.get('kb_knowledge_base')?.ifString()?.isEmpty()) .map((m2m) => { return { knowledgeBase: m2m.get('kb_knowledge_base'), order: convertToNumber(m2m.get('order'), 100), active: m2m.get('active')?.isUndefined() ? undefined : m2m.get('active')?.toBoolean()?.getValue(), } }) // Handle catalogs const catalogs = descendants .query('m2m_sp_portal_catalog') .filter((m2m) => !m2m.get('sc_catalog')?.ifString()?.isEmpty()) .map((m2m) => { return { catalog: m2m.get('sc_catalog'), order: convertToNumber(m2m.get('order'), 100), active: m2m.get('active')?.isUndefined() ? undefined : m2m.get('active')?.toBoolean()?.getValue(), } }) // Handle taxonomies const taxonomies = descendants.query('m2m_sp_portal_taxonomy').map((m2m) => { return { taxonomy: m2m.get('taxonomy'), order: convertToNumber(m2m.get('order'), 100), active: m2m.get('active')?.isUndefined() ? undefined : m2m.get('active')?.toBoolean()?.getValue(), } }) const portalRecord = new CallExpressionShape({ source: record, callee: 'ServicePortal', args: [ record.transform(({ $ }) => ({ $id: $.val(NowIdShape.from(record)), title: $, urlSuffix: $.from('url_suffix').def(''), icon: $.from('icon').def(''), logo: $.from('logo').def(''), logoAltText: $.from('logo_alt_text').def(''), quickStartConfig: $.from('quick_start_config').def(''), embeddableMacroponents: $.from('embeddable_macroponents').def(''), communicationChannels: $.from('communication_channels').def(''), cssVariables: $.from('css_variables').def(''), theme: $.from('theme').def(''), darkTheme: $.from('dark_theme').def(''), mainMenu: $.from('sp_rectangle_menu').def(''), categoryHomePage: $.from('sc_category_page').def(''), homePage: $.from('homepage').def(''), searchResultsConfiguration: $.from('search_results_configuration').def(''), searchApplication: $.from('search_application').def(''), alternatePortal: $.from('alternate_portal').def(''), catalogHomePage: $.from('sc_catalog_page').def(''), loginPage: $.from('login_page').def(''), knowledgeHomePage: $.from('kb_knowledge_page').def(''), chatQueue: $.from('sp_chat_queue').def(''), notFoundPage: $.from('notfound_page').def(''), textIndexGroup: $.from('ts_index_group').def(''), enableAiSearch: $.from('enable_ais').toBoolean().def(false), enableCertificateBasedAuthentication: $.from('enable_certificate_based_authentication') .toBoolean() .def(false), defaultPortal: $.from('default').toBoolean().def(false), enableWebEmbeddables: $.from('enable_embeddables').toBoolean().def(false), enableFavorites: $.from('enable_favorites').toBoolean().def(false), inactive: $.from('inactive').toBoolean().def(false), hidePortalName: $.from('hide_portal_name').toBoolean().def(false), supportRightToLeftLanguages: $.from('rtl_enabled').toBoolean().def(false), searchSources: $.val(searchSources).def([]), knowledgeBases: $.val(knowledgeBases).def([]), catalogs: $.val(catalogs).def([]), taxonomies: $.val(taxonomies).def([]), })), ], }) return { success: true, value: portalRecord, } }, }, m2m_sp_portal_search_source: { coalesce: ['sp_portal', 'sp_search_source'], }, m2m_sp_portal_knowledge_base: { coalesce: ['sp_portal', 'kb_knowledge_base'], }, m2m_sp_portal_catalog: { coalesce: ['sp_portal', 'sc_catalog'], }, m2m_sp_portal_taxonomy: { coalesce: ['sp_portal', 'taxonomy'], }, }, shapes: [ { shape: CallExpressionShape, fileTypes: ['fluent'], async toRecord(callExpression, { factory, diagnostics }) { if (callExpression.getCallee() !== 'ServicePortal') { return { success: false } } const portalArgs = callExpression.getArgument(0).asObject() const urlSuffix = portalArgs.get('urlSuffix').ifString() if (urlSuffix) { const raw = urlSuffix.getValue() let normalized = raw.toLowerCase().replace(/[^a-zA-Z0-9_-]/g, '_') while (normalized.indexOf('__') !== -1) { normalized = normalized.replace(/__/g, '_') } while (normalized !== '' && normalized.charAt(0) === '_') { normalized = normalized.substring(1) } while (normalized !== '' && normalized.charAt(normalized.length - 1) === '_') { normalized = normalized.substring(0, normalized.length - 1) } if (raw !== normalized) { diagnostics.error( urlSuffix.getOriginalNode(), `Invalid url_suffix '${raw}'. Use lowercase alphanumeric characters, hyphens, and single underscores, without a leading or trailing underscore (e.g. '${normalized}')` ) } } const catalogsForM2M = portalArgs.get('catalogs')?.ifArray()?.getElements() ?? [] const knowledgeBasesForM2M = portalArgs.get('knowledgeBases')?.ifArray()?.getElements() ?? [] const deprecatedKnowledgeBase = portalArgs.get('knowledgeBase') if (deprecatedKnowledgeBase?.isDefined()) { if (knowledgeBasesForM2M.length > 0) { diagnostics.error( deprecatedKnowledgeBase.getOriginalNode(), `Cannot use both 'knowledgeBase' and 'knowledgeBases'. Remove 'knowledgeBase' and use only the 'knowledgeBases' array instead.` ) } else { diagnostics.warn( deprecatedKnowledgeBase.getOriginalNode(), `'knowledgeBase' is deprecated. Use the 'knowledgeBases' array instead.` ) } } const deprecatedCatalog = portalArgs.get('catalog') if (deprecatedCatalog?.isDefined()) { if (catalogsForM2M.length > 0) { diagnostics.error( deprecatedCatalog.getOriginalNode(), `Cannot use both 'catalog' and 'catalogs'. Remove 'catalog' and use only the 'catalogs' array instead.` ) } else { diagnostics.warn( deprecatedCatalog.getOriginalNode(), `'catalog' is deprecated. Use the 'catalogs' array instead.` ) } } // Build portal properties const baseProperties = portalArgs.transform(({ $ }) => ({ title: $, url_suffix: $.from('urlSuffix').def(''), icon: $.from('icon'), logo: $.from('logo'), logo_alt_text: $.from('logoAltText'), quick_start_config: $.from('quickStartConfig'), embeddable_macroponents: $.from('embeddableMacroponents'), communication_channels: $.from('communicationChannels'), css_variables: $.from('cssVariables'), theme: $.from('theme'), dark_theme: $.from('darkTheme'), sp_rectangle_menu: $.from('mainMenu'), sc_category_page: $.from('categoryHomePage'), homepage: $.from('homePage'), search_results_configuration: $.from('searchResultsConfiguration'), search_application: $.from('searchApplication'), alternate_portal: $.from('alternatePortal'), sc_catalog_page: $.from('catalogHomePage'), login_page: $.from('loginPage'), kb_knowledge_page: $.from('knowledgeHomePage'), sp_chat_queue: $.from('chatQueue'), notfound_page: $.from('notFoundPage'), ts_index_group: $.from('textIndexGroup'), ...(knowledgeBasesForM2M.length === 0 ? { kb_knowledge_base: $.from('knowledgeBase') } : {}), ...(catalogsForM2M.length === 0 ? { sc_catalog: $.from('catalog') } : {}), enable_ais: $.from('enableAiSearch').def(false), enable_certificate_based_authentication: $.from('enableCertificateBasedAuthentication').def(false), default: $.from('defaultPortal').def(false), enable_embeddables: $.from('enableWebEmbeddables').def(false), enable_favorites: $.from('enableFavorites').def(false), inactive: $.from('inactive').def(false), hide_portal_name: $.from('hidePortalName').def(false), rtl_enabled: $.from('supportRightToLeftLanguages').def(false), })) const portalRecord = await factory.createRecord({ source: callExpression, table: 'sp_portal', explicitId: portalArgs.get('$id'), properties: baseProperties, }) // Handle search sources const searchSources = portalArgs.get('searchSources')?.ifArray()?.getElements() ?? [] const searchSourceRecords: Record[] = [] for (const source of searchSources) { const sourceObj = source.asObject() const searchSourceRecord = await factory.createRecord({ source: callExpression, table: 'm2m_sp_portal_search_source', properties: { sp_portal: portalRecord, sp_search_source: sourceObj.get('searchSource'), order: convertToNumber(sourceObj.get('order'), 100), }, }) searchSourceRecords.push(searchSourceRecord) } // Handle knowledge bases const knowledgeBaseRecords: Record[] = [] for (const kb of knowledgeBasesForM2M) { const kbObj = kb.asObject() const knowledgeBaseRecord = await factory.createRecord({ source: callExpression, table: 'm2m_sp_portal_knowledge_base', properties: { sp_portal: portalRecord, kb_knowledge_base: kbObj.get('knowledgeBase'), order: convertToNumber(kbObj.get('order'), 100), active: kbObj.get('active')?.isUndefined() ? true : kbObj.get('active').toBoolean().getValue(), }, }) knowledgeBaseRecords.push(knowledgeBaseRecord) } // Handle catalogs const catalogRecords: Record[] = [] for (const cat of catalogsForM2M) { const catObj = cat.asObject() const catalogRecord = await factory.createRecord({ source: callExpression, table: 'm2m_sp_portal_catalog', properties: { sp_portal: portalRecord, sc_catalog: catObj.get('catalog'), order: convertToNumber(catObj.get('order'), 100), active: catObj.get('active')?.isUndefined() ? true : catObj.get('active').toBoolean().getValue(), }, }) catalogRecords.push(catalogRecord) } // Handle taxonomies const taxonomies = portalArgs.get('taxonomies')?.ifArray()?.getElements() ?? [] const taxonomyRecords: Record[] = [] for (const tax of taxonomies) { const taxObj = tax.asObject() const taxonomyRecord = await factory.createRecord({ source: callExpression, table: 'm2m_sp_portal_taxonomy', properties: { sp_portal: portalRecord, taxonomy: taxObj.get('taxonomy'), order: convertToNumber(taxObj.get('order'), 100), active: taxObj.get('active')?.isUndefined() ? true : taxObj.get('active').toBoolean().getValue(), }, }) taxonomyRecords.push(taxonomyRecord) } return { success: true, value: portalRecord.with( ...searchSourceRecords, ...knowledgeBaseRecords, ...catalogRecords, ...taxonomyRecords ), } }, }, ], })