From d270417ca6151f765a1b993cfc16494752819e62 Mon Sep 17 00:00:00 2001 From: Harry Brundage Date: Tue, 27 Jun 2023 22:00:27 -0400 Subject: [PATCH] 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. --- package.json | 2 +- .../api-client-core/spec/GadgetRecord.spec.ts | 15 +++++++++ .../api-client-core/src/AnyModelManager.ts | 31 ++++++++++++++++++ .../api-client-core/src/GadgetFunctions.ts | 8 +++++ packages/api-client-core/src/GadgetRecord.ts | 9 ++++-- .../api-client-core/src/GadgetRecordList.ts | 6 ++-- .../src/InternalModelManager.ts | 14 ++++---- packages/api-client-core/src/ModelManager.ts | 15 --------- packages/api-client-core/src/index.ts | 2 +- .../api-client-core/src/operationRunners.ts | 32 +++++++++---------- packages/api-client-core/src/support.ts | 25 +++++++++++---- packages/react/src/auth/useSession.ts | 6 ++-- packages/react/src/useAction.ts | 4 ++- packages/react/src/useBulkAction.ts | 4 ++- packages/react/src/useFindBy.ts | 2 +- packages/react/src/useFindFirst.ts | 13 ++++++-- packages/react/src/useFindMany.ts | 8 ++--- packages/react/src/useFindOne.ts | 13 ++++++-- packages/react/src/useGet.ts | 13 ++++++-- packages/react/src/useMaybeFindFirst.ts | 13 ++++++-- packages/react/src/useMaybeFindOne.ts | 13 ++++++-- 21 files changed, 172 insertions(+), 76 deletions(-) create mode 100644 packages/api-client-core/src/AnyModelManager.ts delete mode 100644 packages/api-client-core/src/ModelManager.ts diff --git a/package.json b/package.json index e586b0643..edaec64c5 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/api-client-core/spec/GadgetRecord.spec.ts b/packages/api-client-core/spec/GadgetRecord.spec.ts index 10910e4a6..7c395700b 100644 --- a/packages/api-client-core/spec/GadgetRecord.spec.ts +++ b/packages/api-client-core/spec/GadgetRecord.spec.ts @@ -1,3 +1,4 @@ +import type { AnyPublicModelManager } from "../src/AnyModelManager.js"; import { ChangeTracking, GadgetRecord } from "../src/GadgetRecord.js"; interface SampleBaseRecord { id?: string; @@ -38,6 +39,8 @@ const expectPersistedChanges = (record: GadgetRecord, ...prope return _expectChanges(record, ChangeTracking.SinceLastPersisted, ...properties); }; +const mockModelManager: AnyPublicModelManager = {} as any; + describe("GadgetRecord", () => { let productBaseRecord: SampleBaseRecord; beforeAll(() => { @@ -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(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(productBaseRecord, mockModelManager); + expect(product.modelManager).toEqual(mockModelManager); + }); + it("should respond toJSON, which returns the inner __gadget.fields properties", () => { const product = new GadgetRecord(productBaseRecord); expect(product.toJSON()).toEqual({ diff --git a/packages/api-client-core/src/AnyModelManager.ts b/packages/api-client-core/src/AnyModelManager.ts new file mode 100644 index 000000000..45721421c --- /dev/null +++ b/packages/api-client-core/src/AnyModelManager.ts @@ -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>; + maybeFindOne(id: string, options: any): Promise | null>; + findMany(options: any): Promise>; + findFirst(options: any): Promise>; + maybeFindFirst(options: any): Promise | 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>; +} + +/** + * Any model manager, either public or internal + */ +export type AnyModelManager = AnyPublicModelManager | AnyPublicSingletonModelManager | InternalModelManager; diff --git a/packages/api-client-core/src/GadgetFunctions.ts b/packages/api-client-core/src/GadgetFunctions.ts index eecf33a1b..372ffb41c 100644 --- a/packages/api-client-core/src/GadgetFunctions.ts +++ b/packages/api-client-core/src/GadgetFunctions.ts @@ -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"; @@ -17,6 +18,7 @@ export interface FindOneFunction { selectionType: SelectionT; optionsType: OptionsT; schemaType: SchemaT | null; + modelManager?: AnyPublicModelManager; } export interface MaybeFindOneFunction { @@ -30,6 +32,7 @@ export interface MaybeFindOneFunction selectionType: SelectionT; optionsType: OptionsT; schemaType: SchemaT | null; + modelManager?: AnyPublicModelManager; } export interface FindManyFunction { @@ -42,6 +45,7 @@ export interface FindManyFunction { selectionType: SelectionT; optionsType: OptionsT; schemaType: SchemaT | null; + modelManager?: AnyPublicModelManager; } export interface FindFirstFunction { @@ -54,6 +58,7 @@ export interface FindFirstFunction { selectionType: SelectionT; optionsType: OptionsT; schemaType: SchemaT | null; + modelManager?: AnyPublicModelManager; } export interface MaybeFindFirstFunction { @@ -66,6 +71,7 @@ export interface MaybeFindFirstFunction { @@ -113,6 +119,7 @@ interface ActionFunctionMetadata = ActionFunctionMetadata< @@ -150,6 +157,7 @@ export interface GetFunction { selectionType: SelectionT; optionsType: OptionsT; schemaType: SchemaT | null; + modelManager?: AnyPublicSingletonModelManager; } export interface GlobalActionFunction { diff --git a/packages/api-client-core/src/GadgetRecord.ts b/packages/api-client-core/src/GadgetRecord.ts index d2bb32266..cf7aaa368 100644 --- a/packages/api-client-core/src/GadgetRecord.ts +++ b/packages/api-client-core/src/GadgetRecord.ts @@ -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 { @@ -22,7 +23,7 @@ export class GadgetRecordImplementation { 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); @@ -192,6 +193,8 @@ export class GadgetRecordImplementation { */ /** 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 (data: Shape) => GadgetRecordImplementation & Shape = - GadgetRecordImplementation as any; +export const GadgetRecord: new ( + data: Shape, + modelManager?: AnyModelManager +) => GadgetRecordImplementation & Shape = GadgetRecordImplementation as any; export type GadgetRecord = GadgetRecordImplementation & Shape; diff --git a/packages/api-client-core/src/GadgetRecordList.ts b/packages/api-client-core/src/GadgetRecordList.ts index 83d15a062..46211560e 100644 --- a/packages/api-client-core/src/GadgetRecordList.ts +++ b/packages/api-client-core/src/GadgetRecordList.ts @@ -1,9 +1,9 @@ /* 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 type { PaginationOptions } from "./operationBuilders.js"; import { GadgetClientError, GadgetOperationError } from "./support.js"; @@ -14,12 +14,12 @@ type PaginationConfig = { /** Represents a list of objects returned from the API. Facilitates iterating and paginating. */ export class GadgetRecordList extends Array> { - modelManager!: AnyModelManager | InternalModelManager; + modelManager!: AnyPublicModelManager | InternalModelManager; pagination!: PaginationConfig; /** Internal method used to create a list. Should not be used by applications. */ static boot( - modelManager: AnyModelManager | InternalModelManager, + modelManager: AnyPublicModelManager | InternalModelManager, records: GadgetRecord[], pagination: PaginationConfig ) { diff --git a/packages/api-client-core/src/InternalModelManager.ts b/packages/api-client-core/src/InternalModelManager.ts index 12cc7ad23..4b0d6463f 100644 --- a/packages/api-client-core/src/InternalModelManager.ts +++ b/packages/api-client-core/src/InternalModelManager.ts @@ -306,7 +306,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 } ) { @@ -358,7 +358,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); } /** @@ -391,7 +391,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 }); } @@ -420,7 +420,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]; } @@ -458,7 +458,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); } /** @@ -488,7 +488,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); } /** @@ -513,7 +513,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); } /** diff --git a/packages/api-client-core/src/ModelManager.ts b/packages/api-client-core/src/ModelManager.ts deleted file mode 100644 index 9c4aaeb03..000000000 --- a/packages/api-client-core/src/ModelManager.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { GadgetConnection } from "./GadgetConnection.js"; -import type { GadgetRecord } from "./GadgetRecord.js"; -import type { GadgetRecordList } from "./GadgetRecordList.js"; - -/** - * Object representing one model's API in a high level way - * This is a generic interface. Concrete ones are generated by Gadget, */ -export interface AnyModelManager { - connection: GadgetConnection; - findOne(id: string, options: any): Promise>; - maybeFindOne(id: string, options: any): Promise | null>; - findMany(options: any): Promise>; - findFirst(options: any): Promise>; - maybeFindFirst(options: any): Promise | null>; -} diff --git a/packages/api-client-core/src/index.ts b/packages/api-client-core/src/index.ts index b0ac2bb55..b49ae5aa5 100644 --- a/packages/api-client-core/src/index.ts +++ b/packages/api-client-core/src/index.ts @@ -1,4 +1,5 @@ export * from "./AnyClient.js"; +export * from "./AnyModelManager.js"; export * from "./ClientOptions.js"; export * from "./DataHydrator.js"; export * from "./FieldSelection.js"; @@ -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"; diff --git a/packages/api-client-core/src/operationRunners.ts b/packages/api-client-core/src/operationRunners.ts index e5fa26f86..cff953730 100644 --- a/packages/api-client-core/src/operationRunners.ts +++ b/packages/api-client-core/src/operationRunners.ts @@ -1,8 +1,8 @@ +import type { AnyPublicModelManager, AnyPublicSingletonModelManager } from "./AnyModelManager.js"; import type { FieldSelection } from "./FieldSelection.js"; import type { GadgetConnection } from "./GadgetConnection.js"; import type { GadgetRecord, RecordShape } from "./GadgetRecord.js"; import { GadgetRecordList } from "./GadgetRecordList.js"; -import type { AnyModelManager } from "./ModelManager.js"; import type { PaginationOptions, SelectionOptions } from "./operationBuilders.js"; import { actionOperation, @@ -26,7 +26,7 @@ import { import type { VariablesOptions } from "./types.js"; export const findOneRunner = async ( - modelManager: { connection: GadgetConnection }, + modelManager: AnyPublicModelManager | AnyPublicSingletonModelManager, operation: string, id: string | undefined, defaultSelection: FieldSelection, @@ -38,11 +38,11 @@ export const findOneRunner = async ( const response = await modelManager.connection.currentClient.query(plan.query, plan.variables).toPromise(); const assertSuccess = throwOnEmptyData ? assertOperationSuccess : assertNullableOperationSuccess; const record = assertSuccess(response, [operation]); - return hydrateRecord(response, record); + return hydrateRecord(response, record, modelManager); }; export const findOneByFieldRunner = async ( - modelManager: { connection: GadgetConnection }, + modelManager: AnyPublicModelManager, operation: string, fieldName: string, fieldValue: string, @@ -53,7 +53,7 @@ export const findOneByFieldRunner = async ( const plan = findOneByFieldOperation(operation, fieldName, fieldValue, defaultSelection, modelApiIdentifier, options); const response = await modelManager.connection.currentClient.query(plan.query, plan.variables).toPromise(); const connectionObject = assertOperationSuccess(response, [operation]); - const records = hydrateConnection(response, connectionObject); + const records = hydrateConnection(response, connectionObject, modelManager); if (records.length > 1) { throw getNonUniqueDataError(modelApiIdentifier, fieldName, fieldValue); @@ -63,7 +63,7 @@ export const findOneByFieldRunner = async ( }; export const findManyRunner = async ( - modelManager: AnyModelManager, + modelManager: AnyPublicModelManager, operation: string, defaultSelection: FieldSelection, modelApiIdentifier: string, @@ -83,13 +83,13 @@ export const findManyRunner = async ( connectionObject = assertOperationSuccess(response, [operation], throwOnEmptyData); } - const records = hydrateConnection(response, connectionObject); + const records = hydrateConnection(response, connectionObject, modelManager); return GadgetRecordList.boot(modelManager, records, { options, pageInfo: connectionObject.pageInfo }); }; export interface ActionRunner { ( - modelManager: { connection: GadgetConnection }, + modelManager: AnyPublicModelManager | AnyPublicSingletonModelManager, operation: string, defaultSelection: FieldSelection | null, modelApiIdentifier: string, @@ -102,7 +102,7 @@ export interface ActionRunner { ): Promise; ( - modelManager: { connection: GadgetConnection }, + modelManager: AnyPublicModelManager | AnyPublicSingletonModelManager, operation: string, defaultSelection: FieldSelection | null, modelApiIdentifier: string, @@ -115,7 +115,7 @@ export interface ActionRunner { ): Promise>; ( - modelManager: { connection: GadgetConnection }, + modelManager: AnyPublicModelManager | AnyPublicSingletonModelManager, operation: string, defaultSelection: FieldSelection | null, modelApiIdentifier: string, @@ -127,7 +127,7 @@ export interface ActionRunner { ): Promise>; ( - modelManager: { connection: GadgetConnection }, + modelManager: AnyPublicModelManager | AnyPublicSingletonModelManager, operation: string, defaultSelection: FieldSelection | null, modelApiIdentifier: string, @@ -139,7 +139,7 @@ export interface ActionRunner { ): Promise[]>; ( - modelManager: { connection: GadgetConnection }, + modelManager: AnyPublicModelManager | AnyPublicSingletonModelManager, operation: string, defaultSelection: FieldSelection | null, modelApiIdentifier: string, @@ -152,7 +152,7 @@ export interface ActionRunner { ): Promise; ( - modelManager: { connection: GadgetConnection }, + modelManager: AnyPublicModelManager | AnyPublicSingletonModelManager, operation: string, defaultSelection: FieldSelection | null, modelApiIdentifier: string, @@ -166,7 +166,7 @@ export interface ActionRunner { } export const actionRunner: ActionRunner = async ( - modelManager: { connection: GadgetConnection }, + modelManager: AnyPublicModelManager | AnyPublicSingletonModelManager, operation: string, defaultSelection: FieldSelection | null, modelApiIdentifier: string, @@ -200,7 +200,7 @@ export const actionRunner: ActionRunner = async (response, mutationTriple[modelSelectionField]); + return hydrateRecord(response, mutationTriple[modelSelectionField], modelManager); } else { return mutationTriple.result; } @@ -208,7 +208,7 @@ export const actionRunner: ActionRunner = async (response, mutationTriple[modelSelectionField]) + ? hydrateRecordArray(response, mutationTriple[modelSelectionField], modelManager) : undefined; if (mutationTriple.errors) { const errors = mutationTriple.errors.map((error: any) => gadgetErrorFor(error)); diff --git a/packages/api-client-core/src/support.ts b/packages/api-client-core/src/support.ts index f3dd9ed80..503ba7b27 100644 --- a/packages/api-client-core/src/support.ts +++ b/packages/api-client-core/src/support.ts @@ -1,5 +1,6 @@ import type { OperationResult } from "@urql/core"; import { CombinedError } from "@urql/core"; +import type { AnyModelManager } from "./AnyModelManager.js"; import { DataHydrator } from "./DataHydrator.js"; import type { RecordShape } from "./GadgetRecord.js"; import { GadgetRecord } from "./GadgetRecord.js"; @@ -357,25 +358,37 @@ export const getHydrator = (response: Result) => { } }; -export const hydrateRecord = (response: Result, record: any): GadgetRecord => { +export const hydrateRecord = ( + response: Result, + record: any, + modelManager?: AnyModelManager +): GadgetRecord => { const hydrator = getHydrator(response); if (hydrator) { record = hydrator.apply(record); } - return new GadgetRecord(record); + return new GadgetRecord(record, modelManager); }; -export const hydrateRecordArray = (response: Result, records: Array) => { +export const hydrateRecordArray = ( + response: Result, + records: Array, + modelManager?: AnyModelManager +) => { const hydrator = getHydrator(response); if (hydrator) { records = hydrator.apply(records) as any; } - return records?.map((record) => new GadgetRecord(record)); + return records?.map((record) => new GadgetRecord(record, modelManager)); }; -export const hydrateConnection = (response: Result, connection: { edges: { node: Node }[] }) => { +export const hydrateConnection = ( + response: Result, + connection: { edges: { node: Node }[] }, + modelManager?: AnyModelManager +) => { const nodes = connection.edges.map((edge) => edge.node); - return hydrateRecordArray(response, nodes); + return hydrateRecordArray(response, nodes, modelManager); }; export const toPrimitiveObject = (value: any): any => { diff --git a/packages/react/src/auth/useSession.ts b/packages/react/src/auth/useSession.ts index fc7355536..828219e1b 100644 --- a/packages/react/src/auth/useSession.ts +++ b/packages/react/src/auth/useSession.ts @@ -1,5 +1,7 @@ import type { AnyClient, + AnyPublicModelManager, + AnyPublicSingletonModelManager, DefaultSelection, FindManyFunction, GadgetRecord, @@ -16,8 +18,8 @@ export type GadgetSession = GadgetRecord>; export type GadgetUser = GadgetRecord>; export type ClientWithSessionAndUserManagers = AnyClient & { - currentSession: { get: GetFunction }; - user: { findMany: FindManyFunction }; + currentSession: { get: GetFunction } & AnyPublicSingletonModelManager; + user: { findMany: FindManyFunction } & AnyPublicModelManager; }; /** diff --git a/packages/react/src/useAction.ts b/packages/react/src/useAction.ts index 1af20f81e..e2d2a61b5 100644 --- a/packages/react/src/useAction.ts +++ b/packages/react/src/useAction.ts @@ -115,7 +115,9 @@ const processResult = ( if (errors && errors[0]) { error = ErrorWrapper.forErrorsResponse(errors, error?.response); } else { - data = action.hasReturnType ? mutationData.result : hydrateRecord(result, mutationData[action.modelSelectionField]); + data = action.hasReturnType + ? mutationData.result + : hydrateRecord(result, mutationData[action.modelSelectionField], action.modelManager); } } } diff --git a/packages/react/src/useBulkAction.ts b/packages/react/src/useBulkAction.ts index c107202ee..5fa4a07bf 100644 --- a/packages/react/src/useBulkAction.ts +++ b/packages/react/src/useBulkAction.ts @@ -116,7 +116,9 @@ const processResult = (result: UseMutationState, action: BulkActionFun if (errors && errors[0]) { error = ErrorWrapper.forErrorsResponse(errors, (error as any)?.response); } else { - data = action.hasReturnType ? mutationData.results : hydrateRecordArray(result, mutationData[action.modelSelectionField]); + data = action.hasReturnType + ? mutationData.results + : hydrateRecordArray(result, mutationData[action.modelSelectionField], action.modelManager); } } } diff --git a/packages/react/src/useFindBy.ts b/packages/react/src/useFindBy.ts index f4ebb7706..965f407eb 100644 --- a/packages/react/src/useFindBy.ts +++ b/packages/react/src/useFindBy.ts @@ -63,7 +63,7 @@ export const useFindBy = < if (data) { const connection = get(rawResult.data, dataPath); if (connection) { - records = hydrateConnection(rawResult, connection); + records = hydrateConnection(rawResult, connection, finder.modelManager); data = records[0]; } } diff --git a/packages/react/src/useFindFirst.ts b/packages/react/src/useFindFirst.ts index aca4dbd51..119f79ff6 100644 --- a/packages/react/src/useFindFirst.ts +++ b/packages/react/src/useFindFirst.ts @@ -1,4 +1,11 @@ -import type { DefaultSelection, FindFirstFunction, GadgetRecord, LimitToKnownKeys, Select } from "@gadgetinc/api-client-core"; +import type { + AnyModelManager, + DefaultSelection, + FindFirstFunction, + GadgetRecord, + LimitToKnownKeys, + Select, +} from "@gadgetinc/api-client-core"; import { findManyOperation, get, hydrateConnection } from "@gadgetinc/api-client-core"; import { useMemo } from "react"; import { useGadgetQuery } from "./useGadgetQuery.js"; @@ -36,7 +43,7 @@ export const useFindFirst = < F extends FindFirstFunction, Options extends F["optionsType"] & ReadOperationOptions >( - manager: { findFirst: F }, + manager: { findFirst: F } & AnyModelManager, options?: LimitToKnownKeys ): ReadHookResult< GadgetRecord, DefaultSelection>> @@ -60,7 +67,7 @@ export const useFindFirst = < if (data) { const connection = get(rawResult.data, dataPath); if (connection) { - data = hydrateConnection(rawResult, connection)[0]; + data = hydrateConnection(rawResult, connection, manager)[0]; } else { data = data[0]; } diff --git a/packages/react/src/useFindMany.ts b/packages/react/src/useFindMany.ts index 23afdacc2..28ad75914 100644 --- a/packages/react/src/useFindMany.ts +++ b/packages/react/src/useFindMany.ts @@ -1,4 +1,4 @@ -import type { AnyModelManager, DefaultSelection, FindManyFunction, LimitToKnownKeys, Select } from "@gadgetinc/api-client-core"; +import type { AnyPublicModelManager, DefaultSelection, FindManyFunction, LimitToKnownKeys, Select } from "@gadgetinc/api-client-core"; import { GadgetRecordList, findManyOperation, get, hydrateConnection } from "@gadgetinc/api-client-core"; import { useMemo } from "react"; import { useGadgetQuery } from "./useGadgetQuery.js"; @@ -36,7 +36,7 @@ export const useFindMany = < F extends FindManyFunction, Options extends F["optionsType"] & ReadOperationOptions >( - manager: { findMany: F }, + manager: { findMany: F } & AnyPublicModelManager, options?: LimitToKnownKeys ): ReadHookResult< GadgetRecordList, DefaultSelection>> @@ -59,8 +59,8 @@ export const useFindMany = < if (data) { const connection = get(rawResult.data, dataPath); if (connection) { - const records = hydrateConnection(rawResult, connection); - data = GadgetRecordList.boot(manager as unknown as AnyModelManager, records, connection); + const records = hydrateConnection(rawResult, connection, manager); + data = GadgetRecordList.boot(manager, records, connection); } } diff --git a/packages/react/src/useFindOne.ts b/packages/react/src/useFindOne.ts index 6c02a74bc..6d2f23a06 100644 --- a/packages/react/src/useFindOne.ts +++ b/packages/react/src/useFindOne.ts @@ -1,4 +1,11 @@ -import type { DefaultSelection, FindOneFunction, GadgetRecord, LimitToKnownKeys, Select } from "@gadgetinc/api-client-core"; +import type { + AnyModelManager, + DefaultSelection, + FindOneFunction, + GadgetRecord, + LimitToKnownKeys, + Select, +} from "@gadgetinc/api-client-core"; import { findOneOperation, get, hydrateRecord } from "@gadgetinc/api-client-core"; import { useMemo } from "react"; import { useGadgetQuery } from "./useGadgetQuery.js"; @@ -36,7 +43,7 @@ export const useFindOne = < F extends FindOneFunction, Options extends F["optionsType"] & ReadOperationOptions >( - manager: { findOne: F }, + manager: { findOne: F } & AnyModelManager, id: string, options?: LimitToKnownKeys ): ReadHookResult< @@ -59,7 +66,7 @@ export const useFindOne = < const dataPath = [manager.findOne.operationName]; let data = rawResult.data && get(rawResult.data, dataPath); if (data) { - data = hydrateRecord(rawResult, data); + data = hydrateRecord(rawResult, data, manager); } const error = ErrorWrapper.errorIfDataAbsent(rawResult, dataPath); diff --git a/packages/react/src/useGet.ts b/packages/react/src/useGet.ts index 07d7bd37a..d25ff9f3f 100644 --- a/packages/react/src/useGet.ts +++ b/packages/react/src/useGet.ts @@ -1,4 +1,11 @@ -import type { DefaultSelection, GadgetRecord, GetFunction, LimitToKnownKeys, Select } from "@gadgetinc/api-client-core"; +import type { + AnyPublicSingletonModelManager, + DefaultSelection, + GadgetRecord, + GetFunction, + LimitToKnownKeys, + Select, +} from "@gadgetinc/api-client-core"; import { findOneOperation, get, hydrateRecord } from "@gadgetinc/api-client-core"; import { useMemo } from "react"; import { useGadgetQuery } from "./useGadgetQuery.js"; @@ -36,7 +43,7 @@ export const useGet = < F extends GetFunction, Options extends F["optionsType"] & ReadOperationOptions >( - manager: { get: F }, + manager: { get: F } & AnyPublicSingletonModelManager, options?: LimitToKnownKeys ): ReadHookResult< GadgetRecord, DefaultSelection>> @@ -58,7 +65,7 @@ export const useGet = < let data = null; const rawRecord = rawResult.data && get(rawResult.data, [manager.get.operationName]); if (rawRecord) { - data = hydrateRecord(rawResult, rawRecord); + data = hydrateRecord(rawResult, rawRecord, manager); } const error = ErrorWrapper.forMaybeCombinedError(rawResult.error); diff --git a/packages/react/src/useMaybeFindFirst.ts b/packages/react/src/useMaybeFindFirst.ts index 385adf677..be936d556 100644 --- a/packages/react/src/useMaybeFindFirst.ts +++ b/packages/react/src/useMaybeFindFirst.ts @@ -1,4 +1,11 @@ -import type { DefaultSelection, FindFirstFunction, GadgetRecord, LimitToKnownKeys, Select } from "@gadgetinc/api-client-core"; +import type { + AnyPublicModelManager, + DefaultSelection, + FindFirstFunction, + GadgetRecord, + LimitToKnownKeys, + Select, +} from "@gadgetinc/api-client-core"; import { findManyOperation, get, hydrateConnection } from "@gadgetinc/api-client-core"; import { useMemo } from "react"; import { useGadgetQuery } from "./useGadgetQuery.js"; @@ -36,7 +43,7 @@ export const useMaybeFindFirst = < F extends FindFirstFunction, Options extends F["optionsType"] & ReadOperationOptions >( - manager: { findFirst: F }, + manager: { findFirst: F } & AnyPublicModelManager, options?: LimitToKnownKeys ): ReadHookResult, DefaultSelection> @@ -60,7 +67,7 @@ export const useMaybeFindFirst = < if (data) { const connection = get(rawResult.data, dataPath); if (connection) { - data = hydrateConnection(rawResult, connection)[0] ?? null; + data = hydrateConnection(rawResult, connection, manager)[0] ?? null; } else { data = data[0] ?? null; } diff --git a/packages/react/src/useMaybeFindOne.ts b/packages/react/src/useMaybeFindOne.ts index 65f6fe9ec..bd23a435b 100644 --- a/packages/react/src/useMaybeFindOne.ts +++ b/packages/react/src/useMaybeFindOne.ts @@ -1,4 +1,11 @@ -import type { DefaultSelection, FindOneFunction, GadgetRecord, LimitToKnownKeys, Select } from "@gadgetinc/api-client-core"; +import type { + AnyPublicModelManager, + DefaultSelection, + FindOneFunction, + GadgetRecord, + LimitToKnownKeys, + Select, +} from "@gadgetinc/api-client-core"; import { findOneOperation, get, hydrateRecord } from "@gadgetinc/api-client-core"; import { useMemo } from "react"; import { useGadgetQuery } from "./useGadgetQuery.js"; @@ -36,7 +43,7 @@ export const useMaybeFindOne = < F extends FindOneFunction, Options extends F["optionsType"] & ReadOperationOptions >( - manager: { findOne: F }, + manager: { findOne: F } & AnyPublicModelManager, id: string, options?: LimitToKnownKeys ): ReadHookResult