@griffin-app/griffin-ts 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.
Files changed (89) hide show
  1. package/ASSERTIONS_QUICK_REF.md +161 -0
  2. package/README.md +297 -0
  3. package/dist/assertions.d.ts +136 -0
  4. package/dist/assertions.d.ts.map +1 -0
  5. package/dist/assertions.js +230 -0
  6. package/dist/assertions.js.map +1 -0
  7. package/dist/builder.d.ts +168 -0
  8. package/dist/builder.d.ts.map +1 -0
  9. package/dist/builder.js +165 -0
  10. package/dist/builder.js.map +1 -0
  11. package/dist/constants.d.ts +5 -0
  12. package/dist/constants.d.ts.map +1 -0
  13. package/dist/constants.js +6 -0
  14. package/dist/constants.js.map +1 -0
  15. package/dist/example-sequential.d.ts +11 -0
  16. package/dist/example-sequential.d.ts.map +1 -0
  17. package/dist/example-sequential.js +160 -0
  18. package/dist/example-sequential.js.map +1 -0
  19. package/dist/example.d.ts +9 -0
  20. package/dist/example.d.ts.map +1 -0
  21. package/dist/example.js +36 -0
  22. package/dist/example.js.map +1 -0
  23. package/dist/frequency.d.ts +15 -0
  24. package/dist/frequency.d.ts.map +1 -0
  25. package/dist/frequency.js +34 -0
  26. package/dist/frequency.js.map +1 -0
  27. package/dist/http-methods.d.ts +6 -0
  28. package/dist/http-methods.d.ts.map +1 -0
  29. package/dist/http-methods.js +9 -0
  30. package/dist/http-methods.js.map +1 -0
  31. package/dist/index.d.ts +24 -0
  32. package/dist/index.d.ts.map +1 -0
  33. package/dist/index.js +62 -0
  34. package/dist/index.js.map +1 -0
  35. package/dist/response-formats.d.ts +4 -0
  36. package/dist/response-formats.d.ts.map +1 -0
  37. package/dist/response-formats.js +7 -0
  38. package/dist/response-formats.js.map +1 -0
  39. package/dist/schema-exports.d.ts +6 -0
  40. package/dist/schema-exports.d.ts.map +1 -0
  41. package/dist/schema-exports.js +43 -0
  42. package/dist/schema-exports.js.map +1 -0
  43. package/dist/schema.d.ts +311 -0
  44. package/dist/schema.d.ts.map +1 -0
  45. package/dist/schema.js +214 -0
  46. package/dist/schema.js.map +1 -0
  47. package/dist/secrets.d.ts +56 -0
  48. package/dist/secrets.d.ts.map +1 -0
  49. package/dist/secrets.js +74 -0
  50. package/dist/secrets.js.map +1 -0
  51. package/dist/sequential-builder.d.ts +127 -0
  52. package/dist/sequential-builder.d.ts.map +1 -0
  53. package/dist/sequential-builder.js +128 -0
  54. package/dist/sequential-builder.js.map +1 -0
  55. package/dist/shared.d.ts +8 -0
  56. package/dist/shared.d.ts.map +1 -0
  57. package/dist/shared.js +36 -0
  58. package/dist/shared.js.map +1 -0
  59. package/dist/target.d.ts +33 -0
  60. package/dist/target.d.ts.map +1 -0
  61. package/dist/target.js +53 -0
  62. package/dist/target.js.map +1 -0
  63. package/dist/type-exports.d.ts +6 -0
  64. package/dist/type-exports.d.ts.map +1 -0
  65. package/dist/type-exports.js +7 -0
  66. package/dist/type-exports.js.map +1 -0
  67. package/dist/wait.d.ts +9 -0
  68. package/dist/wait.d.ts.map +1 -0
  69. package/dist/wait.js +8 -0
  70. package/dist/wait.js.map +1 -0
  71. package/package.json +43 -0
  72. package/src/assertions.ts +327 -0
  73. package/src/builder.ts +336 -0
  74. package/src/constants.ts +5 -0
  75. package/src/example-sequential.ts +191 -0
  76. package/src/example.ts +55 -0
  77. package/src/frequency.ts +38 -0
  78. package/src/http-methods.ts +5 -0
  79. package/src/index.ts +70 -0
  80. package/src/response-formats.ts +3 -0
  81. package/src/schema-exports.ts +43 -0
  82. package/src/schema.ts +289 -0
  83. package/src/secrets.ts +112 -0
  84. package/src/sequential-builder.ts +254 -0
  85. package/src/shared.ts +46 -0
  86. package/src/target.ts +55 -0
  87. package/src/type-exports.ts +20 -0
  88. package/src/wait.ts +4 -0
  89. package/tsconfig.json +20 -0
