@commitspark/graphql-api 2.0.0-beta.4 → 2.0.0-beta.5

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 (154) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +147 -80
  3. package/dist/cjs/graphql/errors.d.ts +3 -2
  4. package/dist/cjs/graphql/errors.d.ts.map +1 -1
  5. package/dist/cjs/graphql/errors.js +11 -1
  6. package/dist/cjs/graphql/errors.js.map +1 -1
  7. package/dist/cjs/graphql/input-type-generator.d.ts.map +1 -1
  8. package/dist/cjs/graphql/input-type-generator.js +5 -10
  9. package/dist/cjs/graphql/input-type-generator.js.map +1 -1
  10. package/dist/cjs/graphql/queries-mutations-generator.d.ts +5 -5
  11. package/dist/cjs/graphql/queries-mutations-generator.d.ts.map +1 -1
  12. package/dist/cjs/graphql/queries-mutations-generator.js +45 -7
  13. package/dist/cjs/graphql/queries-mutations-generator.js.map +1 -1
  14. package/dist/cjs/graphql/resolvers/commit-message.d.ts +11 -0
  15. package/dist/cjs/graphql/resolvers/commit-message.d.ts.map +1 -0
  16. package/dist/cjs/graphql/resolvers/commit-message.js +22 -0
  17. package/dist/cjs/graphql/resolvers/commit-message.js.map +1 -0
  18. package/dist/cjs/graphql/resolvers/field-resolvers/entry-reference-resolver.js +2 -2
  19. package/dist/cjs/graphql/resolvers/field-resolvers/entry-reference-resolver.js.map +1 -1
  20. package/dist/cjs/graphql/resolvers/field-resolvers/field-default-value-resolver.js +3 -3
  21. package/dist/cjs/graphql/resolvers/field-resolvers/field-default-value-resolver.js.map +1 -1
  22. package/dist/cjs/graphql/resolvers/field-resolvers/version-resolver.d.ts +3 -0
  23. package/dist/cjs/graphql/resolvers/field-resolvers/version-resolver.d.ts.map +1 -0
  24. package/dist/cjs/graphql/resolvers/field-resolvers/version-resolver.js +7 -0
  25. package/dist/cjs/graphql/resolvers/field-resolvers/version-resolver.js.map +1 -0
  26. package/dist/cjs/graphql/resolvers/object-type-field-default-value-resolver-generator.js +1 -1
  27. package/dist/cjs/graphql/resolvers/object-type-field-default-value-resolver-generator.js.map +1 -1
  28. package/dist/cjs/graphql/resolvers/query-mutation-resolvers/mutation-create-resolver.d.ts.map +1 -1
  29. package/dist/cjs/graphql/resolvers/query-mutation-resolvers/mutation-create-resolver.js +34 -28
  30. package/dist/cjs/graphql/resolvers/query-mutation-resolvers/mutation-create-resolver.js.map +1 -1
  31. package/dist/cjs/graphql/resolvers/query-mutation-resolvers/mutation-delete-resolver.d.ts.map +1 -1
  32. package/dist/cjs/graphql/resolvers/query-mutation-resolvers/mutation-delete-resolver.js +28 -23
  33. package/dist/cjs/graphql/resolvers/query-mutation-resolvers/mutation-delete-resolver.js.map +1 -1
  34. package/dist/cjs/graphql/resolvers/query-mutation-resolvers/mutation-update-resolver.d.ts.map +1 -1
  35. package/dist/cjs/graphql/resolvers/query-mutation-resolvers/mutation-update-resolver.js +25 -18
  36. package/dist/cjs/graphql/resolvers/query-mutation-resolvers/mutation-update-resolver.js.map +1 -1
  37. package/dist/cjs/graphql/resolvers/query-mutation-resolvers/query-search-resolver.js +5 -5
  38. package/dist/cjs/graphql/resolvers/query-mutation-resolvers/query-search-resolver.js.map +1 -1
  39. package/dist/cjs/graphql/resolvers/query-mutation-resolvers/union-type-resolver.js +1 -1
  40. package/dist/cjs/graphql/resolvers/query-mutation-resolvers/union-type-resolver.js.map +1 -1
  41. package/dist/cjs/graphql/resolvers/types.d.ts +9 -1
  42. package/dist/cjs/graphql/resolvers/types.d.ts.map +1 -1
  43. package/dist/cjs/graphql/schema-generator.d.ts.map +1 -1
  44. package/dist/cjs/graphql/schema-generator.js +9 -8
  45. package/dist/cjs/graphql/schema-generator.js.map +1 -1
  46. package/dist/cjs/graphql/schema-utils/entry-reference-util.js +7 -7
  47. package/dist/cjs/graphql/schema-utils/entry-reference-util.js.map +1 -1
  48. package/dist/cjs/graphql/schema-utils/union-type-util.js +2 -2
  49. package/dist/cjs/graphql/schema-utils/union-type-util.js.map +1 -1
  50. package/dist/cjs/graphql/schema-validator.d.ts.map +1 -1
  51. package/dist/cjs/graphql/schema-validator.js +16 -0
  52. package/dist/cjs/graphql/schema-validator.js.map +1 -1
  53. package/dist/cjs/index.js +3 -0
  54. package/dist/cjs/index.js.map +1 -1
  55. package/dist/cjs/persistence/cache.d.ts +3 -0
  56. package/dist/cjs/persistence/cache.d.ts.map +1 -1
  57. package/dist/cjs/persistence/cache.js +8 -17
  58. package/dist/cjs/persistence/cache.js.map +1 -1
  59. package/dist/cjs/persistence/commit-with-retry.d.ts +16 -0
  60. package/dist/cjs/persistence/commit-with-retry.d.ts.map +1 -0
  61. package/dist/cjs/persistence/commit-with-retry.js +39 -0
  62. package/dist/cjs/persistence/commit-with-retry.js.map +1 -0
  63. package/dist/cjs/persistence/git-adapter-call.d.ts +10 -0
  64. package/dist/cjs/persistence/git-adapter-call.d.ts.map +1 -0
  65. package/dist/cjs/persistence/git-adapter-call.js +29 -0
  66. package/dist/cjs/persistence/git-adapter-call.js.map +1 -0
  67. package/dist/cjs/persistence/persistence.d.ts +11 -0
  68. package/dist/cjs/persistence/persistence.d.ts.map +1 -1
  69. package/dist/cjs/persistence/persistence.js +37 -2
  70. package/dist/cjs/persistence/persistence.js.map +1 -1
  71. package/dist/esm/graphql/errors.d.ts +3 -2
  72. package/dist/esm/graphql/errors.d.ts.map +1 -1
  73. package/dist/esm/graphql/errors.js +10 -0
  74. package/dist/esm/graphql/errors.js.map +1 -1
  75. package/dist/esm/graphql/input-type-generator.d.ts.map +1 -1
  76. package/dist/esm/graphql/input-type-generator.js +6 -11
  77. package/dist/esm/graphql/input-type-generator.js.map +1 -1
  78. package/dist/esm/graphql/queries-mutations-generator.d.ts +5 -5
  79. package/dist/esm/graphql/queries-mutations-generator.d.ts.map +1 -1
  80. package/dist/esm/graphql/queries-mutations-generator.js +43 -6
  81. package/dist/esm/graphql/queries-mutations-generator.js.map +1 -1
  82. package/dist/esm/graphql/resolvers/commit-message.d.ts +11 -0
  83. package/dist/esm/graphql/resolvers/commit-message.d.ts.map +1 -0
  84. package/dist/esm/graphql/resolvers/commit-message.js +19 -0
  85. package/dist/esm/graphql/resolvers/commit-message.js.map +1 -0
  86. package/dist/esm/graphql/resolvers/field-resolvers/entry-reference-resolver.js +3 -3
  87. package/dist/esm/graphql/resolvers/field-resolvers/entry-reference-resolver.js.map +1 -1
  88. package/dist/esm/graphql/resolvers/field-resolvers/field-default-value-resolver.js +4 -4
  89. package/dist/esm/graphql/resolvers/field-resolvers/field-default-value-resolver.js.map +1 -1
  90. package/dist/esm/graphql/resolvers/field-resolvers/version-resolver.d.ts +3 -0
  91. package/dist/esm/graphql/resolvers/field-resolvers/version-resolver.d.ts.map +1 -0
  92. package/dist/esm/graphql/resolvers/field-resolvers/version-resolver.js +3 -0
  93. package/dist/esm/graphql/resolvers/field-resolvers/version-resolver.js.map +1 -0
  94. package/dist/esm/graphql/resolvers/object-type-field-default-value-resolver-generator.js +2 -2
  95. package/dist/esm/graphql/resolvers/object-type-field-default-value-resolver-generator.js.map +1 -1
  96. package/dist/esm/graphql/resolvers/query-mutation-resolvers/mutation-create-resolver.d.ts.map +1 -1
  97. package/dist/esm/graphql/resolvers/query-mutation-resolvers/mutation-create-resolver.js +35 -29
  98. package/dist/esm/graphql/resolvers/query-mutation-resolvers/mutation-create-resolver.js.map +1 -1
  99. package/dist/esm/graphql/resolvers/query-mutation-resolvers/mutation-delete-resolver.d.ts.map +1 -1
  100. package/dist/esm/graphql/resolvers/query-mutation-resolvers/mutation-delete-resolver.js +30 -25
  101. package/dist/esm/graphql/resolvers/query-mutation-resolvers/mutation-delete-resolver.js.map +1 -1
  102. package/dist/esm/graphql/resolvers/query-mutation-resolvers/mutation-update-resolver.d.ts.map +1 -1
  103. package/dist/esm/graphql/resolvers/query-mutation-resolvers/mutation-update-resolver.js +27 -20
  104. package/dist/esm/graphql/resolvers/query-mutation-resolvers/mutation-update-resolver.js.map +1 -1
  105. package/dist/esm/graphql/resolvers/query-mutation-resolvers/query-search-resolver.js +6 -6
  106. package/dist/esm/graphql/resolvers/query-mutation-resolvers/query-search-resolver.js.map +1 -1
  107. package/dist/esm/graphql/resolvers/query-mutation-resolvers/union-type-resolver.js +2 -2
  108. package/dist/esm/graphql/resolvers/query-mutation-resolvers/union-type-resolver.js.map +1 -1
  109. package/dist/esm/graphql/resolvers/types.d.ts +9 -1
  110. package/dist/esm/graphql/resolvers/types.d.ts.map +1 -1
  111. package/dist/esm/graphql/schema-generator.d.ts.map +1 -1
  112. package/dist/esm/graphql/schema-generator.js +11 -10
  113. package/dist/esm/graphql/schema-generator.js.map +1 -1
  114. package/dist/esm/graphql/schema-utils/entry-reference-util.js +8 -8
  115. package/dist/esm/graphql/schema-utils/entry-reference-util.js.map +1 -1
  116. package/dist/esm/graphql/schema-utils/union-type-util.js +3 -3
  117. package/dist/esm/graphql/schema-utils/union-type-util.js.map +1 -1
  118. package/dist/esm/graphql/schema-validator.d.ts.map +1 -1
  119. package/dist/esm/graphql/schema-validator.js +17 -1
  120. package/dist/esm/graphql/schema-validator.js.map +1 -1
  121. package/dist/esm/index.js +2 -0
  122. package/dist/esm/index.js.map +1 -1
  123. package/dist/esm/persistence/cache.d.ts +3 -0
  124. package/dist/esm/persistence/cache.d.ts.map +1 -1
  125. package/dist/esm/persistence/cache.js +6 -15
  126. package/dist/esm/persistence/cache.js.map +1 -1
  127. package/dist/esm/persistence/commit-with-retry.d.ts +16 -0
  128. package/dist/esm/persistence/commit-with-retry.d.ts.map +1 -0
  129. package/dist/esm/persistence/commit-with-retry.js +35 -0
  130. package/dist/esm/persistence/commit-with-retry.js.map +1 -0
  131. package/dist/esm/persistence/git-adapter-call.d.ts +10 -0
  132. package/dist/esm/persistence/git-adapter-call.d.ts.map +1 -0
  133. package/dist/esm/persistence/git-adapter-call.js +24 -0
  134. package/dist/esm/persistence/git-adapter-call.js.map +1 -0
  135. package/dist/esm/persistence/persistence.d.ts +11 -0
  136. package/dist/esm/persistence/persistence.d.ts.map +1 -1
  137. package/dist/esm/persistence/persistence.js +36 -3
  138. package/dist/esm/persistence/persistence.js.map +1 -1
  139. package/package.json +8 -5
  140. package/src/graphql/errors.ts +2 -1
  141. package/src/graphql/input-type-generator.ts +5 -15
  142. package/src/graphql/queries-mutations-generator.ts +51 -10
  143. package/src/graphql/resolvers/commit-message.ts +34 -0
  144. package/src/graphql/resolvers/field-resolvers/version-resolver.ts +8 -0
  145. package/src/graphql/resolvers/query-mutation-resolvers/mutation-create-resolver.ts +40 -29
  146. package/src/graphql/resolvers/query-mutation-resolvers/mutation-delete-resolver.ts +44 -30
  147. package/src/graphql/resolvers/query-mutation-resolvers/mutation-update-resolver.ts +37 -18
  148. package/src/graphql/resolvers/types.ts +18 -1
  149. package/src/graphql/schema-generator.ts +12 -8
  150. package/src/graphql/schema-validator.ts +21 -0
  151. package/src/persistence/cache.ts +7 -13
  152. package/src/persistence/commit-with-retry.ts +47 -0
  153. package/src/persistence/git-adapter-call.ts +27 -0
  154. package/src/persistence/persistence.ts +50 -0
