@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.
Files changed (113) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +84 -1
  3. package/dist/cjs/client.d.ts +4 -2
  4. package/dist/cjs/client.js +4 -2
  5. package/dist/cjs/client.js.map +1 -1
  6. package/dist/cjs/graphql/errors.d.ts +3 -2
  7. package/dist/cjs/graphql/errors.js.map +1 -1
  8. package/dist/cjs/graphql/input-type-generator.js +2 -1
  9. package/dist/cjs/graphql/input-type-generator.js.map +1 -1
  10. package/dist/cjs/graphql/queries-mutations-generator.d.ts +8 -1
  11. package/dist/cjs/graphql/queries-mutations-generator.js +19 -0
  12. package/dist/cjs/graphql/queries-mutations-generator.js.map +1 -1
  13. package/dist/cjs/graphql/resolvers/field-resolvers/field-default-value-resolver.js +2 -1
  14. package/dist/cjs/graphql/resolvers/field-resolvers/field-default-value-resolver.js.map +1 -1
  15. package/dist/cjs/graphql/resolvers/query-mutation-resolvers/query-search-resolver.d.ts +2 -0
  16. package/dist/cjs/graphql/resolvers/query-mutation-resolvers/query-search-resolver.js +74 -0
  17. package/dist/cjs/graphql/resolvers/query-mutation-resolvers/query-search-resolver.js.map +1 -0
  18. package/dist/cjs/graphql/resolvers/types.d.ts +6 -0
  19. package/dist/cjs/graphql/schema-analyzer.js +2 -1
  20. package/dist/cjs/graphql/schema-analyzer.js.map +1 -1
  21. package/dist/cjs/graphql/schema-generator.js +7 -1
  22. package/dist/cjs/graphql/schema-generator.js.map +1 -1
  23. package/dist/cjs/graphql/schema-root-type-generator.d.ts +2 -2
  24. package/dist/cjs/graphql/schema-root-type-generator.js +2 -1
  25. package/dist/cjs/graphql/schema-root-type-generator.js.map +1 -1
  26. package/dist/cjs/graphql/schema-utils/directive-util.d.ts +4 -0
  27. package/dist/cjs/graphql/schema-utils/directive-util.js +10 -0
  28. package/dist/cjs/graphql/schema-utils/directive-util.js.map +1 -0
  29. package/dist/cjs/graphql/schema-utils/entry-reference-util.js +3 -1
  30. package/dist/cjs/graphql/schema-utils/entry-reference-util.js.map +1 -1
  31. package/dist/cjs/graphql/schema-utils/entry-type-util.d.ts +1 -2
  32. package/dist/cjs/graphql/schema-utils/entry-type-util.js +2 -6
  33. package/dist/cjs/graphql/schema-utils/entry-type-util.js.map +1 -1
  34. package/dist/cjs/graphql/schema-utils/searchable-field-util.d.ts +4 -0
  35. package/dist/cjs/graphql/schema-utils/searchable-field-util.js +79 -0
  36. package/dist/cjs/graphql/schema-utils/searchable-field-util.js.map +1 -0
  37. package/dist/cjs/graphql/schema-validator.js +21 -3
  38. package/dist/cjs/graphql/schema-validator.js.map +1 -1
  39. package/dist/cjs/index.d.ts +9 -2
  40. package/dist/cjs/index.js +4 -3
  41. package/dist/cjs/index.js.map +1 -1
  42. package/dist/esm/client.js +4 -2
  43. package/dist/esm/client.js.map +1 -1
  44. package/dist/esm/graphql/errors.js.map +1 -1
  45. package/dist/esm/graphql/input-type-generator.js +3 -2
  46. package/dist/esm/graphql/input-type-generator.js.map +1 -1
  47. package/dist/esm/graphql/queries-mutations-generator.js +18 -0
  48. package/dist/esm/graphql/queries-mutations-generator.js.map +1 -1
  49. package/dist/esm/graphql/resolvers/field-resolvers/field-default-value-resolver.js +3 -2
  50. package/dist/esm/graphql/resolvers/field-resolvers/field-default-value-resolver.js.map +1 -1
  51. package/dist/esm/graphql/resolvers/query-mutation-resolvers/query-search-resolver.js +70 -0
  52. package/dist/esm/graphql/resolvers/query-mutation-resolvers/query-search-resolver.js.map +1 -0
  53. package/dist/esm/graphql/schema-analyzer.js +2 -1
  54. package/dist/esm/graphql/schema-analyzer.js.map +1 -1
  55. package/dist/esm/graphql/schema-generator.js +8 -2
  56. package/dist/esm/graphql/schema-generator.js.map +1 -1
  57. package/dist/esm/graphql/schema-root-type-generator.js +2 -1
  58. package/dist/esm/graphql/schema-root-type-generator.js.map +1 -1
  59. package/dist/esm/graphql/schema-utils/directive-util.js +6 -0
  60. package/dist/esm/graphql/schema-utils/directive-util.js.map +1 -0
  61. package/dist/esm/graphql/schema-utils/entry-reference-util.js +4 -2
  62. package/dist/esm/graphql/schema-utils/entry-reference-util.js.map +1 -1
  63. package/dist/esm/graphql/schema-utils/entry-type-util.js +2 -5
  64. package/dist/esm/graphql/schema-utils/entry-type-util.js.map +1 -1
  65. package/dist/esm/graphql/schema-utils/searchable-field-util.js +76 -0
  66. package/dist/esm/graphql/schema-utils/searchable-field-util.js.map +1 -0
  67. package/dist/esm/graphql/schema-validator.js +22 -4
  68. package/dist/esm/graphql/schema-validator.js.map +1 -1
  69. package/dist/esm/index.js +4 -3
  70. package/dist/esm/index.js.map +1 -1
  71. package/dist/types/client.d.ts +4 -2
  72. package/dist/types/client.d.ts.map +1 -1
  73. package/dist/types/graphql/errors.d.ts +3 -2
  74. package/dist/types/graphql/errors.d.ts.map +1 -1
  75. package/dist/types/graphql/input-type-generator.d.ts.map +1 -1
  76. package/dist/types/graphql/queries-mutations-generator.d.ts +8 -1
  77. package/dist/types/graphql/queries-mutations-generator.d.ts.map +1 -1
  78. package/dist/types/graphql/resolvers/field-resolvers/field-default-value-resolver.d.ts.map +1 -1
  79. package/dist/types/graphql/resolvers/query-mutation-resolvers/query-search-resolver.d.ts +3 -0
  80. package/dist/types/graphql/resolvers/query-mutation-resolvers/query-search-resolver.d.ts.map +1 -0
  81. package/dist/types/graphql/resolvers/types.d.ts +6 -0
  82. package/dist/types/graphql/resolvers/types.d.ts.map +1 -1
  83. package/dist/types/graphql/schema-analyzer.d.ts.map +1 -1
  84. package/dist/types/graphql/schema-generator.d.ts.map +1 -1
  85. package/dist/types/graphql/schema-root-type-generator.d.ts +2 -2
  86. package/dist/types/graphql/schema-root-type-generator.d.ts.map +1 -1
  87. package/dist/types/graphql/schema-utils/directive-util.d.ts +5 -0
  88. package/dist/types/graphql/schema-utils/directive-util.d.ts.map +1 -0
  89. package/dist/types/graphql/schema-utils/entry-reference-util.d.ts.map +1 -1
  90. package/dist/types/graphql/schema-utils/entry-type-util.d.ts +1 -2
  91. package/dist/types/graphql/schema-utils/entry-type-util.d.ts.map +1 -1
  92. package/dist/types/graphql/schema-utils/searchable-field-util.d.ts +5 -0
  93. package/dist/types/graphql/schema-utils/searchable-field-util.d.ts.map +1 -0
  94. package/dist/types/graphql/schema-validator.d.ts.map +1 -1
  95. package/dist/types/index.d.ts +9 -2
  96. package/dist/types/index.d.ts.map +1 -1
  97. package/package.json +2 -1
  98. package/src/client.ts +6 -0
  99. package/src/graphql/errors.ts +3 -2
  100. package/src/graphql/input-type-generator.ts +5 -4
  101. package/src/graphql/queries-mutations-generator.ts +31 -1
  102. package/src/graphql/resolvers/field-resolvers/field-default-value-resolver.ts +5 -4
  103. package/src/graphql/resolvers/query-mutation-resolvers/query-search-resolver.ts +110 -0
  104. package/src/graphql/resolvers/types.ts +8 -0
  105. package/src/graphql/schema-analyzer.ts +6 -1
  106. package/src/graphql/schema-generator.ts +13 -1
  107. package/src/graphql/schema-root-type-generator.ts +3 -0
  108. package/src/graphql/schema-utils/directive-util.ts +15 -0
  109. package/src/graphql/schema-utils/entry-reference-util.ts +6 -2
  110. package/src/graphql/schema-utils/entry-type-util.ts +2 -11
  111. package/src/graphql/schema-utils/searchable-field-util.ts +130 -0
  112. package/src/graphql/schema-validator.ts +37 -7
  113. 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
