prism-ad-campaigns
Version:
Prism Ad Campaigns
456 lines (354 loc) • 10.5 kB
JavaScript
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