mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-01 22:02:11 +03:00
f068e40723
no refs depends on https://github.com/TryGhost/Ghost/pull/12472 The members CSV importer gets an overhaul and works with new importer module in members service, performing the import in a background job when the import will take too long to complete in a reasonable time and send an email with data on completion. Also includes updated CSV mapping UI and error handling to allow easier import from different type of exports. Co-authored-by: Fabien O'Carroll <fabien@allou.is> Co-authored-by: Peter Zimon <zimo@ghost.org>
121 lines
3.8 KiB
JavaScript
121 lines
3.8 KiB
JavaScript
import Service, {inject as service} from '@ember/service';
|
|
import validator from 'validator';
|
|
import {isEmpty} from '@ember/utils';
|
|
|
|
export default Service.extend({
|
|
ajax: service(),
|
|
membersUtils: service(),
|
|
ghostPaths: service(),
|
|
|
|
check(data) {
|
|
let sampledData = this._sampleData(data);
|
|
let mapping = this._detectDataTypes(sampledData);
|
|
return mapping;
|
|
},
|
|
|
|
/**
|
|
* Method implements foollowing sampling logic:
|
|
* Locate 10 non-empty cells from the start/middle(ish)/end of each column (30 non-empty values in total).
|
|
* If the data contains 30 rows or fewer, all rows should be validated.
|
|
*
|
|
* @param {Array} data JSON objects mapped from CSV file
|
|
*/
|
|
_sampleData(data, validationSampleSize = 30) {
|
|
let validatedSet = [{}];
|
|
|
|
if (data && data.length > validationSampleSize) {
|
|
let sampleKeys = Object.keys(data[0]);
|
|
|
|
sampleKeys.forEach(function (key) {
|
|
const nonEmptyKeyEntries = data.filter(entry => !isEmpty(entry[key]));
|
|
let sampledEntries = [];
|
|
|
|
if (nonEmptyKeyEntries.length <= validationSampleSize) {
|
|
sampledEntries = nonEmptyKeyEntries;
|
|
} else {
|
|
// take 3 equal parts from head, tail and middle of the data set
|
|
const partitionSize = validationSampleSize / 3;
|
|
|
|
const head = data.slice(0, partitionSize);
|
|
const tail = data.slice((data.length - partitionSize), data.length);
|
|
|
|
const middleIndex = Math.floor(data.length / 2);
|
|
const middleStartIndex = middleIndex - 2;
|
|
const middleEndIndex = middleIndex + 3;
|
|
const middle = data.slice(middleStartIndex, middleEndIndex);
|
|
|
|
validatedSet.push(...head);
|
|
validatedSet.push(...middle);
|
|
validatedSet.push(...tail);
|
|
}
|
|
|
|
sampledEntries.forEach((entry, index) => {
|
|
if (!validatedSet[index]) {
|
|
validatedSet[index] = {};
|
|
}
|
|
|
|
validatedSet[index][key] = entry[key];
|
|
});
|
|
});
|
|
} else {
|
|
validatedSet = data;
|
|
}
|
|
|
|
return validatedSet;
|
|
},
|
|
|
|
/**
|
|
* Detects supported data types and auto-detects following two needed for validation:
|
|
* 1. email
|
|
* 2. stripe_customer_id
|
|
*
|
|
* Returned "mapping" object contains mappings that could be accepted by the API
|
|
* to map validated types.
|
|
* @param {Array} data sampled data containing non empty values
|
|
*/
|
|
_detectDataTypes(data) {
|
|
const supportedTypes = [
|
|
'email',
|
|
'name',
|
|
'note',
|
|
'subscribed_to_emails',
|
|
'labels',
|
|
'created_at'
|
|
];
|
|
|
|
const autoDetectedTypes = [
|
|
'email'
|
|
];
|
|
|
|
let mapping = {};
|
|
let i = 0;
|
|
// loopping through all sampled data until needed data types are detected
|
|
while (i <= (data.length - 1)) {
|
|
if (mapping.email && mapping.stripe_customer_id) {
|
|
break;
|
|
}
|
|
|
|
let entry = data[i];
|
|
for (const [key, value] of Object.entries(entry)) {
|
|
if (!mapping.email && validator.isEmail(value)) {
|
|
mapping.email = key;
|
|
continue;
|
|
}
|
|
|
|
if (!mapping.name && /name/.test(key)) {
|
|
mapping.name = key;
|
|
continue;
|
|
}
|
|
|
|
if (!mapping[key] && supportedTypes.includes(key) && !(autoDetectedTypes.includes(key))) {
|
|
mapping[key] = key;
|
|
}
|
|
}
|
|
|
|
i += 1;
|
|
}
|
|
|
|
return mapping;
|
|
}
|
|
});
|