@@ -1,9 +1,14 @@
1
- import { findByTypeId } from '../../../persistence/persistence.ts'
1
+ import {
2
+ assertExpectedVersion,
3
+ findByTypeId,
4
+ } from '../../../persistence/persistence.ts'
2
5
  import { validateEntryReferences } from '../../schema-utils/entry-reference-util.ts'
3
6
  import { isObjectType } from 'graphql'
4
7
  import { EntryData } from '@commitspark/git-adapter'
5
8
  import { QueryMutationResolver } from '../types.ts'
6
9
  import { createError, ErrorCode } from '../../errors.ts'
10
+ import { getCommitMessage } from '../commit-message.ts'
11
+ import { commitWithRetry } from '../../../persistence/commit-with-retry.ts'
7
12
 
8
13
  function mergeData(
9
14
  existingEntryData: EntryData,
@@ -29,27 +34,41 @@ export const mutationUpdateResolver: QueryMutationResolver<EntryData> = async (
29
34
  {},
30
35
  )
31
36
  }
37
+ // narrowed type, as narrowing does not apply within the callback below
38
+ const entryType = context.type
39
+ const commitMessage = getCommitMessage(
40
+ args.commit,
41
+ 'Update',
42
+ context.type.name,
43
+ args.id,
44
+ )
32
45
 
33
- const existingEntry = await findByTypeId(context, context.type.name, args.id)
46
+ await commitWithRetry(context, async () => {
47
+ const existingEntry = await findByTypeId(
48
+ context,
49
+ context.type.name,
50
+ args.id,
51
+ )
52
+ await assertExpectedVersion(context, args.id, args.expectedVersion)
34
53
 
35
- const mergedData = mergeData(existingEntry.data ?? null, args.data ?? null)
36
- await validateEntryReferences(context.type, context, mergedData)
54
+ const mergedData = mergeData(existingEntry.data ?? null, args.data ?? null)
55
+ await validateEntryReferences(entryType, context, mergedData)
37
56
 
38
- const commit = await context.gitAdapter.createCommit({
39
- ref: context.branch,
40
- parentSha: context.getCurrentHash(),
41
- entries: [
42
- {
43
- id: existingEntry.id,
44
- // only the type is written, which removes metadata no longer in use from existing entries
45
- metadata: { type: existingEntry.metadata.type },
46
- data: mergedData,
47
- deletion: false,
48
- },
49
- ],
50
- message: args.commitMessage,
57
+ return {
58
+ ref: context.branch,
59
+ parentSha: context.getCurrentHash(),
60
+ entries: [
61
+ {
62
+ id: existingEntry.id,
63
+ // only the type is written, which removes metadata no longer in use from existing entries
64
+ metadata: { type: existingEntry.metadata.type },
65
+ data: mergedData,
66
+ deletion: false,
67
+ },
68
+ ],
69
+ ...commitMessage,
70
+ }
51
71
  })
52
- context.setCurrentHash(commit.commitHash)
53
72
 
54
73
  const updatedEntry = await findByTypeId(context, context.type.name, args.id)
55
74
  return { ...updatedEntry.data, id: updatedEntry.id }
@@ -8,6 +8,11 @@ import { EntryData } from '@commitspark/git-adapter'
8
8
  import { SearchHit } from '@commitspark/search-adapter'
9
9
  import { RecursiveArray } from './field-resolvers/types.ts'
10
10
 
11
+ export interface CommitInput {
12
+ message?: string | null
13
+ body?: string | null
14
+ }
15
+
11
16
  interface QueryMutationResolverContext extends ApolloContext {
12
17
  type: GraphQLNamedType
13
18
  }
@@ -15,7 +20,12 @@ interface QueryMutationResolverContext extends ApolloContext {
15
20
  export type QueryMutationResolver<ResultType> = GraphQLFieldResolver<
16
21
  unknown,
17
22
  QueryMutationResolverContext,
18
- { id: string; data?: EntryData; commitMessage: string },
23
+ {
24
+ id: string
25
+ data?: EntryData
26
+ commit?: CommitInput | null
27
+ expectedVersion?: string | null
28
+ },
19
29
  Promise<ResultType>
20
30
  >
21
31
 
@@ -49,3 +59,10 @@ export type ReferencedByResolver = GraphQLFieldResolver<
49
59
  Record<string, never>,
50
60
  Promise<string[]>
51
61
  >
62
+
63
+ export type VersionResolver = GraphQLFieldResolver<
64
+ EntryData & { id: string },
65
+ ApolloContext,
66
+ Record<string, never>,
67
+ Promise<string>
68
+ >
@@ -1,6 +1,7 @@
1
1
  import {
2
+ generateCommitInputType,
2
3
  generateQueriesAndMutations,
3
- generateReferencedByFields,
4
+ generateEntryFields,
4
5
  generateSearchQuery,
5
6
  generateTypeNameQuery,
6
7
  } from './queries-mutations-generator.ts'
@@ -29,6 +30,7 @@ import {
29
30
  ReferencedByResolver,
30
31
  SearchQueryResolver,
31
32
  UnionTypeResolver,
33
+ VersionResolver,
32
34
  } from './resolvers/types.ts'
33
35
  import { EntryData } from '@commitspark/git-adapter'
34
36
 
@@ -63,7 +65,7 @@ export async function generateSchema(
63
65
  const generatedTypeNameQuery = generateTypeNameQuery()
64
66
  const generatedSearchQuery =
65
67
  context.searchAdapter !== undefined ? generateSearchQuery() : undefined
66
- const generatedReferencedByFields = generateReferencedByFields(
68
+ const generatedEntryFields = generateEntryFields(
67
69
  schemaAnalyzerResult.entryDirectiveTypes,
68
70
  )
69
71
 
@@ -138,6 +140,7 @@ ${generatedSchemaRootTypeStrings}`
138
140
  | ContextInjectionResolver
139
141
  | FieldResolver
140
142
  | ReferencedByResolver
143
+ | VersionResolver
141
144
  >
142
145
  | UnionTypeResolverRecord
143
146
  > = {
@@ -157,21 +160,22 @@ ${generatedSchemaRootTypeStrings}`
157
160
  ...generatedObjectTypeFieldResolvers[typeName],
158
161
  }
159
162
  }
160
- for (const field of generatedReferencedByFields) {
161
- allGeneratedResolvers[field.typeName] = {
162
- ...(allGeneratedResolvers[field.typeName] ?? {}),
163
- [field.name]: field.resolver,
163
+ for (const entryFields of generatedEntryFields) {
164
+ allGeneratedResolvers[entryFields.typeName] = {
165
+ ...(allGeneratedResolvers[entryFields.typeName] ?? {}),
166
+ ...entryFields.resolvers,
164
167
  }
165
168
  }
166
169
 
167
170
  const typeDefs = [
168
171
  filteredOriginalSchemaString,
169
172
  generatedSchemaString,
173
+ generateCommitInputType(),
170
174
  generatedIdInputTypeStrings.join('\n'),
171
175
  generatedObjectInputTypeStrings.join('\n'),
172
176
  generatedUnionInputTypeStrings.join('\n'),
173
- generatedReferencedByFields
174
- .map((field) => field.typeExtensionString)
177
+ generatedEntryFields
178
+ .map((entryFields) => entryFields.typeExtensionString)
175
179
  .join('\n'),
176
180
  generatedSearchQuery?.typeDefinitionString ?? '',
177
181
  ].filter((typeDef) => typeDef.length > 0)
@@ -2,6 +2,7 @@ import {
2
2
  getNamedType,
3
3
  GraphQLSchema,
4
4
  GraphQLUnionType,
5
+ isNonNullType,
5
6
  isObjectType,
6
7
  isScalarType,
7
8
  Kind,
@@ -59,8 +60,28 @@ function checkSearchableDirectiveOnlyOnStringFields(
59
60
  return ''
60
61
  }
61
62
 
63
+ function checkEntryTypesHaveIdField(schema: GraphQLSchema): string {
64
+ for (const type of Object.values(schema.getTypeMap())) {
65
+ if (!isObjectType(type) || !hasDirective(type, ENTRY_DIRECTIVE_NAME)) {
66
+ continue
67
+ }
68
+ const idField = type.getFields()['id']
69
+ if (
70
+ idField === undefined ||
71
+ !isNonNullType(idField.type) ||
72
+ !isScalarType(idField.type.ofType) ||
73
+ idField.type.ofType.name !== 'ID'
74
+ ) {
75
+ return `Type "${type.name}" must have a field "id" of type "ID!" to use "@${ENTRY_DIRECTIVE_NAME}" directive.`
76
+ }
77
+ }
78
+
79
+ return ''
80
+ }
81
+
62
82
  export function getValidationResult(schema: GraphQLSchema): string[] {
63
83
  const results = []
84
+ results.push(checkEntryTypesHaveIdField(schema))
64
85
  results.push(checkUnionMembersConsistentUseOfEntryDirective(schema))
65
86
  results.push(checkSearchableDirectiveOnlyOnStringFields(schema))
66
87
  return results.filter((result) => result !== '')
@@ -1,6 +1,7 @@
1
- import { Entry, GitAdapterError } from '@commitspark/git-adapter'
1
+ import { Entry } from '@commitspark/git-adapter'
2
2
  import { ApolloContext } from '../client.ts'
3
3
  import { createError, ErrorCode } from '../graphql/errors.ts'
4
+ import { callGitAdapter } from './git-adapter-call.ts'
4
5
 
5
6
  type Ref = string
6
7
  type ContentHash = string
@@ -14,6 +15,8 @@ type InflightEntries = Map<Ref, Promise<EntriesRecord>>
14
15
  export interface EntriesRecord {
15
16
  byId: Map<string, Entry>
16
17
  byType: Map<string, Entry[]>
18
+ // content hash of each entry as reported by the Git adapter, exposed to callers as entry version
19
+ hashById: Map<string, ContentHash>
17
20
  }
18
21
 
19
22
  interface CommitCacheRecord {
@@ -113,7 +116,9 @@ const fetchAndCacheEntries = async (
113
116
 
114
117
  const entriesById = new Map<string, Entry>()
115
118
  const entriesByType = new Map<string, Entry[]>()
119
+ const hashesById = new Map<string, ContentHash>()
116
120
  for (const { id, hash } of entryHashes) {
121
+ hashesById.set(id, hash)
117
122
  const entry: Entry = {
118
123
  id: id,
119
124
  ...(contentByHash.get(hash) as EntryContent),
@@ -128,6 +133,7 @@ const fetchAndCacheEntries = async (
128
133
  entriesRecord: {
129
134
  byType: entriesByType,
130
135
  byId: entriesById,
136
+ hashById: hashesById,
131
137
  },
132
138
  contentHashes: [...contentByHash.keys()],
133
139
  }
@@ -174,18 +180,6 @@ const removeUnreferencedContent = (
174
180
  }
175
181
  }
176
182
 
177
- const callGitAdapter = async <T>(operation: () => Promise<T>): Promise<T> => {
178
- try {
179
- return await operation()
180
- } catch (err) {
181
- if (err instanceof GitAdapterError) {
182
- throw createError(err.message, err.code, {})
183
- }
184
- const message = err instanceof Error ? err.message : String(err)
185
- throw createError(message, ErrorCode.INTERNAL_ERROR, {})
186
- }
187
- }
188
-
189
183
  const getSchemaStringByRef = async (
190
184
  schemaCache: SchemaCache,
191
185
  cacheSize: number,
@@ -0,0 +1,47 @@
1
+ import {
2
+ CommitDraft,
3
+ ErrorCode as GitAdapterErrorCode,
4
+ GitAdapterError,
5
+ } from '@commitspark/git-adapter'
6
+ import { ApolloContext } from '../client.ts'
7
+ import { callGitAdapter, toGraphQLError } from './git-adapter-call.ts'
8
+
9
+ const MAX_COMMIT_ATTEMPTS = 3
10
+
11
+ /**
12
+ * Commits a draft that `createDraft` validates against the current commit of the request, guaranteeing that the
13
+ * draft is only committed on top of the exact commit it was validated against.
14
+ *
15
+ * Git adapters only move a branch if it still points to `parentSha` and otherwise throw `CONFLICT`. As most adapters
16
+ * can only check this for the branch as a whole, any concurrent commit to the branch between reading and committing
17
+ * leads to `CONFLICT`, even if it changed unrelated entries. In this case, the current commit of the request is moved
18
+ * to the new branch head and the draft is created and validated again, including expected entry versions and
19
+ * references, so that only actual conflicts are reported to the caller.
20
+ *
21
+ * Errors thrown by `createDraft` are not retried, as the draft was validated against the latest known state.
22
+ */
23
+ export const commitWithRetry = async (
24
+ context: ApolloContext,
25
+ createDraft: () => Promise<CommitDraft>,
26
+ ): Promise<void> => {
27
+ for (let attempt = 1; ; attempt++) {
28
+ const commitDraft = await createDraft()
29
+ try {
30
+ const commit = await context.gitAdapter.createCommit(commitDraft)
31
+ context.setCurrentHash(commit.commitHash)
32
+ return
33
+ } catch (err) {
34
+ const isConflict =
35
+ err instanceof GitAdapterError &&
36
+ err.code === GitAdapterErrorCode.CONFLICT
37
+ if (!isConflict || attempt >= MAX_COMMIT_ATTEMPTS) {
38
+ throw toGraphQLError(err)
39
+ }
40
+ }
41
+
42
+ const headCommitHash = await callGitAdapter(() =>
43
+ context.gitAdapter.getLatestCommitHash(context.branch),
44
+ )
45
+ context.setCurrentHash(headCommitHash)
46
+ }
47
+ }
@@ -0,0 +1,27 @@
1
+ import { GitAdapterError } from '@commitspark/git-adapter'
2
+ import { GraphQLError } from 'graphql'
3
+ import { createError, ErrorCode } from '../graphql/errors.ts'
4
+
5
+ /**
6
+ * Converts an error thrown by a Git adapter to a GraphQL error, keeping the error code of a `GitAdapterError`.
7
+ */
8
+ export const toGraphQLError = (err: unknown): GraphQLError => {
9
+ if (err instanceof GitAdapterError) {
10
+ return createError(err.message, err.code, {})
11
+ }
12
+ const message = err instanceof Error ? err.message : String(err)
13
+ return createError(message, ErrorCode.INTERNAL_ERROR, {})
14
+ }
15
+
16
+ /**
17
+ * Runs a Git adapter operation and converts thrown errors to GraphQL errors.
18
+ */
19
+ export const callGitAdapter = async <T>(
20
+ operation: () => Promise<T>,
21
+ ): Promise<T> => {
22
+ try {
23
+ return await operation()
24
+ } catch (err) {
25
+ throw toGraphQLError(err)
26
+ }
27
+ }
@@ -60,3 +60,53 @@ export async function findByTypeId(
60
60
 
61
61
  return entryById
62
62
  }
63
+
64
+ /**
65
+ * Returns the version of an entry, which is the content hash of the entry reported by the Git adapter. It changes
66
+ * whenever the stored entry changes.
67
+ */
68
+ export async function getEntryVersion(
69
+ context: ApolloContext,
70
+ id: string,
71
+ ): Promise<string> {
72
+ const entriesRecord = await context.repositoryCache.getEntriesRecord(
73
+ context,
74
+ context.getCurrentHash(),
75
+ )
76
+ const version = entriesRecord.hashById.get(id)
77
+ if (version === undefined) {
78
+ throw createError(`No entry with ID "${id}" exists.`, ErrorCode.NOT_FOUND, {
79
+ argumentName: 'id',
80
+ argumentValue: id,
81
+ })
82
+ }
83
+
84
+ return version
85
+ }
86
+
87
+ /**
88
+ * Throws `CONFLICT` if an expected version is given and the entry has a different version at the current commit of
89
+ * the request. This lets callers detect that an entry was changed by someone else since they read it, e.g. while a
90
+ * user was editing the entry, instead of overwriting these changes.
91
+ */
92
+ export async function assertExpectedVersion(
93
+ context: ApolloContext,
94
+ id: string,
95
+ expectedVersion: string | null | undefined,
96
+ ): Promise<void> {
97
+ if (expectedVersion === undefined || expectedVersion === null) {
98
+ return
99
+ }
100
+ const currentVersion = await getEntryVersion(context, id)
101
+ if (currentVersion !== expectedVersion) {
102
+ throw createError(
103
+ `Entry with ID "${id}" has been changed: expected version "${expectedVersion}" but current version is ` +
104
+ `"${currentVersion}".`,
105
+ ErrorCode.CONFLICT,
106
+ {
107
+ argumentName: 'expectedVersion',
108
+ argumentValue: expectedVersion,
109
+ },
110
+ )
111
+ }
112
+ }