crud

package
v0.297.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 1, 2025 License: Apache-2.0 Imports: 3 Imported by: 3

README

CRUD port

Using formalized CRUD (Create, Read, Update, Delete) interfaces in your software design has numerous benefits that can lead to more robust, maintainable, and scalable applications. It also reduce the learning curve for new team members.

Easy Testing

When you create an adapter for a specific role interface, you can effortlessly incorporate the CRUD contract testing suite. This suite verifies that your implementation behaves consistently with other solutions, meeting the expectations of the domain code.

Notable benefits of using the crud port

Consistency

CRUD interfaces provide a standardized way to interact with your application's data, ensuring that developers can easily understand and work with the codebase. This promotes a consistent design pattern across the entire application, making it easier to maintain and extend.

Abstraction

CRUD interfaces abstract the underlying implementation details of data storage and manipulation, allowing developers to focus on the business logic instead of the specifics of the data source. This means that you can easily swap out the data source (e.g., from a local file to a remote database) without having to change the application code.

Flexibility

By using CRUD interfaces, you can easily represent a RESTful API resource from an external system or expose your own entities on your HTTP API.

Caching

Wrapping a repository that uses CRUD interfaces with caching is straightforward and can be done without leaking implementation details into your domain layer. This can improve performance by reducing the number of calls to the underlying data source, which might be slow or have limited resources.

Example use-cases where utilising CRUD port can benefit your system design

TL;DR: using formalized CRUD interfaces in your software design offers consistency, abstraction, flexibility, and the ability to easily incorporate caching. By employing these interfaces, you can more easily interact with external RESTful API resources, expose your entities on your HTTP API, and improve your application's performance. The provided Go code demonstrates a set of CRUD interfaces that can be implemented to achieve these benefits.

Repository Pattern

The Repository pattern is a design pattern used in software development to abstract the way data is stored, fetched, and manipulated. It acts as a middle layer between the data source (such as a database or an API) and the business logic of the application. By decoupling the data access logic from the rest of the application, the Repository pattern promotes separation of concerns, maintainability, and testability.

In the Repository pattern, a repository is responsible for performing CRUD (Create, Read, Update, Delete) operations on a specific entity or a group of related entities. It provides a consistent interface to interact with the underlying data source, allowing developers to focus on the business logic rather than the specifics of data access. This also makes it easier to switch to a different data source or introduce new data sources without having to modify the application's core logic.

Working with External System's RESTful API as Resource

Imagine you want to integrate an external system, like a third-party API, into your application. By using CRUD interfaces, you can define a repository that communicates with the external API and maps the external resources to your internal data models. The CRUD operations can then be used to interact with the external system in a standardized way.

Exposing Entities on HTTP API

Suppose you want to expose your application's entities on an HTTP API. Using CRUD interfaces, you can create a generic RESTful handler that uses your repository as a data source. This handler can then be used to handle requests and perform the necessary CRUD operations on your entities, making it easy to implement the HTTP API without having to write custom code for each operation.

Documentation

Index

Constants

View Source
const (
	ErrAlreadyExists errorkit.Error = "err-already-exists"
	ErrNotFound      errorkit.Error = "err-not-found"
)

Variables

This section is empty.

Functions

This section is empty.

Types

type AllDeleter

type AllDeleter interface {
	// DeleteAll will erase all entity from the resource that has <V> type
	DeleteAll(context.Context) error
}

type AllFinder

type AllFinder[ENT any] interface {
	// FindAll will return all entity that has <V> type
	// TODO: consider using error as 2nd argument, to make it similar to sql package
	FindAll(context.Context) iter.Seq2[ENT, error]
}

type ByIDDeleter

type ByIDDeleter[ID any] interface {
	// DeleteByID will remove a <V> type entity from the repository by a given ID
	DeleteByID(ctx context.Context, id ID) error
}

type ByIDFinder

type ByIDFinder[ENT, ID any] interface {
	// FindByID is a function that tries to find an ENT using its ID.
	// It will inform you if it successfully located the entity or if there was an unexpected issue during the process.
	// Instead of using an error to represent a "not found" situation,
	// a return boolean value is used to provide this information explicitly.
	//
	//
	// Why the return signature includes a found bool value?
	//
	// This approach serves two key purposes.
	// First, it ensures that the go-vet tool checks if the 'found' boolean variable is reviewed before using the entity.
	// Second, it enhances readability and demonstrates the function's cyclomatic complexity.
	//   total: 2^(n+1+1)
	//     -> found/bool 2^(n+1)  | An entity might be found or not.
	//     -> error 2^(n+1)       | An error might occur or not.
	//
	// Additionally, this method prevents returning an initialized pointer type with no value,
	// which could lead to a runtime error if a valid but nil pointer is given to an interface variable type.
	//   (MyInterface)((*ENT)(nil)) != nil
	//
	// Similar approaches can be found in the standard library,
	// such as SQL null value types and environment lookup in the os package.
	FindByID(ctx context.Context, id ID) (ent ENT, found bool, err error)
}

