Setting the file. One moment.
Completion Report · Wix Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page
Number 47.16
Position 16 of 34
Type JavaScript
Size 13 KB
Lines 290 lib/ completion-report.js
JavaScript · 290 lines · 13 KB
11 'complete_with_warnings' ,
12 'complete' ,
13 ];
14
15 const STATUS_RANK = new Map ( COMPLETION_STATUS . map (( status , index ) => [status, index]));
16
17 function count ( value , field ) {
18 if (value === undefined || value === null ) {
19 return 0 ;
20 }
21 if ( ! Number. isInteger (value) || value < 0 ) {
22 throw new Error ( `${ field } must be a non-negative integer` );
23 }
24 return value;
25 }
26
27 function requireString ( value , field ) {
28 if ( ! value || typeof value !== 'string' ) {
29 throw new Error ( `${ field } must be a non-empty string` );
30 }
31 return value;
32 }
33
34 function asArray ( value , field ) {
35 if (value === undefined || value === null ) {
36 return [];
37 }
38 if ( ! Array. isArray (value)) {
39 throw new Error ( `${ field } must be an array` );
40 }
41 return value;
42 }
43
44 function hasCount ( rows , field ) {
45 return rows. some (( row ) => row[field] > 0 );
46 }
47
48 function worseStatus ( left , right ) {
49 const leftRank = STATUS_RANK . has (left) ? STATUS_RANK . get (left) : STATUS_RANK . get ( 'complete' );
50 const rightRank = STATUS_RANK . has (right) ? STATUS_RANK . get (right) : STATUS_RANK . get ( 'complete' );
51 return leftRank <= rightRank ? left : right;
52 }
53
54 function chooseCompletionStatus ( input = {}) {
55 const statuses = asArray (input.statuses, 'statuses' );
56 let selected = input.defaultStatus || 'complete' ;
57 if ( ! STATUS_RANK . has (selected)) {
58 throw new Error ( `unknown completion status: ${ selected }` );
59 }
60
61 for ( const status of statuses) {
62 if ( ! STATUS_RANK . has (status)) {
63 throw new Error ( `unknown completion status: ${ status }` );
64 }
65 selected = worseStatus (selected, status);
66 }
67
68 if (input.aborted) {
69 selected = worseStatus (selected, 'aborted' );
70 }
71 if (input.hasMismatches) {
72 selected = worseStatus (selected, 'incomplete_with_mismatches' );
73 }
74 if (input.hasFailures) {
75 selected = worseStatus (selected, 'incomplete_with_failures' );
76 }
77 if (input.hasDeferred) {
78 selected = worseStatus (selected, 'complete_with_deferred_records' );
79 }
80 if (input.hasRecoveredRecords) {
81 selected = worseStatus (selected, 'complete_with_recovered_records' );
82 }
83 if (input.hasWarnings) {
84 selected = worseStatus (selected, 'complete_with_warnings' );
85 }
86
87 return selected;
88 }
89
90 function normalizeCompletenessRow ( row , index = 0 ) {
91 if ( ! row || typeof row !== 'object' || Array. isArray (row)) {
92 throw new Error ( `entityCompleteness[${ index }] must be an object` );
93 }
94
95 const entity = requireString (row.entity, `entityCompleteness[${ index }].entity` );
96 const subtype = row.subtype === undefined || row.subtype === null ? 'all' : requireString (row.subtype, `entityCompleteness[${ index }].subtype` );
97 const extracted = count (row.extracted, `${ entity }/${ subtype }.extracted` );
98 const inScope = count (row.inScope, `${ entity }/${ subtype }.inScope` );
99 const attempted = count (row.attempted, `${ entity }/${ subtype }.attempted` );
100 const imported = count (row.imported, `${ entity }/${ subtype }.imported` );
101 const alreadyPresentByCrosswalk = count (
102 row.alreadyPresentByCrosswalk === undefined ? row.alreadyPresent : row.alreadyPresentByCrosswalk,
103 `${ entity }/${ subtype }.alreadyPresentByCrosswalk`
104 );
105 const deferred = count (row.deferred, `${ entity }/${ subtype }.deferred` );
106 const failed = count (row.failed, `${ entity }/${ subtype }.failed` );
107 const skippedOutOfScope = count (row.skippedOutOfScope, `${ entity }/${ subtype }.skippedOutOfScope` );
108 const unexpectedSkipped = count (
109 row.unexpectedSkipped === undefined ? row.skippedInScope : row.unexpectedSkipped,
110 `${ entity }/${ subtype }.unexpectedSkipped`
111 );
112 const reconciled = imported + alreadyPresentByCrosswalk + deferred + failed;
113 const expectedReconciled = inScope;
114 const mismatch = reconciled !== expectedReconciled || unexpectedSkipped > 0 ;
115
116 return {
117 entity,
118 subtype,
119 extracted,
120 inScope,
121 attempted,
122 imported,
123 alreadyPresentByCrosswalk,
124 deferred,
125 failed,
126 skippedOutOfScope,
127 unexpectedSkipped,
128 reconciled,
129 expectedReconciled,
130 mismatch,
131 reasons: Array. isArray (row.reasons) ? row.reasons : [],
132 };
133 }
134
135 function headlineForRow ( row ) {
136 const parts = [];
137 if (row.mismatch) {
138 parts. push ( `mismatch ${ row . reconciled }/${ row . expectedReconciled }` );
139 }
140 if (row.deferred > 0 ) {
141 parts. push ( `${ row . deferred } deferred` );
142 }
143 if (row.failed > 0 ) {
144 parts. push ( `${ row . failed } failed` );
145 }
146 if (row.unexpectedSkipped > 0 ) {
147 parts. push ( `${ row . unexpectedSkipped } unexpected skipped` );
148 }
149 if ( ! parts. length ) {
150 return null ;
151 }
152 return {
153 entity: row.entity,
154 subtype: row.subtype,
155 message: `${ row . entity }/${ row . subtype }: ${ parts . join ( ', ' ) }` ,
156 };
157 }
158
159 function nextStepForBlockedRequest ( request , snapshot , requestAggregateOutcome ) {
160 // A `fulfilled` request only means the source-side snapshot was captured cleanly — it says
161 // nothing about whether any target field actually got written using it. Never let this
162 // branch claim "no action is required" while a dependent's own write accounting says the
163 // value was never applied (deferred/failed) or applied only via a degraded default
164 // (warning); check the real per-dependent outcome, not the snapshot status alone.
165 if (request.status === 'fulfilled' && (requestAggregateOutcome === 'deferred' || requestAggregateOutcome === 'failed' )) {
166 return 'The source data was captured successfully, but no target field has been written using it yet — capturing the definitions is not the same as completing the migration. See dependentOutcomes for which field(s) remain unattempted.' ;
167 }
168 if (request.status === 'fulfilled' && requestAggregateOutcome === 'warning' ) {
169 return 'The source data was captured successfully, but at least one target field was written using its existing degraded default rather than this data — see dependentOutcomes for which field(s).' ;
170 }
171 if (request.status === 'fulfilled' && request.stale) {
172 return `The import used data pulled ${ snapshot . extractedAt } (snapshot v${ snapshot . version }); verify no relevant source activity occurred after that date, or restore the ${ request . fulfillment . kind } path and re-run to refresh.` ;
173 }
174 if (request.status === 'fulfilled' ) return 'No action is required; the import used the recorded source-data snapshot.' ;
175 if (request.status === 'invalid' ) {
176 if (request.fulfillmentErrorCode === 'handler-not-registered' ) {
177 return `Fulfillment handler ${ request . fulfillment . handlerId } is not registered in this runner. This is a wiring bug in the import runner, not a data problem — register the handler and re-run.` ;
178 }
179 return request.fulfillment.kind === 'csv-upload'
180 ? `Correct the file at ${ request . fulfillment . expectedInputPath } and re-run.`
181 : 'The source bridge responded with unusable data; correct or reinstall it and re-run.' ;
182 }
183 if (request.status === 'declined' ) return 'Supply the blocked source data and re-run if you want to replace the degraded default.' ;
184 if (request.lastError) return `The source namespace probe failed: ${ request . lastError }. Fix source connectivity or authentication, then re-run.` ;
185 if (request.askedInteractively !== true ) {
186 return request.fulfillment.kind === 'csv-upload'
187 ? `This run did not pause to ask. Supply ${ request . fulfillment . expectedInputPath } and re-run.`
188 : 'This run did not pause to ask. Install the production-ready source bridge and re-run when that option becomes available.' ;
189 }
190 return request.fulfillment.kind === 'csv-upload'
191 ? `Supply ${ request . fulfillment . expectedInputPath } and re-run.`
192 : 'Install the production-ready source bridge and re-run when that option becomes available.' ;
193 }
194
195 function normalizeBlockedDataRequest ( request , index = 0 ) {
196 if ( ! request || typeof request !== 'object' || Array. isArray (request)) throw new Error ( `blockedDataRequests[${ index }] must be an object` );
197 const sourceEntityRef = requireString (request.sourceEntityRef, `blockedDataRequests[${ index }].sourceEntityRef` );
198 const status = requireString (request.status, `blockedDataRequests[${ index }].status` );
199 if ( ! [ 'offered' , 'missing' , 'declined' , 'invalid' , 'fulfilled' ]. includes (status)) throw new Error ( `${ sourceEntityRef }.status is invalid` );
200 if (status === 'declined' && request.askedInteractively !== true ) throw new Error ( `${ sourceEntityRef }: declined requires askedInteractively: true` );
201 const snapshot = request.snapshot || {};
202 if (status === 'fulfilled' && ( ! Number. isInteger (snapshot.version) || ! snapshot.extractedAt || ! snapshot.checksum)) {
203 throw new Error ( `${ sourceEntityRef }: fulfilled requires snapshot version, extractedAt, and checksum` );
204 }
205 const dependentOutcomes = asArray (request.dependentOutcomes, `${ sourceEntityRef }.dependentOutcomes` ). map (( dependent , dependentIndex ) => {
206 let recordOutcome = dependent.recordOutcome;
207 if (recordOutcome === null || recordOutcome === undefined ) {
208 throw new Error ( `${ sourceEntityRef }.dependentOutcomes[${ dependentIndex }].recordOutcome must be filled from write accounting` );
209 }
210 if ( ! OUTCOME_RANK . has (recordOutcome)) throw new Error ( `${ sourceEntityRef }.dependentOutcomes[${ dependentIndex }].recordOutcome is invalid` );
211 if (status !== 'fulfilled' && recordOutcome === 'fulfilled' ) {
212 throw new Error ( `${ sourceEntityRef }.dependentOutcomes[${ dependentIndex }] cannot be fulfilled without a fulfilled request` );
213 }
214 if (status === 'fulfilled' && request.stale && recordOutcome === 'fulfilled' ) recordOutcome = 'warning' ;
215 const row = {
216 targetEntity: requireString (dependent.targetEntity, `${ sourceEntityRef }.dependentOutcomes[${ dependentIndex }].targetEntity` ),
217 degradedField: requireString (dependent.degradedField, `${ sourceEntityRef }.dependentOutcomes[${ dependentIndex }].degradedField` ),
218 pitfallCode: dependent.pitfallCode || null ,
219 recordOutcome,
220 };
221 if (status === 'fulfilled' ) {
222 row.snapshotVersion = snapshot.version;
223 row.extractedAt = snapshot.extractedAt;
224 row.checksum = snapshot.checksum;
225 }
226 return row;
227 });
228 const requestAggregateOutcome = aggregateOutcome (dependentOutcomes);
229 return {
230 sourceEntityRef,
231 status,
232 stale: Boolean (request.stale),
233 snapshotVersion: status === 'fulfilled' ? snapshot.version : null ,
234 snapshotExtractedAt: status === 'fulfilled' ? snapshot.extractedAt : null ,
235 snapshotChecksum: status === 'fulfilled' ? snapshot.checksum : null ,
236 dependentOutcomes,
237 aggregateOutcome: requestAggregateOutcome,
238 consequenceIfMissing: request.consequenceIfMissing || null ,
239 whatUserCanDoNext: nextStepForBlockedRequest ({ ... request, fulfillment: request.fulfillment || {} }, snapshot, requestAggregateOutcome),
240 };
241 }
242
243 function createCompletionReport ( input = {}) {
244 const entityCompleteness = asArray (input.entityCompleteness, 'entityCompleteness' )
245 . map (( row , index ) => normalizeCompletenessRow (row, index));
246 const mismatches = entityCompleteness. filter (( row ) => row.mismatch);
247 const headline = entityCompleteness. map (headlineForRow). filter (Boolean);
248 const warnings = asArray (input.warnings, 'warnings' );
249 const recoveryActions = asArray (input.recoveryActions, 'recoveryActions' );
250 const blockedDataRequests = asArray (input.blockedDataRequests, 'blockedDataRequests' )
251 . map (( request , index ) => normalizeBlockedDataRequest (request, index));
252 const status = chooseCompletionStatus ({
253 statuses: input.statuses,
254 aborted: input.aborted,
255 hasMismatches: mismatches. length > 0 ,
256 hasFailures: hasCount (entityCompleteness, 'failed' ) || blockedDataRequests. some (( request ) => request.aggregateOutcome === 'failed' ),
257 hasDeferred: hasCount (entityCompleteness, 'deferred' ) || blockedDataRequests. some (( request ) => request.aggregateOutcome === 'deferred' ),
258 hasRecoveredRecords: recoveryActions. length > 0 || Boolean (input.hasRecoveredRecords),
259 hasWarnings: warnings. length > 0 || Boolean (input.hasWarnings) || blockedDataRequests. some (( request ) => request.aggregateOutcome === 'warning' ),
260 });
261
262 return {
263 schemaVersion: 1 ,
264 runId: input.runId || null ,
265 status,
266 headline,
267 entityCompleteness,
268 mismatches: mismatches. map (( row ) => ({
269 entity: row.entity,
270 subtype: row.subtype,
271 expectedReconciled: row.expectedReconciled,
272 reconciled: row.reconciled,
273 unexpectedSkipped: row.unexpectedSkipped,
274 })),
275 warnings,
276 recoveryActions,
277 blockedDataRequests,
278 artifacts: input.artifacts || {},
279 destinations: input.destinations || {},
280 urlPreservation: input.urlPreservation || null ,
281 };
282 }
283
284 module . exports = {
285 COMPLETION_STATUS,
286 chooseCompletionStatus,
287 createCompletionReport,
288 normalizeCompletenessRow,
289 normalizeBlockedDataRequest,
290 };