Skip to content

Reapply "Give records a reference to the model manager which loaded t… #813

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

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
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,
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 {
connection: GadgetConnection;
}

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