type ByIDsFinder

type ByIDsFinder[ENT, ID any] interface {
	// FindByIDs finds entities with the given IDs in the repository.
	// If any of the ID points to a non-existent ENT, the returned iterator will eventually yield an error.
	FindByIDs(ctx context.Context, ids ...ID) iter.Seq2[ENT, error]
}

type Creator

type Creator[ENT any] interface {
	// Create is a function that takes a pointer to an entity and stores it in an external resource.
	// And external resource could be a backing service like PostgreSQL.
	// The use of a pointer type allows the function to update the entity's ID value,
	// which is significant in both the external resource and the domain layer.
	// The ID is essential because entities in the backing service are referenced using their IDs,
	// which is why the ID value is included as part of the entity structure fieldset.
	//
	// The pointer is also employed for other fields managed by the external resource, such as UpdatedAt, CreatedAt,
	// and any other fields present in the domain entity but controlled by the external resource.
	Create(ctx context.Context, ptr *ENT) error
}

type Deleter

type Deleter[ID any] interface {
	ByIDDeleter[ID]
	AllDeleter
}

Deleter request to destroy a business entity in the Resource that implement it's test.

type Finder

type Finder[ENT, ID any] interface {
	ByIDFinder[ENT, ID]
	AllFinder[ENT]
}

type Purger

type Purger interface {
	// Purge will completely wipe all state from the given resource.
	// It is meant to be used in testing during clean-ahead arrangements.
	Purge(context.Context) error
}

Purger supplies functionality to purge a resource completely. On high level this looks similar to what AllDeleter do, but in case of an event logged resource, this will purge all the events. After a purge, it is not expected to have anything in the repository. It is heavily discouraged to use Purge for domain interactions.

type QueryManyMethodSignature added in v0.243.0

type QueryManyMethodSignature[ENT, ARGS any] func(context.Context, ARGS) iter.Seq2[ENT, error]

QueryManyMethodSignature defines the structure of a "query many" method signature, designed to handle queries that return an unknown number of entities.

The method returns two values: - An iterator of entities (iter.Seq[ENT]), allowing efficient retrieval of multiple results - An error, which is only returned if there was a fundamental issue with the query itself

The use of an iterator is key when the result set size is unknown or large. It enables processing the results as they are fetched, without needing to load everything into memory at once.

This pattern also improves resource management by fetching results incrementally, making it suitable for working with large datasets or slow data sources.

Why return an iterator?

  1. It provides flexibility in handling a varying number of entities, allowing the caller to iterate over results efficiently, without requiring all data upfront.
  2. It separates concerns, using an error return to indicate issues with the query execution.
  3. It enables the support for supporting a streaming gateway to our application.

Similar to standard library patterns like SQL row iterators, this approach offers control over how the caller consumes the results, ensuring both performance and clarity in handling multiple entities.

type QueryOneMethodSignature added in v0.243.0

type QueryOneMethodSignature[ENT, ARGS any] func(context.Context, ARGS) (_ ENT, found bool, _ error)

QueryOneMethodSignature defines the structure of a "query one" method signature. It outlines how the method should retrieve a single entity and communicate the outcome.

The method returns three values: - The requested entity (_ENT) - A boolean 'found' indicating if the entity was located - An error if something went wrong during execution

Instead of using an error to signal a "not found" case, this signature uses the boolean 'found'. This clearly separates cases where the entity was not found from actual errors, making it explicit that no error occurred, but simply no matching entity was located.

Why return a boolean 'found' instead of using a nil pointer *ENT value?

There are several reasons for this: - The go-vet tool ensures that the 'found' variable is checked before using the entity. - It improves readability and highlights the method's cyclomatic complexity by making the flow easier to understand.

The method signature express the following cyclomatic complexity factors: - found/bool: Entity found (true) or not (false) - error: An error occurred (true) or did not (false)

This approach also prevents returning an initialized pointer that may be nil, which could lead to a runtime error when cast to an interface:

(MyInterface)((*ENT)(nil)) != nil

Similar patterns exist in the standard library, such as handling SQL null values and environment variable lookups in the os package.

type Saver

type Saver[ENT any] interface {
	// Save combines the behaviour of Creator and Updater in a single functionality.
	// If the entity is absent in the resource, the entity is created based on the Creator's behaviour.
	// If the entity is present in the resource, the entity is updated based on the Updater's behaviour.
	// Save requires the entity to have a valid non-empty ID value.
	Save(ctx context.Context, ptr *ENT) error
}

type Updater

type Updater[ENT any] interface {
	// Update will take a pointer to an entity and update the stored entity data by the values in received entity.
	// The ENT must have a valid ID field, which referencing an existing entity in the external resource.
	Update(ctx context.Context, ptr *ENT) error
}

Directories

Path Synopsis
Package erm stands for Entity-Relationship Modeling
Package erm stands for Entity-Relationship Modeling

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL