@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.
Files changed (112) hide show
  1. package/dist/src/authorization/ActionsAuthorizationActions.d.ts +27 -0
  2. package/dist/src/authorization/ActionsAuthorizationActions.d.ts.map +1 -0
  3. package/dist/src/authorization/ActionsAuthorizationActions.js +17 -0
  4. package/dist/src/authorization/ActionsAuthorizationActions.js.map +1 -0
  5. package/dist/src/authorization/ActionsPermissionsFactory.d.ts +5 -0
  6. package/dist/src/authorization/ActionsPermissionsFactory.d.ts.map +1 -0
  7. package/dist/src/authorization/ActionsPermissionsFactory.js +17 -0
  8. package/dist/src/authorization/ActionsPermissionsFactory.js.map +1 -0
  9. package/dist/src/authorization/Identity.d.ts +5 -0
  10. package/dist/src/authorization/Identity.d.ts.map +1 -0
  11. package/dist/src/authorization/Identity.js +3 -0
  12. package/dist/src/authorization/Identity.js.map +1 -0
  13. package/dist/src/authorization/index.d.ts +4 -0
  14. package/dist/src/authorization/index.d.ts.map +1 -0
  15. package/dist/src/authorization/index.js +20 -0
  16. package/dist/src/authorization/index.js.map +1 -0
  17. package/dist/src/dispatch/EventDispatcher.d.ts +2 -2
  18. package/dist/src/dispatch/EventDispatcher.d.ts.map +1 -1
  19. package/dist/src/dispatch/EventsRepository.d.ts +1 -1
  20. package/dist/src/dispatch/EventsRepository.d.ts.map +1 -1
  21. package/dist/src/dispatch/EventsRepository.js +1 -1
  22. package/dist/src/dispatch/EventsRepository.js.map +1 -1
  23. package/dist/src/dispatch/WebhookFetcher.d.ts +14 -0
  24. package/dist/src/dispatch/WebhookFetcher.d.ts.map +1 -0
  25. package/dist/src/dispatch/WebhookFetcher.js +17 -0
  26. package/dist/src/dispatch/WebhookFetcher.js.map +1 -0
  27. package/dist/src/dispatch/WebhookTargetHandler.d.ts +8 -0
  28. package/dist/src/dispatch/WebhookTargetHandler.d.ts.map +1 -1
  29. package/dist/src/dispatch/WebhookTargetHandler.js +145 -44
  30. package/dist/src/dispatch/WebhookTargetHandler.js.map +1 -1
  31. package/dist/src/dispatch/types.d.ts +23 -2
  32. package/dist/src/dispatch/types.d.ts.map +1 -1
  33. package/dist/src/graphql/http/ActionsApiMiddlewareFactory.d.ts +4 -1
  34. package/dist/src/graphql/http/ActionsApiMiddlewareFactory.d.ts.map +1 -1
  35. package/dist/src/graphql/http/ActionsApiMiddlewareFactory.js +10 -2
  36. package/dist/src/graphql/http/ActionsApiMiddlewareFactory.js.map +1 -1
  37. package/dist/src/graphql/http/ActionsContextResolver.d.ts +8 -5
  38. package/dist/src/graphql/http/ActionsContextResolver.d.ts.map +1 -1
  39. package/dist/src/graphql/http/ActionsContextResolver.js +6 -4
  40. package/dist/src/graphql/http/ActionsContextResolver.js.map +1 -1
  41. package/dist/src/graphql/http/ActionsGraphQLHandlerFactory.d.ts +1 -1
  42. package/dist/src/graphql/http/ActionsGraphQLHandlerFactory.d.ts.map +1 -1
  43. package/dist/src/graphql/resolvers/ActionsContext.d.ts +2 -0
  44. package/dist/src/graphql/resolvers/ActionsContext.d.ts.map +1 -1
  45. package/dist/src/graphql/resolvers/mutation/ProcessBatchMutationResolver.d.ts.map +1 -1
  46. package/dist/src/graphql/resolvers/mutation/ProcessBatchMutationResolver.js +2 -0
  47. package/dist/src/graphql/resolvers/mutation/ProcessBatchMutationResolver.js.map +1 -1
  48. package/dist/src/graphql/resolvers/mutation/SetVariablesMutationResolver.d.ts.map +1 -1
  49. package/dist/src/graphql/resolvers/mutation/SetVariablesMutationResolver.js +2 -0
  50. package/dist/src/graphql/resolvers/mutation/SetVariablesMutationResolver.js.map +1 -1
  51. package/dist/src/graphql/resolvers/query/EventsQueryResolver.d.ts.map +1 -1
  52. package/dist/src/graphql/resolvers/query/EventsQueryResolver.js +4 -0
  53. package/dist/src/graphql/resolvers/query/EventsQueryResolver.js.map +1 -1
  54. package/dist/src/graphql/resolvers/query/VariablesQueryResolver.d.ts.map +1 -1
  55. package/dist/src/graphql/resolvers/query/VariablesQueryResolver.js +2 -0
  56. package/dist/src/graphql/resolvers/query/VariablesQueryResolver.js.map +1 -1
  57. package/dist/src/graphql/schema/index.d.ts +43 -43
  58. package/dist/src/graphql/schema/index.d.ts.map +1 -1
  59. package/dist/src/index.d.ts.map +1 -1
  60. package/dist/src/index.js +9 -3
  61. package/dist/src/index.js.map +1 -1
  62. package/dist/src/model/VariablesManager.d.ts +1 -1
  63. package/dist/src/model/VariablesManager.d.ts.map +1 -1
  64. package/dist/src/model/VariablesQuery.d.ts +1 -1
  65. package/dist/src/model/VariablesQuery.d.ts.map +1 -1
  66. package/dist/src/model/types.d.ts +7 -4
  67. package/dist/src/model/types.d.ts.map +1 -1
  68. package/dist/src/triggers/Payload.d.ts +9 -14
  69. package/dist/src/triggers/Payload.d.ts.map +1 -1
  70. package/dist/src/triggers/TriggerHandler.d.ts +1 -1
  71. package/dist/src/triggers/TriggerHandler.d.ts.map +1 -1
  72. package/dist/src/triggers/TriggerListenersStore.d.ts +1 -1
  73. package/dist/src/triggers/TriggerListenersStore.d.ts.map +1 -1
  74. package/dist/src/triggers/TriggerPayloadBuilder.js +4 -4
  75. package/dist/src/triggers/TriggerPayloadBuilder.js.map +1 -1
  76. package/dist/src/triggers/TriggerPayloadManager.d.ts +1 -1
  77. package/dist/src/triggers/TriggerPayloadManager.d.ts.map +1 -1
  78. package/dist/src/triggers/TriggerPayloadPersister.d.ts.map +1 -1
  79. package/dist/src/triggers/TriggerPayloadPersister.js +3 -1
  80. package/dist/src/triggers/TriggerPayloadPersister.js.map +1 -1
  81. package/dist/src/tsconfig.tsbuildinfo +1 -1
  82. package/dist/tests/cases/integration/triggers.rootWatch.test.js +63 -0
  83. package/dist/tests/cases/integration/triggers.rootWatch.test.js.map +1 -1
  84. package/dist/tests/cases/unit/webhookHandler.test.d.ts +2 -0
  85. package/dist/tests/cases/unit/webhookHandler.test.d.ts.map +1 -0
  86. package/dist/tests/cases/unit/webhookHandler.test.js +248 -0
  87. package/dist/tests/cases/unit/webhookHandler.test.js.map +1 -0
  88. package/dist/tests/tsconfig.tsbuildinfo +1 -1
  89. package/package.json +18 -17
  90. package/src/authorization/ActionsAuthorizationActions.ts +14 -0
  91. package/src/authorization/ActionsPermissionsFactory.ts +14 -0
  92. package/src/authorization/Identity.ts +4 -0
  93. package/src/authorization/index.ts +3 -0
  94. package/src/dispatch/EventsRepository.ts +1 -1
  95. package/src/dispatch/WebhookFetcher.ts +24 -0
  96. package/src/dispatch/WebhookTargetHandler.ts +151 -46
  97. package/src/dispatch/types.ts +27 -0
  98. package/src/graphql/http/ActionsApiMiddlewareFactory.ts +10 -1
  99. package/src/graphql/http/ActionsContextResolver.ts +15 -6
  100. package/src/graphql/resolvers/ActionsContext.ts +2 -0
  101. package/src/graphql/resolvers/mutation/ProcessBatchMutationResolver.ts +3 -0
  102. package/src/graphql/resolvers/mutation/SetVariablesMutationResolver.ts +3 -0
  103. package/src/graphql/resolvers/query/EventsQueryResolver.ts +7 -0
  104. package/src/graphql/resolvers/query/VariablesQueryResolver.ts +3 -0
  105. package/src/index.ts +9 -3
  106. package/src/model/types.ts +7 -3
  107. package/src/triggers/Payload.ts +0 -8
  108. package/src/triggers/TriggerPayloadBuilder.ts +1 -1
  109. package/src/triggers/TriggerPayloadPersister.ts +10 -1
  110. package/src/tsconfig.json +1 -0
  111. package/tests/cases/integration/triggers.rootWatch.test.ts +64 -0
  112. 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-alpha.8",
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/database": "^1.3.0-alpha.8",
17
- "@contember/database-migrations": "^1.3.0-alpha.8",
18
- "@contember/engine-common": "^1.3.0-alpha.8",
19
- "@contember/engine-content-api": "^1.3.0-alpha.8",
20
- "@contember/engine-system-api": "^1.3.0-alpha.8",
21
- "@contember/engine-tenant-api": "^1.3.0-alpha.8",
22
- "@contember/engine-http": "^1.3.0-alpha.8",
23
- "@contember/engine-plugins": "^1.3.0-alpha.8",
24
- "@contember/graphql-utils": "^1.3.0-alpha.8",
25
- "@contember/schema": "^1.3.0-alpha.8",
26
- "@contember/schema-utils": "^1.3.0-alpha.8",
27
- "@contember/typesafe": "^1.3.0-alpha.8",
28
- "@contember/logger": "^1.3.0-alpha.8",
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": "^1.3.0-alpha.8",
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
+ }
@@ -0,0 +1,4 @@
1
+ export interface Identity {
2
+ readonly id: string
3
+ readonly roles: string[]
4
+ }
@@ -0,0 +1,3 @@
1
+ export * from './Identity'
2
+ export * from './ActionsAuthorizationActions'
3
+ export * from './ActionsPermissionsFactory'
@@ -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 = 20
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
- // todo: per-event response format
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>({ abortController, timeoutMs }: { abortController: AbortController; timeoutMs?: number }, cb: () => Promise<T>): Promise<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 ?? DEFAULT_TIMEOUT_MS)
187
+ }, timeoutMs)
83
188
  try {
84
- return await cb()
189
+ return await cb(abortController)
85
190
  } finally {
86
191
  clearTimeout(timeoutId)
87
192
  }
@@ -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 { ProjectRole } from '@contember/schema'
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
- const isProjectAdmin = memberships.some(it => it.role === ProjectRole.ADMIN)
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
- return projectContext
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
- .setupService('application', (it, { projectContextResolver, debugMode, actions_eventDispatcher, actions_dispatchWorkerSupervisorFactory, actions_variableManager }) => {
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())
@@ -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: number
11
- schema_id: string
12
+ stage_id: string
13
+ schema_id: number
12
14
  target: string
13
- payload: any
15
+ priority: number
16
+ trigger: string
17
+ payload: AnyEventPayload
14
18
  log: any[]
15
19
  }
@@ -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
  }