Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions .github/copilot-instructions.md → AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copilot Instructions for `counterfact/apis`
# Repository Instructions for `counterfact/apis`

This repository contains Counterfact-based API simulators.

Expand All @@ -21,18 +21,27 @@ Generate and evolve simulator code **one API (or coherent API subset) at a time*
- response status
- response body/headers as applicable
- resulting state changes
placeholder `dummy` test files.

3. **Implement state and business logic in route context files**
- no placeholder `dummy` test files

3. **Use middleware for shared cross-cutting request behavior**
- Put authentication, authorization, and other behavior shared by all routes
in a scope into a `routes/**/_.middleware.ts` file.
- Export a `middleware` function that either returns a response or calls
`respondTo($)` to continue the request chain.
- Keep operation-specific behavior in route handlers; do not duplicate a
uniform authentication check in every handler.
- Follow Counterfact's [middleware pattern](https://github.com/counterfact/api-simulator/blob/main/docs/features/middleware.md), including its path-scoping and chaining semantics.

4. **Implement state and business logic in route context files**
- Put simulator state and business rules in `routes/**/_.context.ts`.
- Do not edit generated `types/_.context.ts` files.
- Keep route handlers thin by delegating behavior to context classes/methods.

4. **Unit test Context classes directly**
5. **Unit test Context classes directly**
- Add direct unit tests for `Context` class behavior in `routes/**/_.context.ts`.
- Cover state transitions and core business logic independently of HTTP tests.

