diff --git a/packages/vlocity-deploy/README.md b/packages/vlocity-deploy/README.md index ebd70311..ed0ba642 100644 --- a/packages/vlocity-deploy/README.md +++ b/packages/vlocity-deploy/README.md @@ -21,12 +21,12 @@ This library is built from scratch to provide a fast and reliable way to deploy ### Also... +- Vlocode supports exporting datapacks with configurable export definitions - Vlocode does client-side OmniScript to LWC compilation and deployment - Vlocode does client-side OmniScript Activation avoiding SOQL limit exceptions when activating big scripts ## What does it **not** do... -- Vlocode vlocity-deploy library is meant for deploying datapacks **not** for exporting/extracting them from an org - Activation of flex cards - Provide you with a (fancy) UI @@ -228,3 +228,29 @@ interface DatapackDeploymentSpec { afterDeployRecord?(event: readonly DatapackDeploymentRecord[]): Promise | any; } ``` + +## Export identity and output + +An export definition can specify `exportKey` independently of `matchingKeyFields`: + +```yaml +OmniScript: + objectType: OmniProcess + name: [Type, SubType, Language] + exportKey: [Type, SubType, Language] + matchingKeyFields: [Type, SubType, Language, VersionNumber] +``` + +During expansion, this writes `OmniProcess/Type/SubType/Language` to `VlocityRecordSourceKey`. `exportKey` is an ordered field list: values are joined with slashes and prefixed with the object type. It only reads the exported datapack; it does not query Salesforce or affect record matching, lookup resolution, or the keys returned by `exportObject`. + +Expansion applies the referenced object's `exportKey` to both internal matching references and external lookup references, including nested lookup fields. This also works when the referenced datapack is exported separately: its key is computed from the fields on the reference. Full records retain their original identity inputs in reserved `VlocityRecordExportKeyValues` metadata; references use it for fields outside their matching key. This metadata survives consolidated JSON exports and is removed during expansion. Identity lookup fields are resolved even when omitted from output, so records and references produce the same portable key. These values do not become deployment lookup criteria. Matching fields remain unchanged; only serialized source-key values and parent keys are rewritten. References without an `exportKey` retain their original keys. Namespace-qualified field names are supported. Omitted or empty field lists retain the original source key. Different source versions may share an output key, preserving the existing overwrite behavior. Include `VersionNumber` in the list when deliberately writing version-specific keys. Fields used in the list remain available until `ignoreFields` is applied. + +Output settings apply to expanded files and consolidated exports: + +- `ignoreFields` removes fields after ordering and filename evaluation, so excluded fields can still control output order and names. +- `sortParentField` names a parent lookup for depth-first traversal of an embedded array. `sortFields` orders siblings. Circular parent links fail the export. +- `sortMode: vlocity` uses the configured sort fields with empty values last and canonical JSON as the final tie-breaker. OmniStudio mapping definitions use the six legacy mapping priorities. +- `nullValues` defaults to `preserve`; `omit` excludes null and empty-string fields, and `emptyString` converts nulls to empty strings. These rules apply to SObject bodies, preserving external matching instructions. +- `parseJson` controls JSON parsing of text fields and can be overridden per field. OmniStudio definitions preserve text unless a field explicitly contains structured JSON. Arrays inside JSON payloads retain their order unless `sortFields` is explicitly configured. + +The bundled native and managed OmniStudio definitions use versionless script keys, hierarchical elements, legacy mapping order and type-specific empty-field policies. Integration Procedures expand sample input into `_SampleInput.json`; OmniScript JavaScript and HTML fields retain their file extensions. diff --git a/packages/vlocity-deploy/src/__tests__/__fixtures__/exportParity/README.md b/packages/vlocity-deploy/src/__tests__/__fixtures__/exportParity/README.md new file mode 100644 index 00000000..82972776 --- /dev/null +++ b/packages/vlocity-deploy/src/__tests__/__fixtures__/exportParity/README.md @@ -0,0 +1,36 @@ +# Issue #476: Build Tools / direct export parity + +These six fixtures lock the expanded-output contract of **Vlocity Build Tools 1.14.18** for native and managed OmniStudio. All records, IDs, names and payloads are synthetic. No org data or credentials are included. + +Each fixture contains: + +- `input`: fake Salesforce records, field-reference metadata and matching-key fields. +- `expectedFiles`: the actual files produced by Build Tools, including filenames and JSON text. These are generated independently of Vlocode's direct exporter. + +The `Build Tools 1.14.18 export parity (#476)` tests in `datapackExporter.test.ts` run the real direct exporter, definition store and expander with only Salesforce schema/data and matching-key retrieval mocked. They compare all file paths and contents against this frozen oracle. JSON whitespace is disregarded; property order, array order, field presence, value types, source keys and filenames are checked. + +Coverage of the four reported differences: + +1. Parent/child traversal with multiple parents and repeated sibling sequence numbers; ignored order/level fields must survive until sorting. +2. Mapping priority order, numeric zero, empty fields, and formula mappings tied on every configured sort field, requiring the legacy canonical-JSON tiebreaker. +3. Versionless root and child export identities and matching references, with version 64 still present in the input and deployment matching definition. +4. DataRaptor empty strings, omitted empty script fields, JSON-as-text, recursively ordered JSON properties, positional JSON arrays, IP `_SampleInput.json`, OS `_JavaScript.js`, and XML filenames/content. + +## Explicit comparison boundaries + +Only two known optional metadata differences are normalized by the test: + +- Extra matching fields on **internal** `VlocityMatchingKeyObject` references. Their object type and source-key value are still compared. External lookup objects are not normalized. +- A source key on an unreferenced managed `DRMapItem__c` record. Build Tools omits it; direct export can retain it. Mapping fields, record count and order are still compared. + +The fixtures do not represent byte-identical output in those two respects and do not test Salesforce's server-side DataPack export API or deployment. The tools transport adapter provides SObject/reference envelopes from synthetic records, using the empty-string transport convention and native DataRaptor IsActive exclusion observed in the live investigation. Build Tools handles key generation, sorting, pruning, parsing and file expansion itself. + +## Regeneration + +From the repository root: + +```sh +node packages/vlocity-deploy/src/__tests__/__fixtures__/exportParity/generate.mjs +``` + +This runs offline using the extension's pinned Build Tools dependency and this repository's dependency patches. It does not call the direct exporter. It checks the Build Tools version, rejects attempted network requests and removes its temporary directory afterwards. Review oracle changes rather than regenerating fixtures merely to make a failing direct-export test pass. diff --git a/packages/vlocity-deploy/src/__tests__/__fixtures__/exportParity/generate.mjs b/packages/vlocity-deploy/src/__tests__/__fixtures__/exportParity/generate.mjs new file mode 100644 index 00000000..748670e5 --- /dev/null +++ b/packages/vlocity-deploy/src/__tests__/__fixtures__/exportParity/generate.mjs @@ -0,0 +1,116 @@ +// Manual oracle generation: uses Build Tools only, never the direct exporter. +import fs from 'node:fs'; +import path from 'node:path'; +import os from 'node:os'; +import process from 'node:process'; +import console from 'node:console'; +import { createRequire } from 'node:module'; +import { fileURLToPath } from 'node:url'; +const fixtureDir = path.dirname(fileURLToPath(import.meta.url)); +const extensionRequire = createRequire(path.resolve(fixtureDir, '../../../../../vscode-extension/package.json')); +const yaml = extensionRequire('js-yaml'); +extensionRequire.extensions['.yaml'] = (module, file) => { module.exports = yaml.load(fs.readFileSync(file, 'utf8')); }; +const Vlocity = extensionRequire('vlocity/lib/vlocity'); +const version = extensionRequire('vlocity/package.json').version; +if (version !== '1.14.18') throw new Error(`Review the oracle upgrade before regenerating: ${version}`); +const temp = fs.mkdtempSync(path.join(os.tmpdir(), 'vlocode-export-parity-')); + +function fixture(runtime, datapackType) { + const managed = runtime === 'managed'; + const field = (native, custom) => managed ? `%vlocity_namespace%__${custom}__c` : native; + const dr = datapackType === 'DataRaptor'; + const rootType = field(dr ? 'OmniDataTransform' : 'OmniProcess', dr ? 'DRBundle' : 'OmniScript'); + const childType = field(dr ? 'OmniDataTransformItem' : 'OmniProcessElement', dr ? 'DRMapItem' : 'Element'); + const ownerField = field(dr ? 'OmniDataTransformationId' : 'OmniProcessId', dr ? 'DRBundleId' : 'OmniScriptId'); + const parentField = field('ParentElementId', 'ParentElementId'); + const rootId = 'a00000000000001AAA'; + const root = { Id: rootId, __type: rootType, Name: dr ? 'MappingParity' : 'ScriptParity' }; + const references = { [childType]: { [ownerField]: rootType, ...(!dr && { [parentField]: childType }) } }; + const matchingKeys = { [rootType]: dr ? ['Name'] : [field('Type', 'Type'), field('SubType', 'SubType'), field('Language', 'Language'), field('VersionNumber', 'Version')], + [childType]: dr ? (managed ? [field('GlobalKey', 'MapId'), 'Name'] : []) : [ownerField, 'Name'] }; + let children; + if (dr) { + Object.assign(root, { [field('GlobalKey', 'GlobalKey')]: 'fixture-global-key', + [field('ExpectedInputJson', 'InputJson')]: '{"z":[{"Name":"Z"},{"Name":"A"}],"a":1}', + [field('ExpectedOutputXml', 'TargetOutXml')]: 'fixture', Description: null }); + if (!managed) root.IsActive = true; + const priorities = ['InputObjectQuerySequence', 'FilterGroup', 'OutputCreationSequence', 'OutputObjectName', 'OutputFieldName', 'InputFieldName'] + .map((name, index) => field(name, ['InterfaceObjectLookupOrder', 'FilterGroup', 'DomainObjectCreationOrder', 'DomainObjectAPIName', 'DomainObjectFieldAPIName', 'InterfaceFieldAPIName'][index])); + children = [ + [1, 1, 1, 'Record', 'Z', 'inputZ'], + [null, null, null, null, null, null], + [1, 1, 1, 'Record', 'A', 'inputA'], + [null, null, null, null, null, null], + [0, 1, 1, 'Record', 'Zero', 'inputZero'] + ].map((values, index) => ({ Id: `a0100000000000${index+1}AAA`, __type: childType, Name: root.Name, + [ownerField]: rootId, [field('GlobalKey', 'MapId')]: `mapping-${index+1}`, + ...Object.fromEntries(priorities.map((name, i) => [name, values[i]])), + [field('FormulaExpression', 'Formula')]: index === 1 ? 'Z_FORMULA' : index === 3 ? 'A_FORMULA' : null, + [field('FormulaConverted', 'FormulaConverted')]: null, Empty: null })); + } else { + Object.assign(root, { [field('Type', 'Type')]: 'Fixture', [field('SubType', 'SubType')]: datapackType, + [field('Language', 'Language')]: 'English', [field('VersionNumber', 'Version')]: 64, + [field('IsIntegrationProcedure', 'IsProcedure')]: datapackType === 'IntegrationProcedure', + [field('IsActive', 'IsActive')]: true, + [field('PropertySetConfig', 'PropertySet')]: '{"z":[{"Name":"Z"},{"Name":"A"}],"a":1}', + [field('CustomJavaScript', 'CustomJavaScript')]: datapackType === 'IntegrationProcedure' ? '{"z":2,"a":1}' : 'console.log("fixture");', + [field('ElementTypeComponentMapping', 'OmniScriptDefinitions')]: '{"z":2,"a":1}', Description: null }); + const specs = [['First',1,0,null],['ChildB',1,1,5],['ChildA',1,1,4],['ParentA',2,0,null],['ParentB',3,0,null],['Last',4,0,null]]; + children = specs.map(([Name, order, level, parent], index) => ({ Id: `a0100000000000${index+1}AAA`, __type: childType, Name, + [ownerField]: rootId, [parentField]: parent ? `a0100000000000${parent}AAA` : null, + [field('SequenceNumber', 'Order')]: order, [field('Level', 'Level')]: level, + [field('PropertySetConfig', 'PropertySet')]: '{"z":2,"a":1}', Empty: null })); + } + return { runtime, datapackType, rootId, rootType, childType, references, matchingKeys, records: [root, ...children] }; +} + +// Build Tools normally receives SObjects/references from the Salesforce DataPack service. +// This adapter supplies that transport from the synthetic records; no Salesforce connection is used. +function toolsTransport(f) { + const byId = new Map(f.records.map(record => [record.Id, record])); + const convert = (record, reference = false) => { + const output = { VlocityDataPackType: reference ? 'VlocityMatchingKeyObject' : 'SObject', VlocityRecordSObjectType: record.__type, + [reference ? 'VlocityMatchingRecordSourceKey' : 'VlocityRecordSourceKey']: `fixture/${record.Id}` }; + if (reference) { output.Name = record.Name; return output; } + for (const [name, value] of Object.entries(record)) { + if (name === '__type' || (f.datapackType === 'DataRaptor' && f.runtime === 'standard' && name === 'IsActive')) continue; + output[name] = f.references[record.__type]?.[name] && value ? convert(byId.get(value), true) : value; + // The DataPack service transport represents empty fields as strings. + if (value === null) output[name] = ''; + } + return output; + }; + const root = convert(byId.get(f.rootId)); + root[f.childType] = f.records.filter(record => record.Id !== f.rootId).map(record => convert(record)); + return { dataPacks: [{ VlocityDataPackType: f.datapackType, VlocityDataPackKey: `${f.datapackType}/Fixture`, + VlocityDataPackData: { [f.rootType]: [root] } }] }; +} + +(async () => { + for (const runtime of ['standard', 'managed']) for (const type of ['DataRaptor', 'IntegrationProcedure', 'OmniScript']) { + const input = fixture(runtime, type); + const v = new Vlocity({ tempFolder: temp, quiet: true }); + v.namespace = 'vlocity_cmt'; + v.namespacePrefix = 'vlocity_cmt__'; + v.datapacksutils.dataPacksExpandedDefinition = v.datapacksutils.updateExpandedDefinitionNamespace(v.datapacksutils.dataPacksExpandedDefinition); + v.datapacksutils.getAllReferenceFields = async () => []; + // Fail any accidental network usage by fixture generation. + v.jsForceConnection.request = () => { throw new Error('Parity fixture generation must remain offline'); }; + const job = { projectPath: temp, maxDepth: 0 }; + await v.datapacksjob.initializeJobInfo(job, 'ExpandFile'); + const folder = path.join(temp, runtime, type); + await v.datapacksexpand.expand(folder, toolsTransport(input), job); + if (job.errors.length) throw new Error(job.errors.join('\n')); + const files = {}; + function read(dir) { + for (const name of fs.readdirSync(dir)) { + const file = path.join(dir, name); + if (fs.statSync(file).isDirectory()) read(file); + else files[path.relative(folder, file)] = fs.readFileSync(file, 'utf8'); + } + } + read(folder); + fs.writeFileSync(path.join(fixtureDir, `${runtime}-${type}.json`), JSON.stringify({ toolsVersion: version, input, expectedFiles: files }, null, 2)+'\n'); + console.log(runtime, type, Object.keys(files).length); + } +})().catch(error => { console.error(error); process.exitCode = 1; }).finally(() => fs.rmSync(temp, { recursive: true })); diff --git a/packages/vlocity-deploy/src/__tests__/__fixtures__/exportParity/managed-DataRaptor.json b/packages/vlocity-deploy/src/__tests__/__fixtures__/exportParity/managed-DataRaptor.json new file mode 100644 index 00000000..76c41623 --- /dev/null +++ b/packages/vlocity-deploy/src/__tests__/__fixtures__/exportParity/managed-DataRaptor.json @@ -0,0 +1,121 @@ +{ + "toolsVersion": "1.14.18", + "input": { + "runtime": "managed", + "datapackType": "DataRaptor", + "rootId": "a00000000000001AAA", + "rootType": "%vlocity_namespace%__DRBundle__c", + "childType": "%vlocity_namespace%__DRMapItem__c", + "references": { + "%vlocity_namespace%__DRMapItem__c": { + "%vlocity_namespace%__DRBundleId__c": "%vlocity_namespace%__DRBundle__c" + } + }, + "matchingKeys": { + "%vlocity_namespace%__DRBundle__c": [ + "Name" + ], + "%vlocity_namespace%__DRMapItem__c": [ + "%vlocity_namespace%__MapId__c", + "Name" + ] + }, + "records": [ + { + "Id": "a00000000000001AAA", + "__type": "%vlocity_namespace%__DRBundle__c", + "Name": "MappingParity", + "%vlocity_namespace%__GlobalKey__c": "fixture-global-key", + "%vlocity_namespace%__InputJson__c": "{\"z\":[{\"Name\":\"Z\"},{\"Name\":\"A\"}],\"a\":1}", + "%vlocity_namespace%__TargetOutXml__c": "fixture", + "Description": null + }, + { + "Id": "a01000000000001AAA", + "__type": "%vlocity_namespace%__DRMapItem__c", + "Name": "MappingParity", + "%vlocity_namespace%__DRBundleId__c": "a00000000000001AAA", + "%vlocity_namespace%__MapId__c": "mapping-1", + "%vlocity_namespace%__InterfaceObjectLookupOrder__c": 1, + "%vlocity_namespace%__FilterGroup__c": 1, + "%vlocity_namespace%__DomainObjectCreationOrder__c": 1, + "%vlocity_namespace%__DomainObjectAPIName__c": "Record", + "%vlocity_namespace%__DomainObjectFieldAPIName__c": "Z", + "%vlocity_namespace%__InterfaceFieldAPIName__c": "inputZ", + "%vlocity_namespace%__Formula__c": null, + "%vlocity_namespace%__FormulaConverted__c": null, + "Empty": null + }, + { + "Id": "a01000000000002AAA", + "__type": "%vlocity_namespace%__DRMapItem__c", + "Name": "MappingParity", + "%vlocity_namespace%__DRBundleId__c": "a00000000000001AAA", + "%vlocity_namespace%__MapId__c": "mapping-2", + "%vlocity_namespace%__InterfaceObjectLookupOrder__c": null, + "%vlocity_namespace%__FilterGroup__c": null, + "%vlocity_namespace%__DomainObjectCreationOrder__c": null, + "%vlocity_namespace%__DomainObjectAPIName__c": null, + "%vlocity_namespace%__DomainObjectFieldAPIName__c": null, + "%vlocity_namespace%__InterfaceFieldAPIName__c": null, + "%vlocity_namespace%__Formula__c": "Z_FORMULA", + "%vlocity_namespace%__FormulaConverted__c": null, + "Empty": null + }, + { + "Id": "a01000000000003AAA", + "__type": "%vlocity_namespace%__DRMapItem__c", + "Name": "MappingParity", + "%vlocity_namespace%__DRBundleId__c": "a00000000000001AAA", + "%vlocity_namespace%__MapId__c": "mapping-3", + "%vlocity_namespace%__InterfaceObjectLookupOrder__c": 1, + "%vlocity_namespace%__FilterGroup__c": 1, + "%vlocity_namespace%__DomainObjectCreationOrder__c": 1, + "%vlocity_namespace%__DomainObjectAPIName__c": "Record", + "%vlocity_namespace%__DomainObjectFieldAPIName__c": "A", + "%vlocity_namespace%__InterfaceFieldAPIName__c": "inputA", + "%vlocity_namespace%__Formula__c": null, + "%vlocity_namespace%__FormulaConverted__c": null, + "Empty": null + }, + { + "Id": "a01000000000004AAA", + "__type": "%vlocity_namespace%__DRMapItem__c", + "Name": "MappingParity", + "%vlocity_namespace%__DRBundleId__c": "a00000000000001AAA", + "%vlocity_namespace%__MapId__c": "mapping-4", + "%vlocity_namespace%__InterfaceObjectLookupOrder__c": null, + "%vlocity_namespace%__FilterGroup__c": null, + "%vlocity_namespace%__DomainObjectCreationOrder__c": null, + "%vlocity_namespace%__DomainObjectAPIName__c": null, + "%vlocity_namespace%__DomainObjectFieldAPIName__c": null, + "%vlocity_namespace%__InterfaceFieldAPIName__c": null, + "%vlocity_namespace%__Formula__c": "A_FORMULA", + "%vlocity_namespace%__FormulaConverted__c": null, + "Empty": null + }, + { + "Id": "a01000000000005AAA", + "__type": "%vlocity_namespace%__DRMapItem__c", + "Name": "MappingParity", + "%vlocity_namespace%__DRBundleId__c": "a00000000000001AAA", + "%vlocity_namespace%__MapId__c": "mapping-5", + "%vlocity_namespace%__InterfaceObjectLookupOrder__c": 0, + "%vlocity_namespace%__FilterGroup__c": 1, + "%vlocity_namespace%__DomainObjectCreationOrder__c": 1, + "%vlocity_namespace%__DomainObjectAPIName__c": "Record", + "%vlocity_namespace%__DomainObjectFieldAPIName__c": "Zero", + "%vlocity_namespace%__InterfaceFieldAPIName__c": "inputZero", + "%vlocity_namespace%__Formula__c": null, + "%vlocity_namespace%__FormulaConverted__c": null, + "Empty": null + } + ] + }, + "expectedFiles": { + "DataRaptor/MappingParity/MappingParity_DataPack.json": "{\n \"%vlocity_namespace%__DRMapItem__c\": \"MappingParity_Mappings.json\",\n \"%vlocity_namespace%__GlobalKey__c\": \"fixture-global-key\",\n \"%vlocity_namespace%__InputJson__c\": \"MappingParity_InputJson.json\",\n \"%vlocity_namespace%__TargetOutXml__c\": \"MappingParity_TargetOutXml.xml\",\n \"Description\": \"\",\n \"Name\": \"MappingParity\",\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__DRBundle__c\",\n \"VlocityRecordSourceKey\": \"%vlocity_namespace%__DRBundle__c/MappingParity\"\n}", + "DataRaptor/MappingParity/MappingParity_InputJson.json": "{\n \"a\": 1,\n \"z\": [\n {\n \"Name\": \"Z\"\n },\n {\n \"Name\": \"A\"\n }\n ]\n}", + "DataRaptor/MappingParity/MappingParity_Mappings.json": "[\n {\n \"%vlocity_namespace%__DRBundleId__c\": {\n \"Name\": \"MappingParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"%vlocity_namespace%__DRBundle__c/MappingParity\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__DRBundle__c\"\n },\n \"%vlocity_namespace%__DomainObjectAPIName__c\": \"Record\",\n \"%vlocity_namespace%__DomainObjectCreationOrder__c\": 1,\n \"%vlocity_namespace%__DomainObjectFieldAPIName__c\": \"Zero\",\n \"%vlocity_namespace%__FilterGroup__c\": 1,\n \"%vlocity_namespace%__InterfaceFieldAPIName__c\": \"inputZero\",\n \"%vlocity_namespace%__InterfaceObjectLookupOrder__c\": 0,\n \"%vlocity_namespace%__MapId__c\": \"mapping-5\",\n \"Name\": \"MappingParity\",\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__DRMapItem__c\"\n },\n {\n \"%vlocity_namespace%__DRBundleId__c\": {\n \"Name\": \"MappingParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"%vlocity_namespace%__DRBundle__c/MappingParity\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__DRBundle__c\"\n },\n \"%vlocity_namespace%__DomainObjectAPIName__c\": \"Record\",\n \"%vlocity_namespace%__DomainObjectCreationOrder__c\": 1,\n \"%vlocity_namespace%__DomainObjectFieldAPIName__c\": \"A\",\n \"%vlocity_namespace%__FilterGroup__c\": 1,\n \"%vlocity_namespace%__InterfaceFieldAPIName__c\": \"inputA\",\n \"%vlocity_namespace%__InterfaceObjectLookupOrder__c\": 1,\n \"%vlocity_namespace%__MapId__c\": \"mapping-3\",\n \"Name\": \"MappingParity\",\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__DRMapItem__c\"\n },\n {\n \"%vlocity_namespace%__DRBundleId__c\": {\n \"Name\": \"MappingParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"%vlocity_namespace%__DRBundle__c/MappingParity\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__DRBundle__c\"\n },\n \"%vlocity_namespace%__DomainObjectAPIName__c\": \"Record\",\n \"%vlocity_namespace%__DomainObjectCreationOrder__c\": 1,\n \"%vlocity_namespace%__DomainObjectFieldAPIName__c\": \"Z\",\n \"%vlocity_namespace%__FilterGroup__c\": 1,\n \"%vlocity_namespace%__InterfaceFieldAPIName__c\": \"inputZ\",\n \"%vlocity_namespace%__InterfaceObjectLookupOrder__c\": 1,\n \"%vlocity_namespace%__MapId__c\": \"mapping-1\",\n \"Name\": \"MappingParity\",\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__DRMapItem__c\"\n },\n {\n \"%vlocity_namespace%__DRBundleId__c\": {\n \"Name\": \"MappingParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"%vlocity_namespace%__DRBundle__c/MappingParity\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__DRBundle__c\"\n },\n \"%vlocity_namespace%__Formula__c\": \"A_FORMULA\",\n \"%vlocity_namespace%__MapId__c\": \"mapping-4\",\n \"Name\": \"MappingParity\",\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__DRMapItem__c\"\n },\n {\n \"%vlocity_namespace%__DRBundleId__c\": {\n \"Name\": \"MappingParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"%vlocity_namespace%__DRBundle__c/MappingParity\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__DRBundle__c\"\n },\n \"%vlocity_namespace%__Formula__c\": \"Z_FORMULA\",\n \"%vlocity_namespace%__MapId__c\": \"mapping-2\",\n \"Name\": \"MappingParity\",\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__DRMapItem__c\"\n }\n]", + "DataRaptor/MappingParity/MappingParity_TargetOutXml.xml": "fixture" + } +} diff --git a/packages/vlocity-deploy/src/__tests__/__fixtures__/exportParity/managed-IntegrationProcedure.json b/packages/vlocity-deploy/src/__tests__/__fixtures__/exportParity/managed-IntegrationProcedure.json new file mode 100644 index 00000000..a3b2a407 --- /dev/null +++ b/packages/vlocity-deploy/src/__tests__/__fixtures__/exportParity/managed-IntegrationProcedure.json @@ -0,0 +1,122 @@ +{ + "toolsVersion": "1.14.18", + "input": { + "runtime": "managed", + "datapackType": "IntegrationProcedure", + "rootId": "a00000000000001AAA", + "rootType": "%vlocity_namespace%__OmniScript__c", + "childType": "%vlocity_namespace%__Element__c", + "references": { + "%vlocity_namespace%__Element__c": { + "%vlocity_namespace%__OmniScriptId__c": "%vlocity_namespace%__OmniScript__c", + "%vlocity_namespace%__ParentElementId__c": "%vlocity_namespace%__Element__c" + } + }, + "matchingKeys": { + "%vlocity_namespace%__OmniScript__c": [ + "%vlocity_namespace%__Type__c", + "%vlocity_namespace%__SubType__c", + "%vlocity_namespace%__Language__c", + "%vlocity_namespace%__Version__c" + ], + "%vlocity_namespace%__Element__c": [ + "%vlocity_namespace%__OmniScriptId__c", + "Name" + ] + }, + "records": [ + { + "Id": "a00000000000001AAA", + "__type": "%vlocity_namespace%__OmniScript__c", + "Name": "ScriptParity", + "%vlocity_namespace%__Type__c": "Fixture", + "%vlocity_namespace%__SubType__c": "IntegrationProcedure", + "%vlocity_namespace%__Language__c": "English", + "%vlocity_namespace%__Version__c": 64, + "%vlocity_namespace%__IsProcedure__c": true, + "%vlocity_namespace%__IsActive__c": true, + "%vlocity_namespace%__PropertySet__c": "{\"z\":[{\"Name\":\"Z\"},{\"Name\":\"A\"}],\"a\":1}", + "%vlocity_namespace%__CustomJavaScript__c": "{\"z\":2,\"a\":1}", + "%vlocity_namespace%__OmniScriptDefinitions__c": "{\"z\":2,\"a\":1}", + "Description": null + }, + { + "Id": "a01000000000001AAA", + "__type": "%vlocity_namespace%__Element__c", + "Name": "First", + "%vlocity_namespace%__OmniScriptId__c": "a00000000000001AAA", + "%vlocity_namespace%__ParentElementId__c": null, + "%vlocity_namespace%__Order__c": 1, + "%vlocity_namespace%__Level__c": 0, + "%vlocity_namespace%__PropertySet__c": "{\"z\":2,\"a\":1}", + "Empty": null + }, + { + "Id": "a01000000000002AAA", + "__type": "%vlocity_namespace%__Element__c", + "Name": "ChildB", + "%vlocity_namespace%__OmniScriptId__c": "a00000000000001AAA", + "%vlocity_namespace%__ParentElementId__c": "a01000000000005AAA", + "%vlocity_namespace%__Order__c": 1, + "%vlocity_namespace%__Level__c": 1, + "%vlocity_namespace%__PropertySet__c": "{\"z\":2,\"a\":1}", + "Empty": null + }, + { + "Id": "a01000000000003AAA", + "__type": "%vlocity_namespace%__Element__c", + "Name": "ChildA", + "%vlocity_namespace%__OmniScriptId__c": "a00000000000001AAA", + "%vlocity_namespace%__ParentElementId__c": "a01000000000004AAA", + "%vlocity_namespace%__Order__c": 1, + "%vlocity_namespace%__Level__c": 1, + "%vlocity_namespace%__PropertySet__c": "{\"z\":2,\"a\":1}", + "Empty": null + }, + { + "Id": "a01000000000004AAA", + "__type": "%vlocity_namespace%__Element__c", + "Name": "ParentA", + "%vlocity_namespace%__OmniScriptId__c": "a00000000000001AAA", + "%vlocity_namespace%__ParentElementId__c": null, + "%vlocity_namespace%__Order__c": 2, + "%vlocity_namespace%__Level__c": 0, + "%vlocity_namespace%__PropertySet__c": "{\"z\":2,\"a\":1}", + "Empty": null + }, + { + "Id": "a01000000000005AAA", + "__type": "%vlocity_namespace%__Element__c", + "Name": "ParentB", + "%vlocity_namespace%__OmniScriptId__c": "a00000000000001AAA", + "%vlocity_namespace%__ParentElementId__c": null, + "%vlocity_namespace%__Order__c": 3, + "%vlocity_namespace%__Level__c": 0, + "%vlocity_namespace%__PropertySet__c": "{\"z\":2,\"a\":1}", + "Empty": null + }, + { + "Id": "a01000000000006AAA", + "__type": "%vlocity_namespace%__Element__c", + "Name": "Last", + "%vlocity_namespace%__OmniScriptId__c": "a00000000000001AAA", + "%vlocity_namespace%__ParentElementId__c": null, + "%vlocity_namespace%__Order__c": 4, + "%vlocity_namespace%__Level__c": 0, + "%vlocity_namespace%__PropertySet__c": "{\"z\":2,\"a\":1}", + "Empty": null + } + ] + }, + "expectedFiles": { + "IntegrationProcedure/Fixture_IntegrationProcedure/Fixture_IntegrationProcedure_DataPack.json": "{\n \"%vlocity_namespace%__CustomJavaScript__c\": \"Fixture_IntegrationProcedure_SampleInput.json\",\n \"%vlocity_namespace%__Element__c\": [\n \"Fixture_IntegrationProcedure_Element_First.json\",\n \"Fixture_IntegrationProcedure_Element_ParentA.json\",\n \"Fixture_IntegrationProcedure_Element_ChildA.json\",\n \"Fixture_IntegrationProcedure_Element_ParentB.json\",\n \"Fixture_IntegrationProcedure_Element_ChildB.json\",\n \"Fixture_IntegrationProcedure_Element_Last.json\"\n ],\n \"%vlocity_namespace%__IsProcedure__c\": true,\n \"%vlocity_namespace%__Language__c\": \"English\",\n \"%vlocity_namespace%__OmniScriptDefinitions__c\": \"{\\\"z\\\":2,\\\"a\\\":1}\",\n \"%vlocity_namespace%__PropertySet__c\": \"Fixture_IntegrationProcedure_PropertySet.json\",\n \"%vlocity_namespace%__SubType__c\": \"IntegrationProcedure\",\n \"%vlocity_namespace%__Type__c\": \"Fixture\",\n \"Name\": \"ScriptParity\",\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__OmniScript__c\",\n \"VlocityRecordSourceKey\": \"%vlocity_namespace%__OmniScript__c/Fixture/IntegrationProcedure/English\"\n}", + "IntegrationProcedure/Fixture_IntegrationProcedure/Fixture_IntegrationProcedure_Element_ChildA.json": "{\n \"%vlocity_namespace%__OmniScriptId__c\": {\n \"Name\": \"ScriptParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"%vlocity_namespace%__OmniScript__c/Fixture/IntegrationProcedure/English\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__OmniScript__c\"\n },\n \"%vlocity_namespace%__ParentElementId__c\": {\n \"Name\": \"ParentA\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"%vlocity_namespace%__Element__c/%vlocity_namespace%__OmniScript__c/Fixture/IntegrationProcedure/English/ParentA\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__Element__c\"\n },\n \"%vlocity_namespace%__PropertySet__c\": {\n \"a\": 1,\n \"z\": 2\n },\n \"Name\": \"ChildA\",\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__Element__c\",\n \"VlocityRecordSourceKey\": \"%vlocity_namespace%__Element__c/%vlocity_namespace%__OmniScript__c/Fixture/IntegrationProcedure/English/ChildA\"\n}", + "IntegrationProcedure/Fixture_IntegrationProcedure/Fixture_IntegrationProcedure_Element_ChildB.json": "{\n \"%vlocity_namespace%__OmniScriptId__c\": {\n \"Name\": \"ScriptParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"%vlocity_namespace%__OmniScript__c/Fixture/IntegrationProcedure/English\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__OmniScript__c\"\n },\n \"%vlocity_namespace%__ParentElementId__c\": {\n \"Name\": \"ParentB\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"%vlocity_namespace%__Element__c/%vlocity_namespace%__OmniScript__c/Fixture/IntegrationProcedure/English/ParentB\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__Element__c\"\n },\n \"%vlocity_namespace%__PropertySet__c\": {\n \"a\": 1,\n \"z\": 2\n },\n \"Name\": \"ChildB\",\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__Element__c\",\n \"VlocityRecordSourceKey\": \"%vlocity_namespace%__Element__c/%vlocity_namespace%__OmniScript__c/Fixture/IntegrationProcedure/English/ChildB\"\n}", + "IntegrationProcedure/Fixture_IntegrationProcedure/Fixture_IntegrationProcedure_Element_First.json": "{\n \"%vlocity_namespace%__OmniScriptId__c\": {\n \"Name\": \"ScriptParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"%vlocity_namespace%__OmniScript__c/Fixture/IntegrationProcedure/English\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__OmniScript__c\"\n },\n \"%vlocity_namespace%__PropertySet__c\": {\n \"a\": 1,\n \"z\": 2\n },\n \"Name\": \"First\",\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__Element__c\",\n \"VlocityRecordSourceKey\": \"%vlocity_namespace%__Element__c/%vlocity_namespace%__OmniScript__c/Fixture/IntegrationProcedure/English/First\"\n}", + "IntegrationProcedure/Fixture_IntegrationProcedure/Fixture_IntegrationProcedure_Element_Last.json": "{\n \"%vlocity_namespace%__OmniScriptId__c\": {\n \"Name\": \"ScriptParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"%vlocity_namespace%__OmniScript__c/Fixture/IntegrationProcedure/English\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__OmniScript__c\"\n },\n \"%vlocity_namespace%__PropertySet__c\": {\n \"a\": 1,\n \"z\": 2\n },\n \"Name\": \"Last\",\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__Element__c\",\n \"VlocityRecordSourceKey\": \"%vlocity_namespace%__Element__c/%vlocity_namespace%__OmniScript__c/Fixture/IntegrationProcedure/English/Last\"\n}", + "IntegrationProcedure/Fixture_IntegrationProcedure/Fixture_IntegrationProcedure_Element_ParentA.json": "{\n \"%vlocity_namespace%__OmniScriptId__c\": {\n \"Name\": \"ScriptParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"%vlocity_namespace%__OmniScript__c/Fixture/IntegrationProcedure/English\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__OmniScript__c\"\n },\n \"%vlocity_namespace%__PropertySet__c\": {\n \"a\": 1,\n \"z\": 2\n },\n \"Name\": \"ParentA\",\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__Element__c\",\n \"VlocityRecordSourceKey\": \"%vlocity_namespace%__Element__c/%vlocity_namespace%__OmniScript__c/Fixture/IntegrationProcedure/English/ParentA\"\n}", + "IntegrationProcedure/Fixture_IntegrationProcedure/Fixture_IntegrationProcedure_Element_ParentB.json": "{\n \"%vlocity_namespace%__OmniScriptId__c\": {\n \"Name\": \"ScriptParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"%vlocity_namespace%__OmniScript__c/Fixture/IntegrationProcedure/English\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__OmniScript__c\"\n },\n \"%vlocity_namespace%__PropertySet__c\": {\n \"a\": 1,\n \"z\": 2\n },\n \"Name\": \"ParentB\",\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__Element__c\",\n \"VlocityRecordSourceKey\": \"%vlocity_namespace%__Element__c/%vlocity_namespace%__OmniScript__c/Fixture/IntegrationProcedure/English/ParentB\"\n}", + "IntegrationProcedure/Fixture_IntegrationProcedure/Fixture_IntegrationProcedure_PropertySet.json": "{\n \"a\": 1,\n \"z\": [\n {\n \"Name\": \"Z\"\n },\n {\n \"Name\": \"A\"\n }\n ]\n}", + "IntegrationProcedure/Fixture_IntegrationProcedure/Fixture_IntegrationProcedure_SampleInput.json": "{\n \"a\": 1,\n \"z\": 2\n}" + } +} diff --git a/packages/vlocity-deploy/src/__tests__/__fixtures__/exportParity/managed-OmniScript.json b/packages/vlocity-deploy/src/__tests__/__fixtures__/exportParity/managed-OmniScript.json new file mode 100644 index 00000000..06a967ca --- /dev/null +++ b/packages/vlocity-deploy/src/__tests__/__fixtures__/exportParity/managed-OmniScript.json @@ -0,0 +1,122 @@ +{ + "toolsVersion": "1.14.18", + "input": { + "runtime": "managed", + "datapackType": "OmniScript", + "rootId": "a00000000000001AAA", + "rootType": "%vlocity_namespace%__OmniScript__c", + "childType": "%vlocity_namespace%__Element__c", + "references": { + "%vlocity_namespace%__Element__c": { + "%vlocity_namespace%__OmniScriptId__c": "%vlocity_namespace%__OmniScript__c", + "%vlocity_namespace%__ParentElementId__c": "%vlocity_namespace%__Element__c" + } + }, + "matchingKeys": { + "%vlocity_namespace%__OmniScript__c": [ + "%vlocity_namespace%__Type__c", + "%vlocity_namespace%__SubType__c", + "%vlocity_namespace%__Language__c", + "%vlocity_namespace%__Version__c" + ], + "%vlocity_namespace%__Element__c": [ + "%vlocity_namespace%__OmniScriptId__c", + "Name" + ] + }, + "records": [ + { + "Id": "a00000000000001AAA", + "__type": "%vlocity_namespace%__OmniScript__c", + "Name": "ScriptParity", + "%vlocity_namespace%__Type__c": "Fixture", + "%vlocity_namespace%__SubType__c": "OmniScript", + "%vlocity_namespace%__Language__c": "English", + "%vlocity_namespace%__Version__c": 64, + "%vlocity_namespace%__IsProcedure__c": false, + "%vlocity_namespace%__IsActive__c": true, + "%vlocity_namespace%__PropertySet__c": "{\"z\":[{\"Name\":\"Z\"},{\"Name\":\"A\"}],\"a\":1}", + "%vlocity_namespace%__CustomJavaScript__c": "console.log(\"fixture\");", + "%vlocity_namespace%__OmniScriptDefinitions__c": "{\"z\":2,\"a\":1}", + "Description": null + }, + { + "Id": "a01000000000001AAA", + "__type": "%vlocity_namespace%__Element__c", + "Name": "First", + "%vlocity_namespace%__OmniScriptId__c": "a00000000000001AAA", + "%vlocity_namespace%__ParentElementId__c": null, + "%vlocity_namespace%__Order__c": 1, + "%vlocity_namespace%__Level__c": 0, + "%vlocity_namespace%__PropertySet__c": "{\"z\":2,\"a\":1}", + "Empty": null + }, + { + "Id": "a01000000000002AAA", + "__type": "%vlocity_namespace%__Element__c", + "Name": "ChildB", + "%vlocity_namespace%__OmniScriptId__c": "a00000000000001AAA", + "%vlocity_namespace%__ParentElementId__c": "a01000000000005AAA", + "%vlocity_namespace%__Order__c": 1, + "%vlocity_namespace%__Level__c": 1, + "%vlocity_namespace%__PropertySet__c": "{\"z\":2,\"a\":1}", + "Empty": null + }, + { + "Id": "a01000000000003AAA", + "__type": "%vlocity_namespace%__Element__c", + "Name": "ChildA", + "%vlocity_namespace%__OmniScriptId__c": "a00000000000001AAA", + "%vlocity_namespace%__ParentElementId__c": "a01000000000004AAA", + "%vlocity_namespace%__Order__c": 1, + "%vlocity_namespace%__Level__c": 1, + "%vlocity_namespace%__PropertySet__c": "{\"z\":2,\"a\":1}", + "Empty": null + }, + { + "Id": "a01000000000004AAA", + "__type": "%vlocity_namespace%__Element__c", + "Name": "ParentA", + "%vlocity_namespace%__OmniScriptId__c": "a00000000000001AAA", + "%vlocity_namespace%__ParentElementId__c": null, + "%vlocity_namespace%__Order__c": 2, + "%vlocity_namespace%__Level__c": 0, + "%vlocity_namespace%__PropertySet__c": "{\"z\":2,\"a\":1}", + "Empty": null + }, + { + "Id": "a01000000000005AAA", + "__type": "%vlocity_namespace%__Element__c", + "Name": "ParentB", + "%vlocity_namespace%__OmniScriptId__c": "a00000000000001AAA", + "%vlocity_namespace%__ParentElementId__c": null, + "%vlocity_namespace%__Order__c": 3, + "%vlocity_namespace%__Level__c": 0, + "%vlocity_namespace%__PropertySet__c": "{\"z\":2,\"a\":1}", + "Empty": null + }, + { + "Id": "a01000000000006AAA", + "__type": "%vlocity_namespace%__Element__c", + "Name": "Last", + "%vlocity_namespace%__OmniScriptId__c": "a00000000000001AAA", + "%vlocity_namespace%__ParentElementId__c": null, + "%vlocity_namespace%__Order__c": 4, + "%vlocity_namespace%__Level__c": 0, + "%vlocity_namespace%__PropertySet__c": "{\"z\":2,\"a\":1}", + "Empty": null + } + ] + }, + "expectedFiles": { + "OmniScript/Fixture_OmniScript_English/Fixture_OmniScript_English_DataPack.json": "{\n \"%vlocity_namespace%__CustomJavaScript__c\": \"Fixture_OmniScript_English_JavaScript.js\",\n \"%vlocity_namespace%__Element__c\": [\n \"Fixture_OmniScript_English_Element_First.json\",\n \"Fixture_OmniScript_English_Element_ParentA.json\",\n \"Fixture_OmniScript_English_Element_ChildA.json\",\n \"Fixture_OmniScript_English_Element_ParentB.json\",\n \"Fixture_OmniScript_English_Element_ChildB.json\",\n \"Fixture_OmniScript_English_Element_Last.json\"\n ],\n \"%vlocity_namespace%__IsProcedure__c\": false,\n \"%vlocity_namespace%__Language__c\": \"English\",\n \"%vlocity_namespace%__OmniScriptDefinitions__c\": \"{\\\"z\\\":2,\\\"a\\\":1}\",\n \"%vlocity_namespace%__PropertySet__c\": \"Fixture_OmniScript_English_PropertySet.json\",\n \"%vlocity_namespace%__SubType__c\": \"OmniScript\",\n \"%vlocity_namespace%__Type__c\": \"Fixture\",\n \"Name\": \"ScriptParity\",\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__OmniScript__c\",\n \"VlocityRecordSourceKey\": \"%vlocity_namespace%__OmniScript__c/Fixture/OmniScript/English\"\n}", + "OmniScript/Fixture_OmniScript_English/Fixture_OmniScript_English_Element_ChildA.json": "{\n \"%vlocity_namespace%__OmniScriptId__c\": {\n \"Name\": \"ScriptParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"%vlocity_namespace%__OmniScript__c/Fixture/OmniScript/English\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__OmniScript__c\"\n },\n \"%vlocity_namespace%__ParentElementId__c\": {\n \"Name\": \"ParentA\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"%vlocity_namespace%__Element__c/%vlocity_namespace%__OmniScript__c/Fixture/OmniScript/English/ParentA\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__Element__c\"\n },\n \"%vlocity_namespace%__PropertySet__c\": {\n \"a\": 1,\n \"z\": 2\n },\n \"Name\": \"ChildA\",\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__Element__c\",\n \"VlocityRecordSourceKey\": \"%vlocity_namespace%__Element__c/%vlocity_namespace%__OmniScript__c/Fixture/OmniScript/English/ChildA\"\n}", + "OmniScript/Fixture_OmniScript_English/Fixture_OmniScript_English_Element_ChildB.json": "{\n \"%vlocity_namespace%__OmniScriptId__c\": {\n \"Name\": \"ScriptParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"%vlocity_namespace%__OmniScript__c/Fixture/OmniScript/English\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__OmniScript__c\"\n },\n \"%vlocity_namespace%__ParentElementId__c\": {\n \"Name\": \"ParentB\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"%vlocity_namespace%__Element__c/%vlocity_namespace%__OmniScript__c/Fixture/OmniScript/English/ParentB\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__Element__c\"\n },\n \"%vlocity_namespace%__PropertySet__c\": {\n \"a\": 1,\n \"z\": 2\n },\n \"Name\": \"ChildB\",\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__Element__c\",\n \"VlocityRecordSourceKey\": \"%vlocity_namespace%__Element__c/%vlocity_namespace%__OmniScript__c/Fixture/OmniScript/English/ChildB\"\n}", + "OmniScript/Fixture_OmniScript_English/Fixture_OmniScript_English_Element_First.json": "{\n \"%vlocity_namespace%__OmniScriptId__c\": {\n \"Name\": \"ScriptParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"%vlocity_namespace%__OmniScript__c/Fixture/OmniScript/English\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__OmniScript__c\"\n },\n \"%vlocity_namespace%__PropertySet__c\": {\n \"a\": 1,\n \"z\": 2\n },\n \"Name\": \"First\",\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__Element__c\",\n \"VlocityRecordSourceKey\": \"%vlocity_namespace%__Element__c/%vlocity_namespace%__OmniScript__c/Fixture/OmniScript/English/First\"\n}", + "OmniScript/Fixture_OmniScript_English/Fixture_OmniScript_English_Element_Last.json": "{\n \"%vlocity_namespace%__OmniScriptId__c\": {\n \"Name\": \"ScriptParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"%vlocity_namespace%__OmniScript__c/Fixture/OmniScript/English\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__OmniScript__c\"\n },\n \"%vlocity_namespace%__PropertySet__c\": {\n \"a\": 1,\n \"z\": 2\n },\n \"Name\": \"Last\",\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__Element__c\",\n \"VlocityRecordSourceKey\": \"%vlocity_namespace%__Element__c/%vlocity_namespace%__OmniScript__c/Fixture/OmniScript/English/Last\"\n}", + "OmniScript/Fixture_OmniScript_English/Fixture_OmniScript_English_Element_ParentA.json": "{\n \"%vlocity_namespace%__OmniScriptId__c\": {\n \"Name\": \"ScriptParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"%vlocity_namespace%__OmniScript__c/Fixture/OmniScript/English\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__OmniScript__c\"\n },\n \"%vlocity_namespace%__PropertySet__c\": {\n \"a\": 1,\n \"z\": 2\n },\n \"Name\": \"ParentA\",\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__Element__c\",\n \"VlocityRecordSourceKey\": \"%vlocity_namespace%__Element__c/%vlocity_namespace%__OmniScript__c/Fixture/OmniScript/English/ParentA\"\n}", + "OmniScript/Fixture_OmniScript_English/Fixture_OmniScript_English_Element_ParentB.json": "{\n \"%vlocity_namespace%__OmniScriptId__c\": {\n \"Name\": \"ScriptParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"%vlocity_namespace%__OmniScript__c/Fixture/OmniScript/English\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__OmniScript__c\"\n },\n \"%vlocity_namespace%__PropertySet__c\": {\n \"a\": 1,\n \"z\": 2\n },\n \"Name\": \"ParentB\",\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"%vlocity_namespace%__Element__c\",\n \"VlocityRecordSourceKey\": \"%vlocity_namespace%__Element__c/%vlocity_namespace%__OmniScript__c/Fixture/OmniScript/English/ParentB\"\n}", + "OmniScript/Fixture_OmniScript_English/Fixture_OmniScript_English_JavaScript.js": "console.log(\"fixture\");", + "OmniScript/Fixture_OmniScript_English/Fixture_OmniScript_English_PropertySet.json": "{\n \"a\": 1,\n \"z\": [\n {\n \"Name\": \"Z\"\n },\n {\n \"Name\": \"A\"\n }\n ]\n}" + } +} diff --git a/packages/vlocity-deploy/src/__tests__/__fixtures__/exportParity/standard-DataRaptor.json b/packages/vlocity-deploy/src/__tests__/__fixtures__/exportParity/standard-DataRaptor.json new file mode 100644 index 00000000..0bed8407 --- /dev/null +++ b/packages/vlocity-deploy/src/__tests__/__fixtures__/exportParity/standard-DataRaptor.json @@ -0,0 +1,119 @@ +{ + "toolsVersion": "1.14.18", + "input": { + "runtime": "standard", + "datapackType": "DataRaptor", + "rootId": "a00000000000001AAA", + "rootType": "OmniDataTransform", + "childType": "OmniDataTransformItem", + "references": { + "OmniDataTransformItem": { + "OmniDataTransformationId": "OmniDataTransform" + } + }, + "matchingKeys": { + "OmniDataTransform": [ + "Name" + ], + "OmniDataTransformItem": [] + }, + "records": [ + { + "Id": "a00000000000001AAA", + "__type": "OmniDataTransform", + "Name": "MappingParity", + "GlobalKey": "fixture-global-key", + "ExpectedInputJson": "{\"z\":[{\"Name\":\"Z\"},{\"Name\":\"A\"}],\"a\":1}", + "ExpectedOutputXml": "fixture", + "Description": null, + "IsActive": true + }, + { + "Id": "a01000000000001AAA", + "__type": "OmniDataTransformItem", + "Name": "MappingParity", + "OmniDataTransformationId": "a00000000000001AAA", + "GlobalKey": "mapping-1", + "InputObjectQuerySequence": 1, + "FilterGroup": 1, + "OutputCreationSequence": 1, + "OutputObjectName": "Record", + "OutputFieldName": "Z", + "InputFieldName": "inputZ", + "FormulaExpression": null, + "FormulaConverted": null, + "Empty": null + }, + { + "Id": "a01000000000002AAA", + "__type": "OmniDataTransformItem", + "Name": "MappingParity", + "OmniDataTransformationId": "a00000000000001AAA", + "GlobalKey": "mapping-2", + "InputObjectQuerySequence": null, + "FilterGroup": null, + "OutputCreationSequence": null, + "OutputObjectName": null, + "OutputFieldName": null, + "InputFieldName": null, + "FormulaExpression": "Z_FORMULA", + "FormulaConverted": null, + "Empty": null + }, + { + "Id": "a01000000000003AAA", + "__type": "OmniDataTransformItem", + "Name": "MappingParity", + "OmniDataTransformationId": "a00000000000001AAA", + "GlobalKey": "mapping-3", + "InputObjectQuerySequence": 1, + "FilterGroup": 1, + "OutputCreationSequence": 1, + "OutputObjectName": "Record", + "OutputFieldName": "A", + "InputFieldName": "inputA", + "FormulaExpression": null, + "FormulaConverted": null, + "Empty": null + }, + { + "Id": "a01000000000004AAA", + "__type": "OmniDataTransformItem", + "Name": "MappingParity", + "OmniDataTransformationId": "a00000000000001AAA", + "GlobalKey": "mapping-4", + "InputObjectQuerySequence": null, + "FilterGroup": null, + "OutputCreationSequence": null, + "OutputObjectName": null, + "OutputFieldName": null, + "InputFieldName": null, + "FormulaExpression": "A_FORMULA", + "FormulaConverted": null, + "Empty": null + }, + { + "Id": "a01000000000005AAA", + "__type": "OmniDataTransformItem", + "Name": "MappingParity", + "OmniDataTransformationId": "a00000000000001AAA", + "GlobalKey": "mapping-5", + "InputObjectQuerySequence": 0, + "FilterGroup": 1, + "OutputCreationSequence": 1, + "OutputObjectName": "Record", + "OutputFieldName": "Zero", + "InputFieldName": "inputZero", + "FormulaExpression": null, + "FormulaConverted": null, + "Empty": null + } + ] + }, + "expectedFiles": { + "DataRaptor/MappingParity/MappingParity_DataPack.json": "{\n \"Description\": \"\",\n \"ExpectedInputJson\": \"MappingParity_InputJson.json\",\n \"ExpectedOutputXml\": \"MappingParity_TargetOutXml.xml\",\n \"GlobalKey\": \"MappingParity\",\n \"Name\": \"MappingParity\",\n \"OmniDataTransformItem\": \"MappingParity_Items.json\",\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"OmniDataTransform\",\n \"VlocityRecordSourceKey\": \"OmniDataTransform/MappingParity\"\n}", + "DataRaptor/MappingParity/MappingParity_InputJson.json": "{\n \"a\": 1,\n \"z\": [\n {\n \"Name\": \"Z\"\n },\n {\n \"Name\": \"A\"\n }\n ]\n}", + "DataRaptor/MappingParity/MappingParity_Items.json": "[\n {\n \"FilterGroup\": 1,\n \"GlobalKey\": \"mapping-5\",\n \"InputFieldName\": \"inputZero\",\n \"InputObjectQuerySequence\": 0,\n \"Name\": \"MappingParity\",\n \"OmniDataTransformationId\": {\n \"Name\": \"MappingParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"OmniDataTransform/MappingParity\",\n \"VlocityRecordSObjectType\": \"OmniDataTransform\"\n },\n \"OutputCreationSequence\": 1,\n \"OutputFieldName\": \"Zero\",\n \"OutputObjectName\": \"Record\",\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"OmniDataTransformItem\"\n },\n {\n \"FilterGroup\": 1,\n \"GlobalKey\": \"mapping-3\",\n \"InputFieldName\": \"inputA\",\n \"InputObjectQuerySequence\": 1,\n \"Name\": \"MappingParity\",\n \"OmniDataTransformationId\": {\n \"Name\": \"MappingParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"OmniDataTransform/MappingParity\",\n \"VlocityRecordSObjectType\": \"OmniDataTransform\"\n },\n \"OutputCreationSequence\": 1,\n \"OutputFieldName\": \"A\",\n \"OutputObjectName\": \"Record\",\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"OmniDataTransformItem\"\n },\n {\n \"FilterGroup\": 1,\n \"GlobalKey\": \"mapping-1\",\n \"InputFieldName\": \"inputZ\",\n \"InputObjectQuerySequence\": 1,\n \"Name\": \"MappingParity\",\n \"OmniDataTransformationId\": {\n \"Name\": \"MappingParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"OmniDataTransform/MappingParity\",\n \"VlocityRecordSObjectType\": \"OmniDataTransform\"\n },\n \"OutputCreationSequence\": 1,\n \"OutputFieldName\": \"Z\",\n \"OutputObjectName\": \"Record\",\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"OmniDataTransformItem\"\n },\n {\n \"FormulaExpression\": \"A_FORMULA\",\n \"GlobalKey\": \"mapping-4\",\n \"Name\": \"MappingParity\",\n \"OmniDataTransformationId\": {\n \"Name\": \"MappingParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"OmniDataTransform/MappingParity\",\n \"VlocityRecordSObjectType\": \"OmniDataTransform\"\n },\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"OmniDataTransformItem\"\n },\n {\n \"FormulaExpression\": \"Z_FORMULA\",\n \"GlobalKey\": \"mapping-2\",\n \"Name\": \"MappingParity\",\n \"OmniDataTransformationId\": {\n \"Name\": \"MappingParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"OmniDataTransform/MappingParity\",\n \"VlocityRecordSObjectType\": \"OmniDataTransform\"\n },\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"OmniDataTransformItem\"\n }\n]", + "DataRaptor/MappingParity/MappingParity_TargetOutXml.xml": "fixture" + } +} diff --git a/packages/vlocity-deploy/src/__tests__/__fixtures__/exportParity/standard-IntegrationProcedure.json b/packages/vlocity-deploy/src/__tests__/__fixtures__/exportParity/standard-IntegrationProcedure.json new file mode 100644 index 00000000..2a2091df --- /dev/null +++ b/packages/vlocity-deploy/src/__tests__/__fixtures__/exportParity/standard-IntegrationProcedure.json @@ -0,0 +1,122 @@ +{ + "toolsVersion": "1.14.18", + "input": { + "runtime": "standard", + "datapackType": "IntegrationProcedure", + "rootId": "a00000000000001AAA", + "rootType": "OmniProcess", + "childType": "OmniProcessElement", + "references": { + "OmniProcessElement": { + "OmniProcessId": "OmniProcess", + "ParentElementId": "OmniProcessElement" + } + }, + "matchingKeys": { + "OmniProcess": [ + "Type", + "SubType", + "Language", + "VersionNumber" + ], + "OmniProcessElement": [ + "OmniProcessId", + "Name" + ] + }, + "records": [ + { + "Id": "a00000000000001AAA", + "__type": "OmniProcess", + "Name": "ScriptParity", + "Type": "Fixture", + "SubType": "IntegrationProcedure", + "Language": "English", + "VersionNumber": 64, + "IsIntegrationProcedure": true, + "IsActive": true, + "PropertySetConfig": "{\"z\":[{\"Name\":\"Z\"},{\"Name\":\"A\"}],\"a\":1}", + "CustomJavaScript": "{\"z\":2,\"a\":1}", + "ElementTypeComponentMapping": "{\"z\":2,\"a\":1}", + "Description": null + }, + { + "Id": "a01000000000001AAA", + "__type": "OmniProcessElement", + "Name": "First", + "OmniProcessId": "a00000000000001AAA", + "ParentElementId": null, + "SequenceNumber": 1, + "Level": 0, + "PropertySetConfig": "{\"z\":2,\"a\":1}", + "Empty": null + }, + { + "Id": "a01000000000002AAA", + "__type": "OmniProcessElement", + "Name": "ChildB", + "OmniProcessId": "a00000000000001AAA", + "ParentElementId": "a01000000000005AAA", + "SequenceNumber": 1, + "Level": 1, + "PropertySetConfig": "{\"z\":2,\"a\":1}", + "Empty": null + }, + { + "Id": "a01000000000003AAA", + "__type": "OmniProcessElement", + "Name": "ChildA", + "OmniProcessId": "a00000000000001AAA", + "ParentElementId": "a01000000000004AAA", + "SequenceNumber": 1, + "Level": 1, + "PropertySetConfig": "{\"z\":2,\"a\":1}", + "Empty": null + }, + { + "Id": "a01000000000004AAA", + "__type": "OmniProcessElement", + "Name": "ParentA", + "OmniProcessId": "a00000000000001AAA", + "ParentElementId": null, + "SequenceNumber": 2, + "Level": 0, + "PropertySetConfig": "{\"z\":2,\"a\":1}", + "Empty": null + }, + { + "Id": "a01000000000005AAA", + "__type": "OmniProcessElement", + "Name": "ParentB", + "OmniProcessId": "a00000000000001AAA", + "ParentElementId": null, + "SequenceNumber": 3, + "Level": 0, + "PropertySetConfig": "{\"z\":2,\"a\":1}", + "Empty": null + }, + { + "Id": "a01000000000006AAA", + "__type": "OmniProcessElement", + "Name": "Last", + "OmniProcessId": "a00000000000001AAA", + "ParentElementId": null, + "SequenceNumber": 4, + "Level": 0, + "PropertySetConfig": "{\"z\":2,\"a\":1}", + "Empty": null + } + ] + }, + "expectedFiles": { + "IntegrationProcedure/Fixture_IntegrationProcedure/Fixture_IntegrationProcedure_DataPack.json": "{\n \"CustomJavaScript\": \"Fixture_IntegrationProcedure_SampleInput.json\",\n \"ElementTypeComponentMapping\": \"{\\\"z\\\":2,\\\"a\\\":1}\",\n \"IsIntegrationProcedure\": true,\n \"Language\": \"English\",\n \"Name\": \"ScriptParity\",\n \"OmniProcessElement\": [\n \"Fixture_IntegrationProcedure_Element_First.json\",\n \"Fixture_IntegrationProcedure_Element_ParentA.json\",\n \"Fixture_IntegrationProcedure_Element_ChildA.json\",\n \"Fixture_IntegrationProcedure_Element_ParentB.json\",\n \"Fixture_IntegrationProcedure_Element_ChildB.json\",\n \"Fixture_IntegrationProcedure_Element_Last.json\"\n ],\n \"PropertySetConfig\": \"Fixture_IntegrationProcedure_PropertySetConfig.json\",\n \"SubType\": \"IntegrationProcedure\",\n \"Type\": \"Fixture\",\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"OmniProcess\",\n \"VlocityRecordSourceKey\": \"OmniProcess/Fixture/IntegrationProcedure/English\"\n}", + "IntegrationProcedure/Fixture_IntegrationProcedure/Fixture_IntegrationProcedure_Element_ChildA.json": "{\n \"Name\": \"ChildA\",\n \"OmniProcessId\": {\n \"Name\": \"ScriptParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"OmniProcess/Fixture/IntegrationProcedure/English\",\n \"VlocityRecordSObjectType\": \"OmniProcess\"\n },\n \"ParentElementId\": {\n \"Name\": \"ParentA\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"OmniProcessElement/OmniProcess/Fixture/IntegrationProcedure/English/ParentA\",\n \"VlocityRecordSObjectType\": \"OmniProcessElement\"\n },\n \"PropertySetConfig\": {\n \"a\": 1,\n \"z\": 2\n },\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"OmniProcessElement\",\n \"VlocityRecordSourceKey\": \"OmniProcessElement/OmniProcess/Fixture/IntegrationProcedure/English/ChildA\"\n}", + "IntegrationProcedure/Fixture_IntegrationProcedure/Fixture_IntegrationProcedure_Element_ChildB.json": "{\n \"Name\": \"ChildB\",\n \"OmniProcessId\": {\n \"Name\": \"ScriptParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"OmniProcess/Fixture/IntegrationProcedure/English\",\n \"VlocityRecordSObjectType\": \"OmniProcess\"\n },\n \"ParentElementId\": {\n \"Name\": \"ParentB\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"OmniProcessElement/OmniProcess/Fixture/IntegrationProcedure/English/ParentB\",\n \"VlocityRecordSObjectType\": \"OmniProcessElement\"\n },\n \"PropertySetConfig\": {\n \"a\": 1,\n \"z\": 2\n },\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"OmniProcessElement\",\n \"VlocityRecordSourceKey\": \"OmniProcessElement/OmniProcess/Fixture/IntegrationProcedure/English/ChildB\"\n}", + "IntegrationProcedure/Fixture_IntegrationProcedure/Fixture_IntegrationProcedure_Element_First.json": "{\n \"Name\": \"First\",\n \"OmniProcessId\": {\n \"Name\": \"ScriptParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"OmniProcess/Fixture/IntegrationProcedure/English\",\n \"VlocityRecordSObjectType\": \"OmniProcess\"\n },\n \"PropertySetConfig\": {\n \"a\": 1,\n \"z\": 2\n },\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"OmniProcessElement\",\n \"VlocityRecordSourceKey\": \"OmniProcessElement/OmniProcess/Fixture/IntegrationProcedure/English/First\"\n}", + "IntegrationProcedure/Fixture_IntegrationProcedure/Fixture_IntegrationProcedure_Element_Last.json": "{\n \"Name\": \"Last\",\n \"OmniProcessId\": {\n \"Name\": \"ScriptParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"OmniProcess/Fixture/IntegrationProcedure/English\",\n \"VlocityRecordSObjectType\": \"OmniProcess\"\n },\n \"PropertySetConfig\": {\n \"a\": 1,\n \"z\": 2\n },\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"OmniProcessElement\",\n \"VlocityRecordSourceKey\": \"OmniProcessElement/OmniProcess/Fixture/IntegrationProcedure/English/Last\"\n}", + "IntegrationProcedure/Fixture_IntegrationProcedure/Fixture_IntegrationProcedure_Element_ParentA.json": "{\n \"Name\": \"ParentA\",\n \"OmniProcessId\": {\n \"Name\": \"ScriptParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"OmniProcess/Fixture/IntegrationProcedure/English\",\n \"VlocityRecordSObjectType\": \"OmniProcess\"\n },\n \"PropertySetConfig\": {\n \"a\": 1,\n \"z\": 2\n },\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"OmniProcessElement\",\n \"VlocityRecordSourceKey\": \"OmniProcessElement/OmniProcess/Fixture/IntegrationProcedure/English/ParentA\"\n}", + "IntegrationProcedure/Fixture_IntegrationProcedure/Fixture_IntegrationProcedure_Element_ParentB.json": "{\n \"Name\": \"ParentB\",\n \"OmniProcessId\": {\n \"Name\": \"ScriptParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"OmniProcess/Fixture/IntegrationProcedure/English\",\n \"VlocityRecordSObjectType\": \"OmniProcess\"\n },\n \"PropertySetConfig\": {\n \"a\": 1,\n \"z\": 2\n },\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"OmniProcessElement\",\n \"VlocityRecordSourceKey\": \"OmniProcessElement/OmniProcess/Fixture/IntegrationProcedure/English/ParentB\"\n}", + "IntegrationProcedure/Fixture_IntegrationProcedure/Fixture_IntegrationProcedure_PropertySetConfig.json": "{\n \"a\": 1,\n \"z\": [\n {\n \"Name\": \"Z\"\n },\n {\n \"Name\": \"A\"\n }\n ]\n}", + "IntegrationProcedure/Fixture_IntegrationProcedure/Fixture_IntegrationProcedure_SampleInput.json": "{\n \"a\": 1,\n \"z\": 2\n}" + } +} diff --git a/packages/vlocity-deploy/src/__tests__/__fixtures__/exportParity/standard-OmniScript.json b/packages/vlocity-deploy/src/__tests__/__fixtures__/exportParity/standard-OmniScript.json new file mode 100644 index 00000000..c0f6d3d1 --- /dev/null +++ b/packages/vlocity-deploy/src/__tests__/__fixtures__/exportParity/standard-OmniScript.json @@ -0,0 +1,122 @@ +{ + "toolsVersion": "1.14.18", + "input": { + "runtime": "standard", + "datapackType": "OmniScript", + "rootId": "a00000000000001AAA", + "rootType": "OmniProcess", + "childType": "OmniProcessElement", + "references": { + "OmniProcessElement": { + "OmniProcessId": "OmniProcess", + "ParentElementId": "OmniProcessElement" + } + }, + "matchingKeys": { + "OmniProcess": [ + "Type", + "SubType", + "Language", + "VersionNumber" + ], + "OmniProcessElement": [ + "OmniProcessId", + "Name" + ] + }, + "records": [ + { + "Id": "a00000000000001AAA", + "__type": "OmniProcess", + "Name": "ScriptParity", + "Type": "Fixture", + "SubType": "OmniScript", + "Language": "English", + "VersionNumber": 64, + "IsIntegrationProcedure": false, + "IsActive": true, + "PropertySetConfig": "{\"z\":[{\"Name\":\"Z\"},{\"Name\":\"A\"}],\"a\":1}", + "CustomJavaScript": "console.log(\"fixture\");", + "ElementTypeComponentMapping": "{\"z\":2,\"a\":1}", + "Description": null + }, + { + "Id": "a01000000000001AAA", + "__type": "OmniProcessElement", + "Name": "First", + "OmniProcessId": "a00000000000001AAA", + "ParentElementId": null, + "SequenceNumber": 1, + "Level": 0, + "PropertySetConfig": "{\"z\":2,\"a\":1}", + "Empty": null + }, + { + "Id": "a01000000000002AAA", + "__type": "OmniProcessElement", + "Name": "ChildB", + "OmniProcessId": "a00000000000001AAA", + "ParentElementId": "a01000000000005AAA", + "SequenceNumber": 1, + "Level": 1, + "PropertySetConfig": "{\"z\":2,\"a\":1}", + "Empty": null + }, + { + "Id": "a01000000000003AAA", + "__type": "OmniProcessElement", + "Name": "ChildA", + "OmniProcessId": "a00000000000001AAA", + "ParentElementId": "a01000000000004AAA", + "SequenceNumber": 1, + "Level": 1, + "PropertySetConfig": "{\"z\":2,\"a\":1}", + "Empty": null + }, + { + "Id": "a01000000000004AAA", + "__type": "OmniProcessElement", + "Name": "ParentA", + "OmniProcessId": "a00000000000001AAA", + "ParentElementId": null, + "SequenceNumber": 2, + "Level": 0, + "PropertySetConfig": "{\"z\":2,\"a\":1}", + "Empty": null + }, + { + "Id": "a01000000000005AAA", + "__type": "OmniProcessElement", + "Name": "ParentB", + "OmniProcessId": "a00000000000001AAA", + "ParentElementId": null, + "SequenceNumber": 3, + "Level": 0, + "PropertySetConfig": "{\"z\":2,\"a\":1}", + "Empty": null + }, + { + "Id": "a01000000000006AAA", + "__type": "OmniProcessElement", + "Name": "Last", + "OmniProcessId": "a00000000000001AAA", + "ParentElementId": null, + "SequenceNumber": 4, + "Level": 0, + "PropertySetConfig": "{\"z\":2,\"a\":1}", + "Empty": null + } + ] + }, + "expectedFiles": { + "OmniScript/Fixture_OmniScript_English/Fixture_OmniScript_English_DataPack.json": "{\n \"CustomJavaScript\": \"Fixture_OmniScript_English_JavaScript.js\",\n \"ElementTypeComponentMapping\": \"{\\\"z\\\":2,\\\"a\\\":1}\",\n \"IsIntegrationProcedure\": false,\n \"Language\": \"English\",\n \"Name\": \"ScriptParity\",\n \"OmniProcessElement\": [\n \"Fixture_OmniScript_English_Element_First.json\",\n \"Fixture_OmniScript_English_Element_ParentA.json\",\n \"Fixture_OmniScript_English_Element_ChildA.json\",\n \"Fixture_OmniScript_English_Element_ParentB.json\",\n \"Fixture_OmniScript_English_Element_ChildB.json\",\n \"Fixture_OmniScript_English_Element_Last.json\"\n ],\n \"PropertySetConfig\": \"Fixture_OmniScript_English_PropertySetConfig.json\",\n \"SubType\": \"OmniScript\",\n \"Type\": \"Fixture\",\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"OmniProcess\",\n \"VlocityRecordSourceKey\": \"OmniProcess/Fixture/OmniScript/English\"\n}", + "OmniScript/Fixture_OmniScript_English/Fixture_OmniScript_English_Element_ChildA.json": "{\n \"Name\": \"ChildA\",\n \"OmniProcessId\": {\n \"Name\": \"ScriptParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"OmniProcess/Fixture/OmniScript/English\",\n \"VlocityRecordSObjectType\": \"OmniProcess\"\n },\n \"ParentElementId\": {\n \"Name\": \"ParentA\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"OmniProcessElement/OmniProcess/Fixture/OmniScript/English/ParentA\",\n \"VlocityRecordSObjectType\": \"OmniProcessElement\"\n },\n \"PropertySetConfig\": {\n \"a\": 1,\n \"z\": 2\n },\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"OmniProcessElement\",\n \"VlocityRecordSourceKey\": \"OmniProcessElement/OmniProcess/Fixture/OmniScript/English/ChildA\"\n}", + "OmniScript/Fixture_OmniScript_English/Fixture_OmniScript_English_Element_ChildB.json": "{\n \"Name\": \"ChildB\",\n \"OmniProcessId\": {\n \"Name\": \"ScriptParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"OmniProcess/Fixture/OmniScript/English\",\n \"VlocityRecordSObjectType\": \"OmniProcess\"\n },\n \"ParentElementId\": {\n \"Name\": \"ParentB\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"OmniProcessElement/OmniProcess/Fixture/OmniScript/English/ParentB\",\n \"VlocityRecordSObjectType\": \"OmniProcessElement\"\n },\n \"PropertySetConfig\": {\n \"a\": 1,\n \"z\": 2\n },\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"OmniProcessElement\",\n \"VlocityRecordSourceKey\": \"OmniProcessElement/OmniProcess/Fixture/OmniScript/English/ChildB\"\n}", + "OmniScript/Fixture_OmniScript_English/Fixture_OmniScript_English_Element_First.json": "{\n \"Name\": \"First\",\n \"OmniProcessId\": {\n \"Name\": \"ScriptParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"OmniProcess/Fixture/OmniScript/English\",\n \"VlocityRecordSObjectType\": \"OmniProcess\"\n },\n \"PropertySetConfig\": {\n \"a\": 1,\n \"z\": 2\n },\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"OmniProcessElement\",\n \"VlocityRecordSourceKey\": \"OmniProcessElement/OmniProcess/Fixture/OmniScript/English/First\"\n}", + "OmniScript/Fixture_OmniScript_English/Fixture_OmniScript_English_Element_Last.json": "{\n \"Name\": \"Last\",\n \"OmniProcessId\": {\n \"Name\": \"ScriptParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"OmniProcess/Fixture/OmniScript/English\",\n \"VlocityRecordSObjectType\": \"OmniProcess\"\n },\n \"PropertySetConfig\": {\n \"a\": 1,\n \"z\": 2\n },\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"OmniProcessElement\",\n \"VlocityRecordSourceKey\": \"OmniProcessElement/OmniProcess/Fixture/OmniScript/English/Last\"\n}", + "OmniScript/Fixture_OmniScript_English/Fixture_OmniScript_English_Element_ParentA.json": "{\n \"Name\": \"ParentA\",\n \"OmniProcessId\": {\n \"Name\": \"ScriptParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"OmniProcess/Fixture/OmniScript/English\",\n \"VlocityRecordSObjectType\": \"OmniProcess\"\n },\n \"PropertySetConfig\": {\n \"a\": 1,\n \"z\": 2\n },\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"OmniProcessElement\",\n \"VlocityRecordSourceKey\": \"OmniProcessElement/OmniProcess/Fixture/OmniScript/English/ParentA\"\n}", + "OmniScript/Fixture_OmniScript_English/Fixture_OmniScript_English_Element_ParentB.json": "{\n \"Name\": \"ParentB\",\n \"OmniProcessId\": {\n \"Name\": \"ScriptParity\",\n \"VlocityDataPackType\": \"VlocityMatchingKeyObject\",\n \"VlocityMatchingRecordSourceKey\": \"OmniProcess/Fixture/OmniScript/English\",\n \"VlocityRecordSObjectType\": \"OmniProcess\"\n },\n \"PropertySetConfig\": {\n \"a\": 1,\n \"z\": 2\n },\n \"VlocityDataPackType\": \"SObject\",\n \"VlocityRecordSObjectType\": \"OmniProcessElement\",\n \"VlocityRecordSourceKey\": \"OmniProcessElement/OmniProcess/Fixture/OmniScript/English/ParentB\"\n}", + "OmniScript/Fixture_OmniScript_English/Fixture_OmniScript_English_JavaScript.js": "console.log(\"fixture\");", + "OmniScript/Fixture_OmniScript_English/Fixture_OmniScript_English_PropertySetConfig.json": "{\n \"a\": 1,\n \"z\": [\n {\n \"Name\": \"Z\"\n },\n {\n \"Name\": \"A\"\n }\n ]\n}" + } +} diff --git a/packages/vlocity-deploy/src/__tests__/datapackExportKeys.test.ts b/packages/vlocity-deploy/src/__tests__/datapackExportKeys.test.ts new file mode 100644 index 00000000..89285e8f --- /dev/null +++ b/packages/vlocity-deploy/src/__tests__/datapackExportKeys.test.ts @@ -0,0 +1,95 @@ +import 'jest'; +import { Logger } from '@vlocode/core'; +import { applyExportKeys } from '../export/datapackExportKeys'; +import { DatapackExpander } from '../export/datapackExpander'; +import { DatapackExportDefinitionStore } from '../export/exportDefinitionStore'; +import { DatapackExportDefinitions } from '../exportDefinitions'; + +const sobject = (type: string, fields: Record) => ({ + VlocityDataPackType: 'SObject' as const, VlocityRecordSObjectType: type, VlocityRecordSourceKey: `${type}/Example`, ...fields +}); + +describe('Datapack export keys', () => { + it('writes field-list keys for records and references without changing input or matching fields', () => { + const definitions = new DatapackExportDefinitionStore(); + definitions.load({ + Root: { objectType: 'Root', name: ['Name'], exportKey: ['Name'], ignoreFields: ['Version'] }, + Child: { objectType: 'Child', name: ['Name'], exportKey: ['Parent', 'Name'] }, + External: { objectType: 'External', name: ['Name'], exportKey: ['Name'] } + }); + const parent = { VlocityDataPackType: 'VlocityMatchingKeyObject', VlocityMatchingRecordSourceKey: 'Root/Example/64' }; + const input = sobject('Root', { Name: 'Example', Version: 64, VlocityRecordSourceKey: 'Root/Example/64', + Children: [sobject('Child', { Name: 'Action', Parent: parent, VlocityRecordSourceKey: 'Child/Root/Example/64/Action' })], + External: { VlocityDataPackType: 'VlocityLookupMatchingKeyObject', VlocityRecordSObjectType: 'External', VlocityLookupRecordSourceKey: 'External/Other/12', Name: 'Other', Version: 12 } + }); + const original = JSON.stringify(input); + const result = new DatapackExpander(definitions, Logger.null).expandDatapack(input, { datapackType: 'Root' }); + const root = JSON.parse(result.files['Example_DataPack.json'].toString()); + expect(root.VlocityRecordSourceKey).toBe('Root/Example'); + expect(root.Children[0].VlocityRecordSourceKey).toBe('Child/Root/Example/Action'); + expect(root.Children[0].Parent.VlocityMatchingRecordSourceKey).toBe(root.VlocityRecordSourceKey); + expect(root.External).toEqual({ ...input.External, VlocityLookupRecordSourceKey: 'External/Other' }); + expect(result.parentKeys).toEqual(['External/Other']); + expect(JSON.stringify(input)).toBe(original); + }); + + it.each(['standard', 'managed'])('uses the referenced %s script export key even when only the dependent datapack is expanded', runtime => { + const managed = runtime === 'managed'; + const config = DatapackExportDefinitions[managed ? 'omniStudioManaged' : 'omniStudioStandard']; + const definitions = new DatapackExportDefinitionStore(); + definitions.load(config.definitions, { scope: config.id }); + const field = (native: string, custom: string) => managed ? `%vlocity_namespace%__${custom}__c` : native; + const type = config.definitions.OmniScript.objectType; + const parent = { + VlocityDataPackType: 'VlocityLookupMatchingKeyObject', VlocityRecordSObjectType: type, + VlocityLookupRecordSourceKey: `${type}/Fixture/Parent/English/64`, + [field('Type', 'Type')]: 'Fixture', [field('SubType', 'SubType')]: 'Parent', + [field('Language', 'Language')]: 'English', [field('VersionNumber', 'Version')]: 64 + }; + const input = sobject('Dependent', { Name: 'Dependent', Parent: parent }); + const result = new DatapackExpander(definitions, Logger.null).expandDatapack(input, { scope: config.id }); + const root = JSON.parse(Object.values(result.files)[0].toString()); + expect(root.Parent).toEqual({ ...parent, VlocityLookupRecordSourceKey: `${type}/Fixture/Parent/English` }); + expect(result.parentKeys).toEqual([`${type}/Fixture/Parent/English`]); + expect(parent.VlocityLookupRecordSourceKey).toBe(`${type}/Fixture/Parent/English/64`); + }); + + it('resolves export-key lookup fields through nested references, retaining the matching fields', () => { + const definitions = new DatapackExportDefinitionStore(); + definitions.load({ + Parent: { objectType: 'Parent', name: ['Name'], exportKey: ['Name'] }, + Child: { objectType: 'Child', name: ['Name'], exportKey: ['Parent', 'Name'] } + }); + const parent = { VlocityDataPackType: 'VlocityLookupMatchingKeyObject', VlocityRecordSObjectType: 'Parent', + VlocityLookupRecordSourceKey: 'Parent/Example/64', Name: 'Example', Version: 64 }; + const child = { VlocityDataPackType: 'VlocityLookupMatchingKeyObject', VlocityRecordSObjectType: 'Child', + VlocityLookupRecordSourceKey: 'Child/Parent/Example/64/Action', Name: 'Action', Parent: parent }; + const output = applyExportKeys(sobject('Root', { Child: child }), { objectType: 'Root' }, definitions); + expect(output.Child.VlocityLookupRecordSourceKey).toBe('Child/Parent/Example/Action'); + expect(output.Child.Parent).toEqual({ ...parent, VlocityLookupRecordSourceKey: 'Parent/Example' }); + }); + + it('retains keys of references without an exportKey definition', () => { + const definitions = new DatapackExportDefinitionStore(); + const reference = { VlocityDataPackType: 'VlocityLookupMatchingKeyObject', VlocityRecordSObjectType: 'External', + VlocityLookupRecordSourceKey: 'External/Example/64', Name: 'Example', Version: 64 }; + const input = sobject('Root', { External: reference }); + expect(applyExportKeys(input, { objectType: 'Root' }, definitions)).toEqual(input); + }); + + it('resolves namespace-qualified fields and preserves ordered empty, zero and false components', () => { + const definitions = new DatapackExportDefinitionStore(); + definitions.load({ Root: { objectType: 'Root', name: ['Name'], exportKey: ['%vlocity_namespace%__Code__c', 'Empty', 'Zero', 'False'] } }); + const input = sobject('Root', { vlocity_cmt__Code__c: 'Example', Empty: null, Zero: 0, False: false }); + expect(applyExportKeys(input, { objectType: 'Root' }, definitions).VlocityRecordSourceKey).toBe('Root/Example//0/false'); + }); + + it('allows source versions to share a configured serialized export key', () => { + const definitions = new DatapackExportDefinitionStore(); + definitions.load({ Child: { objectType: 'Child', name: ['Name'], exportKey: ['Name'] } }); + const input = sobject('Root', { Children: [1, 2].map(version => sobject('Child', { Name: 'Same', VlocityRecordSourceKey: `Child/Same/${version}` })) }); + const output = applyExportKeys(input, { objectType: 'Root' }, definitions); + expect(output.Children.map((child: any) => child.VlocityRecordSourceKey)).toEqual(['Child/Same', 'Child/Same']); + }); + +}); diff --git a/packages/vlocity-deploy/src/__tests__/datapackExporter.test.ts b/packages/vlocity-deploy/src/__tests__/datapackExporter.test.ts index 2c5c5944..ae5acc87 100644 --- a/packages/vlocity-deploy/src/__tests__/datapackExporter.test.ts +++ b/packages/vlocity-deploy/src/__tests__/datapackExporter.test.ts @@ -1,7 +1,9 @@ import 'jest'; import * as path from 'path'; +import * as fs from 'fs'; import { VlocityNamespaceService } from '@vlocode/vlocity'; +import { DatapackLookupService } from '../datapackLookupService'; import { DatapackExporter } from '../export/datapackExporter'; import { DatapackExpander } from '../export/datapackExpander'; import { DatapackExportDefinitionStore } from '../export/exportDefinitionStore'; @@ -51,6 +53,9 @@ describe('DatapackExporter', () => { const definitions = options?.definitions ?? { getEmbeddedObjects: jest.fn((item: any) => options?.embeddedObjects?.(item) ?? []), getDatapackTypes: jest.fn(() => []), + get: jest.fn(), + getExportKey: jest.fn(), + getFieldConfig: jest.fn(), getFieldsWith: jest.fn(() => []), isEmbeddedObject: jest.fn(() => false), isFieldIgnored: jest.fn(() => false) @@ -193,7 +198,6 @@ describe('DatapackExporter', () => { const [embeddedObject] = definitions.getEmbeddedObjects(datapack); expect(embeddedObject.sortFields).toEqual([ - '%vlocity_namespace%__Level__c', '%vlocity_namespace%__Order__c' ]); expect(exporter.resolveEmbeddedLookup(datapack, embeddedObject)).toEqual({ @@ -1072,7 +1076,8 @@ describe('DatapackExporter', () => { childRelationships: [] }; const definitions = { - getDatapackTypes: jest.fn(() => []) + getDatapackTypes: jest.fn(() => []), + getExportKey: jest.fn() }; const salesforce = { data: { @@ -1160,7 +1165,7 @@ describe('DatapackExporter', () => { VlocityRecordSObjectType: 'OmniProcess', VlocityRecordSourceKey: 'OmniProcess/Test' }; - exporter.exportObject = jest.fn(async () => [{ + exporter.exportRawObjects = jest.fn(async () => [{ datapack, objectType: 'OmniProcess', parentKeys: [], @@ -1178,7 +1183,7 @@ describe('DatapackExporter', () => { VlocityRecordSObjectType: 'OmniProcess', VlocityRecordSourceKey: 'OmniProcess/Test' }; - exporter.exportObject = jest.fn(async () => [{ + exporter.exportRawObjects = jest.fn(async () => [{ datapack, objectType: 'OmniProcess', parentKeys: [], @@ -1253,6 +1258,9 @@ describe('DatapackExporter', () => { const definitions = { getEmbeddedObjects: jest.fn(() => []), getDatapackTypes: jest.fn(() => []), + get: jest.fn(), + getExportKey: jest.fn(), + getFieldConfig: jest.fn(), getFieldsWith: jest.fn(() => []), isEmbeddedObject: jest.fn(() => false), isFieldIgnored: jest.fn(() => false) @@ -1374,6 +1382,9 @@ describe('DatapackExporter', () => { }] : []), getDatapackTypes: jest.fn(() => []), + get: jest.fn(), + getExportKey: jest.fn(), + getFieldConfig: jest.fn(), getFieldsWith: jest.fn(() => []), isEmbeddedObject: jest.fn(() => false), isFieldIgnored: jest.fn(() => false) @@ -1488,6 +1499,276 @@ describe('DatapackExporter', () => { expect(datapack.data).not.toHaveProperty('ManagerId'); }); + + describe.each(['omniStudioStandard', 'omniStudioManaged'] as const)('%s export compatibility', runtime => { + function fixture(datapackType = 'IntegrationProcedure') { + const config = DatapackExportDefinitions[runtime]; + const definitions = new DatapackExportDefinitionStore(); + definitions.load(config.definitions, { scope: config.id }); + const managed = runtime === 'omniStudioManaged'; + const field = (standard: string, custom: string) => managed ? `%vlocity_namespace%__${custom}__c` : standard; + const objectType = config.definitions[datapackType].objectType; + const elementType = field('OmniProcessElement', 'Element'); + const typeField = field('Type', 'Type'); + const subtype = field('SubType', 'SubType'); + const language = field('Language', 'Language'); + const version = field('VersionNumber', 'Version'); + const order = field('SequenceNumber', 'Order'); + const parent = field('ParentElementId', 'ParentElementId'); + const owner = field('OmniProcessId', 'OmniScriptId'); + const propertySet = field('PropertySetConfig', 'PropertySet'); + const script = field('CustomJavaScript', 'CustomJavaScript'); + const rootId = 'a00000000000001AAA'; + const ids = ['a01000000000001AAA', 'a01000000000002AAA', 'a01000000000003AAA']; + const records = [ + { Id: rootId, __type: objectType, Name: 'Example', [typeField]: 'Type', [subtype]: 'Sub', + [language]: 'English', [version]: 64, [script]: datapackType === 'IntegrationProcedure' ? '{"z":2,"a":1}' : 'console.log("example");', + [propertySet]: '{"z":[{"Name":"Z"},{"Name":"A"}],"a":1}', JsonText: '{"z":2,"a":1}', Empty: null }, + { Id: ids[0], __type: elementType, Name: 'Second', [owner]: rootId, [order]: 2, [parent]: null }, + { Id: ids[1], __type: elementType, Name: 'Child', [owner]: rootId, [order]: 1, [parent]: ids[0] }, + { Id: ids[2], __type: elementType, Name: 'First', [owner]: rootId, [order]: 1, [parent]: null } + ]; + const describes = Object.fromEntries([objectType, elementType].map(name => [name, { + name, + fields: [...new Set(records.filter(r => r.__type === name).flatMap(Object.keys))] + .filter(key => key !== '__type') + .map(key => ({ name: key, referenceTo: key === owner ? [objectType] : key === parent ? [elementType] : [] })), + childRelationships: [] + }])); + const setup = createExporter({ definitions, records, describes, matchingKeyFieldsByType: { + [objectType]: [typeField, subtype, language, version], [elementType]: [owner, 'Name'] + } }); + setup.exporter.expander = new DatapackExpander(definitions, createLogger() as any); + return { ...setup, definitions, rootId, objectType, elementType, order, owner, parent, version, propertySet, script, + context: { scope: config.id, datapackType, maxDepth: 0, failOnError: true } }; + } + + it.each(['IntegrationProcedure', 'OmniScript'])('exports %s with legacy identity, hierarchy and serialization', async datapackType => { + const f = fixture(datapackType); + const [result] = await f.exporter.exportObjectAndExpand(f.rootId, f.context); + const files = result.files as Record; + const root = JSON.parse(files[Object.keys(files).find(name => name.endsWith('_DataPack.json'))!].toString()); + expect(root.VlocityRecordSourceKey).toBe(`${f.objectType}/Type/Sub/English`); + expect(root).not.toHaveProperty(f.version); + expect(root).not.toHaveProperty('Empty'); + expect(root.JsonText).toBe('{"z":2,"a":1}'); + const elements = root[f.elementType].map((name: string) => JSON.parse(files[name].toString())); + expect(elements.map((element: any) => element.Name)).toEqual(['First', 'Second', 'Child']); + expect(elements.every((element: any) => !(f.order in element))).toBe(true); + expect(elements[2][f.parent].VlocityMatchingRecordSourceKey).toBe(elements[1].VlocityRecordSourceKey); + expect(elements[2][f.owner].VlocityMatchingRecordSourceKey).toBe(root.VlocityRecordSourceKey); + const properties = JSON.parse(files[root[f.propertySet]].toString()); + expect(Object.keys(properties)).toEqual(['a', 'z']); + expect(properties.z.map((value: any) => value.Name)).toEqual(['Z', 'A']); + if (datapackType === 'IntegrationProcedure') { + expect(root[f.script]).toMatch(/_SampleInput.json$/); + expect(JSON.parse(files[root[f.script]].toString())).toEqual({ a: 1, z: 2 }); + } else { + expect(root[f.script]).toMatch(/_JavaScript.js$/); + expect(files[root[f.script]].toString()).toBe('console.log("example");'); + } + expect(f.definitions.getMatchingKeyFields({ objectType: f.objectType, scope: f.context.scope })).toContain(f.version); + }); + + it('applies the same hierarchy and field exclusions to consolidated and streamed exports', async () => { + const f = fixture(); + const [result] = await f.exporter.exportObject(f.rootId, f.context); + expect(result.datapack[f.elementType].map((element: any) => element.Name)).toEqual(['First', 'Second', 'Child']); + expect(result.datapack[f.elementType].every((element: any) => !(f.order in element))).toBe(true); + const stream = fixture(); + const onResults = jest.fn(); + expect(await stream.exporter.exportObject(stream.rootId, { ...stream.context, onResults })).toEqual([]); + expect(onResults.mock.calls[0][0][0].datapack).toEqual(result.datapack); + }); + }); + + describe('Build Tools 1.14.18 export parity (#476)', () => { + const fixturePath = path.join(__dirname, '__fixtures__', 'exportParity'); + const fixtures = ['standard', 'managed'].flatMap(runtime => + ['DataRaptor', 'IntegrationProcedure', 'OmniScript'].map(type => `${runtime}-${type}.json`)); + + // Internal matching objects only need their source key. Build Tools also emits redundant + // matching fields. Unreferenced managed mapping source keys are likewise optional metadata. + // Do not normalize field values, array order, filenames, JSON types or source-key values. + function comparable(value: any): any { + if (Array.isArray(value)) return value.map(comparable); + if (!value || typeof value !== 'object') return value; + if (value.VlocityDataPackType === 'VlocityMatchingKeyObject') { + return Object.fromEntries(['VlocityDataPackType', 'VlocityRecordSObjectType', 'VlocityMatchingRecordSourceKey'].map(field => [field, value[field]])); + } + return Object.fromEntries(Object.entries(value) + .filter(([field]) => !(value.VlocityRecordSObjectType === '%vlocity_namespace%__DRMapItem__c' && field === 'VlocityRecordSourceKey')) + .map(([field, value]) => [field, comparable(value)])); + } + + it.each(fixtures)('matches files, hierarchy, mapping order, identity and serialization: %s', async filename => { + const { input, expectedFiles, toolsVersion } = JSON.parse(fs.readFileSync(path.join(fixturePath, filename), 'utf8')); + expect(toolsVersion).toBe('1.14.18'); + const definitions = new DatapackExportDefinitionStore(); + definitions.load(DatapackExportDefinitions[input.runtime === 'managed' ? 'omniStudioManaged' : 'omniStudioStandard'].definitions); + const describes = Object.fromEntries([input.rootType, input.childType].map(name => [name, { + name, + fields: [...new Set(input.records.filter((record: any) => record.__type === name).flatMap(Object.keys))] + .filter(field => field !== '__type') + .map(field => ({ name: field, referenceTo: input.references[name]?.[field] ? [input.references[name][field]] : [] })), + childRelationships: [] + }])); + const { exporter } = createExporter({ definitions, records: input.records, describes, matchingKeyFieldsByType: input.matchingKeys }); + exporter.expander = new DatapackExpander(definitions, createLogger() as any); + const [result] = await exporter.exportObjectAndExpand(input.rootId, { datapackType: input.datapackType, maxDepth: 0, failOnError: true }); + const files = Object.fromEntries(Object.entries(result.files).map(([file, data]) => [path.posix.join(result.folder, file), String(data)])); + expect(Object.keys(files).sort()).toEqual(Object.keys(expectedFiles).sort()); + for (const [file, expected] of Object.entries(expectedFiles)) { + if (file.endsWith('.json')) { + expect({ file, data: JSON.stringify(comparable(JSON.parse(files[file]))) }) + .toEqual({ file, data: JSON.stringify(comparable(JSON.parse(expected as string))) }); + } else { + expect({ file, data: files[file] }).toEqual({ file, data: expected }); + } + } + }); + }); + + it.each(['omniStudioStandard', 'omniStudioManaged'] as const)('expands external %s references with export keys while retaining matching fields and raw lookup keys', async runtime => { + const config = DatapackExportDefinitions[runtime]; + const definitions = new DatapackExportDefinitionStore(); + definitions.load(config.definitions); + const managed = runtime === 'omniStudioManaged'; + const field = (native: string, custom: string) => managed ? `%vlocity_namespace%__${custom}__c` : native; + const type = config.definitions.OmniScript.objectType; + const parentId = 'a00000000000001AAA', childId = 'a01000000000001AAA'; + const parent = { Id: parentId, __type: type, Name: 'Parent', [field('Type', 'Type')]: 'Fixture', + [field('SubType', 'SubType')]: 'Parent', [field('Language', 'Language')]: 'English', [field('VersionNumber', 'Version')]: 64 }; + const { exporter, salesforce, matchingKeys } = createExporter({ definitions, + records: [parent, { Id: childId, __type: 'Dependent', Name: 'Dependent', Parent: parentId }], + describes: { + [type]: { name: type, fields: Object.keys(parent).map(name => ({ name, referenceTo: [] })), childRelationships: [] }, + Dependent: { name: 'Dependent', fields: [{ name: 'Name' }, { name: 'Parent', referenceTo: [type] }], childRelationships: [] } + }, + matchingKeyFieldsByType: { [type]: [...config.definitions.OmniScript.matchingKeyFields!], Dependent: ['Name'] } + }); + const [raw] = await exporter.exportObject(childId, { maxDepth: 0, failOnError: true }); + const rawReference = raw.datapack.Parent; + expect(rawReference.VlocityLookupRecordSourceKey).toBe(`${type}/Fixture/Parent/English/64`); + const lookupCalls = salesforce.data.lookupById.mock.calls.length; + const matchingCalls = matchingKeys.getMatchingKey.mock.calls.length; + const expanded = new DatapackExpander(definitions, createLogger() as any).expandDatapack(raw.datapack); + const body = JSON.parse(expanded.files['Dependent_DataPack.json'].toString()); + expect(body.Parent).toEqual({ ...rawReference, VlocityLookupRecordSourceKey: `${type}/Fixture/Parent/English` }); + expect(expanded.parentKeys).toEqual([`${type}/Fixture/Parent/English`]); + expect(salesforce.data.lookupById).toHaveBeenCalledTimes(lookupCalls); + expect(matchingKeys.getMatchingKey).toHaveBeenCalledTimes(matchingCalls); + expect(rawReference.VlocityLookupRecordSourceKey).toBe(`${type}/Fixture/Parent/English/64`); + }); + + it.each([ + { fields: ['ExportCode__c'], key: 'Script__c/PortableParent' }, + { fields: ['Name', 'ExportCode__c'], key: 'Script__c/Parent/PortableParent' }, + { fields: ['Group__c', 'ExportCode__c'], key: 'Script__c/Group__c/PortableGroup/PortableParent' } + ].flatMap(test => [false, true].map(ignored => ({ ...test, ignored }))))( + 'retains independent export fields $fields (ignored: $ignored) without changing lookup criteria', + async ({ fields, key, ignored }) => { + const definitions = new DatapackExportDefinitionStore(); + definitions.load({ + Script: { objectType: 'Script__c', name: ['Name'], exportKey: fields, ignoreFields: ignored ? ['ExportCode__c', 'Group__c'] : [] }, + Group: { objectType: 'Group__c', name: ['Name'], exportKey: ['Code__c'] } + }); + const parentId = 'a00000000000001AAA', childId = 'a01000000000001AAA', groupId = 'a02000000000001AAA'; + const { exporter } = createExporter({ definitions, + records: [ + { Id: parentId, __type: 'Script__c', Name: 'Parent', ExportCode__c: 'PortableParent', Group__c: groupId }, + { Id: childId, __type: 'Dependent', Name: 'Dependent', Parent: parentId }, + { Id: groupId, __type: 'Group__c', Name: 'Group', Code__c: 'PortableGroup' } + ], + describes: { + Script__c: { name: 'Script__c', fields: [{ name: 'Name' }, { name: 'ExportCode__c' }, { name: 'Group__c', referenceTo: ['Group__c'] }], childRelationships: [] }, + Dependent: { name: 'Dependent', fields: [{ name: 'Name' }, { name: 'Parent', referenceTo: ['Script__c'] }], childRelationships: [] }, + Group__c: { name: 'Group__c', fields: [{ name: 'Name' }, { name: 'Code__c' }], childRelationships: [] } + }, + matchingKeyFieldsByType: { Script__c: ['Name'], Dependent: ['Name'], Group__c: ['Name'] } + }); + const expander = new DatapackExpander(definitions, createLogger() as any); + exporter.expander = expander; + const context = { maxDepth: 0, failOnError: true }; + const [raw] = await exporter.exportObject(childId, context); + expect(raw.datapack.Parent.VlocityLookupRecordSourceKey).toBe('Script__c/Parent'); + expect(raw.datapack.Parent).not.toHaveProperty('ExportCode__c'); + expect(raw.datapack.Parent).not.toHaveProperty('Group__c'); + + // Consolidated JSON must retain the export values for later expansion, without + // adding them to deployment lookup filters before or after expansion. + const roundTripped = JSON.parse(JSON.stringify(raw.datapack)); + const expanded = expander.expandDatapack(roundTripped); + const body = JSON.parse(expanded.files['Dependent_DataPack.json'].toString()); + expect(body.Parent).toEqual({ + Name: 'Parent', VlocityDataPackType: 'VlocityLookupMatchingKeyObject', + VlocityRecordSObjectType: 'Script__c', VlocityLookupRecordSourceKey: key + }); + expect(expanded.parentKeys).toEqual([key]); + expect(JSON.stringify(expanded.files)).not.toContain('VlocityRecordExportKeyValues'); + + for (const reference of [roundTripped.Parent, body.Parent]) { + const lookup = new DatapackLookupService(new VlocityNamespaceService(''), {} as any, {} as any, createLogger() as any); + const lookupMultiple = jest.spyOn(lookup as any, 'lookupMultiple').mockResolvedValue([[parentId]]); + expect(await lookup.resolveDependency(reference, {} as any)).toBe(parentId); + expect(lookupMultiple.mock.calls[0][0]).toEqual([expect.objectContaining({ filter: { Name: 'Parent' } })]); + } + + const [direct] = await exporter.exportObjectAndExpand(childId, context); + expect(direct.files).toEqual(expanded.files); + const [parent] = await exporter.exportObjectAndExpand(parentId, context); + expect(parent.sourceKey).toBe(key); + const [savedParent] = await exporter.exportObject(parentId, context); + expect(savedParent.sourceKey).toBe('Script__c/Parent'); + const restoredParent = expander.expandDatapack(JSON.parse(JSON.stringify(savedParent.datapack)), savedParent); + expect(restoredParent.sourceKey).toBe(key); + expect(restoredParent.files).toEqual(parent.files); + expect(Object.values(parent.files).map(String).join('')).not.toContain('VlocityRecordExportKeyValues'); + + const onResults = jest.fn(); + await exporter.exportObject(parentId, { ...context, onResults }); + expect(onResults.mock.calls[0][0][0].datapack).toEqual(savedParent.datapack); + } + ); + + it.each([ + { exclusion: 'configured', field: { name: 'Version__c' }, ignoreFields: ['Version__c'] }, + { exclusion: 'calculated', field: { name: 'Version__c', calculated: true }, ignoreFields: [] }, + { exclusion: 'auto-number', field: { name: 'Version__c', autoNumber: true }, ignoreFields: [] } + ])('preserves $exclusion identity inputs across consolidated exports', async ({ field, ignoreFields }) => { + const definitions = new DatapackExportDefinitionStore(); + definitions.load({ Script: { objectType: 'Script__c', name: ['Name'], exportKey: ['Name', 'Version__c'], ignoreFields } }); + const describe = { name: 'Script__c', fields: [{ name: 'Name' }, field] }; + const { exporter } = createExporter({ definitions, describe, matchingKeyFields: ['Name', 'Version__c'], + records: [{ Id: 'a00000000000001AAA', __type: 'Script__c', Name: 'Example', Version__c: 64 }] }); + const expander = new DatapackExpander(definitions, createLogger() as any); + exporter.expander = expander; + const [direct] = await exporter.exportObjectAndExpand('a00000000000001AAA', { failOnError: true }); + const [consolidated] = await exporter.exportObject('a00000000000001AAA', { failOnError: true }); + const later = expander.expandDatapack(JSON.parse(JSON.stringify(consolidated.datapack))); + expect(direct.sourceKey).toBe('Script__c/Example/64'); + expect(later.sourceKey).toBe(direct.sourceKey); + expect(consolidated.datapack).not.toHaveProperty('Version__c'); + const [file] = Object.values(later.files); + const body = JSON.parse(String(file)); + expect(body).not.toHaveProperty('Version__c'); + expect(body).not.toHaveProperty('VlocityRecordExportKeyValues'); + }); + + it('does not use exportKey when generating matching keys or resolving external lookups', async () => { + const definitions = new DatapackExportDefinitionStore(); + definitions.load({ Script: { objectType: 'Script__c', name: ['Name'], exportKey: ['ExportCode__c'] } }); + const describe = { name: 'Script__c', fields: [{ name: 'Name' }, { name: 'Version__c' }, { name: 'ExportCode__c' }] }; + const { exporter, salesforce } = createExporter({ definitions, describe, matchingKeyFields: ['Name', 'Version__c'], + records: [{ Id: 'a00000000000001AAA', __type: 'Script__c', Name: 'Example', Version__c: 64, ExportCode__c: 'Portable' }] }); + const [result] = await exporter.exportObject('a00000000000001AAA', { failOnError: true }); + expect(result.sourceKey).toBe('Script__c/Example/64'); + exporter.matchingKeys.clear(); + salesforce.schema.describeSObjectField.mockClear(); + expect(await exporter.getReferencedMatchingKey('a00000000000001AAA', undefined, new Set())).toBe(result.sourceKey); + expect(salesforce.schema.describeSObjectField).not.toHaveBeenCalledWith('Script__c', 'ExportCode__c'); + }); + }); describe('DatapackExpander', () => { @@ -1504,6 +1785,9 @@ describe('DatapackExpander', () => { it('expands configured array fields into separate files', () => { const definitions = { + get: jest.fn(), + isFieldIgnored: jest.fn(() => false), + getExportKey: jest.fn(), getFileName: jest.fn((_item, field) => field === undefined ? ['Name'] : field === 'Children' ? ['_Child', 'Name'] : undefined), getName: jest.fn(() => ['Name']), getFieldConfig: jest.fn((_item, field, configKey) => field === 'Children' && configKey === 'expandArray') @@ -1566,6 +1850,9 @@ describe('DatapackExpander', () => { it('prefers VlocityRecordSourceKey over Name as the default record-array sort field', () => { const definitions = { + get: jest.fn(), + isFieldIgnored: jest.fn(() => false), + getExportKey: jest.fn(), getFileName: jest.fn((_item, field) => field === undefined ? ['Name'] : undefined), getName: jest.fn(() => ['Name']), getFieldConfig: jest.fn() @@ -1590,6 +1877,9 @@ describe('DatapackExpander', () => { it('sorts record arrays alphabetically by Name when no source keys are present', () => { const definitions = { + get: jest.fn(), + isFieldIgnored: jest.fn(() => false), + getExportKey: jest.fn(), getFileName: jest.fn((_item, field) => field === undefined ? ['Name'] : undefined), getName: jest.fn(() => ['Name']), getFieldConfig: jest.fn() @@ -1613,6 +1903,9 @@ describe('DatapackExpander', () => { it('preserves record-array order when no configured or default sort field is available', () => { const definitions = { + get: jest.fn(), + isFieldIgnored: jest.fn(() => false), + getExportKey: jest.fn(), getFileName: jest.fn((_item, field) => field === undefined ? ['Name'] : undefined), getName: jest.fn(() => ['Name']), getFieldConfig: jest.fn() @@ -1636,6 +1929,9 @@ describe('DatapackExpander', () => { it('keeps configured null fields inline instead of expanding them into files', () => { const definitions = { + get: jest.fn(), + isFieldIgnored: jest.fn(() => false), + getExportKey: jest.fn(), getFileName: jest.fn((_item, field) => field === undefined ? ['Name'] : field === 'PropertySet' ? ['_PropertySet'] : undefined), getName: jest.fn(() => ['Name']), getFieldConfig: jest.fn() @@ -1658,6 +1954,9 @@ describe('DatapackExpander', () => { it('uses the source key name for unconfigured generic SObject folders', () => { const definitions = { + get: jest.fn(), + isFieldIgnored: jest.fn(() => false), + getExportKey: jest.fn(), getFileName: jest.fn(), getName: jest.fn(), getFieldConfig: jest.fn() @@ -1679,6 +1978,9 @@ describe('DatapackExpander', () => { it('sanitizes separators in configured folder and file names', () => { const definitions = { + get: jest.fn(), + isFieldIgnored: jest.fn(() => false), + getExportKey: jest.fn(), getFileName: jest.fn((_item, field) => field === undefined ? ['FolderName'] : undefined), getName: jest.fn(() => ['FolderName']), getFieldConfig: jest.fn() @@ -1700,6 +2002,9 @@ describe('DatapackExpander', () => { it('falls back to the source key name when a configured folder field is empty', () => { const definitions = { + get: jest.fn(), + isFieldIgnored: jest.fn(() => false), + getExportKey: jest.fn(), getFileName: jest.fn((_item, field) => field === undefined ? ['FolderName'] : undefined), getName: jest.fn(() => ['FolderName']), getFieldConfig: jest.fn() @@ -1721,6 +2026,9 @@ describe('DatapackExpander', () => { it('keeps configured field file extensions and writes non-json strings raw', () => { const definitions = { + get: jest.fn(), + isFieldIgnored: jest.fn(() => false), + getExportKey: jest.fn(), getFileName: jest.fn((_item, field) => field === undefined ? ['Name'] : field === 'Code__c' ? ['_Script.js'] : undefined), getName: jest.fn(() => ['Name']), getFieldConfig: jest.fn() @@ -1744,6 +2052,9 @@ describe('DatapackExpander', () => { it('writes expanded files to the filesystem', async () => { const definitions = { + get: jest.fn(), + isFieldIgnored: jest.fn(() => false), + getExportKey: jest.fn(), getFileName: jest.fn((_item, field) => field === undefined ? ['Name'] : field === 'Children' ? ['_Child', 'Name'] : undefined), getName: jest.fn(() => ['Name']), getFieldConfig: jest.fn((_item, field, configKey) => field === 'Children' && configKey === 'expandArray') diff --git a/packages/vlocity-deploy/src/__tests__/datapackNormalization.test.ts b/packages/vlocity-deploy/src/__tests__/datapackNormalization.test.ts new file mode 100644 index 00000000..f8c86837 --- /dev/null +++ b/packages/vlocity-deploy/src/__tests__/datapackNormalization.test.ts @@ -0,0 +1,103 @@ +import 'jest'; +import { Logger } from '@vlocode/core'; +import { DatapackNormalizer } from '../export/datapackNormalizer'; +import { DatapackExpander } from '../export/datapackExpander'; +import { DatapackExportDefinitionStore } from '../export/exportDefinitionStore'; +import { DatapackExportDefinitions } from '../exportDefinitions'; + +const sobject = (type: string, fields: Record) => ({ + VlocityDataPackType: 'SObject' as const, VlocityRecordSObjectType: type, VlocityRecordSourceKey: `${type}/Example`, ...fields +}); + +describe.each(['omniStudioStandard', 'omniStudioManaged'] as const)('%s output', runtime => { + it('sorts mappings using all six priorities, empty values last and canonical JSON ties', () => { + const config = DatapackExportDefinitions[runtime]; + const definitions = new DatapackExportDefinitionStore(); + definitions.load(config.definitions, { scope: config.id }); + const ref = { objectType: config.definitions.DataRaptor.objectType, datapackType: 'DataRaptor', scope: config.id }; + const [embedded] = definitions.getEmbeddedObjects(ref); + const fields = embedded.sortFields!; + expect(fields).toHaveLength(6); + const mapping = (values: any[], extra = {}) => sobject(embedded.objectType!, { + ...Object.fromEntries(fields.map((field, i) => [field, values[i]])), ...extra + }); + const rows = [ + mapping([1, 1, 1, 'A', 'A', 'A'], { Formula: 'A' }), + mapping([1, 1, 1, 'A', 'A', 'A'], { Formula: 'Z' }), + mapping([1, 1, 1, 'A', 'A', 'Z']), + mapping([1, 1, 1, 'A', 'Z', 'A']), + mapping([1, 1, 1, 'Z', 'A', 'A']), + mapping([1, 1, 2, 'A', 'A', 'A']), + mapping([1, 2, 1, 'A', 'A', 'A']), + mapping([2, 1, 1, 'A', 'A', 'A']), + mapping([null, 1, 1, 'A', 'A', 'A']) + ]; + const input = [...rows].reverse(); + const output = new DatapackNormalizer(definitions); + expect(output.sortField(ref, embedded.name, input)).toEqual(rows); + expect(input).toEqual([...rows].reverse()); + const root = output.normalizeRecord(sobject(ref.objectType, { Empty: null, Zero: 0, False: false, [embedded.name]: input }), ref); + expect(root.Empty).toBe(''); + expect(root.Zero).toBe(0); + expect(root.False).toBe(false); + expect(root[embedded.name][8]).not.toHaveProperty(fields[0]); + }); +}); + +describe('Datapack normalization', () => { + it('resolves managed sort fields against concrete namespace keys', () => { + const definitions = new DatapackExportDefinitionStore(); + definitions.load(DatapackExportDefinitions.omniStudioManaged.definitions); + const child = (name: string, order: number) => sobject('vlocity_cmt__Element__c', { + Name: name, VlocityRecordSourceKey: `Element/${name}`, vlocity_cmt__Order__c: order + }); + const root = sobject('vlocity_cmt__OmniScript__c', { + vlocity_cmt__Element__c: [child('Later', 2), child('First', 1)] + }); + const output = new DatapackNormalizer(definitions).normalizeRecord(root, { objectType: root.VlocityRecordSObjectType, datapackType: 'OmniScript' }); + expect(output.vlocity_cmt__Element__c.map((value: any) => value.Name)).toEqual(['First', 'Later']); + expect(output.vlocity_cmt__Element__c[0]).not.toHaveProperty('vlocity_cmt__Order__c'); + }); + + it('uses excluded fields for array order and filenames before pruning them without mutating input', () => { + const definitions = new DatapackExportDefinitionStore(); + definitions.load({ + Root: { objectType: 'Root', name: ['Name'], ignoreFields: ['Name'], embeddedObjects: { + Children: { objectType: 'Child', filter: {}, fileName: ['Name'], expandArray: true, sortFields: ['Position'] } + } }, + Child: { objectType: 'Child', name: ['Name'], ignoreFields: ['Name', 'Position'] } + }); + const input = sobject('Root', { Name: 'RootName', Children: [ + sobject('Child', { Name: 'Later', Position: 2 }), sobject('Child', { Name: 'Earlier', Position: 1 }) + ] }); + const before = JSON.stringify(input); + const result = new DatapackExpander(definitions, Logger.null).expandDatapack(input, { datapackType: 'Root' }); + const root = JSON.parse(result.files['RootName_DataPack.json'].toString()); + expect(root.Children).toEqual(['RootName_Earlier.json', 'RootName_Later.json']); + expect(root).not.toHaveProperty('Name'); + expect(JSON.parse(result.files['RootName_Earlier.json'].toString())).not.toHaveProperty('Position'); + expect(JSON.parse(result.files['RootName_Earlier.json'].toString())).not.toHaveProperty('Name'); + expect(JSON.stringify(input)).toBe(before); + }); + + it('preserves positional arrays in explicitly parsed JSON payloads, including arrays at field level', () => { + const definitions = new DatapackExportDefinitionStore(); + definitions.load({ Root: { objectType: 'Root', name: ['Name'], fields: { Payload: { parseJson: true } } } }); + const input = sobject('Root', { Payload: [{ Name: 'Z', z: 2, a: 1 }, { Name: 'A' }] }); + const output = new DatapackNormalizer(definitions).normalizeRecord(input, { objectType: 'Root' }); + expect(output.Payload.map((item: any) => item.Name)).toEqual(['Z', 'A']); + expect(Object.keys(output.Payload[0])).toEqual(['Name', 'a', 'z']); + }); + + it('reports a circular hierarchy rather than losing rootless elements', () => { + const definitions = new DatapackExportDefinitionStore(); + definitions.load({ Root: { objectType: 'Root', name: ['Name'], fields: { Children: { sortParentField: 'Parent' } } } }); + const child = (key: string, parent: string) => sobject('Child', { VlocityRecordSourceKey: key, Parent: { VlocityMatchingRecordSourceKey: parent } }); + expect(() => new DatapackNormalizer(definitions).sortField({ objectType: 'Root' }, 'Children', [child('a', 'b'), child('b', 'a')])) + .toThrow('Circular export hierarchy'); + expect(() => new DatapackNormalizer(definitions).sortField({ objectType: 'Root' }, 'Children', [child('a', 'a')])) + .toThrow('Circular export hierarchy'); + expect(new DatapackNormalizer(definitions).sortField({ objectType: 'Root' }, 'Children', [{ Name: 'Keyless root' }])) + .toEqual([{ Name: 'Keyless root' }]); + }); +}); diff --git a/packages/vlocity-deploy/src/__tests__/datapackSorting.test.ts b/packages/vlocity-deploy/src/__tests__/datapackSorting.test.ts new file mode 100644 index 00000000..36c45b3f --- /dev/null +++ b/packages/vlocity-deploy/src/__tests__/datapackSorting.test.ts @@ -0,0 +1,24 @@ +import 'jest'; +import { createVlocityComparator } from '../export/datapackSorting'; + +describe('Vlocity record ordering', () => { + it('normalizes each tied record once per sort and reads fresh values on the next sort', () => { + const records = [ + { Priority: 1, Name: 'C' }, + { Priority: 1, Name: 'A' }, + { Priority: 1, Name: 'B' } + ]; + const normalize = jest.fn(record => ({ Name: record.Name })); + const sorted = [...records].sort(createVlocityComparator(['Priority'], normalize)); + expect(sorted.map(record => record.Name)).toEqual(['A', 'B', 'C']); + expect(normalize).toHaveBeenCalledTimes(records.length); + for (const record of records) { + expect(normalize.mock.calls.filter(([value]) => value === record)).toHaveLength(1); + } + + records[0].Name = '0'; + const updated = [...records].sort(createVlocityComparator(['Priority'], normalize)); + expect(updated.map(record => record.Name)).toEqual(['0', 'A', 'B']); + expect(normalize).toHaveBeenCalledTimes(records.length * 2); + }); +}); diff --git a/packages/vlocity-deploy/src/__tests__/exportDefinitionStore.test.ts b/packages/vlocity-deploy/src/__tests__/exportDefinitionStore.test.ts index 0c2299c8..d599e4b0 100644 --- a/packages/vlocity-deploy/src/__tests__/exportDefinitionStore.test.ts +++ b/packages/vlocity-deploy/src/__tests__/exportDefinitionStore.test.ts @@ -4,6 +4,18 @@ import { DatapackExportDefinitionStore } from '../export/exportDefinitionStore'; describe('DatapackExportDefinitionStore', () => { + it('resolves scoped export field lists without inheriting a sibling definition or changing matching keys', () => { + const store = new DatapackExportDefinitionStore(); + store.load({ + OmniScript: { objectType: 'OmniProcess', name: ['Name'], exportKey: ['Type', 'SubType', 'Language'], matchingKeyFields: ['Name', 'VersionNumber'] }, + Generic: { objectType: 'OmniProcess', name: ['Name'] } + }); + store.load({ OmniScript: { objectType: 'OmniProcess', name: ['Name'], exportKey: ['Name', 'VersionNumber'] } }, { scope: 'versions' }); + expect(store.getExportKey({ objectType: 'OmniProcess', datapackType: 'OmniScript', scope: 'versions' })).toEqual(['Name', 'VersionNumber']); + expect(store.getExportKey({ objectType: 'OmniProcess', datapackType: 'Generic' })).toBeUndefined(); + expect(store.getMatchingKeyFields({ objectType: 'OmniProcess', datapackType: 'OmniScript', scope: 'versions' })).toEqual(['Name', 'VersionNumber']); + }); + describe('definition identity', () => { it('keeps definitions for different SObject runtimes under the same Datapack type and scope', () => { const store = new DatapackExportDefinitionStore(); diff --git a/packages/vlocity-deploy/src/export/datapackExpander.ts b/packages/vlocity-deploy/src/export/datapackExpander.ts index 458f1676..2557e368 100644 --- a/packages/vlocity-deploy/src/export/datapackExpander.ts +++ b/packages/vlocity-deploy/src/export/datapackExpander.ts @@ -2,10 +2,13 @@ import path from "path/posix"; import nativePath from "path"; import { injectable, Logger } from "@vlocode/core"; -import { formatString, normalizeName, sortBy, substringAfter } from "@vlocode/util"; +import { formatString, normalizeName, substringAfter } from "@vlocode/util"; import { VlocityDatapackSObject } from "@vlocode/vlocity"; import * as fs from "fs-extra"; +import { DatapackNormalizer } from "./datapackNormalizer"; +import { applyExportKeys } from "./datapackExportKeys"; +import { DatapackObject, DatapackValue, sortObjectKeys } from "./datapackValue"; import { DatapackExportDefinitionStore, ObjectRef } from "./exportDefinitionStore"; export interface DatapackExpandResult { @@ -45,6 +48,8 @@ type FieldRef = ObjectRef & { field: string }; @injectable.transient() export class DatapackExpander { + private readonly normalizer = new DatapackNormalizer(this.definitions); + private static datapackFileName = 'DataPack.json'; private fieldTypeExtension = { @@ -89,6 +94,8 @@ export class DatapackExpander { datapackType: context?.datapackType }; + datapack = applyExportKeys(datapack, itemRef, this.definitions); + const baseSourceKey = substringAfter(datapack.VlocityRecordSourceKey, '/'); const fileNameFormat = this.definitions.getFileName(itemRef) ?? baseSourceKey; const baseName = this.evalPathFormat(fileNameFormat, { context: datapack, fallback: baseSourceKey }); @@ -100,29 +107,30 @@ export class DatapackExpander { this.evalPathFormat(folderFormat, { context: datapack, fallback: baseSourceKey }) ); - const data: Record = {}; + const data: DatapackObject = {}; const files = new DatapackFiles(baseName, folder, this.logger); - for (const field of Object.keys(datapack).sort()) { + for (const { field, sourceValue, value: preparedValue } of this.normalizer.prepareFields(datapack, itemRef)) { if (this.datapackStandardFields.includes(field)) { continue; } const fileNameFormat = this.definitions.getFileName(itemRef, field); - let value = this.sortFieldArray({ ...itemRef, field }, datapack[field]); + let value = preparedValue; - if (fileNameFormat && value !== null) { + if (fileNameFormat && value !== null && value !== '') { const defaultExt = 'json'; if (this.expandFieldArray({ ...itemRef, field }, value)) { - value = value.map(item => { + value = value.map((item, index) => { if (item === null) { return null; } - const fileName = this.evalPathFormat(fileNameFormat, { context: item ?? datapack, defaultExt }); + const original = Array.isArray(sourceValue) ? sourceValue[index] : datapack; + const fileName = this.evalPathFormat(fileNameFormat, { context: original, defaultExt }); return files.addFile(fileName, item); }); } else { - const fileName = this.evalPathFormat(fileNameFormat, { context: value, defaultExt }); + const fileName = this.evalPathFormat(fileNameFormat, { context: sourceValue, defaultExt }); value = files.addFile(fileName, value); } } @@ -179,49 +187,9 @@ export class DatapackExpander { return false; } - /** - * Sort record arrays at the expansion boundary, after their source keys have been finalized. - * Configured sort fields take precedence. Without configuration, a source key is preferred and - * Name is used when a source key is not available on every record. Other arrays retain their - * original order because their values may be intentionally positional. - */ - private sortFieldArray(ref: FieldRef, value: unknown): unknown { - if (!Array.isArray(value) || value.length < 2 || !value.every(this.isRecord)) { - return value; - } - - const configuredSortFields = this.definitions.getFieldConfig(ref, ref.field, 'sortFields'); - const sortFields = Array.isArray(configuredSortFields) - ? configuredSortFields.filter(field => typeof field === 'string' && field.length > 0) - : []; - - const fields = sortFields.length > 0 - ? sortFields - : this.getDefaultSortFields(value); - - if (!fields) { - return value; - } - - return sortBy(value, fields); - } - - private readonly isRecord = (value: unknown): value is Record => - typeof value === 'object' && value !== null && !Array.isArray(value) && !Buffer.isBuffer(value); - - private getDefaultSortFields(records: Record[]): string[] | undefined { - if (records.every(record => record.VlocityRecordSourceKey !== undefined && record.VlocityRecordSourceKey !== null)) { - return ['VlocityRecordSourceKey']; - } - if (records.every(record => record.Name !== undefined && record.Name !== null)) { - return ['Name']; - } - return undefined; - } - - private evalPathFormat(format: string | string[], options?: { context?: object; defaultExt?: string; fallback?: string; }) { + private evalPathFormat(format: string | string[], options?: { context?: DatapackValue; defaultExt?: string; fallback?: string; }) { const name = Array.isArray(format) - ? format.map(f => f.startsWith('_') ? f.substring(1) : options?.context?.[f] ?? '').join('_') + ? format.map(f => f.startsWith('_') ? f.substring(1) : Object(options?.context)[f] ?? '').join('_') : (options?.context ? formatString(format, options?.context) : format); const configuredExtension = this.getConfiguredExtension(format); const extension = configuredExtension && name.endsWith(configuredExtension) ? configuredExtension : (options?.defaultExt ? `.${options.defaultExt}` : ''); @@ -283,7 +251,7 @@ class DatapackFiles { private readonly logger: Logger ) { } - public addFile(fileName: string, value: unknown) { + public addFile(fileName: string, value: DatapackValue) { if (Object.hasOwn(this.files, fileName)) { // Overwriting would silently drop the earlier value from the expanded datapack; // this happens when the configured fileName format does not evaluate to a unique @@ -312,13 +280,13 @@ class DatapackFiles { return options?.withFolder ? path.join(this.folder, fileName) : fileName; } - private getFileData(fileName: string, value: unknown): Buffer | string { + private getFileData(fileName: string, value: DatapackValue): Buffer | string { if (Buffer.isBuffer(value)) { return value; } if (typeof value === 'string' && path.extname(fileName).toLowerCase() !== '.json') { return Buffer.from(value); } - return Buffer.from(JSON.stringify(value, null, 4)); + return Buffer.from(JSON.stringify(sortObjectKeys(value), null, 4)); } } diff --git a/packages/vlocity-deploy/src/export/datapackExportKeys.ts b/packages/vlocity-deploy/src/export/datapackExportKeys.ts new file mode 100644 index 00000000..0957b7a9 --- /dev/null +++ b/packages/vlocity-deploy/src/export/datapackExportKeys.ts @@ -0,0 +1,110 @@ +import { DatapackFields, VlocityDatapackSObject } from '@vlocode/vlocity'; +import { DatapackExportDefinitionStore, ObjectRef } from './exportDefinitionStore'; +import { DatapackObject, DatapackValue, getFieldValue, isDatapackObject } from './datapackValue'; + +interface ExportRecord { + record: DatapackObject; + ref: ObjectRef; +} + +const sourceKeyFields = new Set([DatapackFields.sourceKey, DatapackFields.matchingKey, DatapackFields.lookupKey]); + +/** Apply output identities to a copy of the datapack. Resolution state belongs to this expansion only. */ +export function applyExportKeys( + datapack: VlocityDatapackSObject, + ref: ObjectRef, + definitions: DatapackExportDefinitionStore +): VlocityDatapackSObject { + return new ExportKeyResolver(definitions, datapack, ref).apply(); +} + +class ExportKeyResolver { + private readonly records = new Map(); + private readonly keys = new Map(); + private readonly resolving = new Set(); + + constructor( + private readonly definitions: DatapackExportDefinitionStore, + private readonly datapack: VlocityDatapackSObject, + private readonly ref: ObjectRef + ) {} + + public apply(): VlocityDatapackSObject { + this.collect(this.datapack); + for (const key of this.records.keys()) { + this.resolve(key); + } + return this.rewrite(this.datapack) as VlocityDatapackSObject; + } + + private collect(value: DatapackValue): void { + if (Array.isArray(value)) { + value.forEach(item => this.collect(item)); + } else if (isDatapackObject(value)) { + const sourceKey = value.VlocityRecordSourceKey ?? value.VlocityMatchingRecordSourceKey ?? value.VlocityLookupRecordSourceKey; + const objectType = value.VlocityRecordSObjectType; + if (typeof objectType === 'string' && typeof sourceKey === 'string') { + // Full records supply fields that references can omit, regardless of traversal order. + if (!this.records.has(sourceKey) || value.VlocityDataPackType === 'SObject') { + const ref = value === this.datapack ? this.ref : { objectType, scope: this.ref.scope }; + this.records.set(sourceKey, { record: value, ref }); + } + } + Object.values(value).forEach(item => this.collect(item)); + } + } + + private resolve(sourceKey: string): string { + const resolved = this.keys.get(sourceKey); + if (resolved !== undefined) { + return resolved; + } + const item = this.records.get(sourceKey); + const fields = item && this.definitions.getExportKey(item.ref); + // Circular references retain their original identity, as they do during matching. + if (!item || !fields?.length || this.resolving.has(sourceKey)) { + return sourceKey; + } + this.resolving.add(sourceKey); + const values = fields.map(field => this.resolveField(item.record, field)); + if (values.every(value => value == null || value === '')) { + throw new Error(`Cannot expand ${sourceKey}: all exportKey fields [${fields.join(', ')}] are empty`); + } + const key = [item.record.VlocityRecordSObjectType, ...values].join('/'); + this.resolving.delete(sourceKey); + this.keys.set(sourceKey, key); + return key; + } + + private resolveField(record: DatapackObject, field: string): DatapackValue { + const exportValues = record[DatapackFields.exportKeyValues]; + const exportValue = isDatapackObject(exportValues) ? getFieldValue(exportValues, field) : undefined; + const value = exportValue ?? getFieldValue(record, field); + if (isDatapackObject(value)) { + const reference = value.VlocityMatchingRecordSourceKey ?? value.VlocityLookupRecordSourceKey; + if (typeof reference === 'string') { + return this.resolve(reference); + } + } + return value; + } + + private rewrite(value: DatapackValue): DatapackValue { + if (Array.isArray(value)) { + return value.map(item => this.rewrite(item)); + } + if (!isDatapackObject(value)) { + return value; + } + const output: DatapackObject = {}; + for (const [field, original] of Object.entries(value)) { + if (field === DatapackFields.exportKeyValues) { + continue; + } + output[field] = sourceKeyFields.has(field) && typeof original === 'string' + ? this.keys.get(original) ?? original + : this.rewrite(original); + } + return output; + } +} diff --git a/packages/vlocity-deploy/src/export/datapackExporter.ts b/packages/vlocity-deploy/src/export/datapackExporter.ts index 7184a11b..f40cfd72 100644 --- a/packages/vlocity-deploy/src/export/datapackExporter.ts +++ b/packages/vlocity-deploy/src/export/datapackExporter.ts @@ -1,11 +1,13 @@ import { DescribeSObjectResult, Field, SalesforceDataService, SalesforceService } from "@vlocode/salesforce"; import { ObjectFilter, ObjectRelationship, type LookupFilerPrimitive, type LookupFilerValue, type LookupFilter } from "./exportDefinitions"; -import { VlocityDatapackLookupReference, VlocityDatapackMatchingReference, VlocityDatapackReference, VlocityDatapackReferenceType, VlocityDatapackSObject, VlocityDatapackSourceKey } from "@vlocode/vlocity"; +import { DatapackFields, VlocityDatapackLookupReference, VlocityDatapackMatchingReference, VlocityDatapackReference, VlocityDatapackReferenceType, VlocityDatapackSObject, VlocityDatapackSourceKey } from "@vlocode/vlocity"; import { MatchingKeyService } from "../matchingKeyService"; import { calculateHash, defineAliasedProperties, defineReadonlyProperties, extractNamespaceAndName, forEachAsyncParallel, groupBy, Iterable, mapAsync, mapAsyncParallel, removeNamespacePrefix, visitObject, type CancellationToken } from "@vlocode/util"; import { inject, injectable, Logger } from "@vlocode/core"; import { DatapackExpandResult, DatapackExpander } from "./datapackExpander"; import { DatapackExportDefinitionStore } from "./exportDefinitionStore"; +import { DatapackNormalizer } from "./datapackNormalizer"; +import { DatapackObject, getFieldValue } from "./datapackValue"; import { NAMESPACE_PLACEHOLDER } from "../constants"; interface ExportDatapack { @@ -267,7 +269,7 @@ export class DatapackExporter { input: string[] | string | ExportRequest | ExportRequest[], context?: DatapackExportOptions ): Promise { - const result = await this.exportObject(input, context); + const result = await this.exportRawObjects(input, context); return this.expand(result, context); } @@ -281,6 +283,24 @@ export class DatapackExporter { input: string[] | string | ExportRequest | ExportRequest[], context?: DatapackExportOptions ): Promise { + const normalizer = new DatapackNormalizer(this.definitions); + const prepare = (results: ExportResult[]) => results.map(result => ({ + ...result, + datapack: normalizer.normalizeRecord(result.datapack, { + objectType: result.datapack.VlocityRecordSObjectType, + datapackType: result.datapackType, + scope: result.scope + }) + })); + const onResults = context?.onResults?.bind(context); + const exportContext = onResults ? { + ...context, + onResults: (results: ExportResult[]) => onResults(prepare(results)) + } : context; + return this.exportRawObjects(input, exportContext).then(prepare); + } + + private exportRawObjects(input: string[] | string | ExportRequest | ExportRequest[], context?: DatapackExportOptions): Promise { this.logger.verbose(`Export SObjects ${input}`); this.enqueueExport(input, context); return this.processExportQueue(context); @@ -530,6 +550,12 @@ export class DatapackExporter { return; } + // Keep configured exclusions for sorting/naming, but do not follow discarded lookups. + if (this.definitions.isFieldIgnored(datapack, field.name)) { + resolved[index] = { fieldName: field.name, value }; + return; + } + // Export as reference if (field.referenceTo?.length && value) { if (value.startsWith('005') && !field.name.endsWith('__c')) { @@ -543,7 +569,8 @@ export class DatapackExporter { } else { value = this.buildLookup(datapack, value, 'VlocityLookupMatchingKeyObject', field.referenceTo[0]); } - } else if (typeof value === 'string') { + } else if (typeof value === 'string' && (this.definitions.getFieldConfig(datapack, field.name, 'parseJson') + ?? this.definitions.get(datapack, 'parseJson') ?? true)) { value = this.tryParseAsJson(value) ?? value; } @@ -561,14 +588,17 @@ export class DatapackExporter { this.setDatapackField(datapack, field.fieldName, field.value); } } - } - private setDatapackField(datapack: ExportDatapack, fieldName: string, value: any) { - // Parse JSON values - if (typeof value === 'string') { - value = this.tryParseAsJson(value) ?? value; + const exportFields = this.definitions.getExportKey(datapack); + if (exportFields?.length) { + // Identity inputs must survive output filtering and JSON normalization. Resolve them + // from the source record just like reference fields, including ignored lookups. + datapack.data[DatapackFields.exportKeyValues] = await this.buildReferenceFields( + datapack, datapack.objectType, exportFields, record, { deferLookups: true }); } + } + private setDatapackField(datapack: ExportDatapack, fieldName: string, value: any) { // Normalize the field name and set the value, also define a getter for the original field name const field = extractNamespaceAndName(fieldName); const isVlocityNamespace = !!field.namespace && /^vlocity/i.test(field.namespace); @@ -1120,31 +1150,49 @@ export class DatapackExporter { const describe = await this.salesforce.schema.describeSObjectById(data.id); const fields = (await this.matchingKeyService.getMatchingKey(describe.name, { scope: datapack.scope })).fields; - const matchingKeyObject = {}; - - for (const fieldName of fields) { - const value = data[fieldName] ?? null; - const field = await this.salesforce.schema.describeSObjectField(describe.name, fieldName); - - if (field.referenceTo?.length && value) { - matchingKeyObject[field.name] = deferLookups - ? this.buildLookup(datapack, value, 'VlocityLookupMatchingKeyObject', field.referenceTo[0]) - : await this.resolveLookup(datapack, value, 'VlocityLookupMatchingKeyObject'); - } else { - matchingKeyObject[fieldName] = value; - } - } + const exportFields = this.definitions.getExportKey({ objectType: describe.name, scope: datapack.scope }) ?? []; + const matchingKeyObject = await this.buildReferenceFields(datapack, describe.name, fields, data, { deferLookups }); + // Ordinary reference fields become deployment lookup criteria. Retain additional + // identity inputs separately so exportKey cannot change those criteria. + const exportKeyValues = await this.buildReferenceFields( + datapack, describe.name, exportFields.filter(field => !fields.includes(field)), data, { deferLookups }); const matchingKey = await this.getMatchingKey(describe, data, datapack.scope); return { ...matchingKeyObject, + ...(Object.keys(exportKeyValues).length ? { [DatapackFields.exportKeyValues]: exportKeyValues } : {}), VlocityDataPackType: refType, VlocityRecordSObjectType: describe.name, [VlocityDatapackSourceKey[refType]]: matchingKey } as VlocityDatapackReference; } + /** Read key inputs with the same lookup representation for full records and references. */ + private async buildReferenceFields( + datapack: ExportDatapack, + objectType: string, + fields: readonly string[], + data: DatapackObject, + { deferLookups }: { deferLookups: boolean } + ): Promise> { + const values: Record = {}; + for (const fieldName of fields) { + const value = getFieldValue(data, fieldName) ?? null; + const field = await this.salesforce.schema.describeSObjectField(objectType, fieldName); + if (field.referenceTo?.length && value) { + // Salesforce reference fields contain record IDs. + const id = value as string; + values[field.name] = deferLookups + ? this.buildLookup(datapack, id, 'VlocityLookupMatchingKeyObject', field.referenceTo[0]) + : await this.resolveLookup(datapack, id, 'VlocityLookupMatchingKeyObject'); + } else { + values[fieldName] = value; + } + } + return values; + } + private async resolveDeferredLookups(options?: ExportProcessOptions) { if (this.deferredLookups.size === 0) { return; @@ -1463,12 +1511,6 @@ export class DatapackExporter { return true; } - const objectRef = { datapackType: datapack.datapackType, objectType: type.name, scope: datapack.scope }; - if (this.definitions.isFieldIgnored(objectRef, field.name)) { - this.logger.debug(`Ignore field ${field.name} on ${type.name} (datapack: ${datapack.datapackType}, scope: ${datapack.scope}) as it is explicitly ignored by config`); - return true; - } - return false; } diff --git a/packages/vlocity-deploy/src/export/datapackNormalizer.ts b/packages/vlocity-deploy/src/export/datapackNormalizer.ts new file mode 100644 index 00000000..ca835123 --- /dev/null +++ b/packages/vlocity-deploy/src/export/datapackNormalizer.ts @@ -0,0 +1,87 @@ +import { VlocityDatapackSObject } from '@vlocode/vlocity'; +import { DatapackExportDefinitionStore, ObjectRef } from './exportDefinitionStore'; +import { createFieldComparator, createVlocityComparator, sortRecordHierarchy } from './datapackSorting'; +import { DatapackObject, DatapackValue, isDatapackObject, sortObjectKeys } from './datapackValue'; + +interface PreparedField { + field: string; + sourceValue: DatapackValue; + value: Exclude; +} + +/** Shared output rules for consolidated exports and expanded files. Never mutates input. */ +export class DatapackNormalizer { + constructor(private readonly definitions: DatapackExportDefinitionStore) {} + + public normalizeRecord(record: VlocityDatapackSObject, ref: ObjectRef): VlocityDatapackSObject; + public normalizeRecord(record: DatapackObject, ref: ObjectRef): DatapackObject; + public normalizeRecord(record: DatapackObject, ref: ObjectRef): DatapackObject { + return Object.fromEntries(Array.from(this.prepareFields(record, ref), ({ field, value }) => [field, value])); + } + + /** Retain sorted source values for filenames while preparing values for serialization. */ + public *prepareFields(record: DatapackObject, ref: ObjectRef): Generator { + for (const field of Object.keys(record).sort()) { + if (this.definitions.isFieldIgnored(ref, field)) { + continue; + } + const sourceValue = this.sortField(ref, field, record[field]); + const value = this.normalizeField(ref, sourceValue); + if (value !== undefined) { + yield { field, sourceValue, value }; + } + } + } + + private normalizeField(ref: ObjectRef, value: DatapackValue): DatapackValue { + const policy = this.definitions.get(ref, 'nullValues'); + if (policy === 'omit' && (value === null || value === '')) { + return undefined; + } + if (policy === 'emptyString' && value === null) { + return ''; + } + return this.normalizeValue(value, ref.scope); + } + + private normalizeValue(value: DatapackValue, scope?: string): DatapackValue { + if (Array.isArray(value)) { + return value.map(item => this.normalizeValue(item, scope)); + } + if (isDatapackObject(value) && value.VlocityDataPackType === 'SObject' && typeof value.VlocityRecordSObjectType === 'string') { + return this.normalizeRecord(value, { objectType: value.VlocityRecordSObjectType, scope }); + } + // Matching objects retain their lookup fields, including fields omitted from SObject bodies. + return sortObjectKeys(value); + } + + public sortField(ref: ObjectRef, field: string, value: DatapackValue): DatapackValue { + if (!Array.isArray(value) || !value.length || !value.every(isDatapackObject)) { + return value; + } + const configuredFields = this.definitions.getFieldConfig(ref, field, 'sortFields'); + // Parsed JSON arrays can be positional. Sort them only when explicitly configured. + if (configuredFields === undefined && this.definitions.getFieldConfig(ref, field, 'parseJson') === true) { + return value; + } + const fields = configuredFields?.length ? configuredFields : this.defaultSortFields(value); + const mode = this.definitions.getFieldConfig(ref, field, 'sortMode'); + const compare = mode === 'vlocity' + ? createVlocityComparator(fields, record => this.normalizeValue(record, ref.scope)) + : createFieldComparator(fields); + const parentField = this.definitions.getFieldConfig(ref, field, 'sortParentField'); + return typeof parentField === 'string' + ? sortRecordHierarchy(value, parentField, compare) + : [...value].sort(compare); + } + + private defaultSortFields(records: DatapackObject[]): string[] { + if (records.every(record => record.VlocityRecordSourceKey != null)) { + return ['VlocityRecordSourceKey']; + } + if (records.every(record => record.Name != null)) { + return ['Name']; + } + return []; + } +} diff --git a/packages/vlocity-deploy/src/export/datapackSorting.ts b/packages/vlocity-deploy/src/export/datapackSorting.ts new file mode 100644 index 00000000..182155a0 --- /dev/null +++ b/packages/vlocity-deploy/src/export/datapackSorting.ts @@ -0,0 +1,113 @@ +import { primitiveCompare } from '@vlocode/util'; +import { DatapackObject, DatapackValue, getFieldValue, isDatapackObject, sortObjectKeys } from './datapackValue'; + +type RecordComparator = (left: DatapackObject, right: DatapackObject) => number; +type SortValue = string | number | boolean | null; + +export function createFieldComparator(fields: readonly string[]): RecordComparator { + return (left, right) => { + for (const field of fields) { + const comparison = primitiveCompare(getFieldValue(left, field), getFieldValue(right, field)); + if (comparison !== 0) { + return comparison; + } + } + return 0; + }; +} + +/** Build Tools places empty values last and breaks field ties using normalized JSON. */ +export function createVlocityComparator( + fields: readonly string[], + normalize: (record: DatapackObject) => DatapackValue +): RecordComparator { + const fieldValues = new Map(); + const normalizedJson = new Map(); + + function values(record: DatapackObject): SortValue[] { + let result = fieldValues.get(record); + if (!result) { + result = fields.map(field => toSortValue(getFieldValue(record, field))); + fieldValues.set(record, result); + } + return result; + } + + function json(record: DatapackObject): string { + let result = normalizedJson.get(record); + if (result === undefined) { + result = JSON.stringify(normalize(record)); + normalizedJson.set(record, result); + } + return result; + } + + return (left, right) => { + const leftValues = values(left); + const rightValues = values(right); + for (let index = 0; index < fields.length; index++) { + const comparison = compareSortValues(leftValues[index], rightValues[index]); + if (comparison !== 0) { + return comparison; + } + } + return compareSortValues(json(left), json(right)); + }; +} + +function toSortValue(value: DatapackValue): SortValue { + if (!value && value !== 0) { + return null; + } + return typeof value === 'object' ? JSON.stringify(sortObjectKeys(value)) : value; +} + +function compareSortValues(left: SortValue, right: SortValue): number { + if (left === null) return right === null ? 0 : 1; + if (right === null) return -1; + return left < right ? -1 : left > right ? 1 : 0; +} + +/** Depth-first hierarchy order, using the same comparison for roots and siblings. */ +export function sortRecordHierarchy(records: DatapackObject[], parentField: string, compare: RecordComparator): DatapackObject[] { + const byKey = new Map(records.map(record => [record.VlocityRecordSourceKey, record])); + const children = new Map(); + for (const record of records) { + const reference = getFieldValue(record, parentField); + const parentKey = isDatapackObject(reference) ? reference.VlocityMatchingRecordSourceKey : undefined; + const parent = parentKey == null ? undefined : byKey.get(parentKey); + const siblings = children.get(parent) ?? []; + siblings.push(record); + children.set(parent, siblings); + } + for (const siblings of children.values()) { + siblings.sort(compare); + } + + const output: DatapackObject[] = []; + const visiting = new Set(); + const visited = new Set(); + function visit(record: DatapackObject): void { + if (visiting.has(record)) { + throw new Error(`Circular export hierarchy at ${record.VlocityRecordSourceKey}`); + } + visiting.add(record); + output.push(record); + for (const child of children.get(record) ?? []) { + visit(child); + } + visiting.delete(record); + visited.add(record); + } + + for (const root of children.get(undefined) ?? []) { + visit(root); + } + // Cycles may have no root. Check the remainder instead of silently dropping records. + for (const record of records) { + if (!visited.has(record)) { + visit(record); + } + } + return output; +} diff --git a/packages/vlocity-deploy/src/export/datapackValue.ts b/packages/vlocity-deploy/src/export/datapackValue.ts new file mode 100644 index 00000000..dc60c86e --- /dev/null +++ b/packages/vlocity-deploy/src/export/datapackValue.ts @@ -0,0 +1,31 @@ +import { removeNamespacePrefix } from '@vlocode/util'; + +export type DatapackValue = string | number | boolean | null | undefined | Buffer | DatapackObject | DatapackValue[]; +export interface DatapackObject { + [field: string]: DatapackValue; +} + +export function isDatapackObject(value: DatapackValue): value is DatapackObject { + return value !== null && typeof value === 'object' && !Array.isArray(value) && !Buffer.isBuffer(value); +} + +/** Read API fields regardless of namespace prefix or casing. */ +export function getFieldValue(record: DatapackObject, field: string): DatapackValue { + if (record[field] != null) { + return record[field]; + } + const normalizedField = removeNamespacePrefix(field).toLowerCase(); + const key = Object.keys(record).find(key => removeNamespacePrefix(key).toLowerCase() === normalizedField); + return key === undefined ? undefined : record[key]; +} + +/** Canonical JSON properties; array order and binary values are preserved. */ +export function sortObjectKeys(value: DatapackValue): DatapackValue { + if (Array.isArray(value)) { + return value.map(sortObjectKeys); + } + if (isDatapackObject(value)) { + return Object.fromEntries(Object.keys(value).sort().map(key => [key, sortObjectKeys(value[key])])); + } + return value; +} diff --git a/packages/vlocity-deploy/src/export/exportDefinitionStore.ts b/packages/vlocity-deploy/src/export/exportDefinitionStore.ts index 6f84a0c7..0916aa06 100644 --- a/packages/vlocity-deploy/src/export/exportDefinitionStore.ts +++ b/packages/vlocity-deploy/src/export/exportDefinitionStore.ts @@ -159,6 +159,9 @@ export class DatapackExportDefinitionStore { // Normalize as loaded YAML definitions can specify a single field as scalar value config.matchingKeyFields = asArray(config.matchingKeyFields).map(String); } + if (config.exportKey !== undefined) { + config.exportKey = asArray(config.exportKey).map(String); + } const definitions = this.getDefinitionStore(context); const objectType = normalizeSObjectTypeName(config.objectType ?? datapackType); (definitions[datapackType] ??= {})[objectType] = config; @@ -212,6 +215,20 @@ export class DatapackExportDefinitionStore { return undefined; } + public getExportKey(item: ObjectRef): string[] | undefined { + for (const definition of this.findObjectDefinitions(item)) { + // An export selected by name must not pick up a sibling's export key from another scope. + if (item.datapackType && definition.datapackType !== item.datapackType + && normalizeSObjectTypeName(definition.datapackType) !== normalizeSObjectTypeName(item.objectType)) { + continue; + } + if (definition.exportKey !== undefined) { + return [...definition.exportKey]; + } + } + return undefined; + } + /** * Iterate definitions matching an SObject type. A selected datapack definition is yielded first, * followed by other object definitions in the selected scope and then global fallbacks. Without @@ -242,12 +259,18 @@ export class DatapackExportDefinitionStore { } private getDefinition(scope: string | symbol, datapackType: string, objectType: string) { - return this.config[scope]?.[datapackType]?.[normalizeSObjectTypeName(objectType)]; + const definitions = this.config[scope]; + const byType = definitions?.[datapackType] ?? Object.entries(definitions ?? {}).find(([name]) => + normalizeSObjectTypeName(name) === normalizeSObjectTypeName(datapackType))?.[1]; + return byType?.[normalizeSObjectTypeName(objectType)]; } public getFieldConfig(item: ObjectRef, field: string, configKey?: keyof ExportFieldDefinition) { - const fieldConfig = this.get(item, 'fields')?.[field]; - const embeddedConfig = this.get(item, 'embeddedObjects')?.[field]; + const findField = (fields: Record | undefined): T | undefined => + fields?.[field] ?? Object.entries(fields ?? {}).find(([name]) => + removeNamespacePrefix(name).toLowerCase() === removeNamespacePrefix(field).toLowerCase())?.[1]; + const fieldConfig = findField(this.get(item, 'fields')); + const embeddedConfig = findField(this.get(item, 'embeddedObjects')); if (configKey) { return fieldConfig?.[configKey] ?? embeddedConfig?.[configKey]; } diff --git a/packages/vlocity-deploy/src/export/exportDefinitions.schema.json b/packages/vlocity-deploy/src/export/exportDefinitions.schema.json index 888f0f1a..9a2f2f13 100644 --- a/packages/vlocity-deploy/src/export/exportDefinitions.schema.json +++ b/packages/vlocity-deploy/src/export/exportDefinitions.schema.json @@ -21,6 +21,13 @@ "description": "List of objects that should be embedded in this datapack. The key is the name under which embedded records are exported.", "type": "object" }, + "exportKey": { + "description": "Ordered fields used to write VlocityRecordSourceKey during expansion only. Does not affect record matching, lookup resolution or unexpanded export keys. Values are joined with slashes and prefixed with the object type, using namespace-qualified field names. The same definition applies to serialized matching and lookup reference keys, preserving their matching fields. Lookup fields use the referenced export key. When omitted or empty, the original source key is retained.", + "items": { + "type": "string" + }, + "type": "array" + }, "fields": { "additionalProperties": { "$ref": "#/definitions/ExportFieldDefinition" @@ -33,7 +40,7 @@ "description": "Where condition to filter the object data to export by. Can be a string or an object. When an object, the key is the field name and the value is the value to filter by. Accessing fields from the parent datapack using bracket syntax `{}` is supported: ``` filter: AccountId: $parent.Id IsActive: true ```\n\nWhen defined as a string, the string is used as the where condition and interpolated this allows for more complex where conditions: ```yaml filter: \"AccountId = '{parent.Id}' AND IsActive = 'true'\" ```" }, "ignoreFields": { - "description": "List of fields to exclude from the export.", + "description": "Fields omitted from serialized output, after sorting and file-name evaluation.", "items": { "type": "string" }, @@ -64,10 +71,23 @@ ], "description": "Name format of the datapack, used to create the folder name of the datapack, when a filename is not set this is also used as the file name.\n\nWhen this is a string the string is parsed as format and values between brackets are replace by field values from the object to export, i.e `{Name}`. When this is defined as an array each value in the array is evaluated as a field value expression and constants need to be prixed with a `_`: ```yaml name:\n- Name\n- _constantValue # OR name: \"{Name}_constantValue\" ```" }, + "nullValues": { + "description": "How null/empty SObject fields are serialized. Defaults to preserving their values.", + "enum": [ + "preserve", + "omit", + "emptyString" + ], + "type": "string" + }, "objectType": { "description": "Type of SObject to which this filter applies", "type": "string" }, + "parseJson": { + "description": "Parse JSON-looking field strings. Defaults to true; individual fields can override this.", + "type": "boolean" + }, "relatedObjects": { "description": "List of related objects that are exported as separate datapacks but are related to this object and should be included in the export.", "items": { @@ -128,6 +148,10 @@ "description": "Type of SObject to which this filter applies", "type": "string" }, + "parseJson": { + "description": "Override JSON parsing for this field. Does not affect strings inside JSON documents.", + "type": "boolean" + }, "processor": { "description": "Processor function snippet to use for processing the field data before exporting.", "type": "string" @@ -138,6 +162,18 @@ "type": "string" }, "type": "array" + }, + "sortMode": { + "description": "Vlocity comparison puts empty values last and breaks ties with canonical JSON.", + "enum": [ + "default", + "vlocity" + ], + "type": "string" + }, + "sortParentField": { + "description": "Parent lookup field used for depth-first traversal; sortFields order siblings.", + "type": "string" } }, "required": [ @@ -181,6 +217,10 @@ }, "type": "array" }, + "parseJson": { + "description": "Override JSON parsing for this field. Does not affect strings inside JSON documents.", + "type": "boolean" + }, "processor": { "description": "Processor function snippet to use for processing the field data before exporting.", "type": "string" @@ -195,6 +235,18 @@ "type": "string" }, "type": "array" + }, + "sortMode": { + "description": "Vlocity comparison puts empty values last and breaks ties with canonical JSON.", + "enum": [ + "default", + "vlocity" + ], + "type": "string" + }, + "sortParentField": { + "description": "Parent lookup field used for depth-first traversal; sortFields order siblings.", + "type": "string" } }, "required": [ @@ -240,6 +292,10 @@ }, "type": "array" }, + "parseJson": { + "description": "Override JSON parsing for this field. Does not affect strings inside JSON documents.", + "type": "boolean" + }, "processor": { "description": "Processor function snippet to use for processing the field data before exporting.", "type": "string" @@ -250,6 +306,18 @@ "type": "string" }, "type": "array" + }, + "sortMode": { + "description": "Vlocity comparison puts empty values last and breaks ties with canonical JSON.", + "enum": [ + "default", + "vlocity" + ], + "type": "string" + }, + "sortParentField": { + "description": "Parent lookup field used for depth-first traversal; sortFields order siblings.", + "type": "string" } }, "type": "object" diff --git a/packages/vlocity-deploy/src/export/exportDefinitions.ts b/packages/vlocity-deploy/src/export/exportDefinitions.ts index 19deb654..e8bffef8 100644 --- a/packages/vlocity-deploy/src/export/exportDefinitions.ts +++ b/packages/vlocity-deploy/src/export/exportDefinitions.ts @@ -49,13 +49,25 @@ export interface DatapackExportDefinition extends ObjectFilter { * external lookup reference. */ matchingKeyFields?: string[]; + /** + * Ordered fields used to write VlocityRecordSourceKey during expansion only. + * Does not affect record matching, lookup resolution or unexpanded export keys. Values are joined with slashes and prefixed with the object type, using + * namespace-qualified field names. The same definition applies to serialized matching and + * lookup reference keys, preserving their matching fields. Lookup fields use the referenced export key. + * When omitted or empty, the original source key is retained. + */ + exportKey?: string[]; + /** How null/empty SObject fields are serialized. Defaults to preserving their values. */ + nullValues?: 'preserve' | 'omit' | 'emptyString'; + /** Parse JSON-looking field strings. Defaults to true; individual fields can override this. */ + parseJson?: boolean; /** * When true the matching key is auto-generated random number instead of a value based on the object data. * autogenerated matching keys only work for none-lookup fields. */ autoGeneratedMatchingKey?: boolean; /** - * List of fields to exclude from the export. + * Fields omitted from serialized output, after sorting and file-name evaluation. */ ignoreFields?: string[]; /** @@ -80,6 +92,8 @@ export interface DatapackExportDefinition extends ObjectFilter { } export interface ExportFieldDefinition extends ExpandFileDefinition { + /** Override JSON parsing for this field. Does not affect strings inside JSON documents. */ + parseJson?: boolean; /** * Processor function snippet to use for processing the field data before exporting. */ @@ -109,6 +123,10 @@ export interface ExportFieldDefinition extends ExpandFileDefinition { * when either field is present on every record; otherwise the original array order is preserved. */ sortFields?: string[]; + /** Parent lookup field used for depth-first traversal; sortFields order siblings. */ + sortParentField?: string; + /** Vlocity comparison puts empty values last and breaks ties with canonical JSON. */ + sortMode?: 'default' | 'vlocity'; } export type LookupFilerPrimitive = string | number | boolean | null; diff --git a/packages/vlocity-deploy/src/exportDefinitions/omnistudioManagedDatapackDefinitions.yaml b/packages/vlocity-deploy/src/exportDefinitions/omnistudioManagedDatapackDefinitions.yaml index 52d26121..303a5d94 100644 --- a/packages/vlocity-deploy/src/exportDefinitions/omnistudioManagedDatapackDefinitions.yaml +++ b/packages/vlocity-deploy/src/exportDefinitions/omnistudioManagedDatapackDefinitions.yaml @@ -8,6 +8,12 @@ definitions: - '%vlocity_namespace%__Type__c' - '%vlocity_namespace%__SubType__c' - '%vlocity_namespace%__Language__c' + exportKey: + - "%vlocity_namespace%__Type__c" + - "%vlocity_namespace%__SubType__c" + - "%vlocity_namespace%__Language__c" + nullValues: omit + parseJson: false matchingKeyFields: - '%vlocity_namespace%__Type__c' - '%vlocity_namespace%__SubType__c' @@ -18,16 +24,18 @@ definitions: ignoreFields: - '%vlocity_namespace%__IsActive__c' - '%vlocity_namespace%__Version__c' - fields: + - '%vlocity_namespace%__LwcId__c' + fields: &managedScriptFields '%vlocity_namespace%__CustomJavaScript__c': fileName: - - _JavaScript + - _JavaScript.js '%vlocity_namespace%__PropertySet__c': + parseJson: true fileName: - _PropertySet '%vlocity_namespace%__TestHTMLTemplates__c': fileName: - - _HTMLTemplates + - _HTMLTemplates.html embeddedObjects: '%vlocity_namespace%__Element__c': objectType: '%vlocity_namespace%__Element__c' @@ -38,8 +46,8 @@ definitions: - _Element - Name expandArray: true + sortParentField: '%vlocity_namespace%__ParentElementId__c' sortFields: - - '%vlocity_namespace%__Level__c' - '%vlocity_namespace%__Order__c' IntegrationProcedure: @@ -49,31 +57,42 @@ definitions: - '%vlocity_namespace%__SubType__c' filter: '%vlocity_namespace%__IsProcedure__c': true + fields: + <<: *managedScriptFields + '%vlocity_namespace%__CustomJavaScript__c': + parseJson: true + fileName: + - _SampleInput DataRaptor: &managedDataRaptor objectType: '%vlocity_namespace%__DRBundle__c' name: - Name + nullValues: emptyString + parseJson: false matchingKeyFields: - Name ignoreFields: - '%vlocity_namespace%__FormulaConverted__c' fields: '%vlocity_namespace%__InputJson__c': + parseJson: true fileName: - _InputJson '%vlocity_namespace%__SampleInputJSON__c': + parseJson: true fileName: - _SampleInputJson '%vlocity_namespace%__SampleInputXML__c': fileName: - - _SampleInputXML + - _SampleInputXML.xml '%vlocity_namespace%__TargetOutJson__c': + parseJson: true fileName: - _TargetOutJson '%vlocity_namespace%__TargetOutXml__c': fileName: - - _TargetOutXml + - _TargetOutXml.xml embeddedObjects: '%vlocity_namespace%__DRMapItem__c': objectType: '%vlocity_namespace%__DRMapItem__c' @@ -81,20 +100,35 @@ definitions: Name: '{%vlocity_namespace%__DRBundle__c:Name}' fileName: - _Mappings + sortMode: vlocity + sortFields: + - '%vlocity_namespace%__InterfaceObjectLookupOrder__c' + - '%vlocity_namespace%__FilterGroup__c' + - '%vlocity_namespace%__DomainObjectCreationOrder__c' + - '%vlocity_namespace%__DomainObjectAPIName__c' + - '%vlocity_namespace%__DomainObjectFieldAPIName__c' + - '%vlocity_namespace%__InterfaceFieldAPIName__c' '%vlocity_namespace%__Element__c': dependent: true objectType: '%vlocity_namespace%__Element__c' name: - Name + exportKey: + - '%vlocity_namespace%__OmniScriptId__c' + - Name matchingKeyFields: - '%vlocity_namespace%__OmniScriptId__c' - Name + nullValues: omit + parseJson: false ignoreFields: + - '%vlocity_namespace%__Order__c' - '%vlocity_namespace%__OmniScriptVersion__c' - '%vlocity_namespace%__Level__c' fields: '%vlocity_namespace%__PropertySet__c': + parseJson: true fileName: - _PropertySet @@ -106,5 +140,7 @@ definitions: matchingKeyFields: - '%vlocity_namespace%__MapId__c' - Name + nullValues: omit + parseJson: false ignoreFields: - '%vlocity_namespace%__FormulaConverted__c' diff --git a/packages/vlocity-deploy/src/exportDefinitions/omnistudioStandardDatapackDefinitions.yaml b/packages/vlocity-deploy/src/exportDefinitions/omnistudioStandardDatapackDefinitions.yaml index db5411de..34e45974 100644 --- a/packages/vlocity-deploy/src/exportDefinitions/omnistudioStandardDatapackDefinitions.yaml +++ b/packages/vlocity-deploy/src/exportDefinitions/omnistudioStandardDatapackDefinitions.yaml @@ -8,6 +8,12 @@ definitions: - Type - SubType - Language + exportKey: + - Type + - SubType + - Language + nullValues: omit + parseJson: false matchingKeyFields: - Type - SubType @@ -18,16 +24,17 @@ definitions: - VersionNumber filter: IsIntegrationProcedure: false - fields: + fields: &standardScriptFields CustomJavaScript: fileName: - - _JavaScript + - _JavaScript.js PropertySetConfig: + parseJson: true fileName: - _PropertySetConfig CustomHtmlTemplates: fileName: - - _HTMLTemplates + - _HTMLTemplates.html embeddedObjects: OmniProcessElement: objectType: OmniProcessElement @@ -37,8 +44,8 @@ definitions: - _Element - Name expandArray: true + sortParentField: ParentElementId sortFields: - - Level - SequenceNumber IntegrationProcedure: @@ -48,29 +55,48 @@ definitions: - SubType filter: IsIntegrationProcedure: true + ignoreFields: + - IsActive + - VersionNumber + - WebComponentKey + fields: + <<: *standardScriptFields + CustomJavaScript: + parseJson: true + fileName: + - _SampleInput DataRaptor: &omniDataTransform objectType: OmniDataTransform name: - Name + nullValues: emptyString + parseJson: false matchingKeyFields: - Name + ignoreFields: + - IsActive fields: + GlobalKey: + processor: return datapack.Name; ExpectedInputJson: + parseJson: true fileName: - _InputJson PreviewJsonData: + parseJson: true fileName: - _SampleInputJson PreviewXmlData: fileName: - - _SampleInputXML + - _SampleInputXML.xml ExpectedOutputJson: + parseJson: true fileName: - _TargetOutJson ExpectedOutputXml: fileName: - - _TargetOutXml + - _TargetOutXml.xml embeddedObjects: OmniDataTransformItem: objectType: OmniDataTransformItem @@ -78,16 +104,30 @@ definitions: OmniDataTransformationId: '{OmniDataTransform:Id}' fileName: - _Items + sortMode: vlocity + sortFields: + - InputObjectQuerySequence + - FilterGroup + - OutputCreationSequence + - OutputObjectName + - OutputFieldName + - InputFieldName OmniProcessElement: dependent: true objectType: OmniProcessElement name: - Name + exportKey: + - OmniProcessId + - Name matchingKeyFields: - OmniProcessId - Name + nullValues: omit + parseJson: false ignoreFields: + - SequenceNumber - Level - OmniProcessVersionNumber - IsOmniScriptEmbeddable @@ -95,6 +135,7 @@ definitions: - ParentElementType fields: PropertySetConfig: + parseJson: true fileName: - _PropertySetConfig @@ -103,5 +144,7 @@ definitions: objectType: OmniDataTransformItem name: - GlobalKey + nullValues: omit + parseJson: false ignoreFields: - FormulaConverted diff --git a/packages/vlocity/src/__tests__/vlocityNamespaceService.test.ts b/packages/vlocity/src/__tests__/vlocityNamespaceService.test.ts index 18d4e7a8..cf279c7d 100644 --- a/packages/vlocity/src/__tests__/vlocityNamespaceService.test.ts +++ b/packages/vlocity/src/__tests__/vlocityNamespaceService.test.ts @@ -5,6 +5,19 @@ import { VlocityNamespaceService } from '../vlocityNamespaceService'; describe('VlocityNamespaceService', () => { + it('round-trips namespaced API paths in JSON strings without replacing ordinary text', () => { + const service = new VlocityNamespaceService('vlocity_cmt'); + const value = { sample: '{"url":"/services/apexrest/vlocity_cmt/v1/catalog","label":"vlocity_cmt","other":"/vlocity_cmt_extra/v1"}' }; + const original = value.sample; + const portable = service.replaceObjectNamespace(value); + expect(JSON.parse(portable.sample)).toEqual({ + url: '/services/apexrest/%vlocity_namespace%/v1/catalog', + label: 'vlocity_cmt', + other: '/vlocity_cmt_extra/v1' + }); + expect(service.updateNamespace(portable.sample)).toBe(original); + }); + function createConnectionProvider(connection: any) { return { getJsForceConnection: jest.fn().mockResolvedValue(connection) diff --git a/packages/vlocity/src/constants.ts b/packages/vlocity/src/constants.ts index 77a94c22..07a574c2 100644 --- a/packages/vlocity/src/constants.ts +++ b/packages/vlocity/src/constants.ts @@ -6,5 +6,6 @@ export const DATAPACK_RESERVED_FIELDS = [ 'VlocityMatchingRecordSourceKey', 'VlocityLookupRecordSourceKey', 'VlocityRecordSourceKey', - 'VlocityRecordSObjectType' -]; \ No newline at end of file + 'VlocityRecordSObjectType', + 'VlocityRecordExportKeyValues' +]; diff --git a/packages/vlocity/src/datapack/datapack.ts b/packages/vlocity/src/datapack/datapack.ts index 59b853dd..a97decd6 100644 --- a/packages/vlocity/src/datapack/datapack.ts +++ b/packages/vlocity/src/datapack/datapack.ts @@ -29,6 +29,8 @@ export const DatapackFields = { matchingKey: 'VlocityMatchingRecordSourceKey', lookupKey: 'VlocityLookupRecordSourceKey', dataPackType: 'VlocityDataPackType', + /** Identity inputs retained until expansion; never used as lookup criteria. */ + exportKeyValues: 'VlocityRecordExportKeyValues', } as const; /** @@ -424,4 +426,4 @@ class VlocityDatapackDataProxy extends PropertyTransformHandle const normalizedName = VlocityDatapackDataProxy.nameTransformer(name.toString()); return Object.keys(target).find(key => VlocityDatapackDataProxy.nameTransformer(key) === normalizedName); } -} \ No newline at end of file +} diff --git a/packages/vlocity/src/vlocityNamespaceService.ts b/packages/vlocity/src/vlocityNamespaceService.ts index d246c8b0..4c6dcb8a 100644 --- a/packages/vlocity/src/vlocityNamespaceService.ts +++ b/packages/vlocity/src/vlocityNamespaceService.ts @@ -34,7 +34,9 @@ export class VlocityNamespaceService extends NamespaceService { return name; } const namespacePattern = new RegExp(`${this.escapeRegExp(this.vlocityNamespace)}__`, 'g'); - return name.replace(namespacePattern, `${constants.NAMESPACE_PLACEHOLDER}__`); + const namespacePathPattern = new RegExp(`/${this.escapeRegExp(this.vlocityNamespace)}(?=/)`, 'g'); + return name.replace(namespacePattern, `${constants.NAMESPACE_PLACEHOLDER}__`) + .replace(namespacePathPattern, `/${constants.NAMESPACE_PLACEHOLDER}`); } /**