@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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commit-with-retry.d.ts","sourceRoot":"","sources":["../../../src/persistence/commit-with-retry.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EAGZ,MAAM,0BAA0B,CAAA;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAK5C;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,eAAe,GAC1B,SAAS,aAAa,EACtB,aAAa,MAAM,OAAO,CAAC,WAAW,CAAC,KACtC,OAAO,CAAC,IAAI,CAqBd,CAAA"}
@@ -0,0 +1,35 @@
1
+ import { ErrorCode as GitAdapterErrorCode, GitAdapterError, } from '@commitspark/git-adapter';
2
+ import { callGitAdapter, toGraphQLError } from "./git-adapter-call.js";
3
+ const MAX_COMMIT_ATTEMPTS = 3;
4
+ /**
5
+ * Commits a draft that `createDraft` validates against the current commit of the request, guaranteeing that the
6
+ * draft is only committed on top of the exact commit it was validated against.
7
+ *
8
+ * Git adapters only move a branch if it still points to `parentSha` and otherwise throw `CONFLICT`. As most adapters
9
+ * can only check this for the branch as a whole, any concurrent commit to the branch between reading and committing
10
+ * leads to `CONFLICT`, even if it changed unrelated entries. In this case, the current commit of the request is moved
11
+ * to the new branch head and the draft is created and validated again, including expected entry versions and
12
+ * references, so that only actual conflicts are reported to the caller.
13
+ *
14
+ * Errors thrown by `createDraft` are not retried, as the draft was validated against the latest known state.
15
+ */
16
+ export const commitWithRetry = async (context, createDraft) => {
17
+ for (let attempt = 1;; attempt++) {
18
+ const commitDraft = await createDraft();
19
+ try {
20
+ const commit = await context.gitAdapter.createCommit(commitDraft);
21
+ context.setCurrentHash(commit.commitHash);
22
+ return;
23
+ }
24
+ catch (err) {
25
+ const isConflict = err instanceof GitAdapterError &&
26
+ err.code === GitAdapterErrorCode.CONFLICT;
27
+ if (!isConflict || attempt >= MAX_COMMIT_ATTEMPTS) {
28
+ throw toGraphQLError(err);
29
+ }
30
+ }
31
+ const headCommitHash = await callGitAdapter(() => context.gitAdapter.getLatestCommitHash(context.branch));
32
+ context.setCurrentHash(headCommitHash);
33
+ }
34
+ };
35
+ //# sourceMappingURL=commit-with-retry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commit-with-retry.js","sourceRoot":"","sources":["../../../src/persistence/commit-with-retry.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,SAAS,IAAI,mBAAmB,EAChC,eAAe,GAChB,MAAM,0BAA0B,CAAA;AAEjC,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AAEtE,MAAM,mBAAmB,GAAG,CAAC,CAAA;AAE7B;;;;;;;;;;;GAWG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,EAClC,OAAsB,EACtB,WAAuC,EACxB,EAAE;IACjB,KAAK,IAAI,OAAO,GAAG,CAAC,GAAI,OAAO,EAAE,EAAE,CAAC;QAClC,MAAM,WAAW,GAAG,MAAM,WAAW,EAAE,CAAA;QACvC,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,YAAY,CAAC,WAAW,CAAC,CAAA;YACjE,OAAO,CAAC,cAAc,CAAC,MAAM,CAAC,UAAU,CAAC,CAAA;YACzC,OAAM;QACR,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,UAAU,GACd,GAAG,YAAY,eAAe;gBAC9B,GAAG,CAAC,IAAI,KAAK,mBAAmB,CAAC,QAAQ,CAAA;YAC3C,IAAI,CAAC,UAAU,IAAI,OAAO,IAAI,mBAAmB,EAAE,CAAC;gBAClD,MAAM,cAAc,CAAC,GAAG,CAAC,CAAA;YAC3B,CAAC;QACH,CAAC;QAED,MAAM,cAAc,GAAG,MAAM,cAAc,CAAC,GAAG,EAAE,CAC/C,OAAO,CAAC,UAAU,CAAC,mBAAmB,CAAC,OAAO,CAAC,MAAM,CAAC,CACvD,CAAA;QACD,OAAO,CAAC,cAAc,CAAC,cAAc,CAAC,CAAA;IACxC,CAAC;AACH,CAAC,CAAA"}
@@ -0,0 +1,10 @@
1
+ import { GraphQLError } from 'graphql';
2
+ /**
3
+ * Converts an error thrown by a Git adapter to a GraphQL error, keeping the error code of a `GitAdapterError`.
4
+ */
5
+ export declare const toGraphQLError: (err: unknown) => GraphQLError;
6
+ /**
7
+ * Runs a Git adapter operation and converts thrown errors to GraphQL errors.
8
+ */
9
+ export declare const callGitAdapter: <T>(operation: () => Promise<T>) => Promise<T>;
10
+ //# sourceMappingURL=git-adapter-call.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-adapter-call.d.ts","sourceRoot":"","sources":["../../../src/persistence/git-adapter-call.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAGtC;;GAEG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,OAAO,KAAG,YAM7C,CAAA;AAED;;GAEG;AACH,eAAO,MAAM,cAAc,GAAU,CAAC,EACpC,WAAW,MAAM,OAAO,CAAC,CAAC,CAAC,KAC1B,OAAO,CAAC,CAAC,CAMX,CAAA"}
@@ -0,0 +1,24 @@
1
+ import { GitAdapterError } from '@commitspark/git-adapter';
2
+ import { createError, ErrorCode } from "../graphql/errors.js";
3
+ /**
4
+ * Converts an error thrown by a Git adapter to a GraphQL error, keeping the error code of a `GitAdapterError`.
5
+ */
6
+ export const toGraphQLError = (err) => {
7
+ if (err instanceof GitAdapterError) {
8
+ return createError(err.message, err.code, {});
9
+ }
10
+ const message = err instanceof Error ? err.message : String(err);
11
+ return createError(message, ErrorCode.INTERNAL_ERROR, {});
12
+ };
13
+ /**
14
+ * Runs a Git adapter operation and converts thrown errors to GraphQL errors.
15
+ */
16
+ export const callGitAdapter = async (operation) => {
17
+ try {
18
+ return await operation();
19
+ }
20
+ catch (err) {
21
+ throw toGraphQLError(err);
22
+ }
23
+ };
24
+ //# sourceMappingURL=git-adapter-call.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-adapter-call.js","sourceRoot":"","sources":["../../../src/persistence/git-adapter-call.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAE1D,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAA;AAE7D;;GAEG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,GAAY,EAAgB,EAAE;IAC3D,IAAI,GAAG,YAAY,eAAe,EAAE,CAAC;QACnC,OAAO,WAAW,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,CAAA;IAC/C,CAAC;IACD,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IAChE,OAAO,WAAW,CAAC,OAAO,EAAE,SAAS,CAAC,cAAc,EAAE,EAAE,CAAC,CAAA;AAC3D,CAAC,CAAA;AAED;;GAEG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EACjC,SAA2B,EACf,EAAE;IACd,IAAI,CAAC;QACH,OAAO,MAAM,SAAS,EAAE,CAAA;IAC1B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,cAAc,CAAC,GAAG,CAAC,CAAA;IAC3B,CAAC;AACH,CAAC,CAAA"}
@@ -4,4 +4,15 @@ export declare function getTypeById(context: ApolloContext, id: string): Promise
4
4
  export declare function findById(context: ApolloContext, id: string): Promise<Entry>;