5. **Use scenarios for startup init and REPL setup flows**
6. **Use scenarios for startup init and REPL setup flows**
- Use `startup` to initialize simulator state when the server starts.
- Use other scenario functions for REPL-invoked setup/actions after startup.
- Keep scenarios simple and declarative.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ This repository hosts API simulation packages built with [`counterfact`](https:/

- [`@counterfact/swagger-pet-store`](./swagger-pet-store): simulator package generated from the Swagger Petstore OpenAPI spec.
- [`@counterfact/github`](./github): simulator package generated from GitHub's OpenAPI spec.
- [`@counterfact/ordergroove`](./ordergroove): combined simulator package generated from Ordergroove's Customers, Items, Offers, Orders, Products, and Subscriptions OpenAPI specs.
2 changes: 2 additions & 0 deletions ordergroove/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.cache
node_modules
2 changes: 2 additions & 0 deletions ordergroove/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.cache
openapi
148 changes: 148 additions & 0 deletions ordergroove/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# Ordergroove simulator

This package runs the Customers, Items, Offers, Orders, Products, and
Subscriptions REST APIs together on one Counterfact server. It uses
Counterfact `2.14.2` and serves every operation at its canonical Ordergroove
path, without an API-group prefix.

## Install and start

From this directory, install the locked dependencies and start the HTTP server:

```sh
cd ordergroove
npm ci
npm run serve
```

The server listens at `http://localhost:3100` by default. Use `npm start` instead
when you want Counterfact's full interactive development mode, including its
REPL and file watching. Stop either process with Ctrl-C. Restarting the server
resets all resources to the deterministic startup data below.

## Authentication

All REST operations require the `x-api-key` request header. The simulator's
local test key is:

```text
ordergroove-local-api-key
```

Missing or incorrect keys receive a `401 Unauthorized` response. For example:

```sh
curl \
-H 'x-api-key: ordergroove-local-api-key' \
http://localhost:3100/customers/
```

## Canonical API URLs

All six specifications are mounted on the same origin:

| API | Canonical collection URLs |
| ------------- | --------------------------------------------- |
| Customers | `/customers/` |
| Products | `/products/` |
| Offers | `/offer_profiles/`, `/otd/`, `/entitlements/` |
| Subscriptions | `/subscriptions/` |
| Orders | `/orders/` |
| Items | `/items/` |

Detail and action URLs extend those paths directly. Examples include
`/customers/customer-001/`, `/products/product-001/`,
`/subscriptions/subscription-001/cancel/`, `/orders/order-001/send_now/`, and
`/items/item-001/`. Paths such as `/customers/customers/` or
`/subscriptions/subscriptions/` do not exist.

## Seeded data

Every API has a `startup` scenario. Together they create two coherent commerce
chains:

| Customer | Product | Offer profile | Subscription | Order | Item |
| ----------------------------- | ----------------------------- | ------------------- | ------------------ | ----------- | ---------- |
| `customer-001` (Ada Lovelace) | `product-001` (`sku-coffee`) | `offer-profile-001` | `subscription-001` | `order-001` | `item-001` |
| `customer-002` (Grace Hopper) | `product-002` (`sku-filters`) | `offer-profile-002` | `subscription-002` | `order-002` | `item-002` |

The first subscription is live and monthly; the second is inactive and runs
every two weeks. `order-001` starts unsent, while `order-002` starts successful.
The offer data also includes `discount-001` for `customer-001` and three
entitlements: two for `customer-001` and one for `customer-002`.

State changes persist for the lifetime of the server. Created customers,
one-time discounts, and items can be retrieved or listed afterward; product,
customer, subscription, and order updates are also visible to later requests.

## Example flows

Inspect the seeded resources for the first customer:

```sh
curl -H 'x-api-key: ordergroove-local-api-key' \
'http://localhost:3100/subscriptions/?customer=customer-001'

curl -H 'x-api-key: ordergroove-local-api-key' \
'http://localhost:3100/orders/?customer=customer-001'

curl -H 'x-api-key: ordergroove-local-api-key' \
'http://localhost:3100/items/?subscription=subscription-001'

curl -H 'x-api-key: ordergroove-local-api-key' \
'http://localhost:3100/entitlements/?customer=customer-001'
```

Cancel and reactivate a subscription:

```sh
curl -X POST -H 'x-api-key: ordergroove-local-api-key' \
http://localhost:3100/subscriptions/subscription-001/cancel/

curl -X POST -H 'x-api-key: ordergroove-local-api-key' \
http://localhost:3100/subscriptions/subscription-001/reactivate/
```

Send the seeded unsent order immediately, then retrieve its persisted pending
state:

```sh
curl -X POST -H 'x-api-key: ordergroove-local-api-key' \
http://localhost:3100/orders/order-001/send_now/

curl -H 'x-api-key: ordergroove-local-api-key' \
http://localhost:3100/orders/order-001/
```

Create a one-time item linked to the first seeded chain:

```sh
curl -X POST \
-H 'x-api-key: ordergroove-local-api-key' \
-H 'content-type: application/json' \
--data '{"order_id":"order-001","subscription_id":"subscription-001","product_id":"product-001","quantity":1,"price":"19.99","total_price":"19.99","offer_id":"offer-profile-001","one_time":true}' \
http://localhost:3100/items/
```

Unknown detail IDs return realistic `404` responses. Collection filters include
customer and status for orders; subscription and order for items; customer,
product, live state, and documented creation dates for subscriptions; and
customer for entitlements.

## Contracts and scope

`openapi/upstream/` contains the six unchanged published REST contracts. The
multi-spec `counterfact.yaml` consumes them directly with an empty prefix for
each API; there are no normalized contract copies or duplicated group paths.

Ordergroove's Early Access GraphQL API is explicitly out of scope because no
public schema or confirmed endpoint is available.

## Validation

Run the complete package checks from `ordergroove/`:

```sh
npm test
npm run lint
```
20 changes: 20 additions & 0 deletions ordergroove/counterfact.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
spec:
- source: ./openapi/upstream/customers.yml
group: customers
prefix: ""
- source: ./openapi/upstream/items.yml
group: items
prefix: ""
- source: ./openapi/upstream/offers.yml
group: offers
prefix: ""
- source: ./openapi/upstream/orders.yml
group: orders
prefix: ""
- source: ./openapi/upstream/products.yml
group: products
prefix: ""
- source: ./openapi/upstream/subscriptions.yml
group: subscriptions
prefix: ""
destination: .
1 change: 1 addition & 0 deletions ordergroove/customers/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.cache
14 changes: 14 additions & 0 deletions ordergroove/customers/counterfact-types/cookie-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Options for setting an HTTP cookie on a response.
* These correspond to standard `Set-Cookie` attributes and are passed to the
* `.cookie()` method on the response builder.
*/
export interface CookieOptions {
domain?: string;
expires?: Date;
httpOnly?: boolean;
maxAge?: number;
path?: string;
sameSite?: "lax" | "none" | "strict";
secure?: boolean;
}
15 changes: 15 additions & 0 deletions ordergroove/customers/counterfact-types/counterfact-response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* A unique symbol used as a brand for the `COUNTERFACT_RESPONSE` type.
* This prevents arbitrary objects from being accidentally treated as a
* completed response value.
*/
const counterfactResponse = Symbol("Counterfact Response");

/**
* The terminal value type returned by the fluent response builder once all
* required fields (body, headers, etc.) have been provided. When a route
* handler returns this type, Counterfact treats the response as complete.
*/
export type COUNTERFACT_RESPONSE = {
[counterfactResponse]: typeof counterfactResponse;
};
13 changes: 13 additions & 0 deletions ordergroove/customers/counterfact-types/example-names.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { OpenApiResponse } from "./open-api-response.js";

/**
* Extracts the union of named example keys defined on an OpenAPI response.
* Resolves to `never` when the response has no named examples.
* Used to constrain the argument to the `.example(name)` method on the
* response builder.
*/
export type ExampleNames<Response extends OpenApiResponse> = Response extends {
examples: infer E;
}
? keyof E & string
: never;
14 changes: 14 additions & 0 deletions ordergroove/customers/counterfact-types/example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Represents a named example defined in an OpenAPI document.
* Examples can be referenced by route handlers via the `.example(name)` method
* on the response builder.
*
* OpenAPI 3.2 adds `dataValue` as a structured alternative to `value`.
* When present, `dataValue` is preferred over `value`.
*/
export interface Example {
dataValue?: unknown;
description: string;
summary: string;
value?: unknown;
}
Loading
Loading