@contember/engine-actions 1.3.0-alpha.8 → 1.3.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/authorization/ActionsAuthorizationActions.d.ts +27 -0
- package/dist/src/authorization/ActionsAuthorizationActions.d.ts.map +1 -0
- package/dist/src/authorization/ActionsAuthorizationActions.js +17 -0
- package/dist/src/authorization/ActionsAuthorizationActions.js.map +1 -0
- package/dist/src/authorization/ActionsPermissionsFactory.d.ts +5 -0
- package/dist/src/authorization/ActionsPermissionsFactory.d.ts.map +1 -0
- package/dist/src/authorization/ActionsPermissionsFactory.js +17 -0
- package/dist/src/authorization/ActionsPermissionsFactory.js.map +1 -0
- package/dist/src/authorization/Identity.d.ts +5 -0
- package/dist/src/authorization/Identity.d.ts.map +1 -0
- package/dist/src/authorization/Identity.js +3 -0
- package/dist/src/authorization/Identity.js.map +1 -0
- package/dist/src/authorization/index.d.ts +4 -0
- package/dist/src/authorization/index.d.ts.map +1 -0
- package/dist/src/authorization/index.js +20 -0
- package/dist/src/authorization/index.js.map +1 -0
- package/dist/src/dispatch/EventDispatcher.d.ts +2 -2
- package/dist/src/dispatch/EventDispatcher.d.ts.map +1 -1
- package/dist/src/dispatch/EventsRepository.d.ts +1 -1
- package/dist/src/dispatch/EventsRepository.d.ts.map +1 -1
- package/dist/src/dispatch/EventsRepository.js +1 -1
- package/dist/src/dispatch/EventsRepository.js.map +1 -1
- package/dist/src/dispatch/WebhookFetcher.d.ts +14 -0
- package/dist/src/dispatch/WebhookFetcher.d.ts.map +1 -0
- package/dist/src/dispatch/WebhookFetcher.js +17 -0
- package/dist/src/dispatch/WebhookFetcher.js.map +1 -0
- package/dist/src/dispatch/WebhookTargetHandler.d.ts +8 -0
- package/dist/src/dispatch/WebhookTargetHandler.d.ts.map +1 -1
- package/dist/src/dispatch/WebhookTargetHandler.js +145 -44
- package/dist/src/dispatch/WebhookTargetHandler.js.map +1 -1
- package/dist/src/dispatch/types.d.ts +23 -2
- package/dist/src/dispatch/types.d.ts.map +1 -1
- package/dist/src/graphql/http/ActionsApiMiddlewareFactory.d.ts +4 -1
- package/dist/src/graphql/http/ActionsApiMiddlewareFactory.d.ts.map +1 -1
- package/dist/src/graphql/http/ActionsApiMiddlewareFactory.js +10 -2
- package/dist/src/graphql/http/ActionsApiMiddlewareFactory.js.map +1 -1
- package/dist/src/graphql/http/ActionsContextResolver.d.ts +8 -5
- package/dist/src/graphql/http/ActionsContextResolver.d.ts.map +1 -1
- package/dist/src/graphql/http/ActionsContextResolver.js +6 -4
- package/dist/src/graphql/http/ActionsContextResolver.js.map +1 -1
- package/dist/src/graphql/http/ActionsGraphQLHandlerFactory.d.ts +1 -1
- package/dist/src/graphql/http/ActionsGraphQLHandlerFactory.d.ts.map +1 -1
- package/dist/src/graphql/resolvers/ActionsContext.d.ts +2 -0
- package/dist/src/graphql/resolvers/ActionsContext.d.ts.map +1 -1
- package/dist/src/graphql/resolvers/mutation/ProcessBatchMutationResolver.d.ts.map +1 -1
- package/dist/src/graphql/resolvers/mutation/ProcessBatchMutationResolver.js +2 -0
- package/dist/src/graphql/resolvers/mutation/ProcessBatchMutationResolver.js.map +1 -1
- package/dist/src/graphql/resolvers/mutation/SetVariablesMutationResolver.d.ts.map +1 -1
- package/dist/src/graphql/resolvers/mutation/SetVariablesMutationResolver.js +2 -0
- package/dist/src/graphql/resolvers/mutation/SetVariablesMutationResolver.js.map +1 -1
- package/dist/src/graphql/resolvers/query/EventsQueryResolver.d.ts.map +1 -1
- package/dist/src/graphql/resolvers/query/EventsQueryResolver.js +4 -0
- package/dist/src/graphql/resolvers/query/EventsQueryResolver.js.map +1 -1
- package/dist/src/graphql/resolvers/query/VariablesQueryResolver.d.ts.map +1 -1
- package/dist/src/graphql/resolvers/query/VariablesQueryResolver.js +2 -0
- package/dist/src/graphql/resolvers/query/VariablesQueryResolver.js.map +1 -1
- package/dist/src/graphql/schema/index.d.ts +43 -43
- package/dist/src/graphql/schema/index.d.ts.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +9 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/model/VariablesManager.d.ts +1 -1
- package/dist/src/model/VariablesManager.d.ts.map +1 -1
- package/dist/src/model/VariablesQuery.d.ts +1 -1
- package/dist/src/model/VariablesQuery.d.ts.map +1 -1
- package/dist/src/model/types.d.ts +7 -4
- package/dist/src/model/types.d.ts.map +1 -1
- package/dist/src/triggers/Payload.d.ts +9 -14
- package/dist/src/triggers/Payload.d.ts.map +1 -1
- package/dist/src/triggers/TriggerHandler.d.ts +1 -1
- package/dist/src/triggers/TriggerHandler.d.ts.map +1 -1
- package/dist/src/triggers/TriggerListenersStore.d.ts +1 -1
- package/dist/src/triggers/TriggerListenersStore.d.ts.map +1 -1
- package/dist/src/triggers/TriggerPayloadBuilder.js +4 -4
- package/dist/src/triggers/TriggerPayloadBuilder.js.map +1 -1
- package/dist/src/triggers/TriggerPayloadManager.d.ts +1 -1
- package/dist/src/triggers/TriggerPayloadManager.d.ts.map +1 -1
- package/dist/src/triggers/TriggerPayloadPersister.d.ts.map +1 -1
- package/dist/src/triggers/TriggerPayloadPersister.js +3 -1
- package/dist/src/triggers/TriggerPayloadPersister.js.map +1 -1
- package/dist/src/tsconfig.tsbuildinfo +1 -1
- package/dist/tests/cases/integration/triggers.rootWatch.test.js +63 -0
- package/dist/tests/cases/integration/triggers.rootWatch.test.js.map +1 -1
- package/dist/tests/cases/unit/webhookHandler.test.d.ts +2 -0
- package/dist/tests/cases/unit/webhookHandler.test.d.ts.map +1 -0
- package/dist/tests/cases/unit/webhookHandler.test.js +248 -0
- package/dist/tests/cases/unit/webhookHandler.test.js.map +1 -0
- package/dist/tests/tsconfig.tsbuildinfo +1 -1
- package/package.json +18 -17
- package/src/authorization/ActionsAuthorizationActions.ts +14 -0
- package/src/authorization/ActionsPermissionsFactory.ts +14 -0
- package/src/authorization/Identity.ts +4 -0
- package/src/authorization/index.ts +3 -0
- package/src/dispatch/EventsRepository.ts +1 -1
- package/src/dispatch/WebhookFetcher.ts +24 -0
- package/src/dispatch/WebhookTargetHandler.ts +151 -46
- package/src/dispatch/types.ts +27 -0
- package/src/graphql/http/ActionsApiMiddlewareFactory.ts +10 -1
- package/src/graphql/http/ActionsContextResolver.ts +15 -6
- package/src/graphql/resolvers/ActionsContext.ts +2 -0
- package/src/graphql/resolvers/mutation/ProcessBatchMutationResolver.ts +3 -0
- package/src/graphql/resolvers/mutation/SetVariablesMutationResolver.ts +3 -0
- package/src/graphql/resolvers/query/EventsQueryResolver.ts +7 -0
- package/src/graphql/resolvers/query/VariablesQueryResolver.ts +3 -0
- package/src/index.ts +9 -3
- package/src/model/types.ts +7 -3
- package/src/triggers/Payload.ts +0 -8
- package/src/triggers/TriggerPayloadBuilder.ts +1 -1
- package/src/triggers/TriggerPayloadPersister.ts +10 -1
- package/src/tsconfig.json +1 -0
- package/tests/cases/integration/triggers.rootWatch.test.ts +64 -0
- package/tests/cases/unit/webhookHandler.test.ts +275 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contember/engine-actions",
|
|
3
|
-
"version": "1.3.0-
|
|
3
|
+
"version": "1.3.0-beta.2",
|
|
4
4
|
"license": "proprietary",
|
|
5
5
|
"main": "dist/src/index.js",
|
|
6
6
|
"typings": "dist/src/index.d.ts",
|
|
@@ -10,27 +10,28 @@
|
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
12
|
"generate": "gql-gen --config graphql-actions.codegen.yml",
|
|
13
|
-
"test": "vitest"
|
|
13
|
+
"test": "vitest --no-threads"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@contember/
|
|
17
|
-
"@contember/database
|
|
18
|
-
"@contember/
|
|
19
|
-
"@contember/engine-
|
|
20
|
-
"@contember/engine-
|
|
21
|
-
"@contember/engine-
|
|
22
|
-
"@contember/engine-
|
|
23
|
-
"@contember/engine-
|
|
24
|
-
"@contember/
|
|
25
|
-
"@contember/
|
|
26
|
-
"@contember/
|
|
27
|
-
"@contember/
|
|
28
|
-
"@contember/
|
|
16
|
+
"@contember/authorization": "1.3.0-beta.2",
|
|
17
|
+
"@contember/database": "1.3.0-beta.2",
|
|
18
|
+
"@contember/database-migrations": "1.3.0-beta.2",
|
|
19
|
+
"@contember/engine-common": "1.3.0-beta.2",
|
|
20
|
+
"@contember/engine-content-api": "1.3.0-beta.2",
|
|
21
|
+
"@contember/engine-http": "1.3.0-beta.2",
|
|
22
|
+
"@contember/engine-plugins": "1.3.0-beta.2",
|
|
23
|
+
"@contember/engine-system-api": "1.3.0-beta.2",
|
|
24
|
+
"@contember/engine-tenant-api": "1.3.0-beta.2",
|
|
25
|
+
"@contember/graphql-utils": "1.3.0-beta.2",
|
|
26
|
+
"@contember/logger": "1.3.0-beta.2",
|
|
27
|
+
"@contember/schema": "1.3.0-beta.2",
|
|
28
|
+
"@contember/schema-utils": "1.3.0-beta.2",
|
|
29
|
+
"@contember/typesafe": "1.3.0-beta.2",
|
|
29
30
|
"@graphql-tools/schema": "^8.3.5",
|
|
30
31
|
"graphql-tag": "^2.12.5"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
|
-
"@contember/engine-api-tester": "
|
|
34
|
+
"@contember/engine-api-tester": "1.3.0-beta.2",
|
|
34
35
|
"@graphql-codegen/cli": "^2.6.2",
|
|
35
36
|
"@graphql-codegen/typescript": "^2.5.1",
|
|
36
37
|
"@graphql-codegen/typescript-operations": "^2.4.2",
|
|
@@ -42,4 +43,4 @@
|
|
|
42
43
|
"graphql": ">= 14.6.0",
|
|
43
44
|
"pg": "^8.5.0"
|
|
44
45
|
}
|
|
45
|
-
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Authorizator } from '@contember/authorization'
|
|
2
|
+
|
|
3
|
+
export namespace ActionsAuthorizationActions {
|
|
4
|
+
export enum Resources {
|
|
5
|
+
variables = 'variables',
|
|
6
|
+
events = 'events',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const VARIABLES_VIEW = Authorizator.createAction(Resources.variables, 'view')
|
|
10
|
+
export const VARIABLES_SET = Authorizator.createAction(Resources.variables, 'set')
|
|
11
|
+
|
|
12
|
+
export const EVENTS_VIEW = Authorizator.createAction(Resources.events, 'view')
|
|
13
|
+
export const EVENTS_PROCESS = Authorizator.createAction(Resources.events, 'process')
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Permissions } from '@contember/authorization'
|
|
2
|
+
import { ProjectRole } from '@contember/schema'
|
|
3
|
+
import { ActionsAuthorizationActions } from './ActionsAuthorizationActions'
|
|
4
|
+
|
|
5
|
+
export class ActionsPermissionsFactory {
|
|
6
|
+
public create(): Permissions {
|
|
7
|
+
const permissions = new Permissions()
|
|
8
|
+
permissions.allow(ProjectRole.ADMIN, { resource: Permissions.ALL, privilege: Permissions.ALL })
|
|
9
|
+
permissions.allow(ProjectRole.DEPLOYER, ActionsAuthorizationActions.VARIABLES_SET)
|
|
10
|
+
permissions.allow(ProjectRole.DEPLOYER, ActionsAuthorizationActions.VARIABLES_VIEW)
|
|
11
|
+
|
|
12
|
+
return permissions
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -16,7 +16,7 @@ import { eventsToProcessSpecification, eventsToProcessStateSpecification } from
|
|
|
16
16
|
const ACK_TIMEOUT_MS = 1_000 * 60 * 10 // 10 minutes
|
|
17
17
|
const DEFAULT_REPEAT_INTERVAL_MS = 5_000 // 5 seconds
|
|
18
18
|
const DEFAULT_MAX_ATTEMPTS = 10
|
|
19
|
-
const DEFAULT_BATCH_SIZE =
|
|
19
|
+
const DEFAULT_BATCH_SIZE = 1
|
|
20
20
|
|
|
21
21
|
export class EventsRepository {
|
|
22
22
|
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type FetcherResponse = {
|
|
2
|
+
readonly headers: Headers
|
|
3
|
+
readonly ok: boolean
|
|
4
|
+
readonly status: number
|
|
5
|
+
readonly statusText: string
|
|
6
|
+
readonly responseText: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface WebhookFetcher {
|
|
10
|
+
fetch(url: string, init: RequestInit): Promise<FetcherResponse>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class WebhookFetcherNative implements WebhookFetcher {
|
|
14
|
+
async fetch(url: string, init: RequestInit): Promise<FetcherResponse> {
|
|
15
|
+
const response = await fetch(url, init)
|
|
16
|
+
return {
|
|
17
|
+
ok: response.ok,
|
|
18
|
+
headers: response.headers,
|
|
19
|
+
status: response.status,
|
|
20
|
+
statusText: response.statusText,
|
|
21
|
+
responseText: await response.text(),
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -1,69 +1,173 @@
|
|
|
1
1
|
import { Actions } from '@contember/schema'
|
|
2
|
-
import { HandledEvent, InvokeHandler, InvokeHandlerArgs } from './types'
|
|
2
|
+
import { EventRow, HandledEvent, InvokeHandler, InvokeHandlerArgs, WebhookEvent, WebhookRequestPayload } from './types'
|
|
3
3
|
import { VariablesMap } from '../model/VariablesManager'
|
|
4
|
+
import * as Typesafe from '@contember/typesafe'
|
|
5
|
+
import { FetcherResponse, WebhookFetcher } from './WebhookFetcher'
|
|
4
6
|
|
|
5
7
|
const DEFAULT_TIMEOUT_MS = 30_000 // 30 seconds
|
|
6
8
|
|
|
9
|
+
const ResponseType = Typesafe.noExtraProps(Typesafe.object({
|
|
10
|
+
failures: Typesafe.array(
|
|
11
|
+
Typesafe.intersection(
|
|
12
|
+
Typesafe.object({
|
|
13
|
+
eventId: Typesafe.string,
|
|
14
|
+
}),
|
|
15
|
+
Typesafe.partial({
|
|
16
|
+
error: Typesafe.string,
|
|
17
|
+
}),
|
|
18
|
+
),
|
|
19
|
+
),
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
type EventResponseFactory = (eventRow: EventRow) => ({ ok: boolean; code?: number; response?: string; errorMessage?: string; durationMs?: number })
|
|
23
|
+
|
|
7
24
|
export class WebhookTargetHandler implements InvokeHandler<Actions.WebhookTarget> {
|
|
25
|
+
constructor(
|
|
26
|
+
private readonly fetcher: WebhookFetcher,
|
|
27
|
+
) {
|
|
28
|
+
}
|
|
29
|
+
|
|
8
30
|
public async handle({ target, events, logger, variables }: InvokeHandlerArgs<Actions.WebhookTarget>): Promise<HandledEvent[]> {
|
|
9
|
-
const abortController = new AbortController()
|
|
10
31
|
const timeoutMs = target.timeoutMs
|
|
11
32
|
|
|
12
33
|
const start = process.hrtime.bigint()
|
|
13
34
|
const getDuration = () => Math.floor(Number((process.hrtime.bigint() - start) / BigInt(1_000_000)))
|
|
35
|
+
|
|
36
|
+
let eventResponseFactory: EventResponseFactory
|
|
14
37
|
try {
|
|
15
|
-
const resolvedUrl = this.resolveVariables(target.url, variables)
|
|
16
|
-
const resolvedHeaders = Object.fromEntries(Object.entries(target.headers ?? {})
|
|
17
|
-
.map(([key, value]) => [key, this.resolveVariables(value, variables)]))
|
|
18
|
-
|
|
19
|
-
const response = await withTimeout({ abortController, timeoutMs }, async () => {
|
|
20
|
-
|
|
21
|
-
return await fetch(resolvedUrl, {
|
|
22
|
-
method: 'POST',
|
|
23
|
-
headers: {
|
|
24
|
-
['User-Agent']: 'Contember Actions',
|
|
25
|
-
...resolvedHeaders,
|
|
26
|
-
['Content-type']: 'application/json',
|
|
27
|
-
},
|
|
28
|
-
signal: abortController.signal,
|
|
29
|
-
body: JSON.stringify({
|
|
30
|
-
events: events.map(it => it.payload),
|
|
31
|
-
}),
|
|
32
|
-
})
|
|
33
|
-
})
|
|
34
|
-
let responseText
|
|
35
|
-
try {
|
|
36
|
-
responseText = await withTimeout({ abortController, timeoutMs }, async () => {
|
|
37
|
-
return await response.text()
|
|
38
|
-
})
|
|
39
|
-
} catch {
|
|
40
|
-
}
|
|
41
|
-
const result = {
|
|
42
|
-
ok: response.ok,
|
|
43
|
-
code: response.status,
|
|
44
|
-
durationMs: getDuration(),
|
|
45
|
-
response: responseText,
|
|
46
|
-
errorMessage: !response.ok ? response.statusText : undefined,
|
|
47
|
-
}
|
|
48
38
|
|
|
49
|
-
|
|
39
|
+
const response = await this.fetch(timeoutMs ?? DEFAULT_TIMEOUT_MS, target, variables, events)
|
|
40
|
+
|
|
41
|
+
eventResponseFactory = this.createResponseFactory({
|
|
42
|
+
response: response,
|
|
43
|
+
events,
|
|
44
|
+
})
|
|
50
45
|
|
|
51
|
-
return events.map(it => ({ target, row: it, result }))
|
|
52
46
|
} catch (e) {
|
|
53
47
|
logger.warn(e)
|
|
54
|
-
const errorMessages =
|
|
55
|
-
let err = e
|
|
56
|
-
while (typeof err === 'object' && err !== null && 'message' in err && typeof (err as any).message === 'string') {
|
|
57
|
-
errorMessages.push((err as any).message)
|
|
58
|
-
err = (err as any).cause
|
|
59
|
-
}
|
|
48
|
+
const errorMessages = this.extractErrorMessages(e)
|
|
60
49
|
const result = {
|
|
61
50
|
ok: false,
|
|
62
51
|
errorMessage: errorMessages.length ? errorMessages.join('; ') : undefined,
|
|
63
52
|
durationMs: getDuration(),
|
|
64
53
|
}
|
|
54
|
+
eventResponseFactory = () => ({ ok: false, errorMessage: errorMessages.length ? errorMessages.join('; ') : undefined })
|
|
55
|
+
|
|
65
56
|
return events.map(it => ({ target, row: it, result }))
|
|
66
57
|
}
|
|
58
|
+
const durationMs = getDuration()
|
|
59
|
+
|
|
60
|
+
return events.map(it => ({ target, row: it, result: { durationMs, ...eventResponseFactory(it) } }))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
private createResponseFactory({ response, events }: {
|
|
64
|
+
response: FetcherResponse
|
|
65
|
+
events: EventRow[]
|
|
66
|
+
}): EventResponseFactory {
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
return () => ({
|
|
69
|
+
ok: false,
|
|
70
|
+
code: response.status,
|
|
71
|
+
response: response.responseText,
|
|
72
|
+
errorMessage: response.statusText,
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (response.responseText.trim() === ''
|
|
77
|
+
|| !response.headers.get('content-type')?.toLowerCase().includes('json')
|
|
78
|
+
) {
|
|
79
|
+
return () => ({
|
|
80
|
+
ok: true,
|
|
81
|
+
code: response.status,
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const responseData = ResponseType(JSON.parse(response.responseText))
|
|
87
|
+
const eventsInBatch = new Set(events.map(it => it.id))
|
|
88
|
+
const failedEvents = Object.fromEntries(responseData.failures?.map(it => [it.eventId, it]) ?? [])
|
|
89
|
+
const missingEvents = Object.keys(failedEvents).filter(it => !eventsInBatch.has(it))
|
|
90
|
+
|
|
91
|
+
if (missingEvents.length) {
|
|
92
|
+
return () => ({
|
|
93
|
+
ok: false,
|
|
94
|
+
code: response.status,
|
|
95
|
+
response: response.responseText,
|
|
96
|
+
errorMessage: 'Invalid response: undefined events IDs: ' + missingEvents.join('; '),
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return it => {
|
|
101
|
+
if (!failedEvents[it.id]) {
|
|
102
|
+
return { ok: true, code: response.status }
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
code: response.status,
|
|
107
|
+
errorMessage: failedEvents[it.id].error,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch (e: any) {
|
|
111
|
+
return () => ({
|
|
112
|
+
ok: false,
|
|
113
|
+
code: response.status,
|
|
114
|
+
response: response.responseText,
|
|
115
|
+
errorMessage: 'Invalid response: ' + e.message,
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private extractErrorMessages(e: unknown): string[] {
|
|
121
|
+
const errorMessages = []
|
|
122
|
+
let err = e
|
|
123
|
+
while (typeof err === 'object' && err !== null && 'message' in err && typeof (err as any).message === 'string') {
|
|
124
|
+
errorMessages.push((err as any).message)
|
|
125
|
+
err = (err as any).cause
|
|
126
|
+
}
|
|
127
|
+
return errorMessages
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private async fetch(timeoutMs: number, target: Actions.WebhookTarget, variables: VariablesMap, events: EventRow[]): Promise<FetcherResponse> {
|
|
131
|
+
return await withTimeout(timeoutMs, async abortController => {
|
|
132
|
+
return await this.doFetch(target, variables, abortController, events)
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private async doFetch(target: Actions.WebhookTarget, variables: VariablesMap, abortController: AbortController, events: EventRow[]): Promise<FetcherResponse> {
|
|
137
|
+
const resolvedUrl = this.resolveVariables(target.url, variables)
|
|
138
|
+
const resolvedHeaders = Object.fromEntries(Object.entries(target.headers ?? {})
|
|
139
|
+
.map(([key, value]) => [key, this.resolveVariables(value, variables)]))
|
|
140
|
+
|
|
141
|
+
const payload: WebhookRequestPayload = {
|
|
142
|
+
events: events.map(this.formatEventPayload),
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return await this.fetcher.fetch(resolvedUrl, {
|
|
146
|
+
method: 'POST',
|
|
147
|
+
headers: {
|
|
148
|
+
['User-Agent']: 'Contember Actions',
|
|
149
|
+
...resolvedHeaders,
|
|
150
|
+
['Content-type']: 'application/json',
|
|
151
|
+
},
|
|
152
|
+
signal: abortController.signal,
|
|
153
|
+
body: JSON.stringify(payload),
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private formatEventPayload(it: EventRow): WebhookEvent {
|
|
158
|
+
return {
|
|
159
|
+
meta: {
|
|
160
|
+
eventId: it.id,
|
|
161
|
+
transactionId: it.transaction_id,
|
|
162
|
+
createdAt: it.created_at.toISOString(),
|
|
163
|
+
lastStateChange: it.last_state_change.toISOString(),
|
|
164
|
+
numRetries: it.num_retries,
|
|
165
|
+
trigger: it.trigger,
|
|
166
|
+
target: it.target,
|
|
167
|
+
// todo: add stage and schema version
|
|
168
|
+
},
|
|
169
|
+
...it.payload,
|
|
170
|
+
}
|
|
67
171
|
}
|
|
68
172
|
|
|
69
173
|
private resolveVariables(subject: string, variables: VariablesMap) {
|
|
@@ -76,12 +180,13 @@ export class WebhookTargetHandler implements InvokeHandler<Actions.WebhookTarget
|
|
|
76
180
|
}
|
|
77
181
|
}
|
|
78
182
|
|
|
79
|
-
const withTimeout = async <T>(
|
|
183
|
+
const withTimeout = async <T>(timeoutMs: number, cb: (abortController: AbortController) => Promise<T>): Promise<T> => {
|
|
184
|
+
const abortController = new AbortController()
|
|
80
185
|
const timeoutId = setTimeout(() => {
|
|
81
186
|
abortController.abort()
|
|
82
|
-
}, timeoutMs
|
|
187
|
+
}, timeoutMs)
|
|
83
188
|
try {
|
|
84
|
-
return await cb()
|
|
189
|
+
return await cb(abortController)
|
|
85
190
|
} finally {
|
|
86
191
|
clearTimeout(timeoutId)
|
|
87
192
|
}
|
package/src/dispatch/types.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Logger } from '@contember/logger'
|
|
|
2
2
|
import { Actions } from '@contember/schema'
|
|
3
3
|
import { EventRow } from '../model/types'
|
|
4
4
|
import { VariablesMap } from '../model/VariablesManager'
|
|
5
|
+
import { AnyEventPayload } from '../triggers/Payload'
|
|
5
6
|
|
|
6
7
|
export { EventRow }
|
|
7
8
|
|
|
@@ -29,3 +30,29 @@ export type InvocationResult = {
|
|
|
29
30
|
code?: number
|
|
30
31
|
response?: string
|
|
31
32
|
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
export type WebhookEvent =
|
|
36
|
+
& AnyEventPayload
|
|
37
|
+
& {
|
|
38
|
+
meta: {
|
|
39
|
+
eventId: string
|
|
40
|
+
transactionId: string
|
|
41
|
+
createdAt: string
|
|
42
|
+
lastStateChange: string
|
|
43
|
+
numRetries: number
|
|
44
|
+
trigger: string
|
|
45
|
+
target: string
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type WebhookRequestPayload = {
|
|
50
|
+
events: WebhookEvent[]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type WebhookResponsePayload = {
|
|
54
|
+
failures: {
|
|
55
|
+
eventId: string
|
|
56
|
+
error?: string
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -2,18 +2,22 @@ import { HttpController } from '@contember/engine-http'
|
|
|
2
2
|
import { ActionsGraphQLHandler } from './ActionsGraphQLHandlerFactory'
|
|
3
3
|
import { ActionsContext } from '../resolvers/ActionsContext'
|
|
4
4
|
import { ActionsContextResolver } from './ActionsContextResolver'
|
|
5
|
+
import { AuthorizationScope, Authorizator } from '@contember/authorization'
|
|
6
|
+
import { Identity } from '../../authorization'
|
|
7
|
+
import { ForbiddenError } from '@contember/graphql-utils'
|
|
5
8
|
|
|
6
9
|
export class ActionsApiMiddlewareFactory {
|
|
7
10
|
constructor(
|
|
8
11
|
private readonly actionsContextResolver: ActionsContextResolver,
|
|
9
12
|
private readonly actionsGraphQLHandler: ActionsGraphQLHandler,
|
|
13
|
+
private readonly authorizator: Authorizator<Identity>,
|
|
10
14
|
) {
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
create(): HttpController {
|
|
14
18
|
return async ctx => {
|
|
15
19
|
const { timer, logger, koa } = ctx
|
|
16
|
-
const { projectContainer } = await this.actionsContextResolver.resolve(ctx)
|
|
20
|
+
const { projectContainer, identity } = await this.actionsContextResolver.resolve(ctx)
|
|
17
21
|
|
|
18
22
|
const systemDatabase = projectContainer.systemDatabaseContextFactory.create()
|
|
19
23
|
|
|
@@ -22,6 +26,11 @@ export class ActionsApiMiddlewareFactory {
|
|
|
22
26
|
db: systemDatabase,
|
|
23
27
|
logger,
|
|
24
28
|
contentSchemaResolver: projectContainer.contentSchemaResolver,
|
|
29
|
+
requireAccess: async (action, message) => {
|
|
30
|
+
if (!(await this.authorizator.isAllowed(identity, new AuthorizationScope.Global(), action))) {
|
|
31
|
+
throw new ForbiddenError(message || 'Forbidden')
|
|
32
|
+
}
|
|
33
|
+
},
|
|
25
34
|
}
|
|
26
35
|
const handler = this.actionsGraphQLHandler
|
|
27
36
|
|
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
import { ApplicationContext, HttpErrorResponse, ProjectContextResolver } from '@contember/engine-http'
|
|
2
|
-
import {
|
|
1
|
+
import { ApplicationContext, HttpErrorResponse, ProjectConfig, ProjectContainer, ProjectContextResolver } from '@contember/engine-http'
|
|
2
|
+
import { Identity } from '../../authorization'
|
|
3
|
+
|
|
4
|
+
export interface ActionsContext {
|
|
5
|
+
projectContainer: ProjectContainer
|
|
6
|
+
project: ProjectConfig
|
|
7
|
+
identity: Identity
|
|
8
|
+
}
|
|
3
9
|
|
|
4
10
|
export class ActionsContextResolver {
|
|
5
11
|
constructor(
|
|
@@ -8,7 +14,7 @@ export class ActionsContextResolver {
|
|
|
8
14
|
) {
|
|
9
15
|
}
|
|
10
16
|
|
|
11
|
-
async resolve(ctx: ApplicationContext) {
|
|
17
|
+
async resolve(ctx: ApplicationContext): Promise<ActionsContext> {
|
|
12
18
|
const { timer, projectGroup: { tenantContainer }, logger, authResult } = ctx
|
|
13
19
|
const projectContext = await this.projectContextResolver.resolve(ctx)
|
|
14
20
|
const { project } = projectContext
|
|
@@ -27,12 +33,15 @@ export class ActionsContextResolver {
|
|
|
27
33
|
)
|
|
28
34
|
logger.debug('Memberships fetched', { memberships })
|
|
29
35
|
|
|
30
|
-
|
|
31
|
-
if (!isProjectAdmin) {
|
|
36
|
+
if (memberships.length === 0) {
|
|
32
37
|
throw this.debug
|
|
33
38
|
? new HttpErrorResponse(404, `You are not allowed to access project ${project.slug}`)
|
|
34
39
|
: new HttpErrorResponse(404, `Project ${project.slug} NOT found`)
|
|
35
40
|
}
|
|
36
|
-
|
|
41
|
+
const roles = memberships.map(it => it.role)
|
|
42
|
+
return {
|
|
43
|
+
...projectContext,
|
|
44
|
+
identity: { id: authResult.identityId, roles },
|
|
45
|
+
}
|
|
37
46
|
}
|
|
38
47
|
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { DatabaseContext } from '@contember/engine-system-api'
|
|
2
2
|
import { ContentSchemaResolver } from '@contember/engine-http'
|
|
3
3
|
import { Logger } from '@contember/logger'
|
|
4
|
+
import { Authorizator } from '@contember/authorization'
|
|
4
5
|
|
|
5
6
|
export interface ActionsContext {
|
|
6
7
|
logger: Logger
|
|
7
8
|
db: DatabaseContext
|
|
8
9
|
contentSchemaResolver: ContentSchemaResolver
|
|
10
|
+
requireAccess: (action: Authorizator.Action, message?: string) => Promise<void>
|
|
9
11
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { MutationResolvers } from '../../schema'
|
|
2
2
|
import { ActionsContext } from '../ActionsContext'
|
|
3
3
|
import { EventDispatcher } from '../../../dispatch/EventDispatcher'
|
|
4
|
+
import { ActionsAuthorizationActions } from '../../../authorization'
|
|
4
5
|
|
|
5
6
|
export class ProcessBatchMutationResolver implements MutationResolvers<ActionsContext> {
|
|
6
7
|
constructor(
|
|
@@ -8,6 +9,8 @@ export class ProcessBatchMutationResolver implements MutationResolvers<ActionsCo
|
|
|
8
9
|
) {
|
|
9
10
|
}
|
|
10
11
|
async processBatch(parent: unknown, args: unknown, ctx: ActionsContext) {
|
|
12
|
+
await ctx.requireAccess(ActionsAuthorizationActions.EVENTS_PROCESS)
|
|
13
|
+
|
|
11
14
|
await this.eventDispatcher.processBatch(ctx)
|
|
12
15
|
return {
|
|
13
16
|
ok: true,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { MutationResolvers, MutationSetVariablesArgs } from '../../schema'
|
|
2
2
|
import { ActionsContext } from '../ActionsContext'
|
|
3
3
|
import { VariablesManager } from '../../../model/VariablesManager'
|
|
4
|
+
import { ActionsAuthorizationActions } from '../../../authorization'
|
|
4
5
|
|
|
5
6
|
export class SetVariablesMutationResolver implements MutationResolvers<ActionsContext> {
|
|
6
7
|
constructor(
|
|
@@ -10,6 +11,8 @@ export class SetVariablesMutationResolver implements MutationResolvers<ActionsCo
|
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
async setVariables(parent: unknown, { args }: MutationSetVariablesArgs, ctx: ActionsContext) {
|
|
14
|
+
await ctx.requireAccess(ActionsAuthorizationActions.VARIABLES_SET)
|
|
15
|
+
|
|
13
16
|
await this.variablesManager.setVariables(ctx.db, args)
|
|
14
17
|
|
|
15
18
|
return {
|
|
@@ -12,17 +12,24 @@ import { DatabaseContext, StagesQuery } from '@contember/engine-system-api'
|
|
|
12
12
|
import { Client, DatabaseQuery } from '@contember/database'
|
|
13
13
|
import { EventsInProcessingQuery } from '../../../model/EventsInProcessingQuery'
|
|
14
14
|
import { FailedEventsQuery } from '../../../model/FailedEventsQuery'
|
|
15
|
+
import { ActionsAuthorizationActions } from '../../../authorization'
|
|
15
16
|
|
|
16
17
|
export class EventsQueryResolver implements QueryResolvers<ActionsContext> {
|
|
17
18
|
async eventsInProcessing(parent: unknown, { args }: QueryEventsInProcessingArgs, ctx: ActionsContext) {
|
|
19
|
+
await ctx.requireAccess(ActionsAuthorizationActions.EVENTS_VIEW)
|
|
20
|
+
|
|
18
21
|
return await this.fetchEvents(ctx.db, new EventsInProcessingQuery(args ?? {}))
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
async eventsToProcess(parent: unknown, { args }: QueryEventsToProcessArgs, ctx: ActionsContext) {
|
|
25
|
+
await ctx.requireAccess(ActionsAuthorizationActions.EVENTS_VIEW)
|
|
26
|
+
|
|
22
27
|
return await this.fetchEvents(ctx.db, new EventsToProcessQuery(args ?? {}))
|
|
23
28
|
}
|
|
24
29
|
|
|
25
30
|
async failedEvents(parent: unknown, { args }: QueryFailedEventsArgs, ctx: ActionsContext) {
|
|
31
|
+
await ctx.requireAccess(ActionsAuthorizationActions.EVENTS_VIEW)
|
|
32
|
+
|
|
26
33
|
return await this.fetchEvents(ctx.db, new FailedEventsQuery(args ?? {}))
|
|
27
34
|
}
|
|
28
35
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { QueryResolvers } from '../../schema'
|
|
2
2
|
import { ActionsContext } from '../ActionsContext'
|
|
3
3
|
import { VariablesManager } from '../../../model/VariablesManager'
|
|
4
|
+
import { ActionsAuthorizationActions } from '../../../authorization'
|
|
4
5
|
|
|
5
6
|
export class VariablesQueryResolver implements QueryResolvers<ActionsContext> {
|
|
6
7
|
|
|
@@ -10,6 +11,8 @@ export class VariablesQueryResolver implements QueryResolvers<ActionsContext> {
|
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
async variables(parent: unknown, args: unknown, ctx: ActionsContext) {
|
|
14
|
+
await ctx.requireAccess(ActionsAuthorizationActions.VARIABLES_VIEW)
|
|
15
|
+
|
|
13
16
|
return Object.entries(await this.variablesManager.fetchVariables(ctx.db)).map(([name, value]) => ({
|
|
14
17
|
name,
|
|
15
18
|
value,
|
package/src/index.ts
CHANGED
|
@@ -18,6 +18,9 @@ import { DispatchWorkerSupervisorFactory } from './dispatch/DispatchWorkerSuperv
|
|
|
18
18
|
import { ProjectDispatcherFactory } from './dispatch/ProjectDispatcher'
|
|
19
19
|
import { VariablesQueryResolver } from './graphql/resolvers/query/VariablesQueryResolver'
|
|
20
20
|
import { VariablesManager } from './model/VariablesManager'
|
|
21
|
+
import { AccessEvaluator, Authorizator } from '@contember/authorization'
|
|
22
|
+
import { ActionsPermissionsFactory } from './authorization'
|
|
23
|
+
import { WebhookFetcherNative } from './dispatch/WebhookFetcher'
|
|
21
24
|
|
|
22
25
|
export {
|
|
23
26
|
TriggerListenersFactory,
|
|
@@ -49,7 +52,7 @@ export default class ActionsPlugin implements Plugin {
|
|
|
49
52
|
})
|
|
50
53
|
.addService('actions_eventDispatcher', ({ actions_variableManager }) => {
|
|
51
54
|
const eventsRepository = new EventsRepository()
|
|
52
|
-
const webhookTargetHandler = new WebhookTargetHandler()
|
|
55
|
+
const webhookTargetHandler = new WebhookTargetHandler(new WebhookFetcherNative())
|
|
53
56
|
const targetHandlerResolver = new TargetHandlerResolver(webhookTargetHandler)
|
|
54
57
|
return new EventDispatcher(eventsRepository, actions_variableManager, targetHandlerResolver)
|
|
55
58
|
})
|
|
@@ -57,7 +60,10 @@ export default class ActionsPlugin implements Plugin {
|
|
|
57
60
|
const projectDispatcherFactory = new ProjectDispatcherFactory(actions_eventDispatcher)
|
|
58
61
|
return new DispatchWorkerSupervisorFactory(projectDispatcherFactory)
|
|
59
62
|
})
|
|
60
|
-
.
|
|
63
|
+
.addService('actions_authorizator', () => {
|
|
64
|
+
return new Authorizator.Default(new AccessEvaluator.PermissionEvaluator(new ActionsPermissionsFactory().create()))
|
|
65
|
+
})
|
|
66
|
+
.setupService('application', (it, { projectContextResolver, debugMode, actions_eventDispatcher, actions_dispatchWorkerSupervisorFactory, actions_variableManager, actions_authorizator }) => {
|
|
61
67
|
const handlerFactory = new ActionsGraphQLHandlerFactory()
|
|
62
68
|
const eventsQueryResolver = new EventsQueryResolver()
|
|
63
69
|
const processBatchMutationResolver = new ProcessBatchMutationResolver(actions_eventDispatcher)
|
|
@@ -71,7 +77,7 @@ export default class ActionsPlugin implements Plugin {
|
|
|
71
77
|
)
|
|
72
78
|
|
|
73
79
|
const actionsContextResolver = new ActionsContextResolver(debugMode, projectContextResolver)
|
|
74
|
-
const actionsMiddlewareFactory = new ActionsApiMiddlewareFactory(actionsContextResolver, handlerFactory.create(resolversFactory))
|
|
80
|
+
const actionsMiddlewareFactory = new ActionsApiMiddlewareFactory(actionsContextResolver, handlerFactory.create(resolversFactory), actions_authorizator)
|
|
75
81
|
const actionsWebsocketMiddlewareFactory = new ActionsWebsocketControllerFactory(debugMode, actionsContextResolver, actions_dispatchWorkerSupervisorFactory)
|
|
76
82
|
it.addRoute('actions', '/actions/:projectSlug', actionsMiddlewareFactory.create())
|
|
77
83
|
it.addWebsocketRoute('actions', '/actions/_worker', actionsWebsocketMiddlewareFactory.create())
|
package/src/model/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { AnyEventPayload } from '../triggers/Payload'
|
|
2
|
+
|
|
1
3
|
export type EventRow = {
|
|
2
4
|
id: string
|
|
3
5
|
transaction_id: string
|
|
@@ -7,9 +9,11 @@ export type EventRow = {
|
|
|
7
9
|
num_retries: number
|
|
8
10
|
state: 'created' | 'retrying' | 'processing' | 'succeed' | 'failed' | 'stopped'
|
|
9
11
|
last_state_change: Date
|
|
10
|
-
stage_id:
|
|
11
|
-
schema_id:
|
|
12
|
+
stage_id: string
|
|
13
|
+
schema_id: number
|
|
12
14
|
target: string
|
|
13
|
-
|
|
15
|
+
priority: number
|
|
16
|
+
trigger: string
|
|
17
|
+
payload: AnyEventPayload
|
|
14
18
|
log: any[]
|
|
15
19
|
}
|
package/src/triggers/Payload.ts
CHANGED
|
@@ -72,11 +72,3 @@ export type BasicEventPayload =
|
|
|
72
72
|
export type AnyEventPayload =
|
|
73
73
|
| BasicEventPayload
|
|
74
74
|
| WatchEventPayload
|
|
75
|
-
|
|
76
|
-
export interface TriggerPayload {
|
|
77
|
-
// identityId: string
|
|
78
|
-
// apiKeyId: string
|
|
79
|
-
transactionId: string
|
|
80
|
-
createdAt: string
|
|
81
|
-
event: AnyEventPayload
|
|
82
|
-
}
|
|
@@ -57,7 +57,7 @@ export class TriggerPayloadBuilder {
|
|
|
57
57
|
trigger: events[0].listener.trigger.name,
|
|
58
58
|
entity: events[0].entity.name,
|
|
59
59
|
id: eventsByPrimary.primary,
|
|
60
|
-
selection: selections[eventsByPrimary.primary],
|
|
60
|
+
selection: events[0].selection ?? selections[eventsByPrimary.primary],
|
|
61
61
|
events: eventsByPrimary.events.map(it => this.buildBaseEventPayloads(it)),
|
|
62
62
|
})
|
|
63
63
|
}
|