@crvouga/mockingbird-service-genebygene 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,175 @@
1
+ # @crvouga/mockingbird-service-genebygene
2
+
3
+ Stateful, in-process mock of the [GeneByGene Nucleus API](https://api.genebygene.com/swagger/index.html)
4
+ for test suites. It covers the OAuth client-credentials token endpoint, the product catalog, and
5
+ order create/read: all 4 operations of the vendored OpenAPI subset, each verified by live parity
6
+ against GeneByGene staging.
7
+
8
+ Use it when your backend orders kits from GeneByGene and you want tests to run offline, with no
9
+ staging credentials.
10
+
11
+ - Operation coverage: [SUPPORT.md](https://github.com/crvouga/mockingbird/blob/main/packages/service/genebygene/SUPPORT.md)
12
+ - Developer guide (PDF): https://api.genebygene.com/assets/GxG%20API%20Services%20Developer%20Guide%202022.pdf
13
+ - Swagger UI: https://api.genebygene.com/swagger/index.html
14
+ - Real staging hosts: API `https://staging-api.genebygene.com`, auth
15
+ `https://staging-auth.genebygene.com/connect/token`
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install -D @crvouga/mockingbird-service-genebygene
21
+ ```
22
+
23
+ ESM only. Requires Node >= 22 or Bun >= 1.2. No native dependencies: state lives in an in-memory
24
+ SQLite engine (`@crvouga/mockingbird-service-sqlite`, pure TypeScript). To serve it over HTTP also
25
+ install `@crvouga/mockingbird-adapter-node` or `@crvouga/mockingbird-adapter-bun`.
26
+
27
+ ## Usage
28
+
29
+ Routes and behaviour (all from the source):
30
+
31
+ | Route | Behaviour |
32
+ | --- | --- |
33
+ | `POST /connect/token` | Form (or JSON) body with `grant_type=client_credentials`, `client_id`, `client_secret`. Any non-empty id/secret is accepted; returns `{ access_token, token_type: "Bearer", expires_in: 3600 }`. The token is deterministic per id/secret pair. Wrong grant type: 400 `unsupported_grant_type`; missing id/secret: 400 `invalid_client`. |
34
+ | `GET /api/v2/products` | Requires `Authorization: Bearer <anything>`. Returns an array; a seed product `product_default` ("Mockingbird Default Kit") always exists. |
35
+ | `POST /api/v2/orders` | Bearer required. JSON `{ productId, quantity }` (`quantity` a positive integer, `productId` must exist, else 400). Returns `{ orderId: "order_...", status: "Pending", quantity, productId, createdAt }`. |
36
+ | `GET /api/v2/orders/{orderId}` | Bearer required. The stored order, or 404 `{ message: "order not found" }`. |
37
+
38
+ - **Any host works.** Routing uses only the path, so the auth host and the API host of the real
39
+ service both map onto one mock instance: point both your token URL and API base URL at it.
40
+ - **The bearer token is not validated.** Any `Authorization: Bearer <value>` passes; a missing or
41
+ non-Bearer header returns 401 `{ error: "unauthorized" }`.
42
+ - Orders stay `Pending`; there are no status transitions.
43
+
44
+ ### In-process (inject `fetch`)
45
+
46
+ ```ts
47
+ import { GeneByGeneAPI } from "@crvouga/mockingbird-service-genebygene"
48
+
49
+ const gbg = new GeneByGeneAPI({ now: () => Date.UTC(2026, 0, 1) })
50
+
51
+ const tokenResponse = await gbg.fetch(
52
+ new Request("https://staging-auth.genebygene.com/connect/token", {
53
+ method: "POST",
54
+ headers: { "content-type": "application/x-www-form-urlencoded" },
55
+ body: new URLSearchParams({
56
+ grant_type: "client_credentials",
57
+ client_id: "demo",
58
+ client_secret: "demo",
59
+ }),
60
+ }),
61
+ )
62
+ const { access_token } = (await tokenResponse.json()) as { access_token: string }
63
+ const auth = { authorization: `Bearer ${access_token}` }
64
+
65
+ const products = (await (
66
+ await gbg.fetch(new Request("https://staging-api.genebygene.com/api/v2/products", { headers: auth }))
67
+ ).json()) as { productId: string }[]
68
+
69
+ const orderResponse = await gbg.fetch(
70
+ new Request("https://staging-api.genebygene.com/api/v2/orders", {
71
+ method: "POST",
72
+ headers: { ...auth, "content-type": "application/json" },
73
+ body: JSON.stringify({ productId: products[0]?.productId, quantity: 1 }),
74
+ }),
75
+ )
76
+ console.log(await orderResponse.json())
77
+ // { orderId: "order_...", status: "Pending", quantity: 1, productId: "product_default", createdAt: "2026-01-01T00:00:00.000Z" }
78
+ ```
79
+
80
+ `now` (milliseconds) drives `createdAt`.
81
+
82
+ ### Over HTTP
83
+
84
+ ```ts
85
+ import { GeneByGeneAPI } from "@crvouga/mockingbird-service-genebygene"
86
+
87
+ const gbg = new GeneByGeneAPI()
88
+ // Equivalent to serve(gbg, { port: 0, hostname: "127.0.0.1" }) from @crvouga/mockingbird-adapter-bun
89
+ const server = Bun.serve({ port: 0, hostname: "127.0.0.1", fetch: (request) => gbg.fetch(request) })
90
+ const baseUrl = `http://127.0.0.1:${server.port}`
91
+
92
+ // Use baseUrl for both the auth host and the API host in your app's config:
93
+ const tokenUrl = `${baseUrl}/connect/token`
94
+ const apiBaseUrl = baseUrl
95
+
96
+ const token = await fetch(tokenUrl, {
97
+ method: "POST",
98
+ body: new URLSearchParams({ grant_type: "client_credentials", client_id: "id", client_secret: "secret" }),
99
+ })
100
+ console.log(token.status, apiBaseUrl) // 200
101
+
102
+ server.stop()
103
+ ```
104
+
105
+ Any Fetch-style server works, since `GeneByGeneAPI` only needs `fetch(request)`. On Node use
106
+ `@crvouga/mockingbird-adapter-node`:
107
+
108
+ ```js
109
+ import { serve } from "@crvouga/mockingbird-adapter-node"
110
+ const server = await serve(gbg, { port: 0, host: "127.0.0.1" })
111
+ const { port } = server.address()
112
+ // ... later: server.close()
113
+ ```
114
+
115
+ ### Resetting between tests
116
+
117
+ `reset()` deletes all orders and products, then re-seeds `product_default`:
118
+
119
+ ```ts
120
+ import { beforeEach, expect, test } from "bun:test"
121
+ import { GeneByGeneAPI } from "@crvouga/mockingbird-service-genebygene"
122
+
123
+ const gbg = new GeneByGeneAPI()
124
+ beforeEach(() => gbg.reset())
125
+
126
+ test("catalog has the seed product", async () => {
127
+ const response = await gbg.fetch(
128
+ new Request("https://staging-api.genebygene.com/api/v2/products", {
129
+ headers: { authorization: "Bearer test" },
130
+ }),
131
+ )
132
+ expect(await response.json()).toEqual([
133
+ {
134
+ productId: "product_default",
135
+ name: "Mockingbird Default Kit",
136
+ description: "Seed product for GeneByGene mock",
137
+ },
138
+ ])
139
+ })
140
+ ```
141
+
142
+ ## API
143
+
144
+ | Export | Description |
145
+ | --- | --- |
146
+ | `GeneByGeneAPI` | Class. `new GeneByGeneAPI(options?)`; implements `FetchAPI`. Members: `fetch(request)`, `reset()`, `app` (Hono app), `sqlite` (`SqliteClient`). |
147
+ | `GENEBYGENE_NAMESPACE` | `"genebygene"` — SQLite namespace holding the mock's state when sharing a `sqlite` client. |
148
+ | `document` | The vendored GeneByGene OpenAPI document (Mockingbird subset) that drives routing. |
149
+ | `operationIds` | Every `operationId` in `document`: `PostConnectToken`, `GetProducts`, `PostOrders`, `GetOrder`. |
150
+ | `supportedOperationIds` | The `operationId`s the mock implements (all four). |
151
+
152
+ Options and types:
153
+
154
+ ```text
155
+ type APIOptions = { // from @crvouga/mockingbird-service
156
+ sqlite?: SqliteClient // share one client across services; default: fresh in-memory DB
157
+ now?: () => number // clock in ms for createdAt; default Date.now
158
+ }
159
+ type OperationId / SupportedOperationId // string unions of operationIds / supportedOperationIds
160
+ ```
161
+
162
+ ## Development
163
+
164
+ For contributors to the mockingbird repo only; the parity script is not shipped in the npm package.
165
+
166
+ ```bash
167
+ bun test # offline property suites
168
+ bun run parity # live parity against GeneByGene staging (credentials from env or OpenBao)
169
+ MOCKINGBIRD_GENEBYGENE_CLIENT_ID=... MOCKINGBIRD_GENEBYGENE_CLIENT_SECRET=... bun run parity
170
+ ```
171
+
172
+ Live parity exercises the token endpoint against staging auth and discovers product ids from the
173
+ real catalog before creating orders (the live catalog differs from the mock's seed product).
174
+
175
+ Part of [mockingbird](https://github.com/crvouga/mockingbird) — agent integration guide: [`@crvouga/mockingbird`](https://github.com/crvouga/mockingbird/tree/main/packages/facade#readme).
@@ -0,0 +1,7 @@
1
+ import type { OpenAPIDocument } from "@crvouga/mockingbird-openapi";
2
+ export declare const document: OpenAPIDocument;
3
+ export type OperationId = "PostConnectToken" | "GetProducts" | "PostOrders" | "GetOrder";
4
+ export type SupportedOperationId = "PostConnectToken" | "GetProducts" | "PostOrders" | "GetOrder";
5
+ export declare const operationIds: readonly ["PostConnectToken", "GetProducts", "PostOrders", "GetOrder"];
6
+ export declare const supportedOperationIds: readonly ["PostConnectToken", "GetProducts", "PostOrders", "GetOrder"];
7
+ //# sourceMappingURL=openapi.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openapi.d.ts","sourceRoot":"","sources":["../../src/generated/openapi.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAA;AAEnE,eAAO,MAAM,QAAQ,EAAE,eAA8tJ,CAAA;AAErvJ,MAAM,MAAM,WAAW,GAAG,kBAAkB,GAAG,aAAa,GAAG,YAAY,GAAG,UAAU,CAAA;AACxF,MAAM,MAAM,oBAAoB,GAAG,kBAAkB,GAAG,aAAa,GAAG,YAAY,GAAG,UAAU,CAAA;AACjG,eAAO,MAAM,YAAY,wEAAsE,CAAA;AAC/F,eAAO,MAAM,qBAAqB,wEAAsE,CAAA"}
@@ -0,0 +1,5 @@
1
+ // Generated by @crvouga/mockingbird-openapi-codegen from openapi.yaml. Do not edit.
2
+ export const document = JSON.parse(`{"openapi":"3.1.0","info":{"title":"GeneByGene API (Mockingbird subset)","description":"Stateful mock subset of the GeneByGene Nucleus API.\\nDeveloper guide: https://api.genebygene.com/assets/GxG%20API%20Services%20Developer%20Guide%202022.pdf\\nSwagger UI: https://api.genebygene.com/swagger/index.html\\nStaging API: https://staging-api.genebygene.com\\nStaging auth: https://staging-auth.genebygene.com/connect/token\\n","version":"2.0","x-mockingbird-upstream":{"docs":"https://api.genebygene.com/assets/GxG%20API%20Services%20Developer%20Guide%202022.pdf","swagger":"https://api.genebygene.com/swagger/index.html","note":"Hand-authored subset from the public developer guide (products + orders + client_credentials token)."}},"servers":[{"url":"https://staging-api.genebygene.com"}],"security":[{"bearerAuth":[]}],"paths":{"/connect/token":{"post":{"operationId":"PostConnectToken","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"requestBody":{"required":true,"content":{"application/x-www-form-urlencoded":{"schema":{"type":"object","required":["grant_type","client_id","client_secret"],"properties":{"grant_type":{"type":"string","enum":["client_credentials"]},"client_id":{"type":"string","minLength":1},"client_secret":{"type":"string","minLength":1}}}}}},"responses":{"200":{"description":"Access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenResponse"}}}},"400":{"description":"Invalid grant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/api/v2/products":{"get":{"operationId":"GetProducts","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Product list","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Product"}}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/api/v2/orders":{"post":{"operationId":"PostOrders","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOrderBody"}}}},"responses":{"200":{"description":"Created order","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Order"}}}},"400":{"description":"Bad Request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}},"/api/v2/orders/{orderId}":{"get":{"operationId":"GetOrder","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"parameters":[{"name":"orderId","in":"path","required":true,"schema":{"type":"string","x-mockingbird-resource-ref":{"type":"order","missing":"order_missing"}}}],"responses":{"200":{"description":"Order","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Order"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}},"404":{"description":"Not Found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorBody"}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"TokenResponse":{"type":"object","required":["access_token","token_type","expires_in"],"properties":{"access_token":{"type":"string","x-mockingbird-volatile":{"kind":"token"}},"token_type":{"type":"string","enum":["Bearer"]},"expires_in":{"type":"integer"}}},"Product":{"type":"object","required":["productId","name"],"properties":{"productId":{"type":"string","x-mockingbird-resource":{"type":"product","identity":true}},"name":{"type":"string"},"description":{"type":["string","null"]}}},"CreateOrderBody":{"type":"object","required":["productId","quantity"],"properties":{"productId":{"type":"string","x-mockingbird-resource-ref":{"type":"product","missing":"product_missing"}},"quantity":{"type":"integer","minimum":1,"maximum":100}}},"Order":{"type":"object","required":["orderId","status","quantity","productId","createdAt"],"properties":{"orderId":{"type":"string","x-mockingbird-resource":{"type":"order","identity":true},"x-mockingbird-volatile":{"kind":"id"}},"status":{"type":"string","enum":["Pending","Submitted","Completed","Cancelled"]},"quantity":{"type":"integer"},"productId":{"type":"string","x-mockingbird-resource-ref":{"type":"product","missing":"product_missing"}},"createdAt":{"type":"string","format":"date-time","x-mockingbird-volatile":{"kind":"timestamp"}}}},"ErrorBody":{"type":"object","properties":{"error":{"type":"string"},"error_description":{"type":"string"},"message":{"type":"string"}}}}}}`);
3
+ export const operationIds = ["PostConnectToken", "GetProducts", "PostOrders", "GetOrder"];
4
+ export const supportedOperationIds = ["PostConnectToken", "GetProducts", "PostOrders", "GetOrder"];
5
+ //# sourceMappingURL=openapi.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openapi.js","sourceRoot":"","sources":["../../src/generated/openapi.ts"],"names":[],"mappings":"AAAA,oFAAoF;AAIpF,MAAM,CAAC,MAAM,QAAQ,GAAoB,IAAI,CAAC,KAAK,CAAC,6qJAA6qJ,CAAoB,CAAA;AAIrvJ,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,kBAAkB,EAAC,aAAa,EAAC,YAAY,EAAC,UAAU,CAAU,CAAA;AAC/F,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,kBAAkB,EAAC,aAAa,EAAC,YAAY,EAAC,UAAU,CAAU,CAAA"}
@@ -0,0 +1,9 @@
1
+ import { type OperationContext } from "@crvouga/mockingbird-service";
2
+ import type { GeneByGeneState } from "./state.js";
3
+ export declare const geneByGeneHandlers: (state: GeneByGeneState) => {
4
+ PostConnectToken: (context: OperationContext) => Promise<Response>;
5
+ GetProducts: (context: OperationContext) => Promise<Response>;
6
+ PostOrders: (context: OperationContext) => Promise<Response>;
7
+ GetOrder: (context: OperationContext) => Promise<Response>;
8
+ };
9
+ //# sourceMappingURL=handlers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handlers.d.ts","sourceRoot":"","sources":["../src/handlers.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,gBAAgB,EAEtB,MAAM,8BAA8B,CAAA;AACrC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AA0CjD,eAAO,MAAM,kBAAkB,GAAI,OAAO,eAAe;gCACrB,gBAAgB;2BAsBrB,gBAAgB;0BAOjB,gBAAgB;wBAyBlB,gBAAgB;CAO1C,CAAA"}
@@ -0,0 +1,98 @@
1
+ import { HttpError, jsonRes, opaqueToken, } from "@crvouga/mockingbird-service";
2
+ const requireBearer = (context) => {
3
+ const auth = context.request.headers.get("authorization");
4
+ if (!auth?.toLowerCase().startsWith("bearer ")) {
5
+ throw new HttpError(401, { error: "unauthorized", message: "Bearer token required" });
6
+ }
7
+ };
8
+ const jsonBody = (context) => {
9
+ if (context.body.kind !== "json" ||
10
+ typeof context.body.value !== "object" ||
11
+ context.body.value === null) {
12
+ throw new HttpError(400, { message: "expected a JSON object body" });
13
+ }
14
+ return context.body.value;
15
+ };
16
+ const formBody = (context) => {
17
+ if (context.body.kind === "form") {
18
+ const out = {};
19
+ for (const [key, value] of Object.entries(context.body.value)) {
20
+ if (typeof value === "string")
21
+ out[key] = value;
22
+ }
23
+ return out;
24
+ }
25
+ if (context.body.kind === "json" &&
26
+ typeof context.body.value === "object" &&
27
+ context.body.value) {
28
+ const out = {};
29
+ for (const [key, value] of Object.entries(context.body.value)) {
30
+ if (typeof value === "string")
31
+ out[key] = value;
32
+ }
33
+ return out;
34
+ }
35
+ throw new HttpError(400, { error: "invalid_request", error_description: "expected form body" });
36
+ };
37
+ export const geneByGeneHandlers = (state) => ({
38
+ PostConnectToken: async (context) => {
39
+ const body = formBody(context);
40
+ if (body.grant_type !== "client_credentials") {
41
+ throw new HttpError(400, {
42
+ error: "unsupported_grant_type",
43
+ error_description: "grant_type must be client_credentials",
44
+ });
45
+ }
46
+ if (!body.client_id || !body.client_secret) {
47
+ throw new HttpError(400, {
48
+ error: "invalid_client",
49
+ error_description: "client_id and client_secret are required",
50
+ });
51
+ }
52
+ const token = opaqueToken(`gbg:${body.client_id}:${body.client_secret}`, 32);
53
+ return jsonRes(200, {
54
+ access_token: token,
55
+ token_type: "Bearer",
56
+ expires_in: 3600,
57
+ });
58
+ },
59
+ GetProducts: async (context) => {
60
+ requireBearer(context);
61
+ state.ensureSeedProducts();
62
+ const products = state.products.list({ order: "oldest" }).map((row) => row.value);
63
+ return jsonRes(200, products);
64
+ },
65
+ PostOrders: async (context) => {
66
+ requireBearer(context);
67
+ const body = jsonBody(context);
68
+ const productId = body.productId;
69
+ const quantity = body.quantity;
70
+ if (typeof productId !== "string")
71
+ throw new HttpError(400, { message: "productId is required" });
72
+ if (typeof quantity !== "number" || !Number.isInteger(quantity) || quantity < 1) {
73
+ throw new HttpError(400, { message: "quantity must be a positive integer" });
74
+ }
75
+ if (!state.products.has(productId)) {
76
+ throw new HttpError(400, { message: `unknown productId ${productId}` });
77
+ }
78
+ const orderId = state.nextOrderId();
79
+ const order = {
80
+ orderId,
81
+ status: "Pending",
82
+ quantity,
83
+ productId,
84
+ createdAt: state.isoNow(context.now),
85
+ };
86
+ state.orders.insert(orderId, order);
87
+ return jsonRes(200, order);
88
+ },
89
+ GetOrder: async (context) => {
90
+ requireBearer(context);
91
+ const id = context.params.orderId ?? "";
92
+ const order = state.orders.get(id);
93
+ if (!order)
94
+ throw new HttpError(404, { message: "order not found" });
95
+ return jsonRes(200, order);
96
+ },
97
+ });
98
+ //# sourceMappingURL=handlers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handlers.js","sourceRoot":"","sources":["../src/handlers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,23 @@
1
+ import type { FetchAPI } from "@crvouga/mockingbird-core";
2
+ import { type APIOptions } from "@crvouga/mockingbird-service";
3
+ import type { SqliteClient } from "@crvouga/mockingbird-sqlite";
4
+ import type { Hono } from "hono";
5
+ export type { OperationId, SupportedOperationId } from "./generated/openapi.js";
6
+ export { document, operationIds, supportedOperationIds } from "./generated/openapi.js";
7
+ export declare const GENEBYGENE_NAMESPACE = "genebygene";
8
+ /**
9
+ * Stateful mock of the GeneByGene Nucleus API (token + products + orders).
10
+ *
11
+ * Developer guide: https://api.genebygene.com/assets/GxG%20API%20Services%20Developer%20Guide%202022.pdf
12
+ * Swagger: https://api.genebygene.com/swagger/index.html
13
+ */
14
+ export declare class GeneByGeneAPI implements FetchAPI {
15
+ readonly app: Hono;
16
+ readonly sqlite: SqliteClient;
17
+ private readonly service;
18
+ private readonly state;
19
+ constructor(options?: APIOptions);
20
+ fetch(request: Request): Promise<Response>;
21
+ reset(): Promise<void>;
22
+ }
23
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,2BAA2B,CAAA;AACzD,OAAO,EACL,KAAK,UAAU,EAOhB,MAAM,8BAA8B,CAAA;AACrC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAA;AAC/D,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,MAAM,CAAA;AAKhC,YAAY,EAAE,WAAW,EAAE,oBAAoB,EAAE,MAAM,wBAAwB,CAAA;AAC/E,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AAEtF,eAAO,MAAM,oBAAoB,eAAe,CAAA;AAEhD;;;;;GAKG;AACH,qBAAa,aAAc,YAAW,QAAQ;IAC5C,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAA;IAClB,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAA;IAC7B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiB;gBAE3B,OAAO,GAAE,UAAe;IAsBpC,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC;IAIpC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAI7B"}
package/dist/index.js ADDED
@@ -0,0 +1,48 @@
1
+ import { bootSqlite, createService, defineOperations, HttpError, jsonRes, } from "@crvouga/mockingbird-service";
2
+ import { document } from "./generated/openapi.js";
3
+ import { geneByGeneHandlers } from "./handlers.js";
4
+ import { GeneByGeneState } from "./state.js";
5
+ export { document, operationIds, supportedOperationIds } from "./generated/openapi.js";
6
+ export const GENEBYGENE_NAMESPACE = "genebygene";
7
+ /**
8
+ * Stateful mock of the GeneByGene Nucleus API (token + products + orders).
9
+ *
10
+ * Developer guide: https://api.genebygene.com/assets/GxG%20API%20Services%20Developer%20Guide%202022.pdf
11
+ * Swagger: https://api.genebygene.com/swagger/index.html
12
+ */
13
+ export class GeneByGeneAPI {
14
+ app;
15
+ sqlite;
16
+ service;
17
+ state;
18
+ constructor(options = {}) {
19
+ const sqlite = bootSqlite(options.sqlite);
20
+ this.state = new GeneByGeneState(sqlite, GENEBYGENE_NAMESPACE);
21
+ const handlers = defineOperations({
22
+ ...geneByGeneHandlers(this.state),
23
+ });
24
+ this.service = createService({
25
+ document,
26
+ handlers,
27
+ sqlite,
28
+ namespace: GENEBYGENE_NAMESPACE,
29
+ now: options.now,
30
+ notFound: () => jsonRes(404, { message: "Not Found" }),
31
+ onError: (error) => {
32
+ if (error instanceof HttpError)
33
+ return error.toResponse();
34
+ throw error;
35
+ },
36
+ });
37
+ this.app = this.service.app;
38
+ this.sqlite = this.service.sqlite;
39
+ }
40
+ fetch(request) {
41
+ return this.service.fetch(request);
42
+ }
43
+ async reset() {
44
+ await this.service.reset();
45
+ this.state.ensureSeedProducts();
46
+ }
47
+ }
48
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,UAAU,EACV,aAAa,EACb,gBAAgB,EAChB,SAAS,EACT,OAAO,GAER,MAAM,8BAA8B,CAAA;AAGrC,OAAO,EAAE,QAAQ,EAA6B,MAAM,wBAAwB,CAAA;AAC5E,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAA;AAClD,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAG5C,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,qBAAqB,EAAE,MAAM,wBAAwB,CAAA;AAEtF,MAAM,CAAC,MAAM,oBAAoB,GAAG,YAAY,CAAA;AAEhD;;;;;GAKG;AACH,MAAM,OAAO,aAAa;IACf,GAAG,CAAM;IACT,MAAM,CAAc;IACZ,OAAO,CAAS;IAChB,KAAK,CAAiB;IAEvC,YAAY,UAAsB,EAAE;QAClC,MAAM,MAAM,GAAG,UAAU,CAAC,OAAO,CAAC,MAAM,CAAC,CAAA;QACzC,IAAI,CAAC,KAAK,GAAG,IAAI,eAAe,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAA;QAC9D,MAAM,QAAQ,GAAG,gBAAgB,CAAuB;YACtD,GAAG,kBAAkB,CAAC,IAAI,CAAC,KAAK,CAAC;SAClC,CAAC,CAAA;QACF,IAAI,CAAC,OAAO,GAAG,aAAa,CAAC;YAC3B,QAAQ;YACR,QAAQ;YACR,MAAM;YACN,SAAS,EAAE,oBAAoB;YAC/B,GAAG,EAAE,OAAO,CAAC,GAAG;YAChB,QAAQ,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC;YACtD,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;gBACjB,IAAI,KAAK,YAAY,SAAS;oBAAE,OAAO,KAAK,CAAC,UAAU,EAAE,CAAA;gBACzD,MAAM,KAAK,CAAA;YACb,CAAC;SACF,CAAC,CAAA;QACF,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAA;QAC3B,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAA;IACnC,CAAC;IAED,KAAK,CAAC,OAAgB;QACpB,OAAO,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;IACpC,CAAC;IAED,KAAK,CAAC,KAAK;QACT,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAA;QAC1B,IAAI,CAAC,KAAK,CAAC,kBAAkB,EAAE,CAAA;IACjC,CAAC;CACF"}
@@ -0,0 +1,25 @@
1
+ import { Collection, IdSequence } from "@crvouga/mockingbird-service";
2
+ import type { SqliteClient } from "@crvouga/mockingbird-sqlite";
3
+ export type ProductRecord = {
4
+ productId: string;
5
+ name: string;
6
+ description: string | null;
7
+ };
8
+ export type OrderRecord = {
9
+ orderId: string;
10
+ status: "Pending" | "Submitted" | "Completed" | "Cancelled";
11
+ quantity: number;
12
+ productId: string;
13
+ createdAt: string;
14
+ };
15
+ export declare class GeneByGeneState {
16
+ readonly products: Collection<ProductRecord>;
17
+ readonly orders: Collection<OrderRecord>;
18
+ readonly ids: IdSequence;
19
+ constructor(sqlite: SqliteClient, namespace: string);
20
+ /** Sandbox always exposes at least one catalog product. */
21
+ ensureSeedProducts(): void;
22
+ nextOrderId(): string;
23
+ isoNow(now: () => number): string;
24
+ }
25
+ //# sourceMappingURL=state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAA;AACrE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAA;AAE/D,MAAM,MAAM,aAAa,GAAG;IAC1B,SAAS,EAAE,MAAM,CAAA;IACjB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,GAAG,IAAI,CAAA;CAC3B,CAAA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,OAAO,EAAE,MAAM,CAAA;IACf,MAAM,EAAE,SAAS,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAA;IAC3D,QAAQ,EAAE,MAAM,CAAA;IAChB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,qBAAa,eAAe;IAC1B,QAAQ,CAAC,QAAQ,EAAE,UAAU,CAAC,aAAa,CAAC,CAAA;IAC5C,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,WAAW,CAAC,CAAA;IACxC,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAA;gBAEZ,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM;IAOnD,2DAA2D;IAC3D,kBAAkB;IASlB,WAAW,IAAI,MAAM;IAIrB,MAAM,CAAC,GAAG,EAAE,MAAM,MAAM,GAAG,MAAM;CAGlC"}
package/dist/state.js ADDED
@@ -0,0 +1,29 @@
1
+ import { Collection, IdSequence } from "@crvouga/mockingbird-service";
2
+ export class GeneByGeneState {
3
+ products;
4
+ orders;
5
+ ids;
6
+ constructor(sqlite, namespace) {
7
+ this.products = new Collection(sqlite, namespace, "products");
8
+ this.orders = new Collection(sqlite, namespace, "orders");
9
+ this.ids = new IdSequence(sqlite, namespace, "genebygene");
10
+ this.ensureSeedProducts();
11
+ }
12
+ /** Sandbox always exposes at least one catalog product. */
13
+ ensureSeedProducts() {
14
+ if (this.products.list().length > 0)
15
+ return;
16
+ this.products.insert("product_default", {
17
+ productId: "product_default",
18
+ name: "Mockingbird Default Kit",
19
+ description: "Seed product for GeneByGene mock",
20
+ });
21
+ }
22
+ nextOrderId() {
23
+ return this.ids.next("order_");
24
+ }
25
+ isoNow(now) {
26
+ return new Date(now()).toISOString();
27
+ }
28
+ }
29
+ //# sourceMappingURL=state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state.js","sourceRoot":"","sources":["../src/state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAA;AAiBrE,MAAM,OAAO,eAAe;IACjB,QAAQ,CAA2B;IACnC,MAAM,CAAyB;IAC/B,GAAG,CAAY;IAExB,YAAY,MAAoB,EAAE,SAAiB;QACjD,IAAI,CAAC,QAAQ,GAAG,IAAI,UAAU,CAAC,MAAM,EAAE,SAAS,EAAE,UAAU,CAAC,CAAA;QAC7D,IAAI,CAAC,MAAM,GAAG,IAAI,UAAU,CAAC,MAAM,EAAE,SAAS,EAAE,QAAQ,CAAC,CAAA;QACzD,IAAI,CAAC,GAAG,GAAG,IAAI,UAAU,CAAC,MAAM,EAAE,SAAS,EAAE,YAAY,CAAC,CAAA;QAC1D,IAAI,CAAC,kBAAkB,EAAE,CAAA;IAC3B,CAAC;IAED,2DAA2D;IAC3D,kBAAkB;QAChB,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC;YAAE,OAAM;QAC3C,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,iBAAiB,EAAE;YACtC,SAAS,EAAE,iBAAiB;YAC5B,IAAI,EAAE,yBAAyB;YAC/B,WAAW,EAAE,kCAAkC;SAChD,CAAC,CAAA;IACJ,CAAC;IAED,WAAW;QACT,OAAO,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IAChC,CAAC;IAED,MAAM,CAAC,GAAiB;QACtB,OAAO,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,WAAW,EAAE,CAAA;IACtC,CAAC;CACF"}
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@crvouga/mockingbird-service-genebygene",
3
+ "version": "0.1.0",
4
+ "description": "Stateful mock of the GeneByGene Nucleus API (token, products, orders) verified by differential property tests against the GeneByGene staging sandbox.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "sideEffects": false,
8
+ "files": [
9
+ "dist",
10
+ "README.md"
11
+ ],
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "publishConfig": {
19
+ "access": "public",
20
+ "provenance": true
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/crvouga/mockingbird.git",
25
+ "directory": "packages/service/genebygene"
26
+ },
27
+ "homepage": "https://github.com/crvouga/mockingbird/tree/main/packages/service/genebygene#readme",
28
+ "keywords": [
29
+ "mockingbird",
30
+ "service",
31
+ "genebygene"
32
+ ],
33
+ "mockingbird": {
34
+ "runtime": "portable",
35
+ "layer": "service"
36
+ },
37
+ "scripts": {
38
+ "generate": "bun ../../openapi/codegen/dist/cli.js",
39
+ "generate:check": "bun ../../openapi/codegen/dist/cli.js --check",
40
+ "openapi:check": "bun ../../openapi/codegen/dist/cli.js --validate",
41
+ "build": "tsc -p tsconfig.build.json",
42
+ "typecheck": "tsc -p tsconfig.json --noEmit",
43
+ "lint": "biome check .",
44
+ "test": "bun test",
45
+ "parity": "bun scripts/parity.ts",
46
+ "portability": "bun ../../../scripts/portability.ts",
47
+ "pack:check": "bun ../../../scripts/pack-check.ts"
48
+ },
49
+ "dependencies": {
50
+ "@crvouga/mockingbird-core": "0.0.0-development",
51
+ "@crvouga/mockingbird-sqlite": "0.0.0-development",
52
+ "@crvouga/mockingbird-openapi": "0.0.0-development",
53
+ "@crvouga/mockingbird-service": "0.0.0-development",
54
+ "@crvouga/mockingbird-http-codec": "0.0.0-development",
55
+ "hono": "4.11.9"
56
+ },
57
+ "devDependencies": {
58
+ "fast-check": "4.9.0",
59
+ "@crvouga/mockingbird-testing": "0.1.0",
60
+ "@crvouga/mockingbird-service-sqlite": "0.0.0-development",
61
+ "@crvouga/mockingbird-parity": "0.0.0-development",
62
+ "@crvouga/mockingbird-openbao": "0.0.0-development",
63
+ "@crvouga/mockingbird-openapi-codegen": "0.1.0",
64
+ "@crvouga/mockingbird-openapi-metadata": "0.0.0-development"
65
+ }
66
+ }