diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md
index c4e3d27e9..e803eb4a9 100644
--- a/DOCUMENTATION.md
+++ b/DOCUMENTATION.md
@@ -5345,7 +5345,7 @@ or a dictionary of OsVersion objects by device type slug when an array of device
| Param | Type | Default | Description |
| --- | --- | --- | --- |
| deviceTypes | String
\| Array.<String>
| | device type slug or array of slugs |
-| [options] | Object
| | Extra options to filter the OS releases by |
+| [options] | Object
| | Extra pine options & draft filter to use |
| [options.includeDraft] | Boolean
| false
| Whether pre-releases should be included in the results |
**Example**
diff --git a/src/models/os.ts b/src/models/os.ts
index bf8444228..bf6649a70 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,29 +331,35 @@ 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);
},
);
@@ -364,6 +371,18 @@ const getOsModel = function (
deviceTypes: string[],
options?: { includeDraft?: boolean },
): Promise>;
+ // 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>(
+ deviceType: string,
+ options: TP & { includeDraft?: boolean },
+ ): Promise>>;
+ async function getAvailableOsVersions>(
+ deviceTypes: string[],
+ options: TP & { includeDraft?: boolean },
+ ): Promise<
+ Dictionary>>
+ >;
/**
* @summary Get the supported OS versions for the provided device type(s)
* @name getAvailableOsVersions
@@ -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.
@@ -384,17 +403,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 +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>>;
return singleDeviceTypeArg
? (versionsByDt[singleDeviceTypeArg] ?? [])
diff --git a/tests/integration/models/os.spec.ts b/tests/integration/models/os.spec.ts
index 0eb382ce1..f799c0384 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,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');