Skip to content

Commit

Permalink
feat: introduce org app installations (#1342)
Browse files Browse the repository at this point in the history
* feat: introduce org app installations

* remove duplicate type

* refactor: move endpoint under app definition

* remove unecessary exported entity

* chore: add unit and integration tests

* fix: export error

* refactor: app deletion in integration tests to 'after' function, add description for new api methods

* refactor: remove direct error handling, fallback to empty string for orgId in rest endpoint to generate correct error

* fix: remove extra space on app installation mock

Co-authored-by: Martin Walker <martin.walker@contentful.com>
Co-authored-by: Martin Walker <35141192+martin3walker@users.noreply.github.com>
  • Loading branch information
3 people authored May 13, 2022
1 parent f8851fe commit ea3b20f
Show file tree
Hide file tree
Showing 11 changed files with 244 additions and 13 deletions.
32 changes: 30 additions & 2 deletions lib/adapters/REST/endpoints/app-definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,18 @@ import type { AxiosInstance } from 'contentful-sdk-core'
import * as raw from './raw'
import copy from 'fast-copy'
import { normalizeSelect } from './utils'
import { GetAppDefinitionParams, GetOrganizationParams, QueryParams } from '../../../common-types'
import { AppDefinitionProps, CreateAppDefinitionProps } from '../../../entities/app-definition'
import {
GetAppDefinitionParams,
GetOrganizationParams,
QueryParams,
GetAppInstallationsForOrgParams,
PaginationQueryParams,
} from '../../../common-types'
import {
AppDefinitionProps,
CreateAppDefinitionProps,
AppInstallationsForOrganizationProps,
} from '../../../entities/app-definition'
import { RestEndpoint } from '../types'
import { SetOptional } from 'type-fest'

Expand All @@ -14,6 +24,11 @@ const getBaseUrl = (params: GetOrganizationParams) =>
export const getAppDefinitionUrl = (params: GetAppDefinitionParams) =>
getBaseUrl(params) + `/${params.appDefinitionId}`

const getBaseUrlForOrgInstallations = (params: GetAppInstallationsForOrgParams) =>
`/app_definitions/${params.appDefinitionId}/app_installations?sys.organization.sys.id[in]=${
params.organizationId || ''
}`

export const get: RestEndpoint<'AppDefinition', 'get'> = (
http: AxiosInstance,
params: GetAppDefinitionParams & QueryParams
Expand Down Expand Up @@ -66,3 +81,16 @@ export const del: RestEndpoint<'AppDefinition', 'delete'> = (
) => {
return raw.del(http, getAppDefinitionUrl(params))
}

export const getInstallationsForOrg: RestEndpoint<'AppDefinition', 'getInstallationsForOrg'> = (
http: AxiosInstance,
params: GetAppInstallationsForOrgParams & PaginationQueryParams
) => {
return raw.get<AppInstallationsForOrganizationProps>(
http,
getBaseUrlForOrgInstallations(params),
{
params: normalizeSelect(params.query),
}
)
}
10 changes: 10 additions & 0 deletions lib/common-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { CreateEntryProps, EntryProps, EntryReferenceProps } from './entities/en
import { CreateEnvironmentProps, EnvironmentProps } from './entities/environment'
import { CreateEnvironmentAliasProps, EnvironmentAliasProps } from './entities/environment-alias'
import { CreateLocaleProps, LocaleProps } from './entities/locale'
import { AppInstallationsForOrganizationProps } from './entities/app-definition'
import { OrganizationProp } from './entities/organization'
import {
CreateOrganizationInvitationProps,
Expand Down Expand Up @@ -285,6 +286,10 @@ type MRInternal<UA extends boolean> = {
(opts: MROpts<'AppDefinition', 'create', UA>): MRReturn<'AppDefinition', 'create'>
(opts: MROpts<'AppDefinition', 'update', UA>): MRReturn<'AppDefinition', 'update'>
(opts: MROpts<'AppDefinition', 'delete', UA>): MRReturn<'AppDefinition', 'delete'>
(opts: MROpts<'AppDefinition', 'getInstallationsForOrg', UA>): MRReturn<
'AppDefinition',
'getInstallationsForOrg'
>

(opts: MROpts<'AppInstallation', 'get', UA>): MRReturn<'AppInstallation', 'get'>
(opts: MROpts<'AppInstallation', 'getMany', UA>): MRReturn<'AppInstallation', 'getMany'>
Expand Down Expand Up @@ -675,6 +680,10 @@ export type MRActions = {
return: AppDefinitionProps
}
delete: { params: GetAppDefinitionParams; return: any }
getInstallationsForOrg: {
params: GetOrganizationParams & { appDefinitionId: string }
return: AppInstallationsForOrganizationProps
}
}
AppInstallation: {
get: { params: GetAppInstallationParams; return: AppInstallationProps }
Expand Down Expand Up @@ -1514,6 +1523,7 @@ export type GetAppActionParams = GetAppDefinitionParams & { appActionId: string
export type GetAppActionCallParams = GetAppInstallationParams & { appActionId: string }
export type GetAppBundleParams = GetAppDefinitionParams & { appBundleId: string }
export type GetAppDefinitionParams = GetOrganizationParams & { appDefinitionId: string }
export type GetAppInstallationsForOrgParams = GetOrganizationParams & { appDefinitionId: string }
export type GetAppInstallationParams = GetSpaceEnvironmentParams & { appDefinitionId: string }
export type GetBulkActionParams = GetSpaceEnvironmentParams & { bulkActionId: string }
export type GetCommentParams = GetEntryParams & { commentId: string }
Expand Down
39 changes: 36 additions & 3 deletions lib/create-app-definition-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export default function createAppDefinitionApi(makeRequest: MakeRequest) {
*
* client.getOrganization('<org_id>')
* .then((org) => org.getAppDefinition('<app_def_id>'))
* .then((appDefinition) => appDefinition.getAppBundle('<app_upload_id>')
* .then((appDefinition) => appDefinition.getAppBundle('<app_upload_id>'))
* .then((appBundle) => console.log(appBundle))
* .catch(console.error)
* ```
Expand Down Expand Up @@ -118,7 +118,7 @@ export default function createAppDefinitionApi(makeRequest: MakeRequest) {
*
* client.getOrganization('<org_id>')
* .then((org) => org.getAppDefinition('<app_def_id>'))
* .then((appDefinition) => appDefinition.getAppBundles()
* .then((appDefinition) => appDefinition.getAppBundles())
* .then((response) => console.log(response.items))
* .catch(console.error)
* ```
Expand All @@ -143,7 +143,7 @@ export default function createAppDefinitionApi(makeRequest: MakeRequest) {
* })
* client.getOrganization('<org_id>')
* .then((org) => org.getAppDefinition('<app_def_id>'))
* .then((appDefinition) => appDefinition.createAppBundle('<app_upload_id>')
* .then((appDefinition) => appDefinition.createAppBundle('<app_upload_id>'))
* .then((appBundle) => console.log(appBundle))
* .catch(console.error)
* ```
Expand All @@ -160,5 +160,38 @@ export default function createAppDefinitionApi(makeRequest: MakeRequest) {
payload: data,
}).then((data) => wrapAppBundle(makeRequest, data))
},

/**
* Gets a list of App Installations across an org for given App Definition Id
* Can be any organizationId the user has access to, where the App is installed
* @param Object - organizationId and appDefinitionId
* @return Promise for the newly created AppBundle
* @example ```javascript
* const contentful = require('contentful-management')
* const client = contentful.createClient({
* accessToken: '<content_management_api_key>'
* })
* client.getAppDefinition('<organizationId>', '<appDefinitionId>')
* .then((appDefinition) => appDefinition.getInstallationsForOrg(<{organizationId: string, appDefinitionId: string}>))
* .then((appInstallationsForOrg) => console.log(appInstallationsForOrg.items))
* .catch(console.error)
* ```
*/
getInstallationsForOrg({
organizationId,
appDefinitionId,
}: {
organizationId: string
appDefinitionId: string
}) {
return makeRequest({
entityType: 'AppDefinition',
action: 'getInstallationsForOrg',
params: {
appDefinitionId,
organizationId,
},
})
},
}
}
31 changes: 31 additions & 0 deletions lib/create-contentful-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import {
PaginationQueryParams,
QueryOptions,
QueryParams,
GetAppDefinitionParams,
} from './common-types'
import entities from './entities'
import { Organization, OrganizationProp } from './entities/organization'
import { CreatePersonalAccessTokenProps } from './entities/personal-access-token'
import { Space, SpaceProps } from './entities/space'
import { AppDefinition } from './entities/app-definition'
import { UsageQuery } from './entities/usage'
import { UserProps } from './entities/user'

Expand All @@ -26,6 +28,7 @@ export default function createClientApi(makeRequest: MakeRequest) {
entities.personalAccessToken
const { wrapOrganization, wrapOrganizationCollection } = entities.organization
const { wrapUsageCollection } = entities.usage
const { wrapAppDefinition } = entities.appDefinition

return {
/**
Expand Down Expand Up @@ -180,6 +183,34 @@ export default function createClientApi(makeRequest: MakeRequest) {
}).then((data) => wrapUser<T>(makeRequest, data))
},

/**
* Gets App Definition
* @return Promise for App Definition
* @param organizationId - Id of the organization where the app is installed
* @param appDefinitionId - Id of the app that will be returned
* @example ```javascript
* const contentful = require('contentful-management')
*
* const client = contentful.createClient({
* accessToken: '<content_management_api_key>'
* })
*
* client.getAppDefinition(<'org_id'>, <'app_id'>)
* .then(appDefinition => console.log(appDefinition.name))
* .catch(console.error)
* ```
*/

getAppDefinition: function getAppDefinition(
params: GetAppDefinitionParams
): Promise<AppDefinition> {
return makeRequest({
entityType: 'AppDefinition',
action: 'get',
params,
}).then((data) => wrapAppDefinition(makeRequest, data))
},

/**
* Creates a personal access token
* @param data - personal access token config
Expand Down
12 changes: 12 additions & 0 deletions lib/entities/app-definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import createAppDefinitionApi, { ContentfulAppDefinitionAPI } from '../create-ap
import { SetOptional, Except } from 'type-fest'
import { FieldType } from './field-type'
import { ParameterDefinition } from './widget-parameters'
import { AppInstallationProps } from './app-installation'
import { EnvironmentProps } from './environment'

export interface NavigationItem {
name: string
Expand Down Expand Up @@ -78,6 +80,16 @@ export type AppDefinition = ContentfulAppDefinitionAPI &
AppDefinitionProps &
DefaultElements<AppDefinitionProps>

export type AppInstallationsForOrganizationProps = {
sys: {
type: 'Array'
}
items: AppInstallationProps[]
includes: {
Environment: EnvironmentProps[]
}
}

/**
* @private
* @param makeRequest - function to make requests via an adapter
Expand Down
9 changes: 8 additions & 1 deletion lib/plain/common-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,11 @@ import {
GetWorkflowDefinitionParams,
} from '../common-types'
import { ApiKeyProps, CreateApiKeyProps } from '../entities/api-key'
import { AppDefinitionProps, CreateAppDefinitionProps } from '../entities/app-definition'
import {
AppDefinitionProps,
AppInstallationsForOrganizationProps,
CreateAppDefinitionProps,
} from '../entities/app-definition'
import { AppInstallationProps, CreateAppInstallationProps } from '../entities/app-installation'
import {
AssetFileProp,
Expand Down Expand Up @@ -649,6 +653,9 @@ export type PlainClientAPI = {
headers?: AxiosRequestHeaders
): Promise<AppDefinitionProps>
delete(params: OptionalDefaults<GetAppDefinitionParams>): Promise<any>
getInstallationsForOrg(
params: OptionalDefaults<GetAppDefinitionParams>
): Promise<AppInstallationsForOrganizationProps>
}
appInstallation: {
get(params: OptionalDefaults<GetAppInstallationParams>): Promise<AppInstallationProps>
Expand Down
1 change: 1 addition & 0 deletions lib/plain/plain-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ export const createPlainClient = (
create: wrap(wrapParams, 'AppDefinition', 'create'),
update: wrap(wrapParams, 'AppDefinition', 'update'),
delete: wrap(wrapParams, 'AppDefinition', 'delete'),
getInstallationsForOrg: wrap(wrapParams, 'AppDefinition', 'getInstallationsForOrg'),
},
appInstallation: {
get: wrap(wrapParams, 'AppInstallation', 'get'),
Expand Down
25 changes: 25 additions & 0 deletions test/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,31 @@ export async function getSpecialSpace(feature) {
}
}

export async function getAppDefinition(orgId, appId) {
const appDefinition = await initClient().getAppDefinition(orgId, appId)
return appDefinition
}

export async function createAppDefinition() {
const organization = await getTestOrganization()
const appDefinition = await organization.createAppDefinition({
name: 'Test App',
src: 'http://localhost:3000',
locations: [
{
location: 'app-config',
},
],
})
return { orgId: appDefinition.sys.organization.sys.id, appId: appDefinition.sys.id }
}

export async function createAppInstallation(appDefinitionId) {
const space = await getDefaultSpace()
const env = await space.getEnvironment('master')
return await env.createAppInstallation(appDefinitionId, {}, { acceptAllTerms: true })
}

export const createTestSpace = async (client, testSuiteName = '') => {
return testUtils.createTestSpace({
client,
Expand Down
70 changes: 64 additions & 6 deletions test/integration/app-definition-integration.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { expect } from 'chai'
import { before, describe, test, after } from 'mocha'
import { getTestOrganization } from '../helpers'
import {
createAppDefinition,
getAppDefinition,
getTestOrganization,
createAppInstallation,
} from '../helpers'

describe('AppDefinition api', function () {
let organization
Expand Down Expand Up @@ -29,8 +34,6 @@ describe('AppDefinition api', function () {

expect(appDefinition.sys.type).equals('AppDefinition', 'type')
expect(appDefinition.name).equals('Test App', 'name')

await appDefinition.delete()
})

test('getAppDefintion', async () => {
Expand All @@ -47,8 +50,6 @@ describe('AppDefinition api', function () {
const fetchedAppDefinition = await organization.getAppDefinition(appDefinition.sys.id)

expect(appDefinition.sys.id).equals(fetchedAppDefinition.sys.id)

await appDefinition.delete()
})

test('getAppDefinitions', async () => {
Expand Down Expand Up @@ -92,7 +93,64 @@ describe('AppDefinition api', function () {
await appDefinition.update()

expect(appDefinition.name).equals('Test App Updated', 'name')
})

await appDefinition.delete()
test('getAppDefinition (top level)', async () => {
const { orgId, appId } = await createAppDefinition()
const appDefinition = await getAppDefinition({ organizationId: orgId, appDefinitionId: appId })

expect(appDefinition.sys.organization.sys.id).equals(orgId)
expect(appDefinition.sys.id).equals(appId)
})

test('getInstallationsForOrg returns', async () => {
const { orgId, appId } = await createAppDefinition()
const appDefinition = await getAppDefinition({ organizationId: orgId, appDefinitionId: appId })
const installationsForOrg = await appDefinition.getInstallationsForOrg({
organizationId: orgId,
appDefinitionId: appId,
})
expect(installationsForOrg.sys.type).equals('Array')
})

test('getInstallationsForOrg throws if missing org Id', async () => {
const { orgId, appId } = await createAppDefinition()
const appDefinition = await getAppDefinition({ organizationId: orgId, appDefinitionId: appId })

try {
await appDefinition.getInstallationsForOrg({
appDefinitionId: appId,
})
} catch (e) {
const errorObject = JSON.parse(e.message)
expect(errorObject.status).to.equal(400)
}
})

test('getInstallationsForOrg throws if missing app Id', async () => {
const { orgId, appId } = await createAppDefinition()
const appDefinition = await getAppDefinition({ organizationId: orgId, appDefinitionId: appId })

try {
await appDefinition.getInstallationsForOrg({
organizationId: orgId,
})
} catch (e) {
const errorObject = JSON.parse(e.message)
expect(errorObject.status).to.equal(400)
}
})

test('getInstallationsForOrg returns installations', async () => {
const { orgId, appId } = await createAppDefinition()
const appInstallation = await createAppInstallation(appId)
const appDefinition = await getAppDefinition({ organizationId: orgId, appDefinitionId: appId })
const appInstallationsForOrg = await appDefinition.getInstallationsForOrg({
appDefinitionId: appId,
organizationId: orgId,
})

expect(appInstallationsForOrg.items.length).to.equal(1)
await appInstallation.delete()
})
})
Loading

0 comments on commit ea3b20f

Please sign in to comment.