@aglyn/plugins-workflows 1.0.0-beta.218 → 1.0.0-beta.220

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 (85) hide show
  1. package/README.md +33 -4
  2. package/package.json +13 -13
  3. package/src/index.d.ts +1 -0
  4. package/src/index.js.map +1 -1
  5. package/src/lib/components/host-actions-card.component.js +3 -2
  6. package/src/lib/components/host-actions-card.component.js.map +1 -1
  7. package/src/lib/components/host-run-history-card.component.d.ts +2 -2
  8. package/src/lib/components/host-run-history-card.component.js +3 -2
  9. package/src/lib/components/host-run-history-card.component.js.map +1 -1
  10. package/src/lib/components/host-webhooks-card.component.js +2 -1
  11. package/src/lib/components/host-webhooks-card.component.js.map +1 -1
  12. package/src/lib/components/host-workflows-card.component.js +4 -3
  13. package/src/lib/components/host-workflows-card.component.js.map +1 -1
  14. package/src/lib/components/use-automation-step-pickers.js +50 -42
  15. package/src/lib/components/use-automation-step-pickers.js.map +1 -1
  16. package/src/lib/components/use-org-automation-step-pickers.js +23 -11
  17. package/src/lib/components/use-org-automation-step-pickers.js.map +1 -1
  18. package/src/lib/components/workflow-zones.d.ts +49 -0
  19. package/src/lib/components/workflow-zones.js +3 -0
  20. package/src/lib/components/workflow-zones.js.map +1 -1
  21. package/src/lib/declarations.server.d.ts +6 -0
  22. package/src/lib/declarations.server.js +39 -0
  23. package/src/lib/declarations.server.js.map +1 -1
  24. package/src/lib/engine/crm-action-steps.d.ts +14 -1
  25. package/src/lib/engine/crm-action-steps.js +15 -0
  26. package/src/lib/engine/crm-action-steps.js.map +1 -1
  27. package/src/lib/engine/run-event-actions.d.ts +2 -1
  28. package/src/lib/engine/run-event-actions.js +66 -25
  29. package/src/lib/engine/run-event-actions.js.map +1 -1
  30. package/src/lib/engine/run-event-workflows.js +2 -1
  31. package/src/lib/engine/run-event-workflows.js.map +1 -1
  32. package/src/lib/engine/workflow-steps.d.ts +1 -1
  33. package/src/lib/engine/workflow-steps.js +1 -1
  34. package/src/lib/engine/workflow-steps.js.map +1 -1
  35. package/src/lib/model/automation-record.d.ts +47 -0
  36. package/src/lib/model/automation-record.js +79 -0
  37. package/src/lib/model/automation-record.js.map +1 -0
  38. package/src/lib/model/org-automations.js.map +1 -1
  39. package/src/lib/model/step-outcomes.d.ts +42 -0
  40. package/src/lib/model/step-outcomes.js +70 -0
  41. package/src/lib/model/step-outcomes.js.map +1 -0
  42. package/src/lib/model/webhooks.d.ts +36 -0
  43. package/src/lib/model/webhooks.js +24 -0
  44. package/src/lib/model/webhooks.js.map +1 -0
  45. package/src/lib/model/workflows-record-lists.d.ts +18 -0
  46. package/src/lib/model/workflows-record-lists.js +45 -0
  47. package/src/lib/model/workflows-record-lists.js.map +1 -0
  48. package/src/lib/model/workflows-record-routes.d.ts +22 -0
  49. package/src/lib/model/workflows-record-routes.js +73 -0
  50. package/src/lib/model/workflows-record-routes.js.map +1 -0
  51. package/src/lib/model/workflows-search-sources.d.ts +20 -0
  52. package/src/lib/model/workflows-search-sources.js +35 -0
  53. package/src/lib/model/workflows-search-sources.js.map +1 -0
  54. package/src/lib/model/workflows.d.ts +102 -0
  55. package/src/lib/model/workflows.js +167 -0
  56. package/src/lib/model/workflows.js.map +1 -0
  57. package/src/lib/plan-entitlements.d.ts +36 -0
  58. package/src/lib/plan-entitlements.js +44 -0
  59. package/src/lib/plan-entitlements.js.map +1 -0
  60. package/src/lib/plugin.js +37 -2
  61. package/src/lib/plugin.js.map +1 -1
  62. package/src/lib/server/automation-record-index.d.ts +20 -0
  63. package/src/lib/server/automation-record-index.js +59 -0
  64. package/src/lib/server/automation-record-index.js.map +1 -0
  65. package/src/lib/server/workflow-dependents.d.ts +28 -0
  66. package/src/lib/server/workflow-dependents.js +61 -0
  67. package/src/lib/server/workflow-dependents.js.map +1 -0
  68. package/src/lib/server/workflow-variables.d.ts +20 -0
  69. package/src/lib/server/workflow-variables.js +75 -0
  70. package/src/lib/server/workflow-variables.js.map +1 -0
  71. package/src/lib/server.d.ts +1 -0
  72. package/src/lib/server.js +2 -1
  73. package/src/lib/server.js.map +1 -1
  74. package/src/lib/tenant-emails.d.ts +29 -0
  75. package/src/lib/tenant-emails.js +41 -0
  76. package/src/lib/tenant-emails.js.map +1 -0
  77. package/src/lib/testing/stand-in-dataset-list.d.ts +28 -0
  78. package/src/lib/testing/stand-in-dataset-list.js +55 -0
  79. package/src/lib/testing/stand-in-dataset-list.js.map +1 -0
  80. package/src/lib/testing/stand-in-overlay-list.d.ts +26 -0
  81. package/src/lib/testing/stand-in-overlay-list.js +53 -0
  82. package/src/lib/testing/stand-in-overlay-list.js.map +1 -0
  83. package/src/lib/usage-axes.d.ts +28 -0
  84. package/src/lib/usage-axes.js +72 -0
  85. package/src/lib/usage-axes.js.map +1 -0
@@ -14,7 +14,7 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
14
14
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
15
  * See the License for the specific language governing permissions and
16
16
  * limitations under the License.
17
- */ import { ACTION_MAX_EVENT_DEPTH, ACTION_MAX_STEPS, checkEntitlement, consentGroupForHost, planLabelGrantingFeature, checkQuota, WEBHOOK_URL_PATTERN, evaluateExpression, evaluateStepGuard, evaluateTriggerConditions, FLOW_TIMED_OUT_FIELD, flowEmailTopicId, hostPublicOrigin, isClientActionStep, isCrmActionStep, isFlowSuspendingStep, buildDatasetRecordValues, datasetDisplayName, contactCampaignFieldPath, datasetIntegrityFields, datasetIntegrityUpdate, describeStepOutcome, effectiveDatasetModel, normalizeTriggerConditions, resolveOrgEntitlements, runWorkflow, WORKFLOW_MAX_STEPS } from "@aglyn/aglyn/server";
17
+ */ import { ACTION_MAX_STEPS, checkEntitlement, consentGroupForHost, planLabelGrantingFeature, checkQuota, evaluateExpression, evaluateStepGuard, evaluateTriggerConditions, FLOW_TIMED_OUT_FIELD, flowSubscriptionTopicId, hostPublicOrigin, isClientActionStep, isFlowSuspendingStep, datasetDisplayName, describeDatasetRecordErrors, contactContainerFieldPath, datasetIntegrityFields, datasetIntegrityUpdate, effectiveDatasetModel, ensureDeclaredCustomFieldTypes, normalizeTriggerConditions, prepareDatasetRecordWrite, resolveOrgEntitlements } from "@aglyn/aglyn/server";
18
18
  import { isDeferrableSendResult, isEmailConfigured, sendEmail, sendFailureReason } from "@aglyn/shared-util-email";
19
19
  import { dataStorageRefusal, enrollListMember, firebaseAdmin, flowEmailRefusal, getOrgForHost, hostSendingIdentity, meterHostEmail, notifyHostManagers, consentGroupForSite, orgDataCollectionForHost, orgDataQueryForHost, resolveOrgIdForHost } from "@aglyn/tenant-data-admin";
20
20
  import { announceDatasetRecordChange } from "@aglyn/tenant-data-admin/server/dataset-live-pages";
@@ -23,6 +23,7 @@ import { visibleToHost } from "@aglyn/aglyn/app-utils/scope-tokens";
23
23
  // The leaf, not the barrel: this library's specs substitute the barrel
24
24
  // wholesale, and the lookup must reach the real index logic under them.
25
25
  import { findContactByEmail } from "@aglyn/tenant-data-admin/server/contact-email-index";
26
+ import { findOrgContainersByName, readOrgContainers } from "@aglyn/tenant-data-admin/server/org-containers";
26
27
  // The leaf again, for the phishing screen's hold (AGL-3356): the control
27
28
  // that stops a send must be the real one under a spec that mocks the barrel.
28
29
  import { screenOutboundSend } from "@aglyn/tenant-data-admin/server/outbound-send-review";
@@ -30,9 +31,12 @@ import { renderSiteTextEmail } from "@aglyn/tenant-data-admin/server/host-email-
30
31
  import { createHmac } from "crypto";
31
32
  import { FieldValue } from "firebase-admin/firestore";
32
33
  import { runSummaryFields } from "../model/run-history.js";
34
+ import { ACTION_MAX_EVENT_DEPTH, runWorkflow, WORKFLOW_MAX_STEPS } from "../model/workflows.js";
35
+ import { describeStepOutcome } from "../model/step-outcomes.js";
36
+ import { WEBHOOK_URL_PATTERN } from "../model/webhooks.js";
33
37
  import { eventRunSuspension } from "./site-suspension.js";
34
38
  import { advanceFlowEnrollment, claimFlowEnrollment, deferFlowEnrollment, endFlowEnrollment, enrollInFlow, findFlowEnrollmentsAwaiting, sweepDueFlowEnrollments } from "./flow-enrollments.js";
35
- import { logCrmEmailActivity, prepareCrmEmailActivity, runCrmActionStep } from "./crm-action-steps.js";
39
+ import { isCrmActionStep, logCrmEmailActivity, prepareCrmEmailActivity, runCrmActionStep } from "./crm-action-steps.js";
36
40
  import { resolveDatasetDoc } from "@aglyn/tenant-runtime/resolve-dataset";
37
41
  import { resumeWorkflowEnrollment, runEventWorkflows } from "./run-event-workflows.js";
38
42
  import { ORG_AUTOMATION_RUN_TARGET, orgAutomationStepRefusal } from "../model/org-automations.js";
@@ -384,7 +388,11 @@ async function datasetAppendRefusal(env, datasetRef) {
384
388
  model: datasetDoc.get('model'),
385
389
  fields: Array.isArray(datasetDoc.get('fields')) ? datasetDoc.get('fields') : []
386
390
  };
387
- const values = buildDatasetRecordValues(appendDataset, payload);
391
+ const appendModel = effectiveDatasetModel(appendDataset);
392
+ // A plugin's field validator runs only once its plugin registered it.
393
+ await ensureDeclaredCustomFieldTypes(appendModel);
394
+ const write = prepareDatasetRecordWrite(appendDataset, payload);
395
+ const values = write.values;
388
396
  // Same name precedence `findDatasetByName` resolves in.
389
397
  const appendLabel = (datasetDisplayName({
390
398
  displayName: datasetDoc.get('displayName'),
@@ -394,14 +402,23 @@ async function datasetAppendRefusal(env, datasetRef) {
394
402
  // to write. An error rather than a quiet success: a run history that
395
403
  // says `saved to Leads` while nothing saves is how a mismatched field
396
404
  // name goes unnoticed.
397
- if (!Object.keys(values).length) {
405
+ if (!write.matched.length) {
398
406
  return failed(`no event field matches a field in dataset "${appendLabel || step.datasetId}"`);
399
407
  }
408
+ // Held to the model like every other record write (AGL-2773): a value
409
+ // its field cannot hold refuses the whole record, and the run says
410
+ // which field and why instead of `saved to Leads`.
411
+ if (Object.keys(write.errors).length) {
412
+ return failed(`record failed validation for dataset "${appendLabel || step.datasetId}": ${describeDatasetRecordErrors(write.errors)}`);
413
+ }
414
+ if (!Object.keys(values).length) {
415
+ return failed(`every event field matching dataset "${appendLabel || step.datasetId}" is empty`);
416
+ }
400
417
  const refusal = await datasetAppendRefusal(env, datasetDoc.ref);
401
418
  if (refusal) return failed(refusal);
402
419
  await datasetDoc.ref.collection('records').add(_extends({
403
420
  values
404
- }, datasetIntegrityFields(effectiveDatasetModel(appendDataset), values), {
421
+ }, datasetIntegrityFields(appendModel, values), {
405
422
  createdAt: FieldValue.serverTimestamp()
406
423
  }));
407
424
  await announceDatasetStepWrite(env, datasetDoc.id);
@@ -421,15 +438,22 @@ async function datasetAppendRefusal(env, datasetRef) {
421
438
  fields: Array.isArray(datasetDoc.get('fields')) ? datasetDoc.get('fields') : []
422
439
  };
423
440
  const updateModel = effectiveDatasetModel(updateDataset);
424
- const values = buildDatasetRecordValues(updateDataset, payload);
441
+ await ensureDeclaredCustomFieldTypes(updateModel);
442
+ const updateLabel = String(datasetDisplayName({
443
+ displayName: datasetDoc.get('displayName'),
444
+ name: datasetDoc.get('name')
445
+ }) || step.datasetName || step.datasetId || '').slice(0, 60);
446
+ // Checked before the lookup, as an append: the fields this write
447
+ // supplies are held to the model whichever leg runs below, and a
448
+ // refusal costs no read.
449
+ const incoming = prepareDatasetRecordWrite(updateDataset, payload);
425
450
  // Nothing to merge or append — an error, for the reason the append
426
451
  // branch above gives.
427
- if (!Object.keys(values).length) {
428
- const updateLabel = datasetDisplayName({
429
- displayName: datasetDoc.get('displayName'),
430
- name: datasetDoc.get('name')
431
- }) || step.datasetName || step.datasetId;
432
- return failed(`no event field matches a field in dataset "${String(updateLabel != null ? updateLabel : '').slice(0, 60)}"`);
452
+ if (!incoming.matched.length) {
453
+ return failed(`no event field matches a field in dataset "${updateLabel}"`);
454
+ }
455
+ if (!Object.keys(incoming.values).length) {
456
+ return failed(`every event field matching dataset "${updateLabel}" is empty`);
433
457
  }
434
458
  const email = String((_payload_email = payload.email) != null ? _payload_email : '').trim();
435
459
  // `records.values` is exempt from indexing, so this lookup is served
@@ -439,7 +463,16 @@ async function datasetAppendRefusal(env, datasetRef) {
439
463
  const existing = email ? await datasetDoc.ref.collection('records').where('values.email', '==', email).limit(1).get() : null;
440
464
  if (existing && !existing.empty) {
441
465
  var _existing_docs__get;
442
- const merged = _extends({}, (_existing_docs__get = existing.docs[0].get('values')) != null ? _existing_docs__get : {}, values);
466
+ // A merge holds only the fields this write sent to the model: a row
467
+ // stored as text before AGL-2773 is not refused for a legacy value
468
+ // this run never touched.
469
+ const write = prepareDatasetRecordWrite(updateDataset, payload, {
470
+ existing: (_existing_docs__get = existing.docs[0].get('values')) != null ? _existing_docs__get : {}
471
+ });
472
+ if (Object.keys(write.errors).length) {
473
+ return failed(`record failed validation for dataset "${updateLabel}": ${describeDatasetRecordErrors(write.errors)}`);
474
+ }
475
+ const merged = write.values;
443
476
  await existing.docs[0].ref.set(_extends({
444
477
  values: merged
445
478
  }, datasetIntegrityUpdate(updateModel, merged, FieldValue.delete()), {
@@ -460,12 +493,16 @@ async function datasetAppendRefusal(env, datasetRef) {
460
493
  } else {
461
494
  // The APPEND leg of update-or-append, and the only one of the two
462
495
  // that adds a row — the merge above rewrites a record that already
463
- // counts against the band.
496
+ // counts against the band. A new row is held to the whole model,
497
+ // required fields included, like any other append.
498
+ if (Object.keys(incoming.errors).length) {
499
+ return failed(`record failed validation for dataset "${updateLabel}": ${describeDatasetRecordErrors(incoming.errors)}`);
500
+ }
464
501
  const refusal = await datasetAppendRefusal(env, datasetDoc.ref);
465
502
  if (refusal) return failed(refusal);
466
503
  await datasetDoc.ref.collection('records').add(_extends({
467
- values
468
- }, datasetIntegrityFields(updateModel, values), {
504
+ values: incoming.values
505
+ }, datasetIntegrityFields(updateModel, incoming.values), {
469
506
  createdAt: FieldValue.serverTimestamp()
470
507
  }));
471
508
  }
@@ -546,7 +583,7 @@ async function datasetAppendRefusal(env, datasetRef) {
546
583
  * so the opt-out link the seam mints names it. Without that the
547
584
  * preference page opens on a list of every stream the site has, and
548
585
  * the recipient has to find the one they were trying to leave.
549
- */ const topicId = flowEmailTopicId(step.topicId, scope);
586
+ */ const topicId = flowSubscriptionTopicId(step.topicId, scope);
550
587
  const gate = await flowEmailRefusal({
551
588
  hostId,
552
589
  email: to,
@@ -636,7 +673,7 @@ async function datasetAppendRefusal(env, datasetRef) {
636
673
  priority: 'bulk'
637
674
  } : {}, {
638
675
  // `topicId` is `''` for a step that belongs to no stream, which
639
- // every reader of it treats as absent — see `flowEmailTopicId`.
676
+ // every reader of it treats as absent — see `flowSubscriptionTopicId`.
640
677
  // The consent group comes off the org the run already holds, so the
641
678
  // gate honors an unsubscribe from any site of a declared group —
642
679
  // the sender this site mails as — and the org's confirmation switch,
@@ -753,17 +790,21 @@ async function datasetAppendRefusal(env, datasetRef) {
753
790
  */ const campaignLabel = step.campaignName || step.campaignId;
754
791
  const campaignOrgId = (_env_orgId = env.orgId) != null ? _env_orgId : await resolveOrgIdForHost(hostId);
755
792
  if (!campaignOrgId) return failed(`unknown campaign "${campaignLabel}"`);
756
- const campaignsRef = firebaseAdmin.app().firestore().collection('orgs').doc(campaignOrgId).collection('emailCampaigns');
757
- const usable = (doc)=>Boolean((doc == null ? void 0 : doc.exists) && !doc.get('deletedAt') && visibleToHost(doc.get('visibleTo'), hostId));
793
+ // The org's containers of the `campaign` kind, read where the kind's
794
+ // declaration says they are stored.
795
+ const containerStore = firebaseAdmin.app().firestore();
796
+ const usable = (container)=>Boolean((container == null ? void 0 : container.live) && visibleToHost(container.visibleTo, hostId));
758
797
  const namedId = (_ref7 = (_step_campaignId = step.campaignId) == null ? void 0 : _step_campaignId.trim()) != null ? _ref7 : '';
759
- const campaignDoc = namedId ? await campaignsRef.doc(namedId).get() : (await campaignsRef.where('name', '==', (_ref8 = (_step_campaignName = step.campaignName) == null ? void 0 : _step_campaignName.trim()) != null ? _ref8 : '').limit(10).get()).docs.find(usable);
760
- if (!(campaignDoc == null ? void 0 : campaignDoc.exists)) {
798
+ const campaign = namedId ? (await readOrgContainers(containerStore, 'campaign', campaignOrgId, [
799
+ namedId
800
+ ]))[0] : (await findOrgContainersByName(containerStore, 'campaign', campaignOrgId, (_ref8 = (_step_campaignName = step.campaignName) == null ? void 0 : _step_campaignName.trim()) != null ? _ref8 : '', 10)).find(usable);
801
+ if (!(campaign == null ? void 0 : campaign.exists)) {
761
802
  return failed(`unknown campaign "${campaignLabel}"`);
762
803
  }
763
- if (campaignDoc.get('deletedAt')) {
804
+ if (!campaign.live) {
764
805
  return failed(`campaign "${campaignLabel}" was deleted`);
765
806
  }
766
- if (!usable(campaignDoc)) {
807
+ if (!usable(campaign)) {
767
808
  return failed(`campaign "${campaignLabel}" is not placed on this site`);
768
809
  }
769
810
  /*
@@ -781,7 +822,7 @@ async function datasetAppendRefusal(env, datasetRef) {
781
822
  * update cannot fail for absence.
782
823
  */ const group = await consentGroupForSite(hostId);
783
824
  await contact.ref.update({
784
- [contactCampaignFieldPath(group.groupId)]: FieldValue.arrayUnion(campaignDoc.id),
825
+ [contactContainerFieldPath(group.groupId, 'campaign')]: FieldValue.arrayUnion(campaign.id),
785
826
  updatedAt: FieldValue.serverTimestamp()
786
827
  });
787
828
  } else if (isCrmActionStep(step)) {