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

Give records a reference to the model manager which loaded them #273

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"lint:eslint": "NODE_OPTIONS=\"--max-old-space-size=4096\" eslint --quiet --ext ts,tsx packages scripts",
"lint:fix": "NODE_OPTIONS=\"--max-old-space-size=4096\" prettier --write --check \"(packages|scripts)/**/*.{js,ts,tsx}\" && eslint --ext ts,tsx --fix packages scripts",
"typecheck": "pnpm -r --no-bail run --if-present typecheck",
"build": "pnpm -r --no-bail run --if-present build",
"build": "pnpm -r run --if-present build",
"prerelease": "pnpm -r --no-bail run --if-present prerelease",
"watch": "run-p --print-label watch:*",
"watch:client": "pnpm --filter=@gadgetinc/api-client-core watch",
Expand Down
15 changes: 15 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,18 @@ 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");
expect(product.modelManager).toEqual(null);
});

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

it("should respond toJSON, which returns the inner __gadget.fields properties", () => {
const product = new GadgetRecord<SampleBaseRecord>(productBaseRecord);
expect(product.toJSON()).toEqual({
Expand Down
36 changes: 11 additions & 25 deletions packages/api-client-core/spec/operationRunners.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import nock from "nock";
import type { GadgetErrorGroup } from "../src/index.js";
import type { AnyPublicModelManager, GadgetErrorGroup } from "../src/index.js";
import { GadgetConnection, actionRunner } from "../src/index.js";
import { mockUrqlClient } from "./mockUrqlClient.js";

Expand All @@ -8,17 +8,17 @@ nock.disableNetConnect();
// eslint-disable-next-line jest/no-export
describe("operationRunners", () => {
let connection: GadgetConnection;
let manager: AnyPublicModelManager;
beforeEach(() => {
connection = new GadgetConnection({ endpoint: "https://someapp.gadget.app" });
jest.spyOn(connection, "currentClient", "get").mockReturnValue(mockUrqlClient as any);
manager = { connection } as AnyPublicModelManager;
});

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 @@ -58,9 +58,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 @@ -105,9 +103,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 @@ -152,9 +148,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 @@ -202,9 +196,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 @@ -252,9 +244,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 @@ -292,9 +282,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 @@ -339,9 +327,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",
{ id: true, name: true },
"widget",
Expand Down
31 changes: 31 additions & 0 deletions packages/api-client-core/src/AnyModelManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { GadgetConnection } from "./GadgetConnection";
import type { GadgetRecord } from "./GadgetRecord";
import type { GadgetRecordList } from "./GadgetRecordList";
import type { InternalModelManager } from "./InternalModelManager";

/**
* The manager class for a given model that uses the Public API, like `api.post` or `api.user`
**/
export interface AnyPublicModelManager {
connection: GadgetConnection;
apiIdentifier?: string;
findOne(id: string, options: any): Promise<GadgetRecord<any>>;
maybeFindOne(id: string, options: any): Promise<GadgetRecord<any> | null>;
findMany(options: any): Promise<GadgetRecordList<any>>;
findFirst(options: any): Promise<GadgetRecord<any>>;
maybeFindFirst(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 {
connection: GadgetConnection;
apiIdentifier?: string;
get(): Promise<GadgetRecord<any>>;
}

/**
* Any model manager, either public or internal
*/
export type AnyModelManager = AnyPublicModelManager | AnyPublicSingletonModelManager | InternalModelManager;
8 changes: 8 additions & 0 deletions packages/api-client-core/src/GadgetFunctions.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { AnyPublicModelManager, AnyPublicSingletonModelManager } from "./AnyModelManager.js";
import type { GadgetRecord, RecordShape } from "./GadgetRecord.js";
import type { GadgetRecordList } from "./GadgetRecordList.js";
import type { LimitToKnownKeys, VariablesOptions } from "./types.js";
Expand All @@ -17,6 +18,7 @@ export interface FindOneFunction<OptionsT, SelectionT, SchemaT, DefaultsT> {
selectionType: SelectionT;
optionsType: OptionsT;
schemaType: SchemaT | null;
modelManager?: AnyPublicModelManager;
}

export interface MaybeFindOneFunction<OptionsT, SelectionT, SchemaT, DefaultsT> {
Expand All @@ -30,6 +32,7 @@ export interface MaybeFindOneFunction<OptionsT, SelectionT, SchemaT, DefaultsT>
selectionType: SelectionT;
optionsType: OptionsT;
schemaType: SchemaT | null;
modelManager?: AnyPublicModelManager;
}

export interface FindManyFunction<OptionsT, SelectionT, SchemaT, DefaultsT> {
Expand All @@ -42,6 +45,7 @@ export interface FindManyFunction<OptionsT, SelectionT, SchemaT, DefaultsT> {
selectionType: SelectionT;
optionsType: OptionsT;
schemaType: SchemaT | null;
modelManager?: AnyPublicModelManager;
}

export interface FindFirstFunction<OptionsT, SelectionT, SchemaT, DefaultsT> {
Expand All @@ -54,6 +58,7 @@ export interface FindFirstFunction<OptionsT, SelectionT, SchemaT, DefaultsT> {
selectionType: SelectionT;
optionsType: OptionsT;
schemaType: SchemaT | null;
modelManager?: AnyPublicModelManager;
}

export interface MaybeFindFirstFunction<OptionsT, SelectionT, SchemaT, DefaultsT> {
Expand All @@ -66,6 +71,7 @@ export interface MaybeFindFirstFunction<OptionsT, SelectionT, SchemaT, DefaultsT
selectionType: SelectionT;
optionsType: OptionsT;
schemaType: SchemaT | null;
modelManager?: AnyPublicModelManager;
}

interface ActionWithIdAndVariables<OptionsT, VariablesT> {
Expand Down Expand Up @@ -113,6 +119,7 @@ interface ActionFunctionMetadata<OptionsT, VariablesT, SelectionT, SchemaT, Defa
acceptsModelInput?: boolean;
paramOnlyVariables?: readonly string[];
hasReturnType?: boolean;
modelManager?: AnyPublicModelManager;
}

export type ActionFunction<OptionsT, VariablesT, SelectionT, SchemaT, DefaultsT> = ActionFunctionMetadata<
Expand Down Expand Up @@ -150,6 +157,7 @@ export interface GetFunction<OptionsT, SelectionT, SchemaT, DefaultsT> {
selectionType: SelectionT;
optionsType: OptionsT;
schemaType: SchemaT | null;
modelManager?: AnyPublicSingletonModelManager;
}

export interface GlobalActionFunction<VariablesT> {
Expand Down
9 changes: 6 additions & 3 deletions packages/api-client-core/src/GadgetRecord.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { klona as cloneDeep } from "klona";
import type { Jsonify } from "type-fest";
import type { AnyModelManager } from "./AnyModelManager.js";
import { isEqual, toPrimitiveObject } from "./support.js";

export enum ChangeTracking {
Expand All @@ -22,7 +23,7 @@ export class GadgetRecordImplementation<Shape extends RecordShape> {

private empty = false;

constructor(data: Shape) {
constructor(data: Shape, readonly modelManager: AnyModelManager | null = null) {
this.__gadget.instantiatedFields = cloneDeep(data);
this.__gadget.persistedFields = cloneDeep(data);
Object.assign(this.__gadget.fields, data);
Expand Down Expand Up @@ -192,6 +193,8 @@ export class GadgetRecordImplementation<Shape extends RecordShape> {
*/

/** Instantiate a `GadgetRecord` with the attributes of your model. A `GadgetRecord` can be used to track changes to your model and persist those changes via Gadget actions. */
export const GadgetRecord: new <Shape extends RecordShape>(data: Shape) => GadgetRecordImplementation<Shape> & Shape =
GadgetRecordImplementation as any;
export const GadgetRecord: new <Shape extends RecordShape>(
data: Shape,
modelManager?: AnyModelManager
) => GadgetRecordImplementation<Shape> & Shape = GadgetRecordImplementation as any;
export type GadgetRecord<Shape extends RecordShape> = GadgetRecordImplementation<Shape> & Shape;
8 changes: 4 additions & 4 deletions packages/api-client-core/src/GadgetRecordList.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
/* eslint-disable no-throw-literal */
/* eslint-disable @typescript-eslint/require-await */
import type { Jsonify } from "type-fest";
import type { AnyPublicModelManager } from "./AnyModelManager.js";
import type { GadgetRecord, RecordShape } from "./GadgetRecord.js";
import type { InternalModelManager } from "./InternalModelManager.js";
import type { AnyModelManager } from "./ModelManager.js";
import { GadgetClientError, GadgetOperationError } from "./support.js";
import { PaginateOptions } from "./types.js";
import type { PaginateOptions } from "./types.js";

type PaginationConfig = {
pageInfo: { hasNextPage: boolean; hasPreviousPage: boolean; startCursor: string; endCursor: string };
Expand All @@ -14,12 +14,12 @@ type PaginationConfig = {

/** Represents a list of objects returned from the API. Facilitates iterating and paginating. */
export class GadgetRecordList<Shape extends RecordShape> extends Array<GadgetRecord<Shape>> {
modelManager!: AnyModelManager | InternalModelManager;
modelManager!: AnyPublicModelManager | InternalModelManager;
pagination!: PaginationConfig;

/** Internal method used to create a list. Should not be used by applications. */
static boot<Shape extends RecordShape>(
modelManager: AnyModelManager | InternalModelManager,
modelManager: AnyPublicModelManager | InternalModelManager,
records: GadgetRecord<Shape>[],
pagination: PaginationConfig
) {
Expand Down
14 changes: 7 additions & 7 deletions packages/api-client-core/src/InternalModelManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ export class InternalModelManager {
private readonly capitalizedApiIdentifier: string;

constructor(
private readonly apiIdentifier: string,
readonly apiIdentifier: string,
readonly connection: GadgetConnection,
readonly options?: { pluralApiIdentifier: string; hasAmbiguousIdentifiers?: boolean }
) {
Expand Down Expand Up @@ -278,7 +278,7 @@ export class InternalModelManager {
.toPromise();
const assertSuccess = throwOnEmptyData ? assertOperationSuccess : assertNullableOperationSuccess;
const result = assertSuccess(response, ["internal", this.apiIdentifier]);
return hydrateRecord(response, result);
return hydrateRecord(response, result, this);
}

/**
Expand Down Expand Up @@ -311,7 +311,7 @@ export class InternalModelManager {
const plan = internalFindManyQuery(this.apiIdentifier, options);
const response = await this.connection.currentClient.query(plan.query, plan.variables).toPromise();
const connection = assertNullableOperationSuccess(response, ["internal", `list${this.capitalizedApiIdentifier}`]);
const records = hydrateConnection(response, connection);
const records = hydrateConnection(response, connection, this);

return GadgetRecordList.boot(this, records, { options, pageInfo: connection.pageInfo });
}
Expand Down Expand Up @@ -340,7 +340,7 @@ export class InternalModelManager {
connection = assertOperationSuccess(response, ["internal", `list${this.capitalizedApiIdentifier}`], throwOnEmptyData);
}

const records = hydrateConnection(response, connection);
const records = hydrateConnection(response, connection, this);
const recordList = GadgetRecordList.boot(this, records, { options, pageInfo: connection.pageInfo });
return recordList[0];
}
Expand Down Expand Up @@ -378,7 +378,7 @@ export class InternalModelManager {
})
.toPromise();
const result = assertMutationSuccess(response, ["internal", `create${this.capitalizedApiIdentifier}`]);
return hydrateRecord(response, result[this.apiIdentifier]);
return hydrateRecord(response, result[this.apiIdentifier], this);
}

/**
Expand Down Expand Up @@ -408,7 +408,7 @@ export class InternalModelManager {
})
.toPromise();
const result = assertMutationSuccess(response, ["internal", `bulkCreate${capitalizedPluralApiIdentifier}`]);
return hydrateRecordArray(response, result[this.options.pluralApiIdentifier]);
return hydrateRecordArray(response, result[this.options.pluralApiIdentifier], this);
}

/**
Expand All @@ -433,7 +433,7 @@ export class InternalModelManager {
.toPromise();
const result = assertMutationSuccess(response, ["internal", `update${this.capitalizedApiIdentifier}`]);

return hydrateRecord(response, result[this.apiIdentifier]);
return hydrateRecord(response, result[this.apiIdentifier], this);
}

/**
Expand Down
15 changes: 0 additions & 15 deletions packages/api-client-core/src/ModelManager.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/api-client-core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from "./AnyClient.js";
export * from "./AnyModelManager.js";
export * from "./ClientOptions.js";
export * from "./DataHydrator.js";
export * from "./FieldSelection.js";
Expand All @@ -9,7 +10,6 @@ export * from "./GadgetRecordList.js";
export * from "./GadgetTransaction.js";
export * from "./InMemoryStorage.js";
export * from "./InternalModelManager.js";
export * from "./ModelManager.js";
export * from "./operationBuilders.js";
export * from "./operationRunners.js";
export * from "./support.js";
Expand Down
Loading