@contember/engine-actions 2.2.0-alpha.3 → 2.2.0-alpha.4

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 (200) hide show
  1. package/CLAUDE.md +58 -0
  2. package/dist/development/index.cjs +19 -7
  3. package/dist/development/index.cjs.map +1 -1
  4. package/dist/development/index.js +19 -7
  5. package/dist/development/index.js.map +1 -1
  6. package/dist/development/src/ActionsExecutionContainerHookFactory.cjs +5 -2
  7. package/dist/development/src/ActionsExecutionContainerHookFactory.cjs.map +1 -1
  8. package/dist/development/src/ActionsExecutionContainerHookFactory.js +5 -2
  9. package/dist/development/src/ActionsExecutionContainerHookFactory.js.map +1 -1
  10. package/dist/development/src/ActionsMetrics.cjs +98 -0
  11. package/dist/development/src/ActionsMetrics.cjs.map +1 -0
  12. package/dist/development/src/ActionsMetrics.js +98 -0
  13. package/dist/development/src/ActionsMetrics.js.map +1 -0
  14. package/dist/development/src/dispatch/EventDispatcher.cjs +27 -9
  15. package/dist/development/src/dispatch/EventDispatcher.cjs.map +1 -1
  16. package/dist/development/src/dispatch/EventDispatcher.js +27 -9
  17. package/dist/development/src/dispatch/EventDispatcher.js.map +1 -1
  18. package/dist/development/src/dispatch/EventsRepository.cjs +49 -20
  19. package/dist/development/src/dispatch/EventsRepository.cjs.map +1 -1
  20. package/dist/development/src/dispatch/EventsRepository.js +49 -20
  21. package/dist/development/src/dispatch/EventsRepository.js.map +1 -1
  22. package/dist/development/src/dispatch/ProjectDispatcher.cjs +21 -13
  23. package/dist/development/src/dispatch/ProjectDispatcher.cjs.map +1 -1
  24. package/dist/development/src/dispatch/ProjectDispatcher.js +21 -13
  25. package/dist/development/src/dispatch/ProjectDispatcher.js.map +1 -1
  26. package/dist/development/src/dispatch/WebhookTargetHandler.cjs +29 -2
  27. package/dist/development/src/dispatch/WebhookTargetHandler.cjs.map +1 -1
  28. package/dist/development/src/dispatch/WebhookTargetHandler.js +29 -2
  29. package/dist/development/src/dispatch/WebhookTargetHandler.js.map +1 -1
  30. package/dist/development/src/graphql/resolvers/ResolversFactory.cjs +1 -1
  31. package/dist/development/src/graphql/resolvers/ResolversFactory.cjs.map +1 -1
  32. package/dist/development/src/graphql/resolvers/ResolversFactory.js +1 -1
  33. package/dist/development/src/graphql/resolvers/ResolversFactory.js.map +1 -1
  34. package/dist/development/src/graphql/resolvers/mutation/SetVariablesMutationResolver.cjs +1 -1
  35. package/dist/development/src/graphql/resolvers/mutation/SetVariablesMutationResolver.cjs.map +1 -1
  36. package/dist/development/src/graphql/resolvers/mutation/SetVariablesMutationResolver.js +1 -1
  37. package/dist/development/src/graphql/resolvers/mutation/SetVariablesMutationResolver.js.map +1 -1
  38. package/dist/development/src/graphql/resolvers/query/EventsQueryResolver.cjs +1 -1
  39. package/dist/development/src/graphql/resolvers/query/EventsQueryResolver.cjs.map +1 -1
  40. package/dist/development/src/graphql/resolvers/query/EventsQueryResolver.js +1 -1
  41. package/dist/development/src/graphql/resolvers/query/EventsQueryResolver.js.map +1 -1
  42. package/dist/development/src/graphql/resolvers/query/VariablesQueryResolver.cjs +1 -4
  43. package/dist/development/src/graphql/resolvers/query/VariablesQueryResolver.cjs.map +1 -1
  44. package/dist/development/src/graphql/resolvers/query/VariablesQueryResolver.js +1 -4
  45. package/dist/development/src/graphql/resolvers/query/VariablesQueryResolver.js.map +1 -1
  46. package/dist/development/src/graphql/schema/actions.graphql.cjs +20 -8
  47. package/dist/development/src/graphql/schema/actions.graphql.cjs.map +1 -1
  48. package/dist/development/src/graphql/schema/actions.graphql.js +20 -8
  49. package/dist/development/src/graphql/schema/actions.graphql.js.map +1 -1
  50. package/dist/development/src/model/VariablesManager.cjs +54 -5
  51. package/dist/development/src/model/VariablesManager.cjs.map +1 -1
  52. package/dist/development/src/model/VariablesManager.js +54 -5
  53. package/dist/development/src/model/VariablesManager.js.map +1 -1
  54. package/dist/development/src/triggers/TriggerPayloadPersister.cjs +3 -1
  55. package/dist/development/src/triggers/TriggerPayloadPersister.cjs.map +1 -1
  56. package/dist/development/src/triggers/TriggerPayloadPersister.js +3 -1
  57. package/dist/development/src/triggers/TriggerPayloadPersister.js.map +1 -1
  58. package/dist/production/index.cjs +19 -7
  59. package/dist/production/index.cjs.map +1 -1
  60. package/dist/production/index.js +19 -7
  61. package/dist/production/index.js.map +1 -1
  62. package/dist/production/src/ActionsExecutionContainerHookFactory.cjs +5 -2
  63. package/dist/production/src/ActionsExecutionContainerHookFactory.cjs.map +1 -1
  64. package/dist/production/src/ActionsExecutionContainerHookFactory.js +5 -2
  65. package/dist/production/src/ActionsExecutionContainerHookFactory.js.map +1 -1
  66. package/dist/production/src/ActionsMetrics.cjs +98 -0
  67. package/dist/production/src/ActionsMetrics.cjs.map +1 -0
  68. package/dist/production/src/ActionsMetrics.js +98 -0
  69. package/dist/production/src/ActionsMetrics.js.map +1 -0
  70. package/dist/production/src/dispatch/EventDispatcher.cjs +27 -9
  71. package/dist/production/src/dispatch/EventDispatcher.cjs.map +1 -1
  72. package/dist/production/src/dispatch/EventDispatcher.js +27 -9
  73. package/dist/production/src/dispatch/EventDispatcher.js.map +1 -1
  74. package/dist/production/src/dispatch/EventsRepository.cjs +49 -20
  75. package/dist/production/src/dispatch/EventsRepository.cjs.map +1 -1
  76. package/dist/production/src/dispatch/EventsRepository.js +49 -20
  77. package/dist/production/src/dispatch/EventsRepository.js.map +1 -1
  78. package/dist/production/src/dispatch/ProjectDispatcher.cjs +21 -13
  79. package/dist/production/src/dispatch/ProjectDispatcher.cjs.map +1 -1
  80. package/dist/production/src/dispatch/ProjectDispatcher.js +21 -13
  81. package/dist/production/src/dispatch/ProjectDispatcher.js.map +1 -1
  82. package/dist/production/src/dispatch/WebhookTargetHandler.cjs +29 -2
  83. package/dist/production/src/dispatch/WebhookTargetHandler.cjs.map +1 -1
  84. package/dist/production/src/dispatch/WebhookTargetHandler.js +29 -2
  85. package/dist/production/src/dispatch/WebhookTargetHandler.js.map +1 -1
  86. package/dist/production/src/graphql/resolvers/ResolversFactory.cjs +1 -1
  87. package/dist/production/src/graphql/resolvers/ResolversFactory.cjs.map +1 -1
  88. package/dist/production/src/graphql/resolvers/ResolversFactory.js +1 -1
  89. package/dist/production/src/graphql/resolvers/ResolversFactory.js.map +1 -1
  90. package/dist/production/src/graphql/resolvers/mutation/SetVariablesMutationResolver.cjs +1 -1
  91. package/dist/production/src/graphql/resolvers/mutation/SetVariablesMutationResolver.cjs.map +1 -1
  92. package/dist/production/src/graphql/resolvers/mutation/SetVariablesMutationResolver.js +1 -1
  93. package/dist/production/src/graphql/resolvers/mutation/SetVariablesMutationResolver.js.map +1 -1
  94. package/dist/production/src/graphql/resolvers/query/EventsQueryResolver.cjs +1 -1
  95. package/dist/production/src/graphql/resolvers/query/EventsQueryResolver.cjs.map +1 -1
  96. package/dist/production/src/graphql/resolvers/query/EventsQueryResolver.js +1 -1
  97. package/dist/production/src/graphql/resolvers/query/EventsQueryResolver.js.map +1 -1
  98. package/dist/production/src/graphql/resolvers/query/VariablesQueryResolver.cjs +1 -4
  99. package/dist/production/src/graphql/resolvers/query/VariablesQueryResolver.cjs.map +1 -1
  100. package/dist/production/src/graphql/resolvers/query/VariablesQueryResolver.js +1 -4
  101. package/dist/production/src/graphql/resolvers/query/VariablesQueryResolver.js.map +1 -1
  102. package/dist/production/src/graphql/schema/actions.graphql.cjs +20 -8
  103. package/dist/production/src/graphql/schema/actions.graphql.cjs.map +1 -1
  104. package/dist/production/src/graphql/schema/actions.graphql.js +20 -8
  105. package/dist/production/src/graphql/schema/actions.graphql.js.map +1 -1
  106. package/dist/production/src/model/VariablesManager.cjs +54 -5
  107. package/dist/production/src/model/VariablesManager.cjs.map +1 -1
  108. package/dist/production/src/model/VariablesManager.js +54 -5
  109. package/dist/production/src/model/VariablesManager.js.map +1 -1
  110. package/dist/production/src/triggers/TriggerPayloadPersister.cjs +3 -1
  111. package/dist/production/src/triggers/TriggerPayloadPersister.cjs.map +1 -1
  112. package/dist/production/src/triggers/TriggerPayloadPersister.js +3 -1
  113. package/dist/production/src/triggers/TriggerPayloadPersister.js.map +1 -1
  114. package/dist/tests/cases/unit/actionsMetrics.test.d.ts +2 -0
  115. package/dist/tests/cases/unit/actionsMetrics.test.d.ts.map +1 -0
  116. package/dist/tests/cases/unit/eventsRepository.test.d.ts +2 -0
  117. package/dist/tests/cases/unit/eventsRepository.test.d.ts.map +1 -0
  118. package/dist/tests/cases/unit/variablesManager.test.d.ts +2 -0
  119. package/dist/tests/cases/unit/variablesManager.test.d.ts.map +1 -0
  120. package/dist/tests/src/event.d.ts +4 -0
  121. package/dist/tests/src/event.d.ts.map +1 -0
  122. package/dist/tests/src/gql.d.ts.map +1 -1
  123. package/dist/tests/src/uuid.d.ts.map +1 -1
  124. package/dist/tests/tsconfig.tsbuildinfo +1 -1
  125. package/dist/types/ActionsExecutionContainerHookFactory.d.ts +3 -1
  126. package/dist/types/ActionsExecutionContainerHookFactory.d.ts.map +1 -1
  127. package/dist/types/ActionsMetrics.d.ts +59 -0
  128. package/dist/types/ActionsMetrics.d.ts.map +1 -0
  129. package/dist/types/ListenerStoreProvider.d.ts.map +1 -1
  130. package/dist/types/authorization/ActionsPermissionsFactory.d.ts.map +1 -1
  131. package/dist/types/dispatch/DispatchWorkerSupervisor.d.ts.map +1 -1
  132. package/dist/types/dispatch/EventDispatcher.d.ts +12 -3
  133. package/dist/types/dispatch/EventDispatcher.d.ts.map +1 -1
  134. package/dist/types/dispatch/EventsRepository.d.ts +14 -2
  135. package/dist/types/dispatch/EventsRepository.d.ts.map +1 -1
  136. package/dist/types/dispatch/LazyDispatchWorker.d.ts.map +1 -1
  137. package/dist/types/dispatch/ProjectDispatcher.d.ts +5 -2
  138. package/dist/types/dispatch/ProjectDispatcher.d.ts.map +1 -1
  139. package/dist/types/dispatch/TargetHandlerResolver.d.ts.map +1 -1
  140. package/dist/types/dispatch/WebhookFetcher.d.ts.map +1 -1
  141. package/dist/types/dispatch/WebhookTargetHandler.d.ts.map +1 -1
  142. package/dist/types/graphql/http/ActionsApiMiddlewareFactory.d.ts.map +1 -1
  143. package/dist/types/graphql/http/ActionsContextResolver.d.ts.map +1 -1
  144. package/dist/types/graphql/http/ActionsGraphQLHandlerFactory.d.ts.map +1 -1
  145. package/dist/types/graphql/http/ActionsWebsocketControllerFactory.d.ts.map +1 -1
  146. package/dist/types/graphql/resolvers/ResolversFactory.d.ts.map +1 -1
  147. package/dist/types/graphql/resolvers/mutation/ProcessBatchMutationResolver.d.ts.map +1 -1
  148. package/dist/types/graphql/resolvers/mutation/RetryEventMutationResolver.d.ts.map +1 -1
  149. package/dist/types/graphql/resolvers/mutation/SetVariablesMutationResolver.d.ts.map +1 -1
  150. package/dist/types/graphql/resolvers/mutation/StopEventMutationResolver.d.ts.map +1 -1
  151. package/dist/types/graphql/resolvers/query/EventsQueryResolver.d.ts.map +1 -1
  152. package/dist/types/graphql/resolvers/query/VariablesQueryResolver.d.ts +1 -4
  153. package/dist/types/graphql/resolvers/query/VariablesQueryResolver.d.ts.map +1 -1
  154. package/dist/types/graphql/schema/actions.graphql.d.ts.map +1 -1
  155. package/dist/types/graphql/schema/index.d.ts +27 -17
  156. package/dist/types/graphql/schema/index.d.ts.map +1 -1
  157. package/dist/types/index.d.ts +6 -0
  158. package/dist/types/index.d.ts.map +1 -1
  159. package/dist/types/model/EventByIdQuery.d.ts.map +1 -1
  160. package/dist/types/model/EventsInProcessingQuery.d.ts.map +1 -1
  161. package/dist/types/model/EventsToProcessQuery.d.ts.map +1 -1
  162. package/dist/types/model/FailedEventsQuery.d.ts.map +1 -1
  163. package/dist/types/model/SetVariableCommand.d.ts.map +1 -1
  164. package/dist/types/model/VariablesManager.d.ts +16 -3
  165. package/dist/types/model/VariablesManager.d.ts.map +1 -1
  166. package/dist/types/model/VariablesQuery.d.ts.map +1 -1
  167. package/dist/types/triggers/TriggerHandler.d.ts.map +1 -1
  168. package/dist/types/triggers/TriggerIndirectChangesFetcher.d.ts.map +1 -1
  169. package/dist/types/triggers/TriggerListenersBuilder.d.ts.map +1 -1
  170. package/dist/types/triggers/TriggerListenersFactory.d.ts.map +1 -1
  171. package/dist/types/triggers/TriggerListenersStore.d.ts.map +1 -1
  172. package/dist/types/triggers/TriggerPayloadBuilder.d.ts.map +1 -1
  173. package/dist/types/triggers/TriggerPayloadManager.d.ts.map +1 -1
  174. package/dist/types/triggers/TriggerPayloadPersister.d.ts +3 -1
  175. package/dist/types/triggers/TriggerPayloadPersister.d.ts.map +1 -1
  176. package/dist/types/tsconfig.tsbuildinfo +1 -1
  177. package/dist/types/utils/map.d.ts.map +1 -1
  178. package/dist/types/utils/notifyChannel.d.ts.map +1 -1
  179. package/graphql.codegen.yml +1 -1
  180. package/package.json +18 -16
  181. package/src/ActionsExecutionContainerHookFactory.ts +6 -0
  182. package/src/ActionsMetrics.ts +135 -0
  183. package/src/dispatch/EventDispatcher.ts +36 -11
  184. package/src/dispatch/EventsRepository.ts +68 -26
  185. package/src/dispatch/ProjectDispatcher.ts +27 -11
  186. package/src/dispatch/WebhookTargetHandler.ts +36 -1
  187. package/src/graphql/resolvers/ResolversFactory.ts +1 -1
  188. package/src/graphql/resolvers/mutation/SetVariablesMutationResolver.ts +1 -1
  189. package/src/graphql/resolvers/query/EventsQueryResolver.ts +1 -1
  190. package/src/graphql/resolvers/query/VariablesQueryResolver.ts +1 -4
  191. package/src/graphql/schema/actions.graphql.ts +20 -8
  192. package/src/graphql/schema/index.ts +30 -17
  193. package/src/index.ts +27 -5
  194. package/src/model/VariablesManager.ts +66 -6
  195. package/src/triggers/TriggerPayloadPersister.ts +6 -0
  196. package/tests/cases/unit/actionsMetrics.test.ts +76 -0
  197. package/tests/cases/unit/eventsRepository.test.ts +136 -0
  198. package/tests/cases/unit/variablesManager.test.ts +154 -0
  199. package/tests/cases/unit/webhookHandler.test.ts +80 -24
  200. package/tests/src/event.ts +27 -0
