balena-sdk
Version:
The Balena JavaScript SDK
734 lines (656 loc) • 19.7 kB
TypeScript
import type { WebResourceFile } from 'balena-request';
import type {
AnyObject,
PropsOfType,
StringKeyof,
Dictionary,
ExactlyExtends,
} from './utils';
export interface WithId {
id: number;
}
export interface PineDeferred {
__id: number;
}
/**
* When not selected-out holds a deferred.
* When expanded hold an array with a single element.
*/
export type ConceptTypeNavigationResource<T extends object> =
NavigationResource<T>;
export type NavigationResource<T extends object> = [T] | PineDeferred;
export type OptionalNavigationResource<T extends object> =
| []
| [T]
| PineDeferred
| null;
/**
* When expanded holds an array, otherwise the property is not present.
* Selecting is not suggested,
* in that case it holds a deferred to the original resource.
*/
export type ReverseNavigationResource<T extends object> = T[] | undefined;
export type AssociatedResource<T extends object> =
| NavigationResource<T>
| OptionalNavigationResource<T>
| ReverseNavigationResource<T>;
export type InferAssociatedResourceType<T> =
T extends AssociatedResource<object> & any[] ? T[number] : never;
export type SelectableProps<T> =
// This allows us to get proper results when T is any/AnyObject, otherwise this returned never
PropsOfType<T, ReverseNavigationResource<object>> extends StringKeyof<T>
? StringKeyof<T>
: Exclude<
StringKeyof<T>,
PropsOfType<T, ReverseNavigationResource<object>>
>; // This is the normal typed case
export type ExpandableProps<T> = PropsOfType<T, AssociatedResource<object>> &
string;
type SelectedProperty<T, K extends keyof T> =
T[K] extends NavigationResource<any>
? PineDeferred
: T[K] extends OptionalNavigationResource<any>
? PineDeferred | null
: T[K];
type SelectResultObject<T, Props extends keyof T> = {
[P in Props]: SelectedProperty<T, P>;
};
export type TypedSelectResult<
T,
TParams extends ODataOptions<T>,
> = TParams['$select'] extends keyof T
? SelectResultObject<T, TParams['$select']>
: TParams['$select'] extends Array<keyof T>
? SelectResultObject<T, TParams['$select'][number]>
: TParams['$select'] extends '*'
? SelectResultObject<T, SelectableProps<T>>
: undefined extends TParams['$select']
? SelectResultObject<T, SelectableProps<T>>
: never;
type ExpandedProperty<
T,
K extends keyof T,
KOpts extends ODataOptions<InferAssociatedResourceType<T[K]>>,
> =
KOpts extends ODataOptionsWithCount<any>
? number
: T[K] extends NavigationResource<any>
? [TypedResult<InferAssociatedResourceType<T[K]>, KOpts>]
: T[K] extends OptionalNavigationResource<any>
? [TypedResult<InferAssociatedResourceType<T[K]>, KOpts>] | []
: T[K] extends ReverseNavigationResource<any>
? Array<TypedResult<InferAssociatedResourceType<T[K]>, KOpts>>
: never;
export type ExpandResultObject<T, Props extends keyof T> = {
[P in Props]-?: ExpandedProperty<T, P, object>;
};
type ExpandResourceExpandObject<
T,
TResourceExpand extends ResourceExpand<T>,
> = {
[P in keyof TResourceExpand]-?: ExpandedProperty<
T,
P extends keyof T ? P : never,
Exclude<TResourceExpand[P], undefined>
>;
};
export type TypedExpandResult<T, TParams extends ODataOptions<T>> =
TParams['$expand'] extends ExpandableProps<T>
? ExpandResultObject<T, TParams['$expand']>
: TParams['$expand'] extends ResourceExpand<T>
? keyof TParams['$expand'] extends ExpandableProps<T>
? ExpandResourceExpandObject<T, TParams['$expand']>
: never
: object;
export type TypedResult<T, TParams extends ODataOptions<T> | undefined> =
TParams extends ODataOptionsWithCount<T>
? number
: TParams extends ODataOptions<T>
? Omit<
TypedSelectResult<T, TParams>,
keyof TypedExpandResult<T, TParams>
> &
TypedExpandResult<T, TParams>
: undefined extends TParams
? TypedSelectResult<T, { $select: '*' }>
: never;
export type PostResult<T> = SelectResultObject<
T,
Exclude<StringKeyof<T>, PropsOfType<T, ReverseNavigationResource<object>>>
>;
export type WebResource = {
filename: string;
href: string;
content_type?: string;
content_disposition?: string;
size?: number;
};
// based on https://github.com/balena-io/pinejs-client-js/blob/master/core.d.ts
type RawFilter =
| string
| Array<string | Filter<any>>
| { $string: string; [index: string]: Filter<any> | string };
interface LambdaExpression<T> {
[alias: string]: Filter<T>;
}
interface Lambda<T> {
$alias: string;
$expr:
| LambdaExpression<T>
// It seems that TS atm mixes things up after adding the following UNION rules
// and allows having secondary filter props inside the $expr:LambdaExpression<T>
// that are not props of the T
// See: https://github.com/balena-io/balena-sdk/issues/714
| { 1: number }
| { $and?: Array<LambdaExpression<T>> }
| { $or?: Array<LambdaExpression<T>> }
| { $not?: LambdaExpression<T> };
}
type OrderByDirection = 'asc' | 'desc';
type OrderBy<T> =
| `${string} ${OrderByDirection}` // TODO next major: Change to: `${keyof T & string} ${OrderByDirection}` | [keyof T & string, OrderByDirection]
| Array<OrderBy<T>>
| { [k in keyof T]?: OrderByDirection }
| ({
[k in ExpandableProps<T>]?: {
$count: ODataCountOptions<InferAssociatedResourceType<T[k]>>;
};
} & {
$dir: OrderByDirection;
});
type AssociatedResourceFilter<T> =
T extends NonNullable<ReverseNavigationResource<object>>
? FilterObj<InferAssociatedResourceType<T>>
: FilterObj<InferAssociatedResourceType<T>> | number | null;
type ResourceObjFilterPropValue<T, k extends keyof T> =
T[k] extends AssociatedResource<object>
? AssociatedResourceFilter<T[k]>
: T[k] | FilterExpressions<T[k]> | null;
type ResourceObjFilter<T> = {
[k in keyof T]?: ResourceObjFilterPropValue<T, k>;
};
type Filter<T> = FilterObj<T>;
type FilterObj<T> = ResourceObjFilter<T> | FilterExpressions<T>;
type FilterBaseType = string | number | null | boolean | Date;
type NestedFilter<T> = FilterObj<T> | FilterArray<T> | FilterBaseType;
type FilterArray<T> = Array<NestedFilter<T>>;
type FilterOperationValue<T> =
| NestedFilter<T>
| FilterFunctionExpressions<NestedFilter<T>>;
type FilterFunctionValue<T> = NestedFilter<T>;
type FilterFunctionExpressions<T> = Pick<
FilterExpressions<T>,
FilterFunctionKey
>;
type FilterOperationKey =
| '$ne'
| '$eq'
| '$gt'
| '$ge'
| '$lt'
| '$le'
| '$add'
| '$sub'
| '$mul'
| '$div'
| '$mod';
type FilterFunctionKey =
| '$contains'
| '$endswith'
| '$startswith'
| '$length'
| '$indexof'
| '$substring'
| '$tolower'
| '$toupper'
| '$trim'
| '$concat'
| '$year'
| '$month'
| '$day'
| '$hour'
| '$minute'
| '$second'
| '$fractionalseconds'
| '$date'
| '$time'
| '$totaloffsetminutes'
| '$now'
| '$maxdatetime'
| '$mindatetime'
| '$totalseconds'
| '$round'
| '$floor'
| '$ceiling'
| '$isof'
| '$cast';
interface FilterExpressions<T> {
'@'?: string;
$raw?: RawFilter;
$?: string | string[];
$count?: Filter<T>;
$and?: NestedFilter<T>;
$or?: NestedFilter<T>;
$in?: NestedFilter<T>;
$not?: NestedFilter<T>;
$any?: Lambda<T>;
$all?: Lambda<T>;
// Filter operations
$ne?: FilterOperationValue<T>;
$eq?: FilterOperationValue<T>;
$gt?: FilterOperationValue<T>;
$ge?: FilterOperationValue<T>;
$lt?: FilterOperationValue<T>;
$le?: FilterOperationValue<T>;
$add?: FilterOperationValue<T>;
$sub?: FilterOperationValue<T>;
$mul?: FilterOperationValue<T>;
$div?: FilterOperationValue<T>;
$mod?: FilterOperationValue<T>;
// Filter functions
$contains?: FilterFunctionValue<T>;
$endswith?: FilterFunctionValue<T>;
$startswith?: FilterFunctionValue<T>;
$length?: FilterFunctionValue<T>;
$indexof?: FilterFunctionValue<T>;
$substring?: FilterFunctionValue<T>;
$tolower?: FilterFunctionValue<T>;
$toupper?: FilterFunctionValue<T>;
$trim?: FilterFunctionValue<T>;
$concat?: FilterFunctionValue<T>;
$year?: FilterFunctionValue<T>;
$month?: FilterFunctionValue<T>;
$day?: FilterFunctionValue<T>;
$hour?: FilterFunctionValue<T>;
$minute?: FilterFunctionValue<T>;
$second?: FilterFunctionValue<T>;
$fractionalseconds?: FilterFunctionValue<T>;
$date?: FilterFunctionValue<T>;
$time?: FilterFunctionValue<T>;
$totaloffsetminutes?: FilterFunctionValue<T>;
$now?: FilterFunctionValue<T>;
$maxdatetime?: FilterFunctionValue<T>;
$mindatetime?: FilterFunctionValue<T>;
$totalseconds?: FilterFunctionValue<T>;
$round?: FilterFunctionValue<T>;
$floor?: FilterFunctionValue<T>;
$ceiling?: FilterFunctionValue<T>;
$isof?: FilterFunctionValue<T>;
$cast?: FilterFunctionValue<T>;
}
export type ResourceExpand<T> = {
[k in ExpandableProps<T>]?: ODataOptions<InferAssociatedResourceType<T[k]>>;
};
type ResourceExpandWithSelect<T> = {
[k in ExpandableProps<T>]?: ODataOptionsStrict<
InferAssociatedResourceType<T[k]>
>;
};
type BaseExpand<T> = ResourceExpand<T> | ExpandableProps<T>;
export type Expand<T> = BaseExpand<T> | Array<BaseExpand<T>>;
type ExpandWithSelect<T> =
| ResourceExpandWithSelect<T>
| Array<ResourceExpandWithSelect<T>>;
export type ODataMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
export interface ODataOptionsWithoutCount<T> {
$select?: Array<SelectableProps<T>> | SelectableProps<T> | '*';
$filter?: Filter<T>;
$expand?: Expand<T>;
$orderby?: OrderBy<T>;
$top?: number;
$skip?: number;
}
export type ODataCountOptions<T> = Pick<ODataOptionsWithoutCount<T>, '$filter'>;
export interface ODataOptions<T> extends ODataOptionsWithoutCount<T> {
$count?: ODataCountOptions<T>;
}
export interface ODataOptionsWithCount<T> extends ODataOptionsWithoutCount<T> {
$count: NonNullable<ODataCountOptions<T>>;
}
export type ODataOptionsStrict<T> = Omit<
ODataOptions<T>,
'$select' | '$expand' | '$count'
> &
(
| {
$select: ODataOptions<T>['$select'];
$expand?: ExpandWithSelect<T>;
}
| {
$count: ODataOptionsWithoutCount<T>;
}
);
export type ODataOptionsWithFilter<T> = ODataOptions<T> &
Required<Pick<ODataOptions<T>, '$filter'>>;
export type ReplaceWebResource<K> = K extends WebResource ? WebResourceFile : K;
export type SubmitBody<T> = {
[k in keyof T]?: T[k] extends AssociatedResource<object>
? number | null
: ReplaceWebResource<T[k]>;
};
type BaseResourceId =
| string
| number
| Date
| {
'@': string;
};
type ResourceAlternateKey<T> = SubmitBody<T>;
type ResourceId<T> = BaseResourceId | ResourceAlternateKey<T>;
export interface ParamsObj<T> {
apiPrefix?: string;
method?: ODataMethod;
resource?: string;
id?: ResourceId<T>;
url?: string;
body?: SubmitBody<T>;
passthrough?: AnyObject;
passthroughByMethod?: { [method in ODataMethod]: AnyObject };
options?: ODataOptions<T>;
customOptions?: AnyObject;
retry?: RetryParameters;
}
export interface ParamsObjWithId<T> extends ParamsObj<T> {
id: ResourceId<T>;
}
export interface ParamsObjWithCount<T> extends ParamsObj<T> {
options: { $count: NonNullable<ODataOptions<T>['$count']> };
}
export type ParamsObjStrict<T> = Omit<ParamsObj<T>, 'options'> & {
options: ODataOptionsStrict<T>;
};
export interface ParamsObjWithFilter<T> extends ParamsObj<T> {
options: ODataOptionsWithFilter<T>;
}
export interface GetOrCreateParams<T> extends Omit<ParamsObj<T>, 'method'> {
id: ResourceAlternateKey<T>;
resource: string;
body: SubmitBody<T>;
}
export interface UpsertParams<T>
extends Omit<ParamsObj<T>, 'id' | 'method' | 'options'> {
id: ResourceAlternateKey<T>;
resource: string;
body: SubmitBody<T>;
}
export declare type Primitive = null | string | number | boolean | Date;
export declare type ParameterAlias = Primitive;
export declare type PreparedFn<T extends Dictionary<ParameterAlias>, U, R> = (
parameterAliases?: T,
body?: ParamsObj<R>['body'],
passthrough?: ParamsObj<R>['passthrough'],
) => U;
interface PollOnObj {
unsubscribe: () => void;
}
declare class Poll<T> {
private intervalTime;
private subscribers;
private stopped;
private pollInterval?;
private requestFn;
constructor(requestFn: () => Promise<T>, intervalTime?: number);
setPollInterval(intervalTime: number): void;
runRequest(): Promise<void>;
on(name: 'data', fn: (response: Promise<T>) => void): PollOnObj;
on(name: 'error', fn: (err: any) => void): PollOnObj;
start(): void;
stop(): void;
destroy(): void;
private restartTimeout;
}
declare const validParams: readonly [
'apiPrefix',
'passthrough',
'passthroughByMethod',
'retry',
];
export type RetryParametersObj = {
canRetry?: (err: any) => boolean;
onRetry?: (
prevErr: any,
delayMs: number,
attempt: number,
maxAttempts: number,
) => void;
getRetryAfterHeader?: (err: unknown) => string | undefined;
minDelayMs?: number;
maxDelayMs?: number;
maxAttempts?: number;
};
export type RetryParameters = RetryParametersObj | false;
export interface SubscribeParams<T> extends ParamsObj<T> {
method?: 'GET';
pollInterval?: number;
}
export interface SubscribeParamsWithCount<T> extends ParamsObjWithCount<T> {
method?: 'GET';
pollInterval?: number;
}
export interface SubscribeParamsWithId<T> extends ParamsObjWithId<T> {
method?: 'GET';
pollInterval?: number;
}
export type ConstructorParams = Pick<
ParamsObj<unknown>,
(typeof validParams)[number]
>;
export interface Pine<ResourceTypeMap extends object = object> {
apiPrefix: string;
passthrough: AnyObject;
passthroughByMethod: AnyObject;
backendParams?: AnyObject;
retry: RetryParameters;
clone(params: string | ConstructorParams, backendParams?: AnyObject): this;
// Fully typed result overloads
get<
R extends keyof ResourceTypeMap,
P extends { resource: R } & ParamsObjWithCount<
ResourceTypeMap[P['resource']]
>,
>(
params: ExactlyExtends<
P,
ParamsObjWithCount<ResourceTypeMap[P['resource']]>
>,
): Promise<number>;
get<
R extends keyof ResourceTypeMap,
P extends { resource: R } & ParamsObjWithId<ResourceTypeMap[P['resource']]>,
>(
params: ExactlyExtends<P, ParamsObjWithId<ResourceTypeMap[P['resource']]>>,
): Promise<
TypedResult<ResourceTypeMap[P['resource']], P['options']> | undefined
>;
get<
R extends keyof ResourceTypeMap,
P extends { resource: R } & ParamsObj<ResourceTypeMap[P['resource']]>,
>(
params: ExactlyExtends<P, ParamsObj<ResourceTypeMap[P['resource']]>>,
): Promise<Array<TypedResult<ResourceTypeMap[P['resource']], P['options']>>>;
// User provided resource type overloads
get<T extends object>(params: ParamsObjWithCount<T>): Promise<number>;
get<T extends object>(params: ParamsObjWithId<T>): Promise<T | undefined>;
get<T extends object>(params: ParamsObj<T>): Promise<T[]>;
get<T extends object, Result>(params: ParamsObj<T>): Promise<Result>;
patch<T>(params: ParamsObjWithId<T> | ParamsObjWithFilter<T>): Promise<'OK'>;
post<
R extends keyof ResourceTypeMap,
P extends { resource: R } & ParamsObj<ResourceTypeMap[P['resource']]>,
>(
params: ExactlyExtends<P, ParamsObj<ResourceTypeMap[P['resource']]>> & {
body: object;
},
): Promise<PostResult<ResourceTypeMap[P['resource']]>>;
post<T>(
params: ParamsObj<T> & { body: object },
): Promise<PostResult<T & { id: number }>>;
delete<T>(params: ParamsObjWithId<T> | ParamsObjWithFilter<T>): Promise<'OK'>;
getOrCreate<T>(params: GetOrCreateParams<T>): Promise<T>;
upsert<T>(params: UpsertParams<T>): Promise<T | 'OK'>;
prepare<T extends Dictionary<ParameterAlias>, R>(
params: ParamsObjWithCount<R> & {
method?: 'GET';
},
): PreparedFn<T, Promise<number>, R>;
prepare<T extends Dictionary<ParameterAlias>, R>(
params: ParamsObjWithId<R> & {
method?: 'GET';
},
): PreparedFn<T, Promise<R | undefined>, R>;
prepare<T extends Dictionary<ParameterAlias>, R>(
params: ParamsObj<R> & {
method?: 'GET';
},
): PreparedFn<T, Promise<R[]>, R>;
prepare<T extends Dictionary<ParameterAlias>, R>(
params: ParamsObj<R> & {
method: 'POST';
},
): PreparedFn<T, Promise<R & { id: number }>, R>;
prepare<T extends Dictionary<ParameterAlias>, R>(
params: ParamsObj<R> & {
method: 'PATCH' | 'DELETE';
},
): PreparedFn<T, Promise<'OK'>, R>;
compile<R extends keyof ResourceTypeMap>(
params: {
resource: NonNullable<ParamsObj<R>>;
} & ParamsObj<R>,
): string;
subscribe<T>(
params: SubscribeParamsWithCount<T> & {
method?: 'GET';
},
): Poll<number>;
subscribe<T>(
params: SubscribeParamsWithId<T> & {
method?: 'GET';
},
): Poll<T | undefined>;
subscribe<T>(
params: SubscribeParams<T> & {
method?: 'GET';
},
): Poll<T[]>;
subscribe<T>(
params: SubscribeParams<T> & {
method: 'POST';
},
): Poll<T & { id: number }>;
subscribe<T>(
params: SubscribeParams<T> & {
method: 'PATCH' | 'DELETE';
},
): Poll<'OK'>;
}
/**
* A variant that makes $select mandatory, helping to create
* requests that explicitly fetch only what your code needs.
*/
export type PineStrict<ResourceTypeMap extends object = object> = Omit<
Pine,
'get' | 'prepare' | 'subscribe'
> & {
// Fully typed result overloads
get<
R extends keyof ResourceTypeMap,
P extends { resource: R } & ParamsObjWithCount<
ResourceTypeMap[P['resource']]
>,
>(
params: ExactlyExtends<
P,
ParamsObjWithCount<ResourceTypeMap[P['resource']]>
>,
): Promise<number>;
get<
R extends keyof ResourceTypeMap,
P extends { resource: R } & ParamsObjWithId<
ResourceTypeMap[P['resource']]
> &
ParamsObjStrict<ResourceTypeMap[P['resource']]>,
>(
params: ExactlyExtends<
P,
ParamsObjWithId<ResourceTypeMap[P['resource']]> &
ParamsObjStrict<ResourceTypeMap[P['resource']]>
>,
): Promise<
TypedResult<ResourceTypeMap[P['resource']], P['options']> | undefined
>;
get<
R extends keyof ResourceTypeMap,
P extends { resource: R } & ParamsObjStrict<ResourceTypeMap[P['resource']]>,
>(
params: ExactlyExtends<P, ParamsObjStrict<ResourceTypeMap[P['resource']]>>,
): Promise<Array<TypedResult<ResourceTypeMap[P['resource']], P['options']>>>;
// User provided resource type overloads
get<T extends object>(
params: ParamsObjWithCount<NoInfer<T>>,
): Promise<number>;
get<T extends object>(
params: ParamsObjWithId<NoInfer<T>> & ParamsObjStrict<NoInfer<T>>,
): Promise<T | undefined>;
get<T extends object>(params: ParamsObjStrict<NoInfer<T>>): Promise<T[]>;
get<T extends object, Result extends number>(
params: ParamsObj<NoInfer<T>>,
): Promise<Result>;
get<T extends object, Result>(
params: ParamsObjStrict<NoInfer<T>>,
): Promise<Result>;
prepare<T extends Dictionary<ParameterAlias>, R>(
params: ParamsObjWithCount<R> & {
method?: 'GET';
},
): PreparedFn<T, Promise<number>, R>;
prepare<T extends Dictionary<ParameterAlias>, R>(
params: ParamsObjWithId<R> &
ParamsObjStrict<R> & {
method?: 'GET';
},
): PreparedFn<T, Promise<R | undefined>, R>;
prepare<T extends Dictionary<ParameterAlias>, R>(
params: ParamsObj<R> &
ParamsObjStrict<R> & {
method?: 'GET';
},
): PreparedFn<T, Promise<R[]>, R>;
prepare<T extends Dictionary<ParameterAlias>, R>(
params: ParamsObj<R> & {
method: 'POST';
},
): PreparedFn<T, Promise<R & { id: number }>, R>;
prepare<T extends Dictionary<ParameterAlias>, R>(
params: ParamsObj<R> & {
method: 'PATCH' | 'DELETE';
},
): PreparedFn<T, Promise<'OK'>, R>;
subscribe<T>(
params: SubscribeParamsWithCount<T> & {
method?: 'GET';
},
): Poll<number>;
subscribe<T>(
params: SubscribeParamsWithId<T> &
ParamsObjStrict<T> & {
method?: 'GET';
},
): Poll<T | undefined>;
subscribe<T>(
params: SubscribeParams<T> &
ParamsObjStrict<T> & {
method?: 'GET';
},
): Poll<T[]>;
subscribe<T>(
params: SubscribeParams<T> & {
method: 'POST';
},
): Poll<T & { id: number }>;
subscribe<T>(
params: SubscribeParams<T> & {
method: 'PATCH' | 'DELETE';
},
): Poll<'OK'>;
};