package/src/secrets.ts ADDED
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Secret reference types and utilities for griffin DSL.
3
+ *
4
+ * Secrets are referenced by provider and path, and resolved at runtime
5
+ * by the plan executor using configured secret providers.
6
+ */
7
+
8
+ export interface SecretRefData {
9
+ provider: string;
10
+ ref: string;
11
+ version?: string;
12
+ field?: string;
13
+ }
14
+
15
+ export interface SecretRef {
16
+ $secret: SecretRefData;
17
+ }
18
+
19
+ export interface SecretOptions {
20
+ /** Pin to a specific version (provider-dependent) */
21
+ version?: string;
22
+ /** Extract a specific field from a JSON secret */
23
+ field?: string;
24
+ }
25
+
26
+ /**
27
+ * Create a secret reference for use in endpoint headers or body.
28
+ *
29
+ * @param path - Provider-qualified path in format "provider:path"
30
+ * Examples:
31
+ * - "env:API_KEY" - Environment variable
32
+ * - "aws:prod/api-key" - AWS Secrets Manager
33
+ * - "vault:secret/data/api" - HashiCorp Vault
34
+ * - "doppler:backend/prod/API_KEY" - Doppler
35
+ *
36
+ * @param options - Optional version pinning or field extraction
37
+ *
38
+ * @example
39
+ * ```typescript
40
+ * builder.addEndpoint("call", {
41
+ * method: GET,
42
+ * path: "/api/data",
43
+ * headers: {
44
+ * "Authorization": secret("aws:prod/api-key"),
45
+ * "X-API-Key": secret("env:LOCAL_API_KEY"),
46
+ * },
47
+ * })
48
+ * ```
49
+ */
50
+ export function secret(path: string, options?: SecretOptions): SecretRef {
51
+ const colonIndex = path.indexOf(":");
52
+
53
+ if (colonIndex === -1) {
54
+ throw new Error(
55
+ `Secret path must include provider: "provider:path" (e.g., "aws:my-secret", "env:API_KEY"). Got: "${path}"`,
56
+ );
57
+ }
58
+
59
+ if (colonIndex === 0) {
60
+ throw new Error(
61
+ `Secret path must have a provider name before the colon. Got: "${path}"`,
62
+ );
63
+ }
64
+
65
+ if (colonIndex === path.length - 1) {
66
+ throw new Error(
67
+ `Secret path must have a reference after the colon. Got: "${path}"`,
68
+ );
69
+ }
70
+
71
+ const provider = path.slice(0, colonIndex);
72
+ const ref = path.slice(colonIndex + 1);
73
+
74
+ return {
75
+ $secret: {
76
+ provider,
77
+ ref,
78
+ ...(options?.version !== undefined && { version: options.version }),
79
+ ...(options?.field !== undefined && { field: options.field }),
80
+ },
81
+ };
82
+ }
83
+
84
+ /**
85
+ * Type guard to check if a value is a secret reference.
86
+ */
87
+ export function isSecretRef(value: unknown): value is SecretRef {
88
+ if (typeof value !== "object" || value === null) {
89
+ return false;
90
+ }
91
+
92
+ const obj = value as Record<string, unknown>;
93
+ if (
94
+ !("$secret" in obj) ||
95
+ typeof obj.$secret !== "object" ||
96
+ obj.$secret === null
97
+ ) {
98
+ return false;
99
+ }
100
+
101
+ const secretData = obj.$secret as Record<string, unknown>;
102
+ return (
103
+ typeof secretData.provider === "string" &&
104
+ typeof secretData.ref === "string"
105
+ );
106
+ }
107
+
108
+ /**
109
+ * Type that allows either a literal value or a secret reference.
110
+ * Used for headers and other fields that may contain secrets.
111
+ */
112
+ export type SecretOrValue<T> = T | SecretRef;
@@ -0,0 +1,254 @@
1
+ import {
2
+ Endpoint,
3
+ Wait,
4
+ Assertion,
5
+ type EndpointConfig,
6
+ type WaitDuration,
7
+ } from "./builder";
8
+ import { START, END } from "./constants";
9
+ import { TEST_PLAN_VERSION, Edge, Node, Frequency, TestPlanV1 } from "./schema";
10
+ import {
11
+ createStateProxy,
12
+ type SerializedAssertion,
13
+ type StateProxy,
14
+ } from "./assertions";
15
+
16
+ type RawPlan = Omit<TestPlanV1, "id" | "environment">;
17
+
18
+ /**
19
+ * Callback type for building assertions with type-safe state access
20
+ */
21
+ export type AssertionCallback<NodeNames extends string> = (
22
+ state: StateProxy<NodeNames>,
23
+ ) => SerializedAssertion[];
24
+
25
+ /**
26
+ * SequentialTestBuilder provides a simplified DSL for creating linear test flows.
27
+ * Edges are automatically created based on the order nodes are added.
28
+ * No manual edge management required - just add steps in sequence.
29
+ *
30
+ * @template NodeNames - Union of all registered node names for type-safe state access
31
+ */
32
+ export interface SequentialTestBuilder<NodeNames extends string = never> {
33
+ /**
34
+ * Adds an endpoint request to the sequence.
35
+ *
36
+ * @param name - Unique name for this node
37
+ * @param config - Endpoint configuration
38
+ * @returns Updated builder with node name registered
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * builder.request("create_user", {
43
+ * method: POST,
44
+ * path: "/api/v1/users",
45
+ * response_format: Json,
46
+ * body: { name: "Test User" }
47
+ * })
48
+ * ```
49
+ */
50
+ request<Name extends string>(
51
+ name: Name,
52
+ config: EndpointConfig,
53
+ ): SequentialTestBuilder<NodeNames | Name>;
54
+
55
+ /**
56
+ * Adds a wait/delay to the sequence.
57
+ *
58
+ * @param name - Unique name for this node
59
+ * @param duration - How long to wait
60
+ * @returns Updated builder with node name registered
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * builder.wait("pause", Wait.seconds(5))
65
+ * // or with raw milliseconds:
66
+ * builder.wait("pause", 5000)
67
+ * ```
68
+ */
69
+ wait<Name extends string>(
70
+ name: Name,
71
+ duration: WaitDuration,
72
+ ): SequentialTestBuilder<NodeNames | Name>;
73
+
74
+ /**
75
+ * Adds assertions to the sequence using a callback with type-safe state access.
76
+ *
77
+ * The callback receives a state proxy that provides autocomplete for all registered
78
+ * node names and allows building rich JsonPath-based assertions.
79
+ *
80
+ * @param callback - Function that receives state proxy and returns assertions
81
+ * @returns Updated builder (no new node name registered - uses auto-generated name)
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * builder.assert((state) => [
86
+ * Assert(state["create_user"].status).equals(201),
87
+ * Assert(state["create_user"].body["data"]["id"]).not.isNull(),
88
+ * Assert(state["create_user"].headers["content-type"]).contains("application/json"),
89
+ * ])
90
+ * ```
91
+ */
92
+ assert(
93
+ callback: AssertionCallback<NodeNames>,
94
+ ): SequentialTestBuilder<NodeNames>;
95
+
96
+ /**
97
+ * Builds the final test plan.
98
+ * Automatically connects all nodes in sequence: START → node1 → node2 → ... → END
99
+ *
100
+ * @returns The completed TestPlan
101
+ */
102
+ build(): RawPlan;
103
+ }
104
+
105
+ /**
106
+ * Internal implementation class for SequentialTestBuilder.
107
+ */
108
+ class SequentialTestBuilderImpl<
109
+ NodeNames extends string = never,
110
+ > implements SequentialTestBuilder<NodeNames> {
111
+ private nodes: Node[] = [];
112
+ private nodeNames: string[] = [];
113
+ private nodeCounter = 0;
114
+
115
+ constructor(
116
+ private config: {
117
+ name: string;
118
+ frequency?: Frequency;
119
+ locations?: string[];
120
+ },
121
+ ) {}
122
+
123
+ /**
124
+ * Generates a unique auto-generated node name
125
+ */
126
+ private generateNodeName(): string {
127
+ return `step_${this.nodeCounter++}`;
128
+ }
129
+
130
+ request<Name extends string>(
131
+ name: Name,
132
+ config: EndpointConfig,
133
+ ): SequentialTestBuilder<NodeNames | Name> {
134
+ const node = Endpoint(config);
135
+ this.nodes.push({ ...node, id: name } as Node);
136
+ this.nodeNames.push(name);
137
+ return this as unknown as SequentialTestBuilder<NodeNames | Name>;
138
+ }
139
+
140
+ wait<Name extends string>(
141
+ name: Name,
142
+ duration: WaitDuration,
143
+ ): SequentialTestBuilder<NodeNames | Name> {
144
+ const node = Wait(duration);
145
+ this.nodes.push({ ...node, id: name } as Node);
146
+ this.nodeNames.push(name);
147
+ return this as unknown as SequentialTestBuilder<NodeNames | Name>;
148
+ }
149
+
150
+ assert(
151
+ callback: AssertionCallback<NodeNames>,
152
+ ): SequentialTestBuilder<NodeNames> {
153
+ const stateProxy = createStateProxy(this.nodeNames as NodeNames[]);
154
+ const serializedAssertions = callback(stateProxy);
155
+
156
+ const nodeName = this.generateNodeName();
157
+ const node = Assertion(serializedAssertions);
158
+ this.nodes.push({ ...node, id: nodeName } as Node);
159
+ this.nodeNames.push(nodeName);
160
+ return this as unknown as SequentialTestBuilder<NodeNames>;
161
+ }
162
+
163
+ build(): RawPlan {
164
+ const { name, frequency, locations } = this.config;
165
+ const edges: Edge[] = [];
166
+
167
+ // If no nodes, return empty plan with just START->END
168
+ if (this.nodes.length === 0) {
169
+ return {
170
+ name,
171
+ frequency,
172
+ locations,
173
+ version: TEST_PLAN_VERSION,
174
+ nodes: [],
175
+ edges: [{ from: START, to: END }],
176
+ };
177
+ }
178
+
179
+ // Connect START to first node
180
+ edges.push({ from: START, to: this.nodes[0].id });
181
+
182
+ // Connect consecutive nodes
183
+ for (let i = 0; i < this.nodes.length - 1; i++) {
184
+ edges.push({
185
+ from: this.nodes[i].id,
186
+ to: this.nodes[i + 1].id,
187
+ });
188
+ }
189
+
190
+ // Connect last node to END
191
+ edges.push({ from: this.nodes[this.nodes.length - 1].id, to: END });
192
+
193
+ return {
194
+ name,
195
+ version: TEST_PLAN_VERSION,
196
+ frequency,
197
+ locations,
198
+ nodes: this.nodes,
199
+ edges,
200
+ };
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Creates a sequential test builder for simple linear test flows.
206
+ *
207
+ * Unlike createGraphBuilder, this builder automatically manages edges
208
+ * based on the order in which nodes are added. Perfect for straightforward
209
+ * sequential tests where you don't need complex branching logic.
210
+ *
211
+ * @param config - Test configuration
212
+ * @param config.name - Name of the test
213
+ * @param config.frequency - Optional frequency for scheduled execution
214
+ * @param config.locations - Optional array of location identifiers where this test should run
215
+ * @returns A new SequentialTestBuilder instance
216
+ *
217
+ * @example
218
+ * ```typescript
219
+ * import { createTestBuilder, POST, GET, Json, Frequency, Assert } from "griffin-ts";
220
+ *
221
+ * const plan = createTestBuilder({
222
+ * name: "create-and-verify-user",
223
+ * frequency: Frequency.every(5).minute(),
224
+ * locations: ["us-east-1", "eu-west-1"]
225
+ * })
226
+ * .request("create_user", {
227
+ * method: POST,
228
+ * path: "/api/v1/users",
229
+ * response_format: Json,
230
+ * body: { name: "Test User", email: "test@example.com" }
231
+ * })
232
+ * .assert((state) => [
233
+ * Assert(state["create_user"].status).equals(201),
234
+ * Assert(state["create_user"].body["data"]["id"]).not.isNull(),
235
+ * ])
236
+ * .request("get_user", {
237
+ * method: GET,
238
+ * path: "/api/v1/users/123",
239
+ * response_format: Json
240
+ * })
241
+ * .assert((state) => [
242
+ * Assert(state["get_user"].status).equals(200),
243
+ * Assert(state["get_user"].body["data"]["name"]).equals("Test User"),
244
+ * ])
245
+ * .build();
246
+ * ```
247
+ */
248
+ export function createTestBuilder(config: {
249
+ name: string;
250
+ frequency?: Frequency;
251
+ locations?: string[];
252
+ }): SequentialTestBuilder {
253
+ return new SequentialTestBuilderImpl(config);
254
+ }
package/src/shared.ts ADDED
@@ -0,0 +1,46 @@
1
+ import {
2
+ TSchema,
3
+ TSchemaOptions,
4
+ TUnsafe,
5
+ Static,
6
+ Unsafe,
7
+ Type,
8
+ } from "typebox";
9
+ import { Value } from "typebox/value";
10
+ import { Memory } from "typebox/system";
11
+
12
+ export function Ref<T extends TSchema>(
13
+ t: T,
14
+ options: TSchemaOptions = {},
15
+ ): TUnsafe<Static<T>> {
16
+ const id = (t as unknown as Record<string, string | undefined>).$id;
17
+ if (!id) {
18
+ throw new Error("missing ID on schema");
19
+ }
20
+ return Unsafe<Static<T>>({ ...t, $ref: id, $id: undefined, ...options });
21
+ }
22
+
23
+ export class TUnionOneOf<Types extends TSchema[] = TSchema[]> extends Type.Base<
24
+ TSchema[]
25
+ > {
26
+ public oneOf: Types;
27
+ constructor(oneOf: Types) {
28
+ super();
29
+ this.oneOf = oneOf;
30
+ }
31
+ }
32
+ export function UnionOneOf(oneOf: TSchema[]): TUnionOneOf {
33
+ return new TUnionOneOf(oneOf);
34
+ }
35
+
36
+ //export interface TUnionOneOf<Types extends TSchema[] = TSchema[]> extends TSchema {
37
+ // '~kind': 'UnionOneOf'
38
+ // //static: { [K in keyof Types]: Static<Types[K]> }[number]
39
+ // oneOf: Types
40
+ //}
41
+ //
42
+ //export function UnionOneOf<Types extends TSchema[]>(oneOf: [...Types], options: TSchemaOptions = {}) {
43
+ // return { ...options, ["~kind"]: 'UnionOneOf', oneOf } as TUnionOneOf<Types>
44
+ //
45
+ // ///return Memory.Create({ '~kind': 'UnionOneOf' }, { oneOf }, options) as never
46
+ //}
package/src/target.ts ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Target reference utilities for griffin DSL.
3
+ *
4
+ * Targets are resolved at runtime by the runner based on the execution environment.
5
+ * This allows the same test to run against different environments (staging, production, etc.)
6
+ * without changing the test code.
7
+ */
8
+
9
+ import { TargetRef } from "./schema.js";
10
+
11
+ /**
12
+ * Create a target reference for endpoint base URLs.
13
+ *
14
+ * The runner will resolve this to the appropriate base URL based on the
15
+ * execution environment (passed as a per-run parameter).
16
+ *
17
+ * @param key - The target identifier (e.g., "billing-service", "api-gateway")
18
+ * @returns A target reference object
19
+ *
20
+ * @example
21
+ * ```typescript
22
+ * builder.addNode("billing", Endpoint({
23
+ * method: GET,
24
+ * path: "/invoices",
25
+ * base: target("billing-service"),
26
+ * response_format: JSON
27
+ * }));
28
+ * ```
29
+ */
30
+ export function target(key: string): TargetRef {
31
+ if (!key || typeof key !== "string") {
32
+ throw new Error(`Target key must be a non-empty string. Got: ${key}`);
33
+ }
34
+
35
+ if (key.trim() === "") {
36
+ throw new Error("Target key cannot be empty or whitespace only");
37
+ }
38
+
39
+ return {
40
+ type: "target",
41
+ key: key.trim(),
42
+ };
43
+ }
44
+
45
+ /**
46
+ * Type guard to check if a value is a target reference.
47
+ */
48
+ export function isTargetRef(value: unknown): value is TargetRef {
49
+ if (typeof value !== "object" || value === null) {
50
+ return false;
51
+ }
52
+
53
+ const obj = value as Record<string, unknown>;
54
+ return obj.type === "target" && typeof obj.key === "string";
55
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Static types derived from TypeBox schemas.
3
+ * Import from "griffin/types" for TypeScript type checking.
4
+ */
5
+
6
+ export type {
7
+ TargetRef,
8
+ Endpoint,
9
+ Frequency,
10
+ Wait,
11
+ BinaryPredicate,
12
+ JSONAssertion,
13
+ XMLAssertion,
14
+ TextAssertion,
15
+ Assertion,
16
+ Assertions,
17
+ Node,
18
+ Edge,
19
+ TestPlanV1,
20
+ } from "./schema.js";
package/src/wait.ts ADDED
@@ -0,0 +1,4 @@
1
+ export const WaitDuration = {
2
+ minutes: (value: number) => ({ minutes: value }),
3
+ seconds: (value: number) => ({ seconds: value }),
4
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "commonjs",
5
+ "lib": ["ES2022"],
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "declaration": true,
9
+ "declarationMap": true,
10
+ "sourceMap": true,
11
+ "strict": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "resolveJsonModule": true,
16
+ "moduleResolution": "node"
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist"]
20
+ }