From 0c194ec505a2679ae9c5972c344cfc6494ea152e Mon Sep 17 00:00:00 2001 From: Jorge Cortes Date: Tue, 14 Jan 2025 21:21:27 -0500 Subject: [PATCH] [Components] nextdoor - new action components --- .../create-ad-group/create-ad-group.mjs | 202 ++++++++++++ .../nextdoor/actions/create-ad/create-ad.mjs | 86 +++++ .../create-advertiser/create-advertiser.mjs | 59 ++++ .../create-campaign/create-campaign.mjs | 59 ++++ .../create-scheduled-report.mjs | 94 ++++++ components/nextdoor/common/constants.mjs | 7 + components/nextdoor/common/utils.mjs | 141 +++++++++ components/nextdoor/nextdoor.app.mjs | 295 +++++++++++++++++- components/nextdoor/package.json | 5 +- pnpm-lock.yaml | 11 +- 10 files changed, 949 insertions(+), 10 deletions(-) create mode 100644 components/nextdoor/actions/create-ad-group/create-ad-group.mjs create mode 100644 components/nextdoor/actions/create-ad/create-ad.mjs create mode 100644 components/nextdoor/actions/create-advertiser/create-advertiser.mjs create mode 100644 components/nextdoor/actions/create-campaign/create-campaign.mjs create mode 100644 components/nextdoor/actions/create-scheduled-report/create-scheduled-report.mjs create mode 100644 components/nextdoor/common/constants.mjs create mode 100644 components/nextdoor/common/utils.mjs diff --git a/components/nextdoor/actions/create-ad-group/create-ad-group.mjs b/components/nextdoor/actions/create-ad-group/create-ad-group.mjs new file mode 100644 index 0000000000000..84bd657fee1a3 --- /dev/null +++ b/components/nextdoor/actions/create-ad-group/create-ad-group.mjs @@ -0,0 +1,202 @@ +import app from "../../nextdoor.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "nextdoor-create-ad-group", + name: "Create Ad Group", + description: "Creates an ad group based on the input payload for an existing campaign. [See the documentation](https://developer.nextdoor.com/reference/adgroup-create).", + version: "0.0.1", + type: "action", + props: { + app, + advertiserId: { + propDefinition: [ + app, + "advertiserId", + ], + }, + campaignId: { + propDefinition: [ + app, + "campaignId", + ({ advertiserId }) => ({ + advertiserId, + }), + ], + }, + name: { + description: "The name of the ad group.", + propDefinition: [ + app, + "name", + ], + }, + placements: { + type: "string[]", + label: "Placements", + description: "The placements for the ad group.", + options: [ + "RHR", + "FEED", + "FSF", + ], + }, + bidAmount: { + type: "string", + label: "Bid Amount", + description: "The bid amount for the ad group. The value must be a string in the format `USD 10` as an example.", + }, + bidPricingType: { + type: "string", + label: "Bid Pricing Type", + description: "The bid pricing type for the ad group. The value must be one of `CPM`.", + options: [ + "CPM", + ], + }, + budgetAmount: { + type: "string", + label: "Budget Amount", + description: "The budget amount for the ad group. The value must be a string in the format `USD 10` as an example.", + }, + budgetType: { + type: "string", + label: "Budget Type", + description: "The budget type for the ad group.", + options: [ + "DAILY_CAP_MONEY", + ], + }, + startTime: { + propDefinition: [ + app, + "startTime", + ], + }, + endTime: { + propDefinition: [ + app, + "endTime", + ], + }, + numberOfFrequencyCaps: { + type: "integer", + label: "Number Of Frequency Caps", + description: "The number of frequency caps to be collected. Defaults to `1`.", + default: 1, + reloadProps: true, + }, + }, + methods: { + frequencyCapsPropsMapper(prefix) { + const { + [`${prefix}maxImpressions`]: maxImpressions, + [`${prefix}numTimeunits`]: numTimeunits, + [`${prefix}timeunit`]: timeunit, + } = this; + + return { + max_impressions: maxImpressions, + num_timeunits: numTimeunits, + timeunit, + }; + }, + getFrequencyCapsPropDefinitions({ + prefix, + label, + } = {}) { + return { + [`${prefix}maxImpressions`]: { + type: "integer", + label: `${label} - Max Impressions`, + description: "The maximum number of impressions.", + }, + [`${prefix}numTimeunits`]: { + type: "integer", + label: `${label} - Number of Time Units`, + description: "The number of time units for frequency caps.", + }, + [`${prefix}timeunit`]: { + type: "string", + label: `${label} - Time Unit`, + description: "The time unit for frequency caps.", + options: [ + "MINUTE", + "MINUTES", + "HOUR", + "HOURS", + "DAY", + "DAYS", + "WEEK", + "WEEKS", + "MONTH", + "MONTHS", + ], + }, + }; + }, + createAdGroup(args = {}) { + return this.app.post({ + path: "/adgroup/create", + ...args, + }); + }, + }, + async additionalProps() { + const { + numberOfFrequencyCaps, + getFrequencyCapsPropDefinitions, + } = this; + + return utils.getAdditionalProps({ + numberOfFields: numberOfFrequencyCaps, + fieldName: "frequency cap", + getPropDefinitions: getFrequencyCapsPropDefinitions, + }); + }, + async run({ $ }) { + const { + createAdGroup, + advertiserId, + campaignId, + name, + placements, + bidAmount, + bidPricingType, + budgetAmount, + budgetType, + startTime, + endTime, + numberOfFrequencyCaps, + frequencyCapsPropsMapper, + } = this; + + const response = await createAdGroup({ + $, + data: { + advertiser_id: advertiserId, + campaign_id: campaignId, + name, + placements: utils.parseArray(placements), + bid: { + amount: bidAmount, + pricing_type: bidPricingType, + }, + budget: { + amount: budgetAmount, + budget_type: budgetType, + }, + start_time: startTime, + end_time: endTime, + frequency_caps: utils.getFieldsProps({ + numberOfFields: numberOfFrequencyCaps, + fieldName: "frequency cap", + propsMapper: frequencyCapsPropsMapper, + }), + }, + }); + + $.export("$summary", `Successfully created ad group with ID \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/nextdoor/actions/create-ad/create-ad.mjs b/components/nextdoor/actions/create-ad/create-ad.mjs new file mode 100644 index 0000000000000..215671e99200a --- /dev/null +++ b/components/nextdoor/actions/create-ad/create-ad.mjs @@ -0,0 +1,86 @@ +import app from "../../nextdoor.app.mjs"; + +export default { + key: "nextdoor-create-ad", + name: "Create Ad", + description: "Creates an ad based on the input payload for an existing NAM ad group. [See the documentation](https://developer.nextdoor.com/reference/ad-create).", + version: "0.0.1", + type: "action", + props: { + app, + advertiserId: { + propDefinition: [ + app, + "advertiserId", + ], + }, + campaignId: { + propDefinition: [ + app, + "campaignId", + ({ advertiserId }) => ({ + advertiserId, + }), + ], + }, + adGroupId: { + propDefinition: [ + app, + "adGroupId", + ({ + advertiserId, + campaignId, + }) => ({ + advertiserId, + campaignId, + }), + ], + }, + creativeId: { + propDefinition: [ + app, + "creativeId", + ({ advertiserId }) => ({ + advertiserId, + }), + ], + }, + name: { + description: "The name of the ad.", + propDefinition: [ + app, + "name", + ], + }, + }, + methods: { + createAd(args = {}) { + return this.app.post({ + path: "/ad/create", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createAd, + advertiserId, + adGroupId, + creativeId, + name, + } = this; + + const response = await createAd({ + $, + data: { + advertiser_id: advertiserId, + adgroup_id: adGroupId, + creative_id: creativeId, + name, + }, + }); + + $.export("$summary", `Successfully created ad with ID \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/nextdoor/actions/create-advertiser/create-advertiser.mjs b/components/nextdoor/actions/create-advertiser/create-advertiser.mjs new file mode 100644 index 0000000000000..f99cf05115c3b --- /dev/null +++ b/components/nextdoor/actions/create-advertiser/create-advertiser.mjs @@ -0,0 +1,59 @@ +import app from "../../nextdoor.app.mjs"; + +export default { + key: "nextdoor-create-advertiser", + name: "Create Advertiser", + description: "Creates an advertiser that is tied to the NAM profile the API credentials are tied to. [See the documentation](https://developer.nextdoor.com/reference/advertiser-create).", + version: "0.0.1", + type: "action", + props: { + app, + name: { + description: "The name of the advertiser.", + propDefinition: [ + app, + "name", + ], + }, + websiteUrl: { + propDefinition: [ + app, + "websiteUrl", + ], + }, + categoryId: { + propDefinition: [ + app, + "categoryId", + ], + }, + }, + methods: { + createAdvertiser(args = {}) { + return this.app.post({ + path: "/advertiser/create", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createAdvertiser, + name, + websiteUrl, + categoryId, + } = this; + + const response = await createAdvertiser({ + $, + data: { + name, + website_url: websiteUrl, + category_id: categoryId, + }, + }); + + $.export("$summary", `Successfully created advertiser with ID \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/nextdoor/actions/create-campaign/create-campaign.mjs b/components/nextdoor/actions/create-campaign/create-campaign.mjs new file mode 100644 index 0000000000000..21194b21a1fb2 --- /dev/null +++ b/components/nextdoor/actions/create-campaign/create-campaign.mjs @@ -0,0 +1,59 @@ +import app from "../../nextdoor.app.mjs"; + +export default { + key: "nextdoor-create-campaign", + name: "Create Campaign", + description: "Creates a campaign. [See the documentation](https://developer.nextdoor.com/reference/campaign-create).", + version: "0.0.1", + type: "action", + props: { + app, + advertiserId: { + propDefinition: [ + app, + "advertiserId", + ], + }, + name: { + description: "The name of the campaign.", + propDefinition: [ + app, + "name", + ], + }, + objective: { + propDefinition: [ + app, + "objective", + ], + }, + }, + methods: { + createCampaign(args = {}) { + return this.app.post({ + path: "/campaign/create", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createCampaign, + advertiserId, + name, + objective, + } = this; + + const response = await createCampaign({ + $, + data: { + advertiser_id: advertiserId, + name, + objective, + }, + }); + + $.export("$summary", `Successfully created campaign with ID \`${response.id}\`.`); + return response; + }, +}; diff --git a/components/nextdoor/actions/create-scheduled-report/create-scheduled-report.mjs b/components/nextdoor/actions/create-scheduled-report/create-scheduled-report.mjs new file mode 100644 index 0000000000000..afacbce3ddcdd --- /dev/null +++ b/components/nextdoor/actions/create-scheduled-report/create-scheduled-report.mjs @@ -0,0 +1,94 @@ +import app from "../../nextdoor.app.mjs"; +import utils from "../../common/utils.mjs"; + +export default { + key: "nextdoor-create-scheduled-report", + name: "Create Scheduled Report", + description: "Creates a scheduled report based on the input configuration. Upon a successful request the report will be sent out based on the schedule cadence. [See the documentation](https://developer.nextdoor.com/reference/reporting-scheduled-create).", + version: "0.0.1", + type: "action", + props: { + app, + advertiserId: { + propDefinition: [ + app, + "advertiserId", + ], + }, + name: { + description: "The name of the report.", + propDefinition: [ + app, + "name", + ], + }, + schedule: { + propDefinition: [ + app, + "schedule", + ], + }, + recipientEmails: { + propDefinition: [ + app, + "recipientEmails", + ], + }, + dimensionGranularity: { + propDefinition: [ + app, + "dimensionGranularity", + ], + }, + timeGranularity: { + propDefinition: [ + app, + "timeGranularity", + ], + }, + metrics: { + propDefinition: [ + app, + "metrics", + ], + }, + }, + methods: { + createReport(args = {}) { + return this.app.post({ + path: "/reporting/scheduled/create", + ...args, + }); + }, + }, + async run({ $ }) { + const { + createReport, + advertiserId, + name, + schedule, + recipientEmails, + dimensionGranularity, + timeGranularity, + metrics, + } = this; + + const response = await createReport({ + $, + data: { + advertiser_id: advertiserId, + name, + schedule, + recipient_emails: utils.parseArray(recipientEmails), + dimension_granularity: utils.parseArray(dimensionGranularity), + time_granularity: [ + timeGranularity, + ], + metrics, + }, + }); + + $.export("$summary", `Successfully created report '${this.reportName}'`); + return response; + }, +}; diff --git a/components/nextdoor/common/constants.mjs b/components/nextdoor/common/constants.mjs new file mode 100644 index 0000000000000..14fa0737dde39 --- /dev/null +++ b/components/nextdoor/common/constants.mjs @@ -0,0 +1,7 @@ +const DEFAULT_LIMIT = 25; +const SEP = "_"; + +export default { + DEFAULT_LIMIT, + SEP, +}; diff --git a/components/nextdoor/common/utils.mjs b/components/nextdoor/common/utils.mjs new file mode 100644 index 0000000000000..4801d44be35d0 --- /dev/null +++ b/components/nextdoor/common/utils.mjs @@ -0,0 +1,141 @@ +import { ConfigurationError } from "@pipedream/platform"; +import constants from "./constants.mjs"; + +const parseJson = (input) => { + const parse = (value) => { + if (typeof(value) === "string") { + try { + return parseJson(JSON.parse(value)); + } catch (e) { + return value; + } + } else if (typeof(value) === "object" && value !== null) { + return Object.entries(value) + .reduce((acc, [ + key, + val, + ]) => Object.assign(acc, { + [key]: parse(val), + }), {}); + } + return value; + }; + + return parse(input); +}; + +function parseArray(value) { + try { + if (!value) { + return; + } + + if (Array.isArray(value)) { + return value; + } + + const parsedValue = JSON.parse(value); + + if (!Array.isArray(parsedValue)) { + throw new Error("Not an array"); + } + + return parsedValue; + + } catch (e) { + throw new ConfigurationError("Make sure the custom expression contains a valid array object"); + } +} + +function toPascalCase(str) { + return str.replace(/(\w)(\w*)/g, (_, group1, group2) => + group1.toUpperCase() + group2.toLowerCase()); +} + +function getMetadataProp({ + index, fieldName, prefix, label, +} = {}) { + const fieldIdx = index + 1; + const key = `${fieldName}${fieldIdx}`; + return { + prefix: prefix + ? `${prefix}${key}${constants.SEP}` + : `${key}${constants.SEP}`, + label: label + ? `${label} - ${toPascalCase(fieldName)} ${fieldIdx}` + : `${toPascalCase(fieldName)} ${fieldIdx}`, + }; +} + +function getFieldProps({ + index, fieldName, prefix, + propsMapper = function propsMapper(prefix) { + const { [`${prefix}name`]: name } = this; + return { + name, + }; + }, +} = {}) { + const { prefix: metaPrefix } = getMetadataProp({ + index, + fieldName, + prefix, + }); + return propsMapper(metaPrefix); +} + +function getFieldsProps({ + numberOfFields, fieldName, propsMapper, prefix, +} = {}) { + return Array.from({ + length: numberOfFields, + }).map((_, index) => getFieldProps({ + index, + fieldName, + prefix, + propsMapper, + })); +} + +function getAdditionalProps({ + numberOfFields, fieldName, prefix, label, + getPropDefinitions = ({ + prefix, label, + }) => ({ + [`${prefix}name`]: { + type: "string", + label, + description: "The name of the field.", + optional: true, + }, + }), +} = {}) { + return Array.from({ + length: numberOfFields, + }).reduce((acc, _, index) => { + const { + prefix: metaPrefix, + label: metaLabel, + } = getMetadataProp({ + index, + fieldName, + prefix, + label, + }); + + return { + ...acc, + ...getPropDefinitions({ + prefix: metaPrefix, + label: metaLabel, + }), + }; + }, {}); +} + +export default { + parseJson, + parseArray: (value) => parseArray(value)?.map(parseJson), + getFieldsProps, + getAdditionalProps, +}; diff --git a/components/nextdoor/nextdoor.app.mjs b/components/nextdoor/nextdoor.app.mjs index 24e8246661805..eb71e679c8f13 100644 --- a/components/nextdoor/nextdoor.app.mjs +++ b/components/nextdoor/nextdoor.app.mjs @@ -1,11 +1,298 @@ +import { axios } from "@pipedream/platform"; +import constants from "./common/constants.mjs"; + export default { type: "app", app: "nextdoor", - propDefinitions: {}, + propDefinitions: { + name: { + type: "string", + label: "Name", + description: "A name for the resource being created (e.g., campaign name, advertiser name).", + }, + websiteUrl: { + type: "string", + label: "Website URL", + description: "The website URL for the advertiser.", + optional: true, + }, + categoryId: { + type: "string", + label: "Category ID", + description: "The category ID for the advertiser.", + optional: true, + async options() { + const { categories } = await this.listAdvertiserCategories(); + return categories.map(({ + id: value, + name: label, + }) => ({ + label, + value, + })); + }, + }, + advertiserId: { + type: "string", + label: "Advertiser ID", + description: "The ID of the advertiser.", + async options() { + const { user: { advertisers_with_access: advertisers } } = await this.me(); + return advertisers.map(({ id }) => id); + }, + }, + objective: { + type: "string", + label: "Objective", + description: "The objective of the campaign.", + options: [ + "AWARENESS", + "CONSIDERATION", + ], + }, + campaignId: { + type: "string", + label: "Campaign ID", + description: "The ID of the campaign.", + async options({ + advertiserId, + prevContext: { cursor }, + }) { + if (cursor === null) { + return []; + } + const { + campaigns, + page_info: { end_cursor: nextCursor }, + } = await this.listAdvertiserCampaigns({ + data: { + advertiser_id: advertiserId, + pagination_parameters: { + page_size: constants.DEFAULT_LIMIT, + cursor, + }, + }, + }); + const options = campaigns.map(({ + data: { + id: value, + name: label, + }, + }) => ({ + label, + value, + })); + + return { + options, + context: { + cursor: nextCursor || null, + }, + }; + }, + }, + startTime: { + type: "string", + label: "Start Time", + description: "The start time in **ZonedDateTime** format. *This date time should be in the future*. Eg. `2023-08-03T10:15:30-07:00[America/Los_Angeles]`. [See the documentation](https://developer.nextdoor.com/reference/advertising-data-types).", + }, + endTime: { + type: "string", + label: "End Time", + description: "The end time in **ZonedDateTime** format. If the **End Time** is not passed in, then the AdGroup is assumed to be running continuously. Eg. `2023-08-03T10:15:30-07:00[America/Los_Angeles]`. [See the documentation](https://developer.nextdoor.com/reference/advertising-data-types).", + optional: true, + }, + adGroupId: { + type: "string", + label: "Ad Group ID", + description: "The ID of the ad group.", + async options({ + advertiserId, + campaignId, + prevContext: { cursor }, + }) { + if (cursor === null) { + return []; + } + const { + adgroups, + page_info: { end_cursor: nextCursor }, + } = await this.listAdGroups({ + data: { + advertiser_id: advertiserId, + campaign_id: campaignId, + pagination_parameters: { + page_size: constants.DEFAULT_LIMIT, + cursor, + }, + }, + }); + const options = adgroups.map(({ + data: { + id: value, + name: label, + }, + }) => ({ + label, + value, + })); + + return { + options, + context: { + cursor: nextCursor || null, + }, + }; + }, + }, + creativeId: { + type: "string", + label: "Creative ID", + description: "The ID of the creative.", + async options({ + advertiserId, + prevContext: { cursor }, + }) { + if (cursor === null) { + return []; + } + const { + creatives, + page_info: { end_cursor: nextCursor }, + } = await this.listAdvertiserCreatives({ + data: { + advertiser_id: advertiserId, + pagination_parameters: { + page_size: constants.DEFAULT_LIMIT, + cursor, + }, + }, + }); + const options = creatives.map(({ + data: { + id: value, + name: label, + }, + }) => ({ + label, + value, + })); + + return { + options, + context: { + cursor: nextCursor || null, + }, + }; + }, + }, + schedule: { + type: "string", + label: "Schedule", + description: "The schedule for the report.", + options: [ + "DAILY", + "WEEKLY", + "MONTHLY", + "QUARTERLY", + ], + }, + recipientEmails: { + type: "string[]", + label: "Recipient Emails", + description: "An array of recipient email addresses.", + }, + dimensionGranularity: { + type: "string[]", + label: "Dimension Granularity", + description: "The level of detail for the report.", + options: [ + "CAMPAIGN", + "AD_GROUP", + "AD", + "PLACEMENT", + ], + }, + timeGranularity: { + type: "string", + label: "Time Granularity", + description: "The aggregation level for time in the report.", + options: [ + "DAY", + "WEEK", + "MONTH", + ], + }, + metrics: { + type: "string[]", + label: "Metrics", + description: "The metrics to include in the report.", + options: [ + "CLICKS", + "IMPRESSIONS", + "CTR", + "CONVERSIONS", + "SPEND", + "CPM", + "CPC", + "BILLABLE_SPEND", + ], + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + getUrl(path) { + return `https://ads.nextdoor.com/v2/api${path}`; + }, + getHeaders() { + return { + "Authorization": `Bearer ${this.$auth.authorization_token}`, + "Content-Type": "application/json", + }; + }, + _makeRequest({ + $ = this, path, headers, ...args + } = {}) { + return axios($, { + ...args, + url: this.getUrl(path), + headers: this.getHeaders(headers), + }); + }, + post(args = {}) { + return this._makeRequest({ + method: "POST", + ...args, + }); + }, + me(args = {}) { + return this._makeRequest({ + path: "/me", + ...args, + }); + }, + listAdvertiserCategories(args = {}) { + return this._makeRequest({ + path: "/advertiser/categories/list", + ...args, + }); + }, + listAdvertiserCampaigns(args = {}) { + return this._makeRequest({ + path: "/advertiser/campaign/list", + ...args, + }); + }, + listAdGroups(args = {}) { + return this._makeRequest({ + path: "/adgroup/list", + ...args, + }); + }, + listAdvertiserCreatives(args = {}) { + return this._makeRequest({ + path: "/advertiser/creative/list", + ...args, + }); }, }, }; diff --git a/components/nextdoor/package.json b/components/nextdoor/package.json index 8616aa3642a07..43713cdcf383f 100644 --- a/components/nextdoor/package.json +++ b/components/nextdoor/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/nextdoor", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream Nextdoor Ads Components", "main": "nextdoor.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c3529ca2fa1c7..50f4e8bea65ff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -550,8 +550,7 @@ importers: specifier: ^4.0.0 version: 4.0.1 - components/airpinpoint: - specifiers: {} + components/airpinpoint: {} components/airplane: dependencies: @@ -8571,7 +8570,11 @@ importers: components/nextdns: {} - components/nextdoor: {} + components/nextdoor: + dependencies: + '@pipedream/platform': + specifier: ^3.0.3 + version: 3.0.3 components/nexudus: {} @@ -34700,8 +34703,6 @@ snapshots: '@putout/operator-filesystem': 5.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3)) '@putout/operator-json': 2.2.0 putout: 36.13.1(eslint@8.57.1)(typescript@5.6.3) - transitivePeerDependencies: - - supports-color '@putout/operator-regexp@1.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3))': dependencies: