Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

os.getAvailableOsVersions: Add support for providing additional pine options #1522

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 =
Copy link
Member Author

@thgreasi thgreasi Jan 15, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

primarily just moved from bellow and replaced the === 'all check with convenienceFilter === 'supported' || convenienceFilter === 'include_draft' to be more specific.

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
myarmolinsky marked this conversation as resolved.
Show resolved Hide resolved
? (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
Loading