Skip to content

Commit d6e6704

Browse files
committed
Give records a reference to the model manager which loaded them
We have a few upcoming things that require records to be able to do stuff to themselves -- reload, select new fields from the backend, maybe `record.save` or similar, and I suspect more in the future. I still think they should act mostly as DTOs that don't have a lot of business logic on them, but if we want to be able to even know what model they are for, we need a reference to the thing that produced them! This passes along the model manager which instantiated a record to the record in both imperative API land and in React land. In order to make this change work in React land, we need a new little bit of metadata exported from the generated client. Our React hooks get passed functions from the api client like `useAction(api.post.create)`, so we need to be able to hop back from the `create` function that we get by reference to the `api.post` object. The generated client will need to decorate the `create` function with a reference, which is not super hard, but means that we have a backwards compatibility issue. `@gadgetinc/react` can't assume that it is upgraded at the same time as the api client, so it can't assume it is working against a newly generated api client that has this metadata. For this reason, the model manager property on `GadgetRecord` is optional, which reflects the way it will be used in the real world without necessarily having that metadata available. When we go to build things like `record.reload()`, we can make that fail at runtime with a message saying "regenerate your client to get this to work!", instead of just assuming it is present.
1 parent b689df5 commit d6e6704

25 files changed

+216
-128
lines changed

