Skip to content

Commit

Permalink
Improve docs. Introduce generic loader component. Use Formik using ho…
Browse files Browse the repository at this point in the history
…ok instead of components for more flexibility.
  • Loading branch information
wnederhof committed Mar 11, 2024
1 parent 9293a67 commit 36805bd
Show file tree
Hide file tree
Showing 11 changed files with 152 additions and 115 deletions.
59 changes: 31 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,36 @@
# Basecode - The fastest way to build a web app
Build your next web app in days instead of months.
# Basecode - The fastest way to create a web app

Basecode is a full-stack code generator for Kotlin, Spring Boot, GraphQL, React (NextJS) and PostgreSQL.
Basecode is a full-stack code generator for creating web apps using Kotlin, Spring Boot, GraphQL, React (NextJS) and
PostgreSQL. The focus of Basecode is on creating lean, decoupled code with a solid foundation.

- **Productive**: Generate relational CRUD functionality for the frontend and backend including migrations, GraphQL schema extensions, unit tests and integration tests with a single command.
- **Maintainable**: A package-by-feature backend structure, GraphQL communication and an event-driven backend model make for a highly decoupled and extensible architecture which is built to last.
- **Incremental**: Start with almost no code. Then, once you're ready for the next step, add a GraphQL API, a frontend and more at your own pace.
Basecode is fully open source and community-driven ([MIT](LICENSE.md)).

Basecode introduces the concept of "non-intrusive relational scaffolding", which is designed to keep your code maintainable, even for entities with 1-N relationships.
## Installation
The following software needs to be installed on your machine before you can use Basecode effectively:

- **Relational:** the user may generate generate entities with 1-N relationships.
- **Non-intrusive:** code generated for one entity will not affect code of any another entity, nor will it *change* any other file in the project.
- Go 1.16 or later
- JDK 21 or later
- Node 18 or later
- Docker

## Installation
Make sure you have the Go 1.16 or later installed. Then run:
Install Basecode using the following command:
```shell
go install github.com/wnederhof/basecode/cmd/basecode@latest
```
Or replace `latest` with one of the tags found under Releases in GitHub.

## Usage
### New project
Provided that basecode is available under the alias `basecode`, you can create a new project using `basecode new`.
### Create a New Project
```
basecode new <groupId> <artifactId>
```
Here, `groupId` and `artifactId` are the name of the group and artifact respectively, as defined by Maven.

For example:
```
basecode new com.mycorp blog
basecode new com.example blog
```

### Generate
### Scaffold Generation
Using `basecode generate`, you can generate code based using one of the following generators.
```
backend:scaffold, bes Backend Scaffold
Expand All @@ -38,8 +40,14 @@ Using `basecode generate`, you can generate code based using one of the followin
frontend, fe Frontend Support
frontend:scaffold, fes Frontend Scaffold (Generate frontend support first)
scaffold, s Backend and Frontend Scaffold (Generate frontend support first)
backend:auth, ba Backend Authentication - EXPERIMENTAL
frontend:auth, fa Frontend Authentication - EXPERIMENTAL
```
For more information about the generators, run:
```
For more information about the generators, use `-h`:
basecode generate <generator name> -h
```
For example:
```
basecode generate scaffold -h
```
Expand All @@ -55,15 +63,13 @@ Available types:
- datetime
- boolean

For each of these types, you can add `?` to make this type optional. For example: `title:string?`.

# Example
When you want, for example, to generate a blog, you can do that as following:
```
basecode new com.mycorp blog
cd blog
basecode generate scaffold Post title
basecode generate scaffold Comment postId:Post comment
basecode generate scaffold Post title contents:text
basecode generate scaffold Comment postId:Post contents:text
```
Most generators specify the following parameters:
```
Expand All @@ -75,15 +81,12 @@ Here:
- `delete` will undo the file generation. This command may also additional generate files, such as migration scripts for dropping a previously created table.
- `overwrite` will overwrite any existing files. When this option is not specified, Basecode will abort when a file is about to be overwritten.

## Development
For developing your application, you can use `docker-compose up` to spin up a development database. You can then 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`.
## After Initialization
After initializing your application, you can use `docker-compose up` to spin up a development database.

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

