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 -> - ? FilterObj> - : FilterObj> | number | null; +type AssociatedResourceFilter = + T extends NonNullable> + ? FilterObj> + : FilterObj> | number | null; -type ResourceObjFilterPropValue< - T, - k extends keyof T, -> = T[k] extends AssociatedResource - ? AssociatedResourceFilter - : T[k] | FilterExpressions | null; +type ResourceObjFilterPropValue = + T[k] extends AssociatedResource + ? AssociatedResourceFilter + : T[k] | FilterExpressions | null; type ResourceObjFilter = { [k in keyof T]?: ResourceObjFilterPropValue;