Skip to content

Commit

Permalink
os.getAvailableOsVersions: Add support for providing additional pine …
Browse files Browse the repository at this point in the history
…options

Change-type: minor
  • Loading branch information
thgreasi committed Jan 16, 2025
1 parent 39a4995 commit 2e740aa
Show file tree
Hide file tree
Showing 2 changed files with 163 additions and 30 deletions.
94 changes: 64 additions & 30 deletions src/models/os.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import once from 'lodash/once';
import {
isNotFoundResponse,
onlyIf,
mergePineOptions,
mergePineOptionsTyped,
type ExtendedPineTypedResult,
} from '../util';
Expand Down Expand Up @@ -330,40 +331,52 @@ const getOsModel = function (

const _getAllOsVersions = async (
deviceTypes: string[],
options?: PineOptions<Release>,
options: PineOptions<Release> | undefined,
convenienceFilter: 'supported' | 'include_draft' | 'all',
): Promise<Dictionary<OsVersion[]>> => {
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<Release>)
: 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<Release> | undefined,
>(
deviceType: string,
options?: { includeDraft?: boolean },
): Promise<OsVersion[]>;
async function getAvailableOsVersions(
options?: TP & { includeDraft?: boolean },
): Promise<Array<ExtendedPineTypedResult<Release, OsVersion, TP>>>;
async function getAvailableOsVersions<
TP extends PineOptions<Release> | undefined,
>(
deviceTypes: string[],
options?: { includeDraft?: boolean },
): Promise<Dictionary<OsVersion[]>>;
options?: TP & { includeDraft?: boolean },
): Promise<
Dictionary<Array<ExtendedPineTypedResult<Release, OsVersion, TP>>>
>;
/**
* @summary Get the supported OS versions for the provided device type(s)
* @name getAvailableOsVersions
Expand All @@ -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.
Expand All @@ -384,17 +397,38 @@ const getOsModel = function (
* @example
* balena.models.os.getAvailableOsVersions(['fincm3', 'raspberrypi3']);
*/
async function getAvailableOsVersions(
async function getAvailableOsVersions<
TP extends PineOptions<Release> | undefined,
>(
deviceTypes: string[] | string,
options?: { includeDraft?: boolean },
): Promise<TypeOrDictionary<OsVersion[]>> {
// 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<Array<ExtendedPineTypedResult<Release, OsVersion, TP>>>
> {
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<Array<ExtendedPineTypedResult<Release, OsVersion, TP>>>;
return singleDeviceTypeArg
? (versionsByDt[singleDeviceTypeArg] ?? [])
: versionsByDt;
Expand Down Expand Up @@ -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<Array<ExtendedPineTypedResult<Release, OsVersion, TP>>>;
return singleDeviceTypeArg
? (versionsByDt[singleDeviceTypeArg] ?? [])
Expand Down
99 changes: 99 additions & 0 deletions tests/integration/models/os.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(

Check failure on line 268 in tests/integration/models/os.spec.ts

View workflow job for this annotation

GitHub Actions / Flowzone / Test custom (windows-latest)

Expression produces a union type that is too complex to represent.

Check failure on line 268 in tests/integration/models/os.spec.ts

View workflow job for this annotation

GitHub Actions / Flowzone / Test custom (ubuntu-latest)

Expression produces a union type that is too complex to represent.

Check failure on line 268 in tests/integration/models/os.spec.ts

View workflow job for this annotation

GitHub Actions / Flowzone / Test custom (macos-latest)

Expression produces a union type that is too complex to represent.
['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']);

Expand Down Expand Up @@ -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');
Expand Down

0 comments on commit 2e740aa

Please sign in to comment.