Skip to content

Commit

Permalink
feat(keeper): Add Keeper users integration (#80)
Browse files Browse the repository at this point in the history
## Describe your changes
Add Keeper integration with the following endpoints:

### Syncs
- users

### Actions
- create-user
- delete-user


https://www.loom.com/share/f5fe8066d38f4291b14804dc9f339bd3?sid=0cad5083-07d0-433f-91af-575171a9b550
**Note: At the end of the video, I also demo how to get the node id and
the API key from Keeper's admin console**

## Issue ticket number and link
N/A

## Checklist before requesting a review (skip if just adding/editing
APIs & templates)
- [X] I added tests, otherwise the reason is:
- [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 <[email protected]>
Co-authored-by: Khaliq <[email protected]>
  • Loading branch information
3 people authored Oct 29, 2024
1 parent 27a700a commit a467a36
Show file tree
Hide file tree
Showing 22 changed files with 548 additions and 0 deletions.
58 changes: 58 additions & 0 deletions flows.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4485,6 +4485,64 @@ integrations:
lastName: string
email: string
products?: string[]
keeper-scim:
actions:
create-user:
description: Creates a user in Keeper
input: KeeperCreateUser
endpoint: POST /users
output: User
delete-user:
description: Deletes a user in Keeper
endpoint: DELETE /users
output: SuccessResponse
input: IdEntity
syncs:
users:
description: |
Fetches the list of users from Keeper
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
KeeperCreateUser:
firstName: string
lastName: string
email: string
active?: boolean
externalId?: string
phoneNumbers?: PhoneNumber[]
photos?: Photo[]
addresses?: Address[]
title?: string
PhoneNumber:
type: work | mobile | other
value: string
Photo:
type: photo | thumbnail
value: string
Address:
type: work
streetAddress?: string
locality?: string
region?: string
postalCode?: string
country?: string
kustomer:
syncs:
conversations:
Expand Down
62 changes: 62 additions & 0 deletions integrations/keeper-scim/actions/create-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { NangoAction, ProxyConfiguration, User, KeeperCreateUser } from '../../models';
import { toUser } from '../mappers/to-user.js';
import { keeperCreateUserSchema } from '../schema.zod.js';
import type { KeeperUser } from '../types';

/**
* Creates a Keeper user.
*
* This function validates the input against the defined schema and constructs a request
* to the Keeper 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 {KeeperCreateUser} input - The input data for creating a user contact
*
* @returns {Promise<User>} - 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://docs.keeper.io/en/enterprise-guide/user-and-team-provisioning/automated-provisioning-with-scim
*/
export default async function runAction(nango: NangoAction, input: KeeperCreateUser): Promise<User> {
const parsedInput = keeperCreateUserSchema.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, email, ...data } = parsedInput.data;

const config: ProxyConfiguration = {
// https://docs.keeper.io/en/enterprise-guide/user-and-team-provisioning/automated-provisioning-with-scim
endpoint: `/Users`,
data: {
schemas: ['urn:ietf:params:scim:schemas:core:2.0:User'],
userName: email,
emails: [
{
type: 'work',
value: email
}
],
name: {
givenName: firstName,
familyName: lastName
},
...data
},
retries: 10
};

const response = await nango.post<KeeperUser>(config);

return toUser(response.data);
}
46 changes: 46 additions & 0 deletions integrations/keeper-scim/actions/delete-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { NangoAction, ProxyConfiguration, SuccessResponse, IdEntity } from '../../models';
import { idEntitySchema } from '../schema.zod.js';

/**
* Deletes an Keeper user contact.
*
* This function validates the input against the defined schema and constructs a request
* to the Keeper 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<SuccessResponse>} - 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://docs.keeper.io/en/enterprise-guide/user-and-team-provisioning/automated-provisioning-with-scim
*/
export default async function runAction(nango: NangoAction, input: IdEntity): Promise<SuccessResponse> {
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://docs.keeper.io/en/enterprise-guide/user-and-team-provisioning/automated-provisioning-with-scim
endpoint: `/Users/${parsedInput.data.id}`,
retries: 10
};

// no body content expected for successful requests
await nango.delete(config);

return {
success: true
};
}
5 changes: 5 additions & 0 deletions integrations/keeper-scim/fixtures/create-user.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"firstName": "John",
"lastName": "Doe",
"email": "[email protected]"
}
3 changes: 3 additions & 0 deletions integrations/keeper-scim/fixtures/delete-user.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"id": "969017636421645"
}
17 changes: 17 additions & 0 deletions integrations/keeper-scim/mappers/to-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { User } from '../../models';
import type { KeeperUser } from '../types';

