Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update pinejs-client-core to add support for using model specific typings #23

Merged
merged 1 commit into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,18 @@
"devDependencies": {
"@balena/lint": "^8.0.2",
"husky": "^9.0.11",
"lint-staged": "^15.2.6",
"lint-staged": "^15.2.7",
"typescript": "^5.4.5"
},
"dependencies": {
"@balena/abstract-sql-to-typescript": "^3.2.1",
"@types/chai": "^4.3.16",
"@types/chai-as-promised": "^7.1.8",
"@types/express": "^4.17.21",
"@types/supertest": "^6.0.2",
"chai": "^4.4.1",
"chai-as-promised": "^7.1.2",
"pinejs-client-core": "^6.14.13",
"pinejs-client-core": "^6.15.2",
"supertest": "^7.0.0"
},
"versionist": {
Expand Down
268 changes: 216 additions & 52 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,37 @@
import type { Params } from 'pinejs-client-core';
import type {
AnyObject,
AnyResource,
ConstructorParams,
ODataOptions,
Params,
PromiseResultTypes,
Resource,
RetryParameters,
} from 'pinejs-client-core';
import { PinejsClientCore } from 'pinejs-client-core';
import type * as express from 'express';
import type { CallbackHandler, Response, Test } from 'supertest';
import supertest from './supertest';
import { expect } from './chai';
import type { UserParam, Overwrite } from './common';
import type {
PickDeferred,
PickExpanded,
} from '@balena/abstract-sql-to-typescript';

type StringKeyOf<T> = keyof T & string;
type SelectPropsOf<T extends Resource['Read'], U extends ODataOptions<T>> =
U['$select'] extends ReadonlyArray<StringKeyOf<T>>
? U['$select'][number]
: U['$select'] extends StringKeyOf<T>
? U['$select']
: never;
type ExpandPropsOf<T extends Resource['Read'], U extends ODataOptions<T>> =
U['$expand'] extends ReadonlyArray<StringKeyOf<T>>
? U['$expand'][number]
: U['$expand'] extends { [key in StringKeyOf<T>]?: any }
? StringKeyOf<U['$expand']>
: never;

Comment on lines +32 to +34

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't in this case expand always an array?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, this is the case where $expand did not match any type from which we can infer the expanded keys so it is a never case - it should not happen and we type it like this to let typescript know that (and if it does happen because someone has gone outside of the typings or similar then typescript will error)

type supportedMethod = 'get' | 'put' | 'patch' | 'post' | 'delete';

Expand Down Expand Up @@ -45,17 +72,140 @@ interface BackendParams {
app: express.Express | string;
}

type Super = PinejsClientCore<unknown>;

/** A pine testing client fused with the api of supertest */
export class PineTest extends PinejsClientCore<unknown> {
export class PineTest<
Model extends {
[key in keyof Model]: Resource;
} = {
[key in string]: AnyResource;
},
> extends PinejsClientCore<unknown, Model> {
constructor(
params: Params,
params: ConstructorParams,
public backendParams: BackendParams,
) {
super(params);
}

public get<
TResource extends StringKeyOf<Model>,
TParams extends Params<Model[TResource]> & {
options: {
$count: NonNullable<ODataOptions<Model[TResource]['Read']>['$count']>;
};
},
>(
params: {
resource: TResource;
} & TParams,
): PromiseResult<number>;
public get<
TResource extends StringKeyOf<Model>,
TParams extends Params<Model[TResource]> & {
id: NonNullable<Params<Model[TResource]>['id']>;
options: NonNullable<
Params<Model[TResource]>['options'] &
(
| {
$select: NonNullable<
NonNullable<Params<Model[TResource]>['options']>['$select']
>;
}
| {
$expand: NonNullable<
NonNullable<Params<Model[TResource]>['options']>['$expand']
>;
}
)
>;
},
>(
params: {
resource: TResource;
} & TParams,
): PromiseResult<
| NoInfer<
PickDeferred<
Model[TResource]['Read'],
SelectPropsOf<
Model[TResource]['Read'],
NonNullable<TParams['options']>
>
> &
PickExpanded<
Model[TResource]['Read'],
ExpandPropsOf<
Model[TResource]['Read'],
NonNullable<TParams['options']>
>
>
>
| undefined
>;
public get<
TResource extends StringKeyOf<Model>,
TParams extends Params<Model[TResource]> & {
id: NonNullable<Params<Model[TResource]>['id']>;
resource: TResource;
},
>(
params: { resource: TResource } & TParams,
): PromiseResult<NoInfer<Model[TResource]['Read']> | undefined>;
public get<
TResource extends StringKeyOf<Model>,
TParams extends Omit<Params<Model[TResource]>, 'id'> & {
resource: TResource;
options: NonNullable<
Params<Model[TResource]>['options'] &
(
| {
$select: NonNullable<
NonNullable<Params<Model[TResource]>['options']>['$select']
>;
}
| {
$expand: NonNullable<
NonNullable<Params<Model[TResource]>['options']>['$expand']
>;
}
)
>;
},
>(
params: { resource: TResource } & TParams,
): PromiseResult<
NoInfer<
Array<
PickDeferred<
Model[TResource]['Read'],
SelectPropsOf<
Model[TResource]['Read'],
NonNullable<TParams['options']>
>
> &
PickExpanded<
Model[TResource]['Read'],
ExpandPropsOf<
Model[TResource]['Read'],
NonNullable<TParams['options']>
>
>
>
>
>;
public get<TResource extends StringKeyOf<Model>>(
params: { resource: TResource } & Omit<Params<Model[TResource]>, 'id'>,
): PromiseResult<NoInfer<Array<Model[TResource]['Read']>>>;
/**
* @deprecated GETing via `url` is deprecated
*/
public get<T extends Resource = AnyResource>(
params: {
resource?: undefined;
url: NonNullable<Params<T>['url']>;
} & Params<T>,
): PromiseResult<PromiseResultTypes>;
public get<T = any>(params: Params): PromiseResult<T>;
public get<T = any>(params: Params): PromiseResult<T> {
// Use a different const, since if we just re-assign `params`
// inside the `expect(() => {})` TS forgets that it's ensured
Expand Down Expand Up @@ -83,72 +233,84 @@ export class PineTest extends PinejsClientCore<unknown> {
}) as PromiseResult<T>;
}

public put(
params: { resource: NonNullable<Params['resource']> } & Params,
): PromiseResult<ResolvableReturnType<Super['put']>>;
public put<TResource extends StringKeyOf<Model>>(
params: { resource: TResource; url?: undefined } & Params<Model[TResource]>,
): PromiseResult<void>;
/**
* @deprecated PUTing via `url` is deprecated
*/
public put(
params: { resource?: undefined; url: NonNullable<Params['url']> } & Params,
): PromiseResult<ResolvableReturnType<Super['put']>>;
public put(
params: Params,
): PromiseResult<ResolvableReturnType<Super['put']>> {
return super.put(params as Parameters<Super['put']>[0]) as PromiseResult<
ResolvableReturnType<Super['put']>
public put<T extends Resource = AnyResource>(
params: {
resource?: undefined;
url: NonNullable<Params<T>['url']>;
} & Params<T>,
): PromiseResult<void>;
public put(params: Params<AnyResource>): PromiseResult<void> {
return super.put(
params as Parameters<PinejsClientCore<unknown, Model>['put']>[0],
) as PromiseResult<
ResolvableReturnType<PinejsClientCore<unknown, Model>['put']>
>;
}

public patch(
params: { resource: NonNullable<Params['resource']> } & Params,
): PromiseResult<ResolvableReturnType<Super['patch']>>;
public patch<TResource extends StringKeyOf<Model>>(
params: { resource: TResource; url?: undefined } & Params<Model[TResource]>,
): PromiseResult<void>;
/**
* @deprecated PATCHing via `url` is deprecated
*/
public patch(
params: { resource?: undefined; url: NonNullable<Params['url']> } & Params,
): PromiseResult<ResolvableReturnType<Super['patch']>>;
public patch(
params: Params,
): PromiseResult<ResolvableReturnType<Super['patch']>> {
public patch<T extends Resource = AnyResource>(
params: {
resource?: undefined;
url: NonNullable<Params<T>['url']>;
} & Params<T>,
): PromiseResult<void>;
public patch(params: Params<AnyResource>): PromiseResult<void> {
return super.patch(
params as Parameters<Super['patch']>[0],
) as PromiseResult<ResolvableReturnType<Super['patch']>>;
params as Parameters<PinejsClientCore<unknown, Model>['patch']>[0],
) as PromiseResult<
ResolvableReturnType<PinejsClientCore<unknown, Model>['patch']>
>;
}

public post(
params: { resource: NonNullable<Params['resource']> } & Params,
): PromiseResult<ResolvableReturnType<Super['post']>>;
public post<TResource extends StringKeyOf<Model>>(
params: { resource: TResource } & Params<Model[TResource]>,
): PromiseResult<PickDeferred<Model[TResource]['Read']>>;
/**
* @deprecated POSTing via `url` is deprecated
*/
public post(
params: { resource?: undefined; url: NonNullable<Params['url']> } & Params,
): PromiseResult<ResolvableReturnType<Super['post']>>;
public post(
params: Params,
): PromiseResult<ResolvableReturnType<Super['post']>> {
return super.post(params as Parameters<Super['post']>[0]) as PromiseResult<
ResolvableReturnType<Super['post']>
public post<T extends Resource = AnyResource>(
params: {
resource?: undefined;
url: NonNullable<Params<T>['url']>;
} & Params<T>,
): PromiseResult<AnyObject>;
public post(params: Params<AnyResource>): PromiseResult<AnyObject> {
return super.post(
params as Parameters<PinejsClientCore<unknown, Model>['post']>[0],
) as PromiseResult<
ResolvableReturnType<PinejsClientCore<unknown, Model>['post']>
>;
}

public delete(
params: { resource: NonNullable<Params['resource']> } & Params,
): PromiseResult<ResolvableReturnType<Super['delete']>>;
public delete<TResource extends StringKeyOf<Model>>(
params: { resource: TResource } & Params<Model[TResource]>,
): PromiseResult<void>;
/**
* @deprecated DELETEing via `url` is deprecated
*/
public delete(
params: { resource?: undefined; url: NonNullable<Params['url']> } & Params,
): PromiseResult<ResolvableReturnType<Super['delete']>>;
public delete(
params: Params,
): PromiseResult<ResolvableReturnType<Super['delete']>> {
public delete<T extends Resource = AnyResource>(
params: {
resource?: undefined;
url: NonNullable<Params<T>['url']>;
} & Params<T>,
): PromiseResult<void>;
public delete(params: Params<AnyResource>): PromiseResult<void> {
return super.delete(
params as Parameters<Super['delete']>[0],
) as PromiseResult<ResolvableReturnType<Super['delete']>>;
params as Parameters<PinejsClientCore<unknown, Model>['delete']>[0],
) as PromiseResult<
ResolvableReturnType<PinejsClientCore<unknown, Model>['delete']>
>;
}
Comment on lines +311 to +313

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
) as PromiseResult<
ResolvableReturnType<PinejsClientCore<unknown, Model>['delete']>
>;
) as PromiseResult<void>;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type of the result comes from the return type of PinejsClientCore<unknown, Model>['delete'] so the current form is accurate to what it will actually be, yes we expect that to resolve to PromiseResult<void> but if it does not for some reason (eg pinejs-client-core changes the return type) then typescript would flag it up as an error so we'd get a compile time warning with the current version but not with the as PromiseResult<void> version


public upsert(): never {
Expand All @@ -159,13 +321,15 @@ export class PineTest extends PinejsClientCore<unknown> {
throw new Error('getOrCreate is not supported by pinejs-client-supertest');
}

public request(...args: Parameters<Super['request']>): PromiseResult<any> {
public request(
...args: Parameters<PinejsClientCore<unknown, Model>['request']>
): PromiseResult<any> {
return super.request(...args) as PromiseResult<any>;
}

protected callWithRetry<T>(
fnCall: () => Promise<T>,
retry?: Params['retry'],
retry?: RetryParameters,
): Promise<T> {
if ((retry ?? this.retry) === false) {
return fnCall();
Expand All @@ -181,7 +345,7 @@ export class PineTest extends PinejsClientCore<unknown> {
}: {
method: supportedMethod;
url: string;
body: Params['body'];
body: AnyObject;
user?: UserParam;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for understanding:

  • this changes from Partial<T['Write']> to AnyObject
  • which comes from
export type AnyResource = {
    Read: AnyObject;
    Write: AnyObject;
};

So it's the same :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are the same, but more notably the pinejs-client-core typings for this actually specify body: AnyObject so this version matches that, but I've now updated it to directly take the typings from the parent class outside of the user extension

}) {
const { app } = this.backendParams;
Expand Down
Loading