Skip to content

Command ​

Commands represent intention to perform business operation. It targets a specific audience. It can be an application service and request with intention to “add user” or “change the order status to confirmed”. So the sender of the command must know the recipient and expects the request to be executed. Of course, the recipient may refuse to do it by not passing us the salt or throwing an exception during the request handling.

Command type helps to keep the command definition aligned. It's not a must, but it helps to ensure that it has a type name defined (e.g. AddProductItemToShoppingCart) and read-only payload data.

Usage ​

ts
import type { Command } from '@event-driven-io/emmett';

type AddProductItemToShoppingCart = Command<
  'AddProductItemToShoppingCart',
  {
    shoppingCartId: string;
    productItem: PricedProductItem;
  }
>;

The type is a simple wrapper to ensure the structure's correctness. It defines:

  • type - command type name,
  • data - represents the business data the command contains. It has to be a record structure; primitives are not allowed,
  • metadata - represents the generic data command contains. It can represent telemetry, user id, tenant id, timestamps and other information that can be useful for running infrastructure. It has to be a record structure; primitives are not allowed.

Definition ​

ts
import type { DefaultRecord } from './';

export type Command<
  CommandType extends string = string,
  CommandData extends DefaultRecord = DefaultRecord,
  CommandMetaData extends DefaultRecord | undefined = undefined,
> = Readonly<
  CommandMetaData extends undefined
    ? {
        type: CommandType;
        data: Readonly<CommandData>;
        metadata?: DefaultCommandMetadata | undefined;
      }
    : {
        type: CommandType;
        data: CommandData;
        metadata: CommandMetaData;
      }
> & { readonly kind?: 'Command' };

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyCommand = Command<any, any, any>;

export type CommandTypeOf<T extends Command> = T['type'];
export type CommandDataOf<T extends Command> = T['data'];
export type CommandMetaDataOf<T extends Command> = T extends {
  metadata: infer M;
}
  ? M
  : undefined;

export type CreateCommandType<
  CommandType extends string,
  CommandData extends DefaultRecord,
  CommandMetaData extends DefaultRecord | undefined = undefined,
> = Readonly<
  CommandMetaData extends undefined
    ? {
        type: CommandType;
        data: CommandData;
        metadata?: DefaultCommandMetadata | undefined;
      }
    : {
        type: CommandType;
        data: CommandData;
        metadata: CommandMetaData;
      }
> & { readonly kind?: 'Command' };

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const command = <CommandType extends Command<string, any, any>>(
  ...args: CommandMetaDataOf<CommandType> extends undefined
    ? [
        type: CommandTypeOf<CommandType>,
        data: CommandDataOf<CommandType>,
        metadata?: DefaultCommandMetadata | undefined,
      ]
    : [
        type: CommandTypeOf<CommandType>,
        data: CommandDataOf<CommandType>,
        metadata: CommandMetaDataOf<CommandType>,
      ]
): CommandType => {
  const [type, data, metadata] = args;

  return metadata !== undefined
    ? ({ type, data, metadata, kind: 'Command' } as CommandType)
    : ({ type, data, kind: 'Command' } as CommandType);
};

export type DefaultCommandMetadata = { now: Date };

See also ​

See more context in getting started guide