From 5d68ba94b591071831e9fc4906ec0a0fa38954fb Mon Sep 17 00:00:00 2001 From: Andres Reales Date: Thu, 31 Oct 2024 04:22:18 -0500 Subject: [PATCH] feat(perimeter81): Add users endpoints to Perimeter81 integration (#81) ## Describe your changes Add Perimeter81 integration with the following endpoints: ### Syncs - users ### Actions - create-user - delete-user **Important Note: No fixtures, nor tests/mocks were generated. I could not test any of the syncs and actions added to the Perimeter81 integration as it requires an enterprise account, which is not accessible to me. Hence, once a connection is setup in Nango, these endpoints need to be tested and validated** ## Issue ticket number and link N/A ## Checklist before requesting a review (skip if just adding/editing APIs & templates) - [ ] I added tests, otherwise the reason is: I could not test any of the syncs and actions added to the Perimeter81 integration as it requires an enterprise account, which is not accessible to me. Hence, once a connection is setup in Nango, these endpoints need to be tested and validated - [X] External API requests have `retries` - [X] Pagination is used where appropriate - [X] The built in `nango.paginate` call is used instead of a `while (true)` loop - [ ] Third party requests are NOT parallelized (this can cause issues with rate limits) - [ ] If a sync requires metadata the `nango.yaml` has `auto_start: false` - [X] If the sync is a `full` sync then `track_deletes: true` is set --------- Co-authored-by: Khaliq Co-authored-by: Khaliq --- flows.yaml | 49 ++++++++++++++++ .../perimeter81/actions/create-user.ts | 56 +++++++++++++++++++ .../perimeter81/actions/delete-user.ts | 46 +++++++++++++++ integrations/perimeter81/mappers/to-user.ts | 17 ++++++ integrations/perimeter81/nango.yaml | 48 ++++++++++++++++ integrations/perimeter81/schema.zod.ts | 42 ++++++++++++++ integrations/perimeter81/syncs/users.ts | 39 +++++++++++++ integrations/perimeter81/types.ts | 10 ++++ 8 files changed, 307 insertions(+) create mode 100644 integrations/perimeter81/actions/create-user.ts create mode 100644 integrations/perimeter81/actions/delete-user.ts create mode 100644 integrations/perimeter81/mappers/to-user.ts create mode 100644 integrations/perimeter81/nango.yaml create mode 100644 integrations/perimeter81/schema.zod.ts create mode 100644 integrations/perimeter81/syncs/users.ts create mode 100644 integrations/perimeter81/types.ts diff --git a/flows.yaml b/flows.yaml index bc373c87..cf68e4ae 100644 --- a/flows.yaml +++ b/flows.yaml @@ -5620,6 +5620,55 @@ integrations: DocumentInput: threadId: string attachmentId: string + perimeter81: + actions: + create-user: + description: Creates a user in Perimeter81 + input: Perimeter81CreateUser + endpoint: POST /users + output: User + delete-user: + description: Deletes a user in Perimeter81 + endpoint: DELETE /users + output: SuccessResponse + input: IdEntity + syncs: + users: + description: | + Fetches the list of users from Perimeter81 + endpoint: GET /users + sync_type: full + track_deletes: true + runs: every day + output: User + models: + IdEntity: + id: string + SuccessResponse: + success: boolean + User: + id: string + email: string + firstName: string + lastName: string + CreateUser: + firstName: string + lastName: string + email: string + Perimeter81CreateUser: + firstName: string + lastName: string + email: string + idpType?: string + accessGroups?: string[] + emailVerified?: boolean + inviteMessage?: string + origin?: string + profileData?: + roleName?: string + phone?: string + icon?: string + origin?: string pipedrive: syncs: activities: diff --git a/integrations/perimeter81/actions/create-user.ts b/integrations/perimeter81/actions/create-user.ts new file mode 100644 index 00000000..57cf6d61 --- /dev/null +++ b/integrations/perimeter81/actions/create-user.ts @@ -0,0 +1,56 @@ +import type { NangoAction, ProxyConfiguration, User, Perimeter81CreateUser } from '../../models'; +import { toUser } from '../mappers/to-user.js'; +import { perimeter81CreateUserSchema } from '../schema.zod.js'; +import type { Perimeter81User } from '../types'; + +/** + * Creates an Perimeter81 user. + * + * This function validates the input against the defined schema and constructs a request + * to the Perimeter81 API to create a new user. If the input is invalid, it logs the + * errors and throws an ActionError. + * + * @param {NangoAction} nango - The Nango action context, used for logging and making API requests. + * @param {Perimeter81CreateUser} input - The input data for creating a user contact + * + * @returns {Promise} - A promise that resolves to the created User object. + * + * @throws {nango.ActionError} - Throws an error if the input validation fails. + * + * For detailed endpoint documentation, refer to: + * https://support.perimeter81.com/docs/post-new-member + */ +export default async function runAction(nango: NangoAction, input: Perimeter81CreateUser): Promise { + const parsedInput = perimeter81CreateUserSchema.safeParse(input); + + if (!parsedInput.success) { + for (const error of parsedInput.error.errors) { + await nango.log(`Invalid input provided to create a user: ${error.message} at path ${error.path.join('.')}`, { level: 'error' }); + } + + throw new nango.ActionError({ + message: 'Invalid input provided to create a user' + }); + } + + const { firstName, lastName, profileData = {}, ...data } = parsedInput.data; + + const config: ProxyConfiguration = { + // https://support.perimeter81.com/docs/post-new-member + endpoint: `/v1/users`, + data: { + ...data, + inviteMessage: parsedInput.data.inviteMessage || 'Welcome to the team!', + profileData: { + ...profileData, + firstName, + lastName + } + }, + retries: 10 + }; + + const response = await nango.post(config); + + return toUser(response.data); +} diff --git a/integrations/perimeter81/actions/delete-user.ts b/integrations/perimeter81/actions/delete-user.ts new file mode 100644 index 00000000..5e039c36 --- /dev/null +++ b/integrations/perimeter81/actions/delete-user.ts @@ -0,0 +1,46 @@ +import type { NangoAction, ProxyConfiguration, SuccessResponse, IdEntity } from '../../models'; +import { idEntitySchema } from '../schema.zod.js'; + +/** + * Deletes a Perimeter81 user. + * + * This function validates the input against the defined schema and constructs a request + * to the Perimeter81 API to delete a user by their ID. If the input is invalid, + * it logs the errors and throws an ActionError. + * + * @param {NangoAction} nango - The Nango action context, used for logging and making API requests. + * @param {IdEntity} input - The input data containing the ID of the user contact to be deleted + * + * @returns {Promise} - A promise that resolves to a SuccessResponse object indicating the result of the deletion. + * + * @throws {nango.ActionError} - Throws an error if the input validation fails. + * + * For detailed endpoint documentation, refer to: + * https://support.perimeter81.com/docs/delete-delete-user + */ +export default async function runAction(nango: NangoAction, input: IdEntity): Promise { + const parsedInput = idEntitySchema.safeParse(input); + + if (!parsedInput.success) { + for (const error of parsedInput.error.errors) { + await nango.log(`Invalid input provided to delete a user: ${error.message} at path ${error.path.join('.')}`, { level: 'error' }); + } + + throw new nango.ActionError({ + message: 'Invalid input provided to delete a user' + }); + } + + const config: ProxyConfiguration = { + // https://support.perimeter81.com/docs/delete-delete-user + endpoint: `/v1/users/${parsedInput.data.id}`, + retries: 10 + }; + + // no body content expected for successful requests + await nango.delete(config); + + return { + success: true + }; +} diff --git a/integrations/perimeter81/mappers/to-user.ts b/integrations/perimeter81/mappers/to-user.ts new file mode 100644 index 00000000..2eafa875 --- /dev/null +++ b/integrations/perimeter81/mappers/to-user.ts @@ -0,0 +1,17 @@ +import type { User } from '../../models'; +import type { Perimeter81User } from '../types'; + +/** + * Maps a Perimeter81 API user object to a Nango User object. + * + * @param perimeter81User The raw contact object from the Perimeter81 API. + * @returns Mapped User object with essential properties. + */ +export function toUser(perimeter81User: Perimeter81User): User { + return { + id: perimeter81User.id, + email: perimeter81User.email, + firstName: perimeter81User.firstName, + lastName: perimeter81User.lastName + }; +} diff --git a/integrations/perimeter81/nango.yaml b/integrations/perimeter81/nango.yaml new file mode 100644 index 00000000..baed0b3f --- /dev/null +++ b/integrations/perimeter81/nango.yaml @@ -0,0 +1,48 @@ +integrations: + perimeter81: + actions: + create-user: + description: Creates a user in Perimeter81 + input: Perimeter81CreateUser + endpoint: POST /users + output: User + delete-user: + description: Deletes a user in Perimeter81 + endpoint: DELETE /users + output: SuccessResponse + input: IdEntity + syncs: + users: + description: | + Fetches the list of users from Perimeter81 + endpoint: GET /users + sync_type: full + track_deletes: true + runs: every day + output: User +models: + IdEntity: + id: string + SuccessResponse: + success: boolean + User: + id: string + email: string + firstName: string + lastName: string + CreateUser: + firstName: string + lastName: string + email: string + Perimeter81CreateUser: + __extends: CreateUser + idpType?: string + accessGroups?: string[] + emailVerified?: boolean + inviteMessage?: string + origin?: string + profileData?: + roleName?: string + phone?: string + icon?: string + origin?: string diff --git a/integrations/perimeter81/schema.zod.ts b/integrations/perimeter81/schema.zod.ts new file mode 100644 index 00000000..3d7d7a4a --- /dev/null +++ b/integrations/perimeter81/schema.zod.ts @@ -0,0 +1,42 @@ +// Generated by ts-to-zod +import { z } from 'zod'; + +export const idEntitySchema = z.object({ + id: z.string() +}); + +export const successResponseSchema = z.object({ + success: z.boolean() +}); + +export const userSchema = z.object({ + id: z.string(), + email: z.string(), + firstName: z.string(), + lastName: z.string() +}); + +export const createUserSchema = z.object({ + firstName: z.string(), + lastName: z.string(), + email: z.string() +}); + +export const perimeter81CreateUserSchema = z.object({ + firstName: z.string(), + lastName: z.string(), + email: z.string(), + idpType: z.string().optional(), + accessGroups: z.array(z.string()).optional(), + emailVerified: z.boolean().optional(), + inviteMessage: z.string().optional(), + origin: z.string().optional(), + profileData: z + .object({ + roleName: z.string().optional(), + phone: z.string().optional(), + icon: z.string().optional(), + origin: z.string().optional() + }) + .optional() +}); diff --git a/integrations/perimeter81/syncs/users.ts b/integrations/perimeter81/syncs/users.ts new file mode 100644 index 00000000..9cc22437 --- /dev/null +++ b/integrations/perimeter81/syncs/users.ts @@ -0,0 +1,39 @@ +import type { NangoSync, ProxyConfiguration, User } from '../../models'; +import { toUser } from '../mappers/to-user.js'; +import type { Perimeter81User } from '../types'; + +/** + * Fetches Perimeter81 users, maps them to Nango User objects, + * and saves the processed contacts using NangoSync. + * + * This function handles pagination and ensures that all contacts are fetched, + * transformed, and stored. + * + * For endpoint documentation, refer to: + * https://support.perimeter81.com/docs/get-list-users + * + * @param nango An instance of NangoSync for synchronization tasks. + * @returns Promise that resolves when all users are fetched and saved. + */ +export default async function fetchData(nango: NangoSync): Promise { + const config: ProxyConfiguration = { + // https://support.perimeter81.com/docs/get-list-users + endpoint: '/v1/users', + paginate: { + type: 'offset', + offset_name_in_request: 'page', + offset_start_value: 1, + offset_calculation_method: 'per-page', + limit_name_in_request: 'limit', + response_path: 'data', + limit: 100 + }, + retries: 10 + }; + + for await (const perimeter81Users of nango.paginate(config)) { + const users = perimeter81Users.map(toUser); + + await nango.batchSave(users, 'User'); + } +} diff --git a/integrations/perimeter81/types.ts b/integrations/perimeter81/types.ts new file mode 100644 index 00000000..61eb8e82 --- /dev/null +++ b/integrations/perimeter81/types.ts @@ -0,0 +1,10 @@ +export interface Perimeter81User { + terminated: boolean; + email: string; + emailVerified: boolean; + initials: string; + roleName: string; + lastName: string; + firstName: string; + id: string; +}