@@ -27,7 +27,7 @@ export class ResolversFactory {
27
27
  } = {
28
28
  Json: JSONType,
29
29
  DateTime: DateTimeType,
30
- Uuid: UuidType,
30
+ UUID: UuidType,
31
31
  Mutation: {
32
32
  processBatch: this.processBatchMutationResolver.processBatch.bind(this.processBatchMutationResolver),
33
33
  setVariables: this.setVariablesMutationResolver.setVariables.bind(this.setVariablesMutationResolver),
@@ -12,7 +12,7 @@ export class SetVariablesMutationResolver implements MutationResolvers<ActionsCo
12
12
  async setVariables(parent: unknown, { args }: MutationSetVariablesArgs, ctx: ActionsContext) {
13
13
  await ctx.requireAccess(ActionsAuthorizationActions.VARIABLES_SET)
14
14
 
15
- await this.variablesManager.setVariables(ctx.db, args)
15
+ await this.variablesManager.setVariables(ctx.db, args, ctx.project.slug)
16
16
 
17
17
  return {
18
18
  ok: true,
@@ -51,7 +51,7 @@ export class EventsQueryResolver implements QueryResolvers<ActionsContext> {
51
51
  return rows.map(row => ({
52
52
  id: row.id,
53
53
  createdAt: row.created_at,
54
- resolvedAt: row.created_at,
54
+ resolvedAt: row.resolved_at ?? undefined,
55
55
  lastStateChange: row.last_state_change,
56
56
  visibleAt: row.visible_at,
57
57
  log: row.log,
@@ -12,9 +12,6 @@ export class VariablesQueryResolver implements QueryResolvers<ActionsContext> {
12
12
  async variables(parent: unknown, args: unknown, ctx: ActionsContext) {
13
13
  await ctx.requireAccess(ActionsAuthorizationActions.VARIABLES_VIEW)
14
14
 
15
- return Object.entries(await this.variablesManager.fetchVariables(ctx.db)).map(([name, value]) => ({
16
- name,
17
- value,
18
- }))
15
+ return await this.variablesManager.listVariables(ctx.db, ctx.project.slug)
19
16
  }
20
17
  }
@@ -4,7 +4,7 @@ import { DocumentNode } from 'graphql'
4
4
  export const schema: DocumentNode = gql`
5
5
  scalar DateTime
6
6
  scalar Json
7
- scalar Uuid
7
+ scalar UUID
8
8
 
9
9
  schema {
10
10
  query: Query
@@ -15,14 +15,14 @@ export const schema: DocumentNode = gql`
15
15
  failedEvents(args: EventArgs): [Event!]!
16
16
  eventsToProcess(args: EventArgs): [Event!]!
17
17
  eventsInProcessing(args: EventArgs): [Event!]!
18
- event(id: Uuid!): Event
18
+ event(id: UUID!): Event
19
19
  variables: [Variable!]!
20
20
  }
21
21
 
22
22
  type Mutation {
23
23
  processBatch: ProcessBatchResponse!
24
- retryEvent(id: Uuid!): RetryEventResponse!
25
- stopEvent(id: Uuid!): StopEventResponse!
24
+ retryEvent(id: UUID!): RetryEventResponse!
25
+ stopEvent(id: UUID!): StopEventResponse!
26
26
  setVariables(args: SetVariablesArgs!): SetVariablesResponse!
27
27
  }
28
28
 
@@ -33,9 +33,9 @@ export const schema: DocumentNode = gql`
33
33
  }
34
34
 
35
35
  type Event {
36
- id: Uuid!
37
- transactionId: Uuid!
38
- identityId: Uuid
36
+ id: UUID!
37
+ transactionId: UUID!
38
+ identityId: UUID
39
39
  ipAddress: String
40
40
  userAgent: String
41
41
  createdAt: DateTime!
@@ -99,7 +99,19 @@ export const schema: DocumentNode = gql`
99
99
 
100
100
  type Variable {
101
101
  name: String!
102
- value: String!
102
+ """Null when the value is not readable, which is every ENVIRONMENT one."""
103
+ value: String
104
+ source: VariableSource!
103
105
  }
104
106
 
107
+ """
108
+ Where the value comes from.
109
+ - DATABASE is stored in the project database, readable and writable with setVariables
110
+ - ENVIRONMENT is supplied by the engine environment, overrides a stored value of the same name, and is neither readable nor writable
111
+ """
112
+ enum VariableSource {
113
+ DATABASE
114
+ ENVIRONMENT
115
+ }
116
+
105
117
  `
@@ -16,14 +16,14 @@ export type Scalars = {
16
16
  Float: { input: number; output: number }
17
17
  DateTime: { input: Date; output: Date }
18
18
  Json: { input: any; output: any }
19
- Uuid: { input: string; output: string }
19
+ UUID: { input: string; output: string }
20
20
  }
21
21
 
22
22
  export type Event = {
23
23
  readonly __typename?: 'Event'
24
24
  readonly createdAt: Scalars['DateTime']['output']
25
- readonly id: Scalars['Uuid']['output']
26
- readonly identityId?: Maybe<Scalars['Uuid']['output']>
25
+ readonly id: Scalars['UUID']['output']
26
+ readonly identityId?: Maybe<Scalars['UUID']['output']>
27
27
  readonly ipAddress?: Maybe<Scalars['String']['output']>
28
28
  readonly lastStateChange: Scalars['DateTime']['output']
29
29
  readonly log: Scalars['Json']['output']
@@ -33,7 +33,7 @@ export type Event = {
33
33
  readonly stage: Scalars['String']['output']
34
34
  readonly state: EventState
35
35
  readonly target: Scalars['String']['output']
36
- readonly transactionId: Scalars['Uuid']['output']
36
+ readonly transactionId: Scalars['UUID']['output']
37
37
  readonly userAgent?: Maybe<Scalars['String']['output']>
38
38
  readonly visibleAt?: Maybe<Scalars['DateTime']['output']>
39
39
  }
@@ -61,7 +61,7 @@ export type Mutation = {
61
61
  }
62
62
 
63
63
  export type MutationRetryEventArgs = {
64
- id: Scalars['Uuid']['input']
64
+ id: Scalars['UUID']['input']
65
65
  }
66
66
 
67
67
  export type MutationSetVariablesArgs = {
@@ -69,7 +69,7 @@ export type MutationSetVariablesArgs = {
69
69
  }
70
70
 
71
71
  export type MutationStopEventArgs = {
72
- id: Scalars['Uuid']['input']
72
+ id: Scalars['UUID']['input']
73
73
  }
74
74
 
75
75
  export type ProcessBatchResponse = {
@@ -87,7 +87,7 @@ export type Query = {
87
87
  }
88
88
 
89
89
  export type QueryEventArgs = {
90
- id: Scalars['Uuid']['input']
90
+ id: Scalars['UUID']['input']
91
91
  }
92
92
 
93
93
  export type QueryEventsInProcessingArgs = {
@@ -136,7 +136,9 @@ export type StopEventResponse = {
136
136
  export type Variable = {
137
137
  readonly __typename?: 'Variable'
138
138
  readonly name: Scalars['String']['output']
139
- readonly value: Scalars['String']['output']
139
+ readonly source: VariableSource
140
+ /** Null when the value is not readable, which is every ENVIRONMENT one. */
141
+ readonly value?: Maybe<Scalars['String']['output']>
140
142
  }
141
143
 
142
144
  export type VariableInput = {
@@ -144,6 +146,15 @@ export type VariableInput = {
144
146
  readonly value: Scalars['String']['input']
145
147
  }
146
148
 
149
+ /**
150
+ * Where the value comes from.
151
+ * - DATABASE is stored in the project database, readable and writable with setVariables
152
+ * - ENVIRONMENT is supplied by the engine environment, overrides a stored value of the same name, and is neither readable nor writable
153
+ */
154
+ export type VariableSource =
155
+ | 'DATABASE'
156
+ | 'ENVIRONMENT'
157
+
147
158
  export type ResolverTypeWrapper<T> = Promise<T> | T
148
159
 
149
160
  export type ResolverWithResolve<TResult, TParent, TContext, TArgs> = {
@@ -228,9 +239,10 @@ export type ResolversTypes = {
228
239
  SetVariablesResponse: ResolverTypeWrapper<SetVariablesResponse>
229
240
  StopEventResponse: ResolverTypeWrapper<StopEventResponse>
230
241
  String: ResolverTypeWrapper<Scalars['String']['output']>
231
- Uuid: ResolverTypeWrapper<Scalars['Uuid']['output']>
242
+ UUID: ResolverTypeWrapper<Scalars['UUID']['output']>
232
243
  Variable: ResolverTypeWrapper<Variable>
233
244
  VariableInput: VariableInput
245
+ VariableSource: VariableSource
234
246
  }
235
247
 
236
248
  /** Mapping between all available schema types and the resolvers parents */
@@ -249,7 +261,7 @@ export type ResolversParentTypes = {
249
261
  SetVariablesResponse: SetVariablesResponse
250
262
  StopEventResponse: StopEventResponse
251
263
  String: Scalars['String']['output']
252
- Uuid: Scalars['Uuid']['output']
264
+ UUID: Scalars['UUID']['output']
253
265
  Variable: Variable
254
266
  VariableInput: VariableInput
255
267
  }
@@ -260,8 +272,8 @@ export interface DateTimeScalarConfig extends GraphQLScalarTypeConfig<ResolversT
260
272
 
261
273
  export type EventResolvers<ContextType = any, ParentType extends ResolversParentTypes['Event'] = ResolversParentTypes['Event']> = {
262
274
  createdAt?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>
263
- id?: Resolver<ResolversTypes['Uuid'], ParentType, ContextType>
264
- identityId?: Resolver<Maybe<ResolversTypes['Uuid']>, ParentType, ContextType>
275
+ id?: Resolver<ResolversTypes['UUID'], ParentType, ContextType>
276
+ identityId?: Resolver<Maybe<ResolversTypes['UUID']>, ParentType, ContextType>
265
277
  ipAddress?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>
266
278
  lastStateChange?: Resolver<ResolversTypes['DateTime'], ParentType, ContextType>
267
279
  log?: Resolver<ResolversTypes['Json'], ParentType, ContextType>
@@ -271,7 +283,7 @@ export type EventResolvers<ContextType = any, ParentType extends ResolversParent
271
283
  stage?: Resolver<ResolversTypes['String'], ParentType, ContextType>
272
284
  state?: Resolver<ResolversTypes['EventState'], ParentType, ContextType>
273
285
  target?: Resolver<ResolversTypes['String'], ParentType, ContextType>
274
- transactionId?: Resolver<ResolversTypes['Uuid'], ParentType, ContextType>
286
+ transactionId?: Resolver<ResolversTypes['UUID'], ParentType, ContextType>
275
287
  userAgent?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>
276
288
  visibleAt?: Resolver<Maybe<ResolversTypes['DateTime']>, ParentType, ContextType>
277
289
  __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>
@@ -328,13 +340,14 @@ export type StopEventResponseResolvers<
328
340
  __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>
329
341
  }
330
342
 
331
- export interface UuidScalarConfig extends GraphQLScalarTypeConfig<ResolversTypes['Uuid'], any> {
332
- name: 'Uuid'
343
+ export interface UUIDScalarConfig extends GraphQLScalarTypeConfig<ResolversTypes['UUID'], any> {
344
+ name: 'UUID'
333
345
  }
334
346
 
335
347
  export type VariableResolvers<ContextType = any, ParentType extends ResolversParentTypes['Variable'] = ResolversParentTypes['Variable']> = {
336
348
  name?: Resolver<ResolversTypes['String'], ParentType, ContextType>
337
- value?: Resolver<ResolversTypes['String'], ParentType, ContextType>
349
+ source?: Resolver<ResolversTypes['VariableSource'], ParentType, ContextType>
350
+ value?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>
338
351
  __isTypeOf?: IsTypeOfResolverFn<ParentType, ContextType>
339
352
  }
340
353
 
@@ -348,6 +361,6 @@ export type Resolvers<ContextType = any> = {
348
361
  RetryEventResponse?: RetryEventResponseResolvers<ContextType>
349
362
  SetVariablesResponse?: SetVariablesResponseResolvers<ContextType>
350
363
  StopEventResponse?: StopEventResponseResolvers<ContextType>
351
- Uuid?: GraphQLScalarType
364
+ UUID?: GraphQLScalarType
352
365
  Variable?: VariableResolvers<ContextType>
353
366
  }
package/src/index.ts CHANGED
@@ -28,6 +28,7 @@ import { VariablesManager } from './model/VariablesManager.js'
28
28
  import { AccessEvaluator, Authorizator } from '@contember/authorization'
29
29
  import { ActionsPermissionsFactory } from './authorization/index.js'
30
30
  import { WebhookFetcherNative } from './dispatch/WebhookFetcher.js'
31
+ import { ActionsMetrics } from './ActionsMetrics.js'
31
32
 
32
33
  export {
33
34
  TriggerHandler,
@@ -42,20 +43,33 @@ export { ListenerStoreProvider } from './ListenerStoreProvider.js'
42
43
  export default class ActionsPlugin implements Plugin {
43
44
  name = 'contember/actions'
44
45
 
46
+ /**
47
+ * Metrics singleton, created in {@link getMasterContainerHook} (where the Prometheus registry is
48
+ * available) and read back here in {@link getExecutionContainerHook}, which runs in the per-request
49
+ * content container with no access to master-level services.
50
+ */
51
+ private actionsMetrics: ActionsMetrics | undefined
52
+
45
53
  getSystemMigrations(): MigrationGroup {
46
54
  return migrationsGroup
47
55
  }
48
56
 
49
57
  getExecutionContainerHook() {
50
- const hookFactory = new ActionsExecutionContainerHookFactory(new ListenerStoreProvider())
58
+ const hookFactory = new ActionsExecutionContainerHookFactory(new ListenerStoreProvider(), () => this.actionsMetrics)
51
59
  return hookFactory.create()
52
60
  }
53
61
 
54
62
  getMasterContainerHook() {
55
63
  const hook: MasterContainerHook = (builder: MasterContainerBuilder) => {
56
64
  return builder
57
- .addService('actions_variableManager', () => {
58
- return new VariablesManager()
65
+ .addService('actions_metrics', ({ promRegistry }) => {
66
+ const metrics = new ActionsMetrics(promRegistry)
67
+ // expose to getExecutionContainerHook (runs in a different container scope)
68
+ this.actionsMetrics = metrics
69
+ return metrics
70
+ })
71
+ .addService('actions_variableManager', ({ env }) => {
72
+ return new VariablesManager(env)
59
73
  })
60
74
  .addService('actions_eventRepository', () => {
61
75
  return new EventsRepository()
@@ -65,8 +79,8 @@ export default class ActionsPlugin implements Plugin {
65
79
  const targetHandlerResolver = new TargetHandlerResolver(webhookTargetHandler)
66
80
  return new EventDispatcher(actions_eventRepository, actions_variableManager, targetHandlerResolver)
67
81
  })
68
- .addService('actions_dispatchWorkerSupervisorFactory', ({ actions_eventDispatcher }) => {
69
- const projectDispatcherFactory = new ProjectDispatcherFactory(actions_eventDispatcher)
82
+ .addService('actions_dispatchWorkerSupervisorFactory', ({ actions_eventDispatcher, actions_metrics }) => {
83
+ const projectDispatcherFactory = new ProjectDispatcherFactory(actions_eventDispatcher, actions_metrics)
70
84
  return new DispatchWorkerSupervisorFactory(projectDispatcherFactory)
71
85
  })
72
86
  .addService('actions_authorizator', () => {
@@ -84,6 +98,7 @@ export default class ActionsPlugin implements Plugin {
84
98
  actions_dispatchWorkerSupervisorFactory,
85
99
  actions_variableManager,
86
100
  actions_authorizator,
101
+ panelApiMount,
87
102
  },
88
103
  ) => {
89
104
  const handlerFactory = new ActionsGraphQLHandlerFactory()
@@ -113,6 +128,13 @@ export default class ActionsPlugin implements Plugin {
113
128
  )
114
129
  it.addRoute('actions', '/actions/:projectSlug', actionsMiddlewareFactory.create())
115
130
  it.addWebsocketRoute('actions', '/actions/_worker', actionsWebsocketMiddlewareFactory.create())
131
+ // Does nothing unless the panel is served; when it is, the panel's access gate applies
132
+ // and the UI learns the actions module has an API to talk to.
133
+ panelApiMount.mount(it, {
134
+ name: 'actions',
135
+ path: 'actions/:projectSlug',
136
+ controller: actionsMiddlewareFactory.create(),
137
+ })
116
138
  },
117
139
  )
118
140
  .setupService(
@@ -1,19 +1,54 @@
1
1
  import { DatabaseContext } from '@contember/engine-system-api'
2
2
  import { VariablesQuery } from './VariablesQuery.js'
3
- import { SetVariablesArgs } from '../graphql/schema/index.js'
3
+ import { SetVariablesArgs, Variable } from '../graphql/schema/index.js'
4
4
  import { DeleteBuilder } from '@contember/database'
5
5
  import { SetVariableCommand } from './SetVariableCommand.js'
6
+ import { Env, projectNameToEnvName } from '@contember/engine-http'
7
+ import { UserInputError } from '@contember/graphql-utils'
6
8
 
7
9
  export type VariablesMap = Record<string, string>
8
10
 
11
+ /** Between the project (or DEFAULT) prefix and the variable name: `MY_PROJECT_ACTIONS_VARIABLE_API_KEY`. */
12
+ const ENV_NAME_INFIX = '_ACTIONS_VARIABLE_'
13
+
9
14
  export class VariablesManager {
10
- public async fetchVariables(db: DatabaseContext): Promise<VariablesMap> {
11
- return Object.fromEntries((await db.queryHandler.fetch(new VariablesQuery())).map(it => [it.name, it.value]))
15
+ /** The environment does not change while the process runs, so each project resolves its prefixes once. */
16
+ private readonly envVariablesCache = new Map<string, VariablesMap>()
17
+
18
+ constructor(
19
+ private readonly env: Env,
20
+ ) {
21
+ }
22
+
23
+ /** Values for dispatch. A variable supplied by the environment shadows a stored row of the same name. */
24
+ public async fetchVariables(db: DatabaseContext, projectSlug: string): Promise<VariablesMap> {
25
+ return { ...await this.fetchStoredVariables(db), ...this.fetchEnvVariables(projectSlug) }
26
+ }
27
+
28
+ /**
29
+ * The listing is an inventory, not a way to read secrets: an environment value stays in the operator's
30
+ * secret store and is reported as a name with no value.
31
+ */
32
+ public async listVariables(db: DatabaseContext, projectSlug: string): Promise<Variable[]> {
33
+ const envVariables = this.fetchEnvVariables(projectSlug)
34
+ const stored = await this.fetchStoredVariables(db)
35
+ return [
36
+ ...Object.entries(stored)
37
+ .filter(([name]) => !Object.hasOwn(envVariables, name))
38
+ .map(([name, value]): Variable => ({ name, value, source: 'DATABASE' })),
39
+ ...Object.keys(envVariables).map((name): Variable => ({ name, value: null, source: 'ENVIRONMENT' })),
40
+ ].sort((a, b) => a.name.localeCompare(b.name))
12
41
  }
13
42
 
14
- public async setVariables(db: DatabaseContext, args: SetVariablesArgs) {
43
+ public async setVariables(db: DatabaseContext, args: SetVariablesArgs, projectSlug: string) {
44
+ const envVariables = this.fetchEnvVariables(projectSlug)
45
+ const readOnly = args.variables.map(it => it.name).filter(it => Object.hasOwn(envVariables, it))
46
+ if (readOnly.length > 0) {
47
+ throw new UserInputError(`Variables supplied by the environment are read-only: ${readOnly.join(', ')}`)
48
+ }
49
+
15
50
  return await db.transaction(async db => {
16
- const current = await this.fetchVariables(db)
51
+ const current = await this.fetchStoredVariables(db)
17
52
  const mode = args.mode ?? 'MERGE'
18
53
  const inputVariables = Object.fromEntries(args.variables.map(it => [it.name, it.value]))
19
54
 
@@ -30,7 +65,7 @@ export class VariablesManager {
30
65
  break
31
66
  }
32
67
 
33
- const toDelete = Object.keys(current).filter(it => !(it in newVariables))
68
+ const toDelete = Object.keys(current).filter(it => !Object.hasOwn(newVariables, it))
34
69
  if (toDelete.length > 0) {
35
70
  await DeleteBuilder.create()
36
71
  .from('actions_variable')
@@ -46,4 +81,29 @@ export class VariablesManager {
46
81
  }
47
82
  })
48
83
  }
84
+
85
+ private async fetchStoredVariables(db: DatabaseContext): Promise<VariablesMap> {
86
+ return Object.fromEntries((await db.queryHandler.fetch(new VariablesQuery())).map(it => [it.name, it.value]))
87
+ }
88
+
89
+ private fetchEnvVariables(projectSlug: string): VariablesMap {
90
+ const cached = this.envVariablesCache.get(projectSlug)
91
+ if (cached !== undefined) {
92
+ return cached
93
+ }
94
+ // the project prefix is read last, so it overrides the shared DEFAULT_ one
95
+ const prefixes = [`DEFAULT${ENV_NAME_INFIX}`, projectNameToEnvName(projectSlug) + ENV_NAME_INFIX]
96
+ const result: VariablesMap = {}
97
+ for (const prefix of prefixes) {
98
+ for (const [name, value] of Object.entries(this.env)) {
99
+ // an empty value counts as unset, the same as the `||` fallback of the config parameter resolver
100
+ if (!name.startsWith(prefix) || name.length === prefix.length || value === '') {
101
+ continue
102
+ }
103
+ result[name.slice(prefix.length)] = value
104
+ }
105
+ }
106
+ this.envVariablesCache.set(projectSlug, result)
107
+ return result
108
+ }
49
109
  }
@@ -3,6 +3,7 @@ import { Mapper, TriggeredActionEvent, TriggeredActionsCollector } from '@contem
3
3
  import { Actions, ActionsPayload } from '@contember/schema'
4
4
  import { EventRow } from '../model/types.js'
5
5
  import { notify } from '../utils/notifyChannel.js'
6
+ import { ProjectActionsMetrics } from '../ActionsMetrics.js'
6
7
 
7
8
  type EventRowToInsert = Omit<EventRow, 'created_at' | 'visible_at' | 'last_state_change' | 'log'> & {
8
9
  created_at: string | Date
@@ -21,6 +22,7 @@ export class TriggerPayloadPersister {
21
22
  private readonly identityId: string,
22
23
  private readonly userInfo: { ipAddress: string | null; userAgent: string | null },
23
24
  private readonly triggeredActionsCollector: TriggeredActionsCollector | undefined,
25
+ private readonly metrics: ProjectActionsMetrics | undefined,
24
26
  ) {
25
27
  }
26
28
 
@@ -62,6 +64,10 @@ export class TriggerPayloadPersister {
62
64
  .values(rows)
63
65
  .execute(this.client)
64
66
 
67
+ // Counted before commit (events are written in the mutation's transaction); a rollback
68
+ // would over-count enqueued. Acceptable for a backlog *trend*, which is the intended use.
69
+ this.metrics?.enqueued(rows.length)
70
+
65
71
  if (collected && collected.length > 0) {
66
72
  collector!.add(collected)
67
73
  }
@@ -0,0 +1,76 @@
1
+ import { beforeEach, describe, expect, test } from 'bun:test'
2
+ import { Registry } from 'prom-client'
3
+ import { ActionsMetrics } from '../../../src/ActionsMetrics.js'
4
+
5
+ // Parse the Prometheus text exposition (the actual /metrics output) rather than getMetricsAsJSON,
6
+ // whose return type doesn't expose sample values.
7
+ const valueFor = async (registry: Registry, name: string, project: string): Promise<number | undefined> => {
8
+ const text = await registry.metrics()
9
+ const line = text
10
+ .split('\n')
11
+ .find(it => it.startsWith(`${name}{`) && it.includes(`contember_project="${project}"`))
12
+ if (!line) {
13
+ return undefined
14
+ }
15
+ return Number(line.slice(line.lastIndexOf(' ') + 1))
16
+ }
17
+
18
+ describe('ActionsMetrics', () => {
19
+ let registry: Registry
20
+ let metrics: ActionsMetrics
21
+
22
+ beforeEach(() => {
23
+ registry = new Registry()
24
+ metrics = new ActionsMetrics(registry)
25
+ })
26
+
27
+ test('counts are labelled by project via forProject', async () => {
28
+ metrics.forProject('blog').enqueued(3)
29
+ metrics.forProject('shop').enqueued(5)
30
+
31
+ expect(await valueFor(registry, 'contember_actions_events_enqueued_total', 'blog')).toBe(3)
32
+ expect(await valueFor(registry, 'contember_actions_events_enqueued_total', 'shop')).toBe(5)
33
+ })
34
+
35
+ test('accumulates across calls', async () => {
36
+ const project = metrics.forProject('blog')
37
+ project.succeeded(2)
38
+ project.succeeded(4)
39
+ project.deliveryAttemptFailed(1)
40
+ project.failed(1)
41
+
42
+ expect(await valueFor(registry, 'contember_actions_events_succeeded_total', 'blog')).toBe(6)
43
+ expect(await valueFor(registry, 'contember_actions_delivery_attempts_failed_total', 'blog')).toBe(1)
44
+ expect(await valueFor(registry, 'contember_actions_events_failed_total', 'blog')).toBe(1)
45
+ })
46
+
47
+ test('zero counts are a no-op (no series created)', async () => {
48
+ metrics.forProject('blog').enqueued(0)
49
+
50
+ expect(await valueFor(registry, 'contember_actions_events_enqueued_total', 'blog')).toBeUndefined()
51
+ })
52
+
53
+ test('heartbeat sets a recent timestamp, dispose removes only the heartbeat series', async () => {
54
+ const project = metrics.forProject('blog')
55
+ project.succeeded(1)
56
+ project.heartbeat()
57
+
58
+ const beat = await valueFor(registry, 'contember_actions_worker_heartbeat_timestamp_seconds', 'blog')
59
+ expect(beat).toBeGreaterThan(1_700_000_000)
60
+
61
+ project.dispose()
62
+
63
+ // heartbeat series gone (a removed project must not look perpetually stuck)...
64
+ expect(await valueFor(registry, 'contember_actions_worker_heartbeat_timestamp_seconds', 'blog')).toBeUndefined()
65
+ // ...but the counter is untouched
66
+ expect(await valueFor(registry, 'contember_actions_events_succeeded_total', 'blog')).toBe(1)
67
+ })
68
+
69
+ test('crash counter increments per crash', async () => {
70
+ const project = metrics.forProject('blog')
71
+ project.crashed()
72
+ project.crashed()
73
+
74
+ expect(await valueFor(registry, 'contember_actions_worker_crashed_total', 'blog')).toBe(2)
75
+ })
76
+ })
@@ -0,0 +1,136 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { Client, Connection, EventManager } from '@contember/database'
3
+ import { createLogger, TestLoggerHandler } from '@contember/logger'
4
+ import { Actions } from '@contember/schema'
5
+ import { EventsRepository } from '../../../src/dispatch/EventsRepository.js'
6
+ import { HandledEvent } from '../../../src/dispatch/types.js'
7
+ import { createTestEvent } from '../../src/event.js'
8
+ import { testUuid } from '../../src/uuid.js'
9
+
10
+ /**
11
+ * Answers statements from a script, then with an empty result, so what is under test is the
12
+ * repository's own bookkeeping rather than any SQL.
13
+ */
14
+ const createStubClient = (responses: Connection.Result[] = []): Client => {
15
+ const remaining = [...responses]
16
+ const eventManager = new EventManager()
17
+ const query = async <Row extends Record<string, any>>(): Promise<Connection.Result<Row>> => {
18
+ const next = remaining.shift() ?? { rowCount: 0, rows: [] }
19
+ return { rowCount: next.rowCount, rows: next.rows as Row[] }
20
+ }
21
+ const transaction: Connection.TransactionLike = {
22
+ eventManager,
23
+ isClosed: false,
24
+ query,
25
+ scope: cb => Promise.resolve(cb(transaction)),
26
+ transaction: cb => Promise.resolve(cb(transaction)),
27
+ on: () => () => {},
28
+ rollback: async () => {},
29
+ commit: async () => {},
30
+ }
31
+ const connection: Connection.ConnectionLike = {
32
+ eventManager,
33
+ query,
34
+ scope: cb => Promise.resolve(cb(transaction)),
35
+ transaction: cb => Promise.resolve(cb(transaction)),
36
+ }
37
+ return new Client(connection, 'system', {})
38
+ }
39
+
40
+ const target: Actions.WebhookTarget = {
41
+ name: 'test_target',
42
+ type: 'webhook',
43
+ url: 'http://localhost',
44
+ maxAttempts: 3,
45
+ }
46
+
47
+ const createFailedEvent = (numRetries: number): HandledEvent => ({
48
+ row: createTestEvent(0, { num_retries: numRetries, state: 'processing' }),
49
+ target,
50
+ result: { ok: false, code: 401, errorMessage: 'Unauthorized' },
51
+ })
52
+
53
+ const errorsOf = (handler: TestLoggerHandler) => handler.messages.filter(it => it.level.name === 'error')
54
+
55
+ describe('persistProcessed', () => {
56
+ test('logs an error when the last attempt fails', async () => {
57
+ const testLoggerHandler = new TestLoggerHandler()
58
+
59
+ const result = await new EventsRepository().persistProcessed(
60
+ createStubClient(),
61
+ [createFailedEvent(2)],
62
+ createLogger(testLoggerHandler),
63
+ )
64
+
65
+ expect(result).toEqual({ succeeded: 0, retried: 0, failed: 1 })
66
+ const errors = errorsOf(testLoggerHandler)
67
+ expect(errors).toHaveLength(1)
68
+ expect(errors[0].message).toBe('Action event failed permanently, no attempts left')
69
+ // exact match: neither the target URL nor its headers may end up in the log
70
+ expect(errors[0].ownAttributes).toStrictEqual({
71
+ eventId: testUuid(1),
72
+ target: 'test_target',
73
+ trigger: 'test',
74
+ attempts: 3,
75
+ code: 401,
76
+ errorMessage: 'Unauthorized',
77
+ })
78
+ })
79
+
80
+ test('stays quiet while attempts remain', async () => {
81
+ const testLoggerHandler = new TestLoggerHandler()
82
+
83
+ const result = await new EventsRepository().persistProcessed(
84
+ createStubClient(),
85
+ [createFailedEvent(0)],
86
+ createLogger(testLoggerHandler),
87
+ )
88
+
89
+ expect(result).toEqual({ succeeded: 0, retried: 1, failed: 0 })
90
+ expect(errorsOf(testLoggerHandler)).toHaveLength(0)
91
+ })
92
+ })
93
+
94
+ describe('fetchBatch', () => {
95
+ const eventRow = createTestEvent(0, { state: 'processing' })
96
+
97
+ test('logs an error when the event names a target the schema does not have', async () => {
98
+ const testLoggerHandler = new TestLoggerHandler()
99
+ const client = createStubClient([
100
+ { rowCount: 1, rows: [eventRow] },
101
+ { rowCount: 1, rows: [] },
102
+ ])
103
+
104
+ const result = await new EventsRepository().fetchBatch(
105
+ { triggers: {}, targets: {} },
106
+ client,
107
+ createLogger(testLoggerHandler),
108
+ )
109
+
110
+ expect(result).toEqual({ ok: false, backoffMs: undefined, unknownTargetFailed: 1 })
111
+ const errors = errorsOf(testLoggerHandler)
112
+ expect(errors).toHaveLength(1)
113
+ expect(errors[0].message).toBe('Action event failed permanently, its target is not in the schema')
114
+ expect(errors[0].ownAttributes).toStrictEqual({
115
+ eventId: testUuid(1),
116
+ target: 'test_target',
117
+ trigger: 'test',
118
+ })
119
+ })
120
+
121
+ test('stays quiet when the target is in the schema', async () => {
122
+ const testLoggerHandler = new TestLoggerHandler()
123
+ const client = createStubClient([
124
+ { rowCount: 1, rows: [eventRow] },
125
+ ])
126
+
127
+ const result = await new EventsRepository().fetchBatch(
128
+ { triggers: {}, targets: { test_target: target } },
129
+ client,
130
+ createLogger(testLoggerHandler),
131
+ )
132
+
133
+ expect(result).toEqual({ ok: true, events: [eventRow], target, unknownTargetFailed: 0 })
134
+ expect(testLoggerHandler.messages).toStrictEqual([])
135
+ })
136
+ })