@friggframework/core 2.0.0--canary.696.5d9f1be.0 → 2.0.0--canary.698.b1a2d30.0
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/integrations/data-defined/README.md +31 -1
- package/integrations/data-defined/capabilities/AUDIT.md +451 -0
- package/integrations/data-defined/capabilities/asana.capabilities.json +22 -13
- package/integrations/data-defined/capabilities/fathom.capabilities.json +16 -6
- package/integrations/data-defined/capabilities/hubspot.capabilities.json +78 -27
- package/integrations/data-defined/capabilities/linear.capabilities.json +30 -10
- package/integrations/data-defined/capabilities/phone.capabilities.json +24 -7
- package/integrations/data-defined/capabilities/pipedrive.capabilities.json +26 -11
- package/integrations/data-defined/capabilities/salesforce.capabilities.json +64 -18
- package/integrations/data-defined/capabilities/slack.capabilities.json +12 -5
- package/integrations/data-defined/capabilities/stripe.capabilities.json +28 -10
- package/integrations/data-defined/data-defined-integration.js +2 -2
- package/integrations/data-defined/examples/asana-slack.definition.json +4 -5
- package/integrations/data-defined/examples/hubspot-slack.definition.json +4 -4
- package/integrations/data-defined/examples/klaviyo-hubspot.definition.json +1 -4
- package/integrations/data-defined/examples/phone-hubspot.definition.json +1 -5
- package/integrations/data-defined/expressions.js +2 -0
- package/integrations/data-defined/index.js +6 -0
- package/integrations/data-defined/manifests.js +18 -0
- package/integrations/data-defined/renderers/common.js +67 -0
- package/integrations/data-defined/renderers/html.js +129 -0
- package/integrations/data-defined/renderers/hubspot.js +54 -0
- package/integrations/data-defined/renderers/preview.js +154 -0
- package/integrations/data-defined/renderers/slack.js +146 -0
- package/integrations/data-defined/runtime.js +17 -2
- package/integrations/data-defined/schema/capability-manifest.v0.1.schema.json +40 -0
- package/integrations/data-defined/scripts/check-manifests.js +220 -0
- package/integrations/data-defined/tests/helpers-fake-crm.js +11 -6
- package/integrations/data-defined/validate.js +6 -0
- package/package.json +5 -5
|
@@ -321,30 +321,44 @@
|
|
|
321
321
|
"scope": "installation",
|
|
322
322
|
"topic": "{{body.payload.ChangeEventHeader.entityName}}.{{body.payload.ChangeEventHeader.changeType}}",
|
|
323
323
|
"eventId": "{{body.id}}",
|
|
324
|
-
"verify": "verifyWebhookSignature"
|
|
324
|
+
"verify": "verifyWebhookSignature",
|
|
325
|
+
"status": "unimplemented",
|
|
326
|
+
"note": "The salesforce Api has no verifyWebhookSignature method; webhook signatures cannot be checked until the module adds one."
|
|
325
327
|
},
|
|
326
328
|
"capabilities": {
|
|
327
329
|
"lead.get": {
|
|
328
330
|
"kind": "read",
|
|
329
|
-
"method": "
|
|
331
|
+
"method": "get",
|
|
330
332
|
"input": {
|
|
331
333
|
"id": {
|
|
332
334
|
"type": "string",
|
|
333
335
|
"required": true
|
|
334
336
|
}
|
|
335
337
|
},
|
|
336
|
-
"output": "lead"
|
|
338
|
+
"output": "lead",
|
|
339
|
+
"args": [
|
|
340
|
+
{
|
|
341
|
+
"value": "Lead"
|
|
342
|
+
},
|
|
343
|
+
"id"
|
|
344
|
+
]
|
|
337
345
|
},
|
|
338
346
|
"lead.create": {
|
|
339
347
|
"kind": "write",
|
|
340
|
-
"method": "
|
|
348
|
+
"method": "create",
|
|
341
349
|
"input": {
|
|
342
350
|
"fields": {
|
|
343
351
|
"type": "object",
|
|
344
352
|
"required": true
|
|
345
353
|
}
|
|
346
354
|
},
|
|
347
|
-
"output": "lead"
|
|
355
|
+
"output": "lead",
|
|
356
|
+
"args": [
|
|
357
|
+
{
|
|
358
|
+
"value": "Lead"
|
|
359
|
+
},
|
|
360
|
+
"fields"
|
|
361
|
+
]
|
|
348
362
|
},
|
|
349
363
|
"lead.update": {
|
|
350
364
|
"kind": "write",
|
|
@@ -359,7 +373,9 @@
|
|
|
359
373
|
"required": true
|
|
360
374
|
}
|
|
361
375
|
},
|
|
362
|
-
"output": "void"
|
|
376
|
+
"output": "void",
|
|
377
|
+
"status": "unimplemented",
|
|
378
|
+
"note": "Api.update(object, data) needs the record id inside data (data.Id); the capability passes the id separately, so there is no way to call it."
|
|
363
379
|
},
|
|
364
380
|
"person.searchByEmail": {
|
|
365
381
|
"kind": "search",
|
|
@@ -371,18 +387,26 @@
|
|
|
371
387
|
"required": true
|
|
372
388
|
}
|
|
373
389
|
},
|
|
374
|
-
"output": "{ leads: lead[], contacts: contact[] }"
|
|
390
|
+
"output": "{ leads: lead[], contacts: contact[] }",
|
|
391
|
+
"status": "unimplemented",
|
|
392
|
+
"note": "Api.find(object, filter) searches one sObject at a time; searching Lead and Contact together would need two calls."
|
|
375
393
|
},
|
|
376
394
|
"event.create": {
|
|
377
395
|
"kind": "write",
|
|
378
|
-
"method": "
|
|
396
|
+
"method": "create",
|
|
379
397
|
"input": {
|
|
380
398
|
"fields": {
|
|
381
399
|
"type": "object",
|
|
382
400
|
"required": true
|
|
383
401
|
}
|
|
384
402
|
},
|
|
385
|
-
"output": "event"
|
|
403
|
+
"output": "event",
|
|
404
|
+
"args": [
|
|
405
|
+
{
|
|
406
|
+
"value": "Event"
|
|
407
|
+
},
|
|
408
|
+
"fields"
|
|
409
|
+
]
|
|
386
410
|
},
|
|
387
411
|
"event.update": {
|
|
388
412
|
"kind": "write",
|
|
@@ -397,18 +421,26 @@
|
|
|
397
421
|
"required": true
|
|
398
422
|
}
|
|
399
423
|
},
|
|
400
|
-
"output": "void"
|
|
424
|
+
"output": "void",
|
|
425
|
+
"status": "unimplemented",
|
|
426
|
+
"note": "Api.update(object, data) needs the record id inside data (data.Id); the capability passes the id separately, so there is no way to call it."
|
|
401
427
|
},
|
|
402
428
|
"opportunity.get": {
|
|
403
429
|
"kind": "read",
|
|
404
|
-
"method": "
|
|
430
|
+
"method": "get",
|
|
405
431
|
"input": {
|
|
406
432
|
"id": {
|
|
407
433
|
"type": "string",
|
|
408
434
|
"required": true
|
|
409
435
|
}
|
|
410
436
|
},
|
|
411
|
-
"output": "opportunity"
|
|
437
|
+
"output": "opportunity",
|
|
438
|
+
"args": [
|
|
439
|
+
{
|
|
440
|
+
"value": "Opportunity"
|
|
441
|
+
},
|
|
442
|
+
"id"
|
|
443
|
+
]
|
|
412
444
|
},
|
|
413
445
|
"opportunity.update": {
|
|
414
446
|
"kind": "write",
|
|
@@ -423,18 +455,26 @@
|
|
|
423
455
|
"required": true
|
|
424
456
|
}
|
|
425
457
|
},
|
|
426
|
-
"output": "void"
|
|
458
|
+
"output": "void",
|
|
459
|
+
"status": "unimplemented",
|
|
460
|
+
"note": "Api.update(object, data) needs the record id inside data (data.Id); the capability passes the id separately, so there is no way to call it."
|
|
427
461
|
},
|
|
428
462
|
"task.create": {
|
|
429
463
|
"kind": "write",
|
|
430
|
-
"method": "
|
|
464
|
+
"method": "create",
|
|
431
465
|
"input": {
|
|
432
466
|
"fields": {
|
|
433
467
|
"type": "object",
|
|
434
468
|
"required": true
|
|
435
469
|
}
|
|
436
470
|
},
|
|
437
|
-
"output": "task"
|
|
471
|
+
"output": "task",
|
|
472
|
+
"args": [
|
|
473
|
+
{
|
|
474
|
+
"value": "Task"
|
|
475
|
+
},
|
|
476
|
+
"fields"
|
|
477
|
+
]
|
|
438
478
|
},
|
|
439
479
|
"file.attach": {
|
|
440
480
|
"kind": "write",
|
|
@@ -457,7 +497,9 @@
|
|
|
457
497
|
"required": true
|
|
458
498
|
}
|
|
459
499
|
},
|
|
460
|
-
"output": "contentVersion"
|
|
500
|
+
"output": "contentVersion",
|
|
501
|
+
"status": "unimplemented",
|
|
502
|
+
"note": "Api has the generic create(object, data) only; ContentVersion field names differ from this capability's inputs."
|
|
461
503
|
},
|
|
462
504
|
"note.create": {
|
|
463
505
|
"kind": "write",
|
|
@@ -476,7 +518,9 @@
|
|
|
476
518
|
"required": true
|
|
477
519
|
}
|
|
478
520
|
},
|
|
479
|
-
"output": "contentNote"
|
|
521
|
+
"output": "contentNote",
|
|
522
|
+
"status": "unimplemented",
|
|
523
|
+
"note": "Api has the generic create(object, data) only; ContentNote field names differ from this capability's inputs."
|
|
480
524
|
},
|
|
481
525
|
"user.list": {
|
|
482
526
|
"kind": "read",
|
|
@@ -486,7 +530,9 @@
|
|
|
486
530
|
"type": "boolean"
|
|
487
531
|
}
|
|
488
532
|
},
|
|
489
|
-
"output": "user[]"
|
|
533
|
+
"output": "user[]",
|
|
534
|
+
"status": "unimplemented",
|
|
535
|
+
"note": "Api has find(object, filter) but no user listing with an activeOnly filter."
|
|
490
536
|
}
|
|
491
537
|
},
|
|
492
538
|
"options": {
|
|
@@ -206,7 +206,9 @@
|
|
|
206
206
|
"body": {
|
|
207
207
|
"challenge": "$.body.challenge"
|
|
208
208
|
}
|
|
209
|
-
}
|
|
209
|
+
},
|
|
210
|
+
"status": "unimplemented",
|
|
211
|
+
"note": "The slack Api has no verifyWebhookSignature method; webhook signatures cannot be checked until the module adds one."
|
|
210
212
|
},
|
|
211
213
|
"capabilities": {
|
|
212
214
|
"message.post": {
|
|
@@ -285,7 +287,7 @@
|
|
|
285
287
|
},
|
|
286
288
|
"message.permalink": {
|
|
287
289
|
"kind": "read",
|
|
288
|
-
"method": "
|
|
290
|
+
"method": "getMessagePermalink",
|
|
289
291
|
"label": "chat.getPermalink",
|
|
290
292
|
"input": {
|
|
291
293
|
"channel": {
|
|
@@ -301,7 +303,7 @@
|
|
|
301
303
|
},
|
|
302
304
|
"channel.list": {
|
|
303
305
|
"kind": "read",
|
|
304
|
-
"method": "
|
|
306
|
+
"method": "listChannels",
|
|
305
307
|
"label": "conversations.list",
|
|
306
308
|
"input": {
|
|
307
309
|
"types": {
|
|
@@ -325,7 +327,9 @@
|
|
|
325
327
|
"type": "number"
|
|
326
328
|
}
|
|
327
329
|
},
|
|
328
|
-
"output": "user[]"
|
|
330
|
+
"output": "user[]",
|
|
331
|
+
"status": "unimplemented",
|
|
332
|
+
"note": "Api has lookupUserByEmail, getUserById and getUserProfileById, but no user listing."
|
|
329
333
|
},
|
|
330
334
|
"user.lookupByEmail": {
|
|
331
335
|
"kind": "read",
|
|
@@ -337,7 +341,10 @@
|
|
|
337
341
|
"required": true
|
|
338
342
|
}
|
|
339
343
|
},
|
|
340
|
-
"output": "user"
|
|
344
|
+
"output": "user",
|
|
345
|
+
"args": [
|
|
346
|
+
"email"
|
|
347
|
+
]
|
|
341
348
|
}
|
|
342
349
|
},
|
|
343
350
|
"options": {
|
|
@@ -471,7 +471,9 @@
|
|
|
471
471
|
"topic": "{{body.type}}",
|
|
472
472
|
"eventId": "{{body.id}}",
|
|
473
473
|
"account": "{{body.account}}",
|
|
474
|
-
"verify": "verifyWebhookSignature"
|
|
474
|
+
"verify": "verifyWebhookSignature",
|
|
475
|
+
"status": "unimplemented",
|
|
476
|
+
"note": "The stripe Api has no verifyWebhookSignature method; webhook signatures cannot be checked until the module adds one."
|
|
475
477
|
},
|
|
476
478
|
"capabilities": {
|
|
477
479
|
"customer.get": {
|
|
@@ -483,7 +485,9 @@
|
|
|
483
485
|
"required": true
|
|
484
486
|
}
|
|
485
487
|
},
|
|
486
|
-
"output": "customer"
|
|
488
|
+
"output": "customer",
|
|
489
|
+
"status": "unimplemented",
|
|
490
|
+
"note": "Api wraps balance transactions, charges, accounts and webhook endpoints only; customers, payment intents, refunds and payouts are not exposed."
|
|
487
491
|
},
|
|
488
492
|
"customer.list": {
|
|
489
493
|
"kind": "read",
|
|
@@ -499,7 +503,9 @@
|
|
|
499
503
|
"type": "string"
|
|
500
504
|
}
|
|
501
505
|
},
|
|
502
|
-
"output": "page<customer>"
|
|
506
|
+
"output": "page<customer>",
|
|
507
|
+
"status": "unimplemented",
|
|
508
|
+
"note": "Api wraps balance transactions, charges, accounts and webhook endpoints only; customers, payment intents, refunds and payouts are not exposed."
|
|
503
509
|
},
|
|
504
510
|
"customer.search": {
|
|
505
511
|
"kind": "search",
|
|
@@ -510,7 +516,9 @@
|
|
|
510
516
|
"required": true
|
|
511
517
|
}
|
|
512
518
|
},
|
|
513
|
-
"output": "customer[]"
|
|
519
|
+
"output": "customer[]",
|
|
520
|
+
"status": "unimplemented",
|
|
521
|
+
"note": "Api wraps balance transactions, charges, accounts and webhook endpoints only; customers, payment intents, refunds and payouts are not exposed."
|
|
514
522
|
},
|
|
515
523
|
"customer.create": {
|
|
516
524
|
"kind": "write",
|
|
@@ -521,7 +529,9 @@
|
|
|
521
529
|
"required": true
|
|
522
530
|
}
|
|
523
531
|
},
|
|
524
|
-
"output": "customer"
|
|
532
|
+
"output": "customer",
|
|
533
|
+
"status": "unimplemented",
|
|
534
|
+
"note": "Api wraps balance transactions, charges, accounts and webhook endpoints only; customers, payment intents, refunds and payouts are not exposed."
|
|
525
535
|
},
|
|
526
536
|
"customer.update": {
|
|
527
537
|
"kind": "write",
|
|
@@ -536,7 +546,9 @@
|
|
|
536
546
|
"required": true
|
|
537
547
|
}
|
|
538
548
|
},
|
|
539
|
-
"output": "customer"
|
|
549
|
+
"output": "customer",
|
|
550
|
+
"status": "unimplemented",
|
|
551
|
+
"note": "Api wraps balance transactions, charges, accounts and webhook endpoints only; customers, payment intents, refunds and payouts are not exposed."
|
|
540
552
|
},
|
|
541
553
|
"paymentIntent.get": {
|
|
542
554
|
"kind": "read",
|
|
@@ -550,7 +562,9 @@
|
|
|
550
562
|
"type": "array"
|
|
551
563
|
}
|
|
552
564
|
},
|
|
553
|
-
"output": "paymentIntent"
|
|
565
|
+
"output": "paymentIntent",
|
|
566
|
+
"status": "unimplemented",
|
|
567
|
+
"note": "Api wraps balance transactions, charges, accounts and webhook endpoints only; customers, payment intents, refunds and payouts are not exposed."
|
|
554
568
|
},
|
|
555
569
|
"refund.get": {
|
|
556
570
|
"kind": "read",
|
|
@@ -561,7 +575,9 @@
|
|
|
561
575
|
"required": true
|
|
562
576
|
}
|
|
563
577
|
},
|
|
564
|
-
"output": "refund"
|
|
578
|
+
"output": "refund",
|
|
579
|
+
"status": "unimplemented",
|
|
580
|
+
"note": "Api wraps balance transactions, charges, accounts and webhook endpoints only; customers, payment intents, refunds and payouts are not exposed."
|
|
565
581
|
},
|
|
566
582
|
"payout.list": {
|
|
567
583
|
"kind": "read",
|
|
@@ -580,11 +596,13 @@
|
|
|
580
596
|
"type": "number"
|
|
581
597
|
}
|
|
582
598
|
},
|
|
583
|
-
"output": "payout[]"
|
|
599
|
+
"output": "payout[]",
|
|
600
|
+
"status": "unimplemented",
|
|
601
|
+
"note": "Api wraps balance transactions, charges, accounts and webhook endpoints only; customers, payment intents, refunds and payouts are not exposed."
|
|
584
602
|
},
|
|
585
603
|
"balanceTransaction.list": {
|
|
586
604
|
"kind": "read",
|
|
587
|
-
"method": "
|
|
605
|
+
"method": "getBalanceTransactions",
|
|
588
606
|
"input": {
|
|
589
607
|
"payout": {
|
|
590
608
|
"type": "string",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
const crypto = require('crypto');
|
|
20
20
|
const { compile, routeWebhook, eventName } = require('./compile');
|
|
21
|
-
const { makePorts, runWorkflow, resumeWorkflow, runQuery, renderSurface, runLifecycle, resolveOptions, installContext } = require('./runtime');
|
|
21
|
+
const { argumentsFor, makePorts, runWorkflow, resumeWorkflow, runQuery, renderSurface, runLifecycle, resolveOptions, installContext } = require('./runtime');
|
|
22
22
|
const { createSyncEngine } = require('./sync-engine');
|
|
23
23
|
const { eventsIn, handshakeFor, modulesFor } = require('./webhooks');
|
|
24
24
|
const { buildConfigForm, missingRequired } = require('./config-form');
|
|
@@ -252,7 +252,7 @@ function makeClass({ name, defaultId, definitions, manifests, runCode, Integrati
|
|
|
252
252
|
return runCode(ref, args);
|
|
253
253
|
},
|
|
254
254
|
// The shared class registers modules by module name; a definition may name them by its own keys.
|
|
255
|
-
callApi: (moduleKey, method, args) => this.moduleFor(moduleKey).api[method](args),
|
|
255
|
+
callApi: (moduleKey, method, args, order) => this.moduleFor(moduleKey).api[method](...argumentsFor(args, order)),
|
|
256
256
|
store: this.mappingStore(),
|
|
257
257
|
});
|
|
258
258
|
}
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"options": {
|
|
36
36
|
"source": "asana.project",
|
|
37
37
|
"with": {
|
|
38
|
-
"
|
|
38
|
+
"workspace": "$.settings.workspace"
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
41
|
"label": "Project to announce completed tasks from",
|
|
@@ -163,8 +163,7 @@
|
|
|
163
163
|
"id": "task",
|
|
164
164
|
"use": "asana.task.get",
|
|
165
165
|
"with": {
|
|
166
|
-
"taskGid": "$.record.gid"
|
|
167
|
-
"opt_fields": "name,completed,completed_by.name,assignee.name,permalink_url"
|
|
166
|
+
"taskGid": "$.record.gid"
|
|
168
167
|
}
|
|
169
168
|
},
|
|
170
169
|
{
|
|
@@ -178,14 +177,14 @@
|
|
|
178
177
|
"use": "slack.message.post",
|
|
179
178
|
"with": {
|
|
180
179
|
"channel": "$.settings.channel",
|
|
181
|
-
"text": ":white_check_mark: {{steps.task.completed_by.name | default: 'Someone'}} completed <{{steps.task.permalink_url}}|{{steps.task.name}}>"
|
|
180
|
+
"text": ":white_check_mark: {{steps.task.completed_by.name | default: 'Someone' | mrkdwn}} completed <{{steps.task.permalink_url}}|{{steps.task.name | mrkdwn}}>"
|
|
182
181
|
}
|
|
183
182
|
}
|
|
184
183
|
]
|
|
185
184
|
}
|
|
186
185
|
],
|
|
187
186
|
"templates": {
|
|
188
|
-
"assignee-digest": ":spiral_calendar_pad: Hi {{item.firstName | default: item.name}}, you have {{item.count}} Asana task(s) due tomorrow:\n{{item.lines}}",
|
|
187
|
+
"assignee-digest": ":spiral_calendar_pad: Hi {{item.firstName | default: item.name | mrkdwn}}, you have {{item.count}} Asana task(s) due tomorrow:\n{{item.lines}}",
|
|
189
188
|
"unassigned-digest": ":warning: {{steps.groups.unassignedCount}} task(s) due tomorrow have no assignee:\n{{steps.groups.unassignedLines}}"
|
|
190
189
|
},
|
|
191
190
|
"lifecycle": {
|
|
@@ -258,7 +258,7 @@
|
|
|
258
258
|
"when": "$.steps.slackUser.id exists",
|
|
259
259
|
"with": {
|
|
260
260
|
"channel": "$.steps.slackUser.id",
|
|
261
|
-
"text": "Due today: {{item.properties.hs_task_subject}}",
|
|
261
|
+
"text": "Due today: {{item.properties.hs_task_subject | mrkdwn}}",
|
|
262
262
|
"metadata": {
|
|
263
263
|
"event_type": "hubspot_task",
|
|
264
264
|
"event_payload": {
|
|
@@ -401,9 +401,9 @@
|
|
|
401
401
|
}
|
|
402
402
|
],
|
|
403
403
|
"templates": {
|
|
404
|
-
"deal-won": ":tada: *{{steps.deal.properties.dealname}}* closed won for ${{steps.deal.properties.amount | default: '?'}}. Nice work {{steps.owner.firstName | default: 'team'}}! <https://app.hubspot.com/contacts/{{install.portalId}}/record/0-3/{{steps.deal.id}}|Open deal>",
|
|
405
|
-
"high-value": ":moneybag: New deal over ${{settings.threshold}}: *{{steps.deal.properties.dealname}}* (${{steps.deal.properties.amount}}), closing {{steps.deal.properties.closedate | date}}. <https://app.hubspot.com/contacts/{{install.portalId}}/record/0-3/{{steps.deal.id}}|Open deal>",
|
|
406
|
-
"digest-line": "\u2022 <https://app.hubspot.com/contacts/{{install.portalId}}/record/0-3/{{item.id}}|{{item.properties.dealname}}>: ${{item.properties.amount | default: 0}}, closes {{item.properties.closedate | date}}",
|
|
404
|
+
"deal-won": ":tada: *{{steps.deal.properties.dealname | mrkdwn}}* closed won for ${{steps.deal.properties.amount | default: '?'}}. Nice work {{steps.owner.firstName | default: 'team' | mrkdwn}}! <https://app.hubspot.com/contacts/{{install.portalId}}/record/0-3/{{steps.deal.id}}|Open deal>",
|
|
405
|
+
"high-value": ":moneybag: New deal over ${{settings.threshold}}: *{{steps.deal.properties.dealname | mrkdwn}}* (${{steps.deal.properties.amount}}), closing {{steps.deal.properties.closedate | date}}. <https://app.hubspot.com/contacts/{{install.portalId}}/record/0-3/{{steps.deal.id}}|Open deal>",
|
|
406
|
+
"digest-line": "\u2022 <https://app.hubspot.com/contacts/{{install.portalId}}/record/0-3/{{item.id}}|{{item.properties.dealname | mrkdwn}}>: ${{item.properties.amount | default: 0}}, closes {{item.properties.closedate | date}}",
|
|
407
407
|
"digest": ":calendar: *Closing in the next 7 days* ({{steps.deals | size}} deals)\n{{steps.build | join: '\\n'}}"
|
|
408
408
|
}
|
|
409
409
|
}
|
|
@@ -46,6 +46,8 @@ const FILTERS = {
|
|
|
46
46
|
seconds: (v) => (v == null ? '' : String(Math.round(Number(v) / 1000))),
|
|
47
47
|
date: (v) => (v ? new Date(v).toISOString().slice(0, 10) : ''),
|
|
48
48
|
json: (v) => JSON.stringify(v),
|
|
49
|
+
// Record data inside a Slack message: escaped so a name like "*x* <!channel>" neither formats nor pings.
|
|
50
|
+
mrkdwn: (v) => require('./renderers/slack').escapeMrkdwn(v),
|
|
49
51
|
default: (v, arg) => (v == null || v === '' ? arg : v),
|
|
50
52
|
truncate: (v, arg) => { const s = String(v ?? ''); const n = Number(arg) || 80; return s.length > n ? s.slice(0, n - 1) + '…' : s; },
|
|
51
53
|
// lists
|
|
@@ -15,6 +15,11 @@ const { TRANSFORMS, applyMapping } = require('./transforms');
|
|
|
15
15
|
const { FILTERS } = require('./expressions');
|
|
16
16
|
const { validateManifest } = require('./manifests');
|
|
17
17
|
const { checkDefinition, checkManifest, checkInstallation } = require('./schema-check');
|
|
18
|
+
const { viewToSlack, escapeMrkdwn } = require('./renderers/slack');
|
|
19
|
+
const { viewToHubSpot } = require('./renderers/hubspot');
|
|
20
|
+
const { viewToHtml, slackMessageToHtml } = require('./renderers/html');
|
|
21
|
+
const { previewSurface, previewWorkflow, recordingPorts } = require('./renderers/preview');
|
|
22
|
+
const { decodeAction } = require('./renderers/common');
|
|
18
23
|
const useCases = require('./use-cases/definition-use-cases');
|
|
19
24
|
|
|
20
25
|
/** Load every capabilities/*.capabilities.json (bundled mocks; real modules ship their own). */
|
|
@@ -35,5 +40,6 @@ module.exports = {
|
|
|
35
40
|
defineIntegrationClass, defineSharedIntegrationClass, extendIntegrationClass, memoryDefinitions,
|
|
36
41
|
BUILTINS, TRANSFORMS, FILTERS, applyMapping, resolveOptions,
|
|
37
42
|
loadManifests, loadExamples,
|
|
43
|
+
viewToSlack, escapeMrkdwn, viewToHubSpot, viewToHtml, slackMessageToHtml, previewSurface, previewWorkflow, recordingPorts, decodeAction,
|
|
38
44
|
...useCases,
|
|
39
45
|
};
|
|
@@ -40,6 +40,23 @@ function validateManifest(m) {
|
|
|
40
40
|
if (!KINDS.has(c?.kind)) err(`${p}.kind`, [...KINDS].join(', '));
|
|
41
41
|
if (!/^[A-Za-z_$][\w$]*$/.test(c?.method || '')) err(`${p}.method`, 'Name of a method on the module\'s Api class');
|
|
42
42
|
for (const [arg, spec] of Object.entries(c?.input || {})) if (!spec?.type) err(`${p}.input.${arg}`, 'Input needs a type');
|
|
43
|
+
if (c?.status !== undefined && c.status !== 'unimplemented') err(`${p}.status`, 'Only "unimplemented" (the module\'s Api has no such method)');
|
|
44
|
+
if (c?.args !== undefined) {
|
|
45
|
+
if (!Array.isArray(c.args)) err(`${p}.args`, 'An ordered list: input names, "$rest", or { value }');
|
|
46
|
+
else {
|
|
47
|
+
const named = new Set();
|
|
48
|
+
c.args.forEach((a, i) => {
|
|
49
|
+
if (typeof a === 'string' && a !== '$rest') {
|
|
50
|
+
if (!(a in (c.input || {}))) err(`${p}.args[${i}]`, `"${a}" is not an input of this capability`);
|
|
51
|
+
else if (named.has(a)) err(`${p}.args[${i}]`, `"${a}" is passed twice`);
|
|
52
|
+
named.add(a);
|
|
53
|
+
} else if (a === '$rest') { if (c.args.indexOf('$rest') !== i) err(`${p}.args[${i}]`, '"$rest" appears once'); }
|
|
54
|
+
else if (!(a && typeof a === 'object' && 'value' in a)) err(`${p}.args[${i}]`, 'An input name, "$rest" or { value }');
|
|
55
|
+
});
|
|
56
|
+
// An input that is never passed would be accepted and silently dropped.
|
|
57
|
+
if (!c.args.includes('$rest')) for (const k of Object.keys(c.input || {})) if (!named.has(k)) err(`${p}.args`, `Input "${k}" is declared but never passed to ${c.method}; list it in args or use "$rest"`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
43
60
|
}
|
|
44
61
|
|
|
45
62
|
// How a sync reads and writes an object (sync-engine.js). Each operation names a capability of the right kind on
|
|
@@ -140,6 +157,7 @@ function validateManifest(m) {
|
|
|
140
157
|
if (w.account) tpl(w.account, 'webhook.account');
|
|
141
158
|
const batch = w.batch == null ? [] : Array.isArray(w.batch) ? w.batch : [w.batch];
|
|
142
159
|
if (batch.length > 2 || batch.some((b) => typeof b !== 'string')) err('webhook.batch', 'A path to the array of events, or [outer, inner] for two levels');
|
|
160
|
+
if (w.status !== undefined && w.status !== 'unimplemented') err('webhook.status', 'Only "unimplemented" (the module\'s Api has no verify method)');
|
|
143
161
|
if (w.verify != null && !/^[A-Za-z_$][\w$]*$/.test(w.verify)) err('webhook.verify', 'Name of an Api method that checks the signature');
|
|
144
162
|
if (w.handshake) {
|
|
145
163
|
try { parseCondition(w.handshake.when); } catch (e) { err('webhook.handshake.when', e.message); }
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What every renderer shares: coercion of view props (they come from record data, so any type can arrive), the one
|
|
3
|
+
* URL rule, truncation that never leaves half an entity behind, and the button <-> Slack round trip.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** A view prop as text. Objects are JSON, null/undefined are empty: a renderer never prints "[object Object]" or "undefined". */
|
|
7
|
+
function str(v) {
|
|
8
|
+
if (v == null) return '';
|
|
9
|
+
if (typeof v === 'string') return v;
|
|
10
|
+
if (typeof v === 'object') { try { return JSON.stringify(v); } catch (_) { return ''; } }
|
|
11
|
+
return String(v);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Only http(s) links leave a preview or a card. Record data decides these URLs ("{{data.proposal.recording}}"), and a
|
|
16
|
+
* `javascript:` or `data:` URL in a vendor field must not become a clickable thing in someone's HubSpot or Slack.
|
|
17
|
+
* @returns {string|null}
|
|
18
|
+
*/
|
|
19
|
+
function safeUrl(u) {
|
|
20
|
+
const s = str(u).trim();
|
|
21
|
+
if (!/^https?:\/\/[^\s<>"']+$/i.test(s)) return null;
|
|
22
|
+
try { new URL(s); } catch (_) { return null; }
|
|
23
|
+
return s;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Cut to at most `max` characters, ending in the marker. Never leaves a dangling half of a Slack entity ("&am"), which
|
|
28
|
+
* would render as literal junk instead of the character it stood for.
|
|
29
|
+
*/
|
|
30
|
+
function truncate(s, max, marker = '…') {
|
|
31
|
+
s = String(s);
|
|
32
|
+
if (s.length <= max) return s;
|
|
33
|
+
let cut = s.slice(0, Math.max(0, max - marker.length));
|
|
34
|
+
cut = cut.replace(/&(?:[a-z]{1,4}|#\d{0,6}|#x[0-9a-f]{0,5})?$/i, '');
|
|
35
|
+
return cut + marker;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Badge tone (`neutral`, or a Jira statusCategory key like `done`) -> one of four. Unknown is neutral, never an error. */
|
|
39
|
+
function toneOf(tone) {
|
|
40
|
+
const t = str(tone).toLowerCase();
|
|
41
|
+
if (['positive', 'success', 'good', 'done', 'green', 'complete', 'completed'].includes(t)) return 'positive';
|
|
42
|
+
if (['warning', 'caution', 'indeterminate', 'yellow', 'in_progress', 'pending'].includes(t)) return 'warning';
|
|
43
|
+
if (['critical', 'danger', 'error', 'bad', 'red', 'failed'].includes(t)) return 'critical';
|
|
44
|
+
return 'neutral';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const ACTION_VALUE_MAX = 2000; // Slack's cap on a button's value
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A userAction button's identity as a Slack button value: which installation, which workflow, which surface it was on.
|
|
51
|
+
* `needsInput` marks a button whose workflow asks for values first (a Slack button cannot, so the handler opens a form).
|
|
52
|
+
*/
|
|
53
|
+
function encodeAction({ integrationId, workflow, surface, needsInput }) {
|
|
54
|
+
const value = JSON.stringify({ ...(integrationId ? { integrationId: String(integrationId) } : {}), workflow, surface, ...(needsInput ? { needsInput: true } : {}) });
|
|
55
|
+
return value.length <= ACTION_VALUE_MAX ? value : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The inverse, for the handler that receives a click. null for anything that is not one of ours. */
|
|
59
|
+
function decodeAction(value) {
|
|
60
|
+
let v;
|
|
61
|
+
try { v = JSON.parse(value); } catch (_) { return null; }
|
|
62
|
+
if (!v || typeof v !== 'object' || typeof v.workflow !== 'string' || !v.workflow || typeof v.surface !== 'string') return null;
|
|
63
|
+
if (v.integrationId !== undefined && typeof v.integrationId !== 'string') return null;
|
|
64
|
+
return { ...(v.integrationId ? { integrationId: v.integrationId } : {}), workflow: v.workflow, surface: v.surface, ...(v.needsInput ? { needsInput: true } : {}) };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { str, safeUrl, truncate, toneOf, encodeAction, decodeAction, ACTION_VALUE_MAX };
|