5
5
  export declare function findByType(context: ApolloContext, type: string): Promise<Entry[]>;
6
6
  export declare function findByTypeId(context: ApolloContext, type: string, id: string): Promise<Entry>;
7
+ /**
8
+ * Returns the version of an entry, which is the content hash of the entry reported by the Git adapter. It changes
9
+ * whenever the stored entry changes.
10
+ */
11
+ export declare function getEntryVersion(context: ApolloContext, id: string): Promise<string>;
12
+ /**
13
+ * Throws `CONFLICT` if an expected version is given and the entry has a different version at the current commit of
14
+ * the request. This lets callers detect that an entry was changed by someone else since they read it, e.g. while a
15
+ * user was editing the entry, instead of overwriting these changes.
16
+ */
17
+ export declare function assertExpectedVersion(context: ApolloContext, id: string, expectedVersion: string | null | undefined): Promise<void>;
7
18
  //# sourceMappingURL=persistence.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"persistence.d.ts","sourceRoot":"","sources":["../../../src/persistence/persistence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AAEhD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAE5C,wBAAsB,WAAW,CAC/B,OAAO,EAAE,aAAa,EACtB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,MAAM,CAAC,CAGjB;AAED,wBAAsB,QAAQ,CAC5B,OAAO,EAAE,aAAa,EACtB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,KAAK,CAAC,CAchB;AAED,wBAAsB,UAAU,CAC9B,OAAO,EAAE,aAAa,EACtB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,KAAK,EAAE,CAAC,CAMlB;AAED,wBAAsB,YAAY,CAChC,OAAO,EAAE,aAAa,EACtB,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,KAAK,CAAC,CAehB"}
