@aglyn/plugins-forms 1.0.0-beta.221 → 1.0.0-beta.223

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 (75) hide show
  1. package/README.md +1 -0
  2. package/package.json +14 -12
  3. package/src/lib/components/form-detail-card.js +166 -29
  4. package/src/lib/components/form-detail-card.js.map +1 -1
  5. package/src/lib/components/form-submissions-card.component.d.ts +12 -0
  6. package/src/lib/components/form-submissions-card.component.js +35 -2
  7. package/src/lib/components/form-submissions-card.component.js.map +1 -1
  8. package/src/lib/components/form.js +2 -1
  9. package/src/lib/components/form.js.map +1 -1
  10. package/src/lib/components/host-forms-card.component.d.ts +8 -0
  11. package/src/lib/components/host-forms-card.component.js +26 -1
  12. package/src/lib/components/host-forms-card.component.js.map +1 -1
  13. package/src/lib/components/site-package-form-preview.component.d.ts +13 -0
  14. package/src/lib/components/site-package-form-preview.component.js +57 -0
  15. package/src/lib/components/site-package-form-preview.component.js.map +1 -0
  16. package/src/lib/constants/site-package.d.ts +18 -0
  17. package/src/lib/constants/site-package.js +18 -0
  18. package/src/lib/constants/site-package.js.map +1 -0
  19. package/src/lib/declarations.console-server.d.ts +39 -0
  20. package/src/lib/declarations.console-server.js +68 -0
  21. package/src/lib/declarations.console-server.js.map +1 -0
  22. package/src/lib/declarations.server.js +22 -0
  23. package/src/lib/declarations.server.js.map +1 -1
  24. package/src/lib/model/form-door.d.ts +37 -0
  25. package/src/lib/model/form-door.js +50 -0
  26. package/src/lib/model/form-door.js.map +1 -0
  27. package/src/lib/model/form-submission-counts.d.ts +26 -0
  28. package/src/lib/model/form-submission-counts.js +35 -0
  29. package/src/lib/model/form-submission-counts.js.map +1 -0
  30. package/src/lib/model/form-submission-list.d.ts +40 -0
  31. package/src/lib/model/form-submission-list.js +66 -0
  32. package/src/lib/model/form-submission-list.js.map +1 -0
  33. package/src/lib/model/form-submission-record.d.ts +52 -0
  34. package/src/lib/model/form-submission-record.js +70 -0
  35. package/src/lib/model/form-submission-record.js.map +1 -0
  36. package/src/lib/model/form-unavailable.d.ts +83 -0
  37. package/src/lib/model/form-unavailable.js +88 -0
  38. package/src/lib/model/form-unavailable.js.map +1 -0
  39. package/src/lib/model/forms-record-lists.d.ts +31 -0
  40. package/src/lib/model/forms-record-lists.js +55 -0
  41. package/src/lib/model/forms-record-lists.js.map +1 -0
  42. package/src/lib/plan-entitlements.js +6 -6
  43. package/src/lib/plan-entitlements.js.map +1 -1
  44. package/src/lib/plugin.js +34 -0
  45. package/src/lib/plugin.js.map +1 -1
  46. package/src/lib/server/api-v1/form-submissions.d.ts +34 -0
  47. package/src/lib/server/api-v1/form-submissions.js +273 -0
  48. package/src/lib/server/api-v1/form-submissions.js.map +1 -0
  49. package/src/lib/server/api-v1/openapi.d.ts +26 -0
  50. package/src/lib/server/api-v1/openapi.js +133 -0
  51. package/src/lib/server/api-v1/openapi.js.map +1 -0
  52. package/src/lib/server/form-intake-gate.d.ts +34 -0
  53. package/src/lib/server/form-intake-gate.js +44 -0
  54. package/src/lib/server/form-intake-gate.js.map +1 -0
  55. package/src/lib/server/form-submission-index.d.ts +26 -0
  56. package/src/lib/server/form-submission-index.js +51 -0
  57. package/src/lib/server/form-submission-index.js.map +1 -0
  58. package/src/lib/server/form-submit.d.ts +43 -0
  59. package/src/lib/server/form-submit.js +850 -0
  60. package/src/lib/server/form-submit.js.map +1 -0
  61. package/src/lib/server/increment-form-stats.d.ts +70 -0
  62. package/src/lib/server/increment-form-stats.js +112 -0
  63. package/src/lib/server/increment-form-stats.js.map +1 -0
  64. package/src/lib/server.d.ts +10 -0
  65. package/src/lib/server.js +15 -0
  66. package/src/lib/server.js.map +1 -1
  67. package/src/lib/transfer/form-submissions-transfer-key.d.ts +37 -0
  68. package/src/lib/transfer/form-submissions-transfer-key.js +40 -0
  69. package/src/lib/transfer/form-submissions-transfer-key.js.map +1 -0
  70. package/src/lib/transfer/form-submissions-transfer.d.ts +98 -0
  71. package/src/lib/transfer/form-submissions-transfer.js +563 -0
  72. package/src/lib/transfer/form-submissions-transfer.js.map +1 -0
  73. package/src/lib/transfer/form-submissions-transfer.server.d.ts +26 -0
  74. package/src/lib/transfer/form-submissions-transfer.server.js +31 -0
  75. package/src/lib/transfer/form-submissions-transfer.server.js.map +1 -0
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type PluginRecordListSource } from '@aglyn/aglyn/plugin-manager/plugin-record-lists';
18
+ /**
19
+ * A site's forms, listed in the console for another plugin's picker
20
+ * (AGL-3080) — a recipe that keys an automation on the form a person picks.
21
+ *
22
+ * A form is the site's: the query lists `hosts/{hostId}/forms`, ordered by
23
+ * document id so a reader asking one past its window learns it was cut, and
24
+ * answers nothing at the organization, where no site is named. An archived
25
+ * form collects nothing, so it is left out — an automation keyed on it would
26
+ * never fire. A form with no display name reads by its id, as every form list
27
+ * names one. Its one fact, `routesLeads`, says whether it files leads.
28
+ */
29
+ export declare const formRecordListSource: PluginRecordListSource;
30
+ /** Called from the console registrar, owner named for a spec that calls it directly. */
31
+ export declare function registerFormRecordList(): void;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */ import { registerPluginRecordListSource } from "@aglyn/aglyn/plugin-manager/plugin-record-lists";
17
+ import { collection, documentId, limit, orderBy, query } from "firebase/firestore";
18
+ import { BUNDLE_ID } from "../constants/bundle-common.js";
19
+ /**
20
+ * A site's forms, listed in the console for another plugin's picker
21
+ * (AGL-3080) — a recipe that keys an automation on the form a person picks.
22
+ *
23
+ * A form is the site's: the query lists `hosts/{hostId}/forms`, ordered by
24
+ * document id so a reader asking one past its window learns it was cut, and
25
+ * answers nothing at the organization, where no site is named. An archived
26
+ * form collects nothing, so it is left out — an automation keyed on it would
27
+ * never fire. A form with no display name reads by its id, as every form list
28
+ * names one. Its one fact, `routesLeads`, says whether it files leads.
29
+ */ export const formRecordListSource = {
30
+ query (firestore, request) {
31
+ if (!request.hostId) return null;
32
+ return query(collection(firestore, 'hosts', request.hostId, 'forms'), orderBy(documentId()), limit(request.limit));
33
+ },
34
+ record (id, data) {
35
+ var _data_displayName;
36
+ if (data['archivedAt'] != null) return null;
37
+ // Whether the form files its people as leads (`routing.lead`): a picker's
38
+ // recipe keys on the record such a form makes.
39
+ const routing = data['routing'];
40
+ return {
41
+ id,
42
+ name: String((_data_displayName = data['displayName']) != null ? _data_displayName : '').trim() || id,
43
+ facts: {
44
+ routesLeads: (routing == null ? void 0 : routing.lead) === true
45
+ }
46
+ };
47
+ }
48
+ };
49
+ /** Called from the console registrar, owner named for a spec that calls it directly. */ export function registerFormRecordList() {
50
+ registerPluginRecordListSource('form', formRecordListSource, {
51
+ pluginId: BUNDLE_ID
52
+ });
53
+ }
54
+
55
+ //# sourceMappingURL=forms-record-lists.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../libs/plugins/forms/src/lib/model/forms-record-lists.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n registerPluginRecordListSource,\n type PluginRecordListSource,\n} from '@aglyn/aglyn/plugin-manager/plugin-record-lists'\nimport { collection, documentId, limit, orderBy, query } from 'firebase/firestore'\nimport { BUNDLE_ID } from '../constants/bundle-common'\n\n/**\n * A site's forms, listed in the console for another plugin's picker\n * (AGL-3080) — a recipe that keys an automation on the form a person picks.\n *\n * A form is the site's: the query lists `hosts/{hostId}/forms`, ordered by\n * document id so a reader asking one past its window learns it was cut, and\n * answers nothing at the organization, where no site is named. An archived\n * form collects nothing, so it is left out — an automation keyed on it would\n * never fire. A form with no display name reads by its id, as every form list\n * names one. Its one fact, `routesLeads`, says whether it files leads.\n */\nexport const formRecordListSource: PluginRecordListSource = {\n query(firestore, request) {\n if (!request.hostId) return null\n return query(\n collection(firestore, 'hosts', request.hostId, 'forms'),\n orderBy(documentId()),\n limit(request.limit),\n )\n },\n record(id, data) {\n if (data['archivedAt'] != null) return null\n // Whether the form files its people as leads (`routing.lead`): a picker's\n // recipe keys on the record such a form makes.\n const routing = data['routing'] as { lead?: unknown } | null | undefined\n return {\n id,\n name: String(data['displayName'] ?? '').trim() || id,\n facts: { routesLeads: routing?.lead === true },\n }\n },\n}\n\n/** Called from the console registrar, owner named for a spec that calls it directly. */\nexport function registerFormRecordList(): void {\n registerPluginRecordListSource('form', formRecordListSource, { pluginId: BUNDLE_ID })\n}\n"],"names":["registerPluginRecordListSource","collection","documentId","limit","orderBy","query","BUNDLE_ID","formRecordListSource","firestore","request","hostId","record","id","data","routing","name","String","trim","facts","routesLeads","lead","registerFormRecordList","pluginId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SACEA,8BAA8B,QAEzB,kDAAiD;AACxD,SAASC,UAAU,EAAEC,UAAU,EAAEC,KAAK,EAAEC,OAAO,EAAEC,KAAK,QAAQ,qBAAoB;AAClF,SAASC,SAAS,QAAQ,gCAA4B;AAEtD;;;;;;;;;;CAUC,GACD,OAAO,MAAMC,uBAA+C;IAC1DF,OAAMG,SAAS,EAAEC,OAAO;QACtB,IAAI,CAACA,QAAQC,MAAM,EAAE,OAAO;QAC5B,OAAOL,MACLJ,WAAWO,WAAW,SAASC,QAAQC,MAAM,EAAE,UAC/CN,QAAQF,eACRC,MAAMM,QAAQN,KAAK;IAEvB;IACAQ,QAAOC,EAAE,EAAEC,IAAI;YAOEA;QANf,IAAIA,IAAI,CAAC,aAAa,IAAI,MAAM,OAAO;QACvC,0EAA0E;QAC1E,+CAA+C;QAC/C,MAAMC,UAAUD,IAAI,CAAC,UAAU;QAC/B,OAAO;YACLD;YACAG,MAAMC,QAAOH,oBAAAA,IAAI,CAAC,cAAc,YAAnBA,oBAAuB,IAAII,IAAI,MAAML;YAClDM,OAAO;gBAAEC,aAAaL,CAAAA,2BAAAA,QAASM,IAAI,MAAK;YAAK;QAC/C;IACF;AACF,EAAC;AAED,sFAAsF,GACtF,OAAO,SAASC;IACdrB,+BAA+B,QAAQO,sBAAsB;QAAEe,UAAUhB;IAAU;AACrF"}
@@ -21,9 +21,9 @@
21
21
  free: 20,
