View Source mix scribe.gen.domain (Elixir Scribe v0.3.0)

Generates a module per action for a resource within a domain, with functions around an Ecto schema.

The goal and motivation is to encourage developers to write cleaner code in a more organized folder structure, enabling them to know in seconds all domains, resources, and actions used in a project. This also contributes to reduce the technical debt via less complexity and clear boundaries between domains, resources and actions.

Usage Examples

The scribe.gen.domain generator will generate files in both lib/my_app/* and tests/* folders with the same hierarchy structure.

Usually this command isn't used directly, unless you only want to reap it's benefits for the code at lib/my_app/*, but not at lib/my_app_web/*. Most commonly usage is for the domain generator to be invoked from scribe.gen.html or scribe.gen.live.

For example, to create a fictitious online shop app you may start with this command:

mix scribe.gen.domain Catalog Category categories name:string desc:string

The first argument is the domain name followed by the resource name and its plural name, which is also used as the schema table name.

By default the command will generate some default actions for each resource on a domain: ["list", "new", "read", "edit", "create", "update", "delete"]

We can also pass as many custom actions as we want with the flag --actions:

$ mix scribe.gen.domain Catalog Product products name:string desc:string --actions import,export

When don't need to use the default actions, then just pass the flag --no-default-actions:

mix scribe.gen.domain Warehouse Stock stocks product_id:integer quantity:integer --actions import,export --no-default-actions

The folder structure

By using the previous command examples we get this folder structure:

lib/my_app
├── catalog
│   ├── category
│   │   ├── create
│   │   ├── delete
│   │   ├── edit
│   │   ├── list
│   │   ├── new
│   │   ├── read
│   │   └── update
│   └── product
│       ├── create
│       ├── delete
│       ├── edit
│       ├── export
│       ├── import
│       ├── list
│       ├── new
│       ├── read
│       └── update
└── warehouse
    └── stock
        ├── export
        └── import

Domains: catalog, warehouse Resources: category, product, stock Actions: create, delete, edit, export, import, list, new, read, update

This is a very simplistic view of a project. Now, imagine reaping the benefits of this folder structure implemented on your extensive codebase, which may now contain dozens, hundreds, or even thousands of resources, each with potentially more actions than the ones exemplified here.

The API Boundary

To prevent direct access between resources of the same domain or cross domains an API is provided for each Resource to act as a boundary that MUST never be crossed to not couple domains and resources via the internal implementation.

This enables developers to change the internal implementation of any action of a Resource knowing that the only caller is the resource API boundary.

The API boundary for each resource in a domain is located at the root of the domain folder. For example: MyApp.Catalog.CategoryAPI, which can be found at lib/my_app/catalog/category_api.ex.

Developers need to treat anything inside a resource as private, which for the Catalog domain (exemplified above) means to not access any module inside lib/my_app/catalog/category/*.

The Schema

The schema is responsible for mapping the database fields into an Elixir struct. A migration file for the repository will also be generated.

The schema can be found at lib/my_app/catalog/category_schema.ex and it's considered private, except for it's struct representation. To work with changesets you want to use the Domain API, and only used this module directly to pattern match on it's struct. For example, instead of using Category.changeset/2 to create a new changeset, use "CategoryAPI.new/1".

Generating without a schema

In some cases, you may wish to bootstrap the domain and its resources without any logic to access the schema, leaving the internal implementation to yourself. Use the --no-schema flag to accomplish this.

For example:

mix scribe.gen.domain Accounts Company companies --no-schema

The Database Table

By default, the table name for the migration and schema will be the plural name provided for the resource. To customize this value, a --table option may be provided.

For example:

mix scribe.gen.domain Accounts User users --table cms_users

Binary ID by Default

By default the generated migration uses binary_id for schema's primary key and its references. No toggle is provided to use numeric IDs, because they are the first vulnerability listed in OWASP API TOP 10 2023.

Security Implications of using numeric IDs

Numeric IDs allows an attacker to easily enumerate all resources by incrementing or decrementing the id by one, when proper access controls mechanisms aren't working as expected or may even be absent. This is the top one vulnerability in OWASP API TOP 10 2023, named as BOLA (Broken Object Level Authorization). Don't assume this will never happen in your code base.

Default options

This generator uses default options provided in the :generators configuration of your application in the same way phx.gen.html does, except for binary-id, which is ignored and always set to true by scribe.gen.domain. This is a design choice as per previous section on the use of Binary ID by Default.

Read the documentation for phx.gen.context and phx.gen.schema for more information on attributes.