Setting the file. One moment.
Target Import Support · Wix Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page const writerScope
— line 87
This file
Number 45.155
Position 155 of 182
Type JavaScript
Size 18 KB
Lines 246 lib/ target-import-support.js
JavaScript · 246 lines · 18 KB
8 const nonempty = value => typeof value === 'string' && value. trim (). length > 0 ;
9
10 function deferredRefs ( plan ) {
11 const scope = plan?.executionScope || {};
12 // A checkpoint placeholder (no decisions, no blockers, no human subset) still becomes executable on approval.
13 return new Set ((scope.deferredEntities || []). filter ( d => d?.decisionIds?. length || d?.blockedByEntityRefs?. length || Array. isArray (scope.provisionalEntityRefs)). map ( d => d.entityRef));
14 }
15 function knowledge ( ref ) { try { return refPattern. test (ref) ? proofs. domain (ref) : null ; } catch { return null ; } }
16 // Each executable row's target writes as [targetRef, operation|null]: the row target with its preferred
17 // writer (or the knowledge default), plus every written output's own targetRef/writeOperation.
18 function executableTargets ( plan ) {
19 const deferred = deferredRefs (plan), out = [];
20 const selected = Array. isArray (plan?.executionScope?.selectedEntityRefs) ? new Set (plan.executionScope.selectedEntityRefs) : null ;
21 require ( './website-handoff' ). readEntityMappings (plan). forEach (( row , index ) => {
22 if ( ! row || row.targetClassification === 'manual-mapping' || [ 'excluded' , 'rejected' , 'deferred' ]. includes (row.status)) return ;
23 const ref = row.sourceEntityRef || row.sourceEntity || row.entityName || row.entity || row.entityKey;
24 if (deferred. has (ref) || selected && ! selected. has (ref)) return ;
25 const own = row.targetRef || (row.targetDomain && row.targetEntity ? `${ row . targetDomain }/${ row . targetEntity }` : null );
26 const writes = [];
27 if (own) writes. push ([own, row.preferredWrite?.writerId || row.writeOperation || knowledge (own)?.preferredWrite?.writerId || null ]);
28 for ( const d of row.fieldMappings || []) if ( ! [ 'excluded' , 'unresolved' ]. includes (d?.disposition)) for ( const o of d?.outputs || []) if (o?.targetRef) writes. push ([o.targetRef, o.writeOperation || null ]);
29 for ( const [ targetRef , operation ] of writes) if ( ! out. some ( t => t.planRow === index && t.targetRef === targetRef && t.operation === operation)) out. push ({ planRow: index, entityRef: ref, targetRef, operation, rowTargetRef: own });
30 });
31 return out;
32 }
33 // Bind the rules, not just their current result. Includes targets with no rule yet, so an
34 // added restriction or reliability flag invalidates an old approval. Prose is not semantic.
35 function supportKnowledgeDigest ( plan ) {
36 const targets = [ ...new Set ( executableTargets (plan). map ( t => t.targetRef))]. sort ();
37 const semantic = targets. map ( targetRef => {
38 const k = knowledge (targetRef);
39 return { targetRef, knowledge: k ? {
40 importUnreliable: (k.reliability?.flags || []). includes ( 'IMPORT_UNRELIABLE' ),
41 defaultWriter: k.preferredWrite?.writerId || null ,
42 importSafe: k.preferredWrite?.importSafe ?? null ,
43 alternates: (k.alternateWrites || []). filter ( a => typeof a?.writerId === 'string' )
44 . map ( a => ({ writerId: a.writerId, importSafe: a.importSafe ?? null , verification: a.verification ?? null }))
45 . sort (( a , b ) => a.writerId. localeCompare (b.writerId)),
46 operations: Object. fromEntries (Object. entries (k.operationContracts || {}). map (([ name , contract ]) => [name, {
47 // unsupportedWhen is always null since safe mode was removed; kept so approvals made
48 // before that change stay fresh for plans with no member write.
49 aliases: [ ... (contract.aliases || [])]. sort (), unsupportedWhen: null
50 }]))
51 } : null };
52 });
53 return crypto. createHash ( 'sha256' ). update (proofs. canonical (semantic)). digest ( 'hex' );
54 }
55
56 function validAcknowledgement ( gap ) {
57 return gap && ! Array. isArray (gap) && [gap.gapType, gap.code]. includes ( 'IMPORT_UNRELIABLE' ) &&
58 typeof gap.targetRef === 'string' && refPattern. test (gap.targetRef) && nonempty (gap.summary) &&
59 [gap.fallback, gap.action, gap.reviewAction]. some (nonempty);
60 }
61
62 // A native entity is written through a reviewed rp-target-wix writer for THAT entity: its
63 // default writer, a name, alias or auxiliary operation in its operation contracts (the set the
64 // dependency proofs accept), a function whose request mark names it (WRITE_BUILDERS_BY_ENTITY),
65 // or a reviewed helper listed for it (WRITE_HELPERS_BY_ENTITY).
66 // Another entity's writer is not one (the client refuses it), and neither is the direct-REST
67 // helper, whose unmarked requests the client refuses.
68 // A native entity with NO reviewed writer is written by guarded raw requests; the
69 // client checks each one. A reviewed writer always wins: an entity that has one cannot use them.
70 const DIRECT_REST_HELPERS = new Set ([ 'sendDirectRest' , 'buildDirectRestRequest' , 'notifyMissingWriter' ]);
71 const RAW_REQUEST_OPERATIONS = new Set ([ 'sendGuardedRawRequest' , 'buildGuardedRawRequest' ]);
72 const writers = () => require ( '../resources/rp-target-wix/lib/wix-writers.js' );
73 // The blog author helper lives beside the writers; a member contract names it.
74 const writerFunction = operation => typeof writers ()[operation] === 'function' ||
75 typeof require ( '../resources/rp-target-wix/lib/blog-author-attribution.js' )[operation] === 'function' ;
76 function entityWriter ( ref , k , operation ) {
77 if ( ! operation || DIRECT_REST_HELPERS . has (operation) || RAW_REQUEST_OPERATIONS . has (operation) || ! writerFunction (operation)) return false ;
78 return operation === k.preferredWrite?.writerId || Boolean (proofs. operationContract (ref, operation)) ||
79 Object. values (k.operationContracts || {}). some ( contract => (contract.auxiliaryOperations || []). includes (operation)) ||
80 ( writers (). WRITE_BUILDERS_BY_ENTITY [ref] || []). includes (operation) || ( writers (). WRITE_HELPERS_BY_ENTITY [ref] || []). includes (operation);
81 }
82 // The raw path also serves native-plus-cms entities (a native record plus CMS fields) with no writer.
83 const rawEligible = k => [ 'native' , 'native-plus-cms' ]. includes (k.target?.classification) && ! k.childOf;
84 function reviewedWriterExists ( ref , k ) { return entityWriter (ref, k, k.preferredWrite?.writerId) || Boolean (( writers (). WRITE_BUILDERS_BY_ENTITY [ref] || []). length ); }
85 // A reviewed writer can cover only some records of its entity (preferredWrite.writerScope, e.g.
86 // item SEO for BLOG_POST only); the rest of that entity keeps the raw path. Never blocks.
87 const writerScope = k => k.preferredWrite?.writerScope && Array. isArray (k.preferredWrite.writerScope.covers) ? k.preferredWrite.writerScope : null ;
88 // True when `ref` may be written by guarded raw requests: a native or native-plus-cms entity with no reviewed writer,
89 // or one whose reviewed writer is scoped (the request is then checked by rawScopeRefusal).
90 function rawRequestEntity ( ref ) { const k = knowledge (ref); return Boolean (k) && rawEligible (k) && ( ! reviewedWriterExists (ref, k) || Boolean ( writerScope (k))); }
91 // For a scoped entity: null when this raw request is outside what the reviewed writer covers,
92 // otherwise the refusal reason. The scope value is read from both the body and the URL segment
93 // after `pathAfter`; a covered or refused value in either, a disagreement, or no readable value is
94 // refused. Tags named by `refusedTags` (identities from seo-tags.js) are refused in the body.
95 const SCOPE_VALUE = / ^ [A-Z][A-Z0-9_] * (?:- [A-Za-z0-9-] + ) ?$ / ;
96 function valuesAt ( value , dotted ) {
97 return dotted. split ( '.' ). reduce (( values , key ) => values. flatMap ( v => {
98 const list = key. endsWith ( '[]' ), next = v && typeof v === 'object' ? v[list ? key. slice ( 0 , - 2 ) : key] : undefined ;
99 return list ? (Array. isArray (next) ? next : []) : next === undefined ? [] : [next];
100 }), [value]);
101 }
102 function rawScopeRefusal ( ref , { url , body } = {}) {
103 const k = knowledge (ref), scope = k && writerScope (k);
104 if ( ! scope || ! reviewedWriterExists (ref, k)) return null ;
105 const fromBody = scope.bodyPath && body && typeof body === 'object' ? body[scope.bodyPath] : undefined ;
106 let fromUrl;
107 if (scope.pathAfter) {
108 try { const segments = new URL (url).pathname. split ( '/' ); const at = segments. indexOf (scope.pathAfter); if (at >= 0 ) fromUrl = segments[at + 1 ]; } catch { /* refused below */ }
109 }
110 const blocked = [ ... scope.covers. map ( v => [v, 'reviewed-writer-covers' ]), ... (scope.rawRefused || []). map ( v => [v, 'scope-refused' ])];
111 // Compared without case so a gateway that folds case cannot reach a covered type.
112 for ( const value of [fromBody, fromUrl]) {
113 const hit = typeof value === 'string' && blocked. find (([ name ]) => name. toUpperCase () === value. toUpperCase ());
114 if (hit) return `${ hit [ 1 ] }:${ hit [ 0 ] }` ;
115 }
116 // Scope values are upper-case enum names (BLOG_POST, STATIC_PAGE, WIX_DATA_PAGE_ITEM-{pageId}): a path word such as `set` is not one.
117 const urlValue = typeof fromUrl === 'string' && SCOPE_VALUE . test (fromUrl) ? fromUrl : undefined ;
118 if (fromBody !== undefined && ( typeof fromBody !== 'string' || ! SCOPE_VALUE . test (fromBody))) return `scope-unknown:${ scope . field }` ;
119 if (fromBody !== undefined && urlValue !== undefined && fromBody !== urlValue) return `scope-mismatch:${ scope . field }` ;
120 if (fromBody === undefined && urlValue === undefined ) return `scope-unknown:${ scope . field }` ;
121 if (scope.refusedTags) {
122 const { tagIdentity } = require ( '../resources/rp-target-wix/lib/seo-tags.js' );
123 const tags = scope.refusedTags.paths. flatMap ( dotted => valuesAt (body, dotted)). flatMap ( v => Array. isArray (v) ? v : []);
124 const refused = tags. map ( tag => { try { return tagIdentity (tag); } catch { return null ; } }). find ( id => scope.refusedTags.identities. includes (id));
125 if (refused) return `tag-refused:${ refused }` ;
126 }
127 return null ;
128 }
129 // True when `ref` has a reviewed writer of its own (the raw path is then refused).
130 function hasReviewedWriter ( ref ) { const k = knowledge (ref); return Boolean (k) && reviewedWriterExists (ref, k); }
131 // A writer the entity declares in `alternateWrites` counts only when it is an exported toolkit
132 // writer, import-safe and verified live, and the entity itself is not import-unsafe. `declared`
133 // is false when the operation is not one of this entity's alternates.
134 // An alternate's marked builder (`build<WriterId>Request`, listed for this entity) is the same
135 // alternate: naming the builder instead of the executor does not get around the rule.
136 const builderOf = writerId => `build${ writerId . charAt ( 0 ). toUpperCase () }${ writerId . slice ( 1 ) }Request` ;
137 function alternateWriter ( ref , k , operation ) {
138 const declared = (k.alternateWrites || []). find ( a => a && typeof a.writerId === 'string' && (a.writerId === operation ||
139 ( builderOf (a.writerId) === operation && ( writers (). WRITE_BUILDERS_BY_ENTITY [ref] || []). includes (operation))));
140 const refused = reason => ({ declared: true , ok: false , reason });
141 if ( ! declared) return { declared: false , ok: false , reason: 'alternate-not-declared' };
142 if ( DIRECT_REST_HELPERS . has (operation) || RAW_REQUEST_OPERATIONS . has (operation)) return refused ( 'alternate-helper-name' );
143 if ( typeof writers ()[operation] !== 'function' ) return refused ( 'alternate-not-exported' );
144 if ( otherEntityWriter (ref, operation)) return refused ( 'alternate-other-entity' );
145 if (k.preferredWrite?.importSafe === false ) return refused ( 'entity-import-unsafe' );
146 if (declared.importSafe !== true ) return refused ( 'alternate-import-unsafe' );
147 if (declared.verification !== 'verified-live' ) return refused ( 'alternate-unverified' );
148 return { declared: true , ok: true , reason: null };
149 }
150 // True when `operation` is another entity's default writer or marked builder: the client would
151 // refuse it for `ref` (DEPENDENCY_TASK_MISMATCH), so no alternate may name it.
152 function otherEntityWriter ( ref , operation ) {
153 if (Object. entries ( writers (). WRITE_BUILDERS_BY_ENTITY ). some (([ other , names ]) => other !== ref && names. includes (operation))) return true ;
154 const index = JSON . parse ( require ( 'node:fs' ). readFileSync ( require ( 'node:path' ). join (__dirname, '../resources/rp-target-wix/domains/index.json' ), 'utf8' ));
155 return Object. entries (index.domains || {}). some (([ domain , row ]) => Object. keys (row.entities || {}). some ( entity => {
156 const other = `${ domain }/${ entity }` ;
157 return other !== ref && knowledge (other)?.preferredWrite?.writerId === operation;
158 }));
159 }
160 // Why `operation` is refused as a writer for `ref`, or null: the alternate's own reason and the
161 // default writer to use instead, for the gate message and the mapper.
162 function writerRefusal ( ref , operation ) {
163 const k = knowledge (ref);
164 if ( ! k || ! operation || ! writerMissing (ref, k, operation)) return null ;
165 const alternate = alternateWriter (ref, k, operation);
166 const instead = k.preferredWrite?.writerId || ( writers (). WRITE_BUILDERS_BY_ENTITY [ref] || [])[ 0 ] || ( rawRequestEntity (ref) ? 'sendGuardedRawRequest' : null );
167 return { reason: alternate.declared ? alternate.reason : 'not-a-writer' , instead };
168 }
169 // With no named operation, a writer-less native entity uses the raw path. A named operation must
170 // be the entity's own writer, or the raw path when the entity has no reviewed writer.
171 // The raw path is refused for any entity that is not a writer-less native or native-plus-cms
172 // entity (the client refuses it too). A named operation on either must be its own writer.
173 function writerMissing ( ref , k , operation ) {
174 if ( RAW_REQUEST_OPERATIONS . has (operation)) return ! rawEligible (k) || ( reviewedWriterExists (ref, k) && ! writerScope (k));
175 // A declared alternate is decided by its own rule, for native and native-plus-cms entities.
176 if (operation && rawEligible (k)) { const alternate = alternateWriter (ref, k, operation); if (alternate.declared) return ! alternate.ok; }
177 if ( ! rawEligible (k)) return false ;
178 if (operation) return ! entityWriter (ref, k, operation);
179 return false ;
180 }
181 function hasEntityWriter ( ref , operation = null ) { const k = knowledge (ref); return Boolean (k) && ! writerMissing (ref, k, operation); }
182 function supportErrors ({ plan , gaps = [] }) {
183 const errors = [], acknowledged = new Set (gaps. filter (validAcknowledgement). map ( g => g.targetRef));
184 for ( const t of executableTargets (plan)) {
185 const k = knowledge (t.targetRef);
186 if ( ! k) continue ;
187 const at = { planRow: `mapping-plan#${ t . planRow }` , entityRef: t.entityRef, targetRef: t.targetRef };
188 if ((k.reliability?.flags || []). includes ( 'IMPORT_UNRELIABLE' ) && ! acknowledged. has (t.targetRef) && ! errors. some ( e => e.code === 'import_unreliable_unacknowledged' && e.planRow === at.planRow && e.targetRef === t.targetRef)) errors. push ({ code: 'import_unreliable_unacknowledged' , ... at });
189 }
190 errors. push ( ... parentTaskErrors (plan));
191 return errors;
192 }
193 // An entity written inside another entity's import task (knowledge `writtenInTaskOf`: an order's
194 // payment, refund and invoice are sent as ecom/order requests). Its own row would become its own
195 // task, pass every static gate, and have every write refused at send (DEPENDENCY_TASK_MISMATCH).
196 // It is mapped as outputs of the host's row instead, and the host's task writes it.
197 function writtenInTaskOf ( ref ) { const host = knowledge (ref)?.writtenInTaskOf; return typeof host === 'string' && refPattern. test (host) ? host : null ; }
198 const parentTaskNext = ( ref , host ) => `Map ${ ref } into the ${ host } row (as outputs of that row), not as its own row: the ${ host } import task writes it with the ${ host } grant.` ;
199 function parentTaskErrors ( plan ) {
200 const errors = [];
201 for ( const t of executableTargets (plan)) {
202 const host = writtenInTaskOf (t.targetRef);
203 if ( ! host || t.rowTargetRef === host) continue ;
204 const at = { planRow: `mapping-plan#${ t . planRow }` , entityRef: t.entityRef, targetRef: t.targetRef };
205 if ( ! errors. some ( e => e.planRow === at.planRow && e.targetRef === t.targetRef)) errors. push ({ code: 'target_written_in_parent_task' , ... at, parentRef: host, next: parentTaskNext (t.targetRef, host) });
206 }
207 return errors;
208 }
209 // An entity whose knowledge says importSafe === false (its writer has a side effect site
210 // mute cannot cover) is never imported. Returns the recorded reason, else null.
211 function knowledgeImportSafe ( ref ) { const v = knowledge (ref)?.preferredWrite?.importSafe; return typeof v === 'boolean' ? v : null ; }
212 function importUnsafe ( ref ) {
213 const pw = knowledge (ref)?.preferredWrite;
214 if (pw?.importSafe !== false ) return null ;
215 return { pitfall: pw.importSafeReason?.pitfall || null , summary: pw.importSafeReason?.summary || 'importSafe is false' };
216 }
217 // The pre-write gate: each import task's native entity needs a reviewed writer for that entity,
218 // and no import task may write an entity that is not import-safe.
219 function missingWriterErrors ( manifest ) {
220 const errors = [];
221 for ( const task of (manifest?.tasks || []). filter ( row => row.kind === 'import' )) for ( const ref of task.entityRefs || []) {
222 const k = knowledge (ref);
223 const operation = task.writeOperations?.[ref] || null ;
224 if (k && writerMissing (ref, k, operation)) {
225 const alternate = alternateWriter (ref, k, operation), instead = k.preferredWrite?.writerId;
226 errors. push (alternate.declared ? `MISSING_WRITER:${ task . id }:${ ref }:${ alternate . reason }${ instead ? `:use ${ instead }` : ''}` : `MISSING_WRITER:${ task . id }:${ ref }` );
227 }
228 const unsafe = k && importUnsafe (ref);
229 if (unsafe) errors. push ( `IMPORT_UNSAFE:${ task . id }:${ ref }:${ unsafe . pitfall || 'no-pitfall'}: ${ unsafe . summary . replace ( /;/ g , ',' ) }` );
230 const host = k && writtenInTaskOf (ref);
231 if (host && ! (task.entityRefs || []). includes (host)) errors. push ( `PARENT_TASK_REQUIRED:${ task . id }:${ ref }:${ host }: ${ parentTaskNext ( ref , host ) }` );
232 }
233 return errors;
234 }
235 // Import-task entities written by guarded raw requests, for the plan shown before consent.
236 function rawRequestTargets ( manifest ) {
237 const out = [];
238 for ( const task of (manifest?.tasks || []). filter ( row => row.kind === 'import' )) for ( const ref of task.entityRefs || []) {
239 const operation = task.writeOperations?.[ref] || null ;
240 if ( rawRequestEntity (ref) && ! importUnsafe (ref) && (( ! operation && ! hasReviewedWriter (ref)) || RAW_REQUEST_OPERATIONS . has (operation))) out. push ({ taskId: task.id, entityRef: ref, writer: 'raw-request' });
241 }
242 return out;
243 }
244 module . exports = { executableTargets, supportErrors, missingWriterErrors, rawRequestTargets, rawRequestEntity, rawScopeRefusal, hasReviewedWriter, hasEntityWriter, writerRefusal, alternateWriter : ( ref , operation ) => { const k = knowledge (ref); return k ? alternateWriter (ref, k, operation) : null ; }, importUnsafe, knowledgeImportSafe, supportKnowledgeDigest, RAW_REQUEST_OPERATIONS, writtenInTaskOf };
245 const assessments = require ( './operation-assessments' );
246 module . exports . supportErrors = options => options.projectDir ? assessments. withPlan (options.plan,options.projectDir,() => supportErrors (options)) : supportErrors (options);