diff --git a/examples/ent-rsvp/backend/src/ent/generated/address/actions/create_address_action_base.ts b/examples/ent-rsvp/backend/src/ent/generated/address/actions/create_address_action_base.ts index 8d65be8e0..789f26165 100644 --- a/examples/ent-rsvp/backend/src/ent/generated/address/actions/create_address_action_base.ts +++ b/examples/ent-rsvp/backend/src/ent/generated/address/actions/create_address_action_base.ts @@ -52,7 +52,7 @@ export type CreateAddressActionObservers = Observer< AddressBuilder, Viewer, AddressCreateInput, - Address | null + Address | null >[]; export type CreateAddressActionValidators = Validator< diff --git a/examples/simple/package-lock.json b/examples/simple/package-lock.json index 573f29667..05e0485b9 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.2.0-alpha.7", + "@snowtop/ent": "^0.2.0-alpha.10", "@snowtop/ent-email": "^0.1.0-rc1", "@snowtop/ent-passport": "^0.1.0-rc1", "@snowtop/ent-password": "^0.1.0-rc1", @@ -1301,9 +1301,9 @@ } }, "node_modules/@snowtop/ent": { - "version": "0.2.0-alpha.7", - "resolved": "https://registry.npmjs.org/@snowtop/ent/-/ent-0.2.0-alpha.7.tgz", - "integrity": "sha512-wO6mb1NlC/KVthenDCYHp1d6H3kI+7zjDZqxz0Z/h368QJQGS/v27YO+9vOW2M0wquygOcy9jFxirbddHMlKpw==", + "version": "0.2.0-alpha.10", + "resolved": "https://registry.npmjs.org/@snowtop/ent/-/ent-0.2.0-alpha.10.tgz", + "integrity": "sha512-6DTntYpZlFNEqkle7bAQxhtDYiuPezIERTmX/JCmCL213p2dlU3yjmLlzTKK5GmSHw4HPKWrwhJf5pujgilCrA==", "dependencies": { "@types/node": "^20.10.4", "camel-case": "^4.1.2", @@ -7521,9 +7521,9 @@ } }, "@snowtop/ent": { - "version": "0.2.0-alpha.7", - "resolved": "https://registry.npmjs.org/@snowtop/ent/-/ent-0.2.0-alpha.7.tgz", - "integrity": "sha512-wO6mb1NlC/KVthenDCYHp1d6H3kI+7zjDZqxz0Z/h368QJQGS/v27YO+9vOW2M0wquygOcy9jFxirbddHMlKpw==", + "version": "0.2.0-alpha.10", + "resolved": "https://registry.npmjs.org/@snowtop/ent/-/ent-0.2.0-alpha.10.tgz", + "integrity": "sha512-6DTntYpZlFNEqkle7bAQxhtDYiuPezIERTmX/JCmCL213p2dlU3yjmLlzTKK5GmSHw4HPKWrwhJf5pujgilCrA==", "requires": { "@types/node": "^20.10.4", "camel-case": "^4.1.2", diff --git a/examples/simple/package.json b/examples/simple/package.json index e9fab8fcf..8e2325ff8 100644 --- a/examples/simple/package.json +++ b/examples/simple/package.json @@ -33,7 +33,7 @@ "tsconfig-paths": "^3.11.0" }, "dependencies": { - "@snowtop/ent": "^0.2.0-alpha.7", + "@snowtop/ent": "^0.2.0-alpha.10", "@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/tests/contact.test.ts b/examples/simple/src/ent/tests/contact.test.ts index 2db379b43..435553d9e 100644 --- a/examples/simple/src/ent/tests/contact.test.ts +++ b/examples/simple/src/ent/tests/contact.test.ts @@ -9,7 +9,7 @@ import EditContactAction from "../contact/actions/edit_contact_action"; import { LoggedOutExampleViewer, ExampleViewer } from "../../viewer/viewer"; import { query } from "@snowtop/ent"; import { v4 } from "uuid"; -import { ContactLabel, ContactInfoSource } from "../generated/types"; +import { ContactLabel, ContactInfoSource, NodeType } from "../generated/types"; import { Transaction } from "@snowtop/ent/action"; import CreateFileAction from "../file/actions/create_file_action"; import { advanceTo } from "jest-date-mock"; @@ -117,6 +117,8 @@ test("create contact with explicit attachments", async () => { date: d, note: "test", dupeFileId: file.id, + creatorId: user.id, + creatorType: NodeType.User, }, { fileId: file2.id, @@ -136,6 +138,8 @@ test("create contact with explicit attachments", async () => { date: d.toISOString(), note: "test", dupeFileId: file.id, + creatorId: user.id, + creatorType: NodeType.User, }, { fileId: file2.id, diff --git a/examples/simple/src/graphql/generated/mutations/input/attachment_input_type.ts b/examples/simple/src/graphql/generated/mutations/input/attachment_input_type.ts index fd52accd9..cdf7cb355 100644 --- a/examples/simple/src/graphql/generated/mutations/input/attachment_input_type.ts +++ b/examples/simple/src/graphql/generated/mutations/input/attachment_input_type.ts @@ -36,5 +36,8 @@ export const AttachmentInputType = new GraphQLInputObjectType({ creatorId: { type: GraphQLID, }, + creatorType: { + type: GraphQLString, + }, }), }); diff --git a/examples/simple/src/graphql/generated/resolvers/contact_info_type.ts b/examples/simple/src/graphql/generated/resolvers/contact_info_type.ts index 755b6bbda..acc228817 100644 --- a/examples/simple/src/graphql/generated/resolvers/contact_info_type.ts +++ b/examples/simple/src/graphql/generated/resolvers/contact_info_type.ts @@ -11,5 +11,5 @@ import { export const ContactInfoType = new GraphQLUnionType({ name: "ContactInfo", - types: () => [ContactPhoneNumberType, ContactEmailType], + types: () => [ContactEmailType, ContactPhoneNumberType], }); diff --git a/examples/simple/src/graphql/generated/resolvers/feedback_type.ts b/examples/simple/src/graphql/generated/resolvers/feedback_type.ts index d119df2f3..b35e1d1f6 100644 --- a/examples/simple/src/graphql/generated/resolvers/feedback_type.ts +++ b/examples/simple/src/graphql/generated/resolvers/feedback_type.ts @@ -14,9 +14,9 @@ import { export const FeedbackType = new GraphQLUnionType({ name: "Feedback", types: () => [ + ContactEmailType, ContactPhoneNumberType, UserType, ContactType, - ContactEmailType, ], }); diff --git a/examples/simple/src/graphql/generated/resolvers/with_day_of_week_type.ts b/examples/simple/src/graphql/generated/resolvers/with_day_of_week_type.ts index a41fc435e..a0b4cbb3c 100644 --- a/examples/simple/src/graphql/generated/resolvers/with_day_of_week_type.ts +++ b/examples/simple/src/graphql/generated/resolvers/with_day_of_week_type.ts @@ -8,5 +8,5 @@ import { HolidayType, HoursOfOperationType } from "../../resolvers/internal"; export const WithDayOfWeekType = new GraphQLUnionType({ name: "WithDayOfWeek", - types: () => [HolidayType, HoursOfOperationType], + types: () => [HoursOfOperationType, HolidayType], }); diff --git a/examples/simple/src/graphql/generated/schema.gql b/examples/simple/src/graphql/generated/schema.gql index f4fa991d4..6f25a0109 100644 --- a/examples/simple/src/graphql/generated/schema.gql +++ b/examples/simple/src/graphql/generated/schema.gql @@ -1124,6 +1124,7 @@ input AttachmentInput { phoneNumber: String emailAddress: String creatorId: ID + creatorType: String } input CatTypeInput { diff --git a/examples/simple/src/graphql/tests/contact_type.test.ts b/examples/simple/src/graphql/tests/contact_type.test.ts index 10ec0a1a3..1adcef195 100644 --- a/examples/simple/src/graphql/tests/contact_type.test.ts +++ b/examples/simple/src/graphql/tests/contact_type.test.ts @@ -6,7 +6,7 @@ import { queryRootConfig, } from "@snowtop/ent-graphql-tests"; import { clearAuthHandlers } from "@snowtop/ent/auth"; -import { encodeGQLID } from "@snowtop/ent/graphql"; +import { encodeGQLID, mustDecodeIDFromGQLID } from "@snowtop/ent/graphql"; import schema from "../generated/schema"; import CreateUserAction from "../../ent/user/actions/create_user_action"; import { Contact, User } from "../../ent"; @@ -14,7 +14,7 @@ import { randomEmail, randomPhoneNumber } from "../../util/random"; import EditUserAction from "../../ent/user/actions/edit_user_action"; import CreateContactAction from "../../ent/contact/actions/create_contact_action"; import { LoggedOutExampleViewer, ExampleViewer } from "../../viewer/viewer"; -import { ContactLabel } from "src/ent/generated/types"; +import { ContactLabel, NodeType } from "src/ent/generated/types"; import EditContactAction from "src/ent/contact/actions/edit_contact_action"; import CreateContactEmailAction from "src/ent/contact_email/actions/create_contact_email_action"; import CreateContactPhoneNumberAction from "src/ent/contact_phone_number/actions/create_contact_phone_number_action"; @@ -368,6 +368,8 @@ test("create contact with attachments", async () => { note: "note", date: d.toISOString(), dupeFileId: encodeGQLID(file), + creatorId: encodeGQLID(user), + creatorType: "User", }, { fileId: encodeGQLID(file2), @@ -378,12 +380,24 @@ test("create contact with attachments", async () => { ], }, }, + [ + "contact.id", + async function name(id: string) { + const entId = mustDecodeIDFromGQLID(id); + const contact = await Contact.loadX(user.viewer, entId); + expect(contact.attachments).toHaveLength(2); + expect(contact.attachments?.[0].creatorType).toBe(NodeType.User); + }, + ], ["contact.firstName", "Jon"], ["contact.lastName", "Snow"], ["contact.emails[0].emailAddress", email], ["contact.attachments[0].file.id", encodeGQLID(file)], ["contact.attachments[0].dupeFile.id", encodeGQLID(file)], ["contact.attachments[0].note", "note"], + ["contact.attachments[0].creator.id", encodeGQLID(user)], + // TODO we should have a way to query nested graphql fragments... + // ["contact.attachments[0].creator....on User.type", "User"], ["contact.attachments[1].dupeFile.id", encodeGQLID(file2)], ["contact.attachments[1].file.id", encodeGQLID(file2)], ["contact.attachments[1].note", "note2"], diff --git a/internal/graphql/generate_ts_code.go b/internal/graphql/generate_ts_code.go index b5429a609..4321c49cf 100644 --- a/internal/graphql/generate_ts_code.go +++ b/internal/graphql/generate_ts_code.go @@ -3362,8 +3362,14 @@ func buildCustomInterfaceNode(processor *codegen.Processor, ci *customtype.Custo } for _, f := range ci.Fields { - if !f.ExposeToGraphQL() { - continue + if ciInfo.input { + if !f.EditableGraphQLField() { + continue + } + } else { + if !f.ExposeToGraphQL() { + continue + } } ft := &fieldType{ Name: f.GetGraphQLName(), diff --git a/ts/package.json b/ts/package.json index 5658bd910..ffb348e7d 100644 --- a/ts/package.json +++ b/ts/package.json @@ -1,6 +1,6 @@ { "name": "@snowtop/ent", - "version": "0.2.0-alpha.9", + "version": "0.2.0-alpha.10", "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 8276d190a..737098d4d 100644 --- a/ts/src/schema/schema.ts +++ b/ts/src/schema/schema.ts @@ -592,9 +592,6 @@ export interface FieldOptions { fetchOnDemand?: boolean; - // on uuid types, provides the ability to disable base 64 encoding of the uuid if for example this is a raw uuid not associated with an ent/object in the db - disableBase64Encode?: boolean; - // if dbOnly, field isn't exposed in ent and graphql // will still exit in the db and not be removed // allows keeping the field in the db and avoid data loss if we still want the field for some reason diff --git a/ts/src/schema/struct_field.test.ts b/ts/src/schema/struct_field.test.ts index 60ab39cc2..c69c5d32c 100644 --- a/ts/src/schema/struct_field.test.ts +++ b/ts/src/schema/struct_field.test.ts @@ -860,3 +860,42 @@ describe("struct as list global type", () => { expect(f.format(val)).toBe(JSON.stringify(formatted)); }); }); + +test("struct with polymorphic field", async () => { + const f = structTypeF({ + user_id: UUIDType({ + polymorphic: { + types: ["User"], + }, + }), + value: StringType(), + }); + + const val = { + user_id: v1(), + user_type: "user", + value: "string", + }; + + expect(await f.valid(val)).toBe(true); + expect(f.format(val)).toBe(JSON.stringify(val)); +}); + +test("struct with invalid polymorphic field", async () => { + const f = structTypeF({ + user_id: UUIDType({ + polymorphic: { + types: ["User"], + }, + }), + value: StringType(), + }); + + const val = { + user_id: v1(), + user_type: "hello", + value: "string", + }; + + expect(await f.valid(val)).toBe(false); +}); diff --git a/ts/src/schema/struct_field.ts b/ts/src/schema/struct_field.ts index 27ab27e4e..88e570330 100644 --- a/ts/src/schema/struct_field.ts +++ b/ts/src/schema/struct_field.ts @@ -73,9 +73,8 @@ export class StructField extends BaseField implements Field { throw new Error("valid was not called"); } let ret: Object = {}; - for (const k in this.options.fields) { - const field = this.options.fields[k]; + const processField = (k: string, field: Field) => { // check two values // store in dbKey format @@ -93,7 +92,7 @@ export class StructField extends BaseField implements Field { } if (val === undefined) { - continue; + return; } if (field.format) { // indicate nested so this isn't JSON stringified @@ -101,6 +100,19 @@ export class StructField extends BaseField implements Field { } else { ret[dbKey] = val; } + }; + + for (const k in this.options.fields) { + const field = this.options.fields[k]; + + processField(k, field); + + if (field.getDerivedFields) { + const derivedFields = field.getDerivedFields(k); + for (const k in derivedFields) { + processField(k, derivedFields[k]); + } + } } // don't json.stringify if nested or list if (nested) { @@ -157,11 +169,12 @@ export class StructField extends BaseField implements Field { } let promises: (boolean | Promise)[] = []; - // TODO probably need to support optional fields... - let valid = true; - for (const k in this.options.fields) { - const field = this.options.fields[k]; - let dbKey = getStorageKey(field, k); + + const processField = ( + k: string, + dbKey: string, + field: Field, + ): boolean | undefined => { let fieldName = toFieldName(k); let val = obj[fieldName]; @@ -193,15 +206,36 @@ export class StructField extends BaseField implements Field { if (val === undefined || val === null) { // nullable, nothing to do here if (field.nullable) { - continue; + return; } valid = false; - break; + return false; } if (!field.valid) { - continue; + return; } promises.push(field.valid(val)); + }; + // TODO probably need to support optional fields... + let valid = true; + for (const k in this.options.fields) { + const field = this.options.fields[k]; + let dbKey = getStorageKey(field, k); + + if (processField(k, dbKey, field) === false) { + valid = false; + } + + if (field.getDerivedFields) { + const derivedFields = field.getDerivedFields(k); + for (const k in derivedFields) { + const derivedField = derivedFields[k]; + let dbKey = getStorageKey(derivedField, k); + if (processField(k, dbKey, derivedField) === false) { + valid = false; + } + } + } } if (!valid) { return valid;