- buildsOnTypeWithEntryDirective,
19
- hasEntryDirective,
20
- } from './schema-utils/entry-type-util.ts'
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 (hasEntryDirective(type)) {
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 { QueryMutationResolver } from './resolvers/types.ts'
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
- buildsOnTypeWithEntryDirective,
12
- hasEntryDirective,
13
- } from '../../schema-utils/entry-type-util.ts'
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 (hasEntryDirective(context.currentType)) {
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(schema, objectType, 'Entry')?.[0]
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 { hasEntryDirective, isUnionOfEntryTypes } from './entry-type-util.ts'
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 (type.name !== rootType.name && hasEntryDirective(type)) {
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 hasEntryDirective(type)
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 { GraphQLSchema, GraphQLUnionType, Kind } from 'graphql'
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 "@Entry" directive or none.`
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
- export { Client, GraphQLResponse, SchemaResponse, ErrorCode, ErrorMetadata }
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(gitAdapter: GitAdapter): Promise<Client> {
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) => postGraphQL(gitAdapter, repositoryCache, ...args),
30
- getSchema: (...args) => getSchema(gitAdapter, repositoryCache, ...args),
48
+ postGraphQL: (...args) =>
49
+ postGraphQL(gitAdapter, searchAdapter, repositoryCache, ...args),
50
+ getSchema: (...args) =>
51
+ getSchema(gitAdapter, searchAdapter, repositoryCache, ...args),
31
52
  }
32
53
  }