Skip to content

Commit

Permalink
Adding Java support and future support for other stacks.
Browse files Browse the repository at this point in the history
  • Loading branch information
wnederhof committed Apr 13, 2024
1 parent e865369 commit e90aafd
Show file tree
Hide file tree
Showing 139 changed files with 1,860 additions and 57 deletions.
42 changes: 3 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,13 @@ basic structure of your web application.

Basecode generates code consisting of:

- Kotlin / Java (coming soon)
- GraphQL
- Java / Kotlin
- GraphQL schema files
- Typescript + React (through Next.js)

Finally, Basecode is open source ([MIT](LICENSE.md)).

⭐ If you like Basecode, please consider giving it a star. It would mean the world to us! Your support can help the
project grow and deliver exciting
⭐ If you like Basecode, please consider giving it a star. Your support can help the project grow and deliver exciting
features.

<a href="https://www.youtube.com/watch?v=rx9xL0nhot8"><img src="video-button.png"></a>
Expand Down Expand Up @@ -121,41 +120,6 @@ In order to start your newly generated program, you need to:
- Run `npm install` in the `<artifactId>-web` directory to install the frontend dependencies
- Run `npm dev` in the `<artifactId>-web` directory to start the frontend development server

# Blog Tutorial

Let's discuss how to create a simple blog application.

First, create a new application called `blog` and `cd` into it:

```
basecode new com.mycorp blog
cd blog
```

Next, let's generate scaffolds for the Post and Comment entities.

```
basecode generate scaffold Post title contents:text
basecode generate scaffold Comment postId:Post contents:text
```

Then, fire up a development database by running `docker-compose up` in the `<artifactId>-server` folder.

Now, either start the backend using your IDE by running the `main` method in the `Application.kt` file, or start
the Spring Boot server using `./mvnw spring-boot:run`. You should be able to access your GraphQL dashboard
at: `http://localhost:8080/graphiql`.

To start the frontend, make sure your artifacts are installed using `npm install` and run `npm run dev`.

If you now visit `localhost:3000/posts`, you should be able to see the posts.

Next, add `<CommentList postId={postId as string} />` and `<CommentForm postId={postId as string} />` to
the `/blog-web/src/pages/posts/[postId]/index.tsx` page, right
before the `</DefaultLayout>`. Make sure you import both of them.

You should now be able to create, list, edit and view posts and comment on them. We leave it up to you to turn the rest
of the blog into an absolute gem!

## Tips

- If you want to *update* an existing scaffolding, make start with an empty commit. This way, you can easily see
Expand Down
16 changes: 15 additions & 1 deletion internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,25 @@ func Run(args []string) error {
Name: "new",
Aliases: []string{"n"},
Usage: "Create a new application",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "backend",
Aliases: []string{"b"},
},
},
Action: func(c *cli.Context) error {
if c.Args().Len() != 2 {
return errors.New("required two arguments for: new groupId and artifactId")
}
return generator.GenerateNewProject(c.Args().Get(0), c.Args().Get(1))
backend := c.String("backend")
if backend == "" {
backend = "kotlin"
}
frontend := c.String("frontend")
if frontend == "" {
frontend = "react"
}
return generator.GenerateNewProject(c.Args().Get(0), c.Args().Get(1), backend)
},
},
{
Expand Down
56 changes: 56 additions & 0 deletions pkg/generator/attributes.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,11 @@ func provideFieldContextAttributes(context map[string]interface{}, attributes []
provideIsFieldRelationalContextAttributes(fieldContext, attribute)
provideGraphQLFieldTypeContextAttributes(fieldContext, attribute)
provideKotlinFieldContextAttributes(fieldContext, attribute)
provideJavaFieldContextAttributes(fieldContext, attribute)
provideInputFieldContextAttributes(fieldContext, attribute)
provideReactTemplateTestContextAttributes(fieldContext, attribute)
provideReactTemplateExpectedContextAttributes(fieldContext, attribute)
fieldContext["isNotLast"] = i != len(attributes)-1
}
}

Expand Down Expand Up @@ -230,6 +232,60 @@ func provideGraphQLFieldTypeContextAttributes(context map[string]interface{}, at
}
}

// TODO add tests
func provideJavaFieldContextAttributes(context map[string]interface{}, attribute ModelAttribute) {
switch attribute.Type {
case STRING:
context["fieldJavaType"] = "String"
context["fieldJavaTypeNotNullable"] = "String"
context["fieldJavaTestDummyValue"] = "\"Some " + attribute.Name + "\""
case INT:
context["fieldJavaType"] = "Int"
context["fieldJavaTypeNotNullable"] = "Int"
context["fieldJavaTestDummyValue"] = "1"
case TEXT:
context["fieldJavaAnnotations"] = "@Lob"
context["fieldJavaType"] = "String"
context["fieldJavaTypeNotNullable"] = "String"
context["fieldJavaTestDummyValue"] = "\"Some " + attribute.Name + "\""
case DATE:
context["fieldJavaType"] = "LocalDate"
context["fieldJavaTypeNotNullable"] = "LocalDate"
context["fieldJavaTestDummyValue"] = "LocalDate.of(2000, 1, 1)"
case BOOLEAN:
context["fieldJavaType"] = "Boolean"
context["fieldJavaTypeNotNullable"] = "Boolean"
context["fieldJavaTestDummyValue"] = "true"
case NULL_STRING:
context["fieldJavaType"] = "String?"
context["fieldJavaTypeNotNullable"] = "String"
context["fieldJavaTestDummyValue"] = "\"Some " + attribute.Name + "\""
case NULL_INT:
context["fieldJavaType"] = "Int?"
context["fieldJavaTypeNotNullable"] = "Int"
context["fieldJavaTestDummyValue"] = "1"
case NULL_TEXT:
context["fieldJavaAnnotations"] = "@Lob"
context["fieldJavaType"] = "String?"
context["fieldJavaTypeNotNullable"] = "String"
context["fieldJavaTestDummyValue"] = "\"Some " + attribute.Name + "\""
case NULL_DATE:
context["fieldJavaType"] = "LocalDate?"
context["fieldJavaTypeNotNullable"] = "LocalDate"
context["fieldJavaTestDummyValue"] = "LocalDate.of(2000, 1, 1)"
case NULL_BOOLEAN:
context["fieldJavaType"] = "Boolean?"
context["fieldJavaTypeNotNullable"] = "Boolean"
context["fieldJavaTestDummyValue"] = "true"
case RELATIONAL:
context["fieldJavaType"] = "Int"
context["fieldJavaTypeNotNullable"] = "Int"
context["fieldJavaTestDummyValue"] = "10"
default:
panic("Undetermined attribute type.")
}
}

