UNPKG

prism-ad-campaigns

Version:
456 lines (354 loc) 10.5 kB
const r = require('structure-driver') const {DigitalAssetModel} = require('structure-digital-assets') const {AdAccountModel} = require('prism-ad-accounts') const FacebookApiModel = require('./facebook-api-model') const AdSetModel = require('./ad-set') const AdCreativeModel = require('./ad-creative') const AdModel = require('./ad') const ActivateAdCampaignJob = require('../jobs/activate-ad-campaign') /* * AdCampaignModel Class * * @public * @class AdCampaignModel */ class AdCampaignModel extends FacebookApiModel { /** * AdCampaignModel constructor * * @public * @constructor * @param {Object} options - Options */ constructor(options = {}) { super(Object.assign({}, { table: 'ad_campaigns', relations: { belongsTo: [ { node: 'applications', link: { foreignKey: 'applicationId', localKey: 'adCampaignId' }, joinTable: 'link_applications_ad_campaigns', strip: false } ], hasMany: [ { node: 'ad_sets', link: { foreignKey: 'adSetId', localKey: 'adCampaignId' }, joinTable: 'link_ad_campaigns_ad_sets' }, { node: 'ad_creatives', link: { foreignKey: 'adCreativeId', localKey: 'adCampaignId' }, joinTable: 'link_ad_campaigns_ad_creatives' } ] } }, options)) this.imageHashesForDigitalAssetIds = {} } /* * Get ad campaign by ID * * @public * @param {String} id */ async getById(id, options = {}) { const adCampaign = await FacebookApiModel.prototype.getById.call(this, id, options) await this.populateAdAccount(adCampaign) return adCampaign } /* * Populate ad account for ad campaign. * * @private * @param {Object} adCampaign - ad campaign to populate ad account for */ async populateAdAccount(adCampaign) { if (!adCampaign || !adCampaign.adAccountId) { return } const adAccountModel = new AdAccountModel({ logger: this.logger, organizationId: this.organizationId, applicationId: this.applicationId }) adCampaign.adAccount = await adAccountModel.getById(adCampaign.adAccountId) } /* * Get all ad campaigns, with optional pagination * * @public * @param {Object} ids - optional ids to fetch */ async getAll(ids = [], options = {}) { const orderBy = options.orderBy || 'updatedAt' const orderDir = options.orderDir || 'desc' const pagination = options.pagination || false const page = options.page const limit = options.limit try { let query = r.table(this.table) if(ids.length > 0) { query = query.getAll(r.args(ids)) } else { query = query.getAll(this.applicationId, {index: 'applicationId'}) } if(orderDir === 'asc') { query = query.orderBy(r.asc(orderBy)) } else { query = query.orderBy(r.desc(orderBy)) } let res if (pagination) { res = await this.paginate(query, {page, limit}) for (const adCampaign of res.results) { await this.populateAdAccount(adCampaign) } } else { res = await query.run() for (const adCampaign of res) { await this.populateAdAccount(adCampaign) } } return res } catch(e) { this.logger.error(e) throw e } } /* * Get an ad campaign by link. Links are scoped by application ID. * * @public * @param {Object} link - link to get ad campaign of */ async getByLink(link) { const res = await r .table(this.table) .getAll([this.applicationId, link], {index: 'link_applications_link'}) .limit(1) return res.length ? res[0] : null } /* * Create ad campaign * * @public * @param {Object} pkg */ async create(pkg = {}, options = {}) { const existing = await this.getByLink(pkg.link) if (!pkg.clone && existing) { return existing } delete pkg.clone pkg.organizationId = this.organizationId pkg.applicationId = this.applicationId pkg.status = 'draft' return FacebookApiModel.prototype.create.call(this, pkg, options) } /* * Update an ad campaign * * @public * @param {Object} pkg */ async updateById(adCampaignId, pkg = {}, options = {}) { const oldAdCampaign = await this.getById(adCampaignId) const isActivation = oldAdCampaign.status === 'draft' && pkg.status === 'active' if (isActivation && !oldAdCampaign.adAccountId && !pkg.adAccountId) { throw 'Missing adAccountId' } const newAdCampaign = await FacebookApiModel.prototype.updateById.call( this, adCampaignId, pkg, options ) if (isActivation) { if (!oldAdCampaign.adAccountId && !newAdCampaign.adAccountId) { throw 'Missing adAccountId' } const job = new ActivateAdCampaignJob({ organizationId: this.organizationId, applicationId: this.applicationId, logger: this.logger, }) job.queue({ organizationId: this.organizationId, applicationId: this.applicationId, id: adCampaignId }) } return newAdCampaign } /* * Activate ad campaign * * @public * @param {Object} pkg */ async activate(adCampaignId) { let adCampaign = await this.getById(adCampaignId) if (adCampaign.facebookAdCampaignId) { return adCampaign } const api = await this.getApi(adCampaign.adAccountId) const facebookAdCampaign = await api.createCampaign({ name: adCampaign.title, }) adCampaign = await this.updateById(adCampaign.id, { facebookAdCampaignId: facebookAdCampaign.id, status: 'active', }) const adSets = await this.createAdSets(adCampaign) await this.createAds(adCampaign, adSets) return adCampaign } /* * Generator for creating all possible creatives combinations * * @private * @param {Object} pkg */ * combinations(head, ...tail) { let remainder = tail.length ? this.combinations(...tail) : [[]] for (let r of remainder) for (let h of head) yield [h, ...r] } /* * Get an image hash for a digital asset ID * * Facebook ad creatives can only be created with images by passing image * hashes. These hashses are created by creating an AdImage on the Facebook * marketing API. * * Cache the results to avoid refetching for the same digital asset. * * @private * @param {Object} pkg */ async getImageHashForDigitalAsset(adCampaign, digitalAssetId) { if (this.imageHashesForDigitalAssetIds[digitalAssetId]) { return this.imageHashesForDigitalAssetIds[digitalAssetId] } const digitalAssetModel = new DigitalAssetModel({ logger: this.logger, organizationId: this.organizationId, applicationId: this.applicationId }) const digitalAsset = await digitalAssetModel.getById(digitalAssetId) const api = await this.getApi(adCampaign.adAccountId) const adImage = await api.createAdImage({ url: `http:${digitalAsset.url}` }) const imageHash = adImage.images.bytes.hash this.imageHashesForDigitalAssetIds[digitalAssetId] = imageHash return imageHash } /* * Create ad creatives * * @private * @param {Object} adCampaign - the ad campaign to create the ad creatives on */ async createAdCreatives(adCampaign, adSet) { const adCreativeModel = new AdCreativeModel({ logger: this.logger, organizationId: this.organizationId, applicationId: this.applicationId, adAccountId: adCampaign.adAccountId }) const creativeCombinations = this.combinations( adCampaign.headlines, adCampaign.texts, adCampaign.digitalAssetIds ) const adCreatives = [] let count = 1 for (const [headline, text, digitalAssetId] of creativeCombinations) { const imageHash = await this.getImageHashForDigitalAsset( adCampaign, digitalAssetId ) adCreatives.push( await adCreativeModel.create({ title: `${adCampaign.title} | Ad Creative ${count}`, adCampaignId: adCampaign.id, adSetId: adSet.id, facebookAdSetId: adSet.facebookAdSetId, headline, text, imageHash, link: adCampaign.link }) ) count += 1 } return adCreatives } /* * Create ad sets * * For the moment, we are simply creating a single hardcoded ad set. * In the future, these will also be configurable via the CMS. * * @private * @param {Object} adCampaign - the ad campaign to create the ad sets on */ async createAdSets(adCampaign) { const adSetModel = new AdSetModel({ logger: this.logger, organizationId: this.organizationId, applicationId: this.applicationId, adAccountId: adCampaign.adAccountId }) return [await adSetModel.create({ title: `${adCampaign.title} | Android All`, adCampaignId: adCampaign.id, facebookAdCampaignId: adCampaign.facebookAdCampaignId })] } /* * Create ads of all ad creatives in a campaign for every ad set in the * campaign * * @private * @param {Object} adCampaign - the ad campaign to create the ads on * @param {Object.Array} adSets - the ad sets in the campaign * @param {Object.Array} adCreatives - the ad creatives in the campaign */ async createAds(adCampaign, adSets) { let count = 1 for (const adSet of adSets) { const adCreatives = await this.createAdCreatives(adCampaign, adSet) for (const adCreative of adCreatives) { const adModel = new AdModel({ logger: this.logger, organizationId: this.organizationId, applicationId: this.applicationId, adAccountId: adCampaign.adAccountId }) await adModel.create({ title: `${adCampaign.title} | Ad ${count}`, adCampaignId: adCampaign.id, adSetId: adSet.id, adCreativeId: adCreative.id, facebookAdSetId: adSet.facebookAdSetId, facebookAdCreativeId: adCreative.facebookAdCreativeId, }) count += 1 } } } } module.exports = AdCampaignModel