Skip to content

Conversation

@codeconsole
Copy link
Contributor

add-field CLI Command

Overview

The add-field command adds a new field to an existing Grails domain class, optionally with validation
constraints using either Grails constraints or Jakarta Validation annotations.

Usage

grails add-field FIELD:TYPE [OPTIONS]

Arguments

Argument Description Example
DOMAIN-CLASS The name of the domain class Book, com.example.Author
FIELD:TYPE Field name and type separated by colon title:String, pages:Integer

Supported Field Types

  • String
  • Integer
  • Long
  • Boolean
  • Date
  • BigDecimal
  • Double
  • Float
  • Short
  • Byte
  • Character

Options

Option Description
--nullable Mark the field as nullable
--not-nullable Mark the field as NOT nullable (generates @NotNull or nullable: false)
--blank Allow blank values (String fields only)
--not-blank Disallow blank values (generates @NotBlank or blank: false)
--max-size Maximum size constraint (String fields only)
--min-size Minimum size constraint (String fields only)
--constraint-style <STYLE> Constraint style: grails (default), jakarta, or both

Constraint Styles

Style Description
grails Uses Grails static constraints block (default)
jakarta Uses Jakarta Validation annotations (@NotNull, @NotBlank, @Size)
both Uses both Grails constraints AND Jakarta annotations

Examples

Basic field (no constraints)

add-field Book title:String
Result:

  class Book {
      String title
  }

With Grails constraints (default)

add-field Book title:String --not-nullable --max-size 255
Result:

  class Book {
      String title

      static constraints = {
          title nullable: false, maxSize: 255
      }
  }

With Jakarta Validation annotations

add-field Book title:String --not-nullable --not-blank --max-size 255 --constraint-style jakarta
Result:

  import jakarta.validation.constraints.NotNull
  import jakarta.validation.constraints.NotBlank
  import jakarta.validation.constraints.Size

  class Book {
      @NotNull
      @NotBlank
      @Size(max = 255)
      String title
  }

With both constraint styles

add-field Book title:String --not-nullable --max-size 255 --constraint-style both
Result:

  import jakarta.validation.constraints.NotNull
  import jakarta.validation.constraints.Size

  class Book {
      @NotNull
      @Size(max = 255)
      String title

      static constraints = {
          title nullable: false, maxSize: 255
      }
  }

Constraint Mapping

Option Grails Constraint Jakarta Annotation
--not-nullable nullable: false @NotNull
--nullable nullable: true (none)
--not-blank blank: false @notblank
--blank blank: true (none)
--max-size N maxSize: N @SiZe(max = N)
--min-size N minSize: N @SiZe(min = N)

Error Handling

  • Domain class not found: Displays error message suggesting to run create-domain-class first
  • Field already exists: Displays error message indicating the field already exists
  • Invalid field type: Displays error with list of supported types
  • Invalid constraint options: Displays specific validation error (e.g., --blank only valid for String)


implementation project(':grails-forge-core')
implementation "org.apache.grails.bootstrap:grails-bootstrap:$projectVersion"
implementation "org.codehaus.groovy:groovy:$groovyVersion"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Groovy 4.x is org.apache.groovy! This is polluting the classpath.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is in the forge-cli, which is still a micronaut app and on groovy 3.

Copy link
Contributor

@jdaugherty jdaugherty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only concern I have with this PR is the decision to put it in grails-bootstrap. I am assuming end user apps won't be generating fields at runtime of a Grails application so this is the wrong spot to place it.

* @since 7.0
*/
@CompileStatic
class DomainFieldModifier {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you help me understand why this is in bootstrap and not the CLI itself? Current files in this project all relate to classes that need shared across grails-core, gradle, and gorm. I only see you using this in the CLI libraries, why not put it in one of those or add a new project that's shared between the CLI for common dependencies?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the grails.codegen.model package is in grails-bootstrap. This is adding those 2 generation classes to this package that already exists. This needs to be shared across BOTH clis and profiles.

grails-bootstrap is already a dependency of grails-shell-cli, putting the classes there means:

  • profiles can use it (via shell-cli → bootstrap)
  • shell-cli can use it (already depends on bootstrap)
  • forge-cli can use it (just needs to depend on bootstrap which is part of this PR)


domainDir.eachFileRecurse { File file ->
if (file.name == "${simpleClassName}.groovy") {
found = file
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if there are duplicate class names across different packages? I think you had an example where you had an Admin & User view - couldn't multiple domains exist in that case with a different table name configuration?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added duplicate domain name / different package support

}

for (FieldNode field : classNode.fields) {
if (field.name == fieldName && !field.name.startsWith('$') && !field.name.startsWith('__')) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know Grails uses these prefix conventions for fields that should not be found, but can we at least extract these prefixes into a variable? Can you check if such a variable already exists so we don't forget to change this file over time?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done


return false
} catch (Exception e) {
return domainFile.text.contains("${fieldName}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it refuses to parse, shouldn't we error? What's the scenario here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok

} else {
int constraintBlockIndex = fieldInsertIndex + 1
lines.add(constraintBlockIndex, '')
lines.add(constraintBlockIndex + 1, ' static constraints = {')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't formatting be handled independently of line addition?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you suggesting to reformat the entire file after making changes?

* Parses the Groovy source file and returns the main class node.
*/
private ClassNode parseClass(File sourceFile) {
CompilerConfiguration config = new CompilerConfiguration()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the context of where this is supposed to be used? We usually used forked threads so not to continue expand the memory of the running process.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseClass() is only called by fieldExists() which parses a single domain class file per add-field invocation

BOTH
}

static final Set<String> SUPPORTED_TYPES = [
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why only the built in types? Seems like enum would be a reasonable one ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was intentionally trying to keep this simple for the first phase. I've expanded it a bit.


implementation project(':grails-forge-core')
implementation "org.apache.grails.bootstrap:grails-bootstrap:$projectVersion"
implementation "org.codehaus.groovy:groovy:$groovyVersion"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is in the forge-cli, which is still a micronaut app and on groovy 3.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

4 participants