/**
* Maps a Keeper API contact object to a Nango User object.
*
* @param keeperUser The raw contact object from the Keeper API.
* @returns Mapped User object with essential properties.
*/
export function toUser(keeperUser: KeeperUser): User {
return {
id: keeperUser.id,
email: keeperUser.emails.find((email) => email.primary)?.value ?? '',
firstName: keeperUser.name.givenName,
lastName: keeperUser.name.familyName
};
}
5 changes: 5 additions & 0 deletions integrations/keeper-scim/mocks/create-user/input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"firstName": "John",
"lastName": "Doe",
"email": "[email protected]"
}
6 changes: 6 additions & 0 deletions integrations/keeper-scim/mocks/create-user/output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"id": "969017636421644",
"email": "[email protected]",
"firstName": "John",
"lastName": "Doe"
}
3 changes: 3 additions & 0 deletions integrations/keeper-scim/mocks/delete-user/input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"id": "969017636421645"
}
3 changes: 3 additions & 0 deletions integrations/keeper-scim/mocks/delete-user/output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"success": true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
""
27 changes: 27 additions & 0 deletions integrations/keeper-scim/mocks/nango/get/proxy/Users/users.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"schemas": ["urn:ietf:params:scim:api:messages:2.0:ListResponse"],
"totalResults": 1,
"startIndex": 1,
"itemsPerPage": 1,
"Resources": [
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName": "[email protected]",
"displayName": "[email protected]",
"name": {
"givenName": "John",
"familyName": "Doe"
},
"id": "970404910858250",
"emails": [
{
"primary": true,
"value": "[email protected]",
"type": "work"
}
],
"groups": [],
"active": true
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"schemas": ["urn:ietf:params:scim:schemas:core:2.0:User"],
"userName": "[email protected]",
"displayName": "[email protected]",
"name": {
"givenName": "John",
"familyName": "Doe"
},
"id": "969017636421644",
"emails": [
{
"primary": true,
"value": "[email protected]",
"type": "work"
}
],
"groups": [],
"active": true
}
1 change: 1 addition & 0 deletions integrations/keeper-scim/mocks/users/User/batchDelete.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[]
8 changes: 8 additions & 0 deletions integrations/keeper-scim/mocks/users/User/batchSave.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[
{
"id": "970404910858250",
"email": "[email protected]",
"firstName": "John",
"lastName": "Doe"
}
]
57 changes: 57 additions & 0 deletions integrations/keeper-scim/nango.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
integrations:
keeper-scim:
actions:
create-user:
description: Creates a user in Keeper
input: KeeperCreateUser
endpoint: POST /users
output: User
delete-user:
description: Deletes a user in Keeper
endpoint: DELETE /users
output: SuccessResponse
input: IdEntity
syncs:
users:
description: |
Fetches the list of users from Keeper
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
KeeperCreateUser:
__extends: CreateUser
active?: boolean
externalId?: string
phoneNumbers?: PhoneNumber[]
photos?: Photo[]
addresses?: Address[]
title?: string
PhoneNumber:
type: work | mobile | other
value: string
Photo:
type: photo | thumbnail
value: string
Address:
type: work
streetAddress?: string
locality?: string
region?: string
postalCode?: string
country?: string
54 changes: 54 additions & 0 deletions integrations/keeper-scim/schema.zod.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// 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 phoneNumberSchema = z.object({
type: z.union([z.literal('work'), z.literal('mobile'), z.literal('other')]),
value: z.string()
});

export const photoSchema = z.object({
type: z.union([z.literal('photo'), z.literal('thumbnail')]),
value: z.string()
});

export const addressSchema = z.object({
type: z.literal('work'),
streetAddress: z.string().optional(),
locality: z.string().optional(),
region: z.string().optional(),
postalCode: z.string().optional(),
country: z.string().optional()
});

export const keeperCreateUserSchema = z.object({
firstName: z.string(),
lastName: z.string(),
email: z.string(),
active: z.boolean().optional(),
externalId: z.string().optional(),
phoneNumbers: z.array(phoneNumberSchema).optional(),
photos: z.array(photoSchema).optional(),
addresses: z.array(addressSchema).optional(),
title: z.string().optional()
});
Loading

0 comments on commit a467a36

Please sign in to comment.