diff --git a/application-templates/javascript/event/.env.example b/application-templates/javascript/event/.env.example index 563a2e2..9afc536 100644 --- a/application-templates/javascript/event/.env.example +++ b/application-templates/javascript/event/.env.example @@ -2,4 +2,16 @@ CTP_CLIENT_ID= CTP_CLIENT_SECRET= CTP_PROJECT_KEY= CTP_SCOPE= -CTP_REGION= \ No newline at end of file +CTP_REGION= +PORT= + +# Subscription destination configuration +# Options: "GoogleCloudPubSub" or "SNS" +CONNECT_SUBSCRIPTION_DESTINATION= + +# GCP Pub/Sub configuration (required if CONNECT_SUBSCRIPTION_DESTINATION=GoogleCloudPubSub) +CONNECT_GCP_TOPIC_NAME= +CONNECT_GCP_PROJECT_ID= + +# AWS SNS configuration (required if CONNECT_SUBSCRIPTION_DESTINATION=SNS) +CONNECT_AWS_TOPIC_ARN= \ No newline at end of file diff --git a/application-templates/javascript/event/src/connector/actions.js b/application-templates/javascript/event/src/connector/actions.js index 7d4bc94..4f777e1 100644 --- a/application-templates/javascript/event/src/connector/actions.js +++ b/application-templates/javascript/event/src/connector/actions.js @@ -1,47 +1,101 @@ +import { assertNonNullable } from '../utils/assert.utils.js'; + const CUSTOMER_CREATE_SUBSCRIPTION_KEY = 'myconnector-customerCreateSubscription'; -export async function createGcpPubSubCustomerCreateSubscription( - apiRoot, - topicName, - projectId -) { - const destination = { - type: 'GoogleCloudPubSub', - topic: topicName, - projectId, - }; - await createSubscription(apiRoot, destination); -} - -export async function createAzureServiceBusCustomerCreateSubscription( - apiRoot, - connectionString -) { - const destination = { - type: 'AzureServiceBus', - connectionString: connectionString, - }; - await createSubscription(apiRoot, destination); -} - -async function createSubscription(apiRoot, destination) { - await deleteCustomerCreateSubscription(apiRoot); - await apiRoot +export async function createCustomerCreateSubscription(apiRoot, config) { + // Delete existing subscription first (inline deletion) + const { + body: { results: subscriptions }, + } = await apiRoot .subscriptions() - .post({ - body: { - key: CUSTOMER_CREATE_SUBSCRIPTION_KEY, - destination, - messages: [ - { - resourceTypeId: 'customer', - types: ['CustomerCreated'], - }, - ], + .get({ + queryArgs: { + where: `key = "${CUSTOMER_CREATE_SUBSCRIPTION_KEY}"`, }, }) .execute(); + + if (subscriptions.length > 0) { + const subscription = subscriptions[0]; + await apiRoot + .subscriptions() + .withKey({ key: CUSTOMER_CREATE_SUBSCRIPTION_KEY }) + .delete({ + queryArgs: { + version: subscription.version, + }, + }) + .execute(); + } + + // Google Cloud Pub/Sub subscription + if (config.connectSubscriptionDestination === 'GoogleCloudPubSub') { + assertNonNullable( + config.connectGcpTopicName, + 'GCP Topic Name must be provided for GoogleCloudPubSub destination' + ); + assertNonNullable( + config.connectGcpProjectId, + 'GCP Project ID must be provided for GoogleCloudPubSub destination' + ); + + await apiRoot + .subscriptions() + .post({ + body: { + key: CUSTOMER_CREATE_SUBSCRIPTION_KEY, + destination: { + type: 'GoogleCloudPubSub', + topic: config.connectGcpTopicName, + projectId: config.connectGcpProjectId, + }, + messages: [ + { + resourceTypeId: 'customer', + types: ['CustomerCreated'], + }, + ], + }, + }) + .execute(); + + return; + } + + // AWS SNS subscription + if (config.connectSubscriptionDestination === 'SNS') { + assertNonNullable( + config.connectAwsTopicArn, + 'AWS Topic ARN must be provided for SNS destination' + ); + + await apiRoot + .subscriptions() + .post({ + body: { + key: CUSTOMER_CREATE_SUBSCRIPTION_KEY, + destination: { + type: 'SNS', + topicArn: config.connectAwsTopicArn, + authenticationMode: 'IAM', + }, + messages: [ + { + resourceTypeId: 'customer', + types: ['CustomerCreated'], + }, + ], + }, + }) + .execute(); + + return; + } + + throw new Error( + `Unknown subscription destination type: ${config.connectSubscriptionDestination}` + ); } export async function deleteCustomerCreateSubscription(apiRoot) { diff --git a/application-templates/javascript/event/src/connector/post-deploy.js b/application-templates/javascript/event/src/connector/post-deploy.js index a17eb92..f0f5928 100644 --- a/application-templates/javascript/event/src/connector/post-deploy.js +++ b/application-templates/javascript/event/src/connector/post-deploy.js @@ -1,52 +1,17 @@ -import dotenv from 'dotenv'; -dotenv.config(); - import { createApiRoot } from '../client/create.client.js'; -import { assertError, assertString } from '../utils/assert.utils.js'; -import { - createGcpPubSubCustomerCreateSubscription, - createAzureServiceBusCustomerCreateSubscription, -} from './actions.js'; - -const CONNECT_GCP_TOPIC_NAME_KEY = 'CONNECT_GCP_TOPIC_NAME'; -const CONNECT_GCP_PROJECT_ID_KEY = 'CONNECT_GCP_PROJECT_ID'; -const CONNECT_PROVIDER_KEY = 'CONNECT_PROVIDER'; -const CONNECT_AZURE_CONNECTION_STRING_KEY = 'CONNECT_AZURE_CONNECTION_STRING'; +import { assertError } from '../utils/assert.utils.js'; +import { createCustomerCreateSubscription } from './actions.js'; +import { readConfiguration } from '../utils/config.utils.js'; -async function postDeploy(properties) { - const connectProvider = properties.get(CONNECT_PROVIDER_KEY); - assertString(connectProvider, CONNECT_PROVIDER_KEY); +async function postDeploy(config) { const apiRoot = createApiRoot(); - - switch (connectProvider) { - case 'AZURE': { - const connectionString = properties.get( - CONNECT_AZURE_CONNECTION_STRING_KEY - ); - assertString(connectionString, CONNECT_AZURE_CONNECTION_STRING_KEY); - await createAzureServiceBusCustomerCreateSubscription( - apiRoot, - connectionString - ); - break; - } - default: { - const topicName = properties.get(CONNECT_GCP_TOPIC_NAME_KEY); - const projectId = properties.get(CONNECT_GCP_PROJECT_ID_KEY); - assertString(topicName, CONNECT_GCP_TOPIC_NAME_KEY); - assertString(projectId, CONNECT_GCP_PROJECT_ID_KEY); - await createGcpPubSubCustomerCreateSubscription( - apiRoot, - topicName, - projectId - ); - } - } + await createCustomerCreateSubscription(apiRoot, config); } + async function run() { try { - const properties = new Map(Object.entries(process.env)); - await postDeploy(properties); + const config = readConfiguration(); + await postDeploy(config); } catch (error) { assertError(error); process.stderr.write(`Post-deploy failed: ${error.message}\n`); diff --git a/application-templates/javascript/event/src/connector/pre-undeploy.js b/application-templates/javascript/event/src/connector/pre-undeploy.js index a3228b9..2afc5e5 100644 --- a/application-templates/javascript/event/src/connector/pre-undeploy.js +++ b/application-templates/javascript/event/src/connector/pre-undeploy.js @@ -1,6 +1,3 @@ -import dotenv from 'dotenv'; -dotenv.config(); - import { createApiRoot } from '../client/create.client.js'; import { assertError } from '../utils/assert.utils.js'; import { deleteCustomerCreateSubscription } from './actions.js'; diff --git a/application-templates/javascript/event/src/utils/assert.utils.js b/application-templates/javascript/event/src/utils/assert.utils.js index acd0868..907c097 100644 --- a/application-templates/javascript/event/src/utils/assert.utils.js +++ b/application-templates/javascript/event/src/utils/assert.utils.js @@ -11,3 +11,10 @@ export function assertError(value, message) { export function assertString(value, message) { assert(typeof value === 'string', message ?? 'Invalid string value'); } + +export function assertNonNullable(value, message) { + assert( + value !== null && value !== undefined, + message ?? 'Value is null or undefined' + ); +} diff --git a/application-templates/javascript/event/src/utils/config.utils.js b/application-templates/javascript/event/src/utils/config.utils.js index 933c646..e867d62 100644 --- a/application-templates/javascript/event/src/utils/config.utils.js +++ b/application-templates/javascript/event/src/utils/config.utils.js @@ -16,6 +16,12 @@ export const readConfiguration = () => { projectKey: process.env.CTP_PROJECT_KEY, scope: process.env.CTP_SCOPE, region: process.env.CTP_REGION, + port: process.env.PORT, + connectSubscriptionDestination: + process.env.CONNECT_SUBSCRIPTION_DESTINATION, + connectGcpTopicName: process.env.CONNECT_GCP_TOPIC_NAME, + connectGcpProjectId: process.env.CONNECT_GCP_PROJECT_ID, + connectAwsTopicArn: process.env.CONNECT_AWS_TOPIC_ARN, }; const validationErrors = getValidateMessages(envValidators, envVars); diff --git a/application-templates/javascript/event/src/validators/env.validators.js b/application-templates/javascript/event/src/validators/env.validators.js index 3f211b2..f938d88 100644 --- a/application-templates/javascript/event/src/validators/env.validators.js +++ b/application-templates/javascript/event/src/validators/env.validators.js @@ -35,7 +35,7 @@ const envValidators = [ referencedBy: 'environmentVariables', }), - optional(standardString)( + standardString( ['scope'], { code: 'InvalidScope', @@ -50,6 +50,56 @@ const envValidators = [ message: 'Not a valid region.', referencedBy: 'environmentVariables', }), + + standardString( + ['port'], + { + code: 'InvalidPort', + message: 'Port should be a valid string.', + referencedBy: 'environmentVariables', + }, + { min: 1, max: undefined } + ), + + optional(standardString)( + ['connectSubscriptionDestination'], + { + code: 'InvalidSubscriptionDestination', + message: 'Subscription destination should be a valid string.', + referencedBy: 'environmentVariables', + }, + { min: 2, max: undefined } + ), + + optional(standardString)( + ['connectGcpTopicName'], + { + code: 'InvalidGcpTopicName', + message: 'GCP Topic Name should be a valid string.', + referencedBy: 'environmentVariables', + }, + { min: 2, max: undefined } + ), + + optional(standardString)( + ['connectGcpProjectId'], + { + code: 'InvalidGcpProjectId', + message: 'GCP Project ID should be a valid string.', + referencedBy: 'environmentVariables', + }, + { min: 2, max: undefined } + ), + + optional(standardString)( + ['connectAwsTopicArn'], + { + code: 'InvalidAwsTopicArn', + message: 'AWS Topic ARN should be a valid string.', + referencedBy: 'environmentVariables', + }, + { min: 2, max: undefined } + ), ]; export default envValidators; diff --git a/application-templates/typescript/event/.env.example b/application-templates/typescript/event/.env.example index 563a2e2..9afc536 100644 --- a/application-templates/typescript/event/.env.example +++ b/application-templates/typescript/event/.env.example @@ -2,4 +2,16 @@ CTP_CLIENT_ID= CTP_CLIENT_SECRET= CTP_PROJECT_KEY= CTP_SCOPE= -CTP_REGION= \ No newline at end of file +CTP_REGION= +PORT= + +# Subscription destination configuration +# Options: "GoogleCloudPubSub" or "SNS" +CONNECT_SUBSCRIPTION_DESTINATION= + +# GCP Pub/Sub configuration (required if CONNECT_SUBSCRIPTION_DESTINATION=GoogleCloudPubSub) +CONNECT_GCP_TOPIC_NAME= +CONNECT_GCP_PROJECT_ID= + +# AWS SNS configuration (required if CONNECT_SUBSCRIPTION_DESTINATION=SNS) +CONNECT_AWS_TOPIC_ARN= \ No newline at end of file diff --git a/application-templates/typescript/event/src/connector/actions.ts b/application-templates/typescript/event/src/connector/actions.ts index 78cf9cc..6b88980 100644 --- a/application-templates/typescript/event/src/connector/actions.ts +++ b/application-templates/typescript/event/src/connector/actions.ts @@ -1,57 +1,106 @@ -import { - AzureServiceBusDestination, - Destination, - GoogleCloudPubSubDestination, -} from '@commercetools/platform-sdk'; import { ByProjectKeyRequestBuilder } from '@commercetools/platform-sdk/dist/declarations/src/generated/client/by-project-key-request-builder'; +import { assertNonNullable } from '../utils/assert.utils'; +import { Config } from '../interfaces/config.interface'; const CUSTOMER_CREATE_SUBSCRIPTION_KEY = 'myconnector-customerCreateSubscription'; -export async function createGcpPubSubCustomerCreateSubscription( +export async function createCustomerCreateSubscription( apiRoot: ByProjectKeyRequestBuilder, - topicName: string, - projectId: string + config: Config ): Promise { - const destination: GoogleCloudPubSubDestination = { - type: 'GoogleCloudPubSub', - topic: topicName, - projectId, - }; - await createSubscription(apiRoot, destination); -} - -export async function createAzureServiceBusCustomerCreateSubscription( - apiRoot: ByProjectKeyRequestBuilder, - connectionString: string -): Promise { - const destination: AzureServiceBusDestination = { - type: 'AzureServiceBus', - connectionString: connectionString, - }; - await createSubscription(apiRoot, destination); -} - -async function createSubscription( - apiRoot: ByProjectKeyRequestBuilder, - destination: Destination -) { - await deleteCustomerCreateSubscription(apiRoot); - await apiRoot + // Delete existing subscription first (inline deletion) + const { + body: { results: subscriptions }, + } = await apiRoot .subscriptions() - .post({ - body: { - key: CUSTOMER_CREATE_SUBSCRIPTION_KEY, - destination, - messages: [ - { - resourceTypeId: 'customer', - types: ['CustomerCreated'], - }, - ], + .get({ + queryArgs: { + where: `key = "${CUSTOMER_CREATE_SUBSCRIPTION_KEY}"`, }, }) .execute(); + + if (subscriptions.length > 0) { + const subscription = subscriptions[0]; + await apiRoot + .subscriptions() + .withKey({ key: CUSTOMER_CREATE_SUBSCRIPTION_KEY }) + .delete({ + queryArgs: { + version: subscription.version, + }, + }) + .execute(); + } + + // Google Cloud Pub/Sub subscription + if (config.connectSubscriptionDestination === 'GoogleCloudPubSub') { + assertNonNullable( + config.connectGcpTopicName, + 'GCP Topic Name must be provided for GoogleCloudPubSub destination' + ); + assertNonNullable( + config.connectGcpProjectId, + 'GCP Project ID must be provided for GoogleCloudPubSub destination' + ); + + await apiRoot + .subscriptions() + .post({ + body: { + key: CUSTOMER_CREATE_SUBSCRIPTION_KEY, + destination: { + type: 'GoogleCloudPubSub', + topic: config.connectGcpTopicName, + projectId: config.connectGcpProjectId, + }, + messages: [ + { + resourceTypeId: 'customer', + types: ['CustomerCreated'], + }, + ], + }, + }) + .execute(); + + return; + } + + // AWS SNS subscription + if (config.connectSubscriptionDestination === 'SNS') { + assertNonNullable( + config.connectAwsTopicArn, + 'AWS Topic ARN must be provided for SNS destination' + ); + + await apiRoot + .subscriptions() + .post({ + body: { + key: CUSTOMER_CREATE_SUBSCRIPTION_KEY, + destination: { + type: 'SNS', + topicArn: config.connectAwsTopicArn, + authenticationMode: 'IAM', + }, + messages: [ + { + resourceTypeId: 'customer', + types: ['CustomerCreated'], + }, + ], + }, + }) + .execute(); + + return; + } + + throw new Error( + `Unknown subscription destination type: ${config.connectSubscriptionDestination}` + ); } export async function deleteCustomerCreateSubscription( diff --git a/application-templates/typescript/event/src/connector/post-deploy.ts b/application-templates/typescript/event/src/connector/post-deploy.ts index 3270ee4..1642ffd 100644 --- a/application-templates/typescript/event/src/connector/post-deploy.ts +++ b/application-templates/typescript/event/src/connector/post-deploy.ts @@ -1,53 +1,18 @@ -import dotenv from 'dotenv'; -dotenv.config(); - import { createApiRoot } from '../client/create.client'; -import { assertError, assertString } from '../utils/assert.utils'; -import { - createAzureServiceBusCustomerCreateSubscription, - createGcpPubSubCustomerCreateSubscription, -} from './actions'; - -const CONNECT_GCP_TOPIC_NAME_KEY = 'CONNECT_GCP_TOPIC_NAME'; -const CONNECT_GCP_PROJECT_ID_KEY = 'CONNECT_GCP_PROJECT_ID'; -const CONNECT_PROVIDER_KEY = 'CONNECT_PROVIDER'; -const CONNECT_AZURE_CONNECTION_STRING_KEY = 'CONNECT_AZURE_CONNECTION_STRING'; +import { assertError } from '../utils/assert.utils'; +import { Config } from '../interfaces/config.interface'; +import { createCustomerCreateSubscription } from './actions'; +import { readConfiguration } from '../utils/config.utils'; -async function postDeploy(properties: Map): Promise { - const connectProvider = properties.get(CONNECT_PROVIDER_KEY); - assertString(connectProvider, CONNECT_PROVIDER_KEY); +async function postDeploy(config: Config): Promise { const apiRoot = createApiRoot(); - - switch (connectProvider) { - case 'AZURE': { - const connectionString = properties.get( - CONNECT_AZURE_CONNECTION_STRING_KEY - ); - assertString(connectionString, CONNECT_AZURE_CONNECTION_STRING_KEY); - await createAzureServiceBusCustomerCreateSubscription( - apiRoot, - connectionString - ); - break; - } - default: { - const topicName = properties.get(CONNECT_GCP_TOPIC_NAME_KEY); - const projectId = properties.get(CONNECT_GCP_PROJECT_ID_KEY); - assertString(topicName, CONNECT_GCP_TOPIC_NAME_KEY); - assertString(projectId, CONNECT_GCP_PROJECT_ID_KEY); - await createGcpPubSubCustomerCreateSubscription( - apiRoot, - topicName, - projectId - ); - } - } + await createCustomerCreateSubscription(apiRoot, config); } async function run(): Promise { try { - const properties = new Map(Object.entries(process.env)); - await postDeploy(properties); + const config = readConfiguration(); + await postDeploy(config); } catch (error) { assertError(error); process.stderr.write(`Post-deploy failed: ${error.message}\n`); diff --git a/application-templates/typescript/event/src/connector/pre-undeploy.ts b/application-templates/typescript/event/src/connector/pre-undeploy.ts index 3b7e721..8367b42 100644 --- a/application-templates/typescript/event/src/connector/pre-undeploy.ts +++ b/application-templates/typescript/event/src/connector/pre-undeploy.ts @@ -1,6 +1,3 @@ -import dotenv from 'dotenv'; -dotenv.config(); - import { createApiRoot } from '../client/create.client'; import { assertError } from '../utils/assert.utils'; import { deleteCustomerCreateSubscription } from './actions'; diff --git a/application-templates/typescript/event/src/interfaces/config.interface.ts b/application-templates/typescript/event/src/interfaces/config.interface.ts index cbaf35e..c83d226 100644 --- a/application-templates/typescript/event/src/interfaces/config.interface.ts +++ b/application-templates/typescript/event/src/interfaces/config.interface.ts @@ -5,4 +5,8 @@ export interface Config { scope: string; region: string; port: string; + connectSubscriptionDestination?: string; + connectGcpTopicName?: string; + connectGcpProjectId?: string; + connectAwsTopicArn?: string; } diff --git a/application-templates/typescript/event/src/utils/assert.utils.ts b/application-templates/typescript/event/src/utils/assert.utils.ts index b7cff04..5468dd3 100644 --- a/application-templates/typescript/event/src/utils/assert.utils.ts +++ b/application-templates/typescript/event/src/utils/assert.utils.ts @@ -17,3 +17,13 @@ export function assertString( ): asserts value is string { assert(typeof value === 'string', message ?? 'Invalid string value'); } + +export function assertNonNullable( + value: T, + message?: string +): asserts value is NonNullable { + assert( + value !== null && value !== undefined, + message ?? 'Value is null or undefined' + ); +} diff --git a/application-templates/typescript/event/src/utils/config.utils.ts b/application-templates/typescript/event/src/utils/config.utils.ts index f40b030..38d33d9 100644 --- a/application-templates/typescript/event/src/utils/config.utils.ts +++ b/application-templates/typescript/event/src/utils/config.utils.ts @@ -1,4 +1,5 @@ import CustomError from '../errors/custom.error'; +import { Config } from '../interfaces/config.interface'; import envValidators from '../validators/env.validators'; import { getValidateMessages } from '../validators/helpers.validators'; @@ -10,12 +11,18 @@ import { getValidateMessages } from '../validators/helpers.validators'; */ export const readConfiguration = () => { const envVars = { - clientId: process.env.CTP_CLIENT_ID as string, - clientSecret: process.env.CTP_CLIENT_SECRET as string, - projectKey: process.env.CTP_PROJECT_KEY as string, + clientId: process.env.CTP_CLIENT_ID, + clientSecret: process.env.CTP_CLIENT_SECRET, + projectKey: process.env.CTP_PROJECT_KEY, scope: process.env.CTP_SCOPE, - region: process.env.CTP_REGION as string, - }; + region: process.env.CTP_REGION, + port: process.env.PORT, + connectSubscriptionDestination: + process.env.CONNECT_SUBSCRIPTION_DESTINATION, + connectGcpTopicName: process.env.CONNECT_GCP_TOPIC_NAME, + connectGcpProjectId: process.env.CONNECT_GCP_PROJECT_ID, + connectAwsTopicArn: process.env.CONNECT_AWS_TOPIC_ARN, + } as Config; const validationErrors = getValidateMessages(envValidators, envVars); diff --git a/application-templates/typescript/event/src/validators/env.validators.ts b/application-templates/typescript/event/src/validators/env.validators.ts index 5f27625..246a111 100644 --- a/application-templates/typescript/event/src/validators/env.validators.ts +++ b/application-templates/typescript/event/src/validators/env.validators.ts @@ -35,7 +35,7 @@ const envValidators = [ referencedBy: 'environmentVariables', }), - optional(standardString)( + standardString( ['scope'], { code: 'InvalidScope', @@ -50,6 +50,56 @@ const envValidators = [ message: 'Not a valid region.', referencedBy: 'environmentVariables', }), + + standardString( + ['port'], + { + code: 'InvalidPort', + message: 'Port should be a valid string.', + referencedBy: 'environmentVariables', + }, + { min: 1, max: undefined } + ), + + optional(standardString)( + ['connectSubscriptionDestination'], + { + code: 'InvalidSubscriptionDestination', + message: 'Subscription destination should be a valid string.', + referencedBy: 'environmentVariables', + }, + { min: 2, max: undefined } + ), + + optional(standardString)( + ['connectGcpTopicName'], + { + code: 'InvalidGcpTopicName', + message: 'GCP Topic Name should be a valid string.', + referencedBy: 'environmentVariables', + }, + { min: 2, max: undefined } + ), + + optional(standardString)( + ['connectGcpProjectId'], + { + code: 'InvalidGcpProjectId', + message: 'GCP Project ID should be a valid string.', + referencedBy: 'environmentVariables', + }, + { min: 2, max: undefined } + ), + + optional(standardString)( + ['connectAwsTopicArn'], + { + code: 'InvalidAwsTopicArn', + message: 'AWS Topic ARN should be a valid string.', + referencedBy: 'environmentVariables', + }, + { min: 2, max: undefined } + ), ]; export default envValidators;