diff --git a/DOCUMENTATION.md b/DOCUMENTATION.md
index 1cdc848a5..fc3d7efa0 100644
--- a/DOCUMENTATION.md
+++ b/DOCUMENTATION.md
@@ -359,7 +359,7 @@ const sdk = fromSharedOptions();
* [.get(handleOrId, [options])](#balena.models.organization.get) ⇒ Promise
* [.remove(handleOrId)](#balena.models.organization.remove) ⇒ Promise
* [.os](#balena.models.os) : object
- * [.getAvailableOsVersions(deviceTypes)](#balena.models.os.getAvailableOsVersions) ⇒ Promise
+ * [.getAvailableOsVersions(deviceTypes, [includeDraft])](#balena.models.os.getAvailableOsVersions) ⇒ Promise
* [.getAllOsVersions(deviceTypes, [options])](#balena.models.os.getAllOsVersions) ⇒ Promise
* [.getDownloadSize(deviceType, [version])](#balena.models.os.getDownloadSize) ⇒ Promise
* [.getMaxSatisfyingVersion(deviceType, versionOrRange, [osType])](#balena.models.os.getMaxSatisfyingVersion) ⇒ Promise
@@ -367,7 +367,7 @@ const sdk = fromSharedOptions();
* [.download(options)](#balena.models.os.download) ⇒ Promise
* [.getConfig(slugOrUuidOrId, options)](#balena.models.os.getConfig) ⇒ Promise
* [.isSupportedOsUpdate(deviceType, currentVersion, targetVersion)](#balena.models.os.isSupportedOsUpdate) ⇒ Promise
- * [.getSupportedOsUpdateVersions(deviceType, currentVersion)](#balena.models.os.getSupportedOsUpdateVersions) ⇒ Promise
+ * [.getSupportedOsUpdateVersions(deviceType, currentVersion, [includeDraft])](#balena.models.os.getSupportedOsUpdateVersions) ⇒ Promise
* [.isArchitectureCompatibleWith(osArchitecture, applicationArchitecture)](#balena.models.os.isArchitectureCompatibleWith) ⇒ Boolean
* [.getSupervisorImageForDeviceType(deviceTypeId, version)](#balena.models.os.getSupervisorImageForDeviceType) ⇒ Promise.<String>
* [.config](#balena.models.config) : object
@@ -490,8 +490,7 @@ rejects with an error.
### balena.utils : Object
-The utils instance used internally. This should not be necessary
-in normal usage, but can be useful to handle some specific cases.
+The utils instance offers some convenient features for clients.
**Kind**: static property of [balena
](#balena)
**Summary**: Balena utils instance
@@ -503,6 +502,14 @@ balena.utils.mergePineOptions(
{ $expand: { device: { $select: ['name'] } } },
);
```
+**Example**
+```js
+// Creating a new WebResourceFile in case 'File' API is not available.
+new balena.utils.BalenaWebResourceFile(
+ [fs.readFileSync('./file.tgz')],
+ 'file.tgz'
+);
+```
### balena.request : Object
@@ -759,7 +766,7 @@ balena.models.device.get(123).catch(function (error) {
* [.get(handleOrId, [options])](#balena.models.organization.get) ⇒ Promise
* [.remove(handleOrId)](#balena.models.organization.remove) ⇒ Promise
* [.os](#balena.models.os) : object
- * [.getAvailableOsVersions(deviceTypes)](#balena.models.os.getAvailableOsVersions) ⇒ Promise
+ * [.getAvailableOsVersions(deviceTypes, [includeDraft])](#balena.models.os.getAvailableOsVersions) ⇒ Promise
* [.getAllOsVersions(deviceTypes, [options])](#balena.models.os.getAllOsVersions) ⇒ Promise
* [.getDownloadSize(deviceType, [version])](#balena.models.os.getDownloadSize) ⇒ Promise
* [.getMaxSatisfyingVersion(deviceType, versionOrRange, [osType])](#balena.models.os.getMaxSatisfyingVersion) ⇒ Promise
@@ -767,7 +774,7 @@ balena.models.device.get(123).catch(function (error) {
* [.download(options)](#balena.models.os.download) ⇒ Promise
* [.getConfig(slugOrUuidOrId, options)](#balena.models.os.getConfig) ⇒ Promise
* [.isSupportedOsUpdate(deviceType, currentVersion, targetVersion)](#balena.models.os.isSupportedOsUpdate) ⇒ Promise
- * [.getSupportedOsUpdateVersions(deviceType, currentVersion)](#balena.models.os.getSupportedOsUpdateVersions) ⇒ Promise
+ * [.getSupportedOsUpdateVersions(deviceType, currentVersion, [includeDraft])](#balena.models.os.getSupportedOsUpdateVersions) ⇒ Promise
* [.isArchitectureCompatibleWith(osArchitecture, applicationArchitecture)](#balena.models.os.isArchitectureCompatibleWith) ⇒ Boolean
* [.getSupervisorImageForDeviceType(deviceTypeId, version)](#balena.models.os.getSupervisorImageForDeviceType) ⇒ Promise.<String>
* [.config](#balena.models.config) : object
@@ -5155,6 +5162,33 @@ balena.models.organization.create({ name:'MyOrganization' }).then(function(organ
console.log(organization);
});
```
+**Example**
+```js
+balena.models.organization.create({
+ name:'MyOrganization',
+ logo_image: new balena.utils.BalenaWebResourceFile(
+ [fs.readFileSync('./img.jpeg')],
+ 'img.jpeg'
+ );
+})
+.then(function(organization) {
+ console.log(organization);
+});
+```
+**Example**
+```js
+balena.models.organization.create({
+ name:'MyOrganization',
+ // Only in case File API is avaialable (most browsers and Node 20+)
+ logo_image: new File(
+ imageContent,
+ 'img.jpeg'
+ );
+})
+.then(function(organization) {
+ console.log(organization);
+});
+```
##### organization.getAll([options]) ⇒ Promise
@@ -5219,7 +5253,7 @@ balena.models.organization.remove(123);
**Kind**: static namespace of [models
](#balena.models)
* [.os](#balena.models.os) : object
- * [.getAvailableOsVersions(deviceTypes)](#balena.models.os.getAvailableOsVersions) ⇒ Promise
+ * [.getAvailableOsVersions(deviceTypes, [includeDraft])](#balena.models.os.getAvailableOsVersions) ⇒ Promise
* [.getAllOsVersions(deviceTypes, [options])](#balena.models.os.getAllOsVersions) ⇒ Promise
* [.getDownloadSize(deviceType, [version])](#balena.models.os.getDownloadSize) ⇒ Promise
* [.getMaxSatisfyingVersion(deviceType, versionOrRange, [osType])](#balena.models.os.getMaxSatisfyingVersion) ⇒ Promise
@@ -5227,22 +5261,23 @@ balena.models.organization.remove(123);
* [.download(options)](#balena.models.os.download) ⇒ Promise
* [.getConfig(slugOrUuidOrId, options)](#balena.models.os.getConfig) ⇒ Promise
* [.isSupportedOsUpdate(deviceType, currentVersion, targetVersion)](#balena.models.os.isSupportedOsUpdate) ⇒ Promise
- * [.getSupportedOsUpdateVersions(deviceType, currentVersion)](#balena.models.os.getSupportedOsUpdateVersions) ⇒ Promise
+ * [.getSupportedOsUpdateVersions(deviceType, currentVersion, [includeDraft])](#balena.models.os.getSupportedOsUpdateVersions) ⇒ Promise
* [.isArchitectureCompatibleWith(osArchitecture, applicationArchitecture)](#balena.models.os.isArchitectureCompatibleWith) ⇒ Boolean
* [.getSupervisorImageForDeviceType(deviceTypeId, version)](#balena.models.os.getSupervisorImageForDeviceType) ⇒ Promise.<String>
-##### os.getAvailableOsVersions(deviceTypes) ⇒ Promise
+##### os.getAvailableOsVersions(deviceTypes, [includeDraft]) ⇒ Promise
**Kind**: static method of [os
](#balena.models.os)
**Summary**: Get the supported OS versions for the provided device type(s)
**Access**: public
**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.
-| Param | Type | Description |
-| --- | --- | --- |
-| deviceTypes | String
\| Array.<String>
| device type slug or array of slugs |
+| Param | Type | Default | Description |
+| --- | --- | --- | --- |
+| deviceTypes | String
\| Array.<String>
| | device type slug or array of slugs |
+| [includeDraft] | Boolean
| false
| Whether pre-releases should be included in the results |
**Example**
```js
@@ -5432,7 +5467,7 @@ balena.models.os.isSupportedOsUpgrade('raspberry-pi', '2.9.6+rev2.prod', '2.29.2
```
-##### os.getSupportedOsUpdateVersions(deviceType, currentVersion) ⇒ Promise
+##### os.getSupportedOsUpdateVersions(deviceType, currentVersion, [includeDraft]) ⇒ Promise
**Kind**: static method of [os
](#balena.models.os)
**Summary**: Returns the supported OS update targets for the provided device type
**Access**: public
@@ -5443,10 +5478,11 @@ containing exact version numbers that OS update is supported
that is _not_ pre-release, can be `null`
* current - the provided current version after normalization
-| Param | Type | Description |
-| --- | --- | --- |
-| deviceType | String
| device type slug |
-| currentVersion | String
| semver-compatible version for the starting OS version |
+| Param | Type | Default | Description |
+| --- | --- | --- | --- |
+| deviceType | String
| | device type slug |
+| currentVersion | String
| | semver-compatible version for the starting OS version |
+| [includeDraft] | Boolean
| false
| Whether prerelease OS versions should be included |
**Example**
```js
diff --git a/package.json b/package.json
index e3e72398f..f458b0ceb 100644
--- a/package.json
+++ b/package.json
@@ -124,7 +124,7 @@
"abortcontroller-polyfill": "^1.7.1",
"balena-auth": "^5.1.0",
"balena-errors": "^4.9.0",
- "balena-hup-action-utils": "~5.0.0",
+ "balena-hup-action-utils": "~6.1.0",
"balena-register-device": "^9.0.1",
"balena-request": "^13.2.0",
"balena-semver": "^2.3.0",
diff --git a/src/models/device.ts b/src/models/device.ts
index 0f630e06e..308e3e233 100644
--- a/src/models/device.ts
+++ b/src/models/device.ts
@@ -350,6 +350,9 @@ const getDeviceModel = function (
);
}
+ const isDraft =
+ (bSemver.parse(targetOsVersion)?.prerelease.length ?? 0) > 0;
+
const getDeviceType = memoizee(
async (deviceTypeId: number) =>
await sdkInstance.models.deviceType.get(deviceTypeId, {
@@ -358,8 +361,8 @@ const getDeviceModel = function (
{ primitive: true, promise: true },
);
const getAvailableOsVersions = memoizee(
- async (slug: string) =>
- await sdkInstance.models.os.getAvailableOsVersions(slug),
+ async (slug: string, includeDraft: boolean) =>
+ await sdkInstance.models.os.getAvailableOsVersions(slug, includeDraft),
{ primitive: true, promise: true },
);
@@ -392,7 +395,7 @@ const getDeviceModel = function (
targetOsVersion,
);
- const osVersions = await getAvailableOsVersions(dt.slug);
+ const osVersions = await getAvailableOsVersions(dt.slug, isDraft);
if (
!osVersions.some(
diff --git a/src/models/os.ts b/src/models/os.ts
index bec8b9aeb..86f033bfc 100644
--- a/src/models/os.ts
+++ b/src/models/os.ts
@@ -362,27 +362,32 @@ const getOsModel = function (
};
const _memoizedGetAllOsVersions = authDependentMemoizer(
- async (deviceTypes: string[], listedByDefault: boolean | null) => {
+ async (
+ deviceTypes: string[],
+ filterOptions: 'default' | 'include_draft' | 'all',
+ ) => {
return await _getAllOsVersions(
deviceTypes,
- listedByDefault
- ? {
+ filterOptions === 'all'
+ ? undefined
+ : {
$filter: {
- is_final: true,
+ ...(filterOptions !== 'include_draft' && { is_final: true }),
is_invalidated: false,
status: 'success',
},
- }
- : undefined,
+ },
);
},
);
async function getAvailableOsVersions(
deviceType: string,
+ includeDraft?: boolean,
): Promise;
async function getAvailableOsVersions(
deviceTypes: string[],
+ includeDraft?: boolean,
): Promise>;
/**
* @summary Get the supported OS versions for the provided device type(s)
@@ -392,6 +397,7 @@ const getOsModel = function (
* @memberof balena.models.os
*
* @param {String|String[]} deviceTypes - device type slug or array of slugs
+ * @param {Boolean} [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.
* @returns {Promise}
@@ -404,13 +410,14 @@ const getOsModel = function (
*/
async function getAvailableOsVersions(
deviceTypes: string[] | string,
+ includeDraft = false,
): Promise> {
const singleDeviceTypeArg =
typeof deviceTypes === 'string' ? deviceTypes : false;
deviceTypes = Array.isArray(deviceTypes) ? deviceTypes : [deviceTypes];
const versionsByDt = await _memoizedGetAllOsVersions(
deviceTypes.slice().sort(),
- true,
+ includeDraft ? 'include_draft' : 'default',
);
return singleDeviceTypeArg
? versionsByDt[singleDeviceTypeArg] ?? []
@@ -460,7 +467,7 @@ const getOsModel = function (
deviceTypes = Array.isArray(deviceTypes) ? deviceTypes : [deviceTypes];
const versionsByDt = (
options == null
- ? await _memoizedGetAllOsVersions(deviceTypes.slice().sort(), null)
+ ? await _memoizedGetAllOsVersions(deviceTypes.slice().sort(), 'all')
: await _getAllOsVersions(deviceTypes, options)
) as Dictionary>>;
return singleDeviceTypeArg
@@ -869,6 +876,7 @@ const getOsModel = function (
*
* @param {String} deviceType - device type slug
* @param {String} currentVersion - semver-compatible version for the starting OS version
+ * @param {Boolean} [includeDraft=false] - Whether prerelease OS versions should be included
* @fulfil {Object} - the versions information, of the following structure:
* * versions - an array of strings,
* containing exact version numbers that OS update is supported
@@ -885,9 +893,10 @@ const getOsModel = function (
const getSupportedOsUpdateVersions = async (
deviceType: string,
currentVersion: string,
+ includeDraft: boolean = false,
): Promise => {
deviceType = await _getNormalizedDeviceTypeSlug(deviceType);
- const allVersions = (await getAvailableOsVersions(deviceType))
+ const allVersions = (await getAvailableOsVersions(deviceType, includeDraft))
.filter((v) => v.osType === OsTypes.DEFAULT)
.map((v) => v.raw_version);
// use bSemver.compare to find the current version in the OS list
diff --git a/tests/integration/models/device.spec.ts b/tests/integration/models/device.spec.ts
index 9c94eb43a..8bb3d1e99 100644
--- a/tests/integration/models/device.spec.ts
+++ b/tests/integration/models/device.spec.ts
@@ -3729,40 +3729,6 @@ describe('Device Model', function () {
);
}));
- it('should throw when the device is running a pre-release version', () =>
- [
- ['Resin OS 2.0.0-beta.1', ''],
- ['Resin OS 2.0.0-beta.3', ''],
- ['Resin OS 2.0.0-beta11.rev1', ''],
- ['Resin OS 2.0.0-beta.8', ''],
- ['Resin OS 2.0.0-beta.8', 'prod'],
- ['balenaOS 2.0.0-beta12.rev1', 'prod'],
- ['Resin OS 2.0.0-rc1.rev1', ''],
- ['Resin OS 2.0.0-rc1.rev2', 'prod'],
- ['Resin OS 2.0.0-rc1.rev2', ''],
- ['Resin OS 2.0.0-rc6.rev1 (prod)', ''],
- ['Resin OS 2.0.1-beta.4', ''],
- ['Resin OS 2.0.2-beta.2', ''],
- ['Resin OS 2.0.2-beta.7', ''],
- ['Resin OS 2.9.0-multi1+rev1', 'dev'],
- ['balenaOS 2.28.0-beta1.rev1', 'prod'],
- ].forEach(function ([osVersion, osVariant]) {
- return expect(() =>
- _checkOsUpdateTarget(
- {
- uuid,
- is_of__device_type: [{ slug: 'raspberrypi3' }],
- is_online: true,
- os_version: osVersion,
- os_variant: osVariant,
- },
- '2.29.2+rev1.prod',
- ),
- ).to.throw(
- 'Updates cannot be performed on pre-release balenaOS versions',
- );
- }));
-
describe('v1 -> v1 hup', () =>
['raspberrypi3', 'intel-nuc'].forEach((deviceType) =>
describe(`given a ${deviceType}`, function () {
@@ -3925,7 +3891,7 @@ describe('Device Model', function () {
describe('v2 -> v2 hup', function () {
describe('given a raspberrypi3', function () {
- it('should throw when current os version is < 2.0.0+rev1', () =>
+ it('should throw when current os version is < 2.0.0+rev1', () => {
[['Resin OS 2.0.0.rev0 (prod)', 'prod']].forEach(function ([
osVersion,
osVariant,
@@ -3942,9 +3908,10 @@ describe('Device Model', function () {
'2.1.0+rev1.prod',
),
).to.throw('Current OS version must be >= 2.0.0+rev1');
- }));
+ });
+ });
- it('should not throw when it is a valid v2 -> v2 hup', () =>
+ it('should not throw when it is a valid v2 -> v2 hup', () => {
[
['Resin OS 2.0.0.rev1 (prod)', 'prod'],
['Resin OS 2.0.0.rev1 (prod)', ''],
@@ -3985,7 +3952,88 @@ describe('Device Model', function () {
'2.29.2+rev1.prod',
),
).to.not.throw();
- }));
+ });
+ });
+
+ it('should throw when updating to a pre-release version with an older server', () => {
+ [
+ ['balenaOS 2.29.2-1704382618288+rev1', 'prod'],
+ ['balenaOS 2.29.2+rev1', 'prod'],
+ ].forEach(function ([osVersion, osVariant]) {
+ expect(() =>
+ _checkOsUpdateTarget(
+ {
+ uuid,
+ is_of__device_type: [{ slug: 'raspberrypi3' }],
+ is_online: true,
+ os_version: osVersion,
+ os_variant: osVariant,
+ },
+ '2.28.0-1704382553234+rev1.prod',
+ ),
+ ).to.throw('OS downgrades are not allowed');
+ });
+ });
+
+ it('should not throw when updating to a pre-release version with a newer base server', () => {
+ [['balenaOS 2.28.0+rev1', 'prod']].forEach(function ([
+ osVersion,
+ osVariant,
+ ]) {
+ expect(() =>
+ _checkOsUpdateTarget(
+ {
+ uuid,
+ is_of__device_type: [{ slug: 'raspberrypi3' }],
+ is_online: true,
+ os_version: osVersion,
+ os_variant: osVariant,
+ },
+ '2.29.2-1704382618288+rev1.prod',
+ ),
+ ).to.not.throw();
+ });
+ });
+
+ it('should not throw when updating a device that is running a pre-release version to a version with a newer base server', () => {
+ [['balenaOS 2.28.0-1704382553234', 'prod']].forEach(function ([
+ osVersion,
+ osVariant,
+ ]) {
+ expect(() =>
+ _checkOsUpdateTarget(
+ {
+ uuid,
+ is_of__device_type: [{ slug: 'raspberrypi3' }],
+ is_online: true,
+ os_version: osVersion,
+ os_variant: osVariant,
+ },
+ '2.29.2+rev1.prod',
+ ),
+ ).to.not.throw();
+ });
+ });
+
+ it('should not throw when updating a device that is running a pre-release version updating to a pre-release version with a newer base server', () => {
+ [['balenaOS 2.28.0-1704382553234', 'prod']].forEach(function ([
+ osVersion,
+ osVariant,
+ ]) {
+ expect(() =>
+ _checkOsUpdateTarget(
+ {
+ uuid,
+ is_of__device_type: [{ slug: 'raspberrypi3' }],
+ is_online: true,
+ os_version: osVersion,
+ os_variant: osVariant,
+ },
+ '2.29.2-1704382618288+rev1.prod',
+ ),
+ ).to.not.throw();
+ });
+ });
});
describe('given a jetson-tx2', function () {
diff --git a/typings/pinejs-client-core.d.ts b/typings/pinejs-client-core.d.ts
index a74ea1fed..7e40aa91a 100644
--- a/typings/pinejs-client-core.d.ts
+++ b/typings/pinejs-client-core.d.ts
@@ -59,16 +59,14 @@ export type ExpandableProps = PropsOfType> &
PropsAssignableWithType &
string;
-type SelectedProperty<
- T,
- K extends keyof T,
-> = T[K] extends NavigationResource
- ? PineDeferred
- : T[K] extends OptionalNavigationResource
- ? PineDeferred | null
- : T[K] extends ConceptTypeNavigationResource
- ? Exclude
- : T[K];
+type SelectedProperty =
+ T[K] extends NavigationResource
+ ? PineDeferred
+ : T[K] extends OptionalNavigationResource
+ ? PineDeferred | null
+ : T[K] extends ConceptTypeNavigationResource
+ ? Exclude
+ : T[K];
type SelectResultObject = {
[P in Props]: SelectedProperty;
@@ -91,15 +89,16 @@ type ExpandedProperty<
T,
K extends keyof T,
KOpts extends ODataOptions>,
-> = KOpts extends ODataOptionsWithCount
- ? number
- : T[K] extends NavigationResource | ConceptTypeNavigationResource
- ? [TypedResult, KOpts>]
- : T[K] extends OptionalNavigationResource
- ? [TypedResult, KOpts>] | []
- : T[K] extends ReverseNavigationResource
- ? Array, KOpts>>
- : never;
+> =
+ KOpts extends ODataOptionsWithCount
+ ? number
+ : T[K] extends NavigationResource | ConceptTypeNavigationResource
+ ? [TypedResult, KOpts>]
+ : T[K] extends OptionalNavigationResource
+ ? [TypedResult, KOpts>] | []
+ : T[K] extends ReverseNavigationResource
+ ? Array, KOpts>>
+ : never;
export type ExpandResultObject = {
[P in Props]: ExpandedProperty;
@@ -116,28 +115,27 @@ type ExpandResourceExpandObject<
>;
};
-export type TypedExpandResult<
- T,
- TParams extends ODataOptions,
-> = TParams['$expand'] extends ExpandableProps
- ? ExpandResultObject
- : TParams['$expand'] extends ResourceExpand
- ? keyof TParams['$expand'] extends ExpandableProps
- ? ExpandResourceExpandObject
- : never
- : object;
-
-export type TypedResult<
- T,
- TParams extends ODataOptions | undefined,
-> = TParams extends ODataOptionsWithCount
- ? number
- : TParams extends ODataOptions
- ? Omit, keyof TypedExpandResult> &
- TypedExpandResult
- : undefined extends TParams
- ? TypedSelectResult
- : never;
+export type TypedExpandResult> =
+ TParams['$expand'] extends ExpandableProps
+ ? ExpandResultObject
+ : TParams['$expand'] extends ResourceExpand
+ ? keyof TParams['$expand'] extends ExpandableProps
+ ? ExpandResourceExpandObject
+ : never
+ : object;
+
+export type TypedResult | undefined> =
+ TParams extends ODataOptionsWithCount
+ ? number
+ : TParams extends ODataOptions
+ ? Omit<
+ TypedSelectResult,
+ keyof TypedExpandResult
+ > &
+ TypedExpandResult
+ : undefined extends TParams
+ ? TypedSelectResult
+ : never;
export type PostResult = SelectResultObject<
T,
@@ -190,18 +188,15 @@ type OrderBy =
$dir: OrderByDirection;
});
-type AssociatedResourceFilter = T extends NonNullable<
- ReverseNavigationResource