@contember/engine-actions 2.2.0-alpha.3 → 2.2.0-alpha.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.
- package/CLAUDE.md +58 -0
- package/dist/development/index.cjs +19 -7
- package/dist/development/index.cjs.map +1 -1
- package/dist/development/index.js +19 -7
- package/dist/development/index.js.map +1 -1
- package/dist/development/src/ActionsExecutionContainerHookFactory.cjs +5 -2
- package/dist/development/src/ActionsExecutionContainerHookFactory.cjs.map +1 -1
- package/dist/development/src/ActionsExecutionContainerHookFactory.js +5 -2
- package/dist/development/src/ActionsExecutionContainerHookFactory.js.map +1 -1
- package/dist/development/src/ActionsMetrics.cjs +98 -0
- package/dist/development/src/ActionsMetrics.cjs.map +1 -0
- package/dist/development/src/ActionsMetrics.js +98 -0
- package/dist/development/src/ActionsMetrics.js.map +1 -0
- package/dist/development/src/dispatch/EventDispatcher.cjs +27 -9
- package/dist/development/src/dispatch/EventDispatcher.cjs.map +1 -1
- package/dist/development/src/dispatch/EventDispatcher.js +27 -9
- package/dist/development/src/dispatch/EventDispatcher.js.map +1 -1
- package/dist/development/src/dispatch/EventsRepository.cjs +49 -20
- package/dist/development/src/dispatch/EventsRepository.cjs.map +1 -1
- package/dist/development/src/dispatch/EventsRepository.js +49 -20
- package/dist/development/src/dispatch/EventsRepository.js.map +1 -1
- package/dist/development/src/dispatch/ProjectDispatcher.cjs +21 -13
- package/dist/development/src/dispatch/ProjectDispatcher.cjs.map +1 -1
- package/dist/development/src/dispatch/ProjectDispatcher.js +21 -13
- package/dist/development/src/dispatch/ProjectDispatcher.js.map +1 -1
- package/dist/development/src/dispatch/WebhookTargetHandler.cjs +29 -2
- package/dist/development/src/dispatch/WebhookTargetHandler.cjs.map +1 -1
- package/dist/development/src/dispatch/WebhookTargetHandler.js +29 -2
- package/dist/development/src/dispatch/WebhookTargetHandler.js.map +1 -1
- package/dist/development/src/graphql/resolvers/ResolversFactory.cjs +1 -1
- package/dist/development/src/graphql/resolvers/ResolversFactory.cjs.map +1 -1
- package/dist/development/src/graphql/resolvers/ResolversFactory.js +1 -1
- package/dist/development/src/graphql/resolvers/ResolversFactory.js.map +1 -1
- package/dist/development/src/graphql/resolvers/mutation/SetVariablesMutationResolver.cjs +1 -1
- package/dist/development/src/graphql/resolvers/mutation/SetVariablesMutationResolver.cjs.map +1 -1
- package/dist/development/src/graphql/resolvers/mutation/SetVariablesMutationResolver.js +1 -1
- package/dist/development/src/graphql/resolvers/mutation/SetVariablesMutationResolver.js.map +1 -1
- package/dist/development/src/graphql/resolvers/query/EventsQueryResolver.cjs +1 -1
- package/dist/development/src/graphql/resolvers/query/EventsQueryResolver.cjs.map +1 -1
- package/dist/development/src/graphql/resolvers/query/EventsQueryResolver.js +1 -1
- package/dist/development/src/graphql/resolvers/query/EventsQueryResolver.js.map +1 -1
- package/dist/development/src/graphql/resolvers/query/VariablesQueryResolver.cjs +1 -4
- package/dist/development/src/graphql/resolvers/query/VariablesQueryResolver.cjs.map +1 -1
- package/dist/development/src/graphql/resolvers/query/VariablesQueryResolver.js +1 -4
- package/dist/development/src/graphql/resolvers/query/VariablesQueryResolver.js.map +1 -1
- package/dist/development/src/graphql/schema/actions.graphql.cjs +20 -8
- package/dist/development/src/graphql/schema/actions.graphql.cjs.map +1 -1
- package/dist/development/src/graphql/schema/actions.graphql.js +20 -8
- package/dist/development/src/graphql/schema/actions.graphql.js.map +1 -1
- package/dist/development/src/model/VariablesManager.cjs +54 -5
- package/dist/development/src/model/VariablesManager.cjs.map +1 -1
- package/dist/development/src/model/VariablesManager.js +54 -5
- package/dist/development/src/model/VariablesManager.js.map +1 -1
- package/dist/development/src/triggers/TriggerPayloadPersister.cjs +3 -1
- package/dist/development/src/triggers/TriggerPayloadPersister.cjs.map +1 -1
- package/dist/development/src/triggers/TriggerPayloadPersister.js +3 -1
- package/dist/development/src/triggers/TriggerPayloadPersister.js.map +1 -1
- package/dist/production/index.cjs +19 -7
- package/dist/production/index.cjs.map +1 -1
- package/dist/production/index.js +19 -7
- package/dist/production/index.js.map +1 -1
- package/dist/production/src/ActionsExecutionContainerHookFactory.cjs +5 -2
- package/dist/production/src/ActionsExecutionContainerHookFactory.cjs.map +1 -1
- package/dist/production/src/ActionsExecutionContainerHookFactory.js +5 -2
- package/dist/production/src/ActionsExecutionContainerHookFactory.js.map +1 -1
- package/dist/production/src/ActionsMetrics.cjs +98 -0
- package/dist/production/src/ActionsMetrics.cjs.map +1 -0
- package/dist/production/src/ActionsMetrics.js +98 -0
- package/dist/production/src/ActionsMetrics.js.map +1 -0
- package/dist/production/src/dispatch/EventDispatcher.cjs +27 -9
- package/dist/production/src/dispatch/EventDispatcher.cjs.map +1 -1
- package/dist/production/src/dispatch/EventDispatcher.js +27 -9
- package/dist/production/src/dispatch/EventDispatcher.js.map +1 -1
- package/dist/production/src/dispatch/EventsRepository.cjs +49 -20
- package/dist/production/src/dispatch/EventsRepository.cjs.map +1 -1
- package/dist/production/src/dispatch/EventsRepository.js +49 -20
- package/dist/production/src/dispatch/EventsRepository.js.map +1 -1
- package/dist/production/src/dispatch/ProjectDispatcher.cjs +21 -13
- package/dist/production/src/dispatch/ProjectDispatcher.cjs.map +1 -1
- package/dist/production/src/dispatch/ProjectDispatcher.js +21 -13
- package/dist/production/src/dispatch/ProjectDispatcher.js.map +1 -1
- package/dist/production/src/dispatch/WebhookTargetHandler.cjs +29 -2
- package/dist/production/src/dispatch/WebhookTargetHandler.cjs.map +1 -1
- package/dist/production/src/dispatch/WebhookTargetHandler.js +29 -2
- package/dist/production/src/dispatch/WebhookTargetHandler.js.map +1 -1
- package/dist/production/src/graphql/resolvers/ResolversFactory.cjs +1 -1
- package/dist/production/src/graphql/resolvers/ResolversFactory.cjs.map +1 -1
- package/dist/production/src/graphql/resolvers/ResolversFactory.js +1 -1
- package/dist/production/src/graphql/resolvers/ResolversFactory.js.map +1 -1
- package/dist/production/src/graphql/resolvers/mutation/SetVariablesMutationResolver.cjs +1 -1
- package/dist/production/src/graphql/resolvers/mutation/SetVariablesMutationResolver.cjs.map +1 -1
- package/dist/production/src/graphql/resolvers/mutation/SetVariablesMutationResolver.js +1 -1
- package/dist/production/src/graphql/resolvers/mutation/SetVariablesMutationResolver.js.map +1 -1
- package/dist/production/src/graphql/resolvers/query/EventsQueryResolver.cjs +1 -1
- package/dist/production/src/graphql/resolvers/query/EventsQueryResolver.cjs.map +1 -1
- package/dist/production/src/graphql/resolvers/query/EventsQueryResolver.js +1 -1
- package/dist/production/src/graphql/resolvers/query/EventsQueryResolver.js.map +1 -1
- package/dist/production/src/graphql/resolvers/query/VariablesQueryResolver.cjs +1 -4
- package/dist/production/src/graphql/resolvers/query/VariablesQueryResolver.cjs.map +1 -1
- package/dist/production/src/graphql/resolvers/query/VariablesQueryResolver.js +1 -4
- package/dist/production/src/graphql/resolvers/query/VariablesQueryResolver.js.map +1 -1
- package/dist/production/src/graphql/schema/actions.graphql.cjs +20 -8
- package/dist/production/src/graphql/schema/actions.graphql.cjs.map +1 -1
- package/dist/production/src/graphql/schema/actions.graphql.js +20 -8
- package/dist/production/src/graphql/schema/actions.graphql.js.map +1 -1
- package/dist/production/src/model/VariablesManager.cjs +54 -5
- package/dist/production/src/model/VariablesManager.cjs.map +1 -1
- package/dist/production/src/model/VariablesManager.js +54 -5
- package/dist/production/src/model/VariablesManager.js.map +1 -1
- package/dist/production/src/triggers/TriggerPayloadPersister.cjs +3 -1
- package/dist/production/src/triggers/TriggerPayloadPersister.cjs.map +1 -1
- package/dist/production/src/triggers/TriggerPayloadPersister.js +3 -1
- package/dist/production/src/triggers/TriggerPayloadPersister.js.map +1 -1
- package/dist/tests/cases/unit/actionsMetrics.test.d.ts +2 -0
- package/dist/tests/cases/unit/actionsMetrics.test.d.ts.map +1 -0
- package/dist/tests/cases/unit/eventsRepository.test.d.ts +2 -0
- package/dist/tests/cases/unit/eventsRepository.test.d.ts.map +1 -0
- package/dist/tests/cases/unit/variablesManager.test.d.ts +2 -0
- package/dist/tests/cases/unit/variablesManager.test.d.ts.map +1 -0
- package/dist/tests/src/event.d.ts +4 -0
- package/dist/tests/src/event.d.ts.map +1 -0
- package/dist/tests/src/gql.d.ts.map +1 -1
- package/dist/tests/src/uuid.d.ts.map +1 -1
- package/dist/tests/tsconfig.tsbuildinfo +1 -1
- package/dist/types/ActionsExecutionContainerHookFactory.d.ts +3 -1
- package/dist/types/ActionsExecutionContainerHookFactory.d.ts.map +1 -1
- package/dist/types/ActionsMetrics.d.ts +59 -0
- package/dist/types/ActionsMetrics.d.ts.map +1 -0
- package/dist/types/ListenerStoreProvider.d.ts.map +1 -1
- package/dist/types/authorization/ActionsPermissionsFactory.d.ts.map +1 -1
- package/dist/types/dispatch/DispatchWorkerSupervisor.d.ts.map +1 -1
- package/dist/types/dispatch/EventDispatcher.d.ts +12 -3
- package/dist/types/dispatch/EventDispatcher.d.ts.map +1 -1
- package/dist/types/dispatch/EventsRepository.d.ts +14 -2
- package/dist/types/dispatch/EventsRepository.d.ts.map +1 -1
- package/dist/types/dispatch/LazyDispatchWorker.d.ts.map +1 -1
- package/dist/types/dispatch/ProjectDispatcher.d.ts +5 -2
- package/dist/types/dispatch/ProjectDispatcher.d.ts.map +1 -1
- package/dist/types/dispatch/TargetHandlerResolver.d.ts.map +1 -1
- package/dist/types/dispatch/WebhookFetcher.d.ts.map +1 -1
- package/dist/types/dispatch/WebhookTargetHandler.d.ts.map +1 -1
- package/dist/types/graphql/http/ActionsApiMiddlewareFactory.d.ts.map +1 -1
- package/dist/types/graphql/http/ActionsContextResolver.d.ts.map +1 -1
- package/dist/types/graphql/http/ActionsGraphQLHandlerFactory.d.ts.map +1 -1
- package/dist/types/graphql/http/ActionsWebsocketControllerFactory.d.ts.map +1 -1
- package/dist/types/graphql/resolvers/ResolversFactory.d.ts.map +1 -1
- package/dist/types/graphql/resolvers/mutation/ProcessBatchMutationResolver.d.ts.map +1 -1
- package/dist/types/graphql/resolvers/mutation/RetryEventMutationResolver.d.ts.map +1 -1
- package/dist/types/graphql/resolvers/mutation/SetVariablesMutationResolver.d.ts.map +1 -1
- package/dist/types/graphql/resolvers/mutation/StopEventMutationResolver.d.ts.map +1 -1
- package/dist/types/graphql/resolvers/query/EventsQueryResolver.d.ts.map +1 -1
- package/dist/types/graphql/resolvers/query/VariablesQueryResolver.d.ts +1 -4
- package/dist/types/graphql/resolvers/query/VariablesQueryResolver.d.ts.map +1 -1
- package/dist/types/graphql/schema/actions.graphql.d.ts.map +1 -1
- package/dist/types/graphql/schema/index.d.ts +27 -17
- package/dist/types/graphql/schema/index.d.ts.map +1 -1
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/model/EventByIdQuery.d.ts.map +1 -1
- package/dist/types/model/EventsInProcessingQuery.d.ts.map +1 -1
- package/dist/types/model/EventsToProcessQuery.d.ts.map +1 -1
- package/dist/types/model/FailedEventsQuery.d.ts.map +1 -1
- package/dist/types/model/SetVariableCommand.d.ts.map +1 -1
- package/dist/types/model/VariablesManager.d.ts +16 -3
- package/dist/types/model/VariablesManager.d.ts.map +1 -1
- package/dist/types/model/VariablesQuery.d.ts.map +1 -1
- package/dist/types/triggers/TriggerHandler.d.ts.map +1 -1
- package/dist/types/triggers/TriggerIndirectChangesFetcher.d.ts.map +1 -1
- package/dist/types/triggers/TriggerListenersBuilder.d.ts.map +1 -1
- package/dist/types/triggers/TriggerListenersFactory.d.ts.map +1 -1
- package/dist/types/triggers/TriggerListenersStore.d.ts.map +1 -1
- package/dist/types/triggers/TriggerPayloadBuilder.d.ts.map +1 -1
- package/dist/types/triggers/TriggerPayloadManager.d.ts.map +1 -1
- package/dist/types/triggers/TriggerPayloadPersister.d.ts +3 -1
- package/dist/types/triggers/TriggerPayloadPersister.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/dist/types/utils/map.d.ts.map +1 -1
- package/dist/types/utils/notifyChannel.d.ts.map +1 -1
- package/graphql.codegen.yml +1 -1
- package/package.json +18 -16
- package/src/ActionsExecutionContainerHookFactory.ts +6 -0
- package/src/ActionsMetrics.ts +135 -0
- package/src/dispatch/EventDispatcher.ts +36 -11
- package/src/dispatch/EventsRepository.ts +68 -26
- package/src/dispatch/ProjectDispatcher.ts +27 -11
- package/src/dispatch/WebhookTargetHandler.ts +36 -1
- package/src/graphql/resolvers/ResolversFactory.ts +1 -1
- package/src/graphql/resolvers/mutation/SetVariablesMutationResolver.ts +1 -1
- package/src/graphql/resolvers/query/EventsQueryResolver.ts +1 -1
- package/src/graphql/resolvers/query/VariablesQueryResolver.ts +1 -4
- package/src/graphql/schema/actions.graphql.ts +20 -8
- package/src/graphql/schema/index.ts +30 -17
- package/src/index.ts +27 -5
- package/src/model/VariablesManager.ts +66 -6
- package/src/triggers/TriggerPayloadPersister.ts +6 -0
- package/tests/cases/unit/actionsMetrics.test.ts +76 -0
- package/tests/cases/unit/eventsRepository.test.ts +136 -0
- package/tests/cases/unit/variablesManager.test.ts +154 -0
- package/tests/cases/unit/webhookHandler.test.ts +80 -24
- 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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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:
|
|
18
|
+
event(id: UUID!): Event
|
|
19
19
|
variables: [Variable!]!
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
type Mutation {
|
|
23
23
|
processBatch: ProcessBatchResponse!
|
|
24
|
-
retryEvent(id:
|
|
25
|
-
stopEvent(id:
|
|
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:
|
|
37
|
-
transactionId:
|
|
38
|
-
identityId:
|
|
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
|
|
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
|
-
|
|
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['
|
|
26
|
-
readonly identityId?: Maybe<Scalars['
|
|
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['
|
|
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['
|
|
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['
|
|
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['
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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['
|
|
264
|
-
identityId?: Resolver<Maybe<ResolversTypes['
|
|
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['
|
|
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
|
|
332
|
-
name: '
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
58
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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.
|
|
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
|
|
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
|
+
})
|