1
+ {"version":3,"file":"persistence.d.ts","sourceRoot":"","sources":["../../../src/persistence/persistence.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,0BAA0B,CAAA;AAEhD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAE5C,wBAAsB,WAAW,CAC/B,OAAO,EAAE,aAAa,EACtB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,MAAM,CAAC,CAGjB;AAED,wBAAsB,QAAQ,CAC5B,OAAO,EAAE,aAAa,EACtB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,KAAK,CAAC,CAchB;AAED,wBAAsB,UAAU,CAC9B,OAAO,EAAE,aAAa,EACtB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,KAAK,EAAE,CAAC,CAMlB;AAED,wBAAsB,YAAY,CAChC,OAAO,EAAE,aAAa,EACtB,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,KAAK,CAAC,CAehB;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,aAAa,EACtB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,MAAM,CAAC,CAcjB;AAED;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,aAAa,EACtB,EAAE,EAAE,MAAM,EACV,eAAe,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GACzC,OAAO,CAAC,IAAI,CAAC,CAgBf"}
@@ -1,4 +1,4 @@
1
- import { createError } from "../graphql/errors.js";
1
+ import { createError, ErrorCode } from "../graphql/errors.js";
2
2
  export async function getTypeById(context, id) {
3
3
  const requestedEntry = await findById(context, id);
4
4
  return requestedEntry.metadata.type;
@@ -7,7 +7,7 @@ export async function findById(context, id) {
7
7
  const entriesRecord = await context.repositoryCache.getEntriesRecord(context, context.getCurrentHash());
8
8
  const requestedEntry = entriesRecord.byId.get(id);
9
9
  if (requestedEntry === undefined) {
10
- throw createError(`No entry with ID "${id}" exists.`, "NOT_FOUND" /* ErrorCode.NOT_FOUND */, {
10
+ throw createError(`No entry with ID "${id}" exists.`, ErrorCode.NOT_FOUND, {
11
11
  argumentName: 'id',
12
12
  argumentValue: id,
13
13
  });
@@ -21,7 +21,7 @@ export async function findByType(context, type) {
21
21
  export async function findByTypeId(context, type, id) {
22
22
  const entryById = await findById(context, id);
23
23
  if (entryById === undefined || entryById.metadata.type !== type) {
24
- throw createError(`No entry of type "${type}" with ID "${id}" exists.`, "NOT_FOUND" /* ErrorCode.NOT_FOUND */, {
24
+ throw createError(`No entry of type "${type}" with ID "${id}" exists.`, ErrorCode.NOT_FOUND, {
25
25
  typeName: type,
26
26
  argumentName: 'id',
27
27
  argumentValue: id,
@@ -29,4 +29,37 @@ export async function findByTypeId(context, type, id) {
29
29
  }
30
30
  return entryById;
31
31
  }
32
+ /**
33
+ * Returns the version of an entry, which is the content hash of the entry reported by the Git adapter. It changes
34
+ * whenever the stored entry changes.
35
+ */
36
+ export async function getEntryVersion(context, id) {
37
+ const entriesRecord = await context.repositoryCache.getEntriesRecord(context, context.getCurrentHash());
38
+ const version = entriesRecord.hashById.get(id);
39
+ if (version === undefined) {
40
+ throw createError(`No entry with ID "${id}" exists.`, ErrorCode.NOT_FOUND, {
41
+ argumentName: 'id',
42
+ argumentValue: id,
43
+ });
44
+ }
45
+ return version;
46
+ }
47
+ /**
48
+ * Throws `CONFLICT` if an expected version is given and the entry has a different version at the current commit of
49
+ * the request. This lets callers detect that an entry was changed by someone else since they read it, e.g. while a
50
+ * user was editing the entry, instead of overwriting these changes.
51
+ */
52
+ export async function assertExpectedVersion(context, id, expectedVersion) {
53
+ if (expectedVersion === undefined || expectedVersion === null) {
54
+ return;
55
+ }
56
+ const currentVersion = await getEntryVersion(context, id);
57
+ if (currentVersion !== expectedVersion) {
58
+ throw createError(`Entry with ID "${id}" has been changed: expected version "${expectedVersion}" but current version is ` +
59
+ `"${currentVersion}".`, ErrorCode.CONFLICT, {
60
+ argumentName: 'expectedVersion',
61
+ argumentValue: expectedVersion,
62
+ });
63
+ }
64
+ }
32
65
  //# sourceMappingURL=persistence.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"persistence.js","sourceRoot":"","sources":["../../../src/persistence/persistence.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAa,MAAM,sBAAsB,CAAA;AAG7D,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,OAAsB,EACtB,EAAU;IAEV,MAAM,cAAc,GAAG,MAAM,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC,CAAA;IAClD,OAAO,cAAc,CAAC,QAAQ,CAAC,IAAI,CAAA;AACrC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,OAAsB,EACtB,EAAU;IAEV,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,eAAe,CAAC,gBAAgB,CAClE,OAAO,EACP,OAAO,CAAC,cAAc,EAAE,CACzB,CAAA;IACD,MAAM,cAAc,GAAG,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;IACjD,IAAI,cAAc,KAAK,SAAS,EAAE,CAAC;QACjC,MAAM,WAAW,CAAC,qBAAqB,EAAE,WAAW,yCAAuB;YACzE,YAAY,EAAE,IAAI;YAClB,aAAa,EAAE,EAAE;SAClB,CAAC,CAAA;IACJ,CAAC;IAED,OAAO,cAAc,CAAA;AACvB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,OAAsB,EACtB,IAAY;IAEZ,MAAM,aAAa,GAAG,MAAM,OAAO,CAAC,eAAe,CAAC,gBAAgB,CAClE,OAAO,EACP,OAAO,CAAC,cAAc,EAAE,CACzB,CAAA;IACD,OAAO,aAAa,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAA;AAC7C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,OAAsB,EACtB,IAAY,EACZ,EAAU;IAEV,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,OAAO,EAAE,EAAE,CAAC,CAAA;IAC7C,IAAI,SAAS,KAAK,SAAS,IAAI,SAAS,CAAC,QAAQ,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;QAChE,MAAM,WAAW,CACf,qBAAqB,IAAI,cAAc,EAAE,WAAW,yCAEpD;YACE,QAAQ,EAAE,IAAI;YACd,YAAY,EAAE,IAAI;YAClB,aAAa,EAAE,EAAE;SAClB,CACF,CAAA;IACH,CAAC;IAED,OAAO,SAAS,CAAA;AAClB,CAAC"}
1
+ {"version":3,"file":"persistence.js","sourceRoot":"","sources":["../../../src/persistence/persistence.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -9,13 +9,16 @@
9
9
  "gitops",
10
10
  "api"
11
11
  ],
12
- "version": "2.0.0-beta.4",
12
+ "version": "2.0.0-beta.5",
13
13
  "license": "ISC",
14
14
  "repository": {
15
15
  "type": "git",
16
16
  "url": "git+https://github.com/commitspark/graphql-api.git"
17
17
  },
18
18
  "private": false,
19
+ "engines": {
20
+ "node": ">=22.18.0"
21
+ },
19
22
  "files": [
20
23
  "dist/**",
21
24
  "!dist/**/*.tsbuildinfo",
@@ -53,13 +56,13 @@
53
56
  },
54
57
  "dependencies": {
55
58
  "@apollo/server": "^5.2.0",
56
- "@commitspark/git-adapter": "^2.0.0-beta.2",
59
+ "@commitspark/git-adapter": "^2.0.0-beta.4",
57
60
  "@commitspark/search-adapter": "^1.0.0-beta.1",
58
- "@graphql-tools/schema": "^9.0.0",
59
- "@graphql-tools/utils": "^9.0.0"
61
+ "@graphql-tools/schema": "^11.0.8",
62
+ "@graphql-tools/utils": "^13.2.1"
60
63
  },
61
64
  "peerDependencies": {
62
- "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
65
+ "graphql": "^16.11.0"
63
66
  },
64
67
  "devDependencies": {
65
68
  "@arethetypeswrong/cli": "^0.18.5",
@@ -2,13 +2,14 @@ import { GraphQLError } from 'graphql'
2
2
  import { ErrorCode as GitAdapterErrorCode } from '@commitspark/git-adapter'
3
3
  import { ErrorCode as SearchAdapterErrorCode } from '@commitspark/search-adapter'
4
4
 
5
- export const enum ErrorCode {
5
+ export enum ErrorCode {
6
6
  BAD_USER_INPUT = 'BAD_USER_INPUT',
7
7
  NOT_FOUND = 'NOT_FOUND',
8
8
  BAD_REPOSITORY_DATA = 'BAD_REPOSITORY_DATA',
9
9
  BAD_SCHEMA = 'BAD_SCHEMA',
10
10
  INTERNAL_ERROR = 'INTERNAL_ERROR',
11
11
  IN_USE = 'IN_USE',
12
+ CONFLICT = 'CONFLICT',
12
13
  }
13
14
 
14
15
  export interface ErrorMetadata {
@@ -10,7 +10,6 @@ import {
10
10
  isListType,
11
11
  isNonNullType,
12
12
  isObjectType,
13
- isScalarType,
14
13
  isUnionType,
15
14
  } from 'graphql'
16
15
  import { SchemaAnalyzerResult } from './schema-analyzer.ts'
@@ -72,16 +71,13 @@ export function generateObjectInputTypeStrings(
72
71
  return objectTypes.map((objectType): string => {
73
72
  const name = objectType.name
74
73
  const inputTypeName = `${name}Input`
74
+ const isEntryType = hasDirective(objectType, ENTRY_DIRECTIVE_NAME)
75
75
  let inputType = `input ${inputTypeName} {\n`
76
76
  let inputTypeFieldStrings = ''
77
77
  for (const fieldsKey in objectType.getFields()) {
78
78
  const field = objectType.getFields()[fieldsKey]
79
- if (
80
- (isNonNullType(field.type) &&
81
- isScalarType(field.type.ofType) &&
82
- field.type.ofType.name === 'ID') ||
83
- (isScalarType(field.type) && field.type.name === 'ID')
84
- ) {
79
+ // the ID of an entry is passed as mutation argument instead of as part of the entry data
80
+ if (isEntryType && field.name === 'id') {
85
81
  continue
86
82
  }
87
83
  const inputTypeString = generateFieldInputTypeString(field.type)
@@ -103,12 +99,12 @@ export function generateObjectInputTypeStrings(
103
99
  export function generateUnionInputTypeStrings(
104
100
  unionTypes: GraphQLUnionType[],
105
101
  ): string[] {
106
- const typeStrings = unionTypes.map((unionType): string => {
102
+ return unionTypes.map((unionType): string => {
107
103
  if (buildsOnTypeWithEntryDirective(unionType)) {
108
104
  return ''
109
105
  }
110
106
 
111
- // see https://github.com/graphql/graphql-spec/pull/825
107
+ // `@oneOf` is a directive specified by GraphQL and built into graphql-js since 16.9
112
108
  let unionInputType = `input ${unionType.name}Input @oneOf {\n`
113
109
  for (const innerType of unionType.getTypes()) {
114
110
  unionInputType += ` ${innerType.name}: ${innerType.name}Input\n`
@@ -116,10 +112,4 @@ export function generateUnionInputTypeStrings(
116
112
  unionInputType += '}\n'
117
113
  return unionInputType
118
114
  })
119
-
120
- if (typeStrings.join('').length > 0) {
121
- typeStrings.push('directive @oneOf on INPUT_OBJECT\n')
122
- }
123
-
124
- return typeStrings
125
115
  }
@@ -8,12 +8,16 @@ import { mutationDeleteResolver } from './resolvers/query-mutation-resolvers/mut
8
8
  import { queryTypeByIdResolver } from './resolvers/query-mutation-resolvers/query-type-by-id-resolver.ts'
9
9
  import { querySearchResolver } from './resolvers/query-mutation-resolvers/query-search-resolver.ts'
10
10
  import { referencedByResolver } from './resolvers/field-resolvers/referenced-by-resolver.ts'
11
+ import { versionResolver } from './resolvers/field-resolvers/version-resolver.ts'
11
12
  import {
12
13
  QueryMutationResolver,
13
14
  ReferencedByResolver,
14
15
  SearchQueryResolver,
16
+ VersionResolver,
15
17
  } from './resolvers/types.ts'
16
18
 
19
+ const commitInputTypeName = '_CommitInput'
20
+
17
21
  export function generateQueriesAndMutations(
18
22
  entryDirectiveTypes: GraphQLObjectType[],
19
23
  ): GeneratedSchema[] {
@@ -42,8 +46,20 @@ export function generateQueriesAndMutations(
42
46
  ) => queryByIdResolver(source, args, { ...context, type: objectType }, info)
43
47
 
44
48
  const inputTypeName = `${typeName}Input`
49
+ // mutations are printed with one argument per line, as indented within the generated root type
50
+ const commitArgument = (operation: string): string =>
51
+ ' """\n' +
52
+ ` Details of the commit. If no message is given, the message is '${operation} ${typeName} "<id>"'.\n` +
53
+ ' """\n' +
54
+ ` commit: ${commitInputTypeName}\n`
55
+ const expectedVersionArgument =
56
+ ' """\n' +
57
+ ' Version of the entry (field `_version`) that the change is based on. If given and the entry has a different\n' +
58
+ ' version, the mutation fails with error code CONFLICT instead of overwriting changes made since.\n' +
59
+ ' """\n' +
60
+ ' expectedVersion: String\n'
45
61
  const createMutationName = `create${typeName}`
46
- const createMutationString = `${createMutationName}(id: ID!, data: ${inputTypeName}!, commitMessage: String): ${typeName}`
62
+ const createMutationString = `${createMutationName}(\n id: ID!\n data: ${inputTypeName}!\n${commitArgument('Create')} ): ${typeName}`
47
63
  const createMutationResolver: QueryMutationResolver<EntryData> = (
48
64
  source,
49
65
  args,
@@ -58,7 +74,7 @@ export function generateQueriesAndMutations(
58
74
  )
59
75
 
60
76
  const updateMutationName = `update${typeName}`
61
- const updateMutationString = `${updateMutationName}(id: ID!, data: ${inputTypeName}!, commitMessage: String): ${typeName}`
77
+ const updateMutationString = `${updateMutationName}(\n id: ID!\n data: ${inputTypeName}!\n${expectedVersionArgument}${commitArgument('Update')} ): ${typeName}`
62
78
  const updateMutationResolver: QueryMutationResolver<EntryData> = (
63
79
  source,
64
80
  args,
@@ -73,7 +89,7 @@ export function generateQueriesAndMutations(
73
89
  )
74
90
 
75
91
  const deleteMutationName = `delete${typeName}`
76
- const deleteMutationString = `${deleteMutationName}(id: ID!, commitMessage: String): ID`
92
+ const deleteMutationString = `${deleteMutationName}(\n id: ID!\n${expectedVersionArgument}${commitArgument('Delete')} ): ID`
77
93
  const deleteMutationResolver: QueryMutationResolver<string> = (
78
94
  source,
79
95
  args,
@@ -117,6 +133,24 @@ export function generateQueriesAndMutations(
117
133
  })
118
134
  }
119
135
 
136
+ export function generateCommitInputType(): string {
137
+ return `"""
138
+ Details of the commit created by a mutation.
139
+ """
140
+ input ${commitInputTypeName} {
141
+ """
142
+ Commit message summarizing the change in a single line. Leading and trailing whitespace is removed, and messages
143
+ containing line breaks are rejected with error code BAD_USER_INPUT. If omitted or empty, a message is generated.
144
+ """
145
+ message: String
146
+ """
147
+ Optional details of the change, which may span multiple lines. Leading and trailing whitespace is removed.
148
+ """
149
+ body: String
150
+ }
151
+ `
152
+ }
153
+
120
154
  export function generateTypeNameQuery(): GeneratedQuery {
121
155
  const entryTypeQueryName = '_typeName'
122
156
  const entryTypeQueryString = `${entryTypeQueryName}(id: ID!): String!`
@@ -147,27 +181,34 @@ export function generateSearchQuery(): GeneratedSearchQuery {
147
181
  }
148
182
  }
149
183
 
150
- export function generateReferencedByFields(
184
+ export function generateEntryFields(
151
185
  entryDirectiveTypes: GraphQLObjectType[],
152
- ): GeneratedField[] {
186
+ ): GeneratedEntryFields[] {
153
187
  const referencedByFieldName = '_referencedBy'
188
+ const versionFieldName = '_version'
154
189
 
155
190
  return entryDirectiveTypes.map((objectType) => ({
156
191
  typeName: objectType.name,
157
- name: referencedByFieldName,
158
192
  typeExtensionString: `extend type ${objectType.name} {
159
193
  ${referencedByFieldName}: [ID!]!
194
+ """
195
+ Version of the entry, which changes whenever the stored entry changes. Pass it as \`expectedVersion\` to update
196
+ and delete mutations to prevent overwriting changes made since the entry was read.
197
+ """
198
+ ${versionFieldName}: String!
160
199
  }
161
200
  `,
162
- resolver: referencedByResolver,
201
+ resolvers: {
202
+ [referencedByFieldName]: referencedByResolver,
203
+ [versionFieldName]: versionResolver,
204
+ },
163
205
  }))
164
206
  }
165
207
 
166
- interface GeneratedField {
208
+ interface GeneratedEntryFields {
167
209
  typeName: string
168
- name: string
169
210
  typeExtensionString: string
170
- resolver: ReferencedByResolver
211
+ resolvers: Record<string, ReferencedByResolver | VersionResolver>
171
212
  }
172
213
 
173
214
  export interface GeneratedSearchQuery {
@@ -0,0 +1,34 @@
1
+ import { CommitDraft } from '@commitspark/git-adapter'
2
+ import { createError, ErrorCode } from '../errors.ts'
3
+ import { CommitInput } from './types.ts'
4
+
5
+ type MutationOperation = 'Create' | 'Update' | 'Delete'
6
+
7
+ /**
8
+ * Determines message and body of the commit of a mutation from mutation argument `commit`. The message is trimmed and
9
+ * must be a single line; an omitted or empty message is replaced by a message generated from the mutation. The body
10
+ * is trimmed and omitted if empty.
11
+ */
12
+ export function getCommitMessage(
13
+ commit: CommitInput | null | undefined,
14
+ operation: MutationOperation,
15
+ typeName: string,
16
+ id: string,
17
+ ): Pick<CommitDraft, 'message' | 'body'> {
18
+ const message = commit?.message?.trim() ?? ''
19
+ if (/[\r\n]/.test(message)) {
20
+ throw createError(
21
+ 'Commit message must be a single line; use field "body" for details.',
22
+ ErrorCode.BAD_USER_INPUT,
23
+ { argumentName: 'commit.message', argumentValue: commit?.message },
24
+ )
25
+ }
26
+ const body = commit?.body?.trim() ?? ''
27
+
28
+ const commitMessage =
29
+ message !== '' ? message : `${operation} ${typeName} "${id}"`
30
+ if (body === '') {
31
+ return { message: commitMessage }
32
+ }
33
+ return { message: commitMessage, body: body }
34
+ }
@@ -0,0 +1,8 @@
1
+ import { getEntryVersion } from '../../../persistence/persistence.ts'
2
+ import { VersionResolver } from '../types.ts'
3
+
4
+ export const versionResolver: VersionResolver = async (
5
+ source,
6
+ _args,
7
+ context,
8
+ ) => getEntryVersion(context, source.id)
@@ -8,6 +8,8 @@ import {
8
8
  } from '@commitspark/git-adapter'
9
9
  import { QueryMutationResolver } from '../types.ts'
10
10
  import { createError, ErrorCode } from '../../errors.ts'
11
+ import { getCommitMessage } from '../commit-message.ts'
12
+ import { commitWithRetry } from '../../../persistence/commit-with-retry.ts'
11
13
 
12
14
  export const mutationCreateResolver: QueryMutationResolver<EntryData> = async (
13
15
  source,
@@ -23,6 +25,8 @@ export const mutationCreateResolver: QueryMutationResolver<EntryData> = async (
23
25
  {},
24
26
  )
25
27
  }
28
+ // narrowed type, as narrowing does not apply within the callback below
29
+ const entryType = context.type
26
30
 
27
31
  try {
28
32
  validateEntryId(args.id)
@@ -36,40 +40,47 @@ export const mutationCreateResolver: QueryMutationResolver<EntryData> = async (
36
40
  },
37
41
  )
38
42
  }
43
+ const commitMessage = getCommitMessage(
44
+ args.commit,
45
+ 'Create',
46
+ context.type.name,
47
+ args.id,
48
+ )
39
49
 
40
- let existingEntry
41
- try {
42
- existingEntry = await findById(context, args.id)
43
- } catch {}
44
- if (existingEntry) {
45
- throw createError(
46
- `An entry with id "${args.id}" already exists.`,
47
- ErrorCode.BAD_USER_INPUT,
48
- {
49
- argumentName: 'id',
50
- argumentValue: args.id,
51
- },
52
- )
53
- }
50
+ await commitWithRetry(context, async () => {
51
+ let existingEntry
52
+ try {
53
+ existingEntry = await findById(context, args.id)
54
+ } catch {}
55
+ if (existingEntry) {
56
+ throw createError(
57
+ `An entry with id "${args.id}" already exists.`,
58
+ ErrorCode.BAD_USER_INPUT,
59
+ {
60
+ argumentName: 'id',
61
+ argumentValue: args.id,
62
+ },
63
+ )
64
+ }
54
65
 
55
- await validateEntryReferences(context.type, context, args.data ?? null)
66
+ await validateEntryReferences(entryType, context, args.data ?? null)
56
67
 
57
- const newEntryDraft: EntryDraft = {
58
- id: args.id,
59
- metadata: {
60
- type: context.type.name,
61
- },
62
- data: args.data ?? null,
63
- deletion: false,
64
- }
68
+ const newEntryDraft: EntryDraft = {
69
+ id: args.id,
70
+ metadata: {
71
+ type: context.type.name,
72
+ },
73
+ data: args.data ?? null,
74
+ deletion: false,
75
+ }
65
76
 
66
- const commit = await context.gitAdapter.createCommit({
67
- ref: context.branch,
68
- parentSha: context.getCurrentHash(),
69
- entries: [newEntryDraft],
70
- message: args.commitMessage,
77
+ return {
78
+ ref: context.branch,
79
+ parentSha: context.getCurrentHash(),
80
+ entries: [newEntryDraft],
81
+ ...commitMessage,
82
+ }
71
83
  })
72
- context.setCurrentHash(commit.commitHash)
73
84
 
74
85
  const newEntry = await findByTypeId(context, context.type.name, args.id)
75
86
  return { ...newEntry.data, id: newEntry.id }
@@ -1,8 +1,13 @@
1
- import { findByTypeId } from '../../../persistence/persistence.ts'
1
+ import {
2
+ assertExpectedVersion,
3
+ findByTypeId,
4
+ } from '../../../persistence/persistence.ts'
2
5
  import { Entry } from '@commitspark/git-adapter'
3
6
  import { getReferencingEntryIds } from '../../schema-utils/entry-reference-util.ts'
4
7
  import { QueryMutationResolver } from '../types.ts'
5
8
  import { createError, ErrorCode } from '../../errors.ts'
9
+ import { getCommitMessage } from '../commit-message.ts'
10
+ import { commitWithRetry } from '../../../persistence/commit-with-retry.ts'
6
11
 
7
12
  export const mutationDeleteResolver: QueryMutationResolver<string> = async (
8
13
  source,
@@ -10,38 +15,47 @@ export const mutationDeleteResolver: QueryMutationResolver<string> = async (
10
15
  context,
11
16
  info,
12
17
  ) => {
13
- const entry: Entry = await findByTypeId(context, context.type.name, args.id)
18
+ const commitMessage = getCommitMessage(
19
+ args.commit,
20
+ 'Delete',
21
+ context.type.name,
22
+ args.id,
23
+ )
14
24
 
15
- // references held by the entry itself are deleted together with the entry
16
- const otherReferencingIds = (
17
- await getReferencingEntryIds(context, info.schema, args.id)
18
- ).filter((referencingId) => referencingId !== args.id)
19
- if (otherReferencingIds.length > 0) {
20
- const otherIds = otherReferencingIds
21
- .map((referenceId) => `"${referenceId}"`)
22
- .join(', ')
23
- throw createError(
24
- `Entry with ID "${args.id}" is still referenced by entries [${otherIds}].`,
25
- ErrorCode.IN_USE,
26
- {
27
- argumentName: 'id',
28
- argumentValue: args.id,
29
- },
30
- )
31
- }
25
+ await commitWithRetry(context, async () => {
26
+ const entry: Entry = await findByTypeId(context, context.type.name, args.id)
27
+ await assertExpectedVersion(context, args.id, args.expectedVersion)
32
28
 
33
- const commit = await context.gitAdapter.createCommit({
34
- ref: context.branch,
35
- parentSha: context.getCurrentHash(),
36
- entries: [
37
- {
38
- ...entry,
39
- deletion: true,
40
- },
41
- ],
42
- message: args.commitMessage,
29
+ // references held by the entry itself are deleted together with the entry
30
+ const otherReferencingIds = (
31
+ await getReferencingEntryIds(context, info.schema, args.id)
32
+ ).filter((referencingId) => referencingId !== args.id)
33
+ if (otherReferencingIds.length > 0) {
34
+ const otherIds = otherReferencingIds
35
+ .map((referenceId) => `"${referenceId}"`)
36
+ .join(', ')
37
+ throw createError(
38
+ `Entry with ID "${args.id}" is still referenced by entries [${otherIds}].`,
39
+ ErrorCode.IN_USE,
40
+ {
41
+ argumentName: 'id',
42
+ argumentValue: args.id,
43
+ },
44
+ )
45
+ }
46
+
47
+ return {
48
+ ref: context.branch,
49
+ parentSha: context.getCurrentHash(),
50
+ entries: [
51
+ {
52
+ ...entry,
53
+ deletion: true,
54
+ },
55
+ ],
56
+ ...commitMessage,
57
+ }
43
58
  })
44
- context.setCurrentHash(commit.commitHash)
45
59
 
46
60
  return args.id
47
61
  }