npm package: @griffin-app/griffin-core
Description: TypeScript DSL for defining griffin API tests
Latest version: 0.3.5

# Griffin Core

**@griffin-app/griffin-core** is the TypeScript DSL for defining Griffin API tests. Tests are written in TypeScript and produce JSON test monitors that can be executed by the monitor executor.

## Features

- TypeScript DSL for defining API checks
- Chainable API for building test monitors
- Two builder styles: sequential (linear) and graph (branching/parallel)
- Support for HTTP requests, waits, assertions, and edges
- **Request configs can reference previous responses** – use a callback for `.request()` and pass `state["node"].body["path"]` or `state["node"].headers["name"]` into path, base, headers, or body
- **String templates** – use the `template` tagged template literal to build path or base from variables, secrets, and node refs in one expression
- Outputs JSON test monitors for execution
- Optional notifications, locations, secrets, and variables

## Installation

```bash
npm install @griffin-app/griffin-core
```

Build from source:

```bash
npm install
npm run build
```

## Usage

Create test files in `__griffin__` directories. They export JSON test monitors.

Griffin Core provides two builder APIs:

- **`createMonitorBuilder`**: Sequential tests (recommended for most use cases). Steps are connected in order; no manual edges.
- **`createGraphBuilder`**: Full control over the test graph for parallel execution and branching.

### Sequential Builder (Recommended for Simple Tests)

