Blackline Connector - Web Services

Hi All,

I was able to create a Web Services SaaS connector for Blackline. Along with using some customizers code to work with the role-products that are required for user access. I have posted below some of the source configuration to show API calls that I make to add users to the different, along with the customizer code that I use to translate the role products into a single entitlement value and back again. If you have any questions or improvements, feel free to let me know! For the authentication I have found that doing a curl command is the easiest for this process.

API Calls:

{
                "httpMethodType": "POST",
                "pagingInitialOffset": 0,
                "requestType": "API",
                "sequenceNumberForEndpoint": "3",
                "uniqueNameForEndPoint": "Create Account ",
                "rootPath": "$.",
                "body": {
                    "jsonBody": "{\n    \"loginId\": \"$plan.loginId$\",\n    \"firstName\": \"$plan.firstname$\",\n    \"lastName\": \"$plan.lastName$\",\n    \"isActive\": false,\n    \"email\": \"$plan.email$\",\n    \"allowUserMentions\": true,\n    \"allowUserToEditJournalConfig\": false,\n    \"allowUserToEditIntercompanyConfig\": false,\n    \"allowAdhocMatching\": false,\n    \"allowIntercompanySettlement\": false,\n    \"requiresJournalReviewer\": false,\n    \"allowUserToAccessJournalAnalyser\":false,\n    \"jobTitle\": null,\n    \"supervisor\": null,\n    \"timeZoneId\": 6,\n    \"phoneNumber\": null,\n    \"annualHours\": 0,\n    \"referenceField1\": null,\n    \"referenceField2\": null,\n    \"referenceField3\": null\n}",
                    "bodyFormat": "raw"
                },
                "paginationSteps": null,
                "responseCode": [
                    "201"
                ],
                "resMappingObj": {
                    "firstName": "firstName",
                    "lastName": "lastName",
                    "lastUpdated": "lastUpdated",
                    "loginId": "loginId",
                    "fullName": "fullName",
                    "id": "id",
                    "isActive": "isActive",
                    "email": "email"
                },
                "contextUrl": "/v1/users",
                "pagingSize": 50,
                "header": {
                    "Authorization": "Bearer $application.accesstoken_blackline$"
                },
                "operationType": "Create Account",
                "xpathNamespaces": null,
                "parentEndpointName": null
            },
            {
                "httpMethodType": "GET",
                "pagingInitialOffset": 0,
                "requestType": "API",
                "sequenceNumberForEndpoint": "4",
                "uniqueNameForEndPoint": "Account Aggregation - Get Accounts",
                "rootPath": "$.items",
                "body": {
                    "bodyFormData": null,
                    "jsonBody": null,
                    "bodyFormat": null
                },
                "paginationSteps": null,
                "responseCode": [
                    "200"
                ],
                "resMappingObj": {
                    "lastName": "lastName",
                    "lastUpdated": "lastUpdated",
                    "firstname": "firstName",
                    "loginId": "loginId",
                    "fullName": "fullName",
                    "id": "id",
                    "isActive": "isActive",
                    "email": "email"
                },
                "contextUrl": "/v1/users?pageSize=99999&filter=isActive eq \"true\"",
                "pagingSize": 50,
                "header": {
                    "Authorization": "Bearer $application.accesstoken_blackline$"
                },
                "operationType": "Account Aggregation",
                "xpathNamespaces": null,
                "parentEndpointName": null
            },
            {
                "httpMethodType": "GET",
                "pagingInitialOffset": 0,
                "requestType": "API",
                "sequenceNumberForEndpoint": "5",
                "uniqueNameForEndPoint": "Account Aggregation - Get Account Roles-Products",
                "rootPath": null,
                "body": {
                    "bodyFormData": null,
                    "jsonBody": null,
                    "bodyFormat": null
                },
                "paginationSteps": null,
                "responseCode": [
                    "200"
                ],
                "resMappingObj": {
                    "productId": "$.items..product.id",
                    "roleId": "$.items..role.id",
                    "FullRoleProducts": "$.items[*]"
                },
                "contextUrl": "/v1/users/$response.id$/roles-products",
                "pagingSize": 50,
                "header": {
                    "Authorization": "Bearer $application.accesstoken_blackline$"
                },
                "operationType": "Account Aggregation",
                "xpathNamespaces": null,
                "parentEndpointName": "Account Aggregation - Get Accounts"
            },
            {
                "httpMethodType": "GET",
                "pagingInitialOffset": 0,
                "requestType": "API",
                "sequenceNumberForEndpoint": "6",
                "uniqueNameForEndPoint": "Account Aggregation - Get User Teams",
                "rootPath": "$.items",
                "body": {
                    "bodyFormData": null,
                    "jsonBody": null,
                    "bodyFormat": null
                },
                "paginationSteps": null,
                "responseCode": [
                    "200"
                ],
                "resMappingObj": {
                    "teams": "id"
                },
                "contextUrl": "/v1/users/$response.id$/teams",
                "pagingSize": 50,
                "header": {
                    "Authorization": "Bearer $application.accesstoken_blackline$"
                },
                "operationType": "Account Aggregation",
                "xpathNamespaces": null,
                "parentEndpointName": "Account Aggregation - Get Accounts"
            },
            {
                "httpMethodType": "GET",
                "pagingInitialOffset": 0,
                "requestType": "API",
                "sequenceNumberForEndpoint": "7",
                "uniqueNameForEndPoint": "Account Aggregation - Get User Entities",
                "rootPath": "$.items",
                "body": {
                    "bodyFormData": null,
                    "jsonBody": null,
                    "bodyFormat": null
                },
                "paginationSteps": null,
                "responseCode": [
                    "200"
                ],
                "resMappingObj": {
                    "entities": "id"
                },
                "contextUrl": "/v1/users/$response.id$/entities",
                "pagingSize": 50,
                "header": {
                    "Authorization": "Bearer $application.accesstoken_blackline$"
                },
                "operationType": "Account Aggregation",
                "xpathNamespaces": null,
                "parentEndpointName": "Account Aggregation - Get Accounts"
            },
            {
                "httpMethodType": "GET",
                "pagingInitialOffset": 0,
                "requestType": "API",
                "sequenceNumberForEndpoint": "8",
                "uniqueNameForEndPoint": "Single Account Aggregation - Get Single Account",
                "rootPath": "$.",
                "body": {
                    "bodyFormData": null,
                    "jsonBody": null,
                    "bodyFormat": null
                },
                "paginationSteps": null,
                "responseCode": [
                    "200"
                ],
                "resMappingObj": {
                    "lastName": "lastName",
                    "lastUpdated": "lastUpdated",
                    "firstname": "firstName",
                    "loginId": "loginId",
                    "fullName": "fullName",
                    "id": "id",
                    "isActive": "isActive",
                    "email": "email"
                },
                "contextUrl": "/v1/users/$plan.nativeIdentity$",
                "pagingSize": 50,
                "header": {
                    "Authorization": "Bearer $application.accesstoken_blackline$"
                },
                "operationType": "Get Object",
                "xpathNamespaces": null,
                "parentEndpointName": null
            },
            {
                "httpMethodType": "GET",
                "pagingInitialOffset": 0,
                "requestType": "API",
                "sequenceNumberForEndpoint": "9",
                "uniqueNameForEndPoint": "Single Account Aggregation - Get User Roles-Products",
                "rootPath": null,
                "body": {
                    "bodyFormData": null,
                    "jsonBody": null,
                    "bodyFormat": null
                },
                "paginationSteps": null,
                "responseCode": [
                    "200"
                ],
                "resMappingObj": {
                    "productId": "$.items..product.id",
                    "roleId": "$.items..role.id",
                    "FullRoleProducts": "$.items[*]"
                },
                "contextUrl": "/v1/users/$response.id$/roles-products",
                "pagingSize": 50,
                "header": {
                    "Authorization": "Bearer $application.accesstoken_blackline$"
                },
                "operationType": "Get Object",
                "xpathNamespaces": null,
                "parentEndpointName": "Single Account Aggregation - Get Single Account"
            },
            {
                "httpMethodType": "GET",
                "pagingInitialOffset": 0,
                "requestType": "API",
                "sequenceNumberForEndpoint": "10",
                "uniqueNameForEndPoint": "Single Account Aggregation - Get User Teams",
                "rootPath": "$.items",
                "body": {
                    "bodyFormData": null,
                    "jsonBody": null,
                    "bodyFormat": null
                },
                "paginationSteps": null,
                "responseCode": [
                    "200"
                ],
                "resMappingObj": {
                    "teams": "id"
                },
                "contextUrl": "/v1/users/$response.id$/teams",
                "pagingSize": 50,
                "header": {
                    "Authorization": "Bearer $application.accesstoken_blackline$"
                },
                "operationType": "Get Object",
                "xpathNamespaces": null,
                "parentEndpointName": "Single Account Aggregation - Get Single Account"
            },
            {
                "httpMethodType": "GET",
                "pagingInitialOffset": 0,
                "requestType": "API",
                "sequenceNumberForEndpoint": "11",
                "uniqueNameForEndPoint": "Single Account Aggregation - Get User entities",
                "rootPath": "$.items",
                "body": {
                    "bodyFormData": null,
                    "jsonBody": null,
                    "bodyFormat": null
                },
                "paginationSteps": null,
                "responseCode": [
                    "200"
                ],
                "resMappingObj": {
                    "entities": "id"
                },
                "contextUrl": "/v1/users/$response.id$/entities",
                "pagingSize": 50,
                "header": {
                    "Authorization": "Bearer $application.accesstoken_blackline$"
                },
                "operationType": "Get Object",
                "xpathNamespaces": null,
                "parentEndpointName": "Single Account Aggregation - Get Single Account"
            },
            {
                "httpMethodType": "GET",
                "pagingInitialOffset": 0,
                "requestType": "API",
                "sequenceNumberForEndpoint": "12",
                "uniqueNameForEndPoint": "Group Aggregation - Role Products",
                "rootPath": "$.items[*]",
                "body": {
                    "bodyFormData": null,
                    "jsonBody": null,
                    "bodyFormat": null
                },
                "paginationSteps": null,
                "responseCode": [
                    "200"
                ],
                "resMappingObj": {
                    "roleProductId": "role",
                    "productId": "product.id",
                    "roleId": "role.id",
                    "roleName": "role.name",
                    "productName": "product.name"
                },
                "contextUrl": "/v1/users/roles-products",
                "pagingSize": 50,
                "header": {
                    "Authorization": "Bearer $application.accesstoken_blackline$"
                },
                "operationType": "Group Aggregation-roleProduct",
                "xpathNamespaces": null,
                "parentEndpointName": null
            },
            {
                "httpMethodType": "GET",
                "pagingInitialOffset": 0,
                "requestType": "API",
                "sequenceNumberForEndpoint": "13",
                "uniqueNameForEndPoint": "Group Aggregation - Blackline Teams",
                "rootPath": "$.items",
                "body": {
                    "bodyFormData": null,
                    "jsonBody": null,
                    "bodyFormat": null
                },
                "paginationSteps": null,
                "responseCode": [
                    "200"
                ],
                "resMappingObj": {
                    "uniqueCode": "uniqueCode",
                    "level": "level",
                    "name": "name",
                    "id": "id",
                    "parentId": "parentId"
                },
                "contextUrl": "/v1/teams?pageSize=9999",
                "pagingSize": 50,
                "header": {
                    "Authorization": "Bearer $application.accesstoken_blackline$"
                },
                "operationType": "Group Aggregation-Teams",
                "xpathNamespaces": null,
                "parentEndpointName": null
            },
            {
                "httpMethodType": "GET",
                "pagingInitialOffset": 0,
                "requestType": "API",
                "sequenceNumberForEndpoint": "14",
                "uniqueNameForEndPoint": "Group Aggregation - Entities",
                "rootPath": "$.items",
                "body": {
                    "bodyFormData": null,
                    "jsonBody": null,
                    "bodyFormat": null
                },
                "paginationSteps": null,
                "responseCode": [
                    "200"
                ],
                "resMappingObj": {
                    "lineage": "lineage",
                    "sourceId": "sourceId",
                    "levelId": "levelId",
                    "name": "name",
                    "financialReviewRequired": "financialReviewRequired",
                    "id": "id",
                    "varianceGroupRequired": "varianceGroupRequired",
                    "key": "key",
                    "parentId": "parentId"
                },
                "contextUrl": "/v1/mdm/entities",
                "pagingSize": 50,
                "header": {
                    "Authorization": "Bearer $application.accesstoken_blackline$"
                },
                "operationType": "Group Aggregation-Entity",
                "xpathNamespaces": null,
                "parentEndpointName": null
            },
            {
                "httpMethodType": "POST",
                "pagingInitialOffset": 0,
                "requestType": "API",
                "sequenceNumberForEndpoint": "15",
                "uniqueNameForEndPoint": "Add Entitlement - Teams",
                "rootPath": null,
                "body": {
                    "jsonBody": "[\n$plan.teams$\n]",
                    "bodyFormat": "raw"
                },
                "paginationSteps": null,
                "responseCode": [
                    "204"
                ],
                "resMappingObj": null,
                "contextUrl": "/v1/users/$plan.nativeIdentity$/teams",
                "pagingSize": 50,
                "header": {
                    "Authorization": "Bearer $application.accesstoken_blackline$"
                },
                "operationType": "Add Entitlement-Teams",
                "xpathNamespaces": null,
                "parentEndpointName": null
            },
            {
                "httpMethodType": "POST",
                "pagingInitialOffset": 0,
                "requestType": "API",
                "sequenceNumberForEndpoint": "16",
                "uniqueNameForEndPoint": "Add Entitlement - Entities",
                "rootPath": null,
                "body": {
                    "jsonBody": "[\n$plan.entities$\n]",
                    "bodyFormat": "raw"
                },
                "paginationSteps": null,
                "responseCode": [
                    "204"
                ],
                "resMappingObj": null,
                "contextUrl": "/v1/users/$plan.nativeIdentity$/entities",
                "pagingSize": 50,
                "header": {
                    "Authorization": "Bearer $application.accesstoken_blackline$"
                },
                "operationType": "Add Entitlement-Entity",
                "xpathNamespaces": null,
                "parentEndpointName": null
            },
            {
                "httpMethodType": "POST",
                "pagingInitialOffset": 0,
                "requestType": "API",
                "sequenceNumberForEndpoint": "17",
                "uniqueNameForEndPoint": "Add Entitlement - roleProducts",
                "rootPath": null,
                "body": {
                    "jsonBody": "[\n{$plan.roleProducts$}\n]",
                    "bodyFormat": "raw"
                },
                "paginationSteps": null,
                "responseCode": [
                    "204"
                ],
                "resMappingObj": null,
                "contextUrl": "/v1/users/$plan.nativeIdentity$/roles-products",
                "pagingSize": 50,
                "header": {
                    "Authorization": "Bearer $application.accesstoken_blackline$"
                },
                "operationType": "Add Entitlement-roleProduct",
                "xpathNamespaces": null,
                "parentEndpointName": null
            },
            {
                "httpMethodType": "DELETE",
                "pagingInitialOffset": 0,
                "requestType": "API",
                "sequenceNumberForEndpoint": "18",
                "uniqueNameForEndPoint": "Remove Entitlements - Teams",
                "rootPath": null,
                "body": {
                    "bodyFormat": null
                },
                "paginationSteps": null,
                "responseCode": [
                    "204",
                    "400"
                ],
                "resMappingObj": null,
                "contextUrl": "/v1/users/$plan.nativeIdentity$/teams/$plan.teams$",
                "pagingSize": 50,
                "header": {
                    "Authorization": "Bearer $application.accesstoken_blackline$"
                },
                "operationType": "Remove Entitlement-Teams",
                "xpathNamespaces": null,
                "parentEndpointName": null
            },
            {
                "httpMethodType": "DELETE",
                "pagingInitialOffset": 0,
                "requestType": "API",
                "sequenceNumberForEndpoint": "19",
                "uniqueNameForEndPoint": "Remove Entitlements - Entities",
                "rootPath": null,
                "body": {
                    "bodyFormat": null
                },
                "paginationSteps": null,
                "responseCode": [
                    "204",
                    "400"
                ],
                "resMappingObj": null,
                "contextUrl": "/v1/users/$plan.nativeIdentity$/entities/$plan.entities$",
                "pagingSize": 50,
                "header": {
                    "Authorization": "Bearer $application.accesstoken_blackline$"
                },
                "operationType": "Remove Entitlement-Entity",
                "xpathNamespaces": null,
                "parentEndpointName": null
            },
            {
                "httpMethodType": "DELETE",
                "pagingInitialOffset": 0,
                "requestType": "API",
                "sequenceNumberForEndpoint": "20",
                "uniqueNameForEndPoint": "Remove Entitlements - roleProducts",
                "rootPath": null,
                "body": {
                    "bodyFormat": null
                },
                "paginationSteps": null,
                "responseCode": [
                    "204",
                    "400"
                ],
                "resMappingObj": null,
                "contextUrl": "/v1/users/$plan.nativeIdentity$/roles-products?$plan.roleProducts$",
                "pagingSize": 50,
                "header": {
                    "Authorization": "Bearer $application.accesstoken_blackline$"
                },
                "operationType": "Remove Entitlement-roleProduct",
                "xpathNamespaces": null,
                "parentEndpointName": null
            },
            {
                "httpMethodType": "POST",
                "pagingInitialOffset": 0,
                "requestType": "API",
                "sequenceNumberForEndpoint": "21",
                "uniqueNameForEndPoint": "Disable Account",
                "rootPath": "$.",
                "body": {
                    "bodyFormat": null
                },
                "paginationSteps": null,
                "responseCode": [
                    "200"
                ],
                "resMappingObj": {
                    "id": "userId"
                },
                "contextUrl": "/v1/users/$plan.nativeIdentity$/deprovision",
                "pagingSize": 50,
                "header": {
                    "Authorization": "Bearer $application.accesstoken_blackline$"
                },
                "operationType": "Disable Account",
                "xpathNamespaces": null,
                "parentEndpointName": null
            }

