diff --git a/lib/adapters/REST/endpoints/index.ts b/lib/adapters/REST/endpoints/index.ts index 64dda715f..d09a56793 100644 --- a/lib/adapters/REST/endpoints/index.ts +++ b/lib/adapters/REST/endpoints/index.ts @@ -35,6 +35,7 @@ import * as AccessToken from './access-token' import * as PreviewApiKey from './preview-api-key' import * as Release from './release' import * as ReleaseAction from './release-action' +import * as Resource from './resource' import * as ResourceProvider from './resource-provider' import * as ResourceType from './resource-type' import * as Role from './role' @@ -96,6 +97,7 @@ export default { PreviewApiKey, Release, ReleaseAction, + Resource, ResourceProvider, ResourceType, Role, diff --git a/lib/adapters/REST/endpoints/resource-type.ts b/lib/adapters/REST/endpoints/resource-type.ts index c3e998677..ca738f4bf 100644 --- a/lib/adapters/REST/endpoints/resource-type.ts +++ b/lib/adapters/REST/endpoints/resource-type.ts @@ -2,10 +2,19 @@ import type { RawAxiosRequestHeaders } from 'axios' import type { AxiosInstance } from 'contentful-sdk-core' import * as raw from './raw' import copy from 'fast-copy' -import type { CollectionProp } from '../../../common-types' -import { type GetResourceTypeParams } from '../../../common-types' +import type { + BasicCursorPaginationOptions, + CursorPaginatedCollectionProp, + GetResourceTypeParams, + CollectionProp, + GetSpaceEnvironmentParams, +} from '../../../common-types' import type { RestEndpoint } from '../types' -import type { ResourceTypeProps, UpsertResourceTypeProps } from '../../../entities/resource-type' +import type { + ResourceTypeProps, + SpaceEnvResourceTypeProps, + UpsertResourceTypeProps, +} from '../../../entities/resource-type' const getBaseUrl = ( params: GetResourceTypeParams | Omit @@ -15,6 +24,10 @@ const getBaseUrl = ( const getEntityUrl = (params: GetResourceTypeParams) => `${getBaseUrl(params)}/${params.resourceTypeId}` +const getSpaceEnvUrl = ( + params: GetSpaceEnvironmentParams & { query?: BasicCursorPaginationOptions } +) => `/spaces/${params.spaceId}/environments/${params.environmentId}/resource_types` + export const get: RestEndpoint<'ResourceType', 'get'> = ( http: AxiosInstance, params: GetResourceTypeParams @@ -46,3 +59,13 @@ export const getMany: RestEndpoint<'ResourceType', 'getMany'> = ( ) => { return raw.get>(http, getBaseUrl(params)) } + +export const getForEnvironment: RestEndpoint<'ResourceType', 'getForEnvironment'> = ( + http: AxiosInstance, + params: GetSpaceEnvironmentParams & { query?: BasicCursorPaginationOptions } +) => { + return raw.get>( + http, + getSpaceEnvUrl(params) + ) +} diff --git a/lib/adapters/REST/endpoints/resource.ts b/lib/adapters/REST/endpoints/resource.ts new file mode 100644 index 000000000..21fc178ea --- /dev/null +++ b/lib/adapters/REST/endpoints/resource.ts @@ -0,0 +1,16 @@ +import type { CursorPaginatedCollectionProp, GetResourceParams } from '../../../common-types' +import type { RestEndpoint } from '../types' +import type { AxiosInstance } from 'contentful-sdk-core' +import * as raw from './raw' +import type { ResourceProps, ResourceQueryOptions } from '../../../entities/resource' + +const getBaseUrl = (params: GetResourceParams) => + `/spaces/${params.spaceId}/environments/${params.environmentId}/resource_types/${params.resourceTypeId}/resources` + +export const getMany: RestEndpoint<'Resource', 'getMany'> = ( + http: AxiosInstance, + params: GetResourceParams & { query?: ResourceQueryOptions } +) => + raw.get>(http, getBaseUrl(params), { + params: params.query, + }) diff --git a/lib/common-types.ts b/lib/common-types.ts index b2172bfc1..b66f4d402 100644 --- a/lib/common-types.ts +++ b/lib/common-types.ts @@ -165,7 +165,12 @@ import type { ResourceProviderProps, UpsertResourceProviderProps, } from './entities/resource-provider' -import type { ResourceTypeProps, UpsertResourceTypeProps } from './entities/resource-type' +import type { + ResourceTypeProps, + SpaceEnvResourceTypeProps, + UpsertResourceTypeProps, +} from './entities/resource-type' +import type { ResourceProps, ResourceQueryOptions } from './entities/resource' export interface DefaultElements { toPlainObject(): TPlainObject @@ -594,7 +599,7 @@ type MRInternal = { 'ReleaseAction', 'queryForRelease' > - + (opts: MROpts<'Resource', 'getMany', UA>): MRReturn<'Resource', 'getMany'> (opts: MROpts<'ResourceProvider', 'get', UA>): MRReturn<'ResourceProvider', 'get'> (opts: MROpts<'ResourceProvider', 'upsert', UA>): MRReturn<'ResourceProvider', 'upsert'> (opts: MROpts<'ResourceProvider', 'delete', UA>): MRReturn<'ResourceProvider', 'delete'> @@ -602,6 +607,10 @@ type MRInternal = { (opts: MROpts<'ResourceType', 'get', UA>): MRReturn<'ResourceType', 'get'> (opts: MROpts<'ResourceType', 'upsert', UA>): MRReturn<'ResourceType', 'upsert'> (opts: MROpts<'ResourceType', 'delete', UA>): MRReturn<'ResourceType', 'delete'> + (opts: MROpts<'ResourceType', 'getForEnvironment', UA>): MRReturn< + 'ResourceType', + 'getForEnvironment' + > (opts: MROpts<'ResourceType', 'getMany', UA>): MRReturn<'ResourceType', 'getMany'> (opts: MROpts<'Role', 'get', UA>): MRReturn<'Role', 'get'> @@ -776,6 +785,13 @@ export interface Adapter { * @private */ export type MRActions = { + Resource: { + getMany: { + params: GetResourceParams & { query?: ResourceQueryOptions } + headers?: RawAxiosRequestHeaders + return: CursorPaginatedCollectionProp + } + } ResourceProvider: { get: { params: GetResourceProviderParams; return: ResourceProviderProps } upsert: { @@ -799,6 +815,10 @@ export type MRActions = { return: ResourceTypeProps } delete: { params: GetResourceTypeParams; return: any } + getForEnvironment: { + params: GetSpaceEnvironmentParams & { query?: BasicCursorPaginationOptions } + return: CursorPaginatedCollectionProp + } } Http: { get: { params: { url: string; config?: RawAxiosRequestConfig }; return: any } @@ -2118,6 +2138,8 @@ export type GetResourceProviderParams = GetOrganizationParams & { appDefinitionI export type GetResourceTypeParams = GetResourceProviderParams & { resourceTypeId: string } +export type GetResourceParams = GetSpaceEnvironmentParams & { resourceTypeId: string } + export type QueryParams = { query?: QueryOptions } export type SpaceQueryParams = { query?: SpaceQueryOptions } export type PaginationQueryParams = { query?: PaginationQueryOptions } diff --git a/lib/create-environment-api.ts b/lib/create-environment-api.ts index 712fec58d..5a5ed930f 100644 --- a/lib/create-environment-api.ts +++ b/lib/create-environment-api.ts @@ -47,6 +47,7 @@ import { wrapUIConfig } from './entities/ui-config' import { wrapUserUIConfig } from './entities/user-ui-config' import { wrapEnvironmentTemplateInstallationCollection } from './entities/environment-template-installation' import type { CreateAppAccessTokenProps } from './entities/app-access-token' +import type { ResourceQueryOptions } from './entities/resource' /** * @private @@ -75,6 +76,8 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) { const { wrapAppActionCall } = entities.appActionCall const { wrapBulkAction } = entities.bulkAction const { wrapAppAccessToken } = entities.appAccessToken + const { wrapResourceTypesForEnvironmentCollection } = entities.resourceType + const { wrapResourceCollection } = entities.resource return { /** @@ -2280,5 +2283,81 @@ export default function createEnvironmentApi(makeRequest: MakeRequest) { }, }).then((data) => wrapEnvironmentTemplateInstallationCollection(makeRequest, data)) }, + + /** + * Gets a collection of all resource types based on native external references app installations in the environment + * @param query - BasicCursorPaginationOptions + * @return Promise for a collection of ResourceTypes + * ```javascript + * const contentful = require('contentful-management') + * + * const client = contentful.createClient({ + * accessToken: '' + * }) + * + * client.getSpace('') + * .then((space) => space.getEnvironment('')) + * .then((environment) => environment.getResourceTypes({limit: 10})) + * .then((installations) => console.log(installations.items)) + * .catch(console.error) + * ``` + */ + async getResourceTypes(query?: BasicCursorPaginationOptions) { + const raw: EnvironmentProps = this.toPlainObject() + + return makeRequest({ + entityType: 'ResourceType', + action: 'getForEnvironment', + params: { + query, + spaceId: raw.sys.space.sys.id, + environmentId: raw.sys.id, + }, + }).then((data) => wrapResourceTypesForEnvironmentCollection(makeRequest, data)) + }, + + /** + * Gets a collection of all resources for a given resource type based on native external references app installations in the environment + * @param resourceTypeId - Id of the resourceType to get its resources + * @param query - Either LookupQuery options with 'sys.urn[in]' param or a Search query with 'query' param, in both cases you can add pagination options + * @return Promise for a collection of Resources for a given resourceTypeId + * ```javascript + * const contentful = require('contentful-management') + * + * const client = contentful.createClient({ + * accessToken: '' + * }) + * + * // Search Query + * client.getSpace('') + * .then((space) => space.getEnvironment('')) + * // is a string you want to search for in the external resources + * .then((environment) => environment.getResourcesForResourceType('', {query: '', limit: 10})) + * .then((installations) => console.log(installations.items)) + * .catch(console.error) + * + * // Lookup query + * + * client.getSpace('') + * .then((space) => space.getEnvironment('')) + * .then((environment) => environment.getResourcesForResourceType('', {'sys.urn[in]': ',', limit: 10})) + * .then((installations) => console.log(installations.items)) + * .catch(console.error) + * ``` + */ + async getResourcesForResourceType(resourceTypeId: string, query?: ResourceQueryOptions) { + const raw: EnvironmentProps = this.toPlainObject() + + return makeRequest({ + entityType: 'Resource', + action: 'getMany', + params: { + query, + spaceId: raw.sys.space.sys.id, + environmentId: raw.sys.id, + resourceTypeId, + }, + }).then((data) => wrapResourceCollection(makeRequest, data)) + }, } } diff --git a/lib/entities/index.ts b/lib/entities/index.ts index 5c49e61de..3817f57ee 100644 --- a/lib/entities/index.ts +++ b/lib/entities/index.ts @@ -54,6 +54,7 @@ import * as concept from './concept' import * as conceptScheme from './concept-scheme' import * as resourceProvider from './resource-provider' import * as resourceType from './resource-type' +import * as resource from './resource' export default { accessToken, @@ -94,6 +95,7 @@ export default { releaseAction, resourceProvider, resourceType, + resource, role, scheduledAction, snapshot, diff --git a/lib/entities/resource-provider.ts b/lib/entities/resource-provider.ts index 8c6894c2d..32a9776e5 100644 --- a/lib/entities/resource-provider.ts +++ b/lib/entities/resource-provider.ts @@ -126,6 +126,7 @@ function createResourceProviderApi(makeRequest: MakeRequest) { appDefinitionId: this.sys.appDefinition.sys.id, resourceTypeId: id, }, + headers: {}, payload: data, }).then((data) => wrapResourceType(makeRequest, data)) }, diff --git a/lib/entities/resource-type.ts b/lib/entities/resource-type.ts index cb1ed9928..ad5a3bfec 100644 --- a/lib/entities/resource-type.ts +++ b/lib/entities/resource-type.ts @@ -1,5 +1,6 @@ import type { BasicMetaSysProps, + CursorPaginatedCollectionProp, DefaultElements, GetResourceTypeParams, MakeRequest, @@ -8,6 +9,7 @@ import type { import { toPlainObject, freezeSys } from 'contentful-sdk-core' import copy from 'fast-copy' import enhanceWithMethods from '../enhance-with-methods' +import { wrapCursorPaginatedCollection } from '../common-utils' export type ResourceTypeProps = { /** @@ -15,8 +17,8 @@ export type ResourceTypeProps = { */ sys: Omit & { appDefinition: SysLink - organization: SysLink resourceProvider: SysLink + organization: SysLink } /** * Resource Type name @@ -40,6 +42,24 @@ export type ResourceTypeProps = { } } } +const publicResourceTypeFields = ['name'] as const + +type OptionalSysFields = + | 'createdAt' + | 'createdBy' + | 'updatedAt' + | 'updatedBy' + | 'appDefinition' + | 'organization' + +export type SpaceEnvResourceTypeProps = Pick< + ResourceTypeProps, + typeof publicResourceTypeFields[number] +> & { + // we mark timestamps and users as optional to include system types like `Contentful:Entry` into the public response + sys: Partial> & + Omit +} export type UpsertResourceTypeProps = Omit @@ -138,3 +158,18 @@ export function wrapResourceType(makeRequest: MakeRequest, data: ResourceTypePro ) return freezeSys(ResourceTypeWithMethods) } + +export function wrapResourceTypeforEnvironment( + makeRequest: MakeRequest, + data: SpaceEnvResourceTypeProps +): SpaceEnvResourceTypeProps { + const resourceType = toPlainObject(data) + return freezeSys(resourceType) +} + +export const wrapResourceTypesForEnvironmentCollection: ( + makeRequest: MakeRequest, + data: CursorPaginatedCollectionProp +) => CursorPaginatedCollectionProp = wrapCursorPaginatedCollection( + wrapResourceTypeforEnvironment +) diff --git a/lib/entities/resource.ts b/lib/entities/resource.ts new file mode 100644 index 000000000..b708d38c6 --- /dev/null +++ b/lib/entities/resource.ts @@ -0,0 +1,50 @@ +import type { + BasicCursorPaginationOptions, + CursorPaginatedCollectionProp, + MakeRequest, + SysLink, +} from '../common-types' +import { wrapCursorPaginatedCollection } from '../common-utils' +import { freezeSys, toPlainObject } from 'contentful-sdk-core' + +export type ResourceQueryOptions = LookupQueryOptions | SearchQueryOptions + +type LookupQueryOptions = { + 'sys.urn[in]': string +} & BasicCursorPaginationOptions + +type SearchQueryOptions = { + query: string +} & BasicCursorPaginationOptions + +export type ResourceProps = { + sys: { + type: 'Resource' + urn: string + resourceType: SysLink + resourceProvider: SysLink + appDefinition: SysLink + } + fields: { + title: string + subtitle?: string + description?: string + externalUrl?: string + image?: { + url: string + altText?: string + } + badge?: { + label: string + variant: 'primary' | 'negative' | 'positive' | 'warning' | 'secondary' + } + } +} +export function wrapResource(makeRequest: MakeRequest, data: ResourceProps) { + const resource = toPlainObject(data) + return freezeSys(resource) +} +export const wrapResourceCollection: ( + makeRequest: MakeRequest, + data: CursorPaginatedCollectionProp +) => CursorPaginatedCollectionProp = wrapCursorPaginatedCollection(wrapResource) diff --git a/lib/export-types.ts b/lib/export-types.ts index 4ed69f003..65beaceb0 100644 --- a/lib/export-types.ts +++ b/lib/export-types.ts @@ -290,5 +290,7 @@ export type { export type { ResourceType, ResourceTypeProps, + SpaceEnvResourceTypeProps, UpsertResourceTypeProps, } from './entities/resource-type' +export type { ResourceProps, ResourceQueryOptions } from './entities/resource' diff --git a/lib/plain/common-types.ts b/lib/plain/common-types.ts index 7214151da..7e6c2c596 100644 --- a/lib/plain/common-types.ts +++ b/lib/plain/common-types.ts @@ -115,6 +115,7 @@ import type { ConceptPlainClientAPI } from './entities/concept' import type { ConceptSchemePlainClientAPI } from './entities/concept-scheme' import type { ResourceProviderPlainClientAPI } from './entities/resource-provider' import type { ResourceTypePlainClientAPI } from './entities/resource-type' +import type { ResourcePlainAPI } from './entities/resource' export type PlainClientAPI = { raw: { @@ -450,6 +451,9 @@ export type PlainClientAPI = { params: OptionalDefaults & { query?: ReleaseActionQueryOptions } ): Promise> } + resource: ResourcePlainAPI + resourceProvider: ResourceProviderPlainClientAPI + resourceType: ResourceTypePlainClientAPI role: RolePlainClientAPI scheduledActions: { get( @@ -505,8 +509,6 @@ export type PlainClientAPI = { } appDefinition: AppDefinitionPlainClientAPI appInstallation: AppInstallationPlainClientAPI - resourceProvider: ResourceProviderPlainClientAPI - resourceType: ResourceTypePlainClientAPI extension: ExtensionPlainClientAPI webhook: WebhookPlainClientAPI snapshot: { diff --git a/lib/plain/entities/resource-type.ts b/lib/plain/entities/resource-type.ts index ca70df782..c722b62d9 100644 --- a/lib/plain/entities/resource-type.ts +++ b/lib/plain/entities/resource-type.ts @@ -1,8 +1,15 @@ import type { RawAxiosRequestHeaders } from 'axios' import type { OptionalDefaults } from '../wrappers/wrap' -import type { CollectionProp, GetResourceTypeParams } from '../../common-types' +import type { + BasicCursorPaginationOptions, + CollectionProp, + CursorPaginatedCollectionProp, + GetResourceTypeParams, + GetSpaceEnvironmentParams, +} from '../../common-types' import type { ResourceTypeProps, UpsertResourceTypeProps } from '../../export-types' +import type { SpaceEnvResourceTypeProps } from '../../entities/resource-type' export type ResourceTypePlainClientAPI = { /* @@ -66,6 +73,24 @@ export type ResourceTypePlainClientAPI = { */ delete(params: OptionalDefaults): Promise + /* + * Fetch all Resource Types for an environment + * @param params entity IDs to identify the Resource Type + * @params Optional query params for cursor pagination + * @returns all Resource Types based on the last NER app installed in the environment + * @throws if the request fails, or no Resource Type is found + * @example + * ```javascript + * const resourceTypes = await client.resourceType.getForEnvironment({ + * spaceId: '', + * environmentId: '', + * }); + * ``` + */ + getForEnvironment( + params: OptionalDefaults & { query?: BasicCursorPaginationOptions } + ): Promise> + /* * Fetch all Resource Types * @returns all Resource Types diff --git a/lib/plain/entities/resource.ts b/lib/plain/entities/resource.ts new file mode 100644 index 000000000..756aa80da --- /dev/null +++ b/lib/plain/entities/resource.ts @@ -0,0 +1,39 @@ +import type { OptionalDefaults } from '../wrappers/wrap' +import type { CursorPaginatedCollectionProp, GetResourceParams } from '../../common-types' +import type { ResourceProps, ResourceQueryOptions } from '../../entities/resource' + +export type ResourcePlainAPI = { + /** + * Fetch Resources + * @param params entity IDs to identify the Resources + * @returns the App Definition config + * @throws if the request fails, or the Resource Type is not found + * @example + * ```javascript + * // Lookup example + * const resourceProvider = await client.resource.getMany({ + * spaceId: '', + * environmentId: '', + * resourceTypeId: '', + * query: { + * 'sys.urn[in]': ',', + * limit': , + * } + * }); + * + * // Search example + * const resourceProvider = await client.resource.getMany({ + * spaceId: '', + * environmentId: '', + * resourceTypeId: '', + * query: { + * 'query': 'text', + * 'limit': , + * } + * }); + * ``` + */ + getMany( + params: OptionalDefaults & { query?: ResourceQueryOptions } + ): Promise> +} diff --git a/lib/plain/plain-client.ts b/lib/plain/plain-client.ts index 7acb31141..d57cf26ee 100644 --- a/lib/plain/plain-client.ts +++ b/lib/plain/plain-client.ts @@ -373,6 +373,9 @@ export const createPlainClient = ( upsert: wrap(wrapParams, 'AppInstallation', 'upsert'), delete: wrap(wrapParams, 'AppInstallation', 'delete'), }, + resource: { + getMany: wrap(wrapParams, 'Resource', 'getMany'), + }, resourceProvider: { get: wrap(wrapParams, 'ResourceProvider', 'get'), upsert: wrap(wrapParams, 'ResourceProvider', 'upsert'), @@ -383,6 +386,7 @@ export const createPlainClient = ( getMany: wrap(wrapParams, 'ResourceType', 'getMany'), upsert: wrap(wrapParams, 'ResourceType', 'upsert'), delete: wrap(wrapParams, 'ResourceType', 'delete'), + getForEnvironment: wrap(wrapParams, 'ResourceType', 'getForEnvironment'), }, extension: { get: wrap(wrapParams, 'Extension', 'get'), diff --git a/test/integration/environment-integration.js b/test/integration/environment-integration.js index 8f1185069..eaba803f7 100644 --- a/test/integration/environment-integration.js +++ b/test/integration/environment-integration.js @@ -4,8 +4,11 @@ import { createTestSpace, generateRandomId, waitForEnvironmentToBeReady, + getTestOrganization, + getDefaultSpace, } from '../helpers' import { expect } from 'chai' +import { readFileSync } from 'fs' describe('Environment Api', function () { let space @@ -41,4 +44,111 @@ describe('Environment Api', function () { expect(environment.name).equals(envName, 'env was created with correct name') expect(environment.sys.id).equals(envId, 'env was created with correct id') }) + + describe('With a NER app installed in the environment', async () => { + const functionManifest = { + id: 'tmdbMockFunction', + name: 'Mocked TMDB lookup function', + description: 'This is a mocked example to help test Apps with ERL.', + path: 'functions/index.js', + allowNetworks: ['api.themoviedb.org'], + accepts: ['resources.lookup', 'resources.search'], + } + + let organization + let appDefinition + let appUpload + let appBundle + let appInstallation + let resourceProvider + let resourceType + let env + + before(async () => { + organization = await getTestOrganization() + appDefinition = await organization.createAppDefinition({ + name: 'Test', + src: 'http://localhost:2222', + locations: [{ location: 'app-config' }], + parameters: { + installation: [ + { + id: 'tmdbAccessToken', + name: 'TMDB Read token', + type: 'Symbol', + required: true, + default: 'Test', + }, + ], + }, + }) + + appUpload = await organization.createAppUpload( + readFileSync(`${__dirname}/fixtures/build.zip`) + ) + appBundle = await appDefinition.createAppBundle({ + appUploadId: appUpload.sys.id, + comment: 'Testing ResourceTypeCreation', + functions: [functionManifest], + }) + + appDefinition.bundle = { sys: { id: appBundle.sys.id, type: 'Link', linkType: 'AppBundle' } } + appDefinition.src = undefined + + appDefinition = await appDefinition.update() + + resourceProvider = await appDefinition.upsertResourceProvider({ + sys: { id: 'TMDB' }, + type: 'function', + function: { sys: { id: functionManifest.id, type: 'Link', linkType: 'Function' } }, + }) + + resourceType = await resourceProvider.upsertResourceType('TMDB:Movie', { + name: 'Movie', + defaultFieldMapping: { + title: 'title', + }, + }) + + const space = await getDefaultSpace() + env = await space.getEnvironment('master') + appInstallation = await env.createAppInstallation( + appDefinition.sys.id, + { + parameters: { + tmdbAccessToken: 'test', + }, + }, + { acceptAllTerms: true } + ) + }) + + after(async () => { + if (appInstallation) { + await appInstallation.delete() + } + if (resourceType) { + await resourceType.delete() + } + if (resourceProvider) { + await resourceProvider.delete() + } + + if (appUpload) { + await appUpload.delete() + } + + if (appDefinition) { + await appDefinition.delete() + } + }) + + test('gets all resource types for that environment', async () => { + const resourceTypes = await env.getResourceTypes() + + expect(resourceTypes.items.length).equal(2) + expect(resourceTypes.items[0].sys.id).equal('Contentful:Entry') + expect(resourceTypes.items[1].sys.id).equal('TMDB:Movie') + }) + }) }) diff --git a/test/integration/fixtures/build.zip b/test/integration/fixtures/build.zip index daffa9133..d39d09290 100644 Binary files a/test/integration/fixtures/build.zip and b/test/integration/fixtures/build.zip differ diff --git a/test/integration/resource-integration.ts b/test/integration/resource-integration.ts new file mode 100644 index 000000000..36a0ea6aa --- /dev/null +++ b/test/integration/resource-integration.ts @@ -0,0 +1,164 @@ +import { test } from 'mocha' +import { readFileSync } from 'fs' +import { expect } from 'chai' +import { getTestOrganization, initPlainClient, getDefaultSpace } from '../helpers' +import type { Organization } from '../../lib/entities/organization' +import type { AppDefinition } from '../../lib/entities/app-definition' +import type { AppUpload } from '../../lib/entities/app-upload' +import type { AppBundle } from '../../lib/entities/app-bundle' +import type { + AppInstallation, + Environment, + ResourceProvider, + ResourceType, +} from '../../lib/export-types' +import { TestDefaults } from '../defaults' + +describe('Resource API', () => { + const resourceTypeId = 'TMDB:Movie' + const functionManifest = { + id: 'tmdbMockFunction', + name: 'Mocked TMDB lookup function', + description: 'This is a mocked example to help test Apps with ERL.', + path: 'functions/index.js', + allowNetworks: ['api.themoviedb.org'], + accepts: ['resources.lookup', 'resources.search'], + } + + let organization: Organization + let appDefinition: AppDefinition + let appUpload: AppUpload + let appBundle: AppBundle + let appInstallation: AppInstallation + let resourceProvider: ResourceProvider + let resourceType: ResourceType + let env: Environment + + before(async () => { + organization = (await getTestOrganization()) as Organization + appDefinition = await organization.createAppDefinition({ + name: 'Test', + src: 'http://localhost:2222', + locations: [{ location: 'app-config' }], + parameters: { + installation: [ + { + id: 'tmdbAccessToken', + name: 'TMDB Read token', + type: 'Symbol', + required: true, + default: 'Test', + }, + ], + }, + }) + + appUpload = await organization.createAppUpload(readFileSync(`${__dirname}/fixtures/build.zip`)) + appBundle = await appDefinition.createAppBundle({ + appUploadId: appUpload.sys.id, + comment: 'Testing ResourceTypeCreation', + functions: [functionManifest], + }) + + appDefinition.bundle = { sys: { id: appBundle.sys.id, type: 'Link', linkType: 'AppBundle' } } + appDefinition.src = undefined + + appDefinition = await appDefinition.update() + + resourceProvider = await appDefinition.upsertResourceProvider({ + sys: { id: 'TMDB' }, + type: 'function', + function: { sys: { id: functionManifest.id, type: 'Link', linkType: 'Function' } }, + }) + + resourceType = await resourceProvider.upsertResourceType(resourceTypeId, { + name: 'Movie', + defaultFieldMapping: { + title: 'title', + }, + }) + + const space = await getDefaultSpace() + env = await space.getEnvironment('master') + appInstallation = await env.createAppInstallation( + appDefinition.sys.id, + { + parameters: { + tmdbAccessToken: 'test', + }, + }, + { acceptAllTerms: true } + ) + }) + beforeEach(async () => { + await new Promise((resolve) => setTimeout(resolve, 2000)) + }) + + after(async () => { + if (appInstallation) { + await appInstallation.delete() + } + if (resourceType) { + await resourceType.delete() + } + if (resourceProvider) { + await resourceProvider.delete() + } + + if (appUpload) { + await appUpload.delete() + } + + if (appDefinition) { + await appDefinition.delete() + } + }) + + test('get Resources with search params', async () => { + const resources = await env.getResourcesForResourceType(resourceTypeId, { + query: '', + limit: 1, + }) + + expect(resources.items.length).to.equal(10) + }) + + test('get Resources with lookup params', async () => { + const resources = await env.getResourcesForResourceType(resourceTypeId, { + 'sys.urn[in]': '1022789,945961', + }) + + expect(resources.items.length).to.equal(2) + }) + + describe('PlainClient', async () => { + const plainClient = initPlainClient() + + test('get Resources with search params', async () => { + const resources = await plainClient.resource.getMany({ + spaceId: TestDefaults.spaceId, + environmentId: 'master', + resourceTypeId, + query: { + query: 'in', + limit: 1, + }, + }) + + expect(resources.items.length).to.equal(1) + }) + + test('get Resources with lookup params', async () => { + const resources = await plainClient.resource.getMany({ + spaceId: TestDefaults.spaceId, + environmentId: 'master', + resourceTypeId, + query: { + 'sys.urn[in]': '1022789,945961', + }, + }) + + expect(resources.items.length).to.equal(2) + }) + }) +}) diff --git a/test/integration/resource-type-integration.ts b/test/integration/resource-type-integration.ts index a6ce3b7b8..673862d2d 100644 --- a/test/integration/resource-type-integration.ts +++ b/test/integration/resource-type-integration.ts @@ -6,7 +6,13 @@ import type { Organization } from '../../lib/entities/organization' import type { AppDefinition } from '../../lib/entities/app-definition' import type { AppUpload } from '../../lib/entities/app-upload' import type { AppBundle } from '../../lib/entities/app-bundle' -import type { ResourceProvider, ResourceType, ResourceTypeProps } from '../../lib/export-types' +import type { + AppInstallationProps, + ResourceProvider, + ResourceType, + ResourceTypeProps, +} from '../../lib/export-types' +import { TestDefaults } from '../defaults' describe('ResourceType API', () => { const functionManifest = { @@ -26,6 +32,7 @@ describe('ResourceType API', () => { let resourceProvider: ResourceProvider let resourceType: ResourceType | null let resourceTypePlain: ResourceTypeProps | null + let appInstallationPlain: AppInstallationProps | null before(async () => { organization = (await getTestOrganization()) as Organization @@ -63,9 +70,18 @@ describe('ResourceType API', () => { beforeEach(() => { resourceType = null resourceTypePlain = null + appInstallationPlain = null }) afterEach(async () => { + if (appInstallationPlain) { + const plainClient = initPlainClient() + await plainClient.appInstallation.delete({ + appDefinitionId: appDefinition.sys.id, + environmentId: TestDefaults.environmentId, + spaceId: TestDefaults.spaceId, + }) + } if (resourceType) { resourceType.delete() } @@ -293,6 +309,37 @@ describe('ResourceType API', () => { expect(resourceTypePlain.name).to.equal('resourceType') }) + test('getForEnvironment ResourceType', async () => { + resourceTypePlain = await plainClient.resourceType.upsert( + { + organizationId: organization.sys.id, + appDefinitionId: appDefinition.sys.id, + resourceTypeId: 'resourceProvider:resourceTypeId', + }, + { + name: 'resourceType', + defaultFieldMapping: { + title: 'title', + }, + } + ) + const { spaceId, environmentId } = TestDefaults + appInstallationPlain = await plainClient.appInstallation.upsert( + { spaceId, environmentId, appDefinitionId: appDefinition.sys.id }, + { parameters: { tmdbAccessToken: 'testing' } }, + { acceptAllTerms: true } + ) + + const resourceTypesPlain = await plainClient.resourceType.getForEnvironment({ + spaceId, + environmentId, + }) + + expect(resourceTypesPlain.items.length).to.equal(2) + expect(resourceTypesPlain.items[0].sys.id).to.equal('Contentful:Entry') + expect(resourceTypesPlain.items[1].sys.id).to.equal('resourceProvider:resourceTypeId') + }) + test('getMany returns empty array if no Resource Types are present', async () => { const response = await plainClient.resourceType.getMany({ organizationId: organization.sys.id, diff --git a/test/unit/mocks/entities.js b/test/unit/mocks/entities.js index 066eadd3e..58e23ab89 100644 --- a/test/unit/mocks/entities.js +++ b/test/unit/mocks/entities.js @@ -1075,6 +1075,37 @@ const resourceTypeMock = { }, } +const resourceMock = { + sys: { + type: 'Resource', + urn: 'resource-urn', + resourceType: { + sys: { + type: 'Link', + linkType: 'ResourceType', + id: 'resourceTypeId', + }, + }, + resourceProvider: { + sys: { + type: 'Link', + linkType: 'ResourceProvider', + id: 'resourceProvider-id', + }, + }, + appDefinition: { + sys: { + type: 'Link', + linkType: 'AppDefinition', + id: 'appDefinitionId', + }, + }, + }, + fields: { + title: 'Resource title', + }, +} + const mocks = { apiKey: apiKeyMock, appAction: appActionMock, @@ -1124,6 +1155,7 @@ const mocks = { releaseAction: releaseActionMock, releaseActionValidate: releaseActionValidateMock, releaseActionUnpublish: releaseActionUnpublishMock, + resource: resourceMock, resourceProvider: resourceProviderMock, resourceType: resourceTypeMock, scheduledAction: scheduledActionMock, @@ -1284,6 +1316,9 @@ function setupEntitiesMock(rewiredModuleApi) { resourceType: { wrapResourceType: sinon.stub(), }, + resource: { + wrapResourceCollection: sinon.stub(), + }, apiKey: { wrapApiKey: sinon.stub(), wrapApiKeyCollection: sinon.stub(), @@ -1432,4 +1467,5 @@ export { taskMock, resourceProviderMock, resourceTypeMock, + resourceMock, } diff --git a/test/unit/plain/resource-test.ts b/test/unit/plain/resource-test.ts new file mode 100644 index 000000000..86e117d91 --- /dev/null +++ b/test/unit/plain/resource-test.ts @@ -0,0 +1,36 @@ +import { expect } from 'chai' +import { describe, test } from 'mocha' +import sinon from 'sinon' +import { createClient } from '../../../lib/contentful-management' +import setupRestAdapter from '../adapters/REST/helpers/setupRestAdapter' +import { resourceMock } from '../mocks/entities' + +describe('Resource', () => { + const spaceId = 'spaceId' + const environmentId = 'envId' + const resourceTypeId = 'resourceTypeId' + + test('getForEnvironment', async () => { + const { httpMock, adapterMock } = setupRestAdapter( + Promise.resolve({ data: { items: [resourceMock] } }) + ) + const plainClient = createClient({ apiAdapter: adapterMock }, { type: 'plain' }) + const response = await plainClient.resource.getMany({ + spaceId, + environmentId, + resourceTypeId, + query: { + query: '', + limit: 1, + }, + }) + + expect(response).to.be.an('object') + expect(response.items[0].sys.urn).to.equal('resource-urn') + + sinon.assert.calledWith( + httpMock.get, + `/spaces/spaceId/environments/envId/resource_types/${resourceTypeId}/resources` + ) + }) +}) diff --git a/test/unit/plain/resource-type-test.ts b/test/unit/plain/resource-type-test.ts index d7c1a693e..605cba6d3 100644 --- a/test/unit/plain/resource-type-test.ts +++ b/test/unit/plain/resource-type-test.ts @@ -47,6 +47,22 @@ describe('ResourceType', () => { ) }) + test('getForEnvironment', async () => { + const { httpMock, adapterMock } = setupRestAdapter( + Promise.resolve({ data: { items: [resourceTypeMock] } }) + ) + const plainClient = createClient({ apiAdapter: adapterMock }, { type: 'plain' }) + const response = await plainClient.resourceType.getForEnvironment({ + spaceId: 'spaceId', + environmentId: 'envId', + }) + + expect(response).to.be.an('object') + expect(response.items[0].sys.id).to.equal('id') + + sinon.assert.calledWith(httpMock.get, `/spaces/spaceId/environments/envId/resource_types`) + }) + test('upsert', async () => { const { httpMock, adapterMock } = setupRestAdapter(Promise.resolve({ data: resourceTypeMock })) const plainClient = createClient({ apiAdapter: adapterMock }, { type: 'plain' })