Skip to content

Commit c466d96

Browse files
authored
feat(pos-integration): DOMA-13041 add webhook notifications for integration status changes (#7356)
* feat(webhooks): DOMA-13041 make WebhookSubscription model field degradable when no models registered * refactor(webhooks): DOMA-13041 move modeled plugin inline to WebhookSubscription schema Moved modeled plugin implementation from separate file into WebhookSubscription.js to consolidate webhook-related code. * feat(webhooks): DOMA-13041 update yarn.lock Set all submodules on main and run "yarn" * feat(condo): DOMA-13041 add webhook env vars to CI workflow and update pos-integration submodule with fixed tests * feat(condo): DOMA-13041 remove webhook env vars from CI workflow * refactor(webhooks): DOMA-13041 rename modeled plugin to configureModelField for clarity * feat(webhooks): DOMA-13041 add documentation for getWebhookModels() initialization order * feat(condo): DOMA-13041 updte to rebased submodule branch * feat(webhooks): DOMA-13041 disable WebhookSubscription creation when no models available Replace dynamic hasAvailableModels function with static boolean set during schema initialization to properly disable create access when no webhooked models are registered. * refactor(webhooks): DOMA-13041 pass attrs parameter to canManageWebhookSubscriptions in create access check
1 parent 41cdea4 commit c466d96

4 files changed

Lines changed: 81 additions & 30 deletions

File tree

apps/pos-integration

packages/webhooks/schema/index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ const { getWebhookSubscriptionModel } = require('@open-condo/webhooks/schema/mod
44

55
/**
66
* Returns all webhook models (Webhook, WebhookSubscription, WebhookPayload)
7+
*
8+
* IMPORTANT: This function must be called AFTER all domain schemas that use
9+
* webHooked() plugin have been registered. The WebhookSubscription.model field
10+
* will be a Select containing ONLY models registered via webHooked() plugin.
11+
* If no webHooked() models are registered, the field degrades to Text.
12+
* In the schemas() array, always place getWebhookModels() after domain schemas.
13+
*
714
* @param {string} schemaPath - Path to GraphQL schema file
815
* @param {Array<string>} [appWebhooksEventsTypes] - Array of event types supported by the application
916
* @returns {Object} Object with Webhook, WebhookSubscription, and WebhookPayload models

packages/webhooks/schema/models/WebhookSubscription.js

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,56 @@
11
const conf = require('@open-condo/config')
22
const { uuided, versioned, tracked, softDeleted, dvAndSender, historical } = require('@open-condo/keystone/plugins')
3+
const { plugin } = require('@open-condo/keystone/plugins/utils/typing')
34
const { GQLListSchema } = require('@open-condo/keystone/schema')
45
const { DEFAULT_MAX_PACK_SIZE, DEFAULT_UNAVAILABILITY_THRESHOLD } = require('@open-condo/webhooks/constants')
56
const { WebHookModelValidator, getModelValidator, setModelValidator } = require('@open-condo/webhooks/model-validator')
67
const access = require('@open-condo/webhooks/schema/access/WebhookSubscription')
78

9+
10+
const configureModelField = ({ validator, validateFields, validateFilters, setHasAvailableModels }) => plugin(({ fields = {}, ...rest }) => {
11+
const hasAvailableModels = Boolean(validator && validator.models.length > 0)
12+
setHasAvailableModels(hasAvailableModels)
13+
14+
// WebhookSubscription is intended for model-based webhooks only, so in the normal case
15+
// `model` must be a required Select backed by models registered through `webHooked()`.
16+
// Some applications import webhook schemas only to use custom `WebhookPayload` events and
17+
// therefore never register any `webHooked()` models. In that case `validator.models` stays
18+
// empty, and a required Select with no options makes the schema impossible to use and can
19+
// break app startup. To keep such apps bootable while still requiring callers to provide a
20+
// `model` value, we degrade the field to required Text and disable creation access when
21+
// no models are available.
22+
//
23+
// If `validator.models` later becomes non-empty for the same app, the field automatically
24+
// switches back to the strict Select variant on the next startup because this plugin runs
25+
// during schema registration, after `webHooked()` had a chance to register models. No special
26+
// runtime toggle is needed, but existing WebhookSubscription records should be checked before
27+
// that rollout: rows with arbitrary `model` values are acceptable in the "no models available"
28+
// mode, while the Select mode expects values from the registered model list. If the storage
29+
// representation or generated schema changes are important for deployment, run the usual
30+
// `makemigrations` / schema generation checks as part of that transition.
31+
fields.model = hasAvailableModels
32+
? {
33+
schemaDoc: 'The data model (schema) that the webhook is subscribed to',
34+
type: 'Select',
35+
dataType: 'string',
36+
isRequired: true,
37+
options: validator.models,
38+
hooks: {
39+
validateInput: (args) => {
40+
validateFields(args)
41+
validateFilters(args)
42+
},
43+
},
44+
}
45+
: {
46+
schemaDoc: 'The data model (schema) that the webhook is subscribed to',
47+
type: 'Text',
48+
isRequired: true,
49+
}
50+
51+
return { fields, ...rest }
52+
})
53+
854
const UNAVAILABILITY_THRESHOLD = (typeof conf['WEBHOOK_BLOCK_THRESHOLD'] === 'number' && conf['WEBHOOK_BLOCK_THRESHOLD'] > 0)
955
? conf['WEBHOOK_BLOCK_THRESHOLD']
1056
: DEFAULT_UNAVAILABILITY_THRESHOLD
@@ -15,10 +61,17 @@ function getWebhookSubscriptionModel (schemaPath) {
1561
}
1662

1763
const validator = getModelValidator()
64+
let hasAvailableModels = false
65+
const setHasAvailableModels = (value) => { hasAvailableModels = value }
66+
const checkHasAvailableModels = () => hasAvailableModels
1867

1968
const validateFields = ({ resolvedData, existingItem, addFieldValidationError }) => {
2069
const newItem = { ...existingItem, ...resolvedData }
2170

71+
if (!checkHasAvailableModels()) {
72+
return
73+
}
74+
2275
if (!validator) {
2376
return addFieldValidationError(`Invalid fields for model "${newItem.model}" was provided. Details: ["Validator for this model is not specified!"]`)
2477
}
@@ -39,6 +92,10 @@ function getWebhookSubscriptionModel (schemaPath) {
3992
const validateFilters = ({ resolvedData, existingItem, addFieldValidationError }) => {
4093
const newItem = { ...existingItem, ...resolvedData }
4194

95+
if (!checkHasAvailableModels()) {
96+
return
97+
}
98+
4299
if (!validator) {
43100
return addFieldValidationError(`Invalid filters for model "${newItem.model}" was provided. Details: ["Validator for this model is not specified!"]`)
44101
}
@@ -49,7 +106,6 @@ function getWebhookSubscriptionModel (schemaPath) {
49106
}
50107
}
51108

52-
53109
return new GQLListSchema('WebhookSubscription', {
54110
schemaDoc: 'Determines which models the WebHook will be subscribed to. When model changes subscription task will be triggered to resolve changed data and send a webhook',
55111
fields: {
@@ -112,19 +168,6 @@ function getWebhookSubscriptionModel (schemaPath) {
112168
},
113169
},
114170
},
115-
model: {
116-
schemaDoc: 'The data model (schema) that the webhook is subscribed to',
117-
type: 'Select',
118-
dataType: 'string',
119-
isRequired: true,
120-
options: validator.models,
121-
hooks: {
122-
validateInput: (args) => {
123-
validateFields(args)
124-
validateFilters(args)
125-
},
126-
},
127-
},
128171
fields: {
129172
schemaDoc: 'String representing list of model fields in graphql-query format. ' +
130173
'Exactly the fields specified here will be sent by the webhook. ' +
@@ -170,10 +213,10 @@ function getWebhookSubscriptionModel (schemaPath) {
170213
},
171214
},
172215
},
173-
plugins: [uuided(), versioned(), tracked(), softDeleted(), dvAndSender(), historical()],
216+
plugins: [configureModelField({ validator, validateFields, validateFilters, setHasAvailableModels }), uuided(), versioned(), tracked(), softDeleted(), dvAndSender(), historical()],
174217
access: {
175218
read: access.canReadWebhookSubscriptions,
176-
create: access.canManageWebhookSubscriptions,
219+
create: (attrs) => checkHasAvailableModels() ? access.canManageWebhookSubscriptions(attrs) : false,
177220
update: access.canManageWebhookSubscriptions,
178221
delete: false,
179222
auth: true,

yarn.lock

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1376,6 +1376,7 @@ __metadata:
13761376
"@open-condo/miniapp-utils": "workspace:^"
13771377
"@open-condo/next": "workspace:^"
13781378
"@open-condo/ui": "workspace:^"
1379+
"@open-condo/webhooks": "workspace:^"
13791380
"@open-keystone/app-next": ^9.0.1
13801381
"@open-keystone/server-side-graphql-client": ^2.1.3
13811382
"@pos-integration/domains": "link:./domains"
@@ -8356,17 +8357,17 @@ __metadata:
83568357
languageName: node
83578358
linkType: hard
83588359

8359-
"@graphql-tools/batch-execute@npm:^10.0.5":
8360-
version: 10.0.5
8361-
resolution: "@graphql-tools/batch-execute@npm:10.0.5"
8360+
"@graphql-tools/batch-execute@npm:^10.0.7":
8361+
version: 10.0.7
8362+
resolution: "@graphql-tools/batch-execute@npm:10.0.7"
83628363
dependencies:
83638364
"@graphql-tools/utils": ^11.0.0
83648365
"@whatwg-node/promise-helpers": ^1.3.2
83658366
dataloader: ^2.2.3
83668367
tslib: ^2.8.1
83678368
peerDependencies:
83688369
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
8369-
checksum: fc577bdb7c8de8ee4915caed5c22dda2aeedc8fd33e35f1604d681e613e844985b141a34f9e9fe107bf76d26e81d3772ae28bc6de78a6fffd1f7aa1376e94ee5
8370+
checksum: 535a2c2c4257bf8fb6f361f59a4a11a208ab04b44039063f9fb857d6579bd74d785df2029d0ab19fc4d876536f39ec776cf76787d8058a34c8fd2bc5e7141cfc
83708371
languageName: node
83718372
linkType: hard
83728373

@@ -8431,11 +8432,11 @@ __metadata:
84318432
languageName: node
84328433
linkType: hard
84338434

8434-
"@graphql-tools/delegate@npm:^12.0.9":
8435-
version: 12.0.9
8436-
resolution: "@graphql-tools/delegate@npm:12.0.9"
8435+
"@graphql-tools/delegate@npm:^12.0.11":
8436+
version: 12.0.11
8437+
resolution: "@graphql-tools/delegate@npm:12.0.11"
84378438
dependencies:
8438-
"@graphql-tools/batch-execute": ^10.0.5
8439+
"@graphql-tools/batch-execute": ^10.0.7
84398440
"@graphql-tools/executor": ^1.4.13
84408441
"@graphql-tools/schema": ^10.0.29
84418442
"@graphql-tools/utils": ^11.0.0
@@ -8445,7 +8446,7 @@ __metadata:
84458446
tslib: ^2.8.1
84468447
peerDependencies:
84478448
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
8448-
checksum: aa1ff2c8aff21f2be4ef10d4455d0dfb9841d8019a9f49e0ec7d98a5f284fdb9ccbeeb6fa12d9889f904d222ae7caedba466de70adc8c05ee3cf2bc53e1b5969
8449+
checksum: a390158c31bdadbbf69cbed3117cc990738243facd6779fc75f83042be4ec49e2e579d3f5ada33e38b4c1594234776a2cf50a57eb739d3cab24d6513babf3a3c
84498450
languageName: node
84508451
linkType: hard
84518452

@@ -9188,17 +9189,17 @@ __metadata:
91889189
linkType: hard
91899190

91909191
"@graphql-tools/wrap@npm:^11.1.1":
9191-
version: 11.1.9
9192-
resolution: "@graphql-tools/wrap@npm:11.1.9"
9192+
version: 11.1.11
9193+
resolution: "@graphql-tools/wrap@npm:11.1.11"
91939194
dependencies:
9194-
"@graphql-tools/delegate": ^12.0.9
9195+
"@graphql-tools/delegate": ^12.0.11
91959196
"@graphql-tools/schema": ^10.0.29
91969197
"@graphql-tools/utils": ^11.0.0
91979198
"@whatwg-node/promise-helpers": ^1.3.2
91989199
tslib: ^2.8.1
91999200
peerDependencies:
92009201
graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0
9201-
checksum: 22088b23b049210b8edbe80e21c6d212b2546b2d1c4fe87bc749cccb196945df78d64f15b347529172891588b6c9119806c0cc1fa2ecece9972e70542a267761
9202+
checksum: c036b459f2f59d8a3f4b935fcba61ac2197113521ee3fcf56e94d41a740ef60e907d90fb3ad142dd3c7c46522a8c728b73455936c6d3fb19b180aee35fc3534a
92029203
languageName: node
92039204
linkType: hard
92049205

0 commit comments

Comments
 (0)