Skip to content
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
2 changes: 1 addition & 1 deletion packages/api-client-core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@gadgetinc/api-client-core",
"version": "0.15.38",
"version": "0.15.39",
"files": [
"Readme.md",
"dist/**/*"
Expand Down
13 changes: 13 additions & 0 deletions packages/api-client-core/spec/GadgetRecord.spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { AnyPublicModelManager } from "../src/AnyModelManager.js";
import { ChangeTracking, GadgetRecord } from "../src/GadgetRecord.js";
interface SampleBaseRecord {
id?: string;
Expand Down Expand Up @@ -38,6 +39,8 @@ const expectPersistedChanges = (record: GadgetRecord<SampleBaseRecord>, ...prope
return _expectChanges(record, ChangeTracking.SinceLastPersisted, ...properties);
};

const mockModelManager: AnyPublicModelManager = {} as any;

describe("GadgetRecord", () => {
let productBaseRecord: SampleBaseRecord;
beforeAll(() => {
Expand All @@ -48,6 +51,16 @@ describe("GadgetRecord", () => {
};
});

it("can be constructed with a base record and no model manager for backwards compatibility", () => {
const product = new GadgetRecord<SampleBaseRecord>(productBaseRecord);
expect(product.id).toEqual("123");
expect(product.name).toEqual("A cool product");
});

it("can be constructed with a base record and a model manager", () => {
new GadgetRecord<SampleBaseRecord>(productBaseRecord, mockModelManager);
});

it("should respond toJSON, which returns the inner __gadget.fields properties", () => {
const product = new GadgetRecord<SampleBaseRecord>(productBaseRecord);
expect(product.toJSON()).toEqual({
Expand Down
134 changes: 55 additions & 79 deletions packages/api-client-core/spec/operationRunners.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { diff } from "@n1ru4l/json-patch-plus";
import { CombinedError } from "@urql/core";
import nock from "nock";
import { BackgroundActionHandle } from "../src/BackgroundActionHandle.js";
import type { AnyModelManager, GadgetErrorGroup, LimitToKnownKeys } from "../src/index.js";
import type { AnyPublicModelManager, GadgetErrorGroup, LimitToKnownKeys } from "../src/index.js";
import {
GadgetConnection,
actionRunner,
Expand Down Expand Up @@ -48,6 +48,7 @@ describe("type checks", () => {
// eslint-disable-next-line jest/no-export
describe("operationRunners", () => {
let connection: GadgetConnection;
let manager: AnyPublicModelManager;
let query: string | undefined;
let mockUrqlClient: MockUrqlClient;

Expand All @@ -60,25 +61,26 @@ describe("operationRunners", () => {
},
});
jest.spyOn(connection, "currentClient" as any, "get").mockReturnValue(mockUrqlClient as any);
manager = { connection } as AnyPublicModelManager;
});

describe("findOneRunner", () => {
test("can execute a findOne operation against a model", async () => {
const promise = findOneRunner({ connection }, "widget", "123", { id: true, name: true }, "widget");

expect(query).toMatchInlineSnapshot(`
"query widget($id: GadgetID!) {
widget(id: $id) {
id
name
__typename
}
gadgetMeta {
hydrations(modelName:
"widget")
}
}"
`);
"query widget($id: GadgetID!) {
widget(id: $id) {
id
name
__typename
}
gadgetMeta {
hydrations(modelName:
"widget")
}
}"
`);

mockUrqlClient.executeQuery.pushResponse("widget", {
data: {
Expand Down Expand Up @@ -316,32 +318,32 @@ describe("operationRunners", () => {

describe("findManyRunner", () => {
test("can execute a findMany operation against a model", async () => {
const promise = findManyRunner({ connection } as AnyModelManager, "widgets", { id: true, name: true }, "widget");
const promise = findManyRunner({ connection } as AnyPublicModelManager, "widgets", { id: true, name: true }, "widget");

expect(query).toMatchInlineSnapshot(`
"query widgets($after: String, $first: Int, $before: String, $last: Int) {
widgets(after: $after, first: $first, before: $before, last: $last) {
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
edges {
cursor
node {
id
name
__typename
}
}
}
gadgetMeta {
hydrations(modelName:
"widget")
}
}"
`);
"query widgets($after: String, $first: Int, $before: String, $last: Int) {
widgets(after: $after, first: $first, before: $before, last: $last) {
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
edges {
cursor
node {
id
name
__typename
}
}
}
gadgetMeta {
hydrations(modelName:
"widget")
}
}"
`);

mockUrqlClient.executeQuery.pushResponse("widgets", {
data: {
Expand Down Expand Up @@ -369,7 +371,7 @@ describe("operationRunners", () => {

test("can execute a findMany operation against a namespaced model", async () => {
const promise = findManyRunner(
{ connection } as AnyModelManager,
{ connection } as AnyPublicModelManager,
"widgets",
{ id: true, name: true },
"widget",
Expand Down Expand Up @@ -436,7 +438,7 @@ describe("operationRunners", () => {

test("can execute a findMany operation against a namespaced model when the namespace is a string", async () => {
const promise = findManyRunner(
{ connection } as AnyModelManager,
{ connection } as AnyPublicModelManager,
"widgets",
{ id: true, name: true },
"widget",
Expand Down Expand Up @@ -502,9 +504,7 @@ describe("operationRunners", () => {
describe("actionRunner", () => {
test("can run a single create action", async () => {
const promise = actionRunner<{ id: string; name: string }>(
{
connection,
},
manager,
"createWidget",
{ id: true, name: true },
"widget",
Expand Down Expand Up @@ -544,9 +544,7 @@ describe("operationRunners", () => {

test("can run a single update action", async () => {
const promise = actionRunner<{ id: string; name: string }>(
{
connection,
},
manager,
"updateWidget",
{ id: true, name: true },
"widget",
Expand Down Expand Up @@ -591,9 +589,7 @@ describe("operationRunners", () => {

test("can run a single action with an object result type", async () => {
const promise = actionRunner(
{
connection,
},
manager,
"upsertWidget",
{ id: true, name: true, eventAt: true },
"widget",
Expand Down Expand Up @@ -646,9 +642,7 @@ describe("operationRunners", () => {

test("can run a single action with an object result type that has an inner return type", async () => {
const promise = actionRunner(
{
connection,
},
manager,
"upsertWidget",
{ id: true, name: true, eventAt: true },
"widget",
Expand Down Expand Up @@ -693,9 +687,7 @@ describe("operationRunners", () => {

test("can run an action with hasReturnType", async () => {
const promise = actionRunner(
{
connection,
},
manager,
"createWidget",
{ id: true, name: true },
"widget",
Expand Down Expand Up @@ -733,9 +725,7 @@ describe("operationRunners", () => {

test("can throw the error returned by the server for a single action", async () => {
const promise = actionRunner<{ id: string; name: string }>(
{
connection,
},
manager,
"updateWidget",
{ id: true, name: true },
"widget",
Expand Down Expand Up @@ -780,9 +770,7 @@ describe("operationRunners", () => {

test("can run a bulk action by ids", async () => {
const promise = actionRunner<{ id: string; name: string }>(
{
connection,
},
manager,
"bulkFlipWidgets",
{ id: true, name: true },
"widget",
Expand Down Expand Up @@ -830,9 +818,7 @@ describe("operationRunners", () => {

test("can run a bulk action with params", async () => {
const promise = actionRunner<{ id: string; name: string }>(
{
connection,
},
manager,
"bulkCreateWidgets",
{ id: true, name: true },
"widget",
Expand Down Expand Up @@ -880,9 +866,7 @@ describe("operationRunners", () => {

test("can run a bulk action with a returnType", async () => {
const promise = actionRunner(
{
connection,
},
manager,
"bulkCreateWidgets",
{ id: true, name: true },
"widget",
Expand Down Expand Up @@ -921,9 +905,7 @@ describe("operationRunners", () => {

test("can run a bulk action with an object returnType", async () => {
const promise = actionRunner(
{
connection,
},
manager,
"bulkUpsertWidgets",
{ id: true, name: true },
"widget",
Expand Down Expand Up @@ -970,9 +952,7 @@ describe("operationRunners", () => {

test("throws a nice error when a bulk action returns errors", async () => {
const promise = actionRunner<{ id: string; name: string }>(
{
connection,
},
manager,
"bulkCreateWidgets",
{ id: true, name: true },
"widget",
Expand Down Expand Up @@ -1010,9 +990,7 @@ describe("operationRunners", () => {

test("throws a nice error when a bulk action returns errors and data", async () => {
const promise = actionRunner<{ id: string; name: string }>(
{
connection,
},
manager,
"bulkCreateWidgets",
{ id: true, name: true },
"widget",
Expand Down Expand Up @@ -1057,9 +1035,7 @@ describe("operationRunners", () => {

test("returns undefined when bulk action does not have a result", async () => {
const promise = actionRunner<{ id: string; name: string }>(
{
connection,
},
manager,
"bulkDeleteWidgets",
null,
"widget",
Expand Down Expand Up @@ -1958,7 +1934,7 @@ describe("operationRunners", () => {
test("can run a live findMany", async () => {
const iterator = asyncIterableToIterator(
findManyRunner<{ id: string; name: string }, { live: true }>(
{ connection } as AnyModelManager,
{ connection } as AnyPublicModelManager,
"widgets",
{ id: true, name: true },
"widget",
Expand Down
61 changes: 61 additions & 0 deletions packages/api-client-core/src/AnyModelManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { GadgetConnection } from "./GadgetConnection.js";
import type { FindFirstFunction, FindManyFunction, FindOneFunction, GetFunction } from "./GadgetFunctions.js";
import type { GadgetRecord } from "./GadgetRecord.js";
import type { InternalModelManager } from "./InternalModelManager.js";

export type AnyModelFinderMetadata = {
/** The name of the GraphQL API field that should be called for this operation */
operationName: string;
/** The model's api identifier */
modelApiIdentifier: string;
/** What fields to select from the GraphQL API if no explicit selection is passed */
defaultSelection: Record<string, any>;
/** A namespace this operation is nested in. Absent for old clients or root-namespaced operations */
namespace?: string | string[] | null;
/** Type-time only type member used for strong typing of finders */
selectionType: any;
/** Type-time only type member used for strong typing of finders */
optionsType: any;
/** Type-time only type member used for strong typing of finders */
schemaType: any | null;
};

export type AnyFindOneFunc = FindOneFunction<any, any, any, any>;
export type AnyFindManyFunc = FindManyFunction<any, any, any, any>;
export type AnyFindFirstFunc = FindFirstFunction<any, any, any, any>;

/**
* The manager class for a given model that uses the Public API, like `api.post` or `api.user`
**/
export interface AnyPublicModelManager<
FindOneFunc extends AnyFindOneFunc = AnyFindOneFunc,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is kind of unfortunate but addresses an issue with useSession and useAuth typing. For both these react hooks we accept a type that is:

export type ClientWithSessionAndUserManagers<SessionGivenOptions, SessionSchemaT, UserGivenOptions, UserSchemaT> = AnyClient & {
  currentSession: { get: GetFunction<SessionGivenOptions, any, SessionSchemaT, any> };
  user: { findMany: FindManyFunction<UserGivenOptions, any, UserSchemaT, any> };
};

Client extends ClientWithSessionAndUserManagers<SessionGivenOptions, SessionSchemaT, UserGivenOptions, UserSchemaT>,
ClientType extends Client | undefined

Which scopes the client down to one that has both user findMany function and a session with a get function. This is narrowing the type of the ModelManager and the resulting object is then typed to be a GadgetRecord based on the input client/model manager. In order to make it play nicely with hydration and keeping the reference to the model manager I want to also add the AnyPublicModelManager or AnyPublicSingletonModelManager interface too. I had changed it to be:

export type ClientWithSessionAndUserManagers<SessionGivenOptions, SessionSchemaT, UserGivenOptions, UserSchemaT> = AnyClient & {
  currentSession: { get: GetFunction<SessionGivenOptions, any, SessionSchemaT, any> } & AnyPublicSingletonModelManager;
  user: { findMany: FindManyFunction<UserGivenOptions, any, UserSchemaT, any> } & AnyPublicModelManager;
};

however this leads to an infinite typing/complexity error getting the output GadgetRecord type. To address this I have added these optional generic typings to allow the function typing to be passed through. This way the get and findMany function types can be passed through and the resulting type can be inferred again.

Copy link
Contributor

Choose a reason for hiding this comment

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

Awkward but this makes sense

FindManyFunc extends AnyFindManyFunc = AnyFindManyFunc,
FindFirstFunc extends AnyFindFirstFunc = AnyFindFirstFunc
> {
connection: GadgetConnection;
findOne: FindOneFunc;
findMany: FindManyFunc;
findFirst: FindFirstFunc;
maybeFindFirst(options: any): Promise<GadgetRecord<any> | null>;
maybeFindOne(id: string, options: any): Promise<GadgetRecord<any> | null>;
}

/**
* The manager class for a given single model that uses the Public API, like `api.session`
**/
export interface AnyPublicSingletonModelManager<GetFunc extends GetFunction<any, any, any, any> = GetFunction<any, any, any, any>> {
connection: GadgetConnection;
get: GetFunc;
}

/**
* Prior to 1.1 actions were defined to accept just a connection
*/
export interface AnyLegacyModelManager {
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is to address a test case inside operationRunner that asserts before 1.1 actions were typed with just a { connection: GadgetConnection }. I'm not fully aware if this still needs to be supported, its not the end of the world as we can check the typing inside the record when adding the additional methods.

connection: GadgetConnection;
}

/**
* Any model manager, either public or internal
*/
export type AnyModelManager = AnyPublicModelManager | AnyPublicSingletonModelManager | AnyLegacyModelManager | InternalModelManager;
Loading