Skip to content

Commit

Permalink
Add createIntegrationHelpers
Browse files Browse the repository at this point in the history
  • Loading branch information
Geovanni Pacheco committed May 1, 2024
1 parent d0f25b3 commit f3fb77e
Show file tree
Hide file tree
Showing 108 changed files with 28,376 additions and 14 deletions.
4 changes: 4 additions & 0 deletions packages/integration-sdk-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ npm install @jupiterone/integration-sdk-core
yarn add @jupiterone/integration-sdk-core
```

## Docs

- [createIntegrationHelpers](docs/createIntegrationHelpers.md)
121 changes: 121 additions & 0 deletions packages/integration-sdk-core/docs/createIntegrationHelpers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# createIntegrationHelpers

This function is used to create typeful integration helpers .

```typescript
function createIntegrationHelpers(
options: CreateIntegrationHelpersOptions,
): IntegrationHelpers;
```

#### Parameters

- `options` - An object containing the following properties:
- `integrationName`: Integration name. **_Example: 'aws', 'microsoft_365'_**
- `classSchemaMap`: Typebox class schema map.

#### Returns

An object containing the following properties:

- `createEntityType`: A function to generate entity types with
`${integrationName}_${entityName}` pattern.
- `createIntegrationEntity`: A function to create a entity metadata and a typed
create entity function.

#### Example

```typescript
import { createIntegrationHelpers } from '@jupiterone/integration-sdk-core';
import { classSchemaTypeboxMap } from '@jupiterone/data-model';

const { createEntityType, createIntegrationEntity } = createIntegrationHelpers({
integrationName: 'my_awesome_integration',
schemaMap: classSchemaTypeboxMap,
});

const [USER_ENTITY, createUserAssignEntity] = createIntegrationEntity({
resourceName: 'User',
_class: ['User'],
_type: createEntityType('user'), // This will generate "my_awesome_integration_user", but you are free to not use the createEntityType helper
description: 'Entity description', // This will be used in the json schema
schema: Type.Object({
name: Type.String(),
}),
});

// _type and _class will be generated automatically
createUserAssignEntity({
_key: `${Entities.ACCOUNT._type}|${_integrationInstanceId}`,
name,
});
```

# How to use it in a current graph integration

1. Create a file named helpers.ts in the src folder of your integration
2. Add the following code to the file:

```typescript
import { createIntegrationHelpers } from '@jupiterone/integration-sdk-core';
import { classSchemaTypeboxMap } from '@jupiterone/data-model';

export const { createEntityType, createIntegrationEntity } =
createIntegrationHelpers({
integrationName: INTEGRATION_NAME,
schemaMap: classSchemaTypeboxMap,
});
```

3. Replace INTEGRATION_NAME with the name of your integration
4. Create a file named entities.ts in the src folder of your integration and add
all entities as the following EXAMPLE code:

```typescript
import { createEntityType, createIntegrationEntity } from './helpers';

export const [UserEntityMetadata, createUserAssignEntity] =
createIntegrationEntity({
resourceName: 'User',
_class: ['User'],
_type: createEntityType('user'), // This will generate `${INTEGRATION_NAME}_user`, but you are free to not use the createEntityType helper
description: 'Entity description', // This will be used in the json schema
schema: Type.Object({
name: Type.String(),
}),
});
```

5. Edit the constants.ts files and replace every entity with the new entities
created in the entities.ts file as the following EXAMPLE code:

```typescript
import { UserEntityMetadata } from './entities';

