@commitspark/graphql-api 2.0.0-beta.1 → 2.0.0-beta.2
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/CHANGELOG.md +7 -0
- package/README.md +84 -1
- package/dist/cjs/client.d.ts +4 -2
- package/dist/cjs/client.js +4 -2
- package/dist/cjs/client.js.map +1 -1
- package/dist/cjs/graphql/errors.d.ts +3 -2
- package/dist/cjs/graphql/errors.js.map +1 -1
- package/dist/cjs/graphql/input-type-generator.js +2 -1
- package/dist/cjs/graphql/input-type-generator.js.map +1 -1
- package/dist/cjs/graphql/queries-mutations-generator.d.ts +8 -1
- package/dist/cjs/graphql/queries-mutations-generator.js +19 -0
- package/dist/cjs/graphql/queries-mutations-generator.js.map +1 -1
- package/dist/cjs/graphql/resolvers/field-resolvers/field-default-value-resolver.js +2 -1
- package/dist/cjs/graphql/resolvers/field-resolvers/field-default-value-resolver.js.map +1 -1
- package/dist/cjs/graphql/resolvers/query-mutation-resolvers/query-search-resolver.d.ts +2 -0
- package/dist/cjs/graphql/resolvers/query-mutation-resolvers/query-search-resolver.js +74 -0
- package/dist/cjs/graphql/resolvers/query-mutation-resolvers/query-search-resolver.js.map +1 -0
- package/dist/cjs/graphql/resolvers/types.d.ts +6 -0
- package/dist/cjs/graphql/schema-analyzer.js +2 -1
- package/dist/cjs/graphql/schema-analyzer.js.map +1 -1
- package/dist/cjs/graphql/schema-generator.js +7 -1
- package/dist/cjs/graphql/schema-generator.js.map +1 -1
- package/dist/cjs/graphql/schema-root-type-generator.d.ts +2 -2
- package/dist/cjs/graphql/schema-root-type-generator.js +2 -1
- package/dist/cjs/graphql/schema-root-type-generator.js.map +1 -1
- package/dist/cjs/graphql/schema-utils/directive-util.d.ts +4 -0
- package/dist/cjs/graphql/schema-utils/directive-util.js +10 -0
- package/dist/cjs/graphql/schema-utils/directive-util.js.map +1 -0
- package/dist/cjs/graphql/schema-utils/entry-reference-util.js +3 -1
- package/dist/cjs/graphql/schema-utils/entry-reference-util.js.map +1 -1
- package/dist/cjs/graphql/schema-utils/entry-type-util.d.ts +1 -2
- package/dist/cjs/graphql/schema-utils/entry-type-util.js +2 -6
- package/dist/cjs/graphql/schema-utils/entry-type-util.js.map +1 -1
- package/dist/cjs/graphql/schema-utils/searchable-field-util.d.ts +4 -0
- package/dist/cjs/graphql/schema-utils/searchable-field-util.js +79 -0
- package/dist/cjs/graphql/schema-utils/searchable-field-util.js.map +1 -0
- package/dist/cjs/graphql/schema-validator.js +21 -3
- package/dist/cjs/graphql/schema-validator.js.map +1 -1
- package/dist/cjs/index.d.ts +9 -2
- package/dist/cjs/index.js +4 -3
- package/dist/cjs/index.js.map +1 -1
- package/dist/esm/client.js +4 -2
- package/dist/esm/client.js.map +1 -1
- package/dist/esm/graphql/errors.js.map +1 -1
- package/dist/esm/graphql/input-type-generator.js +3 -2
- package/dist/esm/graphql/input-type-generator.js.map +1 -1
- package/dist/esm/graphql/queries-mutations-generator.js +18 -0
- package/dist/esm/graphql/queries-mutations-generator.js.map +1 -1
- package/dist/esm/graphql/resolvers/field-resolvers/field-default-value-resolver.js +3 -2
- package/dist/esm/graphql/resolvers/field-resolvers/field-default-value-resolver.js.map +1 -1
- package/dist/esm/graphql/resolvers/query-mutation-resolvers/query-search-resolver.js +70 -0
- package/dist/esm/graphql/resolvers/query-mutation-resolvers/query-search-resolver.js.map +1 -0
- package/dist/esm/graphql/schema-analyzer.js +2 -1
- package/dist/esm/graphql/schema-analyzer.js.map +1 -1
- package/dist/esm/graphql/schema-generator.js +8 -2
- package/dist/esm/graphql/schema-generator.js.map +1 -1
- package/dist/esm/graphql/schema-root-type-generator.js +2 -1
- package/dist/esm/graphql/schema-root-type-generator.js.map +1 -1
- package/dist/esm/graphql/schema-utils/directive-util.js +6 -0
- package/dist/esm/graphql/schema-utils/directive-util.js.map +1 -0
- package/dist/esm/graphql/schema-utils/entry-reference-util.js +4 -2
- package/dist/esm/graphql/schema-utils/entry-reference-util.js.map +1 -1
- package/dist/esm/graphql/schema-utils/entry-type-util.js +2 -5
- package/dist/esm/graphql/schema-utils/entry-type-util.js.map +1 -1
- package/dist/esm/graphql/schema-utils/searchable-field-util.js +76 -0
- package/dist/esm/graphql/schema-utils/searchable-field-util.js.map +1 -0
- package/dist/esm/graphql/schema-validator.js +22 -4
- package/dist/esm/graphql/schema-validator.js.map +1 -1
- package/dist/esm/index.js +4 -3
- package/dist/esm/index.js.map +1 -1
- package/dist/types/client.d.ts +4 -2
- package/dist/types/client.d.ts.map +1 -1
- package/dist/types/graphql/errors.d.ts +3 -2
- package/dist/types/graphql/errors.d.ts.map +1 -1
- package/dist/types/graphql/input-type-generator.d.ts.map +1 -1
- package/dist/types/graphql/queries-mutations-generator.d.ts +8 -1
- package/dist/types/graphql/queries-mutations-generator.d.ts.map +1 -1
- package/dist/types/graphql/resolvers/field-resolvers/field-default-value-resolver.d.ts.map +1 -1
- package/dist/types/graphql/resolvers/query-mutation-resolvers/query-search-resolver.d.ts +3 -0
- package/dist/types/graphql/resolvers/query-mutation-resolvers/query-search-resolver.d.ts.map +1 -0
- package/dist/types/graphql/resolvers/types.d.ts +6 -0
- package/dist/types/graphql/resolvers/types.d.ts.map +1 -1
- package/dist/types/graphql/schema-analyzer.d.ts.map +1 -1
- package/dist/types/graphql/schema-generator.d.ts.map +1 -1
- package/dist/types/graphql/schema-root-type-generator.d.ts +2 -2
- package/dist/types/graphql/schema-root-type-generator.d.ts.map +1 -1
- package/dist/types/graphql/schema-utils/directive-util.d.ts +5 -0
- package/dist/types/graphql/schema-utils/directive-util.d.ts.map +1 -0
- package/dist/types/graphql/schema-utils/entry-reference-util.d.ts.map +1 -1
- package/dist/types/graphql/schema-utils/entry-type-util.d.ts +1 -2
- package/dist/types/graphql/schema-utils/entry-type-util.d.ts.map +1 -1
- package/dist/types/graphql/schema-utils/searchable-field-util.d.ts +5 -0
- package/dist/types/graphql/schema-utils/searchable-field-util.d.ts.map +1 -0
- package/dist/types/graphql/schema-validator.d.ts.map +1 -1
- package/dist/types/index.d.ts +9 -2
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/client.ts +6 -0
- package/src/graphql/errors.ts +3 -2
- package/src/graphql/input-type-generator.ts +5 -4
- package/src/graphql/queries-mutations-generator.ts +31 -1
- package/src/graphql/resolvers/field-resolvers/field-default-value-resolver.ts +5 -4
- package/src/graphql/resolvers/query-mutation-resolvers/query-search-resolver.ts +110 -0
- package/src/graphql/resolvers/types.ts +8 -0
- package/src/graphql/schema-analyzer.ts +6 -1
- package/src/graphql/schema-generator.ts +13 -1
- package/src/graphql/schema-root-type-generator.ts +3 -0
- package/src/graphql/schema-utils/directive-util.ts +15 -0
- package/src/graphql/schema-utils/entry-reference-util.ts +6 -2
- package/src/graphql/schema-utils/entry-type-util.ts +2 -11
- package/src/graphql/schema-utils/searchable-field-util.ts +130 -0
- package/src/graphql/schema-validator.ts +37 -7
- package/src/index.ts +25 -4
|
@@ -14,10 +14,11 @@ import {
|
|
|
14
14
|
isUnionType,
|
|
15
15
|
} from 'graphql'
|
|
16
16
|
import { SchemaAnalyzerResult } from './schema-analyzer.ts'
|
|
17
|
+
import { buildsOnTypeWithEntryDirective } from './schema-utils/entry-type-util.ts'
|
|
17
18
|
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} from './schema-utils/
|
|
19
|
+
ENTRY_DIRECTIVE_NAME,
|
|
20
|
+
hasDirective,
|
|
21
|
+
} from './schema-utils/directive-util.ts'
|
|
21
22
|
|
|
22
23
|
function generateFieldInputTypeString(type: GraphQLNullableType): string {
|
|
23
24
|
if (isListType(type)) {
|
|
@@ -25,7 +26,7 @@ function generateFieldInputTypeString(type: GraphQLNullableType): string {
|
|
|
25
26
|
} else if (isNonNullType(type)) {
|
|
26
27
|
return `${generateFieldInputTypeString(type.ofType)}!`
|
|
27
28
|
} else if (isObjectType(type)) {
|
|
28
|
-
if (
|
|
29
|
+
if (hasDirective(type, ENTRY_DIRECTIVE_NAME)) {
|
|
29
30
|
return `${type.name}IdInput`
|
|
30
31
|
} else {
|
|
31
32
|
return `${type.name}Input`
|
|
@@ -6,7 +6,11 @@ import { mutationCreateResolver } from './resolvers/query-mutation-resolvers/mut
|
|
|
6
6
|
import { mutationUpdateResolver } from './resolvers/query-mutation-resolvers/mutation-update-resolver.ts'
|
|
7
7
|
import { mutationDeleteResolver } from './resolvers/query-mutation-resolvers/mutation-delete-resolver.ts'
|
|
8
8
|
import { queryTypeByIdResolver } from './resolvers/query-mutation-resolvers/query-type-by-id-resolver.ts'
|
|
9
|
-
import {
|
|
9
|
+
import { querySearchResolver } from './resolvers/query-mutation-resolvers/query-search-resolver.ts'
|
|
10
|
+
import {
|
|
11
|
+
QueryMutationResolver,
|
|
12
|
+
SearchQueryResolver,
|
|
13
|
+
} from './resolvers/types.ts'
|
|
10
14
|
|
|
11
15
|
export function generateQueriesAndMutations(
|
|
12
16
|
entryDirectiveTypes: GraphQLObjectType[],
|
|
@@ -122,6 +126,32 @@ export function generateTypeNameQuery(): GeneratedQuery {
|
|
|
122
126
|
}
|
|
123
127
|
}
|
|
124
128
|
|
|
129
|
+
export function generateSearchQuery(): GeneratedSearchQuery {
|
|
130
|
+
const searchQueryName = '_search'
|
|
131
|
+
const searchHitTypeName = '_SearchHit'
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
name: searchQueryName,
|
|
135
|
+
schemaString: `${searchQueryName}(query: String!, types: [String!], first: Int): [${searchHitTypeName}!]!`,
|
|
136
|
+
typeDefinitionString: `type ${searchHitTypeName} {
|
|
137
|
+
entryId: ID!
|
|
138
|
+
entryType: String!
|
|
139
|
+
fieldPath: String!
|
|
140
|
+
score: Float!
|
|
141
|
+
snippet: String!
|
|
142
|
+
}
|
|
143
|
+
`,
|
|
144
|
+
resolver: querySearchResolver,
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export interface GeneratedSearchQuery {
|
|
149
|
+
name: string
|
|
150
|
+
schemaString: string
|
|
151
|
+
typeDefinitionString: string
|
|
152
|
+
resolver: SearchQueryResolver
|
|
153
|
+
}
|
|
154
|
+
|
|
125
155
|
export interface GeneratedSchema {
|
|
126
156
|
queryEvery: GeneratedQuery
|
|
127
157
|
queryById: GeneratedQuery
|
|
@@ -7,10 +7,11 @@ import {
|
|
|
7
7
|
isObjectType,
|
|
8
8
|
isUnionType,
|
|
9
9
|
} from 'graphql'
|
|
10
|
+
import { buildsOnTypeWithEntryDirective } from '../../schema-utils/entry-type-util.ts'
|
|
10
11
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
} from '../../schema-utils/
|
|
12
|
+
ENTRY_DIRECTIVE_NAME,
|
|
13
|
+
hasDirective,
|
|
14
|
+
} from '../../schema-utils/directive-util.ts'
|
|
14
15
|
import { resolveEntryReference } from './entry-reference-resolver.ts'
|
|
15
16
|
import { resolveUnionValue } from './union-value-resolver.ts'
|
|
16
17
|
import { createError, ErrorCode } from '../../errors.ts'
|
|
@@ -116,7 +117,7 @@ export const resolveFieldDefaultValue: FieldResolver = async (
|
|
|
116
117
|
}
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
if (
|
|
120
|
+
if (hasDirective(context.currentType, ENTRY_DIRECTIVE_NAME)) {
|
|
120
121
|
return resolveEntryReference(fieldValue, args, context, info)
|
|
121
122
|
}
|
|
122
123
|
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { GraphQLError, GraphQLSchema, isObjectType } from 'graphql'
|
|
2
|
+
import { SearchAdapterError, SearchHit } from '@commitspark/search-adapter'
|
|
3
|
+
import { createError, ErrorCode } from '../../errors.ts'
|
|
4
|
+
import {
|
|
5
|
+
ENTRY_DIRECTIVE_NAME,
|
|
6
|
+
hasDirective,
|
|
7
|
+
} from '../../schema-utils/directive-util.ts'
|
|
8
|
+
import { extractSearchableFieldValues } from '../../schema-utils/searchable-field-util.ts'
|
|
9
|
+
import { SearchQueryResolver } from '../types.ts'
|
|
10
|
+
|
|
11
|
+
const SEARCH_DEFAULT_LIMIT = 10
|
|
12
|
+
const SEARCH_MAX_LIMIT = 100
|
|
13
|
+
|
|
14
|
+
export const querySearchResolver: SearchQueryResolver = async (
|
|
15
|
+
_source,
|
|
16
|
+
args,
|
|
17
|
+
context,
|
|
18
|
+
info,
|
|
19
|
+
) => {
|
|
20
|
+
const searchAdapter = context.searchAdapter
|
|
21
|
+
if (searchAdapter === undefined) {
|
|
22
|
+
throw createError(
|
|
23
|
+
'No search adapter configured.',
|
|
24
|
+
ErrorCode.INTERNAL_ERROR,
|
|
25
|
+
{},
|
|
26
|
+
)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (args.query.trim() === '') {
|
|
30
|
+
throw createError(
|
|
31
|
+
'Search query must not be empty.',
|
|
32
|
+
ErrorCode.BAD_USER_INPUT,
|
|
33
|
+
{
|
|
34
|
+
argumentName: 'query',
|
|
35
|
+
argumentValue: args.query,
|
|
36
|
+
},
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const limit = args.first ?? SEARCH_DEFAULT_LIMIT
|
|
41
|
+
if (limit < 1 || limit > SEARCH_MAX_LIMIT) {
|
|
42
|
+
throw createError(
|
|
43
|
+
`Argument "first" must be between 1 and ${SEARCH_MAX_LIMIT}.`,
|
|
44
|
+
ErrorCode.BAD_USER_INPUT,
|
|
45
|
+
{
|
|
46
|
+
argumentName: 'first',
|
|
47
|
+
argumentValue: limit,
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const entryTypes = args.types ?? undefined
|
|
53
|
+
if (entryTypes !== undefined) {
|
|
54
|
+
validateEntryTypeNames(info.schema, entryTypes)
|
|
55
|
+
if (entryTypes.length === 0) {
|
|
56
|
+
return []
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const commitHash = context.getCurrentHash()
|
|
61
|
+
let hits: SearchHit[]
|
|
62
|
+
try {
|
|
63
|
+
hits = await searchAdapter.search({
|
|
64
|
+
commitHash: commitHash,
|
|
65
|
+
query: args.query,
|
|
66
|
+
entryTypes: entryTypes,
|
|
67
|
+
limit: limit,
|
|
68
|
+
getSearchableFieldValues: async () => {
|
|
69
|
+
const entriesRecord = await context.repositoryCache.getEntriesRecord(
|
|
70
|
+
context,
|
|
71
|
+
commitHash,
|
|
72
|
+
)
|
|
73
|
+
return extractSearchableFieldValues(
|
|
74
|
+
info.schema,
|
|
75
|
+
entriesRecord.byId.values(),
|
|
76
|
+
)
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (err instanceof GraphQLError) {
|
|
81
|
+
throw err
|
|
82
|
+
}
|
|
83
|
+
if (err instanceof SearchAdapterError) {
|
|
84
|
+
throw createError(err.message, err.code, {})
|
|
85
|
+
}
|
|
86
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
87
|
+
throw createError(message, ErrorCode.INTERNAL_ERROR, {})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return hits.slice(0, limit)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function validateEntryTypeNames(
|
|
94
|
+
schema: GraphQLSchema,
|
|
95
|
+
typeNames: string[],
|
|
96
|
+
): void {
|
|
97
|
+
for (const typeName of typeNames) {
|
|
98
|
+
const type = schema.getType(typeName)
|
|
99
|
+
if (!isObjectType(type) || !hasDirective(type, ENTRY_DIRECTIVE_NAME)) {
|
|
100
|
+
throw createError(
|
|
101
|
+
`Type "${typeName}" is not an entry type.`,
|
|
102
|
+
ErrorCode.BAD_USER_INPUT,
|
|
103
|
+
{
|
|
104
|
+
argumentName: 'types',
|
|
105
|
+
argumentValue: typeName,
|
|
106
|
+
},
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
GraphQLTypeResolver,
|
|
6
6
|
} from 'graphql'
|
|
7
7
|
import { EntryData } from '@commitspark/git-adapter'
|
|
8
|
+
import { SearchHit } from '@commitspark/search-adapter'
|
|
8
9
|
import { RecursiveArray } from './field-resolvers/types.ts'
|
|
9
10
|
|
|
10
11
|
export interface QueryMutationResolverContext extends ApolloContext {
|
|
@@ -28,6 +29,13 @@ type UnionTypeResolverSourceData = EntryData & {
|
|
|
28
29
|
__typename: string
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
export type SearchQueryResolver = GraphQLFieldResolver<
|
|
33
|
+
unknown,
|
|
34
|
+
ApolloContext,
|
|
35
|
+
{ query: string; types?: string[] | null; first?: number | null },
|
|
36
|
+
Promise<SearchHit[]>
|
|
37
|
+
>
|
|
38
|
+
|
|
31
39
|
export type ContextInjectionResolver = GraphQLFieldResolver<
|
|
32
40
|
EntryData,
|
|
33
41
|
ApolloContext,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
isUnionType,
|
|
9
9
|
} from 'graphql'
|
|
10
10
|
import { getDirective } from '@graphql-tools/utils'
|
|
11
|
+
import { ENTRY_DIRECTIVE_NAME } from './schema-utils/directive-util.ts'
|
|
11
12
|
|
|
12
13
|
export function analyzeSchema(schema: GraphQLSchema): SchemaAnalyzerResult {
|
|
13
14
|
const result: SchemaAnalyzerResult = {
|
|
@@ -27,7 +28,11 @@ export function analyzeSchema(schema: GraphQLSchema): SchemaAnalyzerResult {
|
|
|
27
28
|
if (isObjectType(type)) {
|
|
28
29
|
const objectType = type as GraphQLObjectType
|
|
29
30
|
result.objectTypes.push(objectType)
|
|
30
|
-
const entityDirective = getDirective(
|
|
31
|
+
const entityDirective = getDirective(
|
|
32
|
+
schema,
|
|
33
|
+
objectType,
|
|
34
|
+
ENTRY_DIRECTIVE_NAME,
|
|
35
|
+
)?.[0]
|
|
31
36
|
if (entityDirective) {
|
|
32
37
|
result.entryDirectiveTypes.push(objectType)
|
|
33
38
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
generateQueriesAndMutations,
|
|
3
|
+
generateSearchQuery,
|
|
3
4
|
generateTypeNameQuery,
|
|
4
5
|
} from './queries-mutations-generator.ts'
|
|
5
6
|
import {
|
|
@@ -24,6 +25,7 @@ import { FieldResolver } from './resolvers/field-resolvers/types.ts'
|
|
|
24
25
|
import {
|
|
25
26
|
ContextInjectionResolver,
|
|
26
27
|
QueryMutationResolver,
|
|
28
|
+
SearchQueryResolver,
|
|
27
29
|
UnionTypeResolver,
|
|
28
30
|
} from './resolvers/types.ts'
|
|
29
31
|
import { EntryData } from '@commitspark/git-adapter'
|
|
@@ -57,6 +59,8 @@ export async function generateSchema(
|
|
|
57
59
|
schemaAnalyzerResult.entryDirectiveTypes,
|
|
58
60
|
)
|
|
59
61
|
const generatedTypeNameQuery = generateTypeNameQuery()
|
|
62
|
+
const generatedSearchQuery =
|
|
63
|
+
context.searchAdapter !== undefined ? generateSearchQuery() : undefined
|
|
60
64
|
|
|
61
65
|
const generatedObjectInputTypeStrings = generateObjectInputTypeStrings(
|
|
62
66
|
schemaAnalyzerResult.objectTypes,
|
|
@@ -69,6 +73,7 @@ export async function generateSchema(
|
|
|
69
73
|
const generatedSchemaRootTypeStrings = generateSchemaRootTypeStrings(
|
|
70
74
|
generatedQueriesMutations,
|
|
71
75
|
generatedTypeNameQuery,
|
|
76
|
+
generatedSearchQuery,
|
|
72
77
|
)
|
|
73
78
|
|
|
74
79
|
const generatedSchemaString = `schema {
|
|
@@ -92,7 +97,8 @@ ${generatedSchemaRootTypeStrings}`
|
|
|
92
97
|
|
|
93
98
|
const generatedQueryResolvers: Record<
|
|
94
99
|
string,
|
|
95
|
-
QueryMutationResolver<EntryData | EntryData[] | string>
|
|
100
|
+
| QueryMutationResolver<EntryData | EntryData[] | string>
|
|
101
|
+
| SearchQueryResolver
|
|
96
102
|
> = {}
|
|
97
103
|
const generatedMutationResolvers: Record<
|
|
98
104
|
string,
|
|
@@ -112,12 +118,17 @@ ${generatedSchemaRootTypeStrings}`
|
|
|
112
118
|
}
|
|
113
119
|
generatedQueryResolvers[generatedTypeNameQuery.name] =
|
|
114
120
|
generatedTypeNameQuery.resolver
|
|
121
|
+
if (generatedSearchQuery !== undefined) {
|
|
122
|
+
generatedQueryResolvers[generatedSearchQuery.name] =
|
|
123
|
+
generatedSearchQuery.resolver
|
|
124
|
+
}
|
|
115
125
|
|
|
116
126
|
const allGeneratedResolvers: Record<
|
|
117
127
|
string,
|
|
118
128
|
| Record<
|
|
119
129
|
string,
|
|
120
130
|
| QueryMutationResolver<EntryData | EntryData[] | string>
|
|
131
|
+
| SearchQueryResolver
|
|
121
132
|
| GraphQLTypeResolver<unknown, ApolloContext>
|
|
122
133
|
| ContextInjectionResolver
|
|
123
134
|
| FieldResolver
|
|
@@ -147,6 +158,7 @@ ${generatedSchemaRootTypeStrings}`
|
|
|
147
158
|
generatedIdInputTypeStrings.join('\n'),
|
|
148
159
|
generatedObjectInputTypeStrings.join('\n'),
|
|
149
160
|
generatedUnionInputTypeStrings.join('\n'),
|
|
161
|
+
generatedSearchQuery?.typeDefinitionString ?? '',
|
|
150
162
|
].filter((typeDef) => typeDef.length > 0)
|
|
151
163
|
|
|
152
164
|
return {
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
GeneratedQuery,
|
|
3
3
|
GeneratedSchema,
|
|
4
|
+
GeneratedSearchQuery,
|
|
4
5
|
} from './queries-mutations-generator.ts'
|
|
5
6
|
|
|
6
7
|
export function generateSchemaRootTypeStrings(
|
|
7
8
|
generatedSchemas: GeneratedSchema[],
|
|
8
9
|
typeQuery: GeneratedQuery,
|
|
10
|
+
searchQuery: GeneratedSearchQuery | undefined,
|
|
9
11
|
): string {
|
|
10
12
|
return (
|
|
11
13
|
`type Query {\n` +
|
|
@@ -19,6 +21,7 @@ export function generateSchemaRootTypeStrings(
|
|
|
19
21
|
'\n' +
|
|
20
22
|
` ${typeQuery.schemaString}` +
|
|
21
23
|
'\n' +
|
|
24
|
+
(searchQuery !== undefined ? ` ${searchQuery.schemaString}\n` : '') +
|
|
22
25
|
'}\n\n' +
|
|
23
26
|
'type Mutation {\n' +
|
|
24
27
|
generatedSchemas
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { GraphQLField, GraphQLNamedType } from 'graphql'
|
|
2
|
+
|
|
3
|
+
export const ENTRY_DIRECTIVE_NAME = 'Entry'
|
|
4
|
+
export const SEARCHABLE_DIRECTIVE_NAME = 'Searchable'
|
|
5
|
+
|
|
6
|
+
export function hasDirective(
|
|
7
|
+
typeOrField: GraphQLNamedType | GraphQLField<unknown, unknown>,
|
|
8
|
+
directiveName: string,
|
|
9
|
+
): boolean {
|
|
10
|
+
return (
|
|
11
|
+
typeOrField.astNode?.directives?.some(
|
|
12
|
+
(directive) => directive.name.value === directiveName,
|
|
13
|
+
) ?? false
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -14,7 +14,8 @@ import {
|
|
|
14
14
|
getUnionTypeNameFromFieldValue,
|
|
15
15
|
getUnionValue,
|
|
16
16
|
} from './union-type-util.ts'
|
|
17
|
-
import {
|
|
17
|
+
import { ENTRY_DIRECTIVE_NAME, hasDirective } from './directive-util.ts'
|
|
18
|
+
import { isUnionOfEntryTypes } from './entry-type-util.ts'
|
|
18
19
|
import { createError, ErrorCode } from '../errors.ts'
|
|
19
20
|
import { EntryData } from '@commitspark/git-adapter'
|
|
20
21
|
import { isEntryData } from '../util.ts'
|
|
@@ -218,7 +219,10 @@ export async function getReferencedEntryIds(
|
|
|
218
219
|
}
|
|
219
220
|
|
|
220
221
|
if (isObjectType(type)) {
|
|
221
|
-
if (
|
|
222
|
+
if (
|
|
223
|
+
type.name !== rootType.name &&
|
|
224
|
+
hasDirective(type, ENTRY_DIRECTIVE_NAME)
|
|
225
|
+
) {
|
|
222
226
|
const referenceId = await getValidatedReferenceId(
|
|
223
227
|
context,
|
|
224
228
|
fieldName,
|
|
@@ -1,21 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
GraphQLNullableType,
|
|
3
|
-
GraphQLObjectType,
|
|
4
3
|
GraphQLUnionType,
|
|
5
4
|
isListType,
|
|
6
5
|
isNonNullType,
|
|
7
6
|
isObjectType,
|
|
8
7
|
isUnionType,
|
|
9
8
|
} from 'graphql'
|
|
10
|
-
|
|
11
|
-
export function hasEntryDirective(type: GraphQLObjectType): boolean {
|
|
12
|
-
return (
|
|
13
|
-
!!type.astNode &&
|
|
14
|
-
type.astNode.directives?.find(
|
|
15
|
-
(directive) => directive.name.value === 'Entry',
|
|
16
|
-
) !== undefined
|
|
17
|
-
)
|
|
18
|
-
}
|
|
9
|
+
import { ENTRY_DIRECTIVE_NAME, hasDirective } from './directive-util.ts'
|
|
19
10
|
|
|
20
11
|
export function isUnionOfEntryTypes(type: GraphQLUnionType): boolean {
|
|
21
12
|
return type
|
|
@@ -33,7 +24,7 @@ export function buildsOnTypeWithEntryDirective(
|
|
|
33
24
|
} else if (isUnionType(type)) {
|
|
34
25
|
return isUnionOfEntryTypes(type)
|
|
35
26
|
} else if (isObjectType(type)) {
|
|
36
|
-
return
|
|
27
|
+
return hasDirective(type, ENTRY_DIRECTIVE_NAME)
|
|
37
28
|
}
|
|
38
29
|
return false
|
|
39
30
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GraphQLObjectType,
|
|
3
|
+
GraphQLOutputType,
|
|
4
|
+
GraphQLSchema,
|
|
5
|
+
isListType,
|
|
6
|
+
isNonNullType,
|
|
7
|
+
isObjectType,
|
|
8
|
+
isScalarType,
|
|
9
|
+
isUnionType,
|
|
10
|
+
} from 'graphql'
|
|
11
|
+
import { Entry } from '@commitspark/git-adapter'
|
|
12
|
+
import { SearchableFieldValue } from '@commitspark/search-adapter'
|
|
13
|
+
import {
|
|
14
|
+
ENTRY_DIRECTIVE_NAME,
|
|
15
|
+
hasDirective,
|
|
16
|
+
SEARCHABLE_DIRECTIVE_NAME,
|
|
17
|
+
} from './directive-util.ts'
|
|
18
|
+
import { isUnionOfEntryTypes } from './entry-type-util.ts'
|
|
19
|
+
import { getUnionTypeNameFromFieldValue } from './union-type-util.ts'
|
|
20
|
+
|
|
21
|
+
// Returns every non-empty string value of searchable fields. Values that do not match the schema are skipped instead
|
|
22
|
+
// of failing the search.
|
|
23
|
+
export function extractSearchableFieldValues(
|
|
24
|
+
schema: GraphQLSchema,
|
|
25
|
+
entries: Iterable<Entry>,
|
|
26
|
+
): SearchableFieldValue[] {
|
|
27
|
+
const searchableFieldValues: SearchableFieldValue[] = []
|
|
28
|
+
for (const entry of entries) {
|
|
29
|
+
const entryType = schema.getType(entry.metadata.type)
|
|
30
|
+
if (!isObjectType(entryType) || !isRecord(entry.data)) {
|
|
31
|
+
continue
|
|
32
|
+
}
|
|
33
|
+
const addValue = (fieldPath: string, value: string): void => {
|
|
34
|
+
searchableFieldValues.push({
|
|
35
|
+
entryId: entry.id,
|
|
36
|
+
entryType: entryType.name,
|
|
37
|
+
fieldPath: fieldPath,
|
|
38
|
+
value: value,
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
collectFromObject(schema, entryType, entry.data, '', addValue)
|
|
42
|
+
}
|
|
43
|
+
return searchableFieldValues
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function collectFromObject(
|
|
47
|
+
schema: GraphQLSchema,
|
|
48
|
+
objectType: GraphQLObjectType,
|
|
49
|
+
data: Record<string, unknown>,
|
|
50
|
+
path: string,
|
|
51
|
+
addValue: (fieldPath: string, value: string) => void,
|
|
52
|
+
): void {
|
|
53
|
+
const isTypeSearchable = hasDirective(objectType, SEARCHABLE_DIRECTIVE_NAME)
|
|
54
|
+
for (const field of Object.values(objectType.getFields())) {
|
|
55
|
+
const value = data[field.name]
|
|
56
|
+
if (value === undefined || value === null) {
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
collectFromValue(
|
|
60
|
+
schema,
|
|
61
|
+
field.type,
|
|
62
|
+
value,
|
|
63
|
+
path === '' ? field.name : `${path}.${field.name}`,
|
|
64
|
+
isTypeSearchable || hasDirective(field, SEARCHABLE_DIRECTIVE_NAME),
|
|
65
|
+
addValue,
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function collectFromValue(
|
|
71
|
+
schema: GraphQLSchema,
|
|
72
|
+
type: GraphQLOutputType,
|
|
73
|
+
value: unknown,
|
|
74
|
+
path: string,
|
|
75
|
+
isSearchable: boolean,
|
|
76
|
+
addValue: (fieldPath: string, value: string) => void,
|
|
77
|
+
): void {
|
|
78
|
+
if (isNonNullType(type)) {
|
|
79
|
+
collectFromValue(schema, type.ofType, value, path, isSearchable, addValue)
|
|
80
|
+
} else if (isListType(type)) {
|
|
81
|
+
if (!Array.isArray(value)) {
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
value.forEach((item, index) =>
|
|
85
|
+
collectFromValue(
|
|
86
|
+
schema,
|
|
87
|
+
type.ofType,
|
|
88
|
+
item,
|
|
89
|
+
`${path}[${index}]`,
|
|
90
|
+
isSearchable,
|
|
91
|
+
addValue,
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
} else if (isScalarType(type)) {
|
|
95
|
+
if (
|
|
96
|
+
isSearchable &&
|
|
97
|
+
type.name === 'String' &&
|
|
98
|
+
typeof value === 'string' &&
|
|
99
|
+
value.trim() !== ''
|
|
100
|
+
) {
|
|
101
|
+
addValue(path, value)
|
|
102
|
+
}
|
|
103
|
+
} else if (isObjectType(type)) {
|
|
104
|
+
// references to other entries are not followed, as each entry is indexed with its own content only
|
|
105
|
+
if (!hasDirective(type, ENTRY_DIRECTIVE_NAME) && isRecord(value)) {
|
|
106
|
+
collectFromObject(schema, type, value, path, addValue)
|
|
107
|
+
}
|
|
108
|
+
} else if (isUnionType(type)) {
|
|
109
|
+
if (isUnionOfEntryTypes(type) || !isRecord(value)) {
|
|
110
|
+
return
|
|
111
|
+
}
|
|
112
|
+
// non-entry union values are stored wrapped in an object keyed by their concrete type name, which is kept in the
|
|
113
|
+
// path so that the path matches stored data and identifies the concrete type
|
|
114
|
+
const concreteType = schema.getType(getUnionTypeNameFromFieldValue(value))
|
|
115
|
+
const concreteValue = value[concreteType?.name ?? '']
|
|
116
|
+
if (isObjectType(concreteType) && isRecord(concreteValue)) {
|
|
117
|
+
collectFromObject(
|
|
118
|
+
schema,
|
|
119
|
+
concreteType,
|
|
120
|
+
concreteValue,
|
|
121
|
+
`${path}.${concreteType.name}`,
|
|
122
|
+
addValue,
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
129
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
130
|
+
}
|
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
getNamedType,
|
|
3
|
+
GraphQLSchema,
|
|
4
|
+
GraphQLUnionType,
|
|
5
|
+
isObjectType,
|
|
6
|
+
isScalarType,
|
|
7
|
+
Kind,
|
|
8
|
+
} from 'graphql'
|
|
9
|
+
import {
|
|
10
|
+
ENTRY_DIRECTIVE_NAME,
|
|
11
|
+
hasDirective,
|
|
12
|
+
SEARCHABLE_DIRECTIVE_NAME,
|
|
13
|
+
} from './schema-utils/directive-util.ts'
|
|
2
14
|
|
|
3
15
|
function checkUnionMembersConsistentUseOfEntryDirective(
|
|
4
16
|
schema: GraphQLSchema,
|
|
@@ -12,18 +24,35 @@ function checkUnionMembersConsistentUseOfEntryDirective(
|
|
|
12
24
|
const innerTypes = (type as GraphQLUnionType).getTypes()
|
|
13
25
|
|
|
14
26
|
const numberUnionMembersWithEntryDirective = innerTypes.filter(
|
|
15
|
-
(innerType) =>
|
|
16
|
-
!!innerType.astNode &&
|
|
17
|
-
innerType.astNode.directives?.find(
|
|
18
|
-
(directive) => directive.name.value === 'Entry',
|
|
19
|
-
) !== undefined,
|
|
27
|
+
(innerType) => hasDirective(innerType, ENTRY_DIRECTIVE_NAME),
|
|
20
28
|
).length
|
|
21
29
|
|
|
22
30
|
if (
|
|
23
31
|
numberUnionMembersWithEntryDirective !== 0 &&
|
|
24
32
|
numberUnionMembersWithEntryDirective !== innerTypes.length
|
|
25
33
|
) {
|
|
26
|
-
return `Either all union members of "${type.name}" must have "
|
|
34
|
+
return `Either all union members of "${type.name}" must have "@${ENTRY_DIRECTIVE_NAME}" directive or none.`
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return ''
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function checkSearchableDirectiveOnlyOnStringFields(
|
|
42
|
+
schema: GraphQLSchema,
|
|
43
|
+
): string {
|
|
44
|
+
for (const type of Object.values(schema.getTypeMap())) {
|
|
45
|
+
if (!isObjectType(type) || type.name.startsWith('__')) {
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
for (const field of Object.values(type.getFields())) {
|
|
49
|
+
if (!hasDirective(field, SEARCHABLE_DIRECTIVE_NAME)) {
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
const namedType = getNamedType(field.type)
|
|
53
|
+
if (!isScalarType(namedType) || namedType.name !== 'String') {
|
|
54
|
+
return `Field "${type.name}.${field.name}" must be of type "String" or a list of "String" to use "@${SEARCHABLE_DIRECTIVE_NAME}" directive.`
|
|
55
|
+
}
|
|
27
56
|
}
|
|
28
57
|
}
|
|
29
58
|
|
|
@@ -33,5 +62,6 @@ function checkUnionMembersConsistentUseOfEntryDirective(
|
|
|
33
62
|
export function getValidationResult(schema: GraphQLSchema): string[] {
|
|
34
63
|
const results = []
|
|
35
64
|
results.push(checkUnionMembersConsistentUseOfEntryDirective(schema))
|
|
65
|
+
results.push(checkSearchableDirectiveOnlyOnStringFields(schema))
|
|
36
66
|
return results.filter((result) => result !== '')
|
|
37
67
|
}
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
VariableValues,
|
|
8
8
|
} from './client.ts'
|
|
9
9
|
import { GitAdapter } from '@commitspark/git-adapter'
|
|
10
|
+
import { SearchAdapter } from '@commitspark/search-adapter'
|
|
10
11
|
import { ErrorCode, ErrorMetadata } from './graphql/errors.ts'
|
|
11
12
|
import { createCacheHandler } from './persistence/cache.ts'
|
|
12
13
|
|
|
@@ -21,12 +22,32 @@ interface Client {
|
|
|
21
22
|
getSchema(ref: string): Promise<SchemaResponse>
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
interface ClientOptions {
|
|
26
|
+
/**
|
|
27
|
+
* Enables query `_search` when set.
|
|
28
|
+
*/
|
|
29
|
+
searchAdapter?: SearchAdapter
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
Client,
|
|
34
|
+
ClientOptions,
|
|
35
|
+
GraphQLResponse,
|
|
36
|
+
SchemaResponse,
|
|
37
|
+
ErrorCode,
|
|
38
|
+
ErrorMetadata,
|
|
39
|
+
}
|
|
25
40
|
|
|
26
|
-
export async function createClient(
|
|
41
|
+
export async function createClient(
|
|
42
|
+
gitAdapter: GitAdapter,
|
|
43
|
+
options: ClientOptions = {},
|
|
44
|
+
): Promise<Client> {
|
|
27
45
|
const repositoryCache = createCacheHandler()
|
|
46
|
+
const searchAdapter = options.searchAdapter
|
|
28
47
|
return {
|
|
29
|
-
postGraphQL: (...args) =>
|
|
30
|
-
|
|
48
|
+
postGraphQL: (...args) =>
|
|
49
|
+
postGraphQL(gitAdapter, searchAdapter, repositoryCache, ...args),
|
|
50
|
+
getSchema: (...args) =>
|
|
51
|
+
getSchema(gitAdapter, searchAdapter, repositoryCache, ...args),
|
|
31
52
|
}
|
|
32
53
|
}
|