func provideKotlinFieldContextAttributes(context map[string]interface{}, attribute ModelAttribute) {
switch attribute.Type {
case STRING:
Expand Down
36 changes: 20 additions & 16 deletions pkg/generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@ var (
type Properties struct {
ArtifactId string `yaml:",omitempty"`
GroupId string `yaml:",omitempty"`
Backend string `yaml:",omitempty"`
}

func GenerateNewProject(groupId string, artifactId string) error {
func GenerateNewProject(groupId string, artifactId string, backend string) error {
projectName := artifactId
context := make(map[string]interface{})
properties := Properties{
GroupId: groupId,
ArtifactId: artifactId,
Backend: backend,
}
err := os.MkdirAll(projectName, os.ModePerm)
if err != nil {
Expand All @@ -35,7 +37,7 @@ func GenerateNewProject(groupId string, artifactId string) error {
}
provideProjectContextAttributes(context, properties)
provideHelperContextAttributes(context)
return writeFiles("templates/new", projectName, context, false)
return writeFiles("templates/backends/[backend]/new", projectName, context, false)
}

func GenerateScaffold(model Model, overwrite bool, delete bool) error {
Expand Down Expand Up @@ -64,21 +66,21 @@ func GenerateBackendScaffold(model Model, overwrite bool, delete bool) error {

func GenerateBackendModel(model Model, overwrite bool, delete bool) error {
if delete {
err := GenerateModelTemplate("templates/entity", model, overwrite, true)
err := GenerateModelTemplate("templates/backends/[backend]/entity", model, overwrite, true)
if err != nil {
return err
}
return GenerateModelTemplate("templates/entity-removal", model, overwrite, false)
return GenerateModelTemplate("templates/backends/[backend]/entity-removal", model, overwrite, false)
}
return GenerateModelTemplate("templates/entity", model, overwrite, false)
return GenerateModelTemplate("templates/backends/[backend]/entity", model, overwrite, false)
}

func GenerateBackendApi(model Model, overwrite bool, delete bool) error {
return GenerateModelTemplate("templates/graphql", model, overwrite, delete)
return GenerateModelTemplate("templates/backends/[backend]/graphql", model, overwrite, delete)
}

func GenerateBackendService(model Model, overwrite bool, delete bool) error {
return GenerateModelTemplate("templates/service", model, overwrite, delete)
return GenerateModelTemplate("templates/backends/[backend]/service", model, overwrite, delete)
}

func GenerateFrontend(overwrite bool, delete bool) error {
Expand All @@ -90,13 +92,13 @@ func GenerateFrontend(overwrite bool, delete bool) error {
provideProjectContextAttributes(context, properties)
provideHelperContextAttributes(context)
if delete {
return deleteFiles("templates/react-frontend", ".", context)
return deleteFiles("templates/frontends/[frontend]/new", ".", context)
}
return writeFiles("templates/react-frontend", ".", context, overwrite)
return writeFiles("templates/frontends/[frontend]/new", ".", context, overwrite)
}

func GenerateFrontendScaffold(model Model, overwrite bool, delete bool) error {
return GenerateModelTemplate("templates/react-frontend-scaffold", model, overwrite, delete)
return GenerateModelTemplate("templates/frontends/[frontend]/scaffold", model, overwrite, delete)
}

func GenerateModelTemplate(templateDirectory string, model Model, overwrite bool, delete bool) error {
Expand Down Expand Up @@ -131,18 +133,18 @@ func GenerateBackendAuthentication(attributes []ModelAttribute, overwrite bool,
}}, attributes...),
}
if delete {
err := GenerateModelTemplate("templates/auth", model, overwrite, true)
err := GenerateModelTemplate("templates/backends/[backend]/auth", model, overwrite, true)
if err != nil {
return err
}
println("Please remove the spring-boot-starter-security dependency manually.")
return GenerateModelTemplate("templates/auth-removal", model, overwrite, false)
return GenerateModelTemplate("templates/backends/[backend]/auth-removal", model, overwrite, false)
}
err := addDependency("org.springframework.boot", "spring-boot-starter-security")
if err != nil {
return err
}
return GenerateModelTemplate("templates/auth", model, overwrite, false)
return GenerateModelTemplate("templates/backends/[backend]/auth", model, overwrite, false)
}

func GenerateFrontendAuthentication(attributes []ModelAttribute, overwrite bool, delete bool) error {
Expand All @@ -159,16 +161,15 @@ func GenerateFrontendAuthentication(attributes []ModelAttribute, overwrite bool,
}}, attributes...),
}
if delete {
err := GenerateModelTemplate("templates/auth-frontend", model, overwrite, true)
err := GenerateModelTemplate("templates/frontends/[frontend]/auth", model, overwrite, true)
if err != nil {
return err
}
}
return GenerateModelTemplate("templates/auth-frontend", model, overwrite, false)
return GenerateModelTemplate("templates/frontends/[frontend]/auth", model, overwrite, false)
}

func GenerateAuthentication(attributes []ModelAttribute, overwrite bool, delete bool) error {
// TODO test
err := GenerateBackendAuthentication(attributes, overwrite, delete)
if err != nil {
return err
Expand Down Expand Up @@ -219,6 +220,9 @@ func writeProperties(properties Properties, projectName string) error {
}

func writeFiles(sourceDirectory string, targetDirectoryTemplate string, context map[string]interface{}, overwrite bool) error {
// Replace /templates/[language] with /templates/java
sourceDirectory = substitutePathParamsAndRemovePeb(sourceDirectory, context)

err := createDirectoryUsingTemplate(targetDirectoryTemplate, context)
if err != nil {
return err
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package {{ groupId }}.{{ artifactId }}.domain.{{ nameLowerCase }};

import lombok.Builder;
import lombok.Data;
import lombok.NonNull;
import lombok.With;

import java.time.OffsetDateTime;

@Data
@With
@Builder
public class {{ namePascalCase }} {
private int id;{%for field in fields%}
@NonNull
private {{ field.fieldJavaType }} {{ field.fieldNameCamelCase }};{%endfor%}
@NonNull
private OffsetDateTime createdAt;
private OffsetDateTime updatedAt;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package {{ groupId }}.{{ artifactId }}.domain.{{ nameLowerCase }};

import lombok.Builder;
import lombok.Data;
import lombok.NonNull;
import lombok.With;
import org.springframework.data.relational.core.mapping.Table;

import java.time.LocalDateTime;

@Data
@With
@Builder
@Table("{{ namePluralSnakeCase }}")
public class {{ namePascalCase }}Entity {
@Id
private Integer id;{%for field in fields%}
private {{ field.fieldJavaType }} {{ field.fieldNameCamelCase }};{%endfor%}

@CreatedDate
private LocalDateTime createdAt;

@LastModifiedDate
private LocalDateTime LocalDateTime;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package {{ groupId }}.{{ artifactId }}.domain.{{ nameLowerCase }};

import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.query.Param;

import java.util.List;

interface {{ namePascalCase }}Repository extends CrudRepository<{{ namePascalCase }}Entity, Integer> {

@Query("SELECT * FROM {{ namePluralSnakeCase }} WHERE id IN (:ids)")
List<{{ namePascalCase }}Entity> findByIds(@Param("ids") Iterable<Integer> ids);
{%for field in fields%}{%if field.isFieldRelational%}
@Query("SELECT * FROM {{ namePluralSnakeCase }} WHERE {{ field.fieldNameSnakeCase }} IN (:{{ field.fieldNamePluralCamelCase }})")
List<{{ namePascalCase }}Entity> findBy{{ field.fieldNamePluralPascalCase }}(@Param("{{ field.fieldNamePluralCamelCase }}") Iterable<Integer> {{ field.fieldNamePluralCamelCase }});
{%endif%}{%endfor%}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package {{ groupId }}.{{ artifactId }}.application.graphql.{{ nameLowerCase }};

import com.netflix.graphql.dgs.DgsComponent;
import com.netflix.graphql.dgs.DgsDataLoader;
import {{ groupId }}.{{ artifactId }}.domain.{{ nameLowerCase }}.{{ namePascalCase }};
import {{ groupId }}.{{ artifactId }}.domain.{{ nameLowerCase }}.{{ namePascalCase }}Service;
import lombok.RequiredArgsConstructor;
import org.dataloader.MappedBatchLoader;

import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

import static java.util.concurrent.CompletableFuture.supplyAsync;

@DgsComponent
@RequiredArgsConstructor
class {{ namePascalCase }}DataLoader {
private {{ namePascalCase }}Service {{ nameCamelCase }}Service;

@DgsDataLoader(name = "{{ nameCamelCase }}ById")
MappedBatchLoader<Integer, {{ namePascalCase }}> {{ nameCamelCase }}ById = ids -> supplyAsync(() ->
{{ nameCamelCase }}Service.findByIds(ids)
.stream()
.collect(Collectors.toMap({{ namePascalCase }}::getId, Function.identity())));
{%for field in fields%}{%if field.isFieldRelational%}
@DgsDataLoader(name = "{{ namePluralCamelCase }}By{{ field.fieldNamePascalCase }}")
MappedBatchLoader<Integer, List<{{ namePascalCase }}>> {{ namePluralCamelCase }}By{{ field.fieldNamePascalCase }} = ids -> supplyAsync(() ->
{{ nameCamelCase }}Service.findBy{{ field.fieldNamePluralPascalCase }}(ids)
.stream()
.collect(Collectors.groupingBy({{ namePascalCase }}::get{{ field.fieldNamePascalCase }})));
{%endif%}{%endfor%}
}
Loading

0 comments on commit e90aafd

Please sign in to comment.