@auto-engineer/narrative 1.88.0 → 1.90.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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +6 -6
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +92 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/parse-graphql-request.d.ts +17 -0
- package/dist/src/parse-graphql-request.d.ts.map +1 -0
- package/dist/src/parse-graphql-request.js +77 -0
- package/dist/src/parse-graphql-request.js.map +1 -0
- package/dist/src/validate-slice-requests.d.ts +9 -0
- package/dist/src/validate-slice-requests.d.ts.map +1 -0
- package/dist/src/validate-slice-requests.js +235 -0
- package/dist/src/validate-slice-requests.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/ketchup-plan.md +13 -0
- package/package.json +4 -4
- package/src/index.ts +4 -1
- package/src/parse-graphql-request.specs.ts +52 -0
- package/src/parse-graphql-request.ts +99 -0
- package/src/validate-slice-requests.specs.ts +850 -0
- package/src/validate-slice-requests.ts +298 -0
package/ketchup-plan.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Ketchup Plan: Validate Slice Requests Against Model Messages
|
|
2
|
+
|
|
3
|
+
## TODO
|
|
4
|
+
|
|
5
|
+
(none)
|
|
6
|
+
|
|
7
|
+
## DONE
|
|
8
|
+
|
|
9
|
+
- [x] Burst 1: Skeleton + parse safety + export convertJsonAstToSdl (4b1d0b12)
|
|
10
|
+
- [x] Burst 2: Mutation validation (wrong op type, missing input, type mismatch, message not found) (4059d393)
|
|
11
|
+
- [x] Burst 3: Query — operation type + state + top-level fields (73c8c99c)
|
|
12
|
+
- [x] Burst 4: Query — nested field validation (inline objects, referenced messages, unresolvable) (39d3d083)
|
|
13
|
+
- [x] Burst 5: Export from index.ts + integration test (885a805f)
|
package/package.json
CHANGED
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
"typescript": "^5.9.2",
|
|
28
28
|
"zod": "^3.22.4",
|
|
29
29
|
"zod-to-json-schema": "^3.22.3",
|
|
30
|
-
"@auto-engineer/file-store": "1.
|
|
31
|
-
"@auto-engineer/id": "1.
|
|
32
|
-
"@auto-engineer/message-bus": "1.
|
|
30
|
+
"@auto-engineer/file-store": "1.90.0",
|
|
31
|
+
"@auto-engineer/id": "1.90.0",
|
|
32
|
+
"@auto-engineer/message-bus": "1.90.0"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
|
35
35
|
"@types/node": "^20.0.0",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"publishConfig": {
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
|
-
"version": "1.
|
|
42
|
+
"version": "1.90.0",
|
|
43
43
|
"scripts": {
|
|
44
44
|
"build": "tsx scripts/build.ts",
|
|
45
45
|
"test": "vitest run --reporter=dot",
|
package/src/index.ts
CHANGED
|
@@ -53,9 +53,12 @@ export {
|
|
|
53
53
|
specs,
|
|
54
54
|
thenError,
|
|
55
55
|
} from './narrative';
|
|
56
|
+
export type { ParsedArg, ParsedGraphQlOperation } from './parse-graphql-request';
|
|
57
|
+
export { parseGraphQlRequest, parseSliceRequest } from './parse-graphql-request';
|
|
56
58
|
export * from './schema';
|
|
57
59
|
export { createNarrativeSpec, given as testGiven, when as testWhen } from './testing';
|
|
58
60
|
export type { GeneratedNarratives } from './transformers/model-to-narrative';
|
|
59
61
|
export { modelToNarrative } from './transformers/model-to-narrative';
|
|
60
|
-
|
|
61
62
|
export { detectQueryAction, extractQueryNameFromRequest } from './transformers/narrative-to-model/spec-processors';
|
|
63
|
+
export type { SliceRequestValidationError } from './validate-slice-requests';
|
|
64
|
+
export { validateSliceRequests } from './validate-slice-requests';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { parseGraphQlRequest, parseSliceRequest } from './parse-graphql-request';
|
|
3
|
+
|
|
4
|
+
describe('parseGraphQlRequest', () => {
|
|
5
|
+
it('parses a query operation', () => {
|
|
6
|
+
const request = `query GetUsers($status: String) {
|
|
7
|
+
users(status: $status) {
|
|
8
|
+
id
|
|
9
|
+
name
|
|
10
|
+
}
|
|
11
|
+
}`;
|
|
12
|
+
|
|
13
|
+
expect(parseGraphQlRequest(request)).toEqual({
|
|
14
|
+
operationName: 'users',
|
|
15
|
+
args: [{ name: 'status', tsType: 'string', graphqlType: 'String', nullable: true }],
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('parses a mutation operation', () => {
|
|
20
|
+
const request = `mutation SubmitAnswer($input: SubmitAnswerInput!) {
|
|
21
|
+
submitQuestionnaireAnswer(input: $input) {
|
|
22
|
+
success
|
|
23
|
+
}
|
|
24
|
+
}`;
|
|
25
|
+
|
|
26
|
+
expect(parseGraphQlRequest(request)).toEqual({
|
|
27
|
+
operationName: 'submitQuestionnaireAnswer',
|
|
28
|
+
args: [{ name: 'input', tsType: 'SubmitAnswerInput', graphqlType: 'SubmitAnswerInput', nullable: false }],
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('parseSliceRequest', () => {
|
|
34
|
+
it('returns parsed operation when request is present', () => {
|
|
35
|
+
const slice = {
|
|
36
|
+
request: `query GetItems { items { id } }`,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
expect(parseSliceRequest(slice)).toEqual({
|
|
40
|
+
operationName: 'items',
|
|
41
|
+
args: [],
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('returns undefined when request is absent', () => {
|
|
46
|
+
expect(parseSliceRequest({})).toEqual(undefined);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('returns undefined when request is undefined', () => {
|
|
50
|
+
expect(parseSliceRequest({ request: undefined })).toEqual(undefined);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { type OperationDefinitionNode, parse, print, type TypeNode } from 'graphql';
|
|
2
|
+
|
|
3
|
+
export interface ParsedArg {
|
|
4
|
+
name: string;
|
|
5
|
+
tsType: string;
|
|
6
|
+
graphqlType: string;
|
|
7
|
+
nullable: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ParsedGraphQlOperation {
|
|
11
|
+
operationName: string;
|
|
12
|
+
args: ParsedArg[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getTypeName(typeNode: TypeNode): { graphqlType: string; nullable: boolean } {
|
|
16
|
+
if (typeNode.kind === 'NamedType') {
|
|
17
|
+
return { graphqlType: typeNode.name.value, nullable: true };
|
|
18
|
+
} else if (typeNode.kind === 'NonNullType') {
|
|
19
|
+
const inner = getTypeName(typeNode.type);
|
|
20
|
+
return { ...inner, nullable: false };
|
|
21
|
+
} else {
|
|
22
|
+
return getTypeName(typeNode.type);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function graphqlToTs(type: string): string {
|
|
27
|
+
switch (type) {
|
|
28
|
+
case 'String':
|
|
29
|
+
return 'string';
|
|
30
|
+
case 'Int':
|
|
31
|
+
case 'Float':
|
|
32
|
+
case 'Number':
|
|
33
|
+
return 'number';
|
|
34
|
+
case 'Boolean':
|
|
35
|
+
return 'boolean';
|
|
36
|
+
case 'Date':
|
|
37
|
+
return 'Date';
|
|
38
|
+
default:
|
|
39
|
+
return type;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function convertJsonAstToSdl(request: string): string {
|
|
44
|
+
if (request.startsWith('{') && request.includes('"kind"')) {
|
|
45
|
+
try {
|
|
46
|
+
const ast = JSON.parse(request) as unknown;
|
|
47
|
+
if (typeof ast === 'object' && ast !== null && 'kind' in ast && ast.kind === 'Document') {
|
|
48
|
+
return print(ast as Parameters<typeof print>[0]);
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
// If parsing fails, assume it's already a GraphQL string
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return request;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function parseGraphQlRequest(request: string): ParsedGraphQlOperation {
|
|
58
|
+
const sdlRequest = convertJsonAstToSdl(request);
|
|
59
|
+
|
|
60
|
+
const ast = parse(sdlRequest);
|
|
61
|
+
const op = ast.definitions.find(
|
|
62
|
+
(d): d is OperationDefinitionNode =>
|
|
63
|
+
d.kind === 'OperationDefinition' && (d.operation === 'query' || d.operation === 'mutation'),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
if (!op) throw new Error('No query or mutation operation found');
|
|
67
|
+
|
|
68
|
+
const operationName = op.name?.value;
|
|
69
|
+
if (operationName == null) throw new Error('Operation must have a name');
|
|
70
|
+
|
|
71
|
+
const args: ParsedArg[] = (op.variableDefinitions ?? []).map((def) => {
|
|
72
|
+
const varName = def.variable.name.value;
|
|
73
|
+
const { graphqlType, nullable } = getTypeName(def.type);
|
|
74
|
+
return {
|
|
75
|
+
name: varName,
|
|
76
|
+
graphqlType,
|
|
77
|
+
tsType: graphqlToTs(graphqlType),
|
|
78
|
+
nullable,
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const field = op.selectionSet.selections[0];
|
|
83
|
+
if (field?.kind !== 'Field' || !field.name.value) {
|
|
84
|
+
throw new Error('Selection must be a field');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
operationName: field.name.value,
|
|
89
|
+
args,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function parseSliceRequest(slice: {
|
|
94
|
+
request?: string;
|
|
95
|
+
[key: string]: unknown;
|
|
96
|
+
}): ParsedGraphQlOperation | undefined {
|
|
97
|
+
if (slice.request == null) return undefined;
|
|
98
|
+
return parseGraphQlRequest(slice.request);
|
|
99
|
+
}
|