Customizer Code:

import {
    Context,
    createConnectorCustomizer,
    readConfig,
    logger,
    StdAccountReadInput,
    StdAccountReadOutput,
    StdAccountListOutput,
    StdTestConnectionOutput,
    StdAccountUpdateInput,
    StdEntitlementListInput,
    StdEntitlementListOutput,
} from '@sailpoint/connector-sdk'

// Connector customizer must be exported as module property named connectorCustomizer
export const connectorCustomizer = async () => {
    // Get connector source config
    const config = await readConfig()

    return (
        createConnectorCustomizer()
            .afterStdTestConnection(async (context: Context, output: StdTestConnectionOutput) => {
                logger.info('Running after test connection')
                return output
            })
            // .beforeStdAccountRead(async (context: Context, input: StdAccountReadInput) => {
            //     logger.info(`Running before account, for account ${input.identity}`)
            //     return input
            // })
            // .afterStdAccountRead(async (context: Context, output: StdAccountReadOutput) => {
            //     logger.info(`Running after account read to add custom attribute "location"`)

            //     output.attributes.location = 'Austin'
            //     return output
            // })

            .afterStdAccountList(async (context: Context, output: StdAccountListOutput) => {
                logger.info(`Running after account list for account ${JSON.stringify(output)}`)
                logger.info(`Account Attributes: ${JSON.stringify(output.attributes)}`)

                logger.info(`Entitlements: ${JSON.stringify(output.attributes.roleProducts)}`)
                logger.info(`Entitlements: ${JSON.stringify(output.attributes.FullRoleProducts)}`)
                logger.info(`Entitlements Count: ${output.attributes.FullRoleProducts}`)

                const ensureStringArray = (value: unknown): String[] => {
                    if (Array.isArray(value)) {
                        return value.filter((v): v is String => typeof v === 'string')
                    }
                    if (typeof value === 'string') {
                        return [value]
                    }
                    return []
                }

                const roleIds_unnormailzed = output.attributes.roleId
                const productIds_unnormailzed = output.attributes.productId
                const roleIds = ensureStringArray(roleIds_unnormailzed)
                const productIds = ensureStringArray(productIds_unnormailzed)

                const roleProducts: string[] = []
                if (Array.isArray(roleIds) && Array.isArray(productIds) && roleIds.length === productIds.length) {
                    for (let i = 0; i < roleIds.length; i++) {
                        roleProducts.push(`${roleIds[i]}${productIds[i]}`)
                    }
                }

                output.attributes.roleProducts = roleProducts
                logger.info(`Updated Entitlements: ${JSON.stringify(output.attributes.roleProducts)}`)

                return output
            })
            .afterStdAccountRead(async (context: Context, output: StdAccountReadOutput) => {
                logger.info(`Running after account read for account ${JSON.stringify(output)}`)
                logger.info(`Running After Account read for Account Attributes ${JSON.stringify(output.attributes)}`)

                const ensureStringArray = (value: unknown): String[] => {
                    if (Array.isArray(value)) {
                        return value.filter((v): v is String => typeof v === 'string')
                    }
                    if (typeof value === 'string') {
                        return [value]
                    }
                    return []
                }

                const roleIds_unnormailzed = output.attributes.roleId
                const productIds_unnormailzed = output.attributes.productId
                const roleIds = ensureStringArray(roleIds_unnormailzed)
                const productIds = ensureStringArray(productIds_unnormailzed)

                const roleProducts: string[] = []
                if (Array.isArray(roleIds) && Array.isArray(productIds) && roleIds.length === productIds.length) {
                    for (let i = 0; i < roleIds.length; i++) {
                        roleProducts.push(`${roleIds[i]}${productIds[i]}`)
                    }
                }
                output.attributes.roleProducts = roleProducts
                logger.info(`Entitlements: ${JSON.stringify(output.attributes)}`)
                return output
            })

            .beforeStdAccountUpdate(async (context: Context, input: StdAccountUpdateInput) => {
                logger.info(`Running before account, for account ${input.identity}`)
                logger.info(`Account Attributes: ${JSON.stringify(input.changes)}`)
                if (input.changes && input.changes[0].attribute === 'roleProducts' && input.changes[0].op === 'Add') {
                    const cleaned = input.changes[0].value.trim()
                    const role = parseInt(cleaned.slice(0, cleaned.length - 1), 10)
                    const product = cleaned.charAt(cleaned.length - 1)
                    logger.info(`Changing input value to custom value for role products`)
                    input.changes[0].value = `"roleId": ${role}, "productId": "${product}"`
                    logger.info(`New Value: ${input.changes[0].value}`)
                }
                for (const change of input.changes) {
                    if (change && change.attribute === 'roleProducts' && change.op === 'Remove') {
                        const removeRoleProductsList = []
                        for (const roleProduct of change.value) {
                            const cleaned = roleProduct.trim()
                            const role = parseInt(cleaned.slice(0, cleaned.length - 1), 10)
                            const product = cleaned.charAt(cleaned.length - 1)
                            logger.info(`Changing input value to custom value for role products`)
                            const newValue = `roleId=${role}&productId=${product}`
                            removeRoleProductsList.push(newValue)
                            logger.info(`New Value: ${newValue}`)
                        }
                        change.value = removeRoleProductsList
                    }
                }
                return input
            })

            .beforeStdEntitlementList(async (context: Context, input: StdEntitlementListInput) => {
                logger.info(`Running before entitlement list for entitlement. State ${input.state}`)
                logger.info(`Entitlement Schema: ${JSON.stringify(input.schema)}`)
                logger.info(`Entitlement Attributes: ${JSON.stringify(input.schema?.attributes)}`)
                logger.info(`Entitlement Attributes: ${input.schema?.attributes[0]?.name}`)
                logger.info(`Entitlement identityAttribute: ${JSON.stringify(input.schema?.identityAttribute)}`)
                return input
            })

            .afterStdEntitlementList(async (context: Context, output: StdEntitlementListOutput) => {
                logger.info(`Running after entitlement list for entitlement type ${output.type}`)
                logger.info(`Entitlements: ${JSON.stringify(output.attributes.roleProductId)}`)
                if (output.attributes && 'roleProduct' === output.type) {
                    output.attributes.id = `${output.attributes.roleName}${output.attributes.productId}`
                    output.attributes.roleProductId = `${output.attributes.roleName}${output.attributes.productId}`
                    output.attributes.roleProductname = `${output.attributes.roleName}-${output.attributes.productName}`
                    let entitlement_id = `${output.attributes.roleId}${output.attributes.productId}`
                    output.key = { simple: { id: entitlement_id } }
                }
                logger.info(`Entitlements: ${output.attributes.roleName}`)
                logger.info(`Identity: ${output.identity}`)
                logger.info(`Output Key: ${JSON.stringify(output.key)}`)
                logger.info(`Entitlements: ${output.attributes.productName}`)
                return output
            })
    )
}