packages/api-client-core/spec/GadgetRecord.spec.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { AnyPublicModelManager } from "../src/AnyModelManager.js";
12
import { ChangeTracking, GadgetRecord } from "../src/GadgetRecord.js";
23
interface SampleBaseRecord {
34
id?: string;
@@ -38,6 +39,8 @@ const expectPersistedChanges = (record: GadgetRecord<SampleBaseRecord>, ...prope
3839
return _expectChanges(record, ChangeTracking.SinceLastPersisted, ...properties);
3940
};
4041

42+
const mockModelManager: AnyPublicModelManager = {} as any;
43+
4144
describe("GadgetRecord", () => {
4245
let productBaseRecord: SampleBaseRecord;
4346
beforeAll(() => {
@@ -48,6 +51,18 @@ describe("GadgetRecord", () => {
4851
};
4952
});
5053

54+
it("can be constructed with a base record and no model manager for backwards compatibility", () => {
55+
const product = new GadgetRecord<SampleBaseRecord>(productBaseRecord);
56+
expect(product.id).toEqual("123");
57+
expect(product.name).toEqual("A cool product");
58+
expect(product.modelManager).toEqual(null);
59+
});
60+
61+
it("can be constructed with a base record and a model manager", () => {
62+
const product = new GadgetRecord<SampleBaseRecord>(productBaseRecord, mockModelManager);
63+
expect(product.modelManager).toEqual(mockModelManager);
64+
});
65+
5166
it("should respond toJSON, which returns the inner __gadget.fields properties", () => {
5267
const product = new GadgetRecord<SampleBaseRecord>(productBaseRecord);
5368
expect(product.toJSON()).toEqual({

packages/api-client-core/spec/operationRunners.spec.ts

Lines changed: 28 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { diff } from "@n1ru4l/json-patch-plus";
44
import { CombinedError } from "@urql/core";
55
import nock from "nock";
66
import { BackgroundActionHandle } from "../src/BackgroundActionHandle.js";
7-
import type { AnyModelManager, GadgetErrorGroup, LimitToKnownKeys } from "../src/index.js";
7+
import type { AnyPublicModelManager, GadgetErrorGroup, LimitToKnownKeys } from "../src/index.js";
88
import {
99
GadgetConnection,
1010
actionRunner,
@@ -30,7 +30,7 @@ describe("type checks", () => {
3030
options?: LimitToKnownKeys<Options, CreatePostOptions>
3131
): Promise<CreatePostResult<Options>> {
3232
return await actionRunner<SelectedPostOrDefault<Options>>(
33-
this,
33+
this, // This is a problem...
3434
"createPost",
3535
DefaultPostSelection,
3636
"post",
@@ -48,6 +48,7 @@ describe("type checks", () => {
4848
// eslint-disable-next-line jest/no-export
4949
describe("operationRunners", () => {
5050
let connection: GadgetConnection;
51+
let manager: AnyPublicModelManager;
5152
let query: string | undefined;
5253
let mockUrqlClient: MockUrqlClient;
5354

@@ -60,6 +61,7 @@ describe("operationRunners", () => {
6061
},
6162
});
6263
jest.spyOn(connection, "currentClient" as any, "get").mockReturnValue(mockUrqlClient as any);
64+
manager = { connection } as AnyPublicModelManager;
6365
});
6466

6567
describe("findOneRunner", () => {
@@ -74,7 +76,7 @@ describe("operationRunners", () => {
7476
__typename
7577
}
7678
gadgetMeta {
77-
hydrations(modelName:
79+
hydrations(modelName:
7880
"widget")
7981
}
8082
}"
@@ -114,7 +116,7 @@ describe("operationRunners", () => {
114116
}
115117
}
116118
gadgetMeta {
117-
hydrations(modelName:
119+
hydrations(modelName:
118120
"outer.inner.widget")
119121
}
120122
}"
@@ -170,7 +172,7 @@ describe("operationRunners", () => {
170172
}
171173
}
172174
gadgetMeta {
173-
hydrations(modelName:
175+
hydrations(modelName:
174176
"widget")
175177
}
176178
}"
@@ -240,7 +242,7 @@ describe("operationRunners", () => {
240242
}
241243
}
242244
gadgetMeta {
243-
hydrations(modelName:
245+
hydrations(modelName:
244246
"outer.inner.widget")
245247
}
246248
}"
@@ -316,7 +318,7 @@ describe("operationRunners", () => {
316318

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

321323
expect(query).toMatchInlineSnapshot(`
322324
"query widgets($after: String, $first: Int, $before: String, $last: Int) {
@@ -337,7 +339,7 @@ describe("operationRunners", () => {
337339
}
338340
}
339341
gadgetMeta {
340-
hydrations(modelName:
342+
hydrations(modelName:
341343
"widget")
342344
}
343345
}"
@@ -369,7 +371,7 @@ describe("operationRunners", () => {
369371

370372
test("can execute a findMany operation against a namespaced model", async () => {
371373
const promise = findManyRunner(
372-
{ connection } as AnyModelManager,
374+
{ connection } as AnyPublicModelManager,
373375
"widgets",
374376
{ id: true, name: true },
375377
"widget",
@@ -400,7 +402,7 @@ describe("operationRunners", () => {
400402
}
401403
}
402404
gadgetMeta {
403-
hydrations(modelName:
405+
hydrations(modelName:
404406
"outer.inner.widget")
405407
}
406408
}"
@@ -436,7 +438,7 @@ describe("operationRunners", () => {
436438

437439
test("can execute a findMany operation against a namespaced model when the namespace is a string", async () => {
438440
const promise = findManyRunner(
439-
{ connection } as AnyModelManager,
441+
{ connection } as AnyPublicModelManager,
440442
"widgets",
441443
{ id: true, name: true },
442444
"widget",
@@ -466,7 +468,7 @@ describe("operationRunners", () => {
466468
}
467469
}
468470
gadgetMeta {
469-
hydrations(modelName:
471+
hydrations(modelName:
470472
"outer.widget")
471473
}
472474
}"
@@ -502,9 +504,7 @@ describe("operationRunners", () => {
502504
describe("actionRunner", () => {
503505
test("can run a single create action", async () => {
504506
const promise = actionRunner<{ id: string; name: string }>(
505-
{
506-
connection,
507-
},
507+
manager,
508508
"createWidget",
509509
{ id: true, name: true },
510510
"widget",
@@ -544,9 +544,7 @@ describe("operationRunners", () => {
544544

545545
test("can run a single update action", async () => {
546546
const promise = actionRunner<{ id: string; name: string }>(
547-
{
548-
connection,
549-
},
547+
manager,
550548
"updateWidget",
551549
{ id: true, name: true },
552550
"widget",
@@ -591,9 +589,7 @@ describe("operationRunners", () => {
591589

592590
test("can run a single action with an object result type", async () => {
593591
const promise = actionRunner(
594-
{
595-
connection,
596-
},
592+
manager,
597593
"upsertWidget",
598594
{ id: true, name: true, eventAt: true },
599595
"widget",
@@ -646,9 +642,7 @@ describe("operationRunners", () => {
646642

647643
test("can run a single action with an object result type that has an inner return type", async () => {
648644
const promise = actionRunner(
649-
{
650-
connection,
651-
},
645+
manager,
652646
"upsertWidget",
653647
{ id: true, name: true, eventAt: true },
654648
"widget",
@@ -693,9 +687,7 @@ describe("operationRunners", () => {
693687

694688
test("can run an action with hasReturnType", async () => {
695689
const promise = actionRunner(
696-
{
697-
connection,
698-
},
690+
manager,
699691
"createWidget",
700692
{ id: true, name: true },
701693
"widget",
@@ -733,9 +725,7 @@ describe("operationRunners", () => {
733725

734726
test("can throw the error returned by the server for a single action", async () => {
735727
const promise = actionRunner<{ id: string; name: string }>(
736-
{
737-
connection,
738-
},
728+
manager,
739729
"updateWidget",
740730
{ id: true, name: true },
741731
"widget",
@@ -780,9 +770,7 @@ describe("operationRunners", () => {
780770

781771
test("can run a bulk action by ids", async () => {
782772
const promise = actionRunner<{ id: string; name: string }>(
783-
{
784-
connection,
785-
},
773+
manager,
786774
"bulkFlipWidgets",
787775
{ id: true, name: true },
788776
"widget",
@@ -830,9 +818,7 @@ describe("operationRunners", () => {
830818

831819
test("can run a bulk action with params", async () => {
832820
const promise = actionRunner<{ id: string; name: string }>(
833-
{
834-
connection,
835-
},
821+
manager,
836822
"bulkCreateWidgets",
837823
{ id: true, name: true },
838824
"widget",
@@ -880,9 +866,7 @@ describe("operationRunners", () => {
880866

881867
test("can run a bulk action with a returnType", async () => {
882868
const promise = actionRunner(
883-
{
884-
connection,
885-
},
869+
manager,
886870
"bulkCreateWidgets",
887871
{ id: true, name: true },
888872
"widget",
@@ -921,9 +905,7 @@ describe("operationRunners", () => {
921905

922906
test("can run a bulk action with an object returnType", async () => {
923907
const promise = actionRunner(
924-
{
925-
connection,
926-
},
908+
manager,
927909
"bulkUpsertWidgets",
928910
{ id: true, name: true },
929911
"widget",
@@ -970,9 +952,7 @@ describe("operationRunners", () => {
970952

971953
test("throws a nice error when a bulk action returns errors", async () => {
972954
const promise = actionRunner<{ id: string; name: string }>(
973-
{
974-
connection,
975-
},
955+
manager,
976956
"bulkCreateWidgets",
977957
{ id: true, name: true },
978958
"widget",
@@ -1010,9 +990,7 @@ describe("operationRunners", () => {
1010990

1011991
test("throws a nice error when a bulk action returns errors and data", async () => {
1012992
const promise = actionRunner<{ id: string; name: string }>(
1013-
{
1014-
connection,
1015-
},
993+
manager,
1016994
"bulkCreateWidgets",
1017995
{ id: true, name: true },
1018996
"widget",
@@ -1057,9 +1035,7 @@ describe("operationRunners", () => {
10571035

10581036
test("returns undefined when bulk action does not have a result", async () => {
10591037
const promise = actionRunner<{ id: string; name: string }>(
1060-
{
1061-
connection,
1062-
},
1038+
manager,
10631039
"bulkDeleteWidgets",
10641040
null,
10651041
"widget",
@@ -1958,7 +1934,7 @@ describe("operationRunners", () => {
19581934
test("can run a live findMany", async () => {
19591935
const iterator = asyncIterableToIterator(
19601936
findManyRunner<{ id: string; name: string }, { live: true }>(
1961-
{ connection } as AnyModelManager,
1937+
{ connection } as AnyPublicModelManager,
19621938
"widgets",
19631939
{ id: true, name: true },
19641940
"widget",

packages/api-client-core/src/ModelManager.ts renamed to packages/api-client-core/src/AnyModelManager.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { GadgetConnection } from "./GadgetConnection.js";
22
import type { GadgetRecord } from "./GadgetRecord.js";
33
import type { GadgetRecordList } from "./GadgetRecordList.js";
4+
import type { InternalModelManager } from "./InternalModelManager.js";
45

56
export type AnyModelFinderMetadata = {
67
/** The name of the GraphQL API field that should be called for this operation */
@@ -20,13 +21,35 @@ export type AnyModelFinderMetadata = {
2021
};
2122

2223
/**
23-
* Object representing one model's API in a high level way
24-
* This is a generic interface. Concrete ones are generated by Gadget, */
25-
export interface AnyModelManager {
24+
* The manager class for a given model that uses the Public API, like `api.post` or `api.user`
25+
**/
26+
export interface AnyPublicModelManager {
2627
connection: GadgetConnection;
28+
apiIdentifier?: string;
2729
findOne: ((id: string, options: any) => Promise<GadgetRecord<any>>) & AnyModelFinderMetadata;
2830
findMany: ((options: any) => Promise<GadgetRecordList<any>>) & AnyModelFinderMetadata;
2931
findFirst: ((options: any) => Promise<GadgetRecord<any>>) & AnyModelFinderMetadata;
3032
maybeFindFirst(options: any): Promise<GadgetRecord<any> | null>;
3133
maybeFindOne(id: string, options: any): Promise<GadgetRecord<any> | null>;
3234
}
35+
36+
/**
37+
* The manager class for a given single model that uses the Public API, like `api.session`
38+
**/
39+
export interface AnyPublicSingletonModelManager {
40+
connection: GadgetConnection;
41+
apiIdentifier?: string;
42+
get(): Promise<GadgetRecord<any>>;
43+
}
44+
45+
/**
46+
* Prior to 1.1 actions were defined to accept just a connection
47+
*/
48+
export interface AnyLegacyModelManager {
49+
connection: GadgetConnection;
50+
}
51+
52+
/**
53+
* Any model manager, either public or internal
54+
*/
55+
export type AnyModelManager = AnyPublicModelManager | AnyPublicSingletonModelManager | AnyLegacyModelManager | InternalModelManager;

0 commit comments

Comments
 (0)