When both the backend and frontend are running, you can build your next best thing at: `http://localhost:3000`. (Note: as of yet, there is no index page). If you created an entity called `Post`, you will find your scaffolds at: `http://localhost:3000/posts`.
You can then 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`.

## License
Licensed under [MIT](LICENSE.md).
To start the frontend, make sure your artifacts are installed using `npm install` and run `npm run dev`.

## Credits
- The `new` template is based on the code generated using Spring Boot Initializr.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ describe('{{ namePascalCase }}Form', () => {
</Provider>
)
const expectedFormValues = {
...{{ nameCamelCase }},
id: undefined{%for field in fields%}{%if field.isFieldRelational%},
...{{ nameCamelCase }}{%for field in fields%}{%if field.isFieldRelational%},
{{field.fieldNameCamelCase}}: '1'{%endif%}{%endfor%}
}
delete expectedFormValues.id
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { gql, useQuery } from 'urql'
import { useMemo } from 'react'
import { Query } from '@generated/graphql'
import { LoadingIndicator } from '@components/LoadingIndicator'

export interface {{ namePascalCase }}DetailsProps {
id: string | number
Expand Down Expand Up @@ -30,7 +31,7 @@ export function {{ namePascalCase }}Details(props: {{ namePascalCase }}DetailsPr
}

if (fetching || !data) {
return <div>Loading...</div>
return <LoadingIndicator />
}

return (
Expand All @@ -51,4 +52,4 @@ export function {{ namePascalCase }}Details(props: {{ namePascalCase }}DetailsPr
</tbody>
</table>
)
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { gql, OperationResult, useMutation, useQuery } from 'urql'
import { useMemo } from 'react'
import { Mutation, Query{%for field in fields%}{%if field.isFieldRelational%}, {{ field.fieldTypePascalCase }}{%endif%}{%endfor%} } from '@generated/graphql'
import { Formik, Field, Form, FormikHelpers } from 'formik'
import { useFormik } from 'formik'
import { LoadingIndicator } from '@components/LoadingIndicator'

export interface {{ namePascalCase }}FormProps {
id?: string | number{%for field in fields%}{%if field.isFieldRelational%}
Expand Down Expand Up @@ -39,14 +40,12 @@ export function {{ namePascalCase }}Form(props: {{ namePascalCase }}FormProps) {
}

const doSubmit = (
formData: {{ namePascalCase }}FormData,
{ setSubmitting }: FormikHelpers<{{ namePascalCase }}FormData>
formData: {{ namePascalCase }}FormData
) => {
if (props.id) {
executeUpdateMutation({ id: props.id, input: formData })
.then(result => handleMutationResult(result) && props.onSave())
.catch((reason) => alert(`Updating {{ nameCamelCase }} failed. Reason: ${reason}`))
.finally(() => setSubmitting(false))
} else {{ "{" }}{%if hasRelations%}
const formDataWithRelations = { ...formData }{%endif%}{%for field in fields%}{%if field.isFieldRelational%}
if (props.{{ field.fieldNameCamelCase }}) {
Expand All @@ -55,7 +54,6 @@ export function {{ namePascalCase }}Form(props: {{ namePascalCase }}FormProps) {
executeCreateMutation({ input: formData{%if hasRelations%}WithRelations{%endif%} })
.then(result => handleMutationResult(result) && props.onSave())
.catch((reason) => alert(`Creating {{ nameCamelCase }} failed. Reason: ${reason}`))
.finally(() => setSubmitting(false))
}
}

Expand Down Expand Up @@ -95,7 +93,7 @@ export function {{ namePascalCase }}Form(props: {{ namePascalCase }}FormProps) {
})
{%endif%}{%endfor%}
if (fetching{%for field in fields%}{%if field.isFieldRelational%} || {{ field.fieldTypePluralCamelCase }}QueryResult.fetching{%endif%}{%endfor%}) {
return <div>Loading...</div>
return <LoadingIndicator />
}

const errorMessage = error?.message{%for field in fields%}{%if field.isFieldRelational%} || {{ field.fieldTypePluralCamelCase }}QueryResult.error?.message{%endif%}{%endfor%}
Expand All @@ -104,61 +102,38 @@ export function {{ namePascalCase }}Form(props: {{ namePascalCase }}FormProps) {
return <div>{errorMessage}</div>
}

const formik = useFormik({
initialValues: {{ "{" }}{%for field in fields%}
{{ field.fieldNameCamelCase }}: data?.{{ nameCamelCase }}?.{{ field.fieldNameCamelCase }}{%if field.fieldType == "BOOLEAN" or field.fieldType == "NULL_BOOLEAN"%} || false{%endif%}{%if field.fieldType == "STRING" or field.fieldType == "NULL_STRING" or field.fieldType == "TEXT" or field.fieldType == "NULL_TEXT"%} || ''{%endif%},{%endfor%}
},
onSubmit: doSubmit
})

return (
<Formik
onSubmit={doSubmit}
initialValues={{ "{" }}{{ "{" }}{%for field in fields%}
{{ field.fieldNameCamelCase }}: data?.{{ nameCamelCase }}?.{{ field.fieldNameCamelCase }}{%if field.fieldType == "BOOLEAN" or field.fieldType == "NULL_BOOLEAN"%} || false{%endif%}{%if field.fieldType == "STRING" or field.fieldType == "NULL_STRING" or field.fieldType == "TEXT" or field.fieldType == "NULL_TEXT"%} || ''{%endif%},{%endfor%}
{{ "}" }} as {{ namePascalCase }}FormData{{ "}" }}
>
<Form role="form">
{%for field in fields%}
<div>
<label htmlFor="{{ field.fieldNameCamelCase }}">{{ field.fieldNamePascalCase }}</label>{%if field.fieldType == "STRING" or field.fieldType == "NULL_STRING" %}
<Field
name="{{ field.fieldNameCamelCase }}"
type="text"
/>
<form role="form" onSubmit={formik.handleSubmit}>{%for field in fields%}
<div>
<label htmlFor="{{ field.fieldNameCamelCase }}">{{ field.fieldNamePascalCase }}</label>{%if field.fieldType == "STRING" or field.fieldType == "NULL_STRING" %}
<input name="{{ field.fieldNameCamelCase }}" type="text" onChange={formik.handleChange} value={formik.values.{{ field.fieldNameCamelCase }}{{ "}" }} />
{%elif field.fieldType == "INT" or field.fieldType == "NULL_INT" %}
<Field
name="{{ field.fieldNameCamelCase }}"
type="number"
/>
<input name="{{ field.fieldNameCamelCase }}" type="number" onChange={formik.handleChange} value={formik.values.{{ field.fieldNameCamelCase }}{{ "}" }} />
{%elif field.fieldType == "TEXT" or field.fieldType == "NULL_TEXT" %}
<Field
name="{{ field.fieldNameCamelCase }}"
as="textarea"
/>
<textarea name="{{ field.fieldNameCamelCase }}" onChange={formik.handleChange} value={formik.values.{{ field.fieldNameCamelCase }}{{ "}" }} />
{%elif field.fieldType == "INT" or field.fieldType == "NULL_INT" %}
<Field
name="{{ field.fieldNameCamelCase }}"
type="number"
/>
<input name="{{ field.fieldNameCamelCase }}" type="number" />
{%elif field.fieldType == "DATE" or field.fieldType == "NULL_DATE" %}
<Field
name="{{ field.fieldNameCamelCase }}"
type="date"
/>
<input name="{{ field.fieldNameCamelCase }}" type="date" onChange={formik.handleChange} value={formik.values.{{ field.fieldNameCamelCase }}{{ "}" }} />
{%elif field.fieldType == "BOOLEAN" or field.fieldType == "NULL_BOOLEAN" %}
<Field
name="{{ field.fieldNameCamelCase }}"
type="checkbox"
/>
<input name="{{ field.fieldNameCamelCase }}" type="checkbox" onChange={formik.handleChange} value={formik.values.{{ field.fieldNameCamelCase }}{{ "}" }} />
{%elif field.fieldType == "RELATIONAL" %}
<Field
name="{{ field.fieldNameCamelCase }}"
as="select"
hidden={props.{{ field.fieldNameCamelCase }}}
>
<option />
{{ "{" }}{{ field.fieldTypePluralCamelCase }}QueryResult?.data?.{{ field.fieldTypePluralCamelCase }}?.map(({{ field.fieldTypeCamelCase }}: {{ field.fieldTypePascalCase }}) =>
<option key={{ "{" }}{{ field.fieldTypeCamelCase }}.id} value={{ "{" }}{{ field.fieldTypeCamelCase }}.id}>{{ "{" }}{{ field.fieldTypeCamelCase }}.id}</option>
)}
</Field>
{%endif%} </div>{%endfor%}
<button type="submit">Save</button>
<button type="button" onClick={props.onCancel}>Cancel</button>
</Form>
</Formik>
{props.{{ field.fieldNameCamelCase }} ? <></> : <select name="{{ field.fieldNameCamelCase }}" onChange={formik.handleChange} value={formik.values.{{ field.fieldNameCamelCase }}{{ "}" }}>
<option />
{{ "{" }}{{ field.fieldTypePluralCamelCase }}QueryResult?.data?.{{ field.fieldTypePluralCamelCase }}?.map(({{ field.fieldTypeCamelCase }}: {{ field.fieldTypePascalCase }}) =>
<option key={{ "{" }}{{ field.fieldTypeCamelCase }}.id} value={{ "{" }}{{ field.fieldTypeCamelCase }}.id}>{{ "{" }}{{ field.fieldTypeCamelCase }}.id}</option>
)}
</select>}
{%endif%} </div>{%endfor%}
<button type="submit">Save</button>
<button type="button" onClick={props.onCancel}>Cancel</button>
</form>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Link from 'next/link'
import { gql, useMutation, useQuery } from 'urql'
import { useMemo } from 'react'
import { Mutation, Query } from '@generated/graphql'
import { LoadingIndicator } from '@components/LoadingIndicator'
{%if hasRelations%}
export interface {{ namePascalCase }}ListProps {{ "{" }}{%for field in fields%}{%if field.isFieldRelational%}
{{ field.fieldNameCamelCase }}?: string{%endif%}{%endfor%}
Expand Down Expand Up @@ -45,7 +46,7 @@ export function {{ namePascalCase }}List({%if hasRelations%}props: {{ namePascal
})

if (fetching) {
return <div>Loading...</div>
return <LoadingIndicator />
}

if (error?.message) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { useEffect, useState } from 'react'

export const LoadingIndicator = () => (
<div>Loading...</div>
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Link from 'next/link'

export interface Breadcrumb {
label: string,
href: string,
}

export interface DefaultLayoutProps {
breadcrumbs?: Breadcrumb[],
children?: ReactNode,
}

export const DefaultLayout(props: DefaultLayoutProps) {
return (
<>
{props.breadcrumbs && <ul>
{props.breadcrumbs.map((breadcrumb, i) => (
<div key={i}>
<Link href={breadcrumb.href}>{breadcrumb.label}</Link}
</div>
)}
</ul>}
{children}
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ import React from 'react'
// )}
// </ResetUrqlClientContext.Consumer>

export const ResetUrqlClientContext = React.createContext(undefined)
export const ResetUrqlClientContext = React.createContext<() => void>(() => {})
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { ResetUrqlClientContext } from '@lib/reset-urql-client-context'
import { withUrqlClient } from 'next-urql'
import type { AppProps } from 'next/app'
import Head from 'next/head'
import {
cacheExchange,
fetchExchange,
mapExchange
} from 'urql'

function App({
Component,
pageProps,
resetUrqlClient,
}: AppProps & { resetUrqlClient: () => void }) {
return (
<>
<Head>
<title>{{ artifactId }}</title>
<meta name="description" content="{{ artifactId }}" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</Head>
<ResetUrqlClientContext.Provider value={resetUrqlClient}>
<Component {...pageProps} />
</ResetUrqlClientContext.Provider>
</>
)
}

export default withUrqlClient((ssrExchange) => {
const exchanges = [
cacheExchange,
mapExchange({}),
ssrExchange,
fetchExchange,
]
return {
url: '/graphql',
exchanges,
fetchOptions: {
credentials: 'include',
},
requestPolicy: 'cache-and-network',
}
})(App)
Loading

0 comments on commit 36805bd

Please sign in to comment.