mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-03-11 02:12:21 -05:00
refs TryGhost/Team#1641 This commit adds a custom query for the members export, to improve the performance and to prevent any timeouts from happening when exporting large amounts of members. Co-authored-by: Simon Backx <simon@ghost.org> Co-authored-by: Matt Hanley <git@matthanley.co.uk>
This commit is contained in:
parent
eae0a6a3b9
commit
8dd009ffa0
6 changed files with 622 additions and 14 deletions
|
@ -349,10 +349,9 @@ module.exports = {
|
|||
},
|
||||
validation: {},
|
||||
async query(frame) {
|
||||
frame.options.withRelated = ['labels', 'stripeSubscriptions', 'stripeSubscriptions.customer', 'products', 'newsletters'];
|
||||
const page = await membersService.api.members.list(frame.options);
|
||||
|
||||
return page;
|
||||
return {
|
||||
data: await membersService.export(frame.options)
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -77,18 +77,13 @@ function bulkAction(bulkActionResult, _apiConfig, frame) {
|
|||
/**
|
||||
* @template PageMeta
|
||||
*
|
||||
* @param {{data: import('bookshelf').Model[], meta: PageMeta}} page
|
||||
* @param {APIConfig} _apiConfig
|
||||
* @param {Frame} frame
|
||||
* @param {{data: any[]}} data
|
||||
*
|
||||
* @returns {string} - A CSV string
|
||||
*/
|
||||
function exportCSV(page, _apiConfig, frame) {
|
||||
function exportCSV(data) {
|
||||
debug('exportCSV');
|
||||
|
||||
const members = page.data.map(model => serializeMember(model, frame.options));
|
||||
|
||||
return unparse(members);
|
||||
return unparse(data.data);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
88
core/server/services/members/exporter/query.js
Normal file
88
core/server/services/members/exporter/query.js
Normal file
|
@ -0,0 +1,88 @@
|
|||
const models = require('../../../models');
|
||||
const {knex} = require('../../../data/db');
|
||||
const moment = require('moment');
|
||||
|
||||
module.exports = async function (options) {
|
||||
const hasFilter = options.limit !== 'all' || options.filter || options.search;
|
||||
|
||||
let ids = null;
|
||||
if (hasFilter) {
|
||||
// do a very minimal query, only to fetch the ids of the filtered values
|
||||
// should be quite fast
|
||||
options.withRelated = [];
|
||||
options.columns = ['id'];
|
||||
|
||||
const page = await models.Member.findPage(options);
|
||||
ids = page.data.map(d => d.id);
|
||||
|
||||
/*
|
||||
const filterOptions = _.pick(options, ['transacting', 'context']);
|
||||
|
||||
if (all !== true) {
|
||||
// Include mongoTransformer to apply subscribed:{true|false} => newsletter relation mapping
|
||||
Object.assign(filterOptions, _.pick(options, ['filter', 'search', 'mongoTransformer']));
|
||||
}
|
||||
|
||||
const memberRows = await models.Member.getFilteredCollectionQuery(filterOptions)
|
||||
.select('members.id')
|
||||
.distinct();
|
||||
|
||||
ids = memberRows.map(row => row.id);
|
||||
*/
|
||||
}
|
||||
|
||||
const allProducts = await models.Product.fetchAll();
|
||||
const allLabels = await models.Label.fetchAll();
|
||||
|
||||
let query = knex('members')
|
||||
.select('id', 'email', 'name', 'note', 'status', 'created_at')
|
||||
.select(knex.raw(`
|
||||
(CASE WHEN EXISTS (SELECT 1 FROM members_newsletters n WHERE n.member_id = members.id)
|
||||
THEN TRUE ELSE FALSE
|
||||
END) as subscribed
|
||||
`))
|
||||
.select(knex.raw(`
|
||||
(SELECT GROUP_CONCAT(product_id) FROM members_products f WHERE f.member_id = members.id) as tiers
|
||||
`))
|
||||
.select(knex.raw(`
|
||||
(SELECT GROUP_CONCAT(label_id) FROM members_labels f WHERE f.member_id = members.id) as labels
|
||||
`))
|
||||
.select(knex.raw(`
|
||||
(SELECT customer_id FROM members_stripe_customers f WHERE f.member_id = members.id limit 1) as stripe_customer_id
|
||||
`));
|
||||
|
||||
if (hasFilter) {
|
||||
query = query.whereIn('id', ids);
|
||||
}
|
||||
|
||||
const rows = await query;
|
||||
for (const row of rows) {
|
||||
const tierIds = row.tiers ? row.tiers.split(',') : [];
|
||||
const tiers = tierIds.map((id) => {
|
||||
const tier = allProducts.find(p => p.id === id);
|
||||
return {
|
||||
name: tier.get('name')
|
||||
};
|
||||
});
|
||||
row.tiers = tiers;
|
||||
|
||||
const labelIds = row.labels ? row.labels.split(',') : [];
|
||||
const labels = labelIds.map((id) => {
|
||||
const label = allLabels.find(l => l.id === id);
|
||||
return {
|
||||
name: label.get('name')
|
||||
};
|
||||
});
|
||||
row.labels = labels;
|
||||
}
|
||||
|
||||
for (const member of rows) {
|
||||
// Note: we don't modify the array or change/duplicate objects
|
||||
// to increase performance
|
||||
member.subscribed = !!member.subscribed;
|
||||
member.comped = member.status === 'comped';
|
||||
member.created_at = moment(member.created_at).toISOString();
|
||||
}
|
||||
|
||||
return rows;
|
||||
};
|
|
@ -175,7 +175,7 @@ module.exports = {
|
|||
|
||||
processImport: processImport,
|
||||
|
||||
stats: membersStats
|
||||
|
||||
stats: membersStats,
|
||||
export: require('./exporter/query')
|
||||
};
|
||||
module.exports.middleware = require('./middleware');
|
||||
|
|
287
test/e2e-api/admin/__snapshots__/members-exporter.test.js.snap
Normal file
287
test/e2e-api/admin/__snapshots__/members-exporter.test.js.snap
Normal file
|
@ -0,0 +1,287 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Members API — exportCSV Can export a member without products 1: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-disposition": Any<String>,
|
||||
"content-length": Any<String>,
|
||||
"content-type": "text/csv; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API — exportCSV Can export a member without products 2: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-disposition": Any<String>,
|
||||
"content-length": Any<String>,
|
||||
"content-type": "text/csv; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API — exportCSV Can export a member without products 3: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-disposition": Any<String>,
|
||||
"content-length": Any<String>,
|
||||
"content-type": "text/csv; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API — exportCSV Can export comped 1: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-disposition": Any<String>,
|
||||
"content-length": Any<String>,
|
||||
"content-type": "text/csv; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API — exportCSV Can export comped 2: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-disposition": Any<String>,
|
||||
"content-length": Any<String>,
|
||||
"content-type": "text/csv; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API — exportCSV Can export comped 3: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-disposition": Any<String>,
|
||||
"content-length": Any<String>,
|
||||
"content-type": "text/csv; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API — exportCSV Can export comped 4: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-disposition": Any<String>,
|
||||
"content-length": Any<String>,
|
||||
"content-type": "text/csv; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API — exportCSV Can export customer id 1: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-disposition": Any<String>,
|
||||
"content-length": Any<String>,
|
||||
"content-type": "text/csv; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API — exportCSV Can export customer id 2: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-disposition": Any<String>,
|
||||
"content-length": Any<String>,
|
||||
"content-type": "text/csv; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API — exportCSV Can export customer id 3: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-disposition": Any<String>,
|
||||
"content-length": Any<String>,
|
||||
"content-type": "text/csv; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API — exportCSV Can export customer id 4: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-disposition": Any<String>,
|
||||
"content-length": Any<String>,
|
||||
"content-type": "text/csv; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API — exportCSV Can export labels 1: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-disposition": Any<String>,
|
||||
"content-length": Any<String>,
|
||||
"content-type": "text/csv; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API — exportCSV Can export labels 2: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-disposition": Any<String>,
|
||||
"content-length": Any<String>,
|
||||
"content-type": "text/csv; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API — exportCSV Can export labels 3: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-disposition": Any<String>,
|
||||
"content-length": Any<String>,
|
||||
"content-type": "text/csv; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API — exportCSV Can export labels 4: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-disposition": Any<String>,
|
||||
"content-length": Any<String>,
|
||||
"content-type": "text/csv; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API — exportCSV Can export newsletters 1: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-disposition": Any<String>,
|
||||
"content-length": Any<String>,
|
||||
"content-type": "text/csv; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API — exportCSV Can export newsletters 2: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-disposition": Any<String>,
|
||||
"content-length": Any<String>,
|
||||
"content-type": "text/csv; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API — exportCSV Can export newsletters 3: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-disposition": Any<String>,
|
||||
"content-length": Any<String>,
|
||||
"content-type": "text/csv; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API — exportCSV Can export products 1: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-disposition": Any<String>,
|
||||
"content-length": Any<String>,
|
||||
"content-type": "text/csv; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API — exportCSV Can export products 2: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-disposition": Any<String>,
|
||||
"content-length": Any<String>,
|
||||
"content-type": "text/csv; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API — exportCSV Can export products 3: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-disposition": Any<String>,
|
||||
"content-length": Any<String>,
|
||||
"content-type": "text/csv; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Members API — exportCSV Can export products 4: [headers] 1`] = `
|
||||
Object {
|
||||
"access-control-allow-origin": "http://127.0.0.1:2369",
|
||||
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
|
||||
"content-disposition": Any<String>,
|
||||
"content-length": Any<String>,
|
||||
"content-type": "text/csv; charset=utf-8",
|
||||
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
|
||||
"vary": "Origin, Accept-Encoding",
|
||||
"x-powered-by": "Express",
|
||||
}
|
||||
`;
|
239
test/e2e-api/admin/members-exporter.test.js
Normal file
239
test/e2e-api/admin/members-exporter.test.js
Normal file
|
@ -0,0 +1,239 @@
|
|||
const {agentProvider, mockManager, fixtureManager, matchers} = require('../../utils/e2e-framework');
|
||||
const {anyEtag, anyString} = matchers;
|
||||
|
||||
const uuid = require('uuid');
|
||||
const should = require('should');
|
||||
const Papa = require('papaparse');
|
||||
const models = require('../../../core/server/models');
|
||||
const moment = require('moment');
|
||||
|
||||
async function createMember(data) {
|
||||
const member = await models.Member.add({
|
||||
email: uuid.v4() + '@example.com',
|
||||
name: '',
|
||||
...data
|
||||
});
|
||||
|
||||
return member;
|
||||
}
|
||||
|
||||
let agent;
|
||||
let tiers, labels, newsletters;
|
||||
|
||||
function basicAsserts(member, row) {
|
||||
// Basic checks
|
||||
should(row.email).eql(member.get('email'));
|
||||
should(row.name).eql(member.get('name'));
|
||||
should(row.note).eql(member.get('note') || '');
|
||||
|
||||
should(row.deleted_at).eql('');
|
||||
should(row.created_at).eql(moment(member.get('created_at')).toISOString());
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {(row: any) => void} asserts
|
||||
*/
|
||||
async function testOutput(member, asserts, filters = []) {
|
||||
// Add default filters that always should match
|
||||
filters.push('limit=all');
|
||||
filters.push(`filter=id:${member.id}`);
|
||||
|
||||
for (const filter of filters) {
|
||||
// Test all
|
||||
let res = await agent
|
||||
.get(`/members/upload/?${filter}`)
|
||||
.expectStatus(200)
|
||||
.expectEmptyBody()
|
||||
.matchHeaderSnapshot({
|
||||
etag: anyEtag,
|
||||
'content-length': anyString,
|
||||
'content-disposition': anyString
|
||||
});
|
||||
|
||||
res.text.should.match(/id,email,name,note,subscribed_to_emails,complimentary_plan,stripe_customer_id,created_at,deleted_at,labels,products/);
|
||||
|
||||
let csv = Papa.parse(res.text, {header: true});
|
||||
let row = csv.data.find(r => r.id === member.id);
|
||||
should.exist(row);
|
||||
|
||||
asserts(row);
|
||||
|
||||
if (filter === 'filter=id:${member.id}') {
|
||||
csv.data.length.should.eql(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('Members API — exportCSV', function () {
|
||||
before(async function () {
|
||||
agent = await agentProvider.getAdminAPIAgent();
|
||||
await fixtureManager.init('newsletters', 'tiers:archived');
|
||||
await agent.loginAsOwner();
|
||||
|
||||
await models.Product.add({
|
||||
name: 'Extra Paid Product',
|
||||
slug: 'extra-product',
|
||||
type: 'paid',
|
||||
active: true,
|
||||
visibility: 'public'
|
||||
});
|
||||
|
||||
tiers = (await models.Product.findAll()).models.filter(m => m.get('type') === 'paid');
|
||||
tiers.length.should.be.greaterThan(1, 'These tests requires at least two paid tiers');
|
||||
|
||||
await models.Label.add({
|
||||
name: 'Label A'
|
||||
});
|
||||
|
||||
await models.Label.add({
|
||||
name: 'Label B'
|
||||
});
|
||||
|
||||
labels = (await models.Label.findAll()).models;
|
||||
labels.length.should.be.greaterThan(1, 'These tests requires at least two labels');
|
||||
|
||||
newsletters = (await models.Newsletter.findAll()).models;
|
||||
newsletters.length.should.be.greaterThan(1, 'These tests requires at least two newsletters');
|
||||
});
|
||||
|
||||
beforeEach(function () {
|
||||
mockManager.mockStripe();
|
||||
mockManager.mockMail();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
mockManager.restore();
|
||||
});
|
||||
|
||||
it('Can export products', async function () {
|
||||
// Create a new member with a product
|
||||
const member = await createMember({
|
||||
name: 'Test member',
|
||||
products: tiers
|
||||
});
|
||||
|
||||
const tiersList = tiers.map(tier => tier.get('name')).sort().join(',');
|
||||
|
||||
await testOutput(member, (row) => {
|
||||
basicAsserts(member, row);
|
||||
should(row.subscribed_to_emails).eql('false');
|
||||
should(row.complimentary_plan).eql('');
|
||||
should(row.products.split(',').sort().join(',')).eql(tiersList);
|
||||
}, [`filter=products:${tiers[0].get('slug')}`, 'filter=subscribed:false']);
|
||||
});
|
||||
|
||||
it('Can export a member without products', async function () {
|
||||
// Create a new member with a product
|
||||
const member = await createMember({
|
||||
name: 'Test member 2',
|
||||
note: 'Just a note 2'
|
||||
});
|
||||
|
||||
await testOutput(member, (row) => {
|
||||
basicAsserts(member, row);
|
||||
should(row.subscribed_to_emails).eql('false');
|
||||
should(row.complimentary_plan).eql('');
|
||||
should(row.products).eql('');
|
||||
}, ['filter=subscribed:false']);
|
||||
});
|
||||
|
||||
it('Can export labels', async function () {
|
||||
// Create a new member with a product
|
||||
const member = await createMember({
|
||||
name: 'Test member',
|
||||
note: 'Just a note',
|
||||
labels: labels.map((l) => {
|
||||
return {
|
||||
name: l.get('name')
|
||||
};
|
||||
})
|
||||
});
|
||||
|
||||
const labelsList = labels.map(label => label.get('name')).join(',');
|
||||
|
||||
await testOutput(member, (row) => {
|
||||
basicAsserts(member, row);
|
||||
should(row.subscribed_to_emails).eql('false');
|
||||
should(row.complimentary_plan).eql('');
|
||||
should(row.labels).eql(labelsList);
|
||||
should(row.products).eql('');
|
||||
}, [`filter=label:${labels[0].get('slug')}`, 'filter=subscribed:false']);
|
||||
});
|
||||
|
||||
it('Can export comped', async function () {
|
||||
// Create a new member with a product
|
||||
const member = await createMember({
|
||||
name: 'Test member',
|
||||
note: 'Just a note',
|
||||
status: 'comped'
|
||||
});
|
||||
|
||||
await testOutput(member, (row) => {
|
||||
basicAsserts(member, row);
|
||||
should(row.subscribed_to_emails).eql('false');
|
||||
should(row.complimentary_plan).eql('true');
|
||||
should(row.labels).eql('');
|
||||
should(row.products).eql('');
|
||||
}, ['filter=status:comped', 'filter=subscribed:false']);
|
||||
});
|
||||
|
||||
it('Can export newsletters', async function () {
|
||||
// Create a new member with a product
|
||||
const member = await createMember({
|
||||
name: 'Test member',
|
||||
note: 'Just a note',
|
||||
newsletters: [{
|
||||
id: newsletters[0].id
|
||||
}]
|
||||
});
|
||||
|
||||
await testOutput(member, (row) => {
|
||||
basicAsserts(member, row);
|
||||
should(row.subscribed_to_emails).eql('true');
|
||||
should(row.complimentary_plan).eql('');
|
||||
should(row.labels).eql('');
|
||||
should(row.products).eql('');
|
||||
}, ['filter=subscribed:true']);
|
||||
});
|
||||
|
||||
it('Can export customer id', async function () {
|
||||
// Create a new member with a product
|
||||
const member = await createMember({
|
||||
name: 'Test member',
|
||||
note: 'Just a note'
|
||||
});
|
||||
|
||||
const customer = await models.MemberStripeCustomer.add({
|
||||
member_id: member.id,
|
||||
customer_id: 'cus_12345',
|
||||
name: 'Test member',
|
||||
email: member.get('email')
|
||||
});
|
||||
|
||||
// NOTE: we need to create a subscription here because of the way the customer id is currently fetched
|
||||
const subscription = await models.StripeCustomerSubscription.add({
|
||||
subscription_id: 'sub_123',
|
||||
customer_id: customer.get('customer_id'),
|
||||
stripe_price_id: 'price_123',
|
||||
status: 'active',
|
||||
cancel_at_period_end: false,
|
||||
current_period_end: '2023-05-19 09:08:53',
|
||||
start_date: '2020-05-19 09:08:53',
|
||||
plan_id: 'price_1L15K4JQCtFaIJka01folNVK',
|
||||
plan_nickname: 'Yearly',
|
||||
plan_interval: 'year',
|
||||
plan_amount: 5000,
|
||||
plan_currency: 'USD'
|
||||
});
|
||||
|
||||
await testOutput(member, (row) => {
|
||||
basicAsserts(member, row);
|
||||
should(row.subscribed_to_emails).eql('false');
|
||||
should(row.complimentary_plan).eql('');
|
||||
should(row.labels).eql('');
|
||||
should(row.products).eql('');
|
||||
should(row.stripe_customer_id).eql('cus_12345');
|
||||
}, ['filter=subscribed:false', 'filter=subscriptions.subscription_id:sub_123']);
|
||||
});
|
||||
});
|
Loading…
Add table
Reference in a new issue