The sequential builder connects steps in order. You pass `name` and `frequency` (and optional `locations`, `notifications`) into `createMonitorBuilder`. Use `.request()` for HTTP calls, `.wait()` for delays, and `.assert()` for assertions. For `.request()` you can pass either a static config object or a callback `(state) => config` to use data from previous node responses (see [Referencing previous responses in requests](#referencing-previous-responses-in-requests)).

```typescript
import {
  GET,
  createMonitorBuilder,
  Json,
  Frequency,
  Assert,
  variable,
} from "@griffin-app/griffin-core";

const monitor = createMonitorBuilder({
  name: "health-check",
  frequency: Frequency.every(1).minute(),
})
  .request("health", {
    method: GET,
    response_format: Json,
    path: "/health",
    base: variable("api-service"), // or base: "https://api.example.com"
  })
  .assert((state) => [
    Assert(state["health"].status).equals(200),
    Assert(state["health"].body["status"]).equals("ok"),
  ])
  .build();

export default monitor;
```

#### Sequential Example with Waits and Assertions

```typescript
import {
  GET,
  POST,
  createMonitorBuilder,
  Json,
  Frequency,
  WaitDuration,
  Assert,
  variable,
} from "@griffin-app/griffin-core";

const monitor = createMonitorBuilder({
  name: "create-and-verify-user",
  frequency: Frequency.every(5).minute(),
})
  .request("create_user", {
    method: POST,
    response_format: Json,
    path: "/api/v1/users",
    base: variable("api-service"),
    body: { name: "Test User", email: "test@example.com" },
  })
  .assert((state) => [
    Assert(state["create_user"].status).equals(201),
    Assert(state["create_user"].body["data"]["id"]).not.isNull(),
  ])
  .wait("pause", WaitDuration.seconds(2))
  .request("get_user", {
    method: GET,
    response_format: Json,
    path: "/api/v1/users/test@example.com",
    base: variable("api-service"),
  })
  .assert((state) => [
    Assert(state["get_user"].status).equals(200),
    Assert(state["get_user"].body["data"]["name"]).equals("Test User"),
    Assert(state["get_user"].latency).lessThan(500),
  ])
  .build();

export default monitor;
```

### Referencing previous responses in requests

You can use the **body** or **headers** of a previous node’s response when building a later request. Pass a **callback** to `.request()` instead of a config object. The callback receives the same `state` proxy as `.assert()`; use `state["nodeName"].body["path"]` or `state["nodeName"].headers["headerName"]` in `path`, `base`, `headers`, or `body`. Only body and headers can be referenced (not status or latency).

Example: create an order, then confirm it using the order ID from the first response.

```typescript
import {
  POST,
  createMonitorBuilder,
  Json,
  Frequency,
  Assert,
  variable,
} from "@griffin-app/griffin-core";

const monitor = createMonitorBuilder({
  name: "order-workflow",
  frequency: Frequency.every(30).minute(),
})
  .request("create_order", {
    method: POST,
    base: variable("api-service"),
    path: "/api/v1/orders",
    response_format: Json,
    body: { items: [{ product_id: "ABC", quantity: 2 }] },
  })
  .assert((state) => [
    Assert(state["create_order"].status).equals(201),
    Assert(state["create_order"].body["order_id"]).isDefined(),
  ])
  .request("confirm_order", (state) => ({
    method: POST,
    base: variable("api-service"),
    path: "/api/v1/orders/confirm",
    response_format: Json,
    body: {
      order_id: state["create_order"].body["order_id"],
    },
  }))
  .assert((state) => [Assert(state["confirm_order"].status).equals(200)])
  .build();

export default monitor;
```

The callback form of `.request()` uses the same state proxy as `.assert()`. References like `state["create_order"].body["order_id"]` are serialized as `$nodeRef` in the monitor and resolved at execution time by the executor.

### Graph Builder (For Complex Workflows)

The graph builder uses `addNode` and `addEdge`. Nodes are created with `HttpRequest(config)`, `Wait(duration)`, and `Assertion(assertions)`. Frequency is set in the builder config.

```typescript
import {
  GET,
  POST,
  createGraphBuilder,
  HttpRequest,
  Wait,
  Json,
  START,
  END,
  Frequency,
  WaitDuration,
  variable,
} from "@griffin-app/griffin-core";

const monitor = createGraphBuilder({
  name: "foo-bar-check",
  frequency: Frequency.every(1).minute(),
})
  .addNode(
    "create_foo",
    HttpRequest({
      method: POST,
      response_format: Json,
      path: "/api/v1/foo",
      base: variable("api-service"),
      body: { name: "test", value: 42 },
    }),
  )
  .addNode("wait_between", Wait(WaitDuration.seconds(2)))
  .addNode(
    "get_foo",
    HttpRequest({
      method: GET,
      response_format: Json,
      path: "/api/v1/foo/1",
      base: variable("api-service"),
    }),
  )
  .addEdge(START, "create_foo")
  .addEdge("create_foo", "wait_between")
  .addEdge("wait_between", "get_foo")
  .addEdge("get_foo", END)
  .build();

export default monitor;
```

For assertion nodes in the graph builder, use `Assertion(assertions)` where `assertions` is an array built with `Assert(...)` and a state proxy (see `createStateProxy` in the assertions API). The sequential builder’s `.assert(callback)` is usually easier for assertion-heavy flows.

### Using Secrets

Secrets are referenced in request config (e.g. headers or body) and resolved at runtime by the executor’s secret providers.

#### With Sequential Builder

```typescript
import {
  GET,
  createMonitorBuilder,
  Json,
  Frequency,
  secret,
  Assert,
  variable,
} from "@griffin-app/griffin-core";

const monitor = createMonitorBuilder({
  name: "authenticated-check",
  frequency: Frequency.every(5).minute(),
})
  .request("protected", {
    method: GET,
    response_format: Json,
    path: "/api/protected",
    base: variable("api-service"),
    headers: {
      "X-API-Key": secret("API_KEY"),
      Authorization: secret("API_TOKEN"),
    },
  })
  .assert((state) => [Assert(state["protected"].status).equals(200)])
  .build();

export default monitor;
```

#### With Graph Builder

```typescript
import {
  GET,
  createGraphBuilder,
  HttpRequest,
  START,
  END,
  Json,
  Frequency,
  secret,
  Assertion,
  createStateProxy,
  Assert,
  variable,
} from "@griffin-app/griffin-core";

const stateProxy = createStateProxy(["authenticated_request"]);
const monitor = createGraphBuilder({
  name: "authenticated-check",
  frequency: Frequency.every(5).minute(),
})
  .addNode(
    "authenticated_request",
    HttpRequest({
      method: GET,
      response_format: Json,
      path: "/api/protected",
      base: variable("api-service"),
      headers: {
        "X-API-Key": secret("API_KEY"),
        Authorization: secret("API_TOKEN"),
      },
    }),
  )
  .addNode(
    "verify",
    Assertion([Assert(stateProxy["authenticated_request"].status).equals(200)]),
  )
  .addEdge(START, "authenticated_request")
  .addEdge("authenticated_request", "verify")
  .addEdge("verify", END)
  .build();

export default monitor;
```

#### Secret API

- **`secret(ref)`** – Reference by name (letters, numbers, underscore). Resolution (e.g. env vars, AWS Secrets Manager) is configured in the executor.

### Variables

Use `variable("key")` for values (e.g. base URL) that are supplied per environment at runtime:

```typescript
base: variable("api-service");
```

For a single variable as a string, use `variable("key")` directly. To build a string from multiple variables, secrets, or node refs, use the **template** tagged template literal (see below).

### Templates

Use the **`template`** tagged template literal when `path` or `base` (or any string field that accepts refs) must combine multiple variables, secrets, or node refs into one value. Each interpolation must be a `variable()`, `secret()`, or a state proxy reference (e.g. `state["node"].body["id"]`).

```typescript
import {
  GET,
  createMonitorBuilder,
  Json,
  Frequency,
  Assert,
  variable,
  secret,
  template,
} from "@griffin-app/griffin-core";

const monitor = createMonitorBuilder({
  name: "versioned-health",
  frequency: Frequency.every(1).minute(),
})
  .request("health", {
    method: GET,
    response_format: Json,
    path: template`/api/${variable("api-version")}/health`,
    base: template`https://${variable("env")}.api.example.com`,
    headers: {
      "X-API-Key": secret("API_KEY"),
    },
  })
  .assert((state) => [Assert(state["health"].status).equals(200)])
  .build();