export const Entities: Record<
| 'USER'
StepEntityMetadata
> = {
USER: UserEntityMetadata,
};
```

6. Now you can go into every step converter and add your create entity helper to
`createIntegrationEntity` assign property and have full autocomplete as the
following EXAMPLE code:

```diff
export function createUserEntity(name: string): Entity {
return createIntegrationEntity({
entityData: {
source: {},
- assign: createUserAssignEntity({
+ assign: createUserAssignEntity({
_key: `${Entities.ACCOUNT._type}|${_integrationInstanceId}`,
name,
- },
+ }),
},
});
}
```
11 changes: 8 additions & 3 deletions packages/integration-sdk-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,18 @@
"test": "jest",
"prebuild:dist": "rm -rf dist && mkdir dist",
"build:dist": "tsc -p tsconfig.dist.json --declaration",
"prepack": "yarn build:dist"
"prepack": "yarn build:dist",
"generate:types": "node -r ts-node/register tools/generateTypeboxSchemas.ts"
},
"dependencies": {
"@jupiterone/data-model": "^0.55.0",
"@jupiterone/data-model": "^0.57.0",
"@jupiterone/integration-sdk-entity-validator": "^12.6.0",
"@sinclair/typebox": "^0.32.25",
"lodash": "^4.17.21"
},
"devDependencies": {
"@types/lodash": "^4.14.168"
"@types/lodash": "^4.14.168",
"schema2typebox": "^1.7.2",
"ts-node": "^10.9.2"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { Type } from '@sinclair/typebox';
import { SchemaMap } from '../../../tools/schemas';
import { createIntegrationHelpers } from '../createIntegrationHelpers';
import { EntityValidator } from '@jupiterone/integration-sdk-entity-validator';
import { entitySchemas } from '@jupiterone/data-model';

describe('createIntegrationHelpers', () => {
const { createEntityType, createIntegrationEntity } =
createIntegrationHelpers({
integrationName: 'test',
classSchemaMap: SchemaMap,
});

test('createEntityType', () => {
expect(createEntityType('entity')).toBe('test_entity');
});

test('createIntegrationEntity createEntity', () => {
const [, createEntity] = createIntegrationEntity({
resourceName: 'Entity',
_class: ['Entity'],
_type: 'entity',
description: 'Entity description',
schema: Type.Object({
id: Type.String(),
name: Type.String(),
}),
});

expect(
createEntity({
id: '1',
name: 'entity',
_key: 'id:123456',
displayName: 'Entity',
}),
).toEqual({
id: '1',
name: 'entity',
_key: 'id:123456',
displayName: 'Entity',
_class: ['Entity'],
_type: 'entity',
});
});

test('createIntegrationEntity schema', () => {
const [{ schema }] = createIntegrationEntity({
resourceName: 'Entity',
_class: ['Entity'],
_type: 'entity',
description: 'Entity description',
schema: Type.Object({
id: Type.String(),
name: Type.String(),
}),
});

expect(schema).toEqual({
$id: '#entity',
description: 'Entity description',
allOf: [
{ $ref: '#Entity' },
{
properties: {
id: { type: 'string' },
name: { type: 'string' },
_class: {
type: 'array',
items: [
{
const: 'Entity',
type: 'string',
},
],
additionalItems: false,
maxItems: 1,
minItems: 1,
},
_type: { const: 'entity', type: 'string' },
},
required: ['_class', '_type', 'id', 'name'],
type: 'object',
},
],
});
});

test('createIntegrationEntity schema should validate createEntity return', () => {
const validator = new EntityValidator({
schemas: Object.values(entitySchemas),
});
const [{ schema }, createEntity] = createIntegrationEntity({
resourceName: 'Type',
_class: ['Entity'],
_type: 'type',
description: 'Type description',
schema: Type.Object({
id: Type.String(),
name: Type.String(),
}),
});
validator.addSchemas(schema);

const { errors } = validator.validateEntity(
createEntity({
id: '1',
name: 'type',
_key: 'id:123456789',
displayName: 'Type',
}),
);

expect(errors).toEqual(null);
});
});
84 changes: 84 additions & 0 deletions packages/integration-sdk-core/src/data/createIntegrationHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Static, TObject, TRef, Type } from '@sinclair/typebox';
import { StepEntityMetadata } from '../types';

interface CreateIntegrationHelpersOptions<
IntegrationName extends string,
SchemaKey extends string,
ClassSchemaMap extends Record<SchemaKey, TObject>,
> {
integrationName: IntegrationName;
classSchemaMap: ClassSchemaMap;
}

export const createIntegrationHelpers = <
IntegrationName extends string,
SchemaKey extends string,
ClassSchemaMap extends Record<SchemaKey, TObject>,
>({
integrationName,
classSchemaMap,
}: CreateIntegrationHelpersOptions<
IntegrationName,
SchemaKey,
ClassSchemaMap
>) => {
const createEntityType = <EntityName extends string>(
entityName: EntityName,
) => `${integrationName}_${entityName}` as const;

const createIntegrationEntity = <
ResourceName extends string,
Class extends keyof ClassSchemaMap & string,
EntityType extends string,
Schema extends TObject,
>({
resourceName,
_class,
_type,
description,
schema,
...entityMetadata
}: Omit<StepEntityMetadata, 'schema'> & {
resourceName: ResourceName;
_class: [Class, ...Class[]];
_type: EntityType;
description: string;
schema: Schema;
}) => {
const classSchemaRefs = _class.map((className) =>
Type.Ref(classSchemaMap[className]),
) as [TRef<ClassSchemaMap[Class]>, ...TRef<ClassSchemaMap[Class]>[]];

const baseSchema = Type.Composite([
Type.Object({
_class: Type.Tuple(_class.map((className) => Type.Literal(className))),
_type: Type.Literal(_type),
}),
schema,
]);

const entitySchema = Type.Intersect([...classSchemaRefs, baseSchema], {
$id: `#${_type}`,
description: description,
});
type EntitySchemaType = Static<typeof entitySchema>;

const createEntity = (
entityData: Omit<EntitySchemaType, '_class' | '_type'>,
): EntitySchemaType => {
return { ...entityData, _class: _class, _type: _type };
};

const stepEntityMetadata = {
_class,
_type,
resourceName,
schema: Type.Strict(entitySchema),
...entityMetadata,
} satisfies StepEntityMetadata;

return [stepEntityMetadata, createEntity] as const;
};

return { createEntityType, createIntegrationEntity };
};
Loading

0 comments on commit f3fb77e

Please sign in to comment.