Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 17 additions & 11 deletions docs/development/concepts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,20 @@ This guide will introduce some concepts and give you a big picture of the develo

Please choose an area that you would like to read about:

| Topic | Content |
|---------------------------------------------------------|:------------------------------------------------------------------------------|
| [Application architecture](../application-architecture) | An introduction of the application architecture used at OpenProject. |
| [State management](state-management) | How does the frontend handle state and react to changes? |
| [HAL resources](hal-resources) | What are HAL resources and how are they used in the frontend? |
| [Permissions](permissions) | How are roles and permissions handled in OpenProject code? |
| [Translations](translations) | How are translations used and built? |
| [Resource schemas](resource-schemas) | What is a schema and how is it tied to an editable resource? |
| [Resource changesets](resource-changesets) | How is change tracked to resources in the frontend? How to save the changes. |
| [Inline editing](inline-editing) | How does inline editing and the edit field functionality work in OpenProject? |
| [Queries and QuerySpace](queries) | What is the Query API concept and how is it used in the frontend? |
| Topic | Content |
|---------------------------------------------------------|:-----------------------------------------------------------------------------------|
| [Application architecture](../application-architecture) | An introduction of the application architecture used at OpenProject. |
| [Infrastructure](infrastructure) | Which distributions and infrastructure models is OpenProject available in? |
| [Hotwire with ViewComponents](hotwire-view-components) | How do we use Hotwire and Turbo Streams alongside ViewComponents? |
| [Stimulus](stimulus) | How do we add interactivity to server-rendered pages with Stimulus? |
| [State management](state-management) | How does the frontend handle state and react to changes? |
| [HAL resources](hal-resources) | What are HAL resources and how are they used in the frontend? |
| [Permissions](permissions) | How are roles and permissions handled in OpenProject code? |
| [Translations](translations) | How are translations used and built? |
| [Resource schemas](resource-schemas) | What is a schema and how is it tied to an editable resource? |
| [Resource changesets](resource-changesets) | How is change tracked to resources in the frontend? How to save the changes. |
| [Inline editing](inline-editing) | How does inline editing and the edit field functionality work in OpenProject? |
| [Queries and QuerySpace](queries) | What is the Query API concept and how is it used in the frontend? |
| [WYSIWYG editor](wysiwyg-editor) | How does the CKEditor-based editor convert between HTML and Markdown? |
| [Detail tables](has-details-table) | How to extend a model with extra columns in a separate table with HasDetailsTable? |
| [Secure coding guidelines](secure-coding) | How to write secure code and prevent common vulnerabilities? |
53 changes: 33 additions & 20 deletions docs/development/concepts/hal-resources/README.md
Original file line number Diff line number Diff line change
@@ -1,28 +1,39 @@
---
sidebar_navigation:
title: HAL+JSON resources
description: Get an overview of how inline-editing of resources works
description: Get an overview of how the frontend loads and represents HAL+JSON API resources
keywords: development concepts, HAL, JSON, hal resources, API requests
---

# Development concept: HAL resources

HAL resources are the frontend counterpart to the `HAL+JSON` API of OpenProject. They are class instance of the JSON resources with action links being turned into callable functions to perform requests.
HAL resources are the frontend counterpart to the `HAL+JSON` API of OpenProject. They are class instances of the JSON resources with action links being turned into callable functions to perform requests.

## Key takeaways

_HAL resources ..._

