Actions
Actions are the way to configure writes in the system. Instead of just a blanket create, edit and delete option, we believe it makes more sense to have more nuanced options when it makes sense.
You can think of an action as a singular action (pun intended!) performed by a user. For example, consider a User
object, there's different things that can be done by the user with very different permission checks or side effects based on what fields are being changed.
In concrete terms, consider a User
schema as follows:
const UserSchema = new EntSchema({
fields: {
firstName: StringType(),
lastName: StringType(),
emailAddress: EmailType({ unique: true }),
PhoneNumber: PhoneNumberType({
unique: true,
}),
password: PasswordType(),
accountStatus: EnumType({ values: ["UNVERIFIED", "VERIFIED", "DEACTIVATED", "DISABLED"], serverDefault: "UNVERIFIED" }),
emailVerified: BooleanType({
hideFromGraphQL: true,
serverDefault: "FALSE",
}),
},
});
export default UserSchema;
Here are a few of the actions that come to mind:
create user with fields
firstName
,lastName
,emailAddress
,phoneNumber
,password
edit profile fields
firstName
,lastName
edit
emailAddress
which may be broken into two actions:editEmailAddress
which actually means send confirmation code to new email address to verify that user has access to email and doesn't get locked outconfirmEditEmailAddress
which takes the email and code, verifies they're correct and then actually updates the email address
edit
phoneNumber
which can be broken up into 2 same as abovedelete user
verify email address by sending code and updating
emailVerified
state to trueupdate
accountStatus
in internal tool and confirm that only users with admin privileges can do itupdate password
Having just one big edit mutation that handles all this gets complicated really quickly hence we break each of these into separate chunks that are much easier to reason about.
Mutations
With all that said, most objects don't need this level of complexity and just need the CUD in CRUD. That can be configured as follows:
const EventSchema = new EntSchema({
fields: {
//...
},
actions: [
{
operation: ActionOperation.Mutations,
},
],
});
export default EventSchema;
That adds eventCreate
, eventEdit
and eventDelete
actions with all the editable fields showing up in the create and delete actions.
And over time as the product evolves, the action(s) can be configured and updated to simplify or make things more complicated.
This allows us to straddle the gap between a simple and complicated experience as needed depending on the use case.
Options
operation
specifies the type of action that's created. We have the following types:
ActionOperation.Create
: create an object. inserts a new row into the database.ActionOperation.Edit
: edits an object. edits an existing row in the database.ActionOperation.Delete
: deletes on object. deletes an existing row in the database.ActionOperation.Mutations
: shortcut to easily getcreate
,edit
, anddelete
actions. Cannot be customized. If you want to customize the actions, have to itemizeActionOperation.AddEdge
: adds a new edgeActionOperation.RemoveEdge
: removes an edgeActionOperation.EdgeGroup
: creates a new action for an edge group
fields
Associated with an edit
or create
action, specifies the fields that should be in the action. When missing, defaults to all the editable fields in the object
actionName
Overrides the default action name created.
- The default for
create
actions is of the formatCreate{Schema}Action
- The default for
edit
actions is of the formatEdit{Schema}Action
- The default for
delete
actions is of the formatDelete{Schema}Action
If more than one action of a type exists, the name must be overriden since we can't have duplicate action names. e.g. in the example given above with the User
object
inputName
Overrides the default input name created.
- The default for
create
actions is of the format{Schema}CreateInput
- The default for
edit
actions is of the format{Schema}EditInput
If more than one action of a type exists, the name must be overriden since we can't have duplicate input names. e.g. in the example given above with the User
object
graphQLName
Overrides the default GraphQL mutation name created.
- The default for
create
actions is of the format{lowerCaseSchema}Create
. - The default for
edit
actions is of the format{lowerCaseSchema}Edit
. - The default for
delete
actions is of the format{lowerCaseSchema}Delete
.
If more than one action of a type in a Schema exists, the name must be overriden since we can't have duplicate action names. e.g. in the example given above with the User
object.
hideFromGraphQL
hides the action from being exposed as a Mutation
in GraphQL. This is used for things that shouldn't be exposed in the public API e.g. an action that's internal to the system.
actionOnlyFields
sometimes, we need fields that are not in the schema that need to be generated in the action input or in the input to the graphql mutation.
For example, a possible confirm email address action which was referenced above, can be configured as follows:
const UserSchema = new EntSchema({
fields: {
//...
},
actions: [
// confirm email address with code sent in last time
{
operation: ActionOperation.Edit,
actionName: "ConfirmEditEmailAddressAction",
graphQLName: "confirmEmailAddressEdit",
inputName: "ConfirmEditEmailAddressInput",
actionOnlyFields: [{ name: "code", type: "String" }],
// fields are default optional in edit mutation, this says make this required in here
fields: [requiredField("EmailAddress")],
},
],
});
export default UserSchema;
This adds an additional required field code
of type String
to be generated for this action.
options
name
: name of the fieldtype
: type of the field. currently restricted to scalarsID
,Boolean
,Int
,Float
,String
,Time
list
: indicates if this is a list e.g. it transforms aString
type fromstring
tostring[]
.nullable
: if true, item is nullable e.g.string | null
. if a list, it gets slightly more complicated- if a list and
nullable
istrue
, the list itself is nullable but not its contents. The GraphQL type here would be[String!]
- if a list and
nullable
iscontents
, the contents of the list are nullable but not the list. The GraphQL type here would be[String!]
- if a list and
nullable
iscontentsAndList
, both the list and its contents are nullable. The GraphQL type here would be[String]
Check out for more about lists and null in GraphQL.
- if a list and
actionName
: allows exposing the fields of another action with the given name as a sub-input.
For example,
const EventSchema = new EntSchema({
fields: {},
actions: [
{
operation: ActionOperation.Create,
actionOnlyFields: [
{
name: "address",
type: "Object",
nullable: true,
actionName: "CreateAddressAction",
},
],
},
],
}).
export default EventSchema;
results in this schema (assuming an Address
object with the fields street
, city
, state
, zipCode
, apartment
):
input EventCreateInput {
name: String!
startTime: Time!
endTime: Time
location: String!
description: String
inviteAllGuests: Boolean!
address: AddressEventCreateInput
}
input AddressEventCreateInput {
street: String!
city: String!
state: String!
zipCode: String!
apartment: String
}
This allows us to create the event and its associated address in the same request using triggers.
See Action Only Fields for more.
NoFields
Sometimes, there's scenarios where we want no fields associated with the schema to be generated in the action input and need a way to indicate this.
NoFields
exposed by the framework enables this.
For example. an edit email address action that takes an email address and generates a unique code, stores the code somewhere e.g. redis and sends a confirmation email with the code embedded can be represented as follows:
const UserSchema = new EntSchema({
fields: {},
actions: [
// send confirmation code for email address
{
// we're not saving anything in the db so we use actionOnlyField to specify a required email address
// send email out
operation: ActionOperation.Edit,
actionName: "EditEmailAddressAction",
graphQLName: "emailAddressEdit",
inputName: "EditEmailAddressInput",
// still need no fields even when we want only actionOnlyFields
fields: [NoFields],
// we use actionOnlyField here so emailAddress is not saved
// we use a different field name so that field is not saved
actionOnlyFields: [{ name: "newEmail", type: "String" }],
},
],
});
export default UserSchema;
We make sure to use a different field name with the action only field such as newEmail
so that the email address isn't saved yet.
requiredField
makes a field required in an action. Needed in the following cases:
- if a field is nullable in the schema but we want it required in the action e.g. a schema with both email address and phone number, both nullable and either can be used as the auth mechanism. When we want to make sure that a confirm email address action has a required email address field, this is used.
- similar to above, we want to make a field which is nullable in the schema required in a create action.
- we want to make a field required in an edit action. By default, all fields in edit mutation are optional since they may not be all edited at the same time.
e.g.
const UserSchema = new EntSchema({
fields: {},
actions: [
// confirm email address with code sent in last time
{
operation: ActionOperation.Edit,
actionName: "ConfirmEditEmailAddressAction",
graphQLName: "confirmEmailAddressEdit",
inputName: "ConfirmEditEmailAddressInput",
actionOnlyFields: [{ name: "code", type: "String" }],
// fields are default optional in edit mutation, this says make this required in here
fields: [requiredField("EmailAddress")],
},
],
});
optionalField
Inverse of requiredField above. Want to make a field which would be required optional. Usually means that a default value is set in a trigger