polen 0.11.0-next.22 → 0.11.0-next.24
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/build/api/config/normalized.d.ts +200 -200
- package/build/api/examples/schemas/catalog.d.ts +4 -4
- package/build/api/examples/schemas/type-usage-index.d.ts +5 -5
- package/build/api/examples/schemas/type-usage-index.js +2 -2
- package/build/api/examples/schemas/type-usage-index.js.map +1 -1
- package/build/api/examples/type-usage-indexer.d.ts.map +1 -1
- package/build/api/examples/type-usage-indexer.js +41 -35
- package/build/api/examples/type-usage-indexer.js.map +1 -1
- package/build/api/schema/input-sources/directory.d.ts +1 -1
- package/build/api/schema/input-sources/directory.js +3 -3
- package/build/api/schema/input-sources/directory.js.map +1 -1
- package/build/api/schema/input-sources/file.d.ts +1 -1
- package/build/api/schema/input-sources/file.js +3 -3
- package/build/api/schema/input-sources/file.js.map +1 -1
- package/build/api/schema/input-sources/introspection-file.d.ts +1 -1
- package/build/api/schema/input-sources/introspection.d.ts +1 -1
- package/build/api/schema/input-sources/memory.d.ts +1 -1
- package/build/api/schema/input-sources/memory.js +3 -3
- package/build/api/schema/input-sources/memory.js.map +1 -1
- package/build/api/schema/input-sources/versioned-directory.d.ts +2 -2
- package/build/api/schema/input-sources/versioned-directory.js +3 -3
- package/build/api/schema/input-sources/versioned-directory.js.map +1 -1
- package/build/cli/commands/open.js +1 -1
- package/build/cli/commands/open.js.map +1 -1
- package/build/lib/catalog/catalog.d.ts +30 -30
- package/build/lib/catalog/unversioned.d.ts +8 -8
- package/build/lib/catalog/versioned.d.ts +16 -16
- package/build/lib/change/change.d.ts +6 -6
- package/build/lib/grafaid/$$.d.ts +2 -0
- package/build/lib/grafaid/$$.d.ts.map +1 -1
- package/build/lib/grafaid/$$.js +2 -0
- package/build/lib/grafaid/$$.js.map +1 -1
- package/build/lib/grafaid/parse-error.d.ts +46 -0
- package/build/lib/grafaid/parse-error.d.ts.map +1 -0
- package/build/lib/grafaid/parse-error.js +29 -0
- package/build/lib/grafaid/parse-error.js.map +1 -0
- package/build/lib/grafaid/parse.d.ts +70 -0
- package/build/lib/grafaid/parse.d.ts.map +1 -0
- package/build/lib/grafaid/parse.js +119 -0
- package/build/lib/grafaid/parse.js.map +1 -0
- package/build/lib/grafaid/schema/ast.d.ts +0 -2
- package/build/lib/grafaid/schema/ast.d.ts.map +1 -1
- package/build/lib/grafaid/schema/ast.js +1 -7
- package/build/lib/grafaid/schema/ast.js.map +1 -1
- package/build/lib/grafaid/schema/read.js +2 -2
- package/build/lib/grafaid/schema/read.js.map +1 -1
- package/build/lib/grafaid/schema/schema.d.ts +2 -1
- package/build/lib/grafaid/schema/schema.d.ts.map +1 -1
- package/build/lib/grafaid/schema/schema.js +5 -1
- package/build/lib/grafaid/schema/schema.js.map +1 -1
- package/build/lib/graphql-schema-loader/graphql-schema-loader.d.ts.map +1 -1
- package/build/lib/graphql-schema-loader/graphql-schema-loader.js +4 -4
- package/build/lib/graphql-schema-loader/graphql-schema-loader.js.map +1 -1
- package/build/lib/revision/revision.d.ts +30 -30
- package/build/lib/schema/schema.d.ts +18 -18
- package/build/lib/schema/unversioned.d.ts +28 -28
- package/build/lib/schema/versioned.d.ts +16 -16
- package/build/template/components/ExampleLink.d.ts.map +1 -1
- package/build/template/components/ExampleLink.js +4 -2
- package/build/template/components/ExampleLink.js.map +1 -1
- package/build/template/components/NamedType.d.ts.map +1 -1
- package/build/template/components/NamedType.js +1 -1
- package/build/template/components/NamedType.js.map +1 -1
- package/build/template/components/VersionCoveragePicker.d.ts.map +1 -1
- package/build/template/components/VersionCoveragePicker.js +6 -1
- package/build/template/components/VersionCoveragePicker.js.map +1 -1
- package/build/template/routes/pages.d.ts.map +1 -1
- package/build/template/routes/pages.js +5 -1
- package/build/template/routes/pages.js.map +1 -1
- package/build/template/stores/changelog.d.ts +1 -1
- package/package.json +1 -1
- package/src/api/examples/schemas/type-usage-index.ts +2 -2
- package/src/api/examples/type-usage-indexer.test.ts +152 -234
- package/src/api/examples/type-usage-indexer.ts +64 -52
- package/src/api/schema/$.test.ts +1 -1
- package/src/api/schema/input-sources/directory.ts +3 -3
- package/src/api/schema/input-sources/file.ts +3 -3
- package/src/api/schema/input-sources/memory.ts +3 -3
- package/src/api/schema/input-sources/versioned-directory.ts +3 -3
- package/src/cli/commands/open.ts +1 -1
- package/src/lib/grafaid/$$.ts +2 -0
- package/src/lib/grafaid/parse-error.ts +69 -0
- package/src/lib/grafaid/parse.test.ts +175 -0
- package/src/lib/grafaid/parse.ts +165 -0
- package/src/lib/grafaid/schema/ast.ts +1 -9
- package/src/lib/grafaid/schema/read.ts +2 -2
- package/src/lib/grafaid/schema/schema.ts +10 -2
- package/src/lib/graphql-schema-loader/graphql-schema-loader.ts +4 -12
- package/src/lib/path-map/$.test.ts +28 -13
- package/src/template/components/ExampleLink.tsx +4 -2
- package/src/template/components/NamedType.tsx +3 -1
- package/src/template/components/VersionCoveragePicker.tsx +8 -2
- package/src/template/routes/pages.tsx +6 -1
@@ -121,11 +121,11 @@ export const normalize = (configInput: Options): Config => {
|
|
121
121
|
const parseSchema = (value: string | GraphQLSchema): Effect.Effect<GraphQLSchema, InputSource.InputSourceError> =>
|
122
122
|
Effect.gen(function*() {
|
123
123
|
if (typeof value === 'string') {
|
124
|
-
const ast = yield* Grafaid.
|
124
|
+
const ast = yield* Grafaid.Parse.parseSchema(value, { source: 'memory' }).pipe(
|
125
125
|
Effect.mapError((error) =>
|
126
126
|
new InputSource.InputSourceError({
|
127
127
|
source: 'memory',
|
128
|
-
message:
|
128
|
+
message: error.message,
|
129
129
|
cause: error,
|
130
130
|
})
|
131
131
|
),
|
@@ -134,7 +134,7 @@ const parseSchema = (value: string | GraphQLSchema): Effect.Effect<GraphQLSchema
|
|
134
134
|
Effect.mapError((error) =>
|
135
135
|
new InputSource.InputSourceError({
|
136
136
|
source: 'memory',
|
137
|
-
message:
|
137
|
+
message: error.message,
|
138
138
|
cause: error,
|
139
139
|
})
|
140
140
|
),
|
@@ -201,11 +201,11 @@ export const readOrThrow = (
|
|
201
201
|
Effect.gen(function*() {
|
202
202
|
const filePath = Path.join(versionInfo.path, revisionFile)
|
203
203
|
const content = yield* fs.readFileString(filePath)
|
204
|
-
const ast = yield* Grafaid.
|
204
|
+
const ast = yield* Grafaid.Parse.parseSchema(content, { source: filePath }).pipe(
|
205
205
|
Effect.mapError((error) =>
|
206
206
|
new InputSource.InputSourceError({
|
207
207
|
source: 'versionedDirectory',
|
208
|
-
message:
|
208
|
+
message: error.message,
|
209
209
|
cause: error,
|
210
210
|
})
|
211
211
|
),
|
@@ -214,7 +214,7 @@ export const readOrThrow = (
|
|
214
214
|
Effect.mapError((error) =>
|
215
215
|
new InputSource.InputSourceError({
|
216
216
|
source: 'versionedDirectory',
|
217
|
-
message:
|
217
|
+
message: error.message,
|
218
218
|
cause: error,
|
219
219
|
})
|
220
220
|
),
|
package/src/cli/commands/open.ts
CHANGED
@@ -74,7 +74,7 @@ const cacheRead = async (source: string, useCache: boolean) => {
|
|
74
74
|
const filePath = Path.join(cacheDir, fileName)
|
75
75
|
const sdl = await Fs.read(filePath)
|
76
76
|
if (!sdl) return null
|
77
|
-
const documentNode = await Effect.runPromise(Grafaid.
|
77
|
+
const documentNode = await Effect.runPromise(Grafaid.Parse.parseSchema(sdl, { source: filePath }))
|
78
78
|
return await Effect.runPromise(Grafaid.Schema.fromAST(documentNode))
|
79
79
|
})
|
80
80
|
}
|
package/src/lib/grafaid/$$.ts
CHANGED
@@ -0,0 +1,69 @@
|
|
1
|
+
import { Data } from 'effect'
|
2
|
+
|
3
|
+
/**
|
4
|
+
* Error that occurs when parsing GraphQL source text fails.
|
5
|
+
*
|
6
|
+
* This error provides structured information about parse failures,
|
7
|
+
* including the source context and the underlying parse error details.
|
8
|
+
*/
|
9
|
+
export class ParseError extends Data.TaggedError('ParseError')<{
|
10
|
+
/**
|
11
|
+
* The type of GraphQL content being parsed.
|
12
|
+
* Helps identify whether the error occurred while parsing a schema or a document.
|
13
|
+
*/
|
14
|
+
readonly parseType: 'schema' | 'document' | 'unknown'
|
15
|
+
|
16
|
+
/**
|
17
|
+
* Optional source identifier for debugging.
|
18
|
+
* Could be a file path, URL, or other identifier.
|
19
|
+
*/
|
20
|
+
readonly source?: string
|
21
|
+
|
22
|
+
/**
|
23
|
+
* The error message describing what went wrong.
|
24
|
+
*/
|
25
|
+
readonly message: string
|
26
|
+
|
27
|
+
/**
|
28
|
+
* The original error that caused the parse failure.
|
29
|
+
*/
|
30
|
+
readonly cause?: unknown
|
31
|
+
|
32
|
+
/**
|
33
|
+
* Optional excerpt of the source text where the error occurred.
|
34
|
+
* Useful for showing context in error messages.
|
35
|
+
*/
|
36
|
+
readonly excerpt?: string
|
37
|
+
}> {}
|
38
|
+
|
39
|
+
/**
|
40
|
+
* Helper to create a ParseError with common defaults.
|
41
|
+
*/
|
42
|
+
export const makeParseError = (
|
43
|
+
message: string,
|
44
|
+
options?: {
|
45
|
+
parseType?: 'schema' | 'document' | 'unknown'
|
46
|
+
source?: string
|
47
|
+
cause?: unknown
|
48
|
+
excerpt?: string
|
49
|
+
},
|
50
|
+
): ParseError => {
|
51
|
+
const props: any = {
|
52
|
+
parseType: options?.parseType ?? 'unknown',
|
53
|
+
message,
|
54
|
+
}
|
55
|
+
|
56
|
+
if (options?.source !== undefined) {
|
57
|
+
props.source = options.source
|
58
|
+
}
|
59
|
+
|
60
|
+
if (options?.cause !== undefined) {
|
61
|
+
props.cause = options.cause
|
62
|
+
}
|
63
|
+
|
64
|
+
if (options?.excerpt !== undefined) {
|
65
|
+
props.excerpt = options.excerpt
|
66
|
+
}
|
67
|
+
|
68
|
+
return new ParseError(props)
|
69
|
+
}
|
@@ -0,0 +1,175 @@
|
|
1
|
+
import { Cause, Effect, Exit, Option } from 'effect'
|
2
|
+
import { expect, test } from 'vitest'
|
3
|
+
import { Grafaid } from './$.js'
|
4
|
+
|
5
|
+
test('parseSchema - parses valid GraphQL schema SDL', () => {
|
6
|
+
const sdl = `type Query { hello: String }`
|
7
|
+
const result = Effect.runSync(Grafaid.Parse.parseSchema(sdl))
|
8
|
+
expect(result).toBeDefined()
|
9
|
+
expect(result.kind).toBe('Document')
|
10
|
+
expect(result.definitions).toHaveLength(1)
|
11
|
+
})
|
12
|
+
|
13
|
+
test('parseSchema - returns ParseError for invalid schema SDL', () => {
|
14
|
+
const invalidSdl = `type Query { hello: String missing_closing_brace`
|
15
|
+
const result = Effect.runSyncExit(Grafaid.Parse.parseSchema(invalidSdl, { source: 'test.graphql' }))
|
16
|
+
expect(Exit.isFailure(result)).toBe(true)
|
17
|
+
|
18
|
+
if (Exit.isFailure(result)) {
|
19
|
+
const error = Cause.failureOption(result.cause)
|
20
|
+
expect(Option.isSome(error)).toBe(true)
|
21
|
+
if (Option.isSome(error)) {
|
22
|
+
expect(error.value).toBeInstanceOf(Grafaid.ParseError)
|
23
|
+
expect(error.value._tag).toBe('ParseError')
|
24
|
+
expect(error.value.parseType).toBe('schema')
|
25
|
+
expect(error.value.source).toBe('test.graphql')
|
26
|
+
expect(error.value.message).toContain('Failed to parse GraphQL schema')
|
27
|
+
expect(error.value.message).toContain('test.graphql')
|
28
|
+
}
|
29
|
+
}
|
30
|
+
})
|
31
|
+
|
32
|
+
test('parseSchema - includes excerpt in error for parse failures', () => {
|
33
|
+
const invalidSdl = `type Query {
|
34
|
+
hello: String
|
35
|
+
invalid syntax here
|
36
|
+
}`
|
37
|
+
const result = Effect.runSyncExit(Grafaid.Parse.parseSchema(invalidSdl))
|
38
|
+
expect(Exit.isFailure(result)).toBe(true)
|
39
|
+
|
40
|
+
if (Exit.isFailure(result)) {
|
41
|
+
const error = Cause.failureOption(result.cause)
|
42
|
+
expect(Option.isSome(error)).toBe(true)
|
43
|
+
if (Option.isSome(error)) {
|
44
|
+
expect(error.value.excerpt).toBeDefined()
|
45
|
+
expect(error.value.excerpt).toContain('Line')
|
46
|
+
}
|
47
|
+
}
|
48
|
+
})
|
49
|
+
|
50
|
+
test('parseDocument - parses valid GraphQL query document', () => {
|
51
|
+
const query = `query GetUser { user { id name } }`
|
52
|
+
const result = Effect.runSync(Grafaid.Parse.parseDocument(query))
|
53
|
+
expect(result).toBeDefined()
|
54
|
+
expect(result.kind).toBe('Document')
|
55
|
+
expect(result.definitions).toHaveLength(1)
|
56
|
+
expect(result.definitions[0]?.kind).toBe('OperationDefinition')
|
57
|
+
})
|
58
|
+
|
59
|
+
test('parseDocument - parses anonymous query', () => {
|
60
|
+
const query = `{ user { id name } }`
|
61
|
+
const result = Effect.runSync(Grafaid.Parse.parseDocument(query))
|
62
|
+
expect(result).toBeDefined()
|
63
|
+
expect(result.kind).toBe('Document')
|
64
|
+
expect(result.definitions).toHaveLength(1)
|
65
|
+
})
|
66
|
+
|
67
|
+
test('parseDocument - returns ParseError for invalid query document', () => {
|
68
|
+
const invalidQuery = `query GetUser { user { id name // invalid comment syntax } }`
|
69
|
+
const result = Effect.runSyncExit(Grafaid.Parse.parseDocument(invalidQuery, { source: 'getUserQuery' }))
|
70
|
+
expect(Exit.isFailure(result)).toBe(true)
|
71
|
+
|
72
|
+
if (Exit.isFailure(result)) {
|
73
|
+
const error = Cause.failureOption(result.cause)
|
74
|
+
expect(Option.isSome(error)).toBe(true)
|
75
|
+
if (Option.isSome(error)) {
|
76
|
+
expect(error.value).toBeInstanceOf(Grafaid.ParseError)
|
77
|
+
expect(error.value._tag).toBe('ParseError')
|
78
|
+
expect(error.value.parseType).toBe('document')
|
79
|
+
expect(error.value.source).toBe('getUserQuery')
|
80
|
+
expect(error.value.message).toContain('Failed to parse GraphQL document')
|
81
|
+
}
|
82
|
+
}
|
83
|
+
})
|
84
|
+
|
85
|
+
test('parse - auto-detects schema type', () => {
|
86
|
+
const sdl = `type Query { hello: String }`
|
87
|
+
const result = Effect.runSyncExit(Grafaid.Parse.parse(sdl))
|
88
|
+
expect(Exit.isSuccess(result)).toBe(true)
|
89
|
+
|
90
|
+
// Check error for parseType inference
|
91
|
+
const invalidSdl = `type Query { invalid`
|
92
|
+
const errorResult = Effect.runSyncExit(Grafaid.Parse.parse(invalidSdl))
|
93
|
+
|
94
|
+
if (Exit.isFailure(errorResult)) {
|
95
|
+
const error = Cause.failureOption(errorResult.cause)
|
96
|
+
if (Option.isSome(error)) {
|
97
|
+
expect(error.value.parseType).toBe('schema')
|
98
|
+
}
|
99
|
+
}
|
100
|
+
})
|
101
|
+
|
102
|
+
test('parse - auto-detects document type for query', () => {
|
103
|
+
const query = `query GetUser { user { id } }`
|
104
|
+
const result = Effect.runSyncExit(Grafaid.Parse.parse(query))
|
105
|
+
expect(Exit.isSuccess(result)).toBe(true)
|
106
|
+
|
107
|
+
// Check error for parseType inference
|
108
|
+
const invalidQuery = `query GetUser { invalid`
|
109
|
+
const errorResult = Effect.runSyncExit(Grafaid.Parse.parse(invalidQuery))
|
110
|
+
|
111
|
+
if (Exit.isFailure(errorResult)) {
|
112
|
+
const error = Cause.failureOption(errorResult.cause)
|
113
|
+
if (Option.isSome(error)) {
|
114
|
+
expect(error.value.parseType).toBe('document')
|
115
|
+
}
|
116
|
+
}
|
117
|
+
})
|
118
|
+
|
119
|
+
test('parse - auto-detects document type for anonymous query', () => {
|
120
|
+
const query = `{ user { id } }`
|
121
|
+
const result = Effect.runSyncExit(Grafaid.Parse.parse(query))
|
122
|
+
expect(Exit.isSuccess(result)).toBe(true)
|
123
|
+
|
124
|
+
// Check error for parseType inference
|
125
|
+
const invalidQuery = `{ invalid`
|
126
|
+
const errorResult = Effect.runSyncExit(Grafaid.Parse.parse(invalidQuery))
|
127
|
+
|
128
|
+
if (Exit.isFailure(errorResult)) {
|
129
|
+
const error = Cause.failureOption(errorResult.cause)
|
130
|
+
if (Option.isSome(error)) {
|
131
|
+
expect(error.value.parseType).toBe('document')
|
132
|
+
}
|
133
|
+
}
|
134
|
+
})
|
135
|
+
|
136
|
+
test('parse - returns unknown type when unable to infer', () => {
|
137
|
+
const ambiguous = `something that is not clearly schema or document`
|
138
|
+
const errorResult = Effect.runSyncExit(Grafaid.Parse.parse(ambiguous))
|
139
|
+
|
140
|
+
if (Exit.isFailure(errorResult)) {
|
141
|
+
const error = Cause.failureOption(errorResult.cause)
|
142
|
+
if (Option.isSome(error)) {
|
143
|
+
expect(error.value.parseType).toBe('unknown')
|
144
|
+
}
|
145
|
+
}
|
146
|
+
})
|
147
|
+
|
148
|
+
test.for([
|
149
|
+
{ input: 'type Query { field: String }', expectedType: 'schema' },
|
150
|
+
{ input: 'interface Node { id: ID! }', expectedType: 'schema' },
|
151
|
+
{ input: 'enum Status { ACTIVE }', expectedType: 'schema' },
|
152
|
+
{ input: 'scalar Date', expectedType: 'schema' },
|
153
|
+
{ input: 'union Result = Success | Error', expectedType: 'schema' },
|
154
|
+
{ input: 'input Filter { name: String }', expectedType: 'schema' },
|
155
|
+
{ input: 'schema { query: Query }', expectedType: 'schema' },
|
156
|
+
{ input: 'extend type Query { extra: String }', expectedType: 'schema' },
|
157
|
+
{ input: 'directive @custom on FIELD', expectedType: 'schema' },
|
158
|
+
{ input: 'query GetData { field }', expectedType: 'document' },
|
159
|
+
{ input: 'mutation UpdateData { field }', expectedType: 'document' },
|
160
|
+
{ input: 'subscription OnData { field }', expectedType: 'document' },
|
161
|
+
{ input: 'fragment UserFields on User { id }', expectedType: 'document' },
|
162
|
+
{ input: '{ field }', expectedType: 'document' },
|
163
|
+
{ input: 'random text', expectedType: 'unknown' },
|
164
|
+
])('parse - infers parseType "$expectedType" for "$input"', ({ input, expectedType }) => {
|
165
|
+
const errorResult = Effect.runSyncExit(Grafaid.Parse.parse(input))
|
166
|
+
|
167
|
+
// These will all fail to parse, but we're testing the type inference
|
168
|
+
if (Exit.isFailure(errorResult)) {
|
169
|
+
const error = Cause.failureOption(errorResult.cause)
|
170
|
+
expect(Option.isSome(error)).toBe(true)
|
171
|
+
if (Option.isSome(error)) {
|
172
|
+
expect(error.value.parseType).toBe(expectedType)
|
173
|
+
}
|
174
|
+
}
|
175
|
+
})
|
@@ -0,0 +1,165 @@
|
|
1
|
+
import { Effect } from 'effect'
|
2
|
+
import { type DocumentNode, Kind, parse as graphqlParse } from 'graphql'
|
3
|
+
import { makeParseError, ParseError } from './parse-error.js'
|
4
|
+
|
5
|
+
/**
|
6
|
+
* Parse GraphQL source text into an AST.
|
7
|
+
*
|
8
|
+
* This is the centralized parsing function that should be used throughout
|
9
|
+
* the codebase for parsing any GraphQL content (schemas or documents).
|
10
|
+
*
|
11
|
+
* @param source - The GraphQL source text to parse
|
12
|
+
* @param options - Optional parsing configuration
|
13
|
+
* @returns An Effect that yields a DocumentNode AST or fails with ParseError
|
14
|
+
*
|
15
|
+
* @example
|
16
|
+
* ```ts
|
17
|
+
* // Parse a schema
|
18
|
+
* const schemaAst = yield* parse(schemaSDL, {
|
19
|
+
* parseType: 'schema',
|
20
|
+
* source: 'schema.graphql'
|
21
|
+
* })
|
22
|
+
*
|
23
|
+
* // Parse a query document
|
24
|
+
* const queryAst = yield* parse(queryString, {
|
25
|
+
* parseType: 'document',
|
26
|
+
* source: 'getUserQuery'
|
27
|
+
* })
|
28
|
+
* ```
|
29
|
+
*/
|
30
|
+
export const parse = (
|
31
|
+
source: string,
|
32
|
+
options?: {
|
33
|
+
/**
|
34
|
+
* The type of content being parsed.
|
35
|
+
* Helps provide better error messages.
|
36
|
+
*/
|
37
|
+
parseType?: 'schema' | 'document' | 'unknown'
|
38
|
+
|
39
|
+
/**
|
40
|
+
* Optional source identifier for debugging.
|
41
|
+
* Could be a file path, URL, or descriptive name.
|
42
|
+
*/
|
43
|
+
source?: string
|
44
|
+
},
|
45
|
+
): Effect.Effect<DocumentNode, ParseError> =>
|
46
|
+
Effect.try({
|
47
|
+
try: () => graphqlParse(source),
|
48
|
+
catch: (error) => {
|
49
|
+
// Extract useful error information
|
50
|
+
let message = 'Failed to parse GraphQL'
|
51
|
+
let excerpt: string | undefined
|
52
|
+
|
53
|
+
if (error instanceof Error) {
|
54
|
+
message = error.message
|
55
|
+
|
56
|
+
// GraphQL parse errors often include location info
|
57
|
+
// Try to extract a meaningful excerpt if possible
|
58
|
+
if ('locations' in error && Array.isArray(error.locations) && error.locations.length > 0) {
|
59
|
+
const location = error.locations[0]
|
60
|
+
if (location && typeof location.line === 'number') {
|
61
|
+
const lines = source.split('\n')
|
62
|
+
const errorLine = lines[location.line - 1]
|
63
|
+
if (errorLine) {
|
64
|
+
excerpt = `Line ${location.line}: ${errorLine.trim()}`
|
65
|
+
}
|
66
|
+
}
|
67
|
+
}
|
68
|
+
}
|
69
|
+
|
70
|
+
const parseType = options?.parseType ?? inferParseType(source)
|
71
|
+
|
72
|
+
const errorOptions: Parameters<typeof makeParseError>[1] = {
|
73
|
+
parseType,
|
74
|
+
cause: error,
|
75
|
+
}
|
76
|
+
|
77
|
+
if (options?.source) {
|
78
|
+
errorOptions.source = options.source
|
79
|
+
}
|
80
|
+
|
81
|
+
if (excerpt) {
|
82
|
+
errorOptions.excerpt = excerpt
|
83
|
+
}
|
84
|
+
|
85
|
+
return makeParseError(
|
86
|
+
`Failed to parse GraphQL ${parseType}${options?.source ? ` from ${options.source}` : ''}: ${message}`,
|
87
|
+
errorOptions,
|
88
|
+
)
|
89
|
+
},
|
90
|
+
})
|
91
|
+
|
92
|
+
/**
|
93
|
+
* Parse GraphQL schema SDL with typed errors.
|
94
|
+
*
|
95
|
+
* This is a specialized version of parse() that's explicitly for schemas.
|
96
|
+
*
|
97
|
+
* @param source - The GraphQL schema SDL to parse
|
98
|
+
* @param options - Optional parsing configuration
|
99
|
+
* @returns An Effect that yields a DocumentNode AST or fails with ParseError
|
100
|
+
*/
|
101
|
+
export const parseSchema = (
|
102
|
+
source: string,
|
103
|
+
options?: {
|
104
|
+
source?: string
|
105
|
+
},
|
106
|
+
): Effect.Effect<DocumentNode, ParseError> =>
|
107
|
+
parse(source, {
|
108
|
+
parseType: 'schema',
|
109
|
+
...options,
|
110
|
+
})
|
111
|
+
|
112
|
+
/**
|
113
|
+
* Parse GraphQL document (query/mutation/subscription) with typed errors.
|
114
|
+
*
|
115
|
+
* This is a specialized version of parse() that's explicitly for executable documents.
|
116
|
+
*
|
117
|
+
* @param source - The GraphQL document to parse
|
118
|
+
* @param options - Optional parsing configuration
|
119
|
+
* @returns An Effect that yields a DocumentNode AST or fails with ParseError
|
120
|
+
*/
|
121
|
+
export const parseDocument = (
|
122
|
+
source: string,
|
123
|
+
options?: {
|
124
|
+
source?: string
|
125
|
+
},
|
126
|
+
): Effect.Effect<DocumentNode, ParseError> =>
|
127
|
+
parse(source, {
|
128
|
+
parseType: 'document',
|
129
|
+
...options,
|
130
|
+
})
|
131
|
+
|
132
|
+
/**
|
133
|
+
* Try to infer whether the source is a schema or document.
|
134
|
+
* This is a best-effort heuristic and may not always be accurate.
|
135
|
+
*/
|
136
|
+
const inferParseType = (source: string): 'schema' | 'document' | 'unknown' => {
|
137
|
+
// Common schema definition keywords
|
138
|
+
const schemaKeywords = /^\s*(type|interface|enum|scalar|union|input|schema|extend|directive)\s+/m
|
139
|
+
// Common document keywords
|
140
|
+
const documentKeywords = /^\s*(query|mutation|subscription|fragment)\s+/m
|
141
|
+
|
142
|
+
if (schemaKeywords.test(source)) {
|
143
|
+
return 'schema'
|
144
|
+
}
|
145
|
+
|
146
|
+
if (documentKeywords.test(source)) {
|
147
|
+
return 'document'
|
148
|
+
}
|
149
|
+
|
150
|
+
// Check for anonymous query syntax { ... }
|
151
|
+
if (/^\s*\{/.test(source)) {
|
152
|
+
return 'document'
|
153
|
+
}
|
154
|
+
|
155
|
+
return 'unknown'
|
156
|
+
}
|
157
|
+
|
158
|
+
/**
|
159
|
+
* Create an empty DocumentNode.
|
160
|
+
* Useful for representing an absence of GraphQL content.
|
161
|
+
*/
|
162
|
+
export const empty: DocumentNode = {
|
163
|
+
definitions: [],
|
164
|
+
kind: Kind.DOCUMENT,
|
165
|
+
}
|
@@ -1,12 +1,4 @@
|
|
1
|
-
import {
|
2
|
-
import { type DocumentNode, Kind, parse as graphqlParse } from 'graphql'
|
3
|
-
|
4
|
-
// Effect-based version of parse
|
5
|
-
export const parse = (source: string): Effect.Effect<DocumentNode, Error> =>
|
6
|
-
Effect.try({
|
7
|
-
try: () => graphqlParse(source),
|
8
|
-
catch: (error) => new Error(`Failed to parse GraphQL: ${error}`),
|
9
|
-
})
|
1
|
+
import { type DocumentNode, Kind } from 'graphql'
|
10
2
|
|
11
3
|
export { type DocumentNode as Document } from 'graphql'
|
12
4
|
|
@@ -2,7 +2,7 @@ import { FileSystem } from '@effect/platform/FileSystem'
|
|
2
2
|
import { Fs } from '@wollybeard/kit'
|
3
3
|
import { Effect } from 'effect'
|
4
4
|
import { type GraphQLSchema } from 'graphql'
|
5
|
-
import * as
|
5
|
+
import * as Parse from '../parse.js'
|
6
6
|
import { fromAST } from './schema.js'
|
7
7
|
|
8
8
|
export const read = (sdlFilePath: string): Effect.Effect<null | Fs.File<GraphQLSchema>, Error, FileSystem> =>
|
@@ -19,7 +19,7 @@ export const read = (sdlFilePath: string): Effect.Effect<null | Fs.File<GraphQLS
|
|
19
19
|
const content = yield* fs.readFileString(sdlFilePath)
|
20
20
|
|
21
21
|
// Parse and build schema
|
22
|
-
const node = yield*
|
22
|
+
const node = yield* Parse.parseSchema(content, { source: sdlFilePath })
|
23
23
|
const schema = yield* fromAST(node)
|
24
24
|
|
25
25
|
return {
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import { Effect } from 'effect'
|
2
2
|
import { buildASTSchema, type GraphQLSchema } from 'graphql'
|
3
|
+
import { makeParseError, ParseError } from '../parse-error.js'
|
3
4
|
|
4
5
|
export {
|
5
6
|
buildClientSchema as fromIntrospectionQuery,
|
@@ -9,10 +10,17 @@ export {
|
|
9
10
|
} from 'graphql'
|
10
11
|
|
11
12
|
// Effect-based version of fromAST
|
12
|
-
export const fromAST = (ast: AST.Document): Effect.Effect<GraphQLSchema,
|
13
|
+
export const fromAST = (ast: AST.Document): Effect.Effect<GraphQLSchema, ParseError> =>
|
13
14
|
Effect.try({
|
14
15
|
try: () => buildASTSchema(ast),
|
15
|
-
catch: (error) =>
|
16
|
+
catch: (error) =>
|
17
|
+
makeParseError(
|
18
|
+
`Failed to build schema from AST: ${error instanceof Error ? error.message : String(error)}`,
|
19
|
+
{
|
20
|
+
parseType: 'schema',
|
21
|
+
cause: error,
|
22
|
+
},
|
23
|
+
),
|
16
24
|
})
|
17
25
|
|
18
26
|
export * as AST from './ast.js'
|
@@ -59,24 +59,16 @@ export const load = (source: SchemaPointer): Effect.Effect<Grafaid.Schema.Schema
|
|
59
59
|
catch: (error) => new Error(`Failed to read response text: ${error}`),
|
60
60
|
})
|
61
61
|
|
62
|
-
const ast = yield* Grafaid.
|
63
|
-
|
64
|
-
)
|
65
|
-
return yield* Grafaid.Schema.fromAST(ast).pipe(
|
66
|
-
Effect.mapError((error) => new Error(`Failed to build schema from ${source.pathOrUrl}: ${error}`)),
|
67
|
-
)
|
62
|
+
const ast = yield* Grafaid.Parse.parseSchema(sdlContent, { source: source.pathOrUrl })
|
63
|
+
return yield* Grafaid.Schema.fromAST(ast)
|
68
64
|
} else {
|
69
65
|
const fs = yield* FileSystem
|
70
66
|
const sdlContent = yield* fs.readFileString(source.pathOrUrl).pipe(
|
71
67
|
Effect.mapError((error) => new Error(`Failed to read SDL from ${source.pathOrUrl}: ${error}`)),
|
72
68
|
)
|
73
69
|
|
74
|
-
const ast = yield* Grafaid.
|
75
|
-
|
76
|
-
)
|
77
|
-
return yield* Grafaid.Schema.fromAST(ast).pipe(
|
78
|
-
Effect.mapError((error) => new Error(`Failed to build schema from ${source.pathOrUrl}: ${error}`)),
|
79
|
-
)
|
70
|
+
const ast = yield* Grafaid.Parse.parseSchema(sdlContent, { source: source.pathOrUrl })
|
71
|
+
return yield* Grafaid.Schema.fromAST(ast)
|
80
72
|
}
|
81
73
|
}
|
82
74
|
case `name`: {
|
@@ -258,22 +258,37 @@ describe('PathMap', () => {
|
|
258
258
|
|
259
259
|
it('absolute paths always start with base', () => {
|
260
260
|
fc.assert(
|
261
|
-
fc.property(
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
261
|
+
fc.property(
|
262
|
+
pathInput,
|
263
|
+
fc.string({ minLength: 1 })
|
264
|
+
.filter(s => s.startsWith('/'))
|
265
|
+
// Exclude paths with .. segments as Path.join will resolve them
|
266
|
+
.filter(s => !s.includes('..')),
|
267
|
+
(input, base) => {
|
268
|
+
const paths = PathMap.create(input, base)
|
269
|
+
const normalizedBase = normalizeBasePath(base)
|
270
|
+
|
271
|
+
function checkAbsolute(obj: any) {
|
272
|
+
for (const [key, value] of Object.entries(obj)) {
|
273
|
+
if (typeof value === 'string') {
|
274
|
+
// Path.join resolves .. segments, so we need to resolve the base too
|
275
|
+
const resolvedBase = Path.resolve(normalizedBase)
|
276
|
+
const resolvedValue = Path.resolve(value)
|
277
|
+
// Check that the resolved absolute path starts with resolved base
|
278
|
+
// or that both resolve to root
|
279
|
+
expect(
|
280
|
+
resolvedValue.startsWith(resolvedBase)
|
281
|
+
|| (resolvedBase === '/' && resolvedValue.startsWith('/')),
|
282
|
+
).toBe(true)
|
283
|
+
} else if (typeof value === 'object') {
|
284
|
+
checkAbsolute(value)
|
285
|
+
}
|
271
286
|
}
|
272
287
|
}
|
273
|
-
}
|
274
288
|
|
275
|
-
|
276
|
-
|
289
|
+
checkAbsolute(paths.absolute)
|
290
|
+
},
|
291
|
+
),
|
277
292
|
)
|
278
293
|
})
|
279
294
|
|
@@ -12,8 +12,10 @@ export interface Props {
|
|
12
12
|
* Always includes the version query parameter for consistent behavior.
|
13
13
|
*/
|
14
14
|
export const ExampleLink: FC<Props> = ({ exampleRef }) => {
|
15
|
-
//
|
16
|
-
const href =
|
15
|
+
// Include version parameter only if version exists
|
16
|
+
const href = exampleRef.version
|
17
|
+
? `/examples/${exampleRef.name}?version=${Version.encodeSync(exampleRef.version)}`
|
18
|
+
: `/examples/${exampleRef.name}`
|
17
19
|
|
18
20
|
return (
|
19
21
|
<Link href={href} style={{ textDecoration: 'none' }}>
|
@@ -59,7 +59,9 @@ export const NamedType: FC<Props> = ({ data }) => {
|
|
59
59
|
<Box style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
|
60
60
|
{[...examples].map((exampleRef) => (
|
61
61
|
<ExampleLink
|
62
|
-
key={`${exampleRef.name}-${
|
62
|
+
key={`${exampleRef.name}-${
|
63
|
+
exampleRef.version ? Version.encodeSync(exampleRef.version) : 'unversioned'
|
64
|
+
}`}
|
63
65
|
exampleRef={exampleRef}
|
64
66
|
/>
|
65
67
|
))}
|
@@ -26,6 +26,12 @@ export const VersionCoveragePicker: FC<Props> = ({
|
|
26
26
|
const options = Array.from(HashMap.keys(document.versionDocuments))
|
27
27
|
if (options.length === 0) return null
|
28
28
|
|
29
|
+
// Helper to format labels with proper Version/Versions prefix
|
30
|
+
const formatLabel = (versionCoverage: VersionCoverage.VersionCoverage): string => {
|
31
|
+
const prefix = VersionCoverage.isSingle(versionCoverage) ? 'Version' : 'Versions'
|
32
|
+
return `${prefix} ${VersionCoverage.toLabel(versionCoverage)}`
|
33
|
+
}
|
34
|
+
|
29
35
|
return (
|
30
36
|
<Select.Root
|
31
37
|
value={VersionCoverage.toLabel(current)}
|
@@ -39,7 +45,7 @@ export const VersionCoveragePicker: FC<Props> = ({
|
|
39
45
|
}}
|
40
46
|
>
|
41
47
|
<Select.Trigger>
|
42
|
-
{
|
48
|
+
{formatLabel(current)}
|
43
49
|
</Select.Trigger>
|
44
50
|
<Select.Content position='popper' sideOffset={5}>
|
45
51
|
{options.map(selection => (
|
@@ -47,7 +53,7 @@ export const VersionCoveragePicker: FC<Props> = ({
|
|
47
53
|
key={VersionCoverage.toLabel(selection)}
|
48
54
|
value={VersionCoverage.toLabel(selection)}
|
49
55
|
>
|
50
|
-
{
|
56
|
+
{formatLabel(selection)}
|
51
57
|
</Select.Item>
|
52
58
|
))}
|
53
59
|
</Select.Content>
|