From 2e740aabc35bbd0531c23c58eeef6f43015af7fe Mon Sep 17 00:00:00 2001 From: Thodoris Greasidis Date: Wed, 15 Jan 2025 19:23:31 +0200 Subject: [PATCH] os.getAvailableOsVersions: Add support for providing additional pine options Change-type: minor --- src/models/os.ts | 94 ++++++++++++++++++--------- tests/integration/models/os.spec.ts | 99 +++++++++++++++++++++++++++++ 2 files changed, 163 insertions(+), 30 deletions(-) diff --git a/src/models/os.ts b/src/models/os.ts index bf8444228..bd774d2c0 100644 --- a/src/models/os.ts +++ b/src/models/os.ts @@ -20,6 +20,7 @@ import once from 'lodash/once'; import { isNotFoundResponse, onlyIf, + mergePineOptions, mergePineOptionsTyped, type ExtendedPineTypedResult, } from '../util'; @@ -330,40 +331,52 @@ const getOsModel = function ( const _getAllOsVersions = async ( deviceTypes: string[], - options?: PineOptions, + options: PineOptions | undefined, + convenienceFilter: 'supported' | 'include_draft' | 'all', ): Promise> => { - const hostapps = await _getOsVersions(deviceTypes, options); + const extraFilterOptions = + convenienceFilter === 'supported' || convenienceFilter === 'include_draft' + ? ({ + $filter: { + ...(convenienceFilter === 'supported' && { is_final: true }), + is_invalidated: false, + status: 'success', + }, + } satisfies PineOptions) + : undefined; + + const finalOptions = + options != null + ? mergePineOptions(options, extraFilterOptions) + : extraFilterOptions; + + const hostapps = await _getOsVersions(deviceTypes, finalOptions); return _transformHostApps(hostapps); }; const _memoizedGetAllOsVersions = authDependentMemoizer( async ( deviceTypes: string[], - filterOptions: 'supported' | 'include_draft' | 'all', + convenienceFilter: 'supported' | 'include_draft' | 'all', ) => { - return await _getAllOsVersions( - deviceTypes, - filterOptions === 'all' - ? undefined - : { - $filter: { - ...(filterOptions === 'supported' && { is_final: true }), - is_invalidated: false, - status: 'success', - }, - }, - ); + return await _getAllOsVersions(deviceTypes, undefined, convenienceFilter); }, ); - async function getAvailableOsVersions( + async function getAvailableOsVersions< + TP extends PineOptions | undefined, + >( deviceType: string, - options?: { includeDraft?: boolean }, - ): Promise; - async function getAvailableOsVersions( + options?: TP & { includeDraft?: boolean }, + ): Promise>>; + async function getAvailableOsVersions< + TP extends PineOptions | undefined, + >( deviceTypes: string[], - options?: { includeDraft?: boolean }, - ): Promise>; + options?: TP & { includeDraft?: boolean }, + ): Promise< + Dictionary>> + >; /** * @summary Get the supported OS versions for the provided device type(s) * @name getAvailableOsVersions @@ -372,7 +385,7 @@ const getOsModel = function ( * @memberof balena.models.os * * @param {String|String[]} deviceTypes - device type slug or array of slugs - * @param {Object} [options] - Extra options to filter the OS releases by + * @param {Object} [options] - Extra pine options & draft filter to use * @param {Boolean} [options.includeDraft=false] - Whether pre-releases should be included in the results * @fulfil {Object[]|Object} - An array of OsVersion objects when a single device type slug is provided, * or a dictionary of OsVersion objects by device type slug when an array of device type slugs is provided. @@ -384,17 +397,38 @@ const getOsModel = function ( * @example * balena.models.os.getAvailableOsVersions(['fincm3', 'raspberrypi3']); */ - async function getAvailableOsVersions( + async function getAvailableOsVersions< + TP extends PineOptions | undefined, + >( deviceTypes: string[] | string, - options?: { includeDraft?: boolean }, - ): Promise> { + // TODO: Consider providing a different way to for specifying includeDraft in the next major + // eg: make a methods that returns the complex filter + options?: TP & { includeDraft?: boolean }, + ): Promise< + TypeOrDictionary>> + > { + const pineOptionEntries = + options != null + ? Object.entries(options).filter(([key]) => key.startsWith('$')) + : undefined; + const pineOptions = + pineOptionEntries != null && pineOptionEntries.length > 0 + ? (Object.fromEntries(pineOptionEntries) as TP) + : undefined; + const singleDeviceTypeArg = typeof deviceTypes === 'string' ? deviceTypes : false; deviceTypes = Array.isArray(deviceTypes) ? deviceTypes : [deviceTypes]; - const versionsByDt = await _memoizedGetAllOsVersions( - deviceTypes.slice().sort(), - options?.includeDraft === true ? 'include_draft' : 'supported', - ); + const convenienceFilter = + options?.includeDraft === true ? 'include_draft' : 'supported'; + const versionsByDt = ( + pineOptions == null + ? await _memoizedGetAllOsVersions( + deviceTypes.slice().sort(), + convenienceFilter, + ) + : await _getAllOsVersions(deviceTypes, pineOptions, convenienceFilter) + ) as Dictionary>>; return singleDeviceTypeArg ? (versionsByDt[singleDeviceTypeArg] ?? []) : versionsByDt; @@ -444,7 +478,7 @@ const getOsModel = function ( const versionsByDt = ( options == null ? await _memoizedGetAllOsVersions(deviceTypes.slice().sort(), 'all') - : await _getAllOsVersions(deviceTypes, options) + : await _getAllOsVersions(deviceTypes, options, 'all') ) as Dictionary>>; return singleDeviceTypeArg ? (versionsByDt[singleDeviceTypeArg] ?? []) diff --git a/tests/integration/models/os.spec.ts b/tests/integration/models/os.spec.ts index 0eb382ce1..a70b02cb3 100644 --- a/tests/integration/models/os.spec.ts +++ b/tests/integration/models/os.spec.ts @@ -254,6 +254,70 @@ describe('OS model', function () { } }); + it('should cache the results when providing the includeDraft option', async () => { + let firstRes = await balena.models.os.getAvailableOsVersions( + ['raspberrypi3'], + { includeDraft: true }, + ); + let secondRes = await balena.models.os.getAvailableOsVersions( + ['raspberrypi3'], + { includeDraft: true }, + ); + expect(firstRes).to.equal(secondRes); + + firstRes = await balena.models.os.getAvailableOsVersions( + ['raspberrypi3'], + { includeDraft: true }, + ); + secondRes = await balena.models.os.getAvailableOsVersions( + ['raspberrypi3'], + { includeDraft: true }, + ); + expect(firstRes).to.equal(secondRes); + }); + + it('should cache the results when providing an empty options object', async () => { + const firstRes = await balena.models.os.getAvailableOsVersions( + ['raspberrypi3'], + {}, + ); + const secondRes = await balena.models.os.getAvailableOsVersions( + ['raspberrypi3'], + {}, + ); + expect(firstRes).to.equal(secondRes); + }); + + it('should not cache the results when providing extra pine options', async () => { + const firstRes = await balena.models.os.getAvailableOsVersions( + ['fincm3'], + { $filter: { raw_version: '2.29.0+rev1' } }, + ); + const secondRes = await balena.models.os.getAvailableOsVersions( + ['fincm3'], + { $filter: { raw_version: '2.29.0+rev1' } }, + ); + expect(firstRes).to.not.equal(secondRes); + }); + + it('should not cache the results when providing the includeDraft option & extra pine options', async () => { + const firstRes = await balena.models.os.getAvailableOsVersions( + ['fincm3'], + { + includeDraft: true, + $filter: { raw_version: '2.29.0-123456789+rev1' }, + }, + ); + const secondRes = await balena.models.os.getAvailableOsVersions( + ['fincm3'], + { + includeDraft: true, + $filter: { raw_version: '2.29.0-123456789+rev1' }, + }, + ); + expect(firstRes).to.not.equal(secondRes); + }); + it('should return an empty object for non-existent DTs', async () => { const res = await balena.models.os.getAllOsVersions(['blahbleh']); @@ -336,6 +400,41 @@ describe('OS model', function () { expect(draftVersions).to.have.length.greaterThan(0); }); + it('should be able to provide additional pine options [string device type argument]', async () => { + const versionInfos = await balena.models.os.getAvailableOsVersions( + 'raspberrypi3', + { $filter: { raw_version: '5.1.20+rev1' } }, + ); + expect(versionInfos).to.be.an('array'); + + expect(versionInfos).to.have.lengthOf(1); + expect(versionInfos[0]).to.have.property('raw_version', '5.1.20+rev1'); + }); + + it('should be able to provide the includeDraft option & extra pine options [string device type argument]', async () => { + const finalizedVersionInfos = + await balena.models.os.getAvailableOsVersions('raspberrypi3', { + includeDraft: false, + $filter: { raw_version: '5.1.10-1706616336246+rev2' }, + }); + expect(finalizedVersionInfos).to.be.an('array'); + expect(finalizedVersionInfos.map((v) => v.raw_version)).to.deep.equal( + [], + ); + + const draftVersionInfos = await balena.models.os.getAvailableOsVersions( + 'raspberrypi3', + { + includeDraft: true, + $filter: { raw_version: '5.1.10-1706616336246+rev2' }, + }, + ); + expect(draftVersionInfos).to.be.an('array'); + expect(draftVersionInfos.map((v) => v.raw_version)).to.deep.equal([ + '5.1.10-1706616336246+rev2', + ]); + }); + it('should contain both balenaOS and balenaOS ESR OS types [array of single device type]', async () => { const res = await balena.models.os.getAvailableOsVersions(['fincm3']); expect(res).to.be.an('object');