From 29bf79ae0e36a7e941008b4a4f7bff4e3e23397b Mon Sep 17 00:00:00 2001 From: Ola Okelola Date: Tue, 15 Aug 2023 10:46:18 -0700 Subject: [PATCH] ability to have a mutation fail silently / return null --- examples/simple/package-lock.json | 14 +- examples/simple/package.json | 2 +- .../custom_edit_holiday_action_base.ts | 173 ++++++++++++++++++ .../actions/custom_edit_holiday_action.ts | 12 ++ .../holiday/holiday_custom_edit_type.ts | 102 +++++++++++ .../generated/mutations/mutation_type.ts | 2 + .../simple/src/graphql/generated/schema.gql | 14 ++ .../simple/src/graphql/generated/schema.ts | 6 + .../src/graphql/tests/holiday_type.test.ts | 19 +- examples/simple/src/schema/holiday_schema.ts | 9 + examples/todo-sqlite/package-lock.json | 14 +- examples/todo-sqlite/package.json | 2 +- .../todo/actions/todo_add_tag_action_base.ts | 11 +- .../todo-sqlite/src/ent/tests/tag.test.ts | 8 +- .../mutations/todo/add_todo_tag_type.ts | 6 +- .../src/graphql/generated/schema.gql | 2 +- .../todo-sqlite/src/schema/todo_schema.ts | 1 + internal/action/action.go | 8 +- internal/action/interface.go | 6 + internal/edge/edge.go | 2 + internal/graphql/generate_ts_code.go | 47 +++-- internal/schema/input/input.go | 2 + internal/tscode/action_base.tmpl | 46 ++++- ts/package.json | 2 +- ts/src/schema/schema.ts | 11 ++ 25 files changed, 466 insertions(+), 55 deletions(-) create mode 100644 examples/simple/src/ent/generated/holiday/actions/custom_edit_holiday_action_base.ts create mode 100644 examples/simple/src/ent/holiday/actions/custom_edit_holiday_action.ts create mode 100644 examples/simple/src/graphql/generated/mutations/holiday/holiday_custom_edit_type.ts diff --git a/examples/simple/package-lock.json b/examples/simple/package-lock.json index f2df07d01..d81c23150 100644 --- a/examples/simple/package-lock.json +++ b/examples/simple/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.1", "license": "ISC", "dependencies": { - "@snowtop/ent": "^0.1.8", + "@snowtop/ent": "^0.1.9", "@snowtop/ent-email": "^0.1.0-rc1", "@snowtop/ent-passport": "^0.1.0-rc1", "@snowtop/ent-password": "^0.1.0-rc1", @@ -1233,9 +1233,9 @@ } }, "node_modules/@snowtop/ent": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@snowtop/ent/-/ent-0.1.8.tgz", - "integrity": "sha512-vr2yiUazqObBD5YnK9mfEXPPUhCaszf42rj+Qy4hKQjL+Eh0yS5Eu7qH6eZWNlPb/RGFDs/Z5x0crsimQ5HUqQ==", + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@snowtop/ent/-/ent-0.1.9.tgz", + "integrity": "sha512-Xr7cS4ejO9LHOhC/UIfOc4l8SkDleOfojrsJpWIff5vdMfzLeH+cF4CFW19Culv3rQDhDyBz9IhzrGjpwFi71A==", "dependencies": { "@types/node": "^20.2.5", "camel-case": "^4.1.2", @@ -7413,9 +7413,9 @@ } }, "@snowtop/ent": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/@snowtop/ent/-/ent-0.1.8.tgz", - "integrity": "sha512-vr2yiUazqObBD5YnK9mfEXPPUhCaszf42rj+Qy4hKQjL+Eh0yS5Eu7qH6eZWNlPb/RGFDs/Z5x0crsimQ5HUqQ==", + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@snowtop/ent/-/ent-0.1.9.tgz", + "integrity": "sha512-Xr7cS4ejO9LHOhC/UIfOc4l8SkDleOfojrsJpWIff5vdMfzLeH+cF4CFW19Culv3rQDhDyBz9IhzrGjpwFi71A==", "requires": { "@types/node": "^20.2.5", "camel-case": "^4.1.2", diff --git a/examples/simple/package.json b/examples/simple/package.json index f6a86131f..1f99cbe4c 100644 --- a/examples/simple/package.json +++ b/examples/simple/package.json @@ -33,7 +33,7 @@ "tsconfig-paths": "^3.11.0" }, "dependencies": { - "@snowtop/ent": "^0.1.8", + "@snowtop/ent": "^0.1.9", "@snowtop/ent-email": "^0.1.0-rc1", "@snowtop/ent-passport": "^0.1.0-rc1", "@snowtop/ent-password": "^0.1.0-rc1", diff --git a/examples/simple/src/ent/generated/holiday/actions/custom_edit_holiday_action_base.ts b/examples/simple/src/ent/generated/holiday/actions/custom_edit_holiday_action_base.ts new file mode 100644 index 000000000..c7c26b452 --- /dev/null +++ b/examples/simple/src/ent/generated/holiday/actions/custom_edit_holiday_action_base.ts @@ -0,0 +1,173 @@ +/** + * Copyright whaa whaa + * Generated by github.com/lolopinto/ent/ent, DO NOT EDIT. + */ + +import { + AllowIfViewerHasIdentityPrivacyPolicy, + ID, + PrivacyPolicy, +} from "@snowtop/ent"; +import { + Action, + Changeset, + ChangesetOptions, + Observer, + Trigger, + Validator, + WriteOperation, +} from "@snowtop/ent/action"; +import { Holiday } from "../../.."; +import { HolidayBuilder } from "./holiday_builder"; +import { DayOfWeek, DayOfWeekAlt } from "../../types"; +import { ExampleViewer as ExampleViewerAlias } from "../../../../viewer/viewer"; + +export interface CustomEditHolidayInput { + dayOfWeek?: DayOfWeek; + dayOfWeekAlt?: DayOfWeekAlt; + label?: string; + date?: Date; +} + +export type CustomEditHolidayActionTriggers = ( + | Trigger< + Holiday, + HolidayBuilder, + ExampleViewerAlias, + CustomEditHolidayInput, + Holiday + > + | Trigger< + Holiday, + HolidayBuilder, + ExampleViewerAlias, + CustomEditHolidayInput, + Holiday + >[] +)[]; + +export type CustomEditHolidayActionObservers = Observer< + Holiday, + HolidayBuilder, + ExampleViewerAlias, + CustomEditHolidayInput, + Holiday +>[]; + +export type CustomEditHolidayActionValidators = Validator< + Holiday, + HolidayBuilder, + ExampleViewerAlias, + CustomEditHolidayInput, + Holiday +>[]; + +export class CustomEditHolidayActionBase + implements + Action< + Holiday, + HolidayBuilder, + ExampleViewerAlias, + CustomEditHolidayInput, + Holiday + > +{ + public readonly builder: HolidayBuilder; + public readonly viewer: ExampleViewerAlias; + protected input: CustomEditHolidayInput; + protected readonly holiday: Holiday; + + constructor( + viewer: ExampleViewerAlias, + holiday: Holiday, + input: CustomEditHolidayInput, + ) { + this.viewer = viewer; + this.input = input; + this.builder = new HolidayBuilder( + this.viewer, + WriteOperation.Edit, + this, + holiday, + ); + this.holiday = holiday; + } + + getPrivacyPolicy(): PrivacyPolicy { + return AllowIfViewerHasIdentityPrivacyPolicy; + } + + getTriggers(): CustomEditHolidayActionTriggers { + return []; + } + + getObservers(): CustomEditHolidayActionObservers { + return []; + } + + getValidators(): CustomEditHolidayActionValidators { + return []; + } + + getInput(): CustomEditHolidayInput { + return this.input; + } + + async changeset(): Promise { + return this.builder.build(); + } + + async changesetWithOptions_BETA( + options: ChangesetOptions, + ): Promise { + return this.builder.buildWithOptions_BETA(options); + } + + async valid(): Promise { + return this.builder.valid(); + } + + async validX(): Promise { + await this.builder.validX(); + } + + async save(): Promise { + await this.builder.save(); + return this.builder.editedEnt(); + } + + async saveX(): Promise { + await this.builder.saveX(); + return this.builder.editedEntX(); + } + + static create( + this: new ( + viewer: ExampleViewerAlias, + holiday: Holiday, + input: CustomEditHolidayInput, + ) => T, + viewer: ExampleViewerAlias, + holiday: Holiday, + input: CustomEditHolidayInput, + ): T { + return new this(viewer, holiday, input); + } + + static async saveFromID( + this: new ( + viewer: ExampleViewerAlias, + holiday: Holiday, + input: CustomEditHolidayInput, + ) => T, + viewer: ExampleViewerAlias, + id: ID, + input: CustomEditHolidayInput, + ): Promise { + const holiday = await Holiday.load(viewer, id); + if (holiday === null) { + return null; + } + return new this(viewer, holiday, input).save(); + } +} diff --git a/examples/simple/src/ent/holiday/actions/custom_edit_holiday_action.ts b/examples/simple/src/ent/holiday/actions/custom_edit_holiday_action.ts new file mode 100644 index 000000000..1ff8688d4 --- /dev/null +++ b/examples/simple/src/ent/holiday/actions/custom_edit_holiday_action.ts @@ -0,0 +1,12 @@ +/** + * Copyright whaa whaa + */ + +import { + CustomEditHolidayActionBase, + CustomEditHolidayInput, +} from "../../generated/holiday/actions/custom_edit_holiday_action_base"; + +export { CustomEditHolidayInput }; + +export default class CustomEditHolidayAction extends CustomEditHolidayActionBase {} diff --git a/examples/simple/src/graphql/generated/mutations/holiday/holiday_custom_edit_type.ts b/examples/simple/src/graphql/generated/mutations/holiday/holiday_custom_edit_type.ts new file mode 100644 index 000000000..d679eae6b --- /dev/null +++ b/examples/simple/src/graphql/generated/mutations/holiday/holiday_custom_edit_type.ts @@ -0,0 +1,102 @@ +/** + * Copyright whaa whaa + * Generated by github.com/lolopinto/ent/ent, DO NOT EDIT. + */ + +import { + GraphQLFieldConfig, + GraphQLFieldConfigMap, + GraphQLID, + GraphQLInputFieldConfigMap, + GraphQLInputObjectType, + GraphQLNonNull, + GraphQLObjectType, + GraphQLResolveInfo, + GraphQLString, +} from "graphql"; +import { RequestContext } from "@snowtop/ent"; +import { GraphQLTime, mustDecodeIDFromGQLID } from "@snowtop/ent/graphql"; +import { Holiday } from "../../../../ent"; +import CustomEditHolidayAction, { + CustomEditHolidayInput, +} from "../../../../ent/holiday/actions/custom_edit_holiday_action"; +import { + DayOfWeekAltType, + DayOfWeekType, + HolidayType, +} from "../../../resolvers"; +import { ExampleViewer as ExampleViewerAlias } from "../../../../viewer/viewer"; + +interface customCustomEditHolidayInput extends CustomEditHolidayInput { + id: string; +} + +interface CustomEditHolidayPayload { + holiday: Holiday | null; +} + +export const CustomEditHolidayInputType = new GraphQLInputObjectType({ + name: "CustomEditHolidayInput", + fields: (): GraphQLInputFieldConfigMap => ({ + id: { + description: "id of Holiday", + type: new GraphQLNonNull(GraphQLID), + }, + dayOfWeek: { + type: DayOfWeekType, + }, + dayOfWeekAlt: { + type: DayOfWeekAltType, + }, + label: { + type: GraphQLString, + }, + date: { + type: GraphQLTime, + }, + }), +}); + +export const CustomEditHolidayPayloadType = new GraphQLObjectType({ + name: "CustomEditHolidayPayload", + fields: (): GraphQLFieldConfigMap< + CustomEditHolidayPayload, + RequestContext + > => ({ + holiday: { + type: HolidayType, + }, + }), +}); + +export const HolidayCustomEditType: GraphQLFieldConfig< + undefined, + RequestContext, + { [input: string]: customCustomEditHolidayInput } +> = { + type: new GraphQLNonNull(CustomEditHolidayPayloadType), + args: { + input: { + description: "", + type: new GraphQLNonNull(CustomEditHolidayInputType), + }, + }, + resolve: async ( + _source, + { input }, + context: RequestContext, + _info: GraphQLResolveInfo, + ): Promise => { + const holiday = await CustomEditHolidayAction.saveFromID( + context.getViewer(), + mustDecodeIDFromGQLID(input.id), + { + dayOfWeek: input.dayOfWeek, + dayOfWeekAlt: input.dayOfWeekAlt, + label: input.label, + date: input.date, + }, + ); + return { holiday }; + }, +}; diff --git a/examples/simple/src/graphql/generated/mutations/mutation_type.ts b/examples/simple/src/graphql/generated/mutations/mutation_type.ts index 0c3113250..32f6e14a9 100644 --- a/examples/simple/src/graphql/generated/mutations/mutation_type.ts +++ b/examples/simple/src/graphql/generated/mutations/mutation_type.ts @@ -25,6 +25,7 @@ import { EventRemoveHostType } from "./event/event_remove_host_type"; import { EventRsvpStatusClearType } from "./event/event_rsvp_status_clear_type"; import { EventRsvpStatusEditType } from "./event/event_rsvp_status_edit_type"; import { HolidayCreateType } from "./holiday/holiday_create_type"; +import { HolidayCustomEditType } from "./holiday/holiday_custom_edit_type"; import { HoursOfOperationCreateType } from "./hours_of_operation/hours_of_operation_create_type"; import { ConfirmEmailAddressEditType } from "./user/confirm_email_address_edit_type"; import { ConfirmPhoneNumberEditType } from "./user/confirm_phone_number_edit_type"; @@ -64,6 +65,7 @@ export const MutationType = new GraphQLObjectType({ eventRsvpStatusClear: EventRsvpStatusClearType, eventRsvpStatusEdit: EventRsvpStatusEditType, holidayCreate: HolidayCreateType, + holidayCustomEdit: HolidayCustomEditType, hoursOfOperationCreate: HoursOfOperationCreateType, phoneNumberEdit: PhoneNumberEditType, userAuth: UserAuthType, diff --git a/examples/simple/src/graphql/generated/schema.gql b/examples/simple/src/graphql/generated/schema.gql index 8fe7cd652..a9c1e350e 100644 --- a/examples/simple/src/graphql/generated/schema.gql +++ b/examples/simple/src/graphql/generated/schema.gql @@ -1134,6 +1134,19 @@ type ContactPhoneNumberEditPayload { contactPhoneNumber: ContactPhoneNumber! } +input CustomEditHolidayInput { + """id of Holiday""" + id: ID! + dayOfWeek: DayOfWeek + dayOfWeekAlt: DayOfWeekAlt + label: String + date: Time +} + +type CustomEditHolidayPayload { + holiday: Holiday +} + type DeleteUserInput2Payload { deletedUserID: ID } @@ -1461,6 +1474,7 @@ type Mutation { eventRsvpStatusClear(input: ClearEventRsvpStatusInput!): ClearEventRsvpStatusPayload! eventRsvpStatusEdit(input: EventRsvpStatusEditInput!): EventRsvpStatusEditPayload! holidayCreate(input: HolidayCreateInput!): HolidayCreatePayload! + holidayCustomEdit(input: CustomEditHolidayInput!): CustomEditHolidayPayload! hoursOfOperationCreate(input: HoursOfOperationCreateInput!): HoursOfOperationCreatePayload! phoneNumberEdit(input: EditPhoneNumberInput!): EditPhoneNumberPayload! userAuth(input: UserAuthInput!): UserAuthPayload! diff --git a/examples/simple/src/graphql/generated/schema.ts b/examples/simple/src/graphql/generated/schema.ts index 39af41cee..5d43a962c 100644 --- a/examples/simple/src/graphql/generated/schema.ts +++ b/examples/simple/src/graphql/generated/schema.ts @@ -86,6 +86,10 @@ import { HolidayCreateInputType, HolidayCreatePayloadType, } from "./mutations/holiday/holiday_create_type"; +import { + CustomEditHolidayInputType, + CustomEditHolidayPayloadType, +} from "./mutations/holiday/holiday_custom_edit_type"; import { HoursOfOperationCreateInputType, HoursOfOperationCreatePayloadType, @@ -364,6 +368,8 @@ export default new GraphQLSchema({ ContactPhoneNumberDeletePayloadType, ContactPhoneNumberEditInputType, ContactPhoneNumberEditPayloadType, + CustomEditHolidayInputType, + CustomEditHolidayPayloadType, DeleteUserInput2PayloadType, DeleteUserInput2Type, EditEmailAddressInputType, diff --git a/examples/simple/src/graphql/tests/holiday_type.test.ts b/examples/simple/src/graphql/tests/holiday_type.test.ts index a8caef664..33f67f9ce 100644 --- a/examples/simple/src/graphql/tests/holiday_type.test.ts +++ b/examples/simple/src/graphql/tests/holiday_type.test.ts @@ -2,12 +2,12 @@ import { ID } from "@snowtop/ent"; import { expectMutation } from "@snowtop/ent-graphql-tests"; import schema from "../generated/schema"; import { DateTime } from "luxon"; -import { mustDecodeIDFromGQLID } from "@snowtop/ent/graphql"; +import { encodeGQLID, mustDecodeIDFromGQLID } from "@snowtop/ent/graphql"; import { Holiday } from "src/ent"; import { LoggedOutExampleViewer } from "../../viewer/viewer"; import { DayOfWeek, DayOfWeekAlt } from "../../ent/generated/types"; -test("create holiday", async () => { +test("create and edit holiday", async () => { let id: ID; const dt = DateTime.fromISO("2021-01-20"); @@ -40,4 +40,19 @@ test("create holiday", async () => { const ent = await Holiday.loadX(new LoggedOutExampleViewer(), id!); expect(ent.dayOfWeek).toBe(DayOfWeek.Wednesday); expect(ent.dayOfWeekAlt).toBe(DayOfWeekAlt.Wednesday); + + await expectMutation( + { + mutation: "holidayCustomEdit", + schema, + viewer: new LoggedOutExampleViewer(), + args: { + id: encodeGQLID(ent), + label: "Inauguration2", + }, + // ok to be safely null since logged out can't do it + nullQueryPaths: ["holiday"], + }, + ["holiday.label", "Inauguration2"], + ); }); diff --git a/examples/simple/src/schema/holiday_schema.ts b/examples/simple/src/schema/holiday_schema.ts index 084522489..9daf6d94c 100644 --- a/examples/simple/src/schema/holiday_schema.ts +++ b/examples/simple/src/schema/holiday_schema.ts @@ -53,6 +53,8 @@ const HolidaySchema = new EntSchemaWithTZ({ inputName: "CustomCreateHolidayInput2", actionName: "CustomCreateHolidayAction2", hideFromGraphQL: true, + // todo not supported yet + // __canFailBETA: true, // this action exists just to test ID list action only field actionOnlyFields: [ { @@ -63,6 +65,13 @@ const HolidaySchema = new EntSchemaWithTZ({ }, ], }, + { + operation: ActionOperation.Edit, + inputName: "CustomEditHolidayInput", + actionName: "CustomEditHolidayAction", + __canFailBETA: true, + graphQLName: "holidayCustomEdit", + }, ], }); export default HolidaySchema; diff --git a/examples/todo-sqlite/package-lock.json b/examples/todo-sqlite/package-lock.json index fc3110d3a..1323760a5 100644 --- a/examples/todo-sqlite/package-lock.json +++ b/examples/todo-sqlite/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.1", "license": "ISC", "dependencies": { - "@snowtop/ent": "^0.1.0-rc1", + "@snowtop/ent": "^0.1.9", "@snowtop/ent-phonenumber": "^0.1.0-rc1", "@snowtop/ent-soft-delete": "^0.1.0-rc1", "@types/node": "^15.0.3", @@ -1172,9 +1172,9 @@ } }, "node_modules/@snowtop/ent": { - "version": "0.1.0-rc1", - "resolved": "https://registry.npmjs.org/@snowtop/ent/-/ent-0.1.0-rc1.tgz", - "integrity": "sha512-ln/fhvdyPthW7aTUeew9zSuGfs6bkgCuG7qEHHIU86u730GFnHyMjDN0I20rdBnC6VwoSmjKswvQg/fxaQxIWQ==", + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@snowtop/ent/-/ent-0.1.9.tgz", + "integrity": "sha512-Xr7cS4ejO9LHOhC/UIfOc4l8SkDleOfojrsJpWIff5vdMfzLeH+cF4CFW19Culv3rQDhDyBz9IhzrGjpwFi71A==", "dependencies": { "@types/node": "^20.2.5", "camel-case": "^4.1.2", @@ -6945,9 +6945,9 @@ } }, "@snowtop/ent": { - "version": "0.1.0-rc1", - "resolved": "https://registry.npmjs.org/@snowtop/ent/-/ent-0.1.0-rc1.tgz", - "integrity": "sha512-ln/fhvdyPthW7aTUeew9zSuGfs6bkgCuG7qEHHIU86u730GFnHyMjDN0I20rdBnC6VwoSmjKswvQg/fxaQxIWQ==", + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@snowtop/ent/-/ent-0.1.9.tgz", + "integrity": "sha512-Xr7cS4ejO9LHOhC/UIfOc4l8SkDleOfojrsJpWIff5vdMfzLeH+cF4CFW19Culv3rQDhDyBz9IhzrGjpwFi71A==", "requires": { "@types/node": "^20.2.5", "camel-case": "^4.1.2", diff --git a/examples/todo-sqlite/package.json b/examples/todo-sqlite/package.json index 5b6283b21..60322fcd3 100644 --- a/examples/todo-sqlite/package.json +++ b/examples/todo-sqlite/package.json @@ -37,7 +37,7 @@ "uuid": "^8.3.2" }, "dependencies": { - "@snowtop/ent": "^0.1.0-rc1", + "@snowtop/ent": "^0.1.9", "@snowtop/ent-phonenumber": "^0.1.0-rc1", "@snowtop/ent-soft-delete": "^0.1.0-rc1", "@types/node": "^15.0.3", diff --git a/examples/todo-sqlite/src/ent/generated/todo/actions/todo_add_tag_action_base.ts b/examples/todo-sqlite/src/ent/generated/todo/actions/todo_add_tag_action_base.ts index 0721762ac..acadfcc59 100644 --- a/examples/todo-sqlite/src/ent/generated/todo/actions/todo_add_tag_action_base.ts +++ b/examples/todo-sqlite/src/ent/generated/todo/actions/todo_add_tag_action_base.ts @@ -130,7 +130,7 @@ export class TodoAddTagActionBase return new this(viewer, todo); } - static async saveXFromID( + static async saveFromID( this: new ( viewer: Viewer, todo: Todo, @@ -138,8 +138,11 @@ export class TodoAddTagActionBase viewer: Viewer, id: ID, tagID: ID, - ): Promise { - const todo = await Todo.loadX(viewer, id); - return new this(viewer, todo).addTag(tagID).saveX(); + ): Promise { + const todo = await Todo.load(viewer, id); + if (todo === null) { + return null; + } + return new this(viewer, todo).addTag(tagID).save(); } } diff --git a/examples/todo-sqlite/src/ent/tests/tag.test.ts b/examples/todo-sqlite/src/ent/tests/tag.test.ts index 5e4fb7376..7ea314c99 100644 --- a/examples/todo-sqlite/src/ent/tests/tag.test.ts +++ b/examples/todo-sqlite/src/ent/tests/tag.test.ts @@ -69,9 +69,9 @@ describe("tag + todo", () => { const tag1 = await createTag("kids", account); const tag2 = await createTag("work", account); - await TodoAddTagAction.saveXFromID(account.viewer, todo1.id, tag1.id); - await TodoAddTagAction.saveXFromID(account.viewer, todo1.id, tag2.id); - await TodoAddTagAction.saveXFromID(account.viewer, todo2.id, tag2.id); + await TodoAddTagAction.saveFromID(account.viewer, todo1.id, tag1.id); + await TodoAddTagAction.saveFromID(account.viewer, todo1.id, tag2.id); + await TodoAddTagAction.saveFromID(account.viewer, todo2.id, tag2.id); const count = await todo1.queryTags().queryRawCount(); expect(count).toBe(2); @@ -91,7 +91,7 @@ describe("tag + todo", () => { const todo = await createTodoForSelf({ creatorID: account.id }); const tag = await createTag("kids", account); - await TodoAddTagAction.saveXFromID(account.viewer, todo.id, tag.id); + await TodoAddTagAction.saveFromID(account.viewer, todo.id, tag.id); const count = await todo.queryTags().queryRawCount(); expect(count).toBe(1); diff --git a/examples/todo-sqlite/src/graphql/generated/mutations/todo/add_todo_tag_type.ts b/examples/todo-sqlite/src/graphql/generated/mutations/todo/add_todo_tag_type.ts index 21672badd..fb6097bae 100644 --- a/examples/todo-sqlite/src/graphql/generated/mutations/todo/add_todo_tag_type.ts +++ b/examples/todo-sqlite/src/graphql/generated/mutations/todo/add_todo_tag_type.ts @@ -21,7 +21,7 @@ interface customAddTodoTagInput { } interface AddTodoTagPayload { - todo: Todo; + todo: Todo | null; } export const AddTodoTagInputType = new GraphQLInputObjectType({ @@ -44,7 +44,7 @@ export const AddTodoTagPayloadType = new GraphQLObjectType({ RequestContext > => ({ todo: { - type: new GraphQLNonNull(TodoType), + type: TodoType, }, }), }); @@ -67,7 +67,7 @@ export const AddTodoTagType: GraphQLFieldConfig< context: RequestContext, _info: GraphQLResolveInfo, ): Promise => { - const todo = await TodoAddTagAction.saveXFromID( + const todo = await TodoAddTagAction.saveFromID( context.getViewer(), input.id, input.tag_id, diff --git a/examples/todo-sqlite/src/graphql/generated/schema.gql b/examples/todo-sqlite/src/graphql/generated/schema.gql index b0b7e8dca..2c40c764d 100644 --- a/examples/todo-sqlite/src/graphql/generated/schema.gql +++ b/examples/todo-sqlite/src/graphql/generated/schema.gql @@ -329,7 +329,7 @@ input AddTodoTagInput { } type AddTodoTagPayload { - todo: Todo! + todo: Todo } input ChangeTodoBountyInput { diff --git a/examples/todo-sqlite/src/schema/todo_schema.ts b/examples/todo-sqlite/src/schema/todo_schema.ts index 524136c90..1501cc754 100644 --- a/examples/todo-sqlite/src/schema/todo_schema.ts +++ b/examples/todo-sqlite/src/schema/todo_schema.ts @@ -65,6 +65,7 @@ const TodoSchema = new TodoBaseEntSchema({ edgeActions: [ { operation: ActionOperation.AddEdge, + __canFailBETA: true, }, { operation: ActionOperation.RemoveEdge, diff --git a/internal/action/action.go b/internal/action/action.go index 123239f51..3c927c7ec 100644 --- a/internal/action/action.go +++ b/internal/action/action.go @@ -53,7 +53,11 @@ func parseActionsFromInput(cfg codegenapi.Config, nodeName string, action *input opt, ) commonInfo.canViewerDo = action.CanViewerDo - + commonInfo.canFail = action.CanFail + _, ok = typ.(*createActionType) + if ok && commonInfo.canFail { + return nil, fmt.Errorf("can fail is currently not supported with create actions") + } return []Action{concreteAction.getAction(commonInfo)}, nil } @@ -329,6 +333,7 @@ func processEdgeActions(cfg codegenapi.Config, nodeName string, assocEdge *edge. lang, ) commonInfo.canViewerDo = edgeAction.CanViewerDo + commonInfo.canFail = edgeAction.CanFail actions[idx] = typ.getAction(commonInfo) @@ -427,6 +432,7 @@ func processEdgeGroupActions(cfg codegenapi.Config, nodeName string, assocGroup fields, ) commonInfo.canViewerDo = edgeAction.CanViewerDo + commonInfo.canFail = edgeAction.CanFail commonInfo.tsEnums = tsEnums commonInfo.gqlEnums = gqlEnums commonInfo.EdgeGroup = assocGroup diff --git a/internal/action/interface.go b/internal/action/interface.go index b318bf4e7..2b5d6867c 100644 --- a/internal/action/interface.go +++ b/internal/action/interface.go @@ -56,6 +56,7 @@ type Action interface { getCommonInfo() commonActionInfo TransformsDelete() bool GetCanViewerDo() *input.CanViewerDo + CanFail() bool } type ActionField interface { @@ -139,6 +140,7 @@ type commonActionInfo struct { nodeinfo.NodeInfo tranformsDelete bool canViewerDo *input.CanViewerDo + canFail bool } func (action *commonActionInfo) GetActionName() string { @@ -238,6 +240,10 @@ func (action *commonActionInfo) GetCanViewerDo() *input.CanViewerDo { return action.canViewerDo } +func (action *commonActionInfo) CanFail() bool { + return action.canFail +} + func getTypes(typ enttype.TSTypeWithCustomType) (string, string) { cti := typ.GetCustomTypeInfo() return cti.TSInterface, cti.GraphQLInterface diff --git a/internal/edge/edge.go b/internal/edge/edge.go index e89830673..7d6ac4042 100644 --- a/internal/edge/edge.go +++ b/internal/edge/edge.go @@ -1138,6 +1138,7 @@ type EdgeAction struct { ExposeToGraphQL bool ActionOnlyFields []*input.ActionField CanViewerDo *input.CanViewerDo + CanFail bool } type AssociationEdgeGroup struct { @@ -1308,6 +1309,7 @@ func edgeActionsFromInput(actions []*input.EdgeAction) ([]*EdgeAction, error) { Action: a, ActionOnlyFields: action.ActionOnlyFields, CanViewerDo: action.CanViewerDo, + CanFail: action.CanFail, } } return ret, nil diff --git a/internal/graphql/generate_ts_code.go b/internal/graphql/generate_ts_code.go index 434b33ab6..c08469263 100644 --- a/internal/graphql/generate_ts_code.go +++ b/internal/graphql/generate_ts_code.go @@ -2664,15 +2664,31 @@ func buildActionPayloadNode(processor *codegen.Processor, nodeData *schema.NodeD nodeInfo := a.GetNodeInfo() if a.GetOperation() != ent.DeleteAction { - if err := result.addField(&fieldType{ - Name: nodeInfo.NodeInstance, - FieldImports: []*tsimport.ImportPath{ + typ := nodeData.Node + var fieldImports []*tsimport.ImportPath + + if a.CanFail() { + typ = fmt.Sprintf("%s | null", typ) + + fieldImports = []*tsimport.ImportPath{ + { + Import: fmt.Sprintf("%sType", nodeInfo.Node), + ImportPath: codepath.GetImportPathForExternalGQLFile(), + }, + } + } else { + fieldImports = []*tsimport.ImportPath{ tsimport.NewGQLClassImportPath("GraphQLNonNull"), { Import: fmt.Sprintf("%sType", nodeInfo.Node), ImportPath: codepath.GetImportPathForExternalGQLFile(), }, - }, + } + } + + if err := result.addField(&fieldType{ + Name: nodeInfo.NodeInstance, + FieldImports: fieldImports, }); err != nil { return nil, err } @@ -2683,9 +2699,9 @@ func buildActionPayloadNode(processor *codegen.Processor, nodeData *schema.NodeD Name: payload, Fields: []*interfaceField{ { - Name: nodeData.NodeInstance, - Type: nodeData.Node, - UseImport: true, + Name: nodeData.NodeInstance, + Type: typ, + UseImports: []string{nodeData.Node}, }, }, }), @@ -2953,6 +2969,11 @@ func buildActionFieldConfig(processor *codegen.Processor, nodeData *schema.NodeD }) } + saveMethod := "saveXFromID" + if a.CanFail() { + saveMethod = "saveFromID" + } + idField, err := getIDField(processor, nodeData) if err != nil { return nil, err @@ -2970,12 +2991,12 @@ func buildActionFieldConfig(processor *codegen.Processor, nodeData *schema.NodeD if base64EncodeIDs { result.FunctionContents = append( result.FunctionContents, - fmt.Sprintf("await %s.saveXFromID(context.getViewer(), mustDecodeIDFromGQLID(input.%s), {", a.GetActionName(), idField), + fmt.Sprintf("await %s.%s(context.getViewer(), mustDecodeIDFromGQLID(input.%s), {", a.GetActionName(), saveMethod, idField), ) } else { result.FunctionContents = append( result.FunctionContents, - fmt.Sprintf("await %s.saveXFromID(context.getViewer(), input.%s, {", a.GetActionName(), idField), + fmt.Sprintf("await %s.%s(context.getViewer(), input.%s, {", a.GetActionName(), saveMethod, idField), ) } for _, f := range a.GetGraphQLFields() { @@ -2997,12 +3018,12 @@ func buildActionFieldConfig(processor *codegen.Processor, nodeData *schema.NodeD if base64EncodeIDs { result.FunctionContents = append( result.FunctionContents, - fmt.Sprintf("const %s = await %s.saveXFromID(context.getViewer(), mustDecodeIDFromGQLID(input.%s), mustDecodeIDFromGQLID(input.%s));", nodeData.NodeInstance, a.GetActionName(), idField, edgeField), + fmt.Sprintf("const %s = await %s.%s(context.getViewer(), mustDecodeIDFromGQLID(input.%s), mustDecodeIDFromGQLID(input.%s));", nodeData.NodeInstance, a.GetActionName(), saveMethod, idField, edgeField), ) } else { result.FunctionContents = append( result.FunctionContents, - fmt.Sprintf("const %s = await %s.saveXFromID(context.getViewer(), input.%s, input.%s);", nodeData.NodeInstance, a.GetActionName(), idField, edgeField), + fmt.Sprintf("const %s = await %s.%s(context.getViewer(), input.%s, input.%s);", nodeData.NodeInstance, a.GetActionName(), saveMethod, idField, edgeField), ) } } else { @@ -3016,13 +3037,13 @@ func buildActionFieldConfig(processor *codegen.Processor, nodeData *schema.NodeD // no fields result.FunctionContents = append( result.FunctionContents, - fmt.Sprintf("await %s.saveXFromID(context.getViewer(), mustDecodeIDFromGQLID(input.%s));", a.GetActionName(), idField), + fmt.Sprintf("await %s.%s(context.getViewer(), mustDecodeIDFromGQLID(input.%s));", a.GetActionName(), saveMethod, idField), ) } else { // no fields result.FunctionContents = append( result.FunctionContents, - fmt.Sprintf("await %s.saveXFromID(context.getViewer(), input.%s);", a.GetActionName(), idField), + fmt.Sprintf("await %s.%s(context.getViewer(), input.%s);", a.GetActionName(), saveMethod, idField), ) } } diff --git a/internal/schema/input/input.go b/internal/schema/input/input.go index f334278c4..97a227123 100644 --- a/internal/schema/input/input.go +++ b/internal/schema/input/input.go @@ -624,6 +624,7 @@ type EdgeAction struct { HideFromGraphQL bool `json:"hideFromGraphQL,omitempty"` ActionOnlyFields []*ActionField `json:"actionOnlyFields,omitempty"` CanViewerDo *CanViewerDo `json:"canViewerDo,omitempty"` + CanFail bool `json:"__canFailBETA,omitempty"` } func getTSStringOperation(op ent.ActionOperation) string { @@ -671,6 +672,7 @@ type Action struct { HideFromGraphQL bool `json:"hideFromGraphQL,omitempty"` ActionOnlyFields []*ActionField `json:"actionOnlyFields,omitempty"` CanViewerDo *CanViewerDo `json:"canViewerDo,omitempty"` + CanFail bool `json:"__canFailBETA,omitempty"` } func (a *Action) GetTSStringOperation() string { diff --git a/internal/tscode/action_base.tmpl b/internal/tscode/action_base.tmpl index bbccbee5e..5e66d8a1b 100644 --- a/internal/tscode/action_base.tmpl +++ b/internal/tscode/action_base.tmpl @@ -383,7 +383,18 @@ export class {{$actionName}} implements {{useImport "Action"}}<{{$node}}, {{useI {{if $hasSaveFromID -}} - static async saveXFromID( + + {{$name := "saveXFromID" }} + {{$returnType := $node}} + {{ if $action.CanFail }} + {{$name = "saveFromID" }} + {{$returnType = printf "%s | null" $node}} + {{ end }} + {{ if $action.IsDeletingNode }} + {{$returnType = "void"}} + {{ end }} + + static async {{$name}}( this: new ({{$constructor}}) => T, viewer: {{$viewerType}}, id: {{useImport "ID"}}, @@ -395,14 +406,21 @@ export class {{$actionName}} implements {{useImport "Action"}}<{{$node}}, {{useI {{$edge.TSNodeID}}: {{useImport "ID"}}, {{end -}} {{ end -}} - {{ if $action.IsDeletingNode -}} - ): Promise { - {{else -}} - ): Promise<{{$node}}> { - {{end -}} - const {{$instance}} = await {{$node}}.loadX(viewer, id); + ): Promise<{{$returnType}}> { + {{ if $action.CanFail -}} + const {{$instance}} = await {{$node}}.load(viewer, id); + if ({{$instance}} === null) { + return null; + } + {{ else -}} + const {{$instance}} = await {{$node}}.loadX(viewer, id); + {{ end -}} {{if $hasInput -}} - return new this(viewer, {{$instance}}, input).saveX(); + {{ if $action.CanFail -}} + return new this(viewer, {{$instance}}, input).save(); + {{else -}} + return new this(viewer, {{$instance}}, input).saveX(); + {{end -}} {{else if edgeAction $action -}} return new this(viewer, {{$instance}}) {{ range $edge := $edges -}} @@ -413,9 +431,17 @@ export class {{$actionName}} implements {{useImport "Action"}}<{{$node}}, {{useI .{{$edge.TSAddMethodName}}({{$edge.TSNodeID}}) {{end -}} {{end -}} - .saveX(); + {{ if $action.CanFail -}} + .save(); + {{else -}} + .saveX(); + {{end -}} {{else -}} - return new this(viewer, {{$instance}}).saveX(); + {{ if $action.CanFail -}} + return new this(viewer, {{$instance}}).save(); + {{else -}} + return new this(viewer, {{$instance}}).saveX(); + {{end -}} {{end -}} } {{end -}} diff --git a/ts/package.json b/ts/package.json index 7d4e322f2..d1508897c 100644 --- a/ts/package.json +++ b/ts/package.json @@ -1,6 +1,6 @@ { "name": "@snowtop/ent", - "version": "0.1.8", + "version": "0.1.9", "description": "snowtop ent framework", "main": "index.js", "types": "index.d.ts", diff --git a/ts/src/schema/schema.ts b/ts/src/schema/schema.ts index 87a462c0d..30ce56084 100644 --- a/ts/src/schema/schema.ts +++ b/ts/src/schema/schema.ts @@ -158,6 +158,11 @@ export interface EdgeAction { // if true, adds under a canViewerDo field on the source Object mapping to graphql name // of this... canViewerDo?: boolean | CanViewerDo; + // indicates that an action is not expected to always pass so + // we should not throw if action fails e.g. no permissions to perform the action + // or if the ent cannot be load afterward + // replaces __failPrivacySilently() implementation + __canFailBETA?: boolean; } // Information about the inverse edge of an assoc edge @@ -929,6 +934,12 @@ export interface Action { // of this... canViewerDo?: boolean | CanViewerDo; + // indicates that an action is not expected to always pass so + // we should not throw if action fails e.g. no permissions to perform the action + // or if the ent cannot be load afterward + // replaces __failPrivacySilently() implementation + __canFailBETA?: boolean; + // allow other keys [x: string]: any; }