Setting the file. One moment.
CSV Fileset · Rp Source CSV · wix/skills · Skills Docs
ContentsBack to the top of the page
Number 17.3
Position 3 of 11
Type JavaScript
Size 16 KB
Lines 482 lib/ csv-fileset.js
JavaScript · 482 lines · 16 KB
normalizeHeaderName
}
=
require
(
'./csv-parse.js'
);
14
15 const CONCAT_POLICY = {
16 STRICT_SET: 'strict-set' ,
17 UNION: 'union' ,
18 };
19
20 const CONCAT_MIN_JACCARD = 0.9 ;
21 const ROLE_HEADER_MIN_HITS = 2 ;
22
23 // Whole-token filename hints. Matched against tokens, never substrings, so
24 // `product-images.csv` cannot be read as a product file.
25 const GENERIC_ROLE_TOKENS = new Map (Object. entries ({
26 product: 'product' ,
27 products: 'product' ,
28 item: 'product' ,
29 items: 'product' ,
30 sku: 'product' ,
31 skus: 'product' ,
32 catalog: 'product' ,
33 inventory: 'product' ,
34 variant: 'variant' ,
35 variants: 'variant' ,
36 customer: 'customer' ,
37 customers: 'customer' ,
38 user: 'customer' ,
39 users: 'customer' ,
40 member: 'customer' ,
41 members: 'customer' ,
42 contact: 'customer' ,
43 contacts: 'customer' ,
44 order: 'order' ,
45 orders: 'order' ,
46 sale: 'order' ,
47 sales: 'order' ,
48 transaction: 'order' ,
49 transactions: 'order' ,
50 category: 'category' ,
51 categories: 'category' ,
52 collection: 'category' ,
53 collections: 'category' ,
54 taxonomy: 'category' ,
55 image: 'media' ,
56 images: 'media' ,
57 media: 'media' ,
58 photo: 'media' ,
59 photos: 'media' ,
60 asset: 'media' ,
61 assets: 'media' ,
62 post: 'post' ,
63 posts: 'post' ,
64 blog: 'post' ,
65 article: 'post' ,
66 articles: 'post' ,
67 }));
68
69 const GENERIC_FILENAME_NOISE = new Set ([
70 'export' , 'exports' , 'exported' , 'data' , 'full' , 'all' , 'backup' , 'sample' ,
71 'csv' , 'part' , 'parts' , 'final' , 'copy' , 'new' , 'wc' , 'shopify' ,
72 ]);
73
74 const ROLE_HEADER_ANCHORS = {
75 product: [ 'sku' , 'price' , 'product name' , 'title' , 'handle' , 'product code/sku' , 'regular price' ],
76 customer: [ 'email' , 'first name' , 'last name' , 'phone' , 'accepts marketing' ],
77 order: [ 'order number' , 'order id' , 'order date' , 'financial status' , 'fulfillment status' , 'line item name' ],
78 category: [ 'category name' , 'parent category' , 'category id' , 'category slug' ],
79 media: [ 'image url' , 'file name' , 'alt text' , 'image src' ],
80 };
81
82 function normalizeBasename ( filePath ) {
83 const base = path. basename ( String (filePath));
84 const withoutExtension = base. replace ( / \. [a-z0-9] +$ / i , '' );
85 // Strip an ISO date stamp before tokenizing so it is not read as a part index.
86 const withoutDate = withoutExtension. replace ( / \d {4} [-_]\d {2} [-_]\d {2} / g , ' ' );
87 const tokens = withoutDate
88 . toLowerCase ()
89 . split ( / [ ^ a-z0-9] + / )
90 . filter (Boolean)
91 // Split letter/digit boundaries so `part10` becomes ['part', '10'].
92 . flatMap (( token ) => token. split ( /(?<= [a-z] )(?= \d ) | (?<= \d )(?= [a-z] )/ ));
93
94 const numericTokens = tokens. filter (( token ) => / ^ \d +$ / . test (token));
95 const partIndex = tokens. length > 1 && numericTokens. length > 0
96 ? Number. parseInt (numericTokens[numericTokens. length - 1 ], 10 )
97 : null ;
98
99 return {
100 basename: base,
101 stem: withoutExtension,
102 normalizedStem: normalizeHeaderName (withoutExtension),
103 tokens,
104 partIndex,
105 };
106 }
107
108 function assignRoleByFilename ( filePath , { vendorProfile = null } = {}) {
109 const { normalizedStem , tokens } = normalizeBasename (filePath);
110
111 if (vendorProfile && Array. isArray (vendorProfile.fileRoles)) {
112 let best = null ;
113 for ( const entry of vendorProfile.fileRoles) {
114 for ( const hint of entry.filenameHints || []) {
115 const normalizedHint = normalizeHeaderName (hint);
116 if (normalizedHint && normalizedStem. includes (normalizedHint)) {
117 if ( ! best || normalizedHint. length > best.hintLength) {
118 best = { role: entry.role, hintLength: normalizedHint. length , hint };
119 }
120 }
121 }
122 }
123 if (best) {
124 return { role: best.role, roleSource: 'vendor-filename-hint' , evidence: `filename matches the vendor hint "${ best . hint }"` };
125 }
126 }
127
128 // Last matching token wins: in `product-images` the head noun is the qualifier
129 // and the tail noun is the entity.
130 const meaningful = tokens. filter (( token ) => ! GENERIC_FILENAME_NOISE . has (token) && ! / ^ \d +$ / . test (token));
131 for ( let i = meaningful. length - 1 ; i >= 0 ; i -= 1 ) {
132 const role = GENERIC_ROLE_TOKENS . get (meaningful[i]);
133 if (role) {
134 return { role, roleSource: 'filename-generic' , evidence: `filename token "${ meaningful [ i ] }"` };
135 }
136 }
137
138 return null ;
139 }
140
141 function assignRoleByHeader ( headerColumns ) {
142 const header = new Set ((headerColumns || []). map (( column ) => normalizeHeaderName (column)));
143 const scored = Object. entries ( ROLE_HEADER_ANCHORS )
144 . map (([ role , anchors ]) => ({
145 role,
146 hits: anchors. filter (( anchor ) => header. has ( normalizeHeaderName (anchor))),
147 }))
148 . sort (( a , b ) => b.hits. length - a.hits. length || a.role. localeCompare (b.role));
149
150 const winner = scored[ 0 ];
151 const runnerUp = scored[ 1 ];
152 if ( ! winner || winner.hits. length < ROLE_HEADER_MIN_HITS ) {
153 return null ;
154 }
155 if (runnerUp && winner.hits. length - runnerUp.hits. length < 1 ) {
156 return null ;
157 }
158 return {
159 role: winner.role,
160 roleSource: 'header' ,
161 evidence: `header anchors: ${ winner . hits . join ( ', ' ) }` ,
162 };
163 }
164
165 // files: [{ file, header, vendor?, profile?, rowCount? }]
166 function assignRoles ( files , { explicitRoles = {} } = {}) {
167 return files. map (( entry ) => {
168 const explicit = explicitRoles[entry.file] || explicitRoles[path. basename (entry.file)];
169 if (explicit) {
170 return { ... entry, role: explicit, roleSource: 'explicit' , roleEvidence: 'stated by the user' , suggestedRole: null };
171 }
172
173 const byFilename = assignRoleByFilename (entry.file, { vendorProfile: entry.profile || null });
174 if (byFilename) {
175 return { ... entry, role: byFilename.role, roleSource: byFilename.roleSource, roleEvidence: byFilename.evidence, suggestedRole: null };
176 }
177
178 const byHeader = assignRoleByHeader (entry.header);
179 if (byHeader) {
180 return { ... entry, role: byHeader.role, roleSource: byHeader.roleSource, roleEvidence: byHeader.evidence, suggestedRole: null };
181 }
182
183 // Never invent a role from the filename stem — an entity map full of
184 // `weird-export` entities is worse than one honest question.
185 return {
186 ... entry,
187 role: 'unknown' ,
188 roleSource: 'none' ,
189 roleEvidence: 'no vendor hint, filename token, or header anchor matched' ,
190 suggestedRole: null ,
191 };
192 });
193 }
194
195 function headerCompatibility ( headerA , headerB ) {
196 const normalizedA = (headerA || []). map (( column ) => normalizeHeaderName (column));
197 const normalizedB = (headerB || []). map (( column ) => normalizeHeaderName (column));
198 const setA = new Set (normalizedA);
199 const setB = new Set (normalizedB);
200
201 const onlyInA = (headerA || []). filter (( column ) => ! setB. has ( normalizeHeaderName (column)));
202 const onlyInB = (headerB || []). filter (( column ) => ! setA. has ( normalizeHeaderName (column)));
203 const intersection = [ ... setA]. filter (( column ) => setB. has (column)). length ;
204 const union = new Set ([ ... setA, ... setB]).size || 1 ;
205
206 const sameSet = onlyInA. length === 0 && onlyInB. length === 0 ;
207 const sameOrder = sameSet && normalizedA. join ( '