```

Mixed variable and secret:

```typescript
path: template`/api/${variable("env")}/data?key=${secret("API_KEY")}`;
```

In a request callback you can combine static parts with state refs:

```typescript
.request("get_order", (state) => ({
  method: GET,
  response_format: Json,
  base: variable("api-service"),
  path: template`/api/v1/orders/${state["create_order"].body["order_id"]}`,
}))
```

Only `variable()`, `secret()`, and state proxy references are allowed inside `template`; plain strings or numbers will throw at build time.

### API Reference

#### Sequential Builder

- **`createMonitorBuilder(config)`** – `config`: `{ name: string; frequency: Frequency; locations?: string[]; notifications?: MonitorNotification[] }`
- **`.request(name, config)`** – Add an HTTP request. `config` is either a static object or a callback `(state) => config`. Use the callback to reference previous responses: `state["nodeName"].body["path"]` or `state["nodeName"].headers["headerName"]` in `path`, `base`, `headers`, or `body` (body and headers only; status and latency cannot be referenced).
- **`.wait(name, duration)`** – Add a wait. `duration`: `WaitDuration.seconds(n)`, `WaitDuration.minutes(n)`, or a number (milliseconds).
- **`.assert(callback)`** – Add assertions. `callback(state)` returns an array of values from `Assert(...)`.
- **`.build()`** – Return the final `MonitorDSL`.

#### Graph Builder

- **`createGraphBuilder(config)`** – Same config shape as sequential (name, frequency, optional locations, notifications).
- **`.addNode(name, node)`** – Add a node. `node` is from `HttpRequest(config)`, `Wait(duration)`, or `Assertion(assertions)`.
- **`.addEdge(from, to)`** – Connect nodes. Use `START` and `END` for entry and exit.
- **`.build()`** – Return the final `MonitorDSL` (validates that all nodes are connected).

#### Request Config (HttpRequest / .request)

- **`method`** – `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, etc.
- **`path`** – Path string, `variable("key")`, `template`... (see [Templates](#templates)), or (in a request callback) a reference like `state["node"].body["id"]` or `state["node"].headers["x-id"]`.
- **`base`** – Base URL: string, `variable("key")`, `template`..., or a state reference in a callback.
- **`response_format`** – `Json`, `Xml` (from `@griffin-app/griffin-core`).
- **`headers`** – Optional record; values may use `secret("...")` or, in a callback, state references.
- **`body`** – Optional body; may use `secret("...")` or, in a callback, state references (e.g. `state["create_order"].body["order_id"]`).

When you use a callback for `.request()`, any value that comes from the state proxy (e.g. `state["node"].body["key"]`) is serialized as a `$nodeRef` and resolved at execution time from the previous node’s response. Only **body** and **headers** of previous nodes can be referenced; **status** and **latency** cannot.

#### Wait Duration

- **`WaitDuration.seconds(n)`** / **`WaitDuration.minutes(n)`** – From `@griffin-app/griffin-core`.
- **`Wait(ms)`** – Raw milliseconds, e.g. `Wait(2000)`.

#### Assertions

Use `Assert()` with the state proxy in `.assert(callback)` or when building an array for `Assertion(...)`:

```typescript
.assert((state) => [
  Assert(state["node"].status).equals(200),
  Assert(state["node"].latency).lessThan(500),
  Assert(state["node"].body["id"]).not.isNull(),
  Assert(state["node"].body["name"]).equals("test"),
  Assert(state["node"].headers["content-type"]).contains("json"),
])
```

See [ASSERTIONS_QUICK_REF.md](./ASSERTIONS_QUICK_REF.md) for the full assertion API.

#### General

- **Frequency** – `Frequency.every(n).minute()`, `.minutes()`, `.hour()`, `.hours()`, `.day()`, `.days()`.
- **Response formats** – `Json`, `Xml` (exported as constants).
- **Schema / types** – Import from `@griffin-app/griffin-core/schema` and `@griffin-app/griffin-core/types` as needed.

## Output

Calling `.build()` returns a `MonitorDSL` object. Serialize it to JSON for the executor. Request nodes may contain `$nodeRef` in `path`, `base`, `headers`, or `body` when you use the callback form of `.request()`; the executor resolves these from previous responses at run time. Example shape:

```json
{
  "name": "health-check",
  "version": "1.0",
  "frequency": {
    "every": 1,
    "unit": "MINUTE"
  },
  "nodes": [
    {
      "id": "health",
      "type": "HTTP_REQUEST",
      "method": "GET",
      "path": { "$literal": "/health" },
      "base": { "$variable": { "key": "api-service" } },
      "response_format": "JSON"
    }
  ],
  "edges": [
    { "from": "__START__", "to": "health" },
    { "from": "health", "to": "__END__" }
  ]
}
```