- are requested from the APIv3 endpoints and generated from their JSON response by the [`HALResourceService`](https://github.com/opf/openproject/tree/dev/frontend/src/app/features/hal/services/hal-resource.service.ts).
- contain `$links` and `$embedded` properties to map the original JSON object for linked resources, and the ones that were embedded to the response.
- Can have an arbitrary number of properties on the object that map to the JSON properties, or elements from the `_links` and `_embedded` JSON segments.
- They unfortunately are complex and mutable objects
- can have an arbitrary number of properties on the object that map to the JSON properties, or elements from the `_links` and `_embedded` JSON segments.
- are, unfortunately, complex and mutable objects.

## Choosing an approach for new code

`HalResource` is the oldest way the frontend works with API data, and much existing Angular code depends on it. For new code, prefer the newer options:

- **Server-rendered UI with Hotwire and Primer.** For new screens and features, render HTML in Rails with Primer ViewComponents and add behaviour with Stimulus. The frontend then does not need to touch HAL at all. See [Using Hotwire with ViewComponents](../hotwire-view-components) and [Stimulus](../stimulus).
- **Modern Angular in existing Angular screens.** New Angular components are still fine where they integrate with existing Angular screens, such as the work package table or work package editing. Write them with `inject()`, signal inputs, and Signals wherever RxJS is not the right mental model.
- **Resource stores in `core/state`.** When Angular code needs API data, use a [`ResourceStoreService`](https://github.com/opf/openproject/tree/dev/frontend/src/app/core/state/resource-store.service.ts) subclass such as `ProjectsResourceService`. These cache resources by ID and expose them as plain, typed interfaces (e.g., `IProject`) over the HAL JSON, without `HalResource` classes or in-place mutation. These are the current option for Angular code, not a long-term direction: they are built on [Akita](https://github.com/salesforce/akita), which is no longer maintained, and the frontend is moving towards Hotwire.
- **`HalResource`.** Use it when working on existing code built around it, or where no resource store exists for the type yet.

The rest of this document explains `HalResource` in full.

## Prerequisites

HAL resources on the frontend have no explicit prerequisite on our frontend. You will likely want to take a look at the [API documentation and the section on HAL+JSON](../../../api/introduction).

## Primer on HAL JSON
## HAL JSON in a nutshell

The JSON response in HAL standard can contain these things:

Expand Down Expand Up @@ -129,22 +140,22 @@ In this linked example, only the `status` and `project` links and embedded resou

## HalResourceService

On to loading the JSON resources from the API and turning them into usable class instances. This is the job of the the [`HALResourceService`](https://github.com/opf/openproject/tree/dev/frontend/src/app/features/hal/services/hal-resource.service.ts). It has two responsibilities:
On to loading the JSON resources from the API and turning them into usable class instances. This is the job of the [`HALResourceService`](https://github.com/opf/openproject/tree/dev/frontend/src/app/features/hal/services/hal-resource.service.ts). It has two responsibilities:

1. It uses the Angular `HTTPModule` for performing API requests to the APIv3
2. It turns the responses of these requests (or HAL JSON generated in the frontend) into a HAL resource class
1. It uses Angular's `HttpClient` for performing API requests to the APIv3
2. It turns the responses of these requests (or HAL JSON generated in the frontend) into a HAL resource class

### Performing requests against HAL API endpoints

The service has HTTP `get`, `post`, `put`, etc. methods as well as a generic `request` method that accept an URL and params/payload, and respond with an observable to the JSON transformed into a HAL resource.
The service has HTTP `get`, `post`, `put`, etc. methods as well as a generic `request` method that accept a URL and params/payload, and respond with an observable to the JSON transformed into a HAL resource.

### Error Handling

For errors returned by the HAL API (specific error `_type` response in the JSON) or when erroneous HTTP statuses are being returned, the `HALResourceService` will wrap these into `ErrorResources` for identifying the cause and potentially, additional details to present to the frontend. This is used for example when saving work packages and validation errors occur. The validations are being output in details for individual attributes.

## Linked HAL resources

The `_links` entries of a HAL resource can have a `url`, `method`, and `title` property. They can also be `templated` if the link needs to be filled out by the frontend (e.g., to set a related ID to pass into it).
The `_links` entries of a HAL resource can have an `href`, `method`, and `title` property. They can also be `templated` if the link needs to be filled out by the frontend (e.g., to set a related ID to pass into it).

In the process of building the HAL resource, action `_links` objects are being turned into resources themselves:

Expand Down Expand Up @@ -174,7 +185,7 @@ console.log(wp.project.href); // /api/v3/projects/1
// The resource is not embedded, thus not loaded
console.log(wp.project.$loaded); // false
// The name property is available from the title attribute
console.log(project.name); // Demo Project
console.log(wp.project.name); // Demo Project

// Explicitly load the HAL resource
const project = await wp.project.$load();
Expand All @@ -183,11 +194,10 @@ console.log(project.name); // Demo Project
console.log(wp.project.$loaded); // true
```

On first glance, it might look nice to be able to `$load()` the embedded project on the fly and use the returning promise. However, this request will not be cached anywhere, thus loading the same project on multiple work packages will result in multiple requests.

Also, the `workPackage` state will be constantly mutated whenever these requests happen. You will always have to check whether the resource was loaded.
> [!CAUTION]
> `$load()` is not cached and mutates the shared resource. Loading the same linked resource from several places sends repeated requests. Prefer a resource store where one exists.
Instead of explicitly loading embedded resources, the frontend now usually uses a `CacheService` to load and cache a resource of a specific type by its href. For example, for the project, there is a `ProjectCacheService#require(href)` method that will ensure a project is loaded, or fetched from cache and returns a promise to use. This will no longer mutate the work package resource.
For example, `ProjectsResourceService#requireEntity(href)` returns an observable of the project, fetched from the API only if it is not already cached. This does not mutate the work package resource. See [Choosing an approach for new code](#choosing-an-approach-for-new-code).

However, there are still use cases where `.$load()` is used and the resource is mutated.

Expand All @@ -197,11 +207,11 @@ In order to turn the JSON properties from `_embedded` and `_links` into writable

- Maintaining a `$source` property which is the pristine JSON response from the API.

- Mapping the properties under `_links` into `$links` property with `HalLinks` that can be called in the application. `e.g., workPackage.$links.update()` will call the API to the URL behind that link.
- Mapping the properties under `_links` into a `$links` property with `HalLink`s that can be called in the application. E.g., `workPackage.$links.update()` will call the API to the URL behind that link.

- Mapping the properties under `_embedded` into `$embedded` and turning each of these into their own `HalResource` instance.

- It definers setters to all properties of the HAL resource to modify the `$source` object. For example, if you have a link `_links.project` in your JSON, you can override the project used for the resource with `resource.project = projectResource` or `resource.project = { href: '/api/v3/projects/1234' }`. This will modify the `$source` object.
- It defines setters for all properties of the HAL resource to modify the `$source` object. For example, if you have a link `_links.project` in your JSON, you can override the project used for the resource with `resource.project = projectResource` or `resource.project = { href: '/api/v3/projects/1234' }`. This will modify the `$source` object.

The frontend doesn't really use this anymore due to it boiling down to a large mutable object. Instead, we use `ResourceChangesets` to modify resources and save them. [Click here to see the separate concept on them](../resource-changesets).

Expand All @@ -211,8 +221,11 @@ In order to turn the JSON properties from `_embedded` and `_links` into writable
- [`halResource.config.ts`](https://github.com/opf/openproject/tree/dev/frontend/src/app/features/hal/services/hal-resource.config.ts) for identifying what types in the JSON response and its members/links are being turned into which classes.
- [`HalResource`](https://github.com/opf/openproject/tree/dev/frontend/src/app/features/hal/resources/hal-resource.ts) the base HAL resource class
- [`HAL resource builder`](https://github.com/opf/openproject/tree/dev/frontend/src/app/features/hal/helpers/hal-resource-builder.ts) used for wiring up the links and embedded JSON properties into members of the HAL resource classes
- [`ResourceStoreService`](https://github.com/opf/openproject/tree/dev/frontend/src/app/core/state/resource-store.service.ts) the base class for the typed, cached resource stores that are preferred for new code

## Discussions
## Known problems

- Due to the dynamic properties of the HAL resource, it traditionally has an index map to `any` which is the source of many typing issues and in turn, quite a number of bugs: [hal-resource.ts](https://github.com/opf/openproject/blob/dev/frontend/src/app/features/hal/resources/hal-resource.ts#L63)
- The way HAL resources work by embedding and allowing to load
- **Dynamic properties are untyped.** Because properties are generated from whatever the JSON contains, `HalResource` has an index signature. It used to be `any`, which hid many typing errors and caused bugs. It is now `unknown` ([OP-20349](https://community.openproject.org/wp/OP-20349)), so reading an undeclared property requires narrowing it. Prefer declaring the property on the resource's class or interface (e.g., `WorkPackageResource`) over casting at the call site.
- **Resources are mutated in place.** `$load()` and the generated setters change the resource object, and every holder of that object sees the change. To edit a resource, use [resource changesets](../resource-changesets) instead of assigning to its properties.
- **`$load()` is not cached.** Calling `$load()` on the same linked resource from several places sends a request each time. Use a resource store instead.
- **Linked and embedded resources look alike.** Whether a property is fully loaded depends on what the backend chose to embed for that endpoint. Check `$loaded` before relying on anything beyond `href` and the link `title`.
Loading
Loading