diff --git a/lib/commands/build.ts b/lib/commands/build.ts index deaf8fdf..9d8b92cb 100644 --- a/lib/commands/build.ts +++ b/lib/commands/build.ts @@ -20,7 +20,7 @@ import Command from '../command'; import { getBalenaSdk } from '../utils/lazy'; import * as cf from '../utils/common-flags'; import * as compose from '../utils/compose'; -import type { Application, ApplicationType, BalenaSDK } from 'balena-sdk'; +import type { ApplicationType, BalenaSDK } from 'balena-sdk'; import { buildArgDeprecation, dockerignoreHelp, @@ -218,7 +218,9 @@ ${dockerignoreHelp} logger: import('../utils/logger'), composeOpts: ComposeOpts, opts: { - app?: Application; + app?: { + application_type: [Pick]; + }; arch: string; deviceType: string; buildEmulated: boolean; @@ -234,7 +236,7 @@ ${dockerignoreHelp} opts.buildOpts.t, ); - const appType = (opts.app?.application_type as ApplicationType[])?.[0]; + const appType = opts.app?.application_type?.[0]; if ( appType != null && project.descriptors.length > 1 && diff --git a/lib/commands/config/generate.ts b/lib/commands/config/generate.ts index 9f337a60..90a33b09 100644 --- a/lib/commands/config/generate.ts +++ b/lib/commands/config/generate.ts @@ -24,7 +24,7 @@ import { devModeInfo, secureBootInfo, } from '../../utils/messages'; -import type { PineDeferred } from 'balena-sdk'; +import type { BalenaSDK, PineDeferred } from 'balena-sdk'; interface FlagsDef { version: string; // OS version @@ -144,17 +144,24 @@ export default class ConfigGenerateCmd extends Command { public static authenticated = true; + public async getApplication(balena: BalenaSDK, fleet: string) { + const { getApplication } = await import('../../utils/sdk'); + return await getApplication(balena, fleet, { + $expand: { + is_for__device_type: { $select: 'slug' }, + }, + }); + } + public async run() { const { flags: options } = this.parse(ConfigGenerateCmd); - - const { getApplication } = await import('../../utils/sdk'); - const balena = getBalenaSdk(); await this.validateOptions(options); let resourceDeviceType: string; - let application: ApplicationWithDeviceType | null = null; + let application: Awaited> | null = + null; let device: | (DeviceWithDeviceType & { belongs_to__application: PineDeferred }) | null = null; @@ -174,11 +181,7 @@ export default class ConfigGenerateCmd extends Command { resourceDeviceType = device.is_of__device_type[0].slug; } else { // Disambiguate application (if is a number, it could either be an ID or a numerical name) - application = (await getApplication(balena, options.fleet!, { - $expand: { - is_for__device_type: { $select: 'slug' }, - }, - })) as ApplicationWithDeviceType; + application = await this.getApplication(balena, options.fleet!); resourceDeviceType = application.is_for__device_type[0].slug; } diff --git a/lib/commands/deploy.ts b/lib/commands/deploy.ts index 91efd6b9..c562ec5e 100644 --- a/lib/commands/deploy.ts +++ b/lib/commands/deploy.ts @@ -43,15 +43,13 @@ import { parseReleaseTagKeysAndValues, } from '../utils/compose_ts'; import { dockerCliFlags } from '../utils/docker'; -import type { - Application, - ApplicationType, - DeviceType, - Release, -} from 'balena-sdk'; +import type { ApplicationType, DeviceType, Release } from 'balena-sdk'; -interface ApplicationWithArch extends Application { +interface ApplicationWithArch { + id: number; arch: string; + is_for__device_type: [Pick]; + application_type: [Pick]; } interface FlagsDef extends ComposeCliFlags, DockerCliFlags { @@ -262,7 +260,7 @@ ${dockerignoreHelp} '../utils/compose_ts' ); - const appType = (opts.app?.application_type as ApplicationType[])?.[0]; + const appType = opts.app.application_type[0]; try { const project = await loadProject( @@ -319,7 +317,7 @@ ${dockerignoreHelp} projectName: project.name, composition: compositionToBuild, arch: opts.app.arch, - deviceType: (opts.app?.is_for__device_type as DeviceType[])?.[0].slug, + deviceType: opts.app.is_for__device_type[0].slug, emulated: opts.buildEmulated, buildOpts: opts.buildOpts, inlineLogs: composeOpts.inlineLogs, diff --git a/lib/commands/device/init.ts b/lib/commands/device/init.ts index fa6dc2e8..925f81fb 100644 --- a/lib/commands/device/init.ts +++ b/lib/commands/device/init.ts @@ -124,7 +124,7 @@ export default class DeviceInitCmd extends Command { const balena = getBalenaSdk(); // Get application and - const application = (await getApplication( + const application = await getApplication( balena, options.fleet || ( @@ -137,7 +137,7 @@ export default class DeviceInitCmd extends Command { }, }, }, - )) as ApplicationWithDeviceType; + ); // Register new device const deviceUuid = balena.models.device.generateUniqueKey(); diff --git a/lib/commands/fleet/index.ts b/lib/commands/fleet/index.ts index 1f9ff687..87b3989a 100644 --- a/lib/commands/fleet/index.ts +++ b/lib/commands/fleet/index.ts @@ -17,7 +17,6 @@ import type { flags as flagsType } from '@oclif/command'; import { flags } from '@oclif/command'; -import type { Release } from 'balena-sdk'; import Command from '../../command'; import * as cf from '../../utils/common-flags'; @@ -74,17 +73,12 @@ export default class FleetCmd extends Command { const balena = getBalenaSdk(); - const application = (await getApplication(balena, params.fleet, { + const application = await getApplication(balena, params.fleet, { $expand: { is_for__device_type: { $select: 'slug' }, should_be_running__release: { $select: 'commit' }, }, - })) as ApplicationWithDeviceType & { - should_be_running__release: [Release?]; - // For display purposes: - device_type: string; - commit?: string; - }; + }); if (options.view) { const open = await import('open'); @@ -95,11 +89,14 @@ export default class FleetCmd extends Command { return; } - application.device_type = application.is_for__device_type[0].slug; - application.commit = application.should_be_running__release[0]?.commit; + const outputApplication = { + ...application, + device_type: application.is_for__device_type[0].slug, + commit: application.should_be_running__release[0]?.commit, + }; await this.outputData( - application, + outputApplication, ['app_name', 'id', 'device_type', 'slug', 'commit'], options, ); diff --git a/lib/commands/fleet/rename.ts b/lib/commands/fleet/rename.ts index 2ec0da89..8d87bef8 100644 --- a/lib/commands/fleet/rename.ts +++ b/lib/commands/fleet/rename.ts @@ -16,7 +16,6 @@ */ import type { flags } from '@oclif/command'; -import type { ApplicationType } from 'balena-sdk'; import Command from '../../command'; import * as cf from '../../utils/common-flags'; @@ -91,7 +90,7 @@ export default class FleetRenameCmd extends Command { } // Check app supports renaming - const appType = (application.application_type as ApplicationType[])?.[0]; + const appType = application.application_type[0]; if (appType.slug === 'legacy-v1' || appType.slug === 'legacy-v2') { throw new ExpectedError( `Fleet ${params.fleet} is of 'legacy' type, and cannot be renamed.`, diff --git a/lib/commands/os/configure.ts b/lib/commands/os/configure.ts index e1073858..c7f7aeaf 100644 --- a/lib/commands/os/configure.ts +++ b/lib/commands/os/configure.ts @@ -381,7 +381,9 @@ async function getOsVersionFromImage( */ async function checkDeviceTypeCompatibility( options: FlagsDef, - app: ApplicationWithDeviceType, + app: { + is_for__device_type: [Pick]; + }, ) { if (options['device-type']) { const helpers = await import('../../utils/helpers'); diff --git a/lib/commands/preload.ts b/lib/commands/preload.ts index ceda876e..badc168e 100644 --- a/lib/commands/preload.ts +++ b/lib/commands/preload.ts @@ -31,7 +31,14 @@ import { parseAsInteger } from '../utils/validation'; import { flags } from '@oclif/command'; import * as _ from 'lodash'; -import type { Application, BalenaSDK, PineExpand, Release } from 'balena-sdk'; +import type { + Application, + BalenaSDK, + PineExpand, + PineOptions, + PineTypedResult, + Release, +} from 'balena-sdk'; import type { Preloader } from 'balena-preload'; interface FlagsDef extends DockerConnectionCliFlags { @@ -308,7 +315,7 @@ Can be repeated to add multiple certificates.\ } } - readonly applicationExpandOptions: PineExpand = { + readonly applicationExpandOptions = { owns__release: { $select: ['id', 'commit', 'end_timestamp', 'composition'], $expand: { @@ -329,7 +336,7 @@ Can be repeated to add multiple certificates.\ should_be_running__release: { $select: 'commit', }, - }; + } satisfies PineExpand; isCurrentCommit(commit: string) { return commit === 'latest' || commit === 'current'; @@ -343,7 +350,7 @@ Can be repeated to add multiple certificates.\ } catch { throw new Error(`Device type "${deviceTypeSlug}" not found in API query`); } - return (await balena.models.application.getAllDirectlyAccessible({ + const options = { $select: ['id', 'slug', 'should_track_latest_release'], $expand: this.applicationExpandOptions, $filter: { @@ -388,11 +395,10 @@ Can be repeated to add multiple certificates.\ }, }, $orderby: 'slug asc', - })) as Array< - ApplicationWithDeviceType & { - should_be_running__release: [Release?]; - } - >; + } satisfies PineOptions; + return (await balena.models.application.getAllDirectlyAccessible( + options, + )) as Array>; } async selectApplication(deviceTypeSlug: string) { @@ -442,7 +448,7 @@ Can be repeated to add multiple certificates.\ } async offerToDisableAutomaticUpdates( - application: Application, + application: Pick, commit: string, pinDevice: boolean, ) { @@ -494,9 +500,9 @@ Would you like to disable automatic updates for this fleet now?\ async getAppWithReleases(balenaSdk: BalenaSDK, slug: string) { const { getApplication } = await import('../utils/sdk'); - return (await getApplication(balenaSdk, slug, { + return await getApplication(balenaSdk, slug, { $expand: this.applicationExpandOptions, - })) as Application & { should_be_running__release: [Release?] }; + }); } async prepareAndPreload( diff --git a/lib/utils/config.ts b/lib/utils/config.ts index 06c64542..fef0b0b7 100644 --- a/lib/utils/config.ts +++ b/lib/utils/config.ts @@ -59,7 +59,7 @@ export interface ImgConfig { } export async function generateApplicationConfig( - application: BalenaSdk.Application, + application: Pick, options: { version: string; appUpdatePollInterval?: number; diff --git a/lib/utils/helpers.ts b/lib/utils/helpers.ts index ceada9ca..0d480806 100644 --- a/lib/utils/helpers.ts +++ b/lib/utils/helpers.ts @@ -180,11 +180,10 @@ export async function osProgressHandler(step: InitializeEmitter) { }); } -export async function getAppWithArch( - applicationName: string, -): Promise { +export async function getAppWithArch(applicationName: string) { const { getApplication } = await import('./sdk'); - const options: BalenaSdk.PineOptions = { + const balena = getBalenaSdk(); + const app = await getApplication(balena, applicationName, { $expand: { application_type: { $select: ['name', 'slug', 'supports_multicontainer'], @@ -198,20 +197,10 @@ export async function getAppWithArch( }, }, }, - }; - const balena = getBalenaSdk(); - const app = (await getApplication( - balena, - applicationName, - options, - )) as ApplicationWithDeviceType; - const { getExpanded } = await import('./pine'); - + }); return { ...app, - arch: getExpanded( - getExpanded(app.is_for__device_type)!.is_of__cpu_architecture, - )!.slug, + arch: app.is_for__device_type[0].is_of__cpu_architecture[0].slug, }; } diff --git a/lib/utils/patterns.ts b/lib/utils/patterns.ts index c9cd326b..f8d8486e 100644 --- a/lib/utils/patterns.ts +++ b/lib/utils/patterns.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import type { Application, BalenaSDK, Device, Organization } from 'balena-sdk'; +import type { BalenaSDK, Device, Organization } from 'balena-sdk'; import _ = require('lodash'); import { instanceOf, NotLoggedInError, ExpectedError } from '../errors'; @@ -193,10 +193,11 @@ export function selectApplication( }); } -export async function selectOrganization(organizations?: Organization[]) { +export async function selectOrganization( + organizations?: Array>, +) { // Use either provided orgs (if e.g. already loaded) or load from cloud - organizations = - organizations || (await getBalenaSdk().models.organization.getAll()); + organizations ??= await getBalenaSdk().models.organization.getAll(); return getCliForm().ask({ message: 'Select an organization', type: 'list', @@ -295,19 +296,20 @@ export async function getOnlineTargetDeviceUuid( } // Not a device UUID, try application - let application: Application; - try { - logger.logDebug(`Fetching fleet ${fleetOrDevice}`); - const { getApplication } = await import('./sdk'); - application = await getApplication(sdk, fleetOrDevice); - } catch (err) { - const { BalenaApplicationNotFound } = await import('balena-errors'); - if (instanceOf(err, BalenaApplicationNotFound)) { - throw new ExpectedError(`Fleet or Device not found: ${fleetOrDevice}`); - } else { - throw err; + const application = await (async () => { + try { + logger.logDebug(`Fetching fleet ${fleetOrDevice}`); + const { getApplication } = await import('./sdk'); + return await getApplication(sdk, fleetOrDevice); + } catch (err) { + const { BalenaApplicationNotFound } = await import('balena-errors'); + if (instanceOf(err, BalenaApplicationNotFound)) { + throw new ExpectedError(`Fleet or Device not found: ${fleetOrDevice}`); + } else { + throw err; + } } - } + })(); // App found, load its devices const devices = await sdk.models.device.getAllByApplication(application.id, { diff --git a/lib/utils/sdk.ts b/lib/utils/sdk.ts index 6c227b38..dd1bb32e 100644 --- a/lib/utils/sdk.ts +++ b/lib/utils/sdk.ts @@ -20,8 +20,18 @@ import type { BalenaSDK, Organization, PineOptions, + PineTypedResult, } from 'balena-sdk'; +export async function getApplication( + sdk: BalenaSDK, + nameOrSlugOrId: string | number, +): Promise; +export async function getApplication>( + sdk: BalenaSDK, + nameOrSlugOrId: string | number, + options?: TP, +): Promise>; /** * Get a fleet object, disambiguating the fleet identifier which may be a * a fleet slug or name. @@ -29,21 +39,24 @@ import type { */ export async function getApplication( sdk: BalenaSDK, - nameOrSlug: string, + nameOrSlugOrId: string | number, options?: PineOptions, ): Promise { const { looksLikeFleetSlug } = await import('./validation'); - if (!looksLikeFleetSlug(nameOrSlug)) { + if ( + typeof nameOrSlugOrId === 'string' && + !looksLikeFleetSlug(nameOrSlugOrId) + ) { // Not a slug: must be an app name. // TODO: revisit this logic when we add support for fleet UUIDs. return await sdk.models.application.getAppByName( - nameOrSlug, + nameOrSlugOrId, options, 'directly_accessible', ); } return await sdk.models.application.getDirectlyAccessible( - nameOrSlug, + nameOrSlugOrId, options, ); }