diff --git a/packages/auth-manager/openapi3.yaml b/packages/auth-manager/openapi3.yaml index b8b20f84..fe319134 100644 --- a/packages/auth-manager/openapi3.yaml +++ b/packages/auth-manager/openapi3.yaml @@ -282,6 +282,10 @@ paths: - connection parameters: - $ref: '#/components/parameters/environmentQueryParam' + - in: query + name: latest + schema: + type: boolean - in: query name: isEnabled schema: @@ -534,6 +538,11 @@ paths: name: isTemplate schema: type: boolean + - in: query + name: latest + schema: + type: boolean + default: false responses: '200': description: OK diff --git a/packages/auth-manager/src/asset/DAL/assetRepository.ts b/packages/auth-manager/src/asset/DAL/assetRepository.ts index cce4e7c9..031c31cf 100644 --- a/packages/auth-manager/src/asset/DAL/assetRepository.ts +++ b/packages/auth-manager/src/asset/DAL/assetRepository.ts @@ -1,16 +1,53 @@ import { Asset } from '@map-colonies/auth-core'; import { FactoryFunction } from 'tsyringe'; -import { DataSource, Repository } from 'typeorm'; +import { ArrayContains, DataSource, Repository } from 'typeorm'; +import { Criteria } from '../models/assetManager'; export type AssetRepository = Repository & { getMaxVersionWithLock: (name: string) => Promise; getMaxVersion: (name: string) => Promise; + findAllBy: (criteria: Criteria) => Promise; }; export const assetRepositoryFactory: FactoryFunction = (container) => { const dataSource = container.resolve(DataSource); return dataSource.getRepository(Asset).extend({ + // New method to find the latest assets based on dynamic criteria + async findAllBy(criteria): Promise { + // Start building the main query, filtering by the initial criteria + const qb = this.createQueryBuilder(); + + if (criteria.isTemplate !== undefined) { + qb.andWhere('isTemplate = :isTemplate', { + isTemplate: criteria.isTemplate, + }); + } + + if (criteria.type !== undefined) { + qb.andWhere('type = :type', { + type: criteria.type, + }); + } + + if (criteria.environment !== undefined) { + qb.andWhere({ + environment: ArrayContains(criteria.environment), + }); + } + + if (criteria.latest ?? false) { + qb.distinctOn(['name']).orderBy({ + name: 'ASC', + version: 'DESC', + }); + } + + const result = await qb.getMany(); + + return result; + }, + async getMaxVersionWithLock(name: string): Promise { const result = await this.createQueryBuilder() .select('version') diff --git a/packages/auth-manager/src/asset/models/assetManager.ts b/packages/auth-manager/src/asset/models/assetManager.ts index c39fe32e..15e98ab1 100644 --- a/packages/auth-manager/src/asset/models/assetManager.ts +++ b/packages/auth-manager/src/asset/models/assetManager.ts @@ -1,7 +1,6 @@ import { type Logger } from '@map-colonies/js-logger'; import { IAsset } from '@map-colonies/auth-core'; import { inject, injectable } from 'tsyringe'; -import { ArrayContains } from 'typeorm'; import type { SetRequired } from 'type-fest'; import { operations } from '@openapi'; import { SERVICES } from '@common/constants'; @@ -10,6 +9,7 @@ import { AssetVersionMismatchError, AssetNotFoundError } from './errors'; export type ResponseAsset = SetRequired; export type RequestAsset = Omit; +export type Criteria = NonNullable; @injectable() export class AssetManager { @@ -18,11 +18,11 @@ export class AssetManager { @inject(SERVICES.ASSET_REPOSITORY) private readonly assetRepository: AssetRepository ) {} - public async getAssets(searchParams: NonNullable): Promise { + public async getAssets(searchParams: Criteria): Promise { this.logger.info({ msg: 'fetching assets', searchParams }); - const { environment, isTemplate, type } = searchParams; + const criteria = searchParams; - return this.assetRepository.findBy({ environment: environment ? ArrayContains(environment) : undefined, isTemplate, type }); + return this.assetRepository.findAllBy(criteria); } public async getNamedAssets(name: string): Promise { diff --git a/packages/auth-manager/src/common/config.ts b/packages/auth-manager/src/common/config.ts index 79e07e46..e348daa1 100644 --- a/packages/auth-manager/src/common/config.ts +++ b/packages/auth-manager/src/common/config.ts @@ -13,6 +13,7 @@ let configInstance: ConfigType | undefined; async function initConfig(): Promise { configInstance = await config({ schema: infraOpalaManagerV1, + offlineMode: true, }); } diff --git a/packages/auth-manager/src/openapi.d.ts b/packages/auth-manager/src/openapi.d.ts index 02528b16..075d23e0 100644 --- a/packages/auth-manager/src/openapi.d.ts +++ b/packages/auth-manager/src/openapi.d.ts @@ -781,6 +781,7 @@ export interface operations { parameters: { query?: { environment?: components['parameters']['environmentQueryParam']; + latest?: boolean; isEnabled?: boolean; isNoBrowser?: boolean; isNoOrigin?: boolean; @@ -1011,6 +1012,7 @@ export interface operations { environment?: components['parameters']['environmentQueryParam']; type?: components['schemas']['assetType']; isTemplate?: boolean; + latest?: boolean; }; header?: never; path?: never; diff --git a/packages/auth-manager/tests/integration/asset/asset.spec.ts b/packages/auth-manager/tests/integration/asset/asset.spec.ts index ac2c008b..e09f9983 100644 --- a/packages/auth-manager/tests/integration/asset/asset.spec.ts +++ b/packages/auth-manager/tests/integration/asset/asset.spec.ts @@ -93,6 +93,22 @@ describe('client', function () { expect(res).toSatisfyApiSpec(); expect(res.body).toSatisfyAll((a: IAsset) => a.type === AssetType.DATA); }); + + it('should return 200 status code and all the latest assets', async function () { + const asset = getFakeAsset(); + const assets: IAsset[] = [asset, { ...asset, version: 2 }]; + + const connection = depContainer.resolve(DataSource); + await connection.getRepository(Asset).save(assets); + + const res = await requestSender.getAssets({ queryParams: { latest: true } }); + + expect(res).toHaveProperty('status', httpStatusCodes.OK); + expect(res).toSatisfyApiSpec(); + const returnedAssetsWithName = (res.body as unknown as IAsset[]).filter((a: IAsset) => a.name === asset.name); + expect(returnedAssetsWithName).toHaveLength(1); + expect(returnedAssetsWithName[0]?.version).toBe(2); + }); }); describe('POST /asset', function () { @@ -263,7 +279,7 @@ describe('client', function () { describe('GET /asset', function () { it('should return 500 status code if db throws an error', async function () { const repo = depContainer.resolve(SERVICES.ASSET_REPOSITORY); - jest.spyOn(repo, 'findBy').mockRejectedValue(new Error()); + jest.spyOn(repo, 'findAllBy').mockRejectedValue(new Error()); const res = await requestSender.getAssets(); diff --git a/packages/auth-manager/tests/unit/asset/models/assetManager.spec.ts b/packages/auth-manager/tests/unit/asset/models/assetManager.spec.ts index 92999de9..cf6a37d7 100644 --- a/packages/auth-manager/tests/unit/asset/models/assetManager.spec.ts +++ b/packages/auth-manager/tests/unit/asset/models/assetManager.spec.ts @@ -9,6 +9,7 @@ describe('AssetManager', () => { let assetManager: AssetManager; const mockedRepository = { findBy: jest.fn(), + findAllBy: jest.fn(), findOne: jest.fn(), transaction: jest.fn(), }; @@ -19,7 +20,7 @@ describe('AssetManager', () => { describe('#getAssets', () => { it('should return the array of assets', async function () { const asset = getFakeAsset(); - mockedRepository.findBy.mockResolvedValue([asset]); + mockedRepository.findAllBy.mockResolvedValue([asset]); const assetPromise = assetManager.getAssets({}); @@ -27,7 +28,7 @@ describe('AssetManager', () => { }); it('should throw an error if one is thrown by the repository', async function () { - mockedRepository.findBy.mockRejectedValue(new Error()); + mockedRepository.findAllBy.mockRejectedValue(new Error()); const assetPromise = assetManager.getAssets({});