Building Block View

Backend Structure

  • config/: Contains the servers configuration.
  • controller/: Contains all endpoints of the application.
  • db/: Contains utility functions for the database.
  • model/: Contains the data PermaplanT is acting upon and their implementation.
  • service/: Contains business logic and maps between entities and DTOs.
  • test/: Contains integration tests.
graph LR;
    config -->|Configures routes<br>Uses endpoints for OpenAPI doc| controller
    config -->|Uses DTOs for OpenAPI doc| model
    controller -->|Calls functions| service
    controller -->|Uses DTOs| model
    controller -->|Uses data pool| db
    db -->|Uses `Page` struct| model
    model -->|Uses helper functions| db
    service -->|Calls database| model
    service -->|Uses DTOs and Entities| db

Controller

The controller layer contains all endpoints of the application.
The actual routes are defined here while the controller layer only contains the actual implementation of the endpoints.

When an endpoint gets called Actix clones an internally stored pool of connections to the database and passes it to the endpoint to be used.
We then 'forward' the pool to the service layer where a connection is retrieved from the pool.
The persistence layer then uses that connection to make calls to the database.

The endpoints are automatically documented using utoipa which can generate OpenAPI documentation from code.

Service

The service layer is responsible for handling our business logic as well as mapping entities and DTOs.

Model

The model/ folder contains the data PermaplanT is acting upon.
Entities are shared with the database using the ORM diesel.
DTOs are shared with the frontend using typeshare.

The files entity.rs and dto.rs contain the actual structs. That way you have a quick overview of what the data looks like without having to navigate multiple files.
The actual implementation of the structs is in separate files to reduce the line length of the files.

Frontend Structure

  • __mocks__/: Contains mocked modules for tests.
  • assets/: Contains small asset files like SVGs.
  • components/: Contains components that are shared across features.
  • config/: Contains initialization code for the environment and libraries.
  • features/: Contains the distinct features of the application.
  • generated/: Contains generated code.
    • docs/: Automatically generated documentation.
    • api_types/: The types generated via typeshare from the backend API.
  • hooks/: Contains hooks that are shared across features.
  • routes/: Contains the routes of the application.
  • styles/: Contains the global and other style sheets, e.g. to customize a component from a third party library like react-leaflet.
  • utils/: Contains additional utility functions, that are shared across features or components and are not hooks.
flowchart LR
  routes --> |Renders route features| features
  features -->|Uses types for API calls| generated/api_types
  features -->|Uses shared components| components
  features -->|Calls functions| config
  features -->|Uses hooks| hooks
  features -->|Calls functions| utils
  features -->|Uses asset files| assets
  features -->|Uses some miscellaneous styles| styles
  features ---->|Uses another feature's public API| i
    subgraph f[example feature]
      direction TB
      i[index]
      e[fa:fa-folder example feature structure] --> |Components scoped to a specific feature| c[components]
      e -->|API request declarations and API hooks related to a specific feature| a[API]
      e -->|Route components for a specific feature's pages| r[routes]
      e -->|State stores for a specific feature| s[stores]
    end

__mocks__ is not used during runtime

__mocks__

This folder has a special name to be recognized by our test runner vitest. More details on how mocking with vitest works can be found in the documentation.

assets

This folder should contain only small asset files like SVGs, so that our bundle size stays small. Larger files should be hosted on Nextcloud.

generated/api_types

This folder contains API types that are generated by typeshare. They can be regenerated by running the command npm run generate-api-types.

components

All components that reside inside here need to be documented in storybook.

The minimum requirements for documentation are:

  • If the component can be in different states (e.g. a visual variant, or an open/closed state), each state should be described by a story.
  • Every property that can be passed to the component needs to be documented.

features

Most of the application's code lives here. Every folder created here represents a given feature and contains its domain specific code. For example, if a feature interacts with the backend via network requests, it would have a sub module api that encapsulates this.

Features are allowed to import another feature's public API which is exported from its index.ts file. Features are also allowed to import all other previously mentioned modules if needed.

Each feature can have its own separate store to manage its state. This allows for modular and independent state management across different features.

Folder Structure for each Feature

  • api/: Contains API calls to the backend
  • components/: Contains components that are related to the feature.
  • hooks/: Contains hooks that are related to the feature.
  • routes/: Contains the routes of the feature.
  • utils/: Contains additional utility functions, that are related to the feature.
  • store/: Contains stores that manage state of the feature.

The map_planning feature

Store

The MapStore is related to the map planning feature and contains all layer-related state and logic. The main map store is composed of two sub-stores:

  • TrackedMapStore: stores the state of the map that is tracked by the history. This state is used to undo/redo actions.
  • UntrackedMapStore: stores the state of the map that is not tracked by the history (e.g., the selected layer and the layer opacities).

Components like the timeline or the transformer use separate independent stores. This separation allows the components or sub-features to manage its specific state that is not directly related to the map or layer management.

Additional Structure

  • layers/: Contains components and utilities that are related to layers. Each layer type has its own folder.

Subfeature Structure

If there are subfeatures such as a timeline or transformer component, corresponding subfolders named after the subfeature are created within directories like store or components to encapsulate their specific implementations.

Layer states

For each layer there is a TrackedLayerState and an UntrackedLayerState. If the layer introduces new properties the new types (e.g. TrackedPlantLayerState and UntrackedPlantLayerState) should be extended from these. The same applies for the corresponding ObjectState types.

  • add new types
/**
 * The state of an image object.
 */
export type ImageObjectState = ObjectState & {
  imageUri: string;
};

/**
 * The state of a map's photo layer.
 */
export type TrackedPhotoLayerState = {
  index: LayerName;
  /**
   * The state of the objects on the layer.
   */
  objects: ImageObjectState[];
};
  • modify layer type
/**
 * The state of the layers of the map.
 */
export type TrackedLayers = {
  [key in Exclude<LayerName, "Photo">]: TrackedLayerState;
} & {
  Photo: TrackedPhotoLayerState;
};

the above construct may eventually be refactored into the below as more specific types get added to the layer state.

export type TrackedLayers = {
  Base: TrackedBaseLayerState;
} & {
  Plant: TrackedPlantLayerState;
} & {
  Photo: TrackedPhotoLayerState;
} & {
  ...
}