Setting the file. One moment. Field Mapping Examples · Wix Replatform · wix/skills · Skills Docslib/field-mapping-examples.js
JavaScript·93 lines·7 KB
);
8const { canonical, digest, entityRows, entityRef, validateEntity, assertExecutionScope } = require('./mapping-field-contract.js');
9function readValues(value, fieldPath) {
10 const tokens = fieldPath.replace(/\[\]/g, '.[]').split('.');
11 function walk(v,i) {
12 if (i===tokens.length) return [v];
13 if (tokens[i]==='[]') return Array.isArray(v) ? v.flatMap(x=>walk(x,i+1)) : [undefined];
14 return walk(v && Object.hasOwn(v,tokens[i]) ? v[tokens[i]] : undefined,i+1);
15 }
16 return walk(value,0);
17}
18// An example names only the fields its decision reads. A writer that checks the whole request
19// (Import Order needs status and buyer details) needs the rest: the entity's mapper-authored
20// `exampleBase` supplies it, and the example's own values win.
21function exampleInput(base, input) {
22 if (base===undefined) return structuredClone(input);
23 const plain=v=>v!==null && typeof v==='object' && !Array.isArray(v);
24 const merge=(a,b)=>plain(a) && plain(b) ? Object.fromEntries([...new Set([...Object.keys(a),...Object.keys(b)])].map(k=>[k,Object.hasOwn(b,k)?merge(a[k],b[k]):structuredClone(a[k])])) : structuredClone(b);
25 return merge(base,input);
26}
27async function runFieldMappingExamples({ projectDir, plan, mappingPlanDigest, execute, passId=crypto.randomUUID() }={}) {
28 if (!projectDir || !plan || typeof execute!=='function' || !/^[a-zA-Z0-9_-]+$/.test(passId)) throw new Error('projectDir, plan, execute and safe passId required');
29 if (!mappingPlanDigest || digest(plan)!==mappingPlanDigest) throw new Error('synthetic examples require the independently pinned mapping digest');
30 const captureRef=`state/attempts/field-mapping-examples/${passId}.ndjson`;
31 const capturePath=path.join(projectDir,captureRef);
32 fs.mkdirSync(path.dirname(capturePath),{recursive:true});
33 // Exclusive creation retains all older passes and prevents accidental evidence overwrite.
34 fs.closeSync(fs.openSync(capturePath,'wx'));
35 const rows=[], mappingQuestions=[];
36 for (const entity of entityRows(plan)) {
37 const ref=entityRef(entity);
38 const questions=validateEntity(entity);
39 if (questions.length) {
40 mappingQuestions.push({sourceEntityRef:ref,returnTo:'mapping',questions});
41 rows.push({sourceEntityRef:ref,decisionId:null,outputId:null,exampleId:null,writeOperation:null,passId,
42 inputDigest:null,expectedDigest:null,captureRef,captureDigest:null,requestCaptureIds:[],expected:null,actual:[],
43 status:'not-run',returnTo:'mapping',reason:questions.join('; ')});
44 continue;
45 }
46 if (!plan.executionScope.resolvedEntityRefs.includes(ref) || entity.targetClassification==='manual-mapping') {
47 rows.push({sourceEntityRef:ref,decisionId:null,outputId:null,exampleId:null,writeOperation:null,passId,
48 inputDigest:null,expectedDigest:null,captureRef,captureDigest:null,requestCaptureIds:[],expected:null,actual:[],
49 status:'not-run',reason:'Deferred scope or a manual runbook has no generated write to exercise.'});
50 continue;
51 }
52 assertExecutionScope(plan,ref,mappingPlanDigest,projectDir);
53 for (const decision of entity.fieldMappings) for (const example of decision.examples || []) {
54 // No configuration spread: credentials, projectDir and live sinks cannot leak in.
55 const wix=createWixCaptureClient({requestCapturePath:capturePath,
56 runId:`field-example:${passId}:${decision.id}:${example.id}`});
57 const before=fs.readFileSync(capturePath,'utf8').split('\n').filter(Boolean).length;
58 let findings=[], error=null;
59 const input=exampleInput(entity.exampleBase,example.input);
60 try {
61 const result=await execute({ sourceEntityRef:ref, decisionId:decision.id,
62 // The callback executes the actual generated transform and request builder.
63 input, wix, crosswalk:new Map(), exampleId:example.id });
64 findings=result?.findings || [];
65 } catch(e) { error=e.code || e.name || 'example_failed'; }
66 const captures=fs.readFileSync(capturePath,'utf8').split('\n').filter(Boolean).slice(before).map(JSON.parse);
67 for (const expected of example.expected) {
68 const output=decision.outputs.find(o=>o.id===expected.outputId);
69 if (!output || expected.writeOperation!==output.writeOperation) throw new Error('example expectation must bind an approved output and operation');
70 const boundPath=`${output.writeInput}.${output.targetPath}`;
71 const matching=captures.filter(c=>c.operation===output.writeOperation);
72 const actual=matching.flatMap(c=>readValues(c.body,boundPath));
73 const expectedValues=expected.values || (Object.hasOwn(expected,'value')?[expected.value]:[]);
74 const passed=!error && (expected.noWrite===true ? matching.length===0 && expected.findingCodes?.length>0 : expected.omitted===true ? matching.length>0 && actual.every(v=>v===undefined) : actual.length>0 && actual.every(v=>v!==undefined) && canonical(actual)===canonical(expectedValues)) &&
75 (expected.findingCodes || []).every(code=>findings.some(f=>f.code===code));
76 rows.push({ sourceEntityRef:ref,decisionId:decision.id,outputId:output.id,exampleId:example.id,
77 writeOperation:output.writeOperation,passId,inputDigest:digest(input),expectedDigest:digest(expected),
78 captureRef,captureDigest:captures.length?digest(captures):null,
79 requestCaptureIds:matching.map(c=>c.requestCaptureId),expected,actual:actual.map(v=>v===undefined?{omitted:true}:v),
80 findings,status:passed?'passed':'failed',reason:error || (passed?null:'prepared request differs from the independently authored expectation') });
81 }
82 }
83 }
84 return { passId,captureRef,rows,mappingQuestions,ok:mappingQuestions.length===0 && rows.length>0 && rows.filter(r=>r.status!=='not-run').length>0 && rows.filter(r=>r.status!=='not-run').every(r=>r.status==='passed'),
85 liveVerified:false };
86}
87function renderExampleResults(result) {
88 const safe=v=>JSON.stringify(v).replace(/\|/g,'\\|');
89 return ['## Field mapping examples','', 'Synthetic prepared requests only; no live verification.', '',
90 '| Decision / output / example | Operation | Expected / actual | Status / reason | Evidence |',
91 '| --- | --- | --- | --- | --- |', ...result.rows.map(r=>`| ${r.decisionId} / ${r.outputId} / ${r.exampleId} | ${r.writeOperation} | ${safe(r.expected)} / ${safe(r.actual)} | ${r.status}: ${r.reason || ''} | ${r.captureRef}; pass ${r.passId}; input ${r.inputDigest}; expected ${r.expectedDigest}; capture ${r.captureDigest} |`)].join('\n')+'\n';
92}
93module.exports={runFieldMappingExamples,renderExampleResults,readValues,exampleInput};