:bangbang: Please be sure you’ve read the docs and API specs before asking for help. Also, please be sure you’ve searched the forum for your answer before you create a new topic.

Please consider addressing the following when creating your topic:

  • What have you tried?
  • What errors did you face (share screenshots)?
  • Share the details of your efforts (code / search query, workflow json etc.)?
  • What is the result you are getting and what were you expecting?
2 Likes

Hi Nicholas,

The application looks good. Are you able to perform all the operations ?

Thanks

Yea, I do not have the Account Enable API call in the source because for our use case we do not need it, but It will remove and add all entitlements, and when they are disabled it will strip all access the user has on Blackline and remove them from any tasks that they were assigned to.

Nice work on this.

Small improvements you can make.

Right now the normalized value is built by simple concatenation like roleId + productId. That works, but it can become ambiguous later. Using a delimiter like 123|A instead of 123A would make parsing safer and easier to maintain.

Because roleId + productId has no separator, the parser has to guess where one value ends and the other begins.

Example with your current logic:

  • 123A → easy to read as roleId=123, productId=A
  • but 12AB becomes confusing if product format ever changes
  • and if product IDs are not always exactly one character, parsing breaks

In your code, this line assumes the last character is always the product:


const role = parseInt(cleaned.slice(0, cleaned.length - 1), 10)
const product = cleaned.charAt(cleaned.length - 1)

So:

  • 123A → role=123, product=A
  • 12310 → role=1231, product=0 (wrong if product was actually 10)

If you store it as:

123|A

then parsing becomes explicit:

  • split on |
  • left side = roleId
  • right side = productId

Example:

const [rolePart, productPart] = cleaned.split('|')
const role = parseInt(rolePart, 10)
const product = productPart

Instead of


roleProducts.push(`${roleIds[i]}${productIds[i]}`)

Use

roleProducts.push(`${roleIds[i]}|${productIds[i]}`)

Sweet thank you! A couple days after I posted this, I realized that same problem lol. Thankfully they will only use a Single character for now for the product, but defiantly makes it work a lot better!

1 Like

Happy to help.
Thank you for the Connector. Keep up the good work.