@griffin-app/griffin-core
v0.3.5
Published
TypeScript DSL for defining griffin API tests
Downloads
127
Readme
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 passstate["node"].body["path"]orstate["node"].headers["name"]into path, base, headers, or body - String templates – use the
templatetagged 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
npm install @griffin-app/griffin-coreBuild from source:
npm install
npm run buildUsage
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).
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
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: "[email protected]" },
})
.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/[email protected]",
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.
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.
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
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
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:
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"]).
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:
path: template`/api/${variable("env")}/data?key=${secret("API_KEY")}`;In a request callback you can combine static parts with state refs:
.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.configis either a static object or a callback(state) => config. Use the callback to reference previous responses:state["nodeName"].body["path"]orstate["nodeName"].headers["headerName"]inpath,base,headers, orbody(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 fromAssert(...)..build()– Return the finalMonitorDSL.
Graph Builder
createGraphBuilder(config)– Same config shape as sequential (name, frequency, optional locations, notifications)..addNode(name, node)– Add a node.nodeis fromHttpRequest(config),Wait(duration), orAssertion(assertions)..addEdge(from, to)– Connect nodes. UseSTARTandENDfor entry and exit..build()– Return the finalMonitorDSL(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), or (in a request callback) a reference likestate["node"].body["id"]orstate["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 usesecret("...")or, in a callback, state references.body– Optional body; may usesecret("...")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(...):
.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 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/schemaand@griffin-app/griffin-core/typesas 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:
{
"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__" }
]
}