22
22
  starter: 200,
23
23
  pro: 1000,
24
- business: 8000,
25
- scale: 25000,
26
- advanced: 40000,
24
+ business: 5000,
25
+ scale: 10000,
26
+ advanced: 10000,
27
27
  // FINITE, where every other capacity row on this tier is unbounded. The
28
28
  // band is multiplied by `hostLimit` to get the org-wide band, so at 100
29
29
  // hosts an unbounded figure was not merely large — it made this tier's cost
@@ -34,9 +34,9 @@
34
34
  //
35
35
  // Metering makes it safe to bound. `meteredInfraPassThrough` is true here,
36
36
  // so submissions past the band BILL at the pass-through rate rather than
37
- // being refused — a merchant's lead form does not stop working at 25,000.
38
- agency: 25000,
39
- enterprise: 50000
37
+ // being refused — a merchant's lead form does not stop working at the band.
38
+ agency: 10000,
39
+ enterprise: 20000
40
40
  };
41
41
  /** The forms plugin's plan figures, for the manifest generator. */ export function formsPlanEntitlements() {
42
42
  return {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/forms/src/lib/plan-entitlements.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Types only: the manifest generator loads this module on its own to compile\n// the plan figures, so nothing here may reach a core module at runtime.\nimport type { OrgPlan } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type { PluginPlanEntitlementsDeclaration } from '@aglyn/aglyn/plugin-manager/plugin-plan-entitlements'\n\n/**\n * THE FORMS PLUGIN'S PLAN BAND (AGL-3080): the form submissions each plan\n * includes a month, per site. Compiled into core's `PLAN_ENTITLEMENTS` by the\n * manifest generator (`register.planEntitlements`); this is where the figure\n * is written, beside the reasoning that sized it.\n */\ndeclare module '@aglyn/aglyn/plugin-manager/plugin-entitlement-keys' {\n interface PluginEntitlementQuotas {\n /**\n * Form submissions accepted per calendar month, per site (Forms & Lead\n * Capture). The org-wide band is this times `hostLimit`.\n *\n * Not the same dimension as `formsPerHost`, which counts the saved form\n * CATALOG rather than the replies: only this one is tiered, metered and\n * part of a charged price.\n */\n formSubmissionsPerMonth?: number\n }\n}\n\n/** The form submissions each plan includes a month, per site. */\nexport const FORM_SUBMISSIONS_PER_MONTH_BY_PLAN: Readonly<Record<OrgPlan, number>> = {\n // A `Form` node placed on a page needs no saved definition to collect, so\n // Free — which has no form catalog — still spends this band on its replies.\n free: 20,\n starter: 200,\n pro: 1000,\n business: 8000,\n scale: 25000,\n advanced: 40000,\n // FINITE, where every other capacity row on this tier is unbounded. The\n // band is multiplied by `hostLimit` to get the org-wide band, so at 100\n // hosts an unbounded figure was not merely large — it made this tier's cost\n // model unbounded, and an unbounded term reads as ZERO in any analysis that\n // scores an absent band as nothing. That is how it stayed invisible: the\n // biggest line item on the most expensive self-serve plan, contributing 0\n // to every total.\n //\n // Metering makes it safe to bound. `meteredInfraPassThrough` is true here,\n // so submissions past the band BILL at the pass-through rate rather than\n // being refused — a merchant's lead form does not stop working at 25,000.\n agency: 25000,\n enterprise: 50_000,\n}\n\n/** The forms plugin's plan figures, for the manifest generator. */\nexport function formsPlanEntitlements(): PluginPlanEntitlementsDeclaration {\n return {\n quotas: [\n {\n key: 'formSubmissionsPerMonth',\n label: 'Form subs / mo',\n byPlan: FORM_SUBMISSIONS_PER_MONTH_BY_PLAN,\n },\n ],\n }\n}\n"],"names":["FORM_SUBMISSIONS_PER_MONTH_BY_PLAN","free","starter","pro","business","scale","advanced","agency","enterprise","formsPlanEntitlements","quotas","key","label","byPlan"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,6EAA6E;AAC7E,wEAAwE;AAwBxE,+DAA+D,GAC/D,OAAO,MAAMA,qCAAwE;IACnF,0EAA0E;IAC1E,4EAA4E;IAC5EC,MAAM;IACNC,SAAS;IACTC,KAAK;IACLC,UAAU;IACVC,OAAO;IACPC,UAAU;IACV,wEAAwE;IACxE,wEAAwE;IACxE,4EAA4E;IAC5E,4EAA4E;IAC5E,yEAAyE;IACzE,0EAA0E;IAC1E,kBAAkB;IAClB,EAAE;IACF,2EAA2E;IAC3E,yEAAyE;IACzE,0EAA0E;IAC1EC,QAAQ;IACRC,YAAY;AACd,EAAC;AAED,iEAAiE,GACjE,OAAO,SAASC;IACd,OAAO;QACLC,QAAQ;YACN;gBACEC,KAAK;gBACLC,OAAO;gBACPC,QAAQb;YACV;SACD;IACH;AACF"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/forms/src/lib/plan-entitlements.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Types only: the manifest generator loads this module on its own to compile\n// the plan figures, so nothing here may reach a core module at runtime.\nimport type { OrgPlan } from '@aglyn/aglyn/foundation/definitions/org-billing.types'\nimport type { PluginPlanEntitlementsDeclaration } from '@aglyn/aglyn/plugin-manager/plugin-plan-entitlements'\n\n/**\n * THE FORMS PLUGIN'S PLAN BAND (AGL-3080): the form submissions each plan\n * includes a month, per site. Compiled into core's `PLAN_ENTITLEMENTS` by the\n * manifest generator (`register.planEntitlements`); this is where the figure\n * is written, beside the reasoning that sized it.\n */\ndeclare module '@aglyn/aglyn/plugin-manager/plugin-entitlement-keys' {\n interface PluginEntitlementQuotas {\n /**\n * Form submissions accepted per calendar month, per site (Forms & Lead\n * Capture). The org-wide band is this times `hostLimit`.\n *\n * Not the same dimension as `formsPerHost`, which counts the saved form\n * CATALOG rather than the replies: only this one is tiered, metered and\n * part of a charged price.\n */\n formSubmissionsPerMonth?: number\n }\n}\n\n/** The form submissions each plan includes a month, per site. */\nexport const FORM_SUBMISSIONS_PER_MONTH_BY_PLAN: Readonly<Record<OrgPlan, number>> = {\n // A `Form` node placed on a page needs no saved definition to collect, so\n // Free — which has no form catalog — still spends this band on its replies.\n free: 20,\n starter: 200,\n pro: 1000,\n business: 5000,\n scale: 10000,\n advanced: 10000,\n // FINITE, where every other capacity row on this tier is unbounded. The\n // band is multiplied by `hostLimit` to get the org-wide band, so at 100\n // hosts an unbounded figure was not merely large — it made this tier's cost\n // model unbounded, and an unbounded term reads as ZERO in any analysis that\n // scores an absent band as nothing. That is how it stayed invisible: the\n // biggest line item on the most expensive self-serve plan, contributing 0\n // to every total.\n //\n // Metering makes it safe to bound. `meteredInfraPassThrough` is true here,\n // so submissions past the band BILL at the pass-through rate rather than\n // being refused — a merchant's lead form does not stop working at the band.\n agency: 10000,\n enterprise: 20_000,\n}\n\n/** The forms plugin's plan figures, for the manifest generator. */\nexport function formsPlanEntitlements(): PluginPlanEntitlementsDeclaration {\n return {\n quotas: [\n {\n key: 'formSubmissionsPerMonth',\n label: 'Form subs / mo',\n byPlan: FORM_SUBMISSIONS_PER_MONTH_BY_PLAN,\n },\n ],\n }\n}\n"],"names":["FORM_SUBMISSIONS_PER_MONTH_BY_PLAN","free","starter","pro","business","scale","advanced","agency","enterprise","formsPlanEntitlements","quotas","key","label","byPlan"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,6EAA6E;AAC7E,wEAAwE;AAwBxE,+DAA+D,GAC/D,OAAO,MAAMA,qCAAwE;IACnF,0EAA0E;IAC1E,4EAA4E;IAC5EC,MAAM;IACNC,SAAS;IACTC,KAAK;IACLC,UAAU;IACVC,OAAO;IACPC,UAAU;IACV,wEAAwE;IACxE,wEAAwE;IACxE,4EAA4E;IAC5E,4EAA4E;IAC5E,yEAAyE;IACzE,0EAA0E;IAC1E,kBAAkB;IAClB,EAAE;IACF,2EAA2E;IAC3E,yEAAyE;IACzE,4EAA4E;IAC5EC,QAAQ;IACRC,YAAY;AACd,EAAC;AAED,iEAAiE,GACjE,OAAO,SAASC;IACd,OAAO;QACLC,QAAQ;YACN;gBACEC,KAAK;gBACLC,OAAO;gBACPC,QAAQb;YACV;SACD;IACH;AACF"}
package/src/lib/plugin.js CHANGED
@@ -16,11 +16,18 @@
16
16
  */ import * as Aglyn from "@aglyn/aglyn";
17
17
  import { mdiEmailFastOutline } from "@aglyn/shared-data-mdi";
18
18
  import { lazy } from "react";
19
+ import { registerPluginTransferResourceUi } from "@aglyn/aglyn/plugin-manager/plugin-transfer-resources";
19
20
  import { registerPluginZone } from "@aglyn/aglyn/plugin-manager/plugin-zones";
20
21
  import { FORM_CONTACT_FIELDS_ZONE, FORM_SUBMISSIONS_ZONE, HOST_FORMS_ZONE } from "./components/form-zones.js";
21
22
  import { BUNDLE_ID } from "./constants/bundle-common.js";
23
+ import { FORM_PACKAGE_KIND } from "./constants/site-package.js";
24
+ import { registerFormSubmissionCounts } from "./model/form-submission-counts.js";
25
+ import { registerFormSubmissionList } from "./model/form-submission-list.js";
26
+ import { registerFormRecordList } from "./model/forms-record-lists.js";
22
27
  import { registerFormsRecordRoutes } from "./model/forms-record-routes.js";
28
+ import { FORM_SUBMISSIONS_TRANSFER_KEY } from "./transfer/form-submissions-transfer-key.js";
23
29
  /** Code-split: the Forms console surface only loads when opened. */ const FormsConsolePage = lazy(()=>import("./components/forms-console-page.js"));
30
+ const SitePackageFormPreview = lazy(()=>import("./components/site-package-form-preview.component.js"));
24
31
  /**
25
32
  * Console half: the Forms catalog and one form's own surface, served by the
26
33
  * shell's generic plugin route.
@@ -29,6 +36,20 @@ import { registerFormsRecordRoutes } from "./model/forms-record-routes.js";
29
36
  * declared section. Safe to call at console app load — the page is lazy.
30
37
  */ export function registerFormsConsole() {
31
38
  registerFormsRecordRoutes();
39
+ registerFormRecordList();
40
+ registerFormSubmissionList();
41
+ // How many submissions a site received, for another plugin's figure.
42
+ registerFormSubmissionCounts();
43
+ // The export dialog's name for a site's submissions; export only, so the
44
+ // wizard never offers it.
45
+ registerPluginTransferResourceUi(FORM_SUBMISSIONS_TRANSFER_KEY, {
46
+ label: 'Form submissions',
47
+ icon: {
48
+ path: mdiEmailFastOutline.path
49
+ }
50
+ }, {
51
+ pluginId: BUNDLE_ID
52
+ });
32
53
  registerPluginZone({
33
54
  zone: FORM_SUBMISSIONS_ZONE,
34
55
  label: 'One form’s submissions',
@@ -59,6 +80,19 @@ import { registerFormsRecordRoutes } from "./model/forms-record-routes.js";
59
80
  Aglyn.registerConsoleExtension({
60
81
  pluginId: BUNDLE_ID,
61
82
  displayName: 'Forms',
83
+ widgets: [
84
+ {
85
+ // A form in a site package import, each side drawn by the preview
86
+ // the form's own page shows (AGL-3545).
87
+ slot: Aglyn.CONSOLE_WIDGET_SLOTS.sitePackageItemPreview,
88
+ widgetId: 'forms-site-package-preview',
89
+ title: 'Form preview',
90
+ itemKinds: [
91
+ FORM_PACKAGE_KIND
92
+ ],
93
+ Component: SitePackageFormPreview
94
+ }
95
+ ],
62
96
  navItems: [
63
97
  {
64
98
  label: 'Forms',
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/plugins/forms/src/lib/plugin.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { mdiEmailFastOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { registerPluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport {\n FORM_CONTACT_FIELDS_ZONE,\n FORM_SUBMISSIONS_ZONE,\n HOST_FORMS_ZONE,\n} from './components/form-zones'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { registerFormsRecordRoutes } from './model/forms-record-routes'\n\n/** Code-split: the Forms console surface only loads when opened. */\nconst FormsConsolePage = lazy(() => import('./components/forms-console-page'))\n\n/**\n * Console half: the Forms catalog and one form's own surface, served by the\n * shell's generic plugin route.\n *\n * `ownsSubtree` because a form's detail URL names a document id rather than a\n * declared section. Safe to call at console app load — the page is lazy.\n */\nexport function registerFormsConsole(): void {\n registerFormsRecordRoutes()\n registerPluginZone(\n {\n zone: FORM_SUBMISSIONS_ZONE,\n label: 'One form’s submissions',\n surface: 'console',\n description:\n 'On one form’s page, behind the reader’s ask. A widget here reads the submissions to that form alone; it is handed the site and the form and nothing else.',\n },\n // Named, because a spec calls this registrar without the loader.\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: FORM_CONTACT_FIELDS_ZONE,\n label: 'Where a form’s fields save on the person',\n surface: 'console',\n description:\n 'On one form’s page, beside routing and consent. A widget here decides which of the person’s fields each form field saves to and hands the new declaration to `saveFields`; the page writes the form document, and the widget writes nothing itself.',\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: HOST_FORMS_ZONE,\n label: 'A site’s Forms page',\n surface: 'console',\n // One item in the page's row of actions, beside Create Form.\n layout: 'bare',\n description:\n 'On a site’s Forms page, beside Create Form: another way to start a form. A widget here is handed the site and its org and writes nothing through the page.',\n },\n { pluginId: BUNDLE_ID },\n )\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'Forms',\n navItems: [\n {\n label: 'Forms',\n href: '/forms',\n // The tab id the console has always keyed this surface's active state\n // on. It carries no release flag: forms is on for every workspace, and\n // a site that switches it off loses this tab with the rest.\n navTabId: 'nav-tab-forms',\n icon: { path: mdiEmailFastOutline.path },\n ownsSubtree: true,\n header: {\n title: 'Forms',\n icon: { path: mdiEmailFastOutline.path },\n docsTopic: 'forms',\n },\n Component: FormsConsolePage,\n },\n ],\n })\n}\n\nexport * from './site'\n"],"names":["Aglyn","mdiEmailFastOutline","lazy","registerPluginZone","FORM_CONTACT_FIELDS_ZONE","FORM_SUBMISSIONS_ZONE","HOST_FORMS_ZONE","BUNDLE_ID","registerFormsRecordRoutes","FormsConsolePage","registerFormsConsole","zone","label","surface","description","pluginId","layout","registerConsoleExtension","displayName","navItems","href","navTabId","icon","path","ownsSubtree","header","title","docsTopic","Component"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,eAAc;AACrC,SAASC,mBAAmB,QAAQ,yBAAwB;AAC5D,SAASC,IAAI,QAAQ,QAAO;AAC5B,SAASC,kBAAkB,QAAQ,2CAA0C;AAC7E,SACEC,wBAAwB,EACxBC,qBAAqB,EACrBC,eAAe,QACV,6BAAyB;AAChC,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SAASC,yBAAyB,QAAQ,iCAA6B;AAEvE,kEAAkE,GAClE,MAAMC,mBAAmBP,KAAK,IAAM,MAAM,CAAC;AAE3C;;;;;;CAMC,GACD,OAAO,SAASQ;IACdF;IACAL,mBACE;QACEQ,MAAMN;QACNO,OAAO;QACPC,SAAS;QACTC,aACE;IACJ,GACA,iEAAiE;IACjE;QAAEC,UAAUR;IAAU;IAExBJ,mBACE;QACEQ,MAAMP;QACNQ,OAAO;QACPC,SAAS;QACTC,aACE;IACJ,GACA;QAAEC,UAAUR;IAAU;IAExBJ,mBACE;QACEQ,MAAML;QACNM,OAAO;QACPC,SAAS;QACT,6DAA6D;QAC7DG,QAAQ;QACRF,aACE;IACJ,GACA;QAAEC,UAAUR;IAAU;IAExBP,MAAMiB,wBAAwB,CAAC;QAC7BF,UAAUR;QACVW,aAAa;QACbC,UAAU;YACR;gBACEP,OAAO;gBACPQ,MAAM;gBACN,sEAAsE;gBACtE,uEAAuE;gBACvE,4DAA4D;gBAC5DC,UAAU;gBACVC,MAAM;oBAAEC,MAAMtB,oBAAoBsB,IAAI;gBAAC;gBACvCC,aAAa;gBACbC,QAAQ;oBACNC,OAAO;oBACPJ,MAAM;wBAAEC,MAAMtB,oBAAoBsB,IAAI;oBAAC;oBACvCI,WAAW;gBACb;gBACAC,WAAWnB;YACb;SACD;IACH;AACF;AAEA,cAAc,YAAQ"}
1
+ {"version":3,"sources":["../../../../../../libs/plugins/forms/src/lib/plugin.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn'\nimport { mdiEmailFastOutline } from '@aglyn/shared-data-mdi'\nimport { lazy } from 'react'\nimport { registerPluginTransferResourceUi } from '@aglyn/aglyn/plugin-manager/plugin-transfer-resources'\nimport { registerPluginZone } from '@aglyn/aglyn/plugin-manager/plugin-zones'\nimport {\n FORM_CONTACT_FIELDS_ZONE,\n FORM_SUBMISSIONS_ZONE,\n HOST_FORMS_ZONE,\n} from './components/form-zones'\nimport { BUNDLE_ID } from './constants/bundle-common'\nimport { FORM_PACKAGE_KIND } from './constants/site-package'\nimport { registerFormSubmissionCounts } from './model/form-submission-counts'\nimport { registerFormSubmissionList } from './model/form-submission-list'\nimport { registerFormRecordList } from './model/forms-record-lists'\nimport { registerFormsRecordRoutes } from './model/forms-record-routes'\nimport { FORM_SUBMISSIONS_TRANSFER_KEY } from './transfer/form-submissions-transfer-key'\n\n/** Code-split: the Forms console surface only loads when opened. */\nconst FormsConsolePage = lazy(() => import('./components/forms-console-page'))\nconst SitePackageFormPreview = lazy(\n () => import('./components/site-package-form-preview.component'),\n)\n\n/**\n * Console half: the Forms catalog and one form's own surface, served by the\n * shell's generic plugin route.\n *\n * `ownsSubtree` because a form's detail URL names a document id rather than a\n * declared section. Safe to call at console app load — the page is lazy.\n */\nexport function registerFormsConsole(): void {\n registerFormsRecordRoutes()\n registerFormRecordList()\n registerFormSubmissionList()\n // How many submissions a site received, for another plugin's figure.\n registerFormSubmissionCounts()\n // The export dialog's name for a site's submissions; export only, so the\n // wizard never offers it.\n registerPluginTransferResourceUi(\n FORM_SUBMISSIONS_TRANSFER_KEY,\n { label: 'Form submissions', icon: { path: mdiEmailFastOutline.path } },\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: FORM_SUBMISSIONS_ZONE,\n label: 'One form’s submissions',\n surface: 'console',\n description:\n 'On one form’s page, behind the reader’s ask. A widget here reads the submissions to that form alone; it is handed the site and the form and nothing else.',\n },\n // Named, because a spec calls this registrar without the loader.\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: FORM_CONTACT_FIELDS_ZONE,\n label: 'Where a form’s fields save on the person',\n surface: 'console',\n description:\n 'On one form’s page, beside routing and consent. A widget here decides which of the person’s fields each form field saves to and hands the new declaration to `saveFields`; the page writes the form document, and the widget writes nothing itself.',\n },\n { pluginId: BUNDLE_ID },\n )\n registerPluginZone(\n {\n zone: HOST_FORMS_ZONE,\n label: 'A site’s Forms page',\n surface: 'console',\n // One item in the page's row of actions, beside Create Form.\n layout: 'bare',\n description:\n 'On a site’s Forms page, beside Create Form: another way to start a form. A widget here is handed the site and its org and writes nothing through the page.',\n },\n { pluginId: BUNDLE_ID },\n )\n Aglyn.registerConsoleExtension({\n pluginId: BUNDLE_ID,\n displayName: 'Forms',\n widgets: [\n {\n // A form in a site package import, each side drawn by the preview\n // the form's own page shows (AGL-3545).\n slot: Aglyn.CONSOLE_WIDGET_SLOTS.sitePackageItemPreview,\n widgetId: 'forms-site-package-preview',\n title: 'Form preview',\n itemKinds: [FORM_PACKAGE_KIND],\n Component: SitePackageFormPreview,\n },\n ],\n navItems: [\n {\n label: 'Forms',\n href: '/forms',\n // The tab id the console has always keyed this surface's active state\n // on. It carries no release flag: forms is on for every workspace, and\n // a site that switches it off loses this tab with the rest.\n navTabId: 'nav-tab-forms',\n icon: { path: mdiEmailFastOutline.path },\n ownsSubtree: true,\n header: {\n title: 'Forms',\n icon: { path: mdiEmailFastOutline.path },\n docsTopic: 'forms',\n },\n Component: FormsConsolePage,\n },\n ],\n })\n}\n\nexport * from './site'\n"],"names":["Aglyn","mdiEmailFastOutline","lazy","registerPluginTransferResourceUi","registerPluginZone","FORM_CONTACT_FIELDS_ZONE","FORM_SUBMISSIONS_ZONE","HOST_FORMS_ZONE","BUNDLE_ID","FORM_PACKAGE_KIND","registerFormSubmissionCounts","registerFormSubmissionList","registerFormRecordList","registerFormsRecordRoutes","FORM_SUBMISSIONS_TRANSFER_KEY","FormsConsolePage","SitePackageFormPreview","registerFormsConsole","label","icon","path","pluginId","zone","surface","description","layout","registerConsoleExtension","displayName","widgets","slot","CONSOLE_WIDGET_SLOTS","sitePackageItemPreview","widgetId","title","itemKinds","Component","navItems","href","navTabId","ownsSubtree","header","docsTopic"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,YAAYA,WAAW,eAAc;AACrC,SAASC,mBAAmB,QAAQ,yBAAwB;AAC5D,SAASC,IAAI,QAAQ,QAAO;AAC5B,SAASC,gCAAgC,QAAQ,wDAAuD;AACxG,SAASC,kBAAkB,QAAQ,2CAA0C;AAC7E,SACEC,wBAAwB,EACxBC,qBAAqB,EACrBC,eAAe,QACV,6BAAyB;AAChC,SAASC,SAAS,QAAQ,+BAA2B;AACrD,SAASC,iBAAiB,QAAQ,8BAA0B;AAC5D,SAASC,4BAA4B,QAAQ,oCAAgC;AAC7E,SAASC,0BAA0B,QAAQ,kCAA8B;AACzE,SAASC,sBAAsB,QAAQ,gCAA4B;AACnE,SAASC,yBAAyB,QAAQ,iCAA6B;AACvE,SAASC,6BAA6B,QAAQ,8CAA0C;AAExF,kEAAkE,GAClE,MAAMC,mBAAmBb,KAAK,IAAM,MAAM,CAAC;AAC3C,MAAMc,yBAAyBd,KAC7B,IAAM,MAAM,CAAC;AAGf;;;;;;CAMC,GACD,OAAO,SAASe;IACdJ;IACAD;IACAD;IACA,qEAAqE;IACrED;IACA,yEAAyE;IACzE,0BAA0B;IAC1BP,iCACEW,+BACA;QAAEI,OAAO;QAAoBC,MAAM;YAAEC,MAAMnB,oBAAoBmB,IAAI;QAAC;IAAE,GACtE;QAAEC,UAAUb;IAAU;IAExBJ,mBACE;QACEkB,MAAMhB;QACNY,OAAO;QACPK,SAAS;QACTC,aACE;IACJ,GACA,iEAAiE;IACjE;QAAEH,UAAUb;IAAU;IAExBJ,mBACE;QACEkB,MAAMjB;QACNa,OAAO;QACPK,SAAS;QACTC,aACE;IACJ,GACA;QAAEH,UAAUb;IAAU;IAExBJ,mBACE;QACEkB,MAAMf;QACNW,OAAO;QACPK,SAAS;QACT,6DAA6D;QAC7DE,QAAQ;QACRD,aACE;IACJ,GACA;QAAEH,UAAUb;IAAU;IAExBR,MAAM0B,wBAAwB,CAAC;QAC7BL,UAAUb;QACVmB,aAAa;QACbC,SAAS;YACP;gBACE,kEAAkE;gBAClE,wCAAwC;gBACxCC,MAAM7B,MAAM8B,oBAAoB,CAACC,sBAAsB;gBACvDC,UAAU;gBACVC,OAAO;gBACPC,WAAW;oBAACzB;iBAAkB;gBAC9B0B,WAAWnB;YACb;SACD;QACDoB,UAAU;YACR;gBACElB,OAAO;gBACPmB,MAAM;gBACN,sEAAsE;gBACtE,uEAAuE;gBACvE,4DAA4D;gBAC5DC,UAAU;gBACVnB,MAAM;oBAAEC,MAAMnB,oBAAoBmB,IAAI;gBAAC;gBACvCmB,aAAa;gBACbC,QAAQ;oBACNP,OAAO;oBACPd,MAAM;wBAAEC,MAAMnB,oBAAoBmB,IAAI;oBAAC;oBACvCqB,WAAW;gBACb;gBACAN,WAAWpB;YACb;SACD;IACH;AACF;AAEA,cAAc,YAAQ"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type ApiV1Context } from '@aglyn/tenant-data-admin/server/api-v1-kit';
18
+ /**
19
+ * `/v1/sites/{siteId}/form-submissions[/{submissionId}]` (AGL-2127).
20
+ *
21
+ * The list was the whole surface, and that shaped every integration written
22
+ * against it badly. A lead sync polls, pushes new rows into a CRM, and then
23
+ * has nowhere to record that it did — so it either re-pushes the same lead
24
+ * next poll, or keeps its own high-water mark of ids against a list that
25
+ * `conventions.md` publishes as ordered by DOCUMENT ID, not by time. The
26
+ * `read` flag the console's inbox toggles on the very same document is the
27
+ * state the integration needed and could not write.
28
+ *
29
+ * `read` is the ONLY writable field. A submission is what a visitor typed,
30
+ * and an API that let an integration quietly rewrite it would make the
31
+ * inbox's contents unattributable — so anything else in the body is a
32
+ * `validation_failed` naming the offending key rather than a silent drop.
33
+ */
34
+ export declare function handleFormSubmissions(request: Request, ctx: ApiV1Context, segments: string[], url: URL): Promise<Response>;
@@ -0,0 +1,273 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { ApiErrors, apiJson, listResponse } from "@aglyn/tenant-data-admin";
18
+ import { claimWrite, paginate, readJsonBody, requireScope, serialize } from "@aglyn/tenant-data-admin/server/api-v1-kit";
19
+ import { runPluginEventHandlers } from "@aglyn/aglyn/plugin-manager/plugin-events";
20
+ /**
21
+ * A site's form submissions on the customer REST API,
22
+ * `/v1/sites/{siteId}/form-submissions/…` (AGL-2127, AGL-3080). Registered
23
+ * from this plugin's console server declarations; the console's `/v1`
24
+ * router owns the pipeline in front — the key, the plan's API access, the
25
+ * request quota, the rate limit — and refuses a site the key's organization
26
+ * does not own before it hands a request here.
27
+ */ function formSubmissionView(doc) {
28
+ var _doc_get, _doc_get1, _doc_get2, _doc_get3, _doc_get4, _serialize;
29
+ return {
30
+ id: doc.id,
31
+ object: 'form_submission',
32
+ // The form ENTITY this was sent to, `null` for a row written before the
33
+ // form was adopted. `form` below stays the caption — an integration
34
+ // grouping by it is grouping by a display string that a rename splits,
35
+ // which is the whole reason this field exists.
36
+ form_id: (_doc_get = doc.get('formId')) != null ? _doc_get : null,
37
+ form: (_doc_get1 = doc.get('formName')) != null ? _doc_get1 : null,
38
+ path: (_doc_get2 = doc.get('path')) != null ? _doc_get2 : null,
39
+ fields: (_doc_get3 = doc.get('fields')) != null ? _doc_get3 : {},
40
+ read: Boolean(doc.get('read')),
41
+ // Where the platform already sent this row. Omitting it meant an
42
+ // integration syncing submissions into a CRM could not tell that a record
43
+ // had already been written to a dataset — the one fact that stops it
44
+ // duplicating work the platform had done.
45
+ routing: (_doc_get4 = doc.get('routing')) != null ? _doc_get4 : null,
46
+ created: (_serialize = serialize(doc.get('createdAt'))) != null ? _serialize : null
47
+ };
48
+ }
49
+ /**
50
+ * `/v1/sites/{siteId}/form-submissions[/{submissionId}]` (AGL-2127).
51
+ *
52
+ * The list was the whole surface, and that shaped every integration written
53
+ * against it badly. A lead sync polls, pushes new rows into a CRM, and then
54
+ * has nowhere to record that it did — so it either re-pushes the same lead
55
+ * next poll, or keeps its own high-water mark of ids against a list that
56
+ * `conventions.md` publishes as ordered by DOCUMENT ID, not by time. The
57
+ * `read` flag the console's inbox toggles on the very same document is the
58
+ * state the integration needed and could not write.
59
+ *
60
+ * `read` is the ONLY writable field. A submission is what a visitor typed,
61
+ * and an API that let an integration quietly rewrite it would make the
62
+ * inbox's contents unattributable — so anything else in the body is a
63
+ * `validation_failed` naming the offending key rather than a silent drop.
64
+ */ export async function handleFormSubmissions(request, ctx, segments, url) {
65
+ const [, hostId, , submissionId] = segments;
66
+ const collection = ctx.firestore.collection('hosts').doc(hostId).collection('formSubmissions');
67
+ if (!submissionId) {
68
+ const denied = requireScope(ctx, 'forms:read');
69
+ if (denied) return denied;
70
+ if (request.method !== 'GET') {
71
+ return ApiErrors.methodNotAllowed({
72
+ headers: _extends({}, ctx.headers, {
73
+ Allow: 'GET'
74
+ })
75
+ });
76
+ }
77
+ // An EMPTY value means the filter is absent, matching `?email=` and
78
+ // `?tag=` on contacts and `conventions.md`'s single rule for all three.
79
+ // A client serializing an unset form field sends `?read=`, and refusing
80
+ // that while `?email=` accepts it would be an inconsistency an integrator
81
+ // discovers one filter at a time.
82
+ const rawRead = url.searchParams.get('read') || null;
83
+ if (rawRead !== null && rawRead !== 'true' && rawRead !== 'false') {
84
+ return ApiErrors.badRequest({
85
+ message: 'Form submission filter failed validation',
86
+ code: 'validation_failed',
87
+ fields: {
88
+ read: 'Must be true or false'
89
+ },
90
+ headers: ctx.headers
91
+ });
92
+ }
93
+ const read = rawRead === null ? null : rawRead === 'true';
94
+ let query = collection;
95
+ const form = url.searchParams.get('form');
96
+ const formId = url.searchParams.get('formId');
97
+ // `formId` is the id-first filter and wins when both are sent: it is the
98
+ // one that survives a rename. `?form=` is NOT removed and is not
99
+ // deprecated here — it filters on the caption every submission still
100
+ // carries, which is the only thing a form that has not been adopted yet
101
+ // can be filtered by. The same posture the legacy `?collection=` content
102
+ // parameters take.
103
+ if (formId) query = query.where('formId', '==', formId);
104
+ else if (form) query = query.where('formName', '==', form);
105
+ // `read` goes to FIRESTORE only when it is the sole filter, and is
106
+ // applied after the read when it joins `form` (AGL-2460).
107
+ //
108
+ // Two equality clauses plus the `orderBy(FieldPath.documentId())` every
109
+ // list here applies is a three-clause query, and Firestore serves that
110
+ // only from a composite index. Shipping one to serve a filter
111
+ // COMBINATION is a migration with a backfill, not a feature — and the
112
+ // failure mode while it builds is this route's documented realistic 500
113
+ // (see the route's `safeDispatch` docblock). Narrowing on `formName` and
114
+ // dropping the rest in memory keeps the pre-existing `?form=` query
115
+ // byte-for-byte what it already was, which is the property worth more
116
+ // than one saved round trip.
117
+ //
118
+ // `read=false` IS exact against Firestore, unlike `?channel=online` on
119
+ // orders. That filter is applied after the read because older orders
120
+ // predate the `channel` field and a `where` would silently drop them.
121
+ // The equivalent question was checked here rather than assumed: the
122
+ // ONLY writer of this collection is the tenant's form-submit route, and
123
+ // it has stamped `read: false` on every row since the feature's first
124
+ // commit (AGL-76/77, `fc149e538`). There is no fieldless generation to
125
+ // drop, so the cheap query is also the correct one.
126
+ // Either form filter already spent this list's one equality clause, so
127
+ // `read` is applied after the read exactly as it is for `?form=` — a
128
+ // second `where` plus the document-id ordering is a three-clause query
129
+ // and needs its own composite index per combination. The `formId ASC,
130
+ // createdAt DESC` index this work ships serves the CONSOLE's ordered
131
+ // list; `/v1` lists are ordered by document id and are a different query.
132
+ const narrowedByForm = Boolean(formId || form);
133
+ if (read !== null && !narrowedByForm) {
134
+ query = query.where('read', '==', read);
135
+ }
136
+ const { docs, nextCursor } = await paginate(query, url);
137
+ const matched = read !== null && narrowedByForm ? docs.filter((doc)=>Boolean(doc.get('read')) === read) : docs;
138
+ return listResponse(matched.map(formSubmissionView), nextCursor, ctx.headers);
139
+ }
140
+ const submissionRef = collection.doc(submissionId);
141
+ if (request.method === 'GET') {
142
+ const denied = requireScope(ctx, 'forms:read');
143
+ if (denied) return denied;
144
+ const snap = await submissionRef.get();
145
+ if (!snap.exists) {
146
+ return ApiErrors.notFound({
147
+ message: 'No such form submission',
148
+ headers: ctx.headers
149
+ });
150
+ }
151
+ return apiJson(formSubmissionView(snap), {
152
+ headers: ctx.headers
153
+ });
154
+ }
155
+ if (request.method === 'PATCH') {
156
+ const denied = requireScope(ctx, 'forms:write');
157
+ if (denied) return denied;
158
+ return updateFormSubmission(request, ctx, submissionRef);
159
+ }
160
+ if (request.method === 'DELETE') {
161
+ const denied = requireScope(ctx, 'forms:write');
162
+ if (denied) return denied;
163
+ return deleteFormSubmission(request, ctx, hostId, submissionRef);
164
+ }
165
+ return ApiErrors.methodNotAllowed({
166
+ headers: _extends({}, ctx.headers, {
167
+ Allow: 'GET, PATCH, DELETE'
168
+ })
169
+ });
170
+ }
171
+ /**
172
+ * Mark one submission read or unread. No `Idempotency-Key`: the same body
173
+ * twice lands the same state AND returns the same `200`, which is the test
174
+ * `updateRecord` and `updateDataset` are held to.
175
+ */ async function updateFormSubmission(request, ctx, submissionRef) {
176
+ const body = await readJsonBody(request);
177
+ const unknown = Object.keys(body).filter((key)=>key !== 'read');
178
+ if (unknown.length > 0) {
179
+ // Named, not dropped. `values` on a record drops unknown fields because a
180
+ // dataset model defines what exists; a submission has no model, so a
181
+ // silent drop here would read as "we stored your correction" when nothing
182
+ // was stored, and the visitor's answers are exactly the thing that must
183
+ // not be quietly editable.
184
+ return ApiErrors.badRequest({
185
+ message: 'Only `read` can be changed on a form submission',
186
+ code: 'validation_failed',
187
+ fields: Object.fromEntries(unknown.map((key)=>[
188
+ key,
189
+ 'Not writable on a form submission'
190
+ ])),
191
+ headers: ctx.headers
192
+ });
193
+ }
194
+ if (typeof body.read !== 'boolean') {
195
+ return ApiErrors.badRequest({
196
+ message: 'Form submission failed validation',
197
+ code: 'validation_failed',
198
+ fields: {
199
+ read: 'Must be true or false'
200
+ },
201
+ headers: ctx.headers
202
+ });
203
+ }
204
+ const snap = await submissionRef.get();
205
+ if (!snap.exists) {
206
+ return ApiErrors.notFound({
207
+ message: 'No such form submission',
208
+ headers: ctx.headers
209
+ });
210
+ }
211
+ await submissionRef.update({
212
+ read: body.read
213
+ });
214
+ return apiJson(formSubmissionView(await submissionRef.get()), {
215
+ headers: ctx.headers
216
+ });
217
+ }
218
+ /**
219
+ * Delete one submission. Accepts an `Idempotency-Key` for the reason
220
+ * `deleteRecord` does: a purge that runs after an export is the operation
221
+ * most likely to be retried on a timer, and without a key the retry cannot
222
+ * tell "already gone" from "wrong id".
223
+ */ async function deleteFormSubmission(request, ctx, hostId, submissionRef) {
224
+ const claimed = await claimWrite(ctx, hostId, request.headers.get('Idempotency-Key'), 'form-submission-deletes');
225
+ if ('replay' in claimed) return claimed.replay;
226
+ const { claim } = claimed;
227
+ try {
228
+ var _snap_data;
229
+ const snap = await submissionRef.get();
230
+ if (!snap.exists) {
231
+ await claim.release();
232
+ return ApiErrors.notFound({
233
+ message: 'No such form submission',
234
+ headers: ctx.headers
235
+ });
236
+ }
237
+ const removed = {
238
+ id: snap.id,
239
+ data: (_snap_data = snap.data()) != null ? _snap_data : {}
240
+ };
241
+ await submissionRef.delete();
242
+ /*
243
+ * Whatever counted this row when it arrived (a form's counters, AGL-3330)
244
+ * cannot see a delete, so the plugins are told what left. Best effort,
245
+ * isolated per plugin by the seam: the delete is the request, and a
246
+ * recount that fails leaves its figures for the next one rather than
247
+ * failing a purge that already happened.
248
+ */ await runPluginEventHandlers('host.records.removed', {
249
+ orgId: ctx.orgId,
250
+ hostIds: [
251
+ hostId
252
+ ],
253
+ collection: 'formSubmissions',
254
+ records: [
255
+ removed
256
+ ]
257
+ }).catch((error)=>console.error('records-removed event after an API delete failed', error));
258
+ const view = {
259
+ id: submissionRef.id,
260
+ object: 'form_submission',
261
+ deleted: true
262
+ };
263
+ await claim.record(200, view);
264
+ return apiJson(view, {
265
+ headers: ctx.headers
266
+ });
267
+ } catch (error) {
268
+ await claim.release();
269
+ throw error;
270
+ }
271
+ }
272
+
273
+ //# sourceMappingURL=form-submissions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../../../../../libs/plugins/forms/src/lib/server/api-v1/form-submissions.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n ApiErrors,\n apiJson,\n listResponse,\n} from '@aglyn/tenant-data-admin'\nimport {\n type ApiV1Context,\n claimWrite,\n paginate,\n readJsonBody,\n requireScope,\n serialize,\n} from '@aglyn/tenant-data-admin/server/api-v1-kit'\nimport { runPluginEventHandlers } from '@aglyn/aglyn/plugin-manager/plugin-events'\n\n/**\n * A site's form submissions on the customer REST API,\n * `/v1/sites/{siteId}/form-submissions/…` (AGL-2127, AGL-3080). Registered\n * from this plugin's console server declarations; the console's `/v1`\n * router owns the pipeline in front — the key, the plan's API access, the\n * request quota, the rate limit — and refuses a site the key's organization\n * does not own before it hands a request here.\n */\n\nfunction formSubmissionView(doc: FirebaseFirestore.DocumentSnapshot) {\n return {\n id: doc.id,\n object: 'form_submission',\n // The form ENTITY this was sent to, `null` for a row written before the\n // form was adopted. `form` below stays the caption — an integration\n // grouping by it is grouping by a display string that a rename splits,\n // which is the whole reason this field exists.\n form_id: doc.get('formId') ?? null,\n form: doc.get('formName') ?? null,\n path: doc.get('path') ?? null,\n fields: doc.get('fields') ?? {},\n read: Boolean(doc.get('read')),\n // Where the platform already sent this row. Omitting it meant an\n // integration syncing submissions into a CRM could not tell that a record\n // had already been written to a dataset — the one fact that stops it\n // duplicating work the platform had done.\n routing: doc.get('routing') ?? null,\n created: serialize(doc.get('createdAt')) ?? null,\n }\n}\n\n/**\n * `/v1/sites/{siteId}/form-submissions[/{submissionId}]` (AGL-2127).\n *\n * The list was the whole surface, and that shaped every integration written\n * against it badly. A lead sync polls, pushes new rows into a CRM, and then\n * has nowhere to record that it did — so it either re-pushes the same lead\n * next poll, or keeps its own high-water mark of ids against a list that\n * `conventions.md` publishes as ordered by DOCUMENT ID, not by time. The\n * `read` flag the console's inbox toggles on the very same document is the\n * state the integration needed and could not write.\n *\n * `read` is the ONLY writable field. A submission is what a visitor typed,\n * and an API that let an integration quietly rewrite it would make the\n * inbox's contents unattributable — so anything else in the body is a\n * `validation_failed` naming the offending key rather than a silent drop.\n */\nexport async function handleFormSubmissions(\n request: Request,\n ctx: ApiV1Context,\n segments: string[],\n url: URL,\n): Promise<Response> {\n const [, hostId, , submissionId] = segments\n const collection = ctx.firestore\n .collection('hosts')\n .doc(hostId)\n .collection('formSubmissions')\n\n if (!submissionId) {\n const denied = requireScope(ctx, 'forms:read')\n if (denied) return denied\n if (request.method !== 'GET') {\n return ApiErrors.methodNotAllowed({\n headers: { ...ctx.headers, Allow: 'GET' },\n })\n }\n // An EMPTY value means the filter is absent, matching `?email=` and\n // `?tag=` on contacts and `conventions.md`'s single rule for all three.\n // A client serializing an unset form field sends `?read=`, and refusing\n // that while `?email=` accepts it would be an inconsistency an integrator\n // discovers one filter at a time.\n const rawRead = url.searchParams.get('read') || null\n if (rawRead !== null && rawRead !== 'true' && rawRead !== 'false') {\n return ApiErrors.badRequest({\n message: 'Form submission filter failed validation',\n code: 'validation_failed',\n fields: { read: 'Must be true or false' },\n headers: ctx.headers,\n })\n }\n const read = rawRead === null ? null : rawRead === 'true'\n\n let query: FirebaseFirestore.Query = collection\n const form = url.searchParams.get('form')\n const formId = url.searchParams.get('formId')\n // `formId` is the id-first filter and wins when both are sent: it is the\n // one that survives a rename. `?form=` is NOT removed and is not\n // deprecated here — it filters on the caption every submission still\n // carries, which is the only thing a form that has not been adopted yet\n // can be filtered by. The same posture the legacy `?collection=` content\n // parameters take.\n if (formId) query = query.where('formId', '==', formId)\n else if (form) query = query.where('formName', '==', form)\n // `read` goes to FIRESTORE only when it is the sole filter, and is\n // applied after the read when it joins `form` (AGL-2460).\n //\n // Two equality clauses plus the `orderBy(FieldPath.documentId())` every\n // list here applies is a three-clause query, and Firestore serves that\n // only from a composite index. Shipping one to serve a filter\n // COMBINATION is a migration with a backfill, not a feature — and the\n // failure mode while it builds is this route's documented realistic 500\n // (see the route's `safeDispatch` docblock). Narrowing on `formName` and\n // dropping the rest in memory keeps the pre-existing `?form=` query\n // byte-for-byte what it already was, which is the property worth more\n // than one saved round trip.\n //\n // `read=false` IS exact against Firestore, unlike `?channel=online` on\n // orders. That filter is applied after the read because older orders\n // predate the `channel` field and a `where` would silently drop them.\n // The equivalent question was checked here rather than assumed: the\n // ONLY writer of this collection is the tenant's form-submit route, and\n // it has stamped `read: false` on every row since the feature's first\n // commit (AGL-76/77, `fc149e538`). There is no fieldless generation to\n // drop, so the cheap query is also the correct one.\n // Either form filter already spent this list's one equality clause, so\n // `read` is applied after the read exactly as it is for `?form=` — a\n // second `where` plus the document-id ordering is a three-clause query\n // and needs its own composite index per combination. The `formId ASC,\n // createdAt DESC` index this work ships serves the CONSOLE's ordered\n // list; `/v1` lists are ordered by document id and are a different query.\n const narrowedByForm = Boolean(formId || form)\n if (read !== null && !narrowedByForm) {\n query = query.where('read', '==', read)\n }\n const { docs, nextCursor } = await paginate(query, url)\n const matched =\n read !== null && narrowedByForm\n ? docs.filter((doc) => Boolean(doc.get('read')) === read)\n : docs\n return listResponse(matched.map(formSubmissionView), nextCursor, ctx.headers)\n }\n\n const submissionRef = collection.doc(submissionId)\n\n if (request.method === 'GET') {\n const denied = requireScope(ctx, 'forms:read')\n if (denied) return denied\n const snap = await submissionRef.get()\n if (!snap.exists) {\n return ApiErrors.notFound({\n message: 'No such form submission',\n headers: ctx.headers,\n })\n }\n return apiJson(formSubmissionView(snap), { headers: ctx.headers })\n }\n\n if (request.method === 'PATCH') {\n const denied = requireScope(ctx, 'forms:write')\n if (denied) return denied\n return updateFormSubmission(request, ctx, submissionRef)\n }\n\n if (request.method === 'DELETE') {\n const denied = requireScope(ctx, 'forms:write')\n if (denied) return denied\n return deleteFormSubmission(request, ctx, hostId, submissionRef)\n }\n\n return ApiErrors.methodNotAllowed({\n headers: { ...ctx.headers, Allow: 'GET, PATCH, DELETE' },\n })\n}\n\n/**\n * Mark one submission read or unread. No `Idempotency-Key`: the same body\n * twice lands the same state AND returns the same `200`, which is the test\n * `updateRecord` and `updateDataset` are held to.\n */\nasync function updateFormSubmission(\n request: Request,\n ctx: ApiV1Context,\n submissionRef: FirebaseFirestore.DocumentReference,\n): Promise<Response> {\n const body = await readJsonBody(request)\n const unknown = Object.keys(body).filter((key) => key !== 'read')\n if (unknown.length > 0) {\n // Named, not dropped. `values` on a record drops unknown fields because a\n // dataset model defines what exists; a submission has no model, so a\n // silent drop here would read as \"we stored your correction\" when nothing\n // was stored, and the visitor's answers are exactly the thing that must\n // not be quietly editable.\n return ApiErrors.badRequest({\n message: 'Only `read` can be changed on a form submission',\n code: 'validation_failed',\n fields: Object.fromEntries(\n unknown.map((key) => [key, 'Not writable on a form submission']),\n ),\n headers: ctx.headers,\n })\n }\n if (typeof body.read !== 'boolean') {\n return ApiErrors.badRequest({\n message: 'Form submission failed validation',\n code: 'validation_failed',\n fields: { read: 'Must be true or false' },\n headers: ctx.headers,\n })\n }\n\n const snap = await submissionRef.get()\n if (!snap.exists) {\n return ApiErrors.notFound({\n message: 'No such form submission',\n headers: ctx.headers,\n })\n }\n await submissionRef.update({ read: body.read })\n return apiJson(formSubmissionView(await submissionRef.get()), {\n headers: ctx.headers,\n })\n}\n\n/**\n * Delete one submission. Accepts an `Idempotency-Key` for the reason\n * `deleteRecord` does: a purge that runs after an export is the operation\n * most likely to be retried on a timer, and without a key the retry cannot\n * tell \"already gone\" from \"wrong id\".\n */\nasync function deleteFormSubmission(\n request: Request,\n ctx: ApiV1Context,\n hostId: string,\n submissionRef: FirebaseFirestore.DocumentReference,\n): Promise<Response> {\n const claimed = await claimWrite(\n ctx,\n hostId,\n request.headers.get('Idempotency-Key'),\n 'form-submission-deletes',\n )\n if ('replay' in claimed) return claimed.replay\n const { claim } = claimed\n\n try {\n const snap = await submissionRef.get()\n if (!snap.exists) {\n await claim.release()\n return ApiErrors.notFound({\n message: 'No such form submission',\n headers: ctx.headers,\n })\n }\n const removed = { id: snap.id, data: (snap.data() ?? {}) as Record<string, unknown> }\n await submissionRef.delete()\n /*\n * Whatever counted this row when it arrived (a form's counters, AGL-3330)\n * cannot see a delete, so the plugins are told what left. Best effort,\n * isolated per plugin by the seam: the delete is the request, and a\n * recount that fails leaves its figures for the next one rather than\n * failing a purge that already happened.\n */\n await runPluginEventHandlers('host.records.removed', {\n orgId: ctx.orgId,\n hostIds: [hostId],\n collection: 'formSubmissions',\n records: [removed],\n }).catch((error) => console.error('records-removed event after an API delete failed', error))\n const view = {\n id: submissionRef.id,\n object: 'form_submission',\n deleted: true,\n }\n await claim.record(200, view)\n return apiJson(view, { headers: ctx.headers })\n } catch (error) {\n await claim.release()\n throw error\n }\n}\n\n"],"names":["ApiErrors","apiJson","listResponse","claimWrite","paginate","readJsonBody","requireScope","serialize","runPluginEventHandlers","formSubmissionView","doc","id","object","form_id","get","form","path","fields","read","Boolean","routing","created","handleFormSubmissions","request","ctx","segments","url","hostId","submissionId","collection","firestore","denied","method","methodNotAllowed","headers","Allow","rawRead","searchParams","badRequest","message","code","query","formId","where","narrowedByForm","docs","nextCursor","matched","filter","map","submissionRef","snap","exists","notFound","updateFormSubmission","deleteFormSubmission","body","unknown","Object","keys","key","length","fromEntries","update","claimed","replay","claim","release","removed","data","delete","orgId","hostIds","records","catch","error","console","view","deleted","record"],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { type ApiV1ResourceDescription } from '@aglyn/tenant-data-admin/server/api-v1-description';
18
+ /**
19
+ * `/v1/sites/{siteId}/form-submissions` as the customer API's OpenAPI
20
+ * document describes it — what the console's builder turns into paths, a tag
21
+ * and schemas, and the MCP tools derive from. Transcribed unchanged from the
22
+ * builder's own list when the resource moved here (AGL-3080);
23
+ * `api-v1-openapi.spec.ts` holds the document to every endpoint the
24
+ * documentation promises.
25
+ */
26
+ export declare const FORM_SUBMISSIONS_API_V1_DESCRIPTION: ApiV1ResourceDescription;