Skip to content

Node.js Cody Tutorial Exercise III

Alexander Miertsch edited this page Feb 14, 2021 · 3 revisions

Exercise II introduced Card Metadata and how it can be used to provide additional information to a Cody Hook.

One last basic concept is missing to complete the picture. On an InspectIO event map you model message flows, behavior, business processes. Different objects interact with each other by exchanging messages like commands and events. A Cody Hook can make use of this information to assist developers by generating all required glue code if not a fully working implementation (depends on complexity).

Task

We start again by looking at the failing test case for exercise III:

docker-compose run --rm exercises npm run exercise3

Ok, this time we're asked to generate a command handling function named addBuilding in an aggregate directory Aggregate/Building. The function should handle AddBuilding command from previous exercises.

Aggregate Hook

In Exercise I you've learned how to create a Command Hook and register it in codyconfig.ts. Do the same for an onAggregateHook and let the hook generate a directory exercises/src/Aggregate/${aggregateName}.

Hint: You can use the util function mkdirIfNotExistsSync to generate the directory. Make sure to check if the util function returns a Cody-Error-Response!

If you think the hook is ready, switch to InspectIO, add an aggregate card with label Building on the event map and trigger Cody.

Did it work? You can verify the result by executing:

docker-compose run --rm exercises npm run exercise3 again.

Test case is still failing but the error has changed. src/Aggregate/Building/addBuilding.ts file is missing.

Event Map Connections

Go back to the event map and draw an arrow from AddBuilding command to Building aggregate.

The connection we've just drawn can be read in a hook. Here is the Node interface passed to a hook:

interface Node {
  getId: () => NodeId;
  getName: () => NodeName;
  getType: () => NodeType;
  getLink: () => NodeLink;
  getTags: () => List<NodeTag>;
  isLayer: () => boolean;
  isDefaultLayer: () => boolean;
  getParent: () => Node | null;
  getChildren: () => List<Node>;
  getGeometry: () => GraphPoint;
  getSources: () => List<Node>;
  getTargets: () => List<Node>;
  getMetadata: () => string | null;
}

We already worked with getName() and getMetadata(). From a Node you can follow connections using getSources() and getTargets(). In our case the AddBuilding command node has the Building aggregate referenced as a target and from Building point of view AddBuilding command is a source.

One important thing to note here is that a hook gets only access to directly connected nodes, even if those nodes have further connections! This avoids circular reference problems and keeps payload exchanged between InspectIO and Cody small.

Node interface also provides information about getParent() and getChildren() which are useful when grouping cards in a frame

Enough theory! Let's see it in action. Update your onAggregateHook with this version:

import {CodyHook} from "../board/code";
import {Context} from "./Context";
import {Node, NodeType} from "../board/graph";
import {CodyResponse, isCodyError} from "../general/response";
import {nodeNameToCamelCase, nodeNameToPascalCase} from "../utils/string";
import {mkdirIfNotExistsSync, writeFileSync} from "../utils/filesystem";
import {getSingleSource} from "../utils/node-traversing";

export const onAggregateHook: CodyHook<Context> = async (aggregate: Node, ctx: Context): Promise<CodyResponse> => {
    const aggregateName = nodeNameToPascalCase(aggregate);
    const aggregateDir = ctx.srcFolder + `/Aggregate/${aggregateName}`;
    let successDetails = 'Checklist\n\n';

    const dirErrResponse = mkdirIfNotExistsSync(aggregateDir);

    if(isCodyError(dirErrResponse)) {
        return dirErrResponse;
    }

    successDetails = successDetails + `✔️ Aggregate directory ${aggregateDir} exists\n`;

    const command = getSingleSource(aggregate, NodeType.command);

    if(isCodyError(command)) {
        return command;
    }

    const commandName = nodeNameToPascalCase(command);
    const commandFunction = nodeNameToCamelCase(command);
    const functionFile = `${commandFunction}.ts`;
    const content = `import {${commandName}} from "../../Command/${commandName}";

export function ${commandFunction}(command: ${commandName}): void {}
`;

    const writeFileErr = writeFileSync(aggregateDir + `/${functionFile}`, content);

    if(isCodyError(writeFileErr)) {
        return writeFileErr;
    }

    successDetails = successDetails + `✔️ Command handler ${aggregateDir}/${functionFile} created\n`;

    return {
        cody: `${aggregateName} Aggregate can now handle ${commandName} commands`,
        details: ['%c'+successDetails, 'color: #73dd8e;font-weight: bold'],
    }
}

The implementation should be self explanatory. The util function getSingleSource is used to ensure that an aggregate card should only have one command as a source. If more than one is found a Cody error is returned. This way you can harden your hooks and validate event maps according to your own rules.

Trigger Cody with the Building aggregate card in InspectIO and run exercise III once again:

docker-compose run --rm exercises npm run exercise3

Recap Exercise III

This exercise introduced the last basic building block: Connections between cards. Depending on arrow direction on InspectIO a connected card appears either in the sources or targets collection of the Node passed to a hook. It's important to keep in mind that a hook only has access to directly connected cards.

Next - Exercise IV