DDD.Identity (vicander v0.1.1)

Copy Markdown View Source

Identities of entities and aggregates, and generating them.

The identity string

An entity's identity is made of one or more fields, declared with one identity/2 each:

identity(:tenant_id, :uuid)
identity(:sku, :string)

Entity.identity(entity) returns it as a string: the value itself for a single field, the values joined with : for several ("6f1f...:A-100"). A \ or : inside a part is escaped (\\, \:) so the string always splits back into the same parts (decode/1). The string is what find_by_id/1, the outbox aggregate_id and a {:ref, Aggregate} to a composite aggregate use.

Entity.build_identity(parts) makes the same string from a map or keyword list of the parts, without an entity. Entity.identity_fields() lists the part names.

A composite identity part must be a :string, :uuid, :integer, :atom, :date or enum field. Every part is required, and none may change once the entity exists.

Generating ids

new/1 fills in a UUIDv7 only for a single identity(:id, :uuid) that the caller left out. For any other identity the caller passes every part. generate/0 and the functions below make ids by hand:

Summary

Functions

Build the identity string for module from a map or keyword list holding its parts. Raises ArgumentError when a part is missing or nil.

Split a composite identity string back into its parts (the inverse of encode/1), also when a part itself contains a :.

Join identity parts into the identity string. One part is returned as text unchanged; several are escaped and joined with :.

A new id: a UUIDv7.

The identity string of an entity instance, for naming it in an error, or nil when it has none or can't be told (a part is missing or has no text form). Never raises.

The value a repository stores an entity under: the field value for a single-field identity, the identity string for a composite one. nil for a struct without identity.

Fills in a generated :id when the identity is a lone identity(:id, :uuid) and the caller left it out. Any other identity is returned untouched. A function rather than inline code in the generated new/1, so a value object (no identity) does not get a clause that can never match.

A UUIDv1 (time and node based).

A name-based UUIDv3 (MD5). namespace is :dns, :url, :oid, :x500 or a UUID.

A random UUIDv4.

A name-based UUIDv5 (SHA-1). namespace is :dns, :url, :oid, :x500 or a UUID.

A UUIDv7: a 48-bit millisecond timestamp followed by random bits.

Functions

build(module, parts)

@spec build(module(), map() | keyword()) :: String.t()

Build the identity string for module from a map or keyword list holding its parts. Raises ArgumentError when a part is missing or nil.

decode(string)

@spec decode(String.t()) :: [String.t()]

Split a composite identity string back into its parts (the inverse of encode/1), also when a part itself contains a :.

iex> DDD.Identity.decode("t1:sku-9")
["t1", "sku-9"]
iex> ["a:b", "c"] |> DDD.Identity.encode() |> DDD.Identity.decode()
["a:b", "c"]

encode(parts)

@spec encode([term()]) :: String.t()

Join identity parts into the identity string. One part is returned as text unchanged; several are escaped and joined with :.

iex> DDD.Identity.encode(["order-7"])
"order-7"
iex> DDD.Identity.encode(["t1", "sku-9"])
"t1:sku-9"

generate()

@spec generate() :: String.t()

A new id: a UUIDv7.

id_of(struct)

@spec id_of(term()) :: String.t() | nil

The identity string of an entity instance, for naming it in an error, or nil when it has none or can't be told (a part is missing or has no text form). Never raises.

key(struct)

@spec key(struct()) :: term()

The value a repository stores an entity under: the field value for a single-field identity, the identity string for a composite one. nil for a struct without identity.

put_generated(attrs, arg2)

@spec put_generated(map(), [{atom(), atom(), keyword()}]) :: map()

Fills in a generated :id when the identity is a lone identity(:id, :uuid) and the caller left it out. Any other identity is returned untouched. A function rather than inline code in the generated new/1, so a value object (no identity) does not get a clause that can never match.

uuid1()

@spec uuid1() :: String.t()

A UUIDv1 (time and node based).

uuid3(namespace, name)

@spec uuid3(atom() | String.t(), String.t()) :: String.t()

A name-based UUIDv3 (MD5). namespace is :dns, :url, :oid, :x500 or a UUID.

uuid4()

@spec uuid4() :: String.t()

A random UUIDv4.

uuid5(namespace, name)

@spec uuid5(atom() | String.t(), String.t()) :: String.t()

A name-based UUIDv5 (SHA-1). namespace is :dns, :url, :oid, :x500 or a UUID.

uuid7()

@spec uuid7() :: String.t()

A UUIDv7: a 48-bit millisecond timestamp followed by random bits.

Ids made in different milliseconds sort in creation order. Ids made within the same millisecond are unique but not ordered.