diff --git a/packages/api-client-core/spec/GadgetRecord.spec.ts b/packages/api-client-core/spec/GadgetRecord.spec.ts index 36a6e70f9..7568338fe 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,16 @@ 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"); + }); + + it("can be constructed with a base record and a model manager", () => { + new GadgetRecord(productBaseRecord, 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/spec/operationRunners.spec.ts b/packages/api-client-core/spec/operationRunners.spec.ts index 88482391c..a09d06b30 100644 --- a/packages/api-client-core/spec/operationRunners.spec.ts +++ b/packages/api-client-core/spec/operationRunners.spec.ts @@ -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, @@ -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; @@ -60,6 +61,7 @@ describe("operationRunners", () => { }, }); jest.spyOn(connection, "currentClient" as any, "get").mockReturnValue(mockUrqlClient as any); + manager = { connection } as AnyPublicModelManager; }); describe("findOneRunner", () => { @@ -67,18 +69,18 @@ describe("operationRunners", () => { 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: { @@ -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: { @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", @@ -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", diff --git a/packages/api-client-core/src/AnyModelManager.ts b/packages/api-client-core/src/AnyModelManager.ts new file mode 100644 index 000000000..114d6520d --- /dev/null +++ b/packages/api-client-core/src/AnyModelManager.ts @@ -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; + /** 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; +export type AnyFindManyFunc = FindManyFunction; +export type AnyFindFirstFunc = FindFirstFunction; + +/** + * 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 | null>; + maybeFindOne(id: string, options: any): Promise | null>; +} + +/** + * The manager class for a given single model that uses the Public API, like `api.session` + **/ +export interface AnyPublicSingletonModelManager = GetFunction> { + 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; diff --git a/packages/api-client-core/src/GadgetFunctions.ts b/packages/api-client-core/src/GadgetFunctions.ts index 64087b8ee..966b4e298 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"; @@ -24,6 +25,7 @@ export interface FindOneFunction { optionsType: OptionsT; schemaType: SchemaT | null; plan?: (fieldValue: string, options?: LimitToKnownKeys) => GQLBuilderResult; + modelManager?: AnyPublicModelManager; } export interface MaybeFindOneFunction { @@ -39,6 +41,7 @@ export interface MaybeFindOneFunction optionsType: OptionsT; schemaType: SchemaT | null; plan?: (fieldValue: string, options?: LimitToKnownKeys) => GQLBuilderResult; + modelManager?: AnyPublicModelManager; } export interface FindManyFunction { @@ -53,6 +56,7 @@ export interface FindManyFunction { optionsType: OptionsT; schemaType: SchemaT | null; plan?: (options?: LimitToKnownKeys) => GQLBuilderResult; + modelManager?: AnyPublicModelManager; } export interface FindFirstFunction { @@ -67,6 +71,7 @@ export interface FindFirstFunction { optionsType: OptionsT; schemaType: SchemaT | null; plan?: (options?: LimitToKnownKeys) => GQLBuilderResult; + modelManager?: AnyPublicModelManager; } export interface MaybeFindFirstFunction { @@ -81,6 +86,7 @@ export interface MaybeFindFirstFunction(options?: LimitToKnownKeys) => GQLBuilderResult; + modelManager?: AnyPublicModelManager; } export interface ActionWithIdAndVariables { @@ -135,6 +141,7 @@ export interface ActionFunctionMetadata(options?: LimitToKnownKeys) => GQLBuilderResult; /** @deprecated */ hasCreateOrUpdateEffect?: boolean; + modelManager?: AnyPublicModelManager; } export type StubbedActionReason = "MissingApiTrigger"; @@ -190,6 +197,7 @@ export interface GetFunction { optionsType: OptionsT; schemaType: SchemaT | null; plan?: (options?: LimitToKnownKeys) => GQLBuilderResult; + modelManager?: AnyPublicSingletonModelManager>; } export interface GlobalActionFunction { diff --git a/packages/api-client-core/src/GadgetRecord.ts b/packages/api-client-core/src/GadgetRecord.ts index 7a3cd7a99..ec57cde01 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 { @@ -14,6 +15,7 @@ const kInstantiatedFields = Symbol.for("g/if"); const kPersistedFields = Symbol.for("g/pf"); const kFieldKeys = Symbol.for("g/fk"); const kTouched = Symbol.for("g/t"); +const kModelManager = Symbol.for("g/mm"); /** Represents one record returned from a high level Gadget API call */ export class GadgetRecord_ { @@ -26,10 +28,12 @@ export class GadgetRecord_ { /** Storage of the keys and values of this record at the time it was last persisted */ [kFieldKeys]: Set; [kTouched] = false; + [kModelManager]: AnyModelManager | undefined; private empty = false; - constructor(data: Shape) { + constructor(data: Shape, modelManager?: AnyModelManager) { + this[kModelManager] = modelManager; this[kInstantiatedFields] = cloneDeep(data) ?? {}; this[kPersistedFields] = cloneDeep(data) ?? {}; Object.assign(this[kFields], data); @@ -221,7 +225,8 @@ export type GadgetRecord = GadgetRecord_ & Sha /** * Instantiates 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) => GadgetRecord_ & Shape = GadgetRecord_ as any; +export const GadgetRecord: new (data: Shape, modelManager?: AnyModelManager) => GadgetRecord_ & Shape = + GadgetRecord_ as any; /** * Legacy export for old generated clients expecting to find the class named this diff --git a/packages/api-client-core/src/GadgetRecordList.ts b/packages/api-client-core/src/GadgetRecordList.ts index 03144c5ce..ba03081f3 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 { GadgetClientError, GadgetOperationError } from "./support.js"; import type { PaginateOptions } from "./types.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 1a9549544..fe84b12b7 100644 --- a/packages/api-client-core/src/InternalModelManager.ts +++ b/packages/api-client-core/src/InternalModelManager.ts @@ -24,6 +24,7 @@ import { } from "./support.js"; import type { AnyFilter, + AnySelection, InternalFieldSelection, InternalFindListOptions, InternalFindManyOptions, @@ -277,7 +278,7 @@ export class InternalModelManager { private readonly namespace: string[]; constructor( - private readonly apiIdentifier: string, + readonly apiIdentifier: string, readonly connection: GadgetConnection, readonly options?: { pluralApiIdentifier: string; hasAmbiguousIdentifiers?: boolean; namespace?: string[] } ) { @@ -329,7 +330,7 @@ export class InternalModelManager { const response = await this.connection.currentClient.query(plan.query, plan.variables).toPromise(); const assertSuccess = throwOnEmptyData ? assertOperationSuccess : assertNullableOperationSuccess; const result = assertSuccess(response, this.dataPath(this.apiIdentifier)); - return hydrateRecord(response, result); + return hydrateRecord(response, result, this); } /** @@ -362,7 +363,7 @@ export class InternalModelManager { const plan = internalFindManyQuery(this.apiIdentifier, this.namespace, options); const response = await this.connection.currentClient.query(plan.query, plan.variables).toPromise(); const connection = assertNullableOperationSuccess(response, this.dataPath(`list${this.capitalizedApiIdentifier}`)); - const records = hydrateConnection(response, connection); + const records = hydrateConnection(response, connection, this); return GadgetRecordList.boot(this, records, { options, pageInfo: connection.pageInfo }); } @@ -392,7 +393,7 @@ export class InternalModelManager { connection = assertOperationSuccess(response, dataPath, 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]; } @@ -427,7 +428,7 @@ export class InternalModelManager { const plan = internalCreateMutation(this.apiIdentifier, this.namespace, this.getRecordFromData(record, "create")); const response = await this.connection.currentClient.mutation(plan.query, plan.variables).toPromise(); const result = assertMutationSuccess(response, this.dataPath(`create${this.capitalizedApiIdentifier}`)); - return hydrateRecord(response, result[this.apiIdentifier]); + return hydrateRecord(response, result[this.apiIdentifier], this); } /** @@ -454,7 +455,7 @@ export class InternalModelManager { const plan = internalBulkCreateMutation(this.apiIdentifier, this.options.pluralApiIdentifier, this.namespace, records); const response = await this.connection.currentClient.mutation(plan.query, plan.variables).toPromise(); const result = assertMutationSuccess(response, this.dataPath(`bulkCreate${capitalizedPluralApiIdentifier}`)); - return hydrateRecordArray(response, result[this.options.pluralApiIdentifier]); + return hydrateRecordArray(response, result[this.options.pluralApiIdentifier], this); } /** @@ -475,7 +476,7 @@ export class InternalModelManager { const response = await this.connection.currentClient.mutation(plan.query, plan.variables).toPromise(); const result = assertMutationSuccess(response, this.dataPath(`update${this.capitalizedApiIdentifier}`)); - return hydrateRecord(response, result[this.apiIdentifier]); + return hydrateRecord(response, result[this.apiIdentifier], this); } /** @@ -507,7 +508,7 @@ export class InternalModelManager { const response = await this.connection.currentClient.mutation(plan.query, plan.variables).toPromise(); const result = assertMutationSuccess(response, this.dataPath(`upsert${this.capitalizedApiIdentifier}`)); - return hydrateRecord(response, result[this.apiIdentifier]); + return hydrateRecord(response, result[this.apiIdentifier], this); } /** @@ -550,7 +551,7 @@ export class InternalModelManager { } } -function formatInternalSelectVariable(select: InternalFieldSelection | undefined): undefined | string[] { +function formatInternalSelectVariable(select?: AnySelection | InternalFieldSelection | null): undefined | string[] { if (!select) return; if (Array.isArray(select)) return select; const result: string[] = []; diff --git a/packages/api-client-core/src/ModelManager.ts b/packages/api-client-core/src/ModelManager.ts deleted file mode 100644 index 4785e8147..000000000 --- a/packages/api-client-core/src/ModelManager.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { GadgetConnection } from "./GadgetConnection.js"; -import type { GadgetRecord } from "./GadgetRecord.js"; -import type { GadgetRecordList } from "./GadgetRecordList.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; - /** 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; -}; - -/** - * 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>) & AnyModelFinderMetadata; - findMany: ((options: any) => Promise>) & AnyModelFinderMetadata; - findFirst: ((options: any) => Promise>) & AnyModelFinderMetadata; - maybeFindFirst(options: any): Promise | null>; - maybeFindOne(id: string, options: any): Promise | null>; -} diff --git a/packages/api-client-core/src/index.ts b/packages/api-client-core/src/index.ts index a652fb845..d365d134c 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 "./BackgroundActionHandle.js"; export * from "./ClientOptions.js"; export * from "./DataHydrator.js"; @@ -10,7 +11,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 9e2fc9571..b62b8a238 100644 --- a/packages/api-client-core/src/operationRunners.ts +++ b/packages/api-client-core/src/operationRunners.ts @@ -4,6 +4,7 @@ import { BackgroundActionHandle } from "./BackgroundActionHandle.js"; /* eslint-disable @typescript-eslint/ban-types */ import type { OperationResult } from "@urql/core"; import type { Source } from "wonka"; +import type { AnyLegacyModelManager, AnyModelManager, AnyPublicModelManager, AnyPublicSingletonModelManager } from "./AnyModelManager.js"; import type { FieldSelection } from "./FieldSelection.js"; import type { GadgetConnection } from "./GadgetConnection.js"; import type { @@ -15,7 +16,6 @@ import type { } from "./GadgetFunctions.js"; import type { GadgetRecord, RecordShape } from "./GadgetRecord.js"; import { GadgetRecordList } from "./GadgetRecordList.js"; -import type { AnyModelManager } from "./ModelManager.js"; import { actionOperation, backgroundActionResultOperation, @@ -92,7 +92,7 @@ function maybeLiveStream( - modelManager: { connection: GadgetConnection }, + modelManager: AnyPublicModelManager | AnyPublicSingletonModelManager | AnyLegacyModelManager, operation: string, id: string | undefined, defaultSelection: FieldSelection, @@ -110,14 +110,14 @@ export const findOneRunner = (response, record); + return hydrateRecord(response, record, modelManager); }, options ); }; export const findOneByFieldRunner = ( - modelManager: { connection: GadgetConnection }, + modelManager: AnyPublicModelManager | AnyLegacyModelManager, operation: string, fieldName: string, fieldValue: string, @@ -151,7 +151,7 @@ export const findOneByFieldRunner = ( - modelManager: AnyModelManager, + modelManager: AnyPublicModelManager, operation: string, defaultSelection: FieldSelection, modelApiIdentifier: string, @@ -185,7 +185,7 @@ export const findManyRunner = ; ( - modelManager: { connection: GadgetConnection }, + modelManager: AnyPublicModelManager | AnyPublicSingletonModelManager | AnyLegacyModelManager, operation: string, defaultSelection: FieldSelection | null, modelApiIdentifier: string, @@ -211,7 +211,7 @@ export interface ActionRunner { ): Promise>; ( - modelManager: { connection: GadgetConnection }, + modelManager: AnyPublicModelManager | AnyPublicSingletonModelManager | AnyLegacyModelManager, operation: string, defaultSelection: FieldSelection | null, modelApiIdentifier: string, @@ -223,7 +223,7 @@ export interface ActionRunner { ): Promise>; ( - modelManager: { connection: GadgetConnection }, + modelManager: AnyPublicModelManager | AnyPublicSingletonModelManager | AnyLegacyModelManager, operation: string, defaultSelection: FieldSelection | null, modelApiIdentifier: string, @@ -235,7 +235,7 @@ export interface ActionRunner { ): Promise[]>; ( - modelManager: { connection: GadgetConnection }, + modelManager: AnyPublicModelManager | AnyPublicSingletonModelManager | AnyLegacyModelManager, operation: string, defaultSelection: FieldSelection | null, modelApiIdentifier: string, @@ -248,7 +248,7 @@ export interface ActionRunner { ): Promise; ( - modelManager: { connection: GadgetConnection }, + modelManager: AnyPublicModelManager | AnyPublicSingletonModelManager | AnyLegacyModelManager, operation: string, defaultSelection: FieldSelection | null, modelApiIdentifier: string, @@ -262,7 +262,7 @@ export interface ActionRunner { } export const actionRunner: ActionRunner = async ( - modelManager: { connection: GadgetConnection }, + modelManager: AnyPublicModelManager | AnyPublicSingletonModelManager | AnyLegacyModelManager, operation: string, defaultSelection: FieldSelection | null, modelApiIdentifier: string, @@ -293,11 +293,11 @@ export const actionRunner: ActionRunner = async ( if (!isBulkAction) { const mutationTriple = assertMutationSuccess(response, dataPath); - return processActionResponse(defaultSelection, response, mutationTriple, modelSelectionField, hasReturnType); + return processActionResponse(defaultSelection, response, mutationTriple, modelSelectionField, hasReturnType, modelManager); } else { const mutationTriple = get(response.data, dataPath); - const results = processBulkActionResponse(defaultSelection, response, mutationTriple, modelSelectionField, hasReturnType); + const results = processBulkActionResponse(defaultSelection, response, mutationTriple, modelSelectionField, hasReturnType, modelManager); if (mutationTriple.errors) { const errors = mutationTriple.errors.map((error: any) => gadgetErrorFor(error)); throw new GadgetErrorGroup(errors, results); @@ -312,11 +312,12 @@ const processBulkActionResponse = ( response: any, records: any, modelSelectionField: string, - hasReturnType?: HasReturnType | null + hasReturnType?: HasReturnType | null, + modelManager?: AnyPublicModelManager | AnyPublicSingletonModelManager | AnyLegacyModelManager ) => { if (defaultSelection == null) return; if (!hasReturnType) { - return hydrateRecordArray(response, records[modelSelectionField]); + return hydrateRecordArray(response, records[modelSelectionField], modelManager); } else if (typeof hasReturnType == "boolean") { return records.results; } else { @@ -332,7 +333,7 @@ const processBulkActionResponse = ( "hasReturnType" in innerHasReturnType ? returnTypeForRecord(result, innerHasReturnType.hasReturnType) : false; if (!returnTypeForResult) { - return hydrateRecord(response, result); + return hydrateRecord(response, result, modelManager); } else { return processActionResponse(defaultSelection, response, result, modelSelectionField, returnTypeForResult); } @@ -346,19 +347,20 @@ export const processActionResponse = ( response: any, record: any, modelSelectionField: string, - hasReturnType?: HasReturnType | null + hasReturnType?: HasReturnType | null, + modelManager?: AnyPublicModelManager | AnyPublicSingletonModelManager | AnyLegacyModelManager ): any => { // Delete actions have a null selection. We do an early return for this because `hydrateRecordArray` will fail // if there's nothing at `mutationResult[modelSelectionField]`, but the caller isn't expecting a return (void). if (defaultSelection == null) return; if (!hasReturnType) { - return hydrateRecord(response, record[modelSelectionField]); + return hydrateRecord(response, record[modelSelectionField], modelManager); } else if (typeof hasReturnType == "boolean") { return record.result; } else { const innerReturnType = returnTypeForRecord(record, hasReturnType); - return processActionResponse(defaultSelection, response, record, modelSelectionField, innerReturnType); + return processActionResponse(defaultSelection, response, record, modelSelectionField, innerReturnType, modelManager); } }; @@ -435,7 +437,8 @@ export const backgroundActionResultRunner = async < connection: GadgetConnection, id: string, action: Action, - options?: Options + options?: Options, + modelManager?: AnyModelManager ): Promise> => { const plan = backgroundActionResultOperation(id, action, options); const subscription = connection.currentClient.subscription(plan.query, plan.variables); @@ -458,7 +461,8 @@ export const backgroundActionResultRunner = async < response.data, backgroundAction.result, action.isBulk ? action.modelApiIdentifier : action.modelSelectionField, - action.hasReturnType + action.hasReturnType, + modelManager ); break; } diff --git a/packages/api-client-core/src/support.ts b/packages/api-client-core/src/support.ts index d4188211e..dd589ae55 100644 --- a/packages/api-client-core/src/support.ts +++ b/packages/api-client-core/src/support.ts @@ -1,6 +1,7 @@ import type { OperationResult } from "@urql/core"; import { CombinedError } from "@urql/core"; import { Call, type FieldSelection as BuilderFieldSelection } from "tiny-graphql-query-compiler"; +import type { AnyModelManager } from "./AnyModelManager.js"; import { DataHydrator } from "./DataHydrator.js"; import type { ActionFunctionMetadata, AnyActionFunction } from "./GadgetFunctions.js"; import type { RecordShape } from "./GadgetRecord.js"; @@ -386,25 +387,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); }; const objObjType = "[object Object]"; diff --git a/packages/api-client-core/src/types.ts b/packages/api-client-core/src/types.ts index 69baf1455..b5693387b 100644 --- a/packages/api-client-core/src/types.ts +++ b/packages/api-client-core/src/types.ts @@ -700,7 +700,7 @@ export interface InternalFindListOptions { * What fields to retrieve from the API for this API call * __Note__: This selection is different than the top level select option -- it just accepts a list of string fields, and not a nested selection. To use a nested selection, use the top level API. **/ - select?: InternalFieldSelection; + select?: AnySelection | InternalFieldSelection | null; } /** Options for functions that return a paginated list of records from an InternalModelManager */ diff --git a/packages/react/src/auth/useSession.ts b/packages/react/src/auth/useSession.ts index fc7355536..b02039fb4 100644 --- a/packages/react/src/auth/useSession.ts +++ b/packages/react/src/auth/useSession.ts @@ -1,5 +1,6 @@ import type { AnyClient, + AnyPublicSingletonModelManager, DefaultSelection, FindManyFunction, GadgetRecord, @@ -16,7 +17,7 @@ export type GadgetSession = GadgetRecord>; export type GadgetUser = GadgetRecord>; export type ClientWithSessionAndUserManagers = AnyClient & { - currentSession: { get: GetFunction }; + currentSession: AnyPublicSingletonModelManager>; user: { findMany: FindManyFunction }; }; diff --git a/packages/react/src/auto/AutoTable.tsx b/packages/react/src/auto/AutoTable.tsx index dc57d9d01..80b2ea296 100644 --- a/packages/react/src/auto/AutoTable.tsx +++ b/packages/react/src/auto/AutoTable.tsx @@ -1,4 +1,4 @@ -import type { FindManyFunction, GadgetRecord } from "@gadgetinc/api-client-core"; +import type { AnyFindOneFunc, AnyPublicModelManager, FindManyFunction, GadgetRecord } from "@gadgetinc/api-client-core"; import { type DefaultSelection, type Select } from "@gadgetinc/api-client-core"; import type { TableOptions, TableRow } from "../use-table/types.js"; import type { OptionsType } from "../utils.js"; @@ -12,7 +12,7 @@ export type AutoTableProps< FinderFunction extends FindManyFunction, Options extends FinderFunction["optionsType"] > = { - model: { findMany: FinderFunction }; + model: { findMany: FinderFunction } & AnyPublicModelManager; select?: Options["select"]; pageSize?: number; initialCursor?: string; diff --git a/packages/react/src/useAction.ts b/packages/react/src/useAction.ts index 3f37c84cb..ea2ff907b 100644 --- a/packages/react/src/useAction.ts +++ b/packages/react/src/useAction.ts @@ -148,7 +148,14 @@ const processResult = ( if (errors && errors[0]) { error = ErrorWrapper.forErrorsResponse(errors, error?.response); } else { - data = processActionResponse(action.defaultSelection, result, mutationData, action.modelSelectionField, action.hasReturnType); + data = processActionResponse( + action.defaultSelection, + result, + mutationData, + action.modelSelectionField, + action.hasReturnType, + action.modelManager + ); } } } diff --git a/packages/react/src/useBulkAction.ts b/packages/react/src/useBulkAction.ts index ca9ce38e9..476daf6a9 100644 --- a/packages/react/src/useBulkAction.ts +++ b/packages/react/src/useBulkAction.ts @@ -144,7 +144,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); } } else { // Delete action diff --git a/packages/react/src/useFindBy.ts b/packages/react/src/useFindBy.ts index 2122ce8c4..36d90aac4 100644 --- a/packages/react/src/useFindBy.ts +++ b/packages/react/src/useFindBy.ts @@ -71,7 +71,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 cfae2d3f7..883346a50 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 { + AnyPublicModelManager, + DefaultSelection, + FindFirstFunction, + GadgetRecord, + LimitToKnownKeys, + Select, +} from "@gadgetinc/api-client-core"; import { findManyOperation, get, hydrateConnection, namespaceDataPath } 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 } & AnyPublicModelManager, options?: LimitToKnownKeys ): ReadHookResult< GadgetRecord, DefaultSelection>> @@ -61,7 +68,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 e71ebddb3..63657bddd 100644 --- a/packages/react/src/useFindMany.ts +++ b/packages/react/src/useFindMany.ts @@ -1,4 +1,11 @@ -import type { AnyModelManager, DefaultSelection, FindManyFunction, LimitToKnownKeys, Select } from "@gadgetinc/api-client-core"; +import type { + AnyPublicModelManager, + DefaultSelection, + FindManyFunction, + FindOneFunction, + LimitToKnownKeys, + Select, +} from "@gadgetinc/api-client-core"; import { GadgetRecordList, findManyOperation, get, hydrateConnection, namespaceDataPath } from "@gadgetinc/api-client-core"; import { useMemo } from "react"; import { useGadgetQuery } from "./useGadgetQuery.js"; @@ -36,7 +43,7 @@ export const useFindMany = < F extends FindManyFunction, Options extends F["optionsType"] & ReadOperationOptions >( - manager: { findMany: F }, + manager: { findMany: F } & AnyPublicModelManager, F>, options?: LimitToKnownKeys ): ReadHookResult< GadgetRecordList, DefaultSelection>> @@ -60,8 +67,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 as unknown as AnyPublicModelManager, records, connection); } } diff --git a/packages/react/src/useFindOne.ts b/packages/react/src/useFindOne.ts index acd65b4d0..812f5c298 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 { + AnyPublicModelManager, + DefaultSelection, + FindOneFunction, + GadgetRecord, + LimitToKnownKeys, + Select, +} from "@gadgetinc/api-client-core"; import { findOneOperation, get, hydrateRecord, namespaceDataPath } 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 } & AnyPublicModelManager, id: string, options?: LimitToKnownKeys ): ReadHookResult< @@ -61,7 +68,7 @@ export const useFindOne = < 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, options?.pause); diff --git a/packages/react/src/useGet.ts b/packages/react/src/useGet.ts index 104934419..d6bcf49fc 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, namespaceDataPath } 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>> @@ -59,7 +66,7 @@ export const useGet = < let data = null; const rawRecord = rawResult.data && get(rawResult.data, dataPath); if (rawRecord) { - data = hydrateRecord(rawResult, rawRecord); + data = hydrateRecord(rawResult, rawRecord, manager); } const error = ErrorWrapper.forMaybeCombinedError(rawResult.error); diff --git a/packages/react/src/useList.ts b/packages/react/src/useList.ts index 49958bae7..1b4c17e95 100644 --- a/packages/react/src/useList.ts +++ b/packages/react/src/useList.ts @@ -1,4 +1,12 @@ -import type { DefaultSelection, FindManyFunction, GadgetRecord, LimitToKnownKeys, Select } from "@gadgetinc/api-client-core"; +import type { + AnyFindOneFunc, + AnyPublicModelManager, + DefaultSelection, + FindManyFunction, + GadgetRecord, + LimitToKnownKeys, + Select, +} from "@gadgetinc/api-client-core"; import type { OperationContext } from "@urql/core"; import { useCallback, useMemo, useState } from "react"; import type { SearchResult } from "./useDebouncedSearch.js"; @@ -46,7 +54,7 @@ export const useList = < F extends FindManyFunction, Options extends F["optionsType"] & ReadOperationOptions & ListOptions >( - manager: { findMany: F }, + manager: { findMany: F } & AnyPublicModelManager, options?: LimitToKnownKeys ): ListResult< Array< @@ -93,6 +101,7 @@ export const useList = < ...(search.debouncedValue && { search: search.debouncedValue }), }); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion const selection = useSelectedRecordsController({ currentPageIds: data?.map((record) => (record as any).id) ?? [] }); const goToNextPage = useCallback(() => { diff --git a/packages/react/src/useMaybeFindFirst.ts b/packages/react/src/useMaybeFindFirst.ts index e2702519b..a84e9f759 100644 --- a/packages/react/src/useMaybeFindFirst.ts +++ b/packages/react/src/useMaybeFindFirst.ts @@ -1,4 +1,13 @@ -import type { DefaultSelection, FindFirstFunction, GadgetRecord, LimitToKnownKeys, Select } from "@gadgetinc/api-client-core"; +import type { + AnyFindManyFunc, + AnyFindOneFunc, + AnyPublicModelManager, + DefaultSelection, + FindFirstFunction, + GadgetRecord, + LimitToKnownKeys, + Select, +} from "@gadgetinc/api-client-core"; import { findManyOperation, get, hydrateConnection, namespaceDataPath } from "@gadgetinc/api-client-core"; import { useMemo } from "react"; import { useGadgetQuery } from "./useGadgetQuery.js"; @@ -36,7 +45,7 @@ export const useMaybeFindFirst = < F extends FindFirstFunction, Options extends F["optionsType"] & ReadOperationOptions >( - manager: { findFirst: F }, + manager: { findFirst: F } & AnyPublicModelManager, options?: LimitToKnownKeys ): ReadHookResult, DefaultSelection> @@ -61,7 +70,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 cf9477db8..153d79b53 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, namespaceDataPath } 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, Options extends F["optionsType"] & ReadOperationOptions & TableOptions >( - manager: { findMany: F }, + manager: { findMany: F } & AnyPublicModelManager, options?: LimitToKnownKeys< Options, Omit & ReadOperationOptions & TableOptions