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 c00e677
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 25 deletions.
2 changes: 1 addition & 1 deletion DOCUMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -5109,7 +5109,7 @@ or a dictionary of OsVersion objects by device type slug when an array of device
| Param | Type | Default | Description |
| --- | --- | --- | --- |
| deviceTypes | <code>String</code> \| <code>Array.&lt;String&gt;</code> | | device type slug or array of slugs |
| [options] | <code>Object</code> | | Extra options to filter the OS releases by |
| [options] | <code>Object</code> | | Extra pine options & draft filter to use |
| [options.includeDraft] | <code>Boolean</code> | <code>false</code> | Whether pre-releases should be included in the results |

**Example**
Expand Down
88 changes: 64 additions & 24 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,29 +331,35 @@ 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);
},
);

Expand All @@ -364,6 +371,18 @@ const getOsModel = function (
deviceTypes: string[],
options?: { includeDraft?: boolean },
): Promise<Dictionary<OsVersion[]>>;
// We define the includeDraft-only overloads separately in order to avoid,
// "Expression produces a union type that is too complex to represent." errors.
async function getAvailableOsVersions<TP extends PineOptions<Release>>(
deviceType: string,
options: TP & { includeDraft?: boolean },
): Promise<Array<ExtendedPineTypedResult<Release, OsVersion, TP>>>;
async function getAvailableOsVersions<TP extends PineOptions<Release>>(
deviceTypes: string[],
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 +391,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 +403,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 +484,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
120 changes: 120 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(
['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,62 @@ 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',
{
$expand: { belongs_to__application: { $select: 'id' } },
$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');
// Testing the values separatelly as well so that the correctness of the return type is also tested
expect(versionInfos[0].raw_version).to.equal('5.1.20+rev1');
expect(versionInfos[0]).to.have.nested.property(
'belongs_to__application[0].id',
);
expect(versionInfos[0].belongs_to__application[0].id).to.be.a('number');
});

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.getAllOsVersions(
'raspberrypi3',
{
includeDraft: true,
$expand: { belongs_to__application: { $select: 'id' } },
$filter: { raw_version: '5.1.10-1706616336246+rev2' },
},
);
expect(draftVersionInfos).to.be.an('array');
expect(draftVersionInfos).to.have.lengthOf(1);
expect(draftVersionInfos[0]).to.have.property(
'raw_version',
'5.1.10-1706616336246+rev2',
);
// Testing the values separatelly as well so that the correctness of the return type is also tested
expect(draftVersionInfos[0].raw_version).to.equal(
'5.1.10-1706616336246+rev2',
);
expect(draftVersionInfos[0]).to.have.nested.property(
'belongs_to__application[0].id',
);
expect(draftVersionInfos[0].belongs_to__application[0].id).to.be.a(
'number',
);
});

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 c00e677

Please sign in to comment.