mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-21 13:00:04 +08:00
feat(jotform): add Jotform integration (#6772)
* feat(jotform): add Jotform integration
Adds 43 tools covering forms, questions, submissions, reports, webhooks,
labels, and account operations, plus the block, icon, and generated docs.
Request shapes are pinned against the API's own curl samples and the
official SDKs: PUT /form/{id}/properties and PUT /form/{id}/questions each
take a named envelope while PUT /form and the bulk-submission PUT take
their payload bare, and submission answers accept both the nested object
and the documented {qid}_{subfield} shorthand.
Skips the deprecated folder endpoints in favor of labels, and leaves out
endpoints whose response shape the docs do not publish.
* fix(jotform): harden the error envelope against quoted codes and non-JSON bodies
Jotform quotes `responseCode` on some endpoints and not others, so a
typeof-number test skipped the check on the quoted ones and turned an auth
failure into a successful tool result with empty output. Also caps the raw
body fallback, since an upstream gateway can answer with an HTML page
instead of the documented envelope.
* fix(jotform): stop duplicate question labels overwriting derived answers
Question labels are not unique — a form can carry two questions both
labelled "Email" — so keying the derived `values` map on the label alone
dropped all but the last and handed downstream workflows a confidently
wrong answer.
Every occurrence of a repeated label is now suffixed with its question ID,
rather than only the later ones, so the result does not depend on answer
order and a newly duplicated label reads as absent instead of as an
arbitrary winner. The id-keyed `answers` record was already complete and
is unchanged.
* fix(jotform): make the label-keyed answer map collision-proof
Question labels are free text, so the disambiguation key added in f9026cf
was not itself safe: a question literally labelled "Email (3)" lands on the
key generated for a duplicate "Email" at qid 3, dropping one of them. Any
key already taken is now widened again until it is free.
Accumulates in a Map rather than an object literal on the way out, since a
question labelled `__proto__` assigned onto `{}` sets the prototype instead
of an own property and disappears from the map entirely.
This commit is contained in:
@@ -9140,6 +9140,35 @@ export function FlowiseIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function JotformIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox='147 132 306 336'
|
||||
fill='none'
|
||||
role='img'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path
|
||||
fill='#0A1551'
|
||||
d='M231.287 450.612C237.601 456.733 233.139 467.221 224.173 467.221H168.06C156.989 467.221 147.98 458.488 147.98 447.756V393.358C147.98 384.666 158.799 380.341 165.113 386.462L231.287 450.612Z'
|
||||
/>
|
||||
<path
|
||||
fill='#FFB629'
|
||||
d='M319.003 454.845C302.393 438.343 302.394 411.589 319.003 395.088L378.947 335.535C395.557 319.033 422.486 319.033 439.096 335.535C455.705 352.036 455.705 378.79 439.096 395.292L379.152 454.845C362.542 471.346 335.613 471.346 319.003 454.845Z'
|
||||
/>
|
||||
<path
|
||||
fill='#0099FF'
|
||||
d='M160.64 305.204C144.031 288.703 144.031 261.949 160.64 245.447L261.52 145.155C278.129 128.653 305.059 128.653 321.669 145.155C338.278 161.656 338.278 188.41 321.669 204.912L220.789 305.204C204.179 321.705 177.25 321.705 160.64 305.204Z'
|
||||
/>
|
||||
<path
|
||||
fill='#FF6100'
|
||||
d='M243.108 376.686C226.498 360.185 226.498 333.43 243.108 316.929L379.414 181.511C396.024 165.009 422.953 165.009 439.563 181.511C456.173 198.012 456.173 224.766 439.563 241.268L303.256 376.686C286.647 393.187 259.717 393.187 243.108 376.686Z'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function JupyterIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 44 51' xmlns='http://www.w3.org/2000/svg'>
|
||||
|
||||
@@ -124,6 +124,7 @@ import {
|
||||
JinaAIIcon,
|
||||
JiraIcon,
|
||||
JiraServiceManagementIcon,
|
||||
JotformIcon,
|
||||
JupyterIcon,
|
||||
KalshiIcon,
|
||||
KetchIcon,
|
||||
@@ -402,6 +403,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
jina: JinaAIIcon,
|
||||
jira: JiraIcon,
|
||||
jira_service_management: JiraServiceManagementIcon,
|
||||
jotform: JotformIcon,
|
||||
jsm: JiraServiceManagementIcon,
|
||||
jupyter: JupyterIcon,
|
||||
kalshi: KalshiIcon,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -129,6 +129,7 @@
|
||||
"jina",
|
||||
"jira",
|
||||
"jira_service_management",
|
||||
"jotform",
|
||||
"jupyter",
|
||||
"kalshi",
|
||||
"ketch",
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { JotformBlock } from '@/blocks/blocks/jotform'
|
||||
|
||||
/**
|
||||
* Every assertion here runs against `{ ...inputs, ...buildParams(inputs) }`, the
|
||||
* shape the generic tool handler actually forwards. A key the mapper omits is
|
||||
* *not* dropped by that merge — the raw subBlock value survives — so asserting
|
||||
* on the mapper's return alone would prove nothing about what the tool receives.
|
||||
*/
|
||||
describe('JotformBlock', () => {
|
||||
const buildParams = JotformBlock.tools.config.params!
|
||||
const selectTool = JotformBlock.tools.config.tool!
|
||||
|
||||
const operationIds =
|
||||
JotformBlock.subBlocks
|
||||
.find((subBlock) => subBlock.id === 'operation')
|
||||
?.options?.map((option) => (option as { id: string }).id) ?? []
|
||||
|
||||
it('maps every dropdown operation onto a registered tool', () => {
|
||||
expect(operationIds).toHaveLength(43)
|
||||
expect(new Set(operationIds.map((id) => selectTool({ operation: id })))).toEqual(
|
||||
new Set(JotformBlock.tools.access)
|
||||
)
|
||||
})
|
||||
|
||||
it('declares an input for every subblock', () => {
|
||||
const inputIds = new Set(Object.keys(JotformBlock.inputs))
|
||||
const missing = JotformBlock.subBlocks
|
||||
.map((subBlock) => subBlock.id)
|
||||
.filter((id) => !inputIds.has(id))
|
||||
|
||||
expect(missing).toEqual([])
|
||||
})
|
||||
|
||||
it('gives every subblock a unique id', () => {
|
||||
const ids = JotformBlock.subBlocks.map((subBlock) => subBlock.id)
|
||||
expect(new Set(ids).size).toBe(ids.length)
|
||||
})
|
||||
|
||||
/**
|
||||
* `properties`, `text`, `order`, `name`, `title`, and `fields` are tool params on
|
||||
* one operation and would be meaningless on another. Naming a subblock after one
|
||||
* of them would leak its stale value into every other operation through the raw
|
||||
* merge, so each is deliberately carried by a prefixed subblock instead.
|
||||
*/
|
||||
it('never names a subblock after a param another operation owns', () => {
|
||||
const contested = ['properties', 'questions', 'emails', 'text', 'order', 'name', 'title']
|
||||
const collisions = JotformBlock.subBlocks
|
||||
.map((subBlock) => subBlock.id)
|
||||
.filter((id) => contested.includes(id))
|
||||
|
||||
expect(collisions).toEqual([])
|
||||
})
|
||||
|
||||
it('renames the create-form subblocks onto the tool params', () => {
|
||||
const inputs = {
|
||||
operation: 'create_form',
|
||||
apiKey: 'key',
|
||||
newFormQuestions: '[{"type":"control_email","text":"Email","order":"1","name":"email"}]',
|
||||
newFormProperties: '{"title":"Contact Us"}',
|
||||
newFormEmails: '[{"type":"notification","to":"team@example.com"}]',
|
||||
}
|
||||
const finalInputs = { ...inputs, ...buildParams(inputs) }
|
||||
|
||||
expect(finalInputs.questions).toBe(inputs.newFormQuestions)
|
||||
expect(finalInputs.properties).toBe(inputs.newFormProperties)
|
||||
expect(finalInputs.emails).toBe(inputs.newFormEmails)
|
||||
})
|
||||
|
||||
it('renames the question subblocks onto the tool params', () => {
|
||||
const inputs = {
|
||||
operation: 'create_question',
|
||||
apiKey: 'key',
|
||||
formId: '2315',
|
||||
questionType: 'control_email',
|
||||
questionText: 'Your email',
|
||||
questionOrder: '2',
|
||||
questionName: 'yourEmail',
|
||||
}
|
||||
const finalInputs = { ...inputs, ...buildParams(inputs) }
|
||||
|
||||
expect(finalInputs.text).toBe('Your email')
|
||||
expect(finalInputs.order).toBe('2')
|
||||
expect(finalInputs.name).toBe('yourEmail')
|
||||
expect(finalInputs.questionType).toBe('control_email')
|
||||
})
|
||||
|
||||
it('renames the report subblocks onto the tool params', () => {
|
||||
const inputs = {
|
||||
operation: 'create_report',
|
||||
apiKey: 'key',
|
||||
formId: '2315',
|
||||
reportTitle: 'Weekly responses',
|
||||
reportType: 'csv',
|
||||
reportFields: 'ip,dt,3,4',
|
||||
}
|
||||
const finalInputs = { ...inputs, ...buildParams(inputs) }
|
||||
|
||||
expect(finalInputs.title).toBe('Weekly responses')
|
||||
expect(finalInputs.listType).toBe('csv')
|
||||
expect(finalInputs.fields).toBe('ip,dt,3,4')
|
||||
})
|
||||
|
||||
it('renames the bulk subblocks onto the tool params', () => {
|
||||
const submissions = {
|
||||
operation: 'create_submissions',
|
||||
apiKey: 'key',
|
||||
formId: '2315',
|
||||
bulkSubmissions: '[{"1":{"text":"a"}}]',
|
||||
}
|
||||
expect({ ...submissions, ...buildParams(submissions) }.submissions).toBe(
|
||||
submissions.bulkSubmissions
|
||||
)
|
||||
|
||||
const questions = {
|
||||
operation: 'create_questions',
|
||||
apiKey: 'key',
|
||||
formId: '2315',
|
||||
bulkQuestions: '[{"type":"control_head"}]',
|
||||
}
|
||||
expect({ ...questions, ...buildParams(questions) }.questions).toBe(questions.bulkQuestions)
|
||||
})
|
||||
|
||||
/**
|
||||
* `create_form` and `create_questions` both feed a `questions` tool param from
|
||||
* different subblocks. A leftover value from one must not arrive as the other.
|
||||
*/
|
||||
it('keeps the two questions sources from bleeding into each other', () => {
|
||||
const inputs = {
|
||||
operation: 'create_questions',
|
||||
apiKey: 'key',
|
||||
formId: '2315',
|
||||
bulkQuestions: '[{"type":"control_head"}]',
|
||||
newFormQuestions: '[{"type":"control_email"}]',
|
||||
}
|
||||
|
||||
expect({ ...inputs, ...buildParams(inputs) }.questions).toBe(inputs.bulkQuestions)
|
||||
})
|
||||
|
||||
it('renames the history subblocks onto the tool params', () => {
|
||||
const inputs = {
|
||||
operation: 'get_history',
|
||||
apiKey: 'key',
|
||||
historyAction: 'formCreation',
|
||||
historySortBy: 'ASC',
|
||||
historyStartDate: '01/01/2026',
|
||||
historyEndDate: '02/01/2026',
|
||||
}
|
||||
const finalInputs = { ...inputs, ...buildParams(inputs) }
|
||||
|
||||
expect(finalInputs.action).toBe('formCreation')
|
||||
expect(finalInputs.sortBy).toBe('ASC')
|
||||
expect(finalInputs.startDate).toBe('01/01/2026')
|
||||
expect(finalInputs.endDate).toBe('02/01/2026')
|
||||
})
|
||||
|
||||
/**
|
||||
* A leftover value from a previously selected operation still reaches the mapper.
|
||||
* The rename must not fire for the operation that does not own it, or a stale
|
||||
* report title would arrive as a question label.
|
||||
*/
|
||||
it('leaves renames untouched for operations that do not own them', () => {
|
||||
const inputs = {
|
||||
operation: 'list_form_submissions',
|
||||
apiKey: 'key',
|
||||
formId: '2315',
|
||||
reportTitle: 'Left over from an earlier operation',
|
||||
questionText: 'Also left over',
|
||||
}
|
||||
const finalInputs = { ...inputs, ...buildParams(inputs) }
|
||||
|
||||
expect(finalInputs.title).toBeUndefined()
|
||||
expect(finalInputs.text).toBeUndefined()
|
||||
expect(finalInputs.listType).toBeUndefined()
|
||||
})
|
||||
|
||||
it('marks the identifier each operation addresses as required', () => {
|
||||
const requiredFor = (id: string) => {
|
||||
const subBlock = JotformBlock.subBlocks.find((candidate) => candidate.id === id)
|
||||
const required = subBlock?.required as { field: string; value: string | string[] } | undefined
|
||||
const value = required?.value ?? []
|
||||
return Array.isArray(value) ? value : [value]
|
||||
}
|
||||
|
||||
expect(requiredFor('formId')).toContain('list_form_submissions')
|
||||
expect(requiredFor('submissionId')).toEqual([
|
||||
'get_submission',
|
||||
'update_submission',
|
||||
'delete_submission',
|
||||
])
|
||||
expect(requiredFor('questionId')).toEqual([
|
||||
'get_question',
|
||||
'update_question',
|
||||
'delete_question',
|
||||
])
|
||||
expect(requiredFor('reportId')).toEqual(['get_report', 'delete_report'])
|
||||
expect(requiredFor('webhookId')).toEqual(['delete_webhook'])
|
||||
})
|
||||
})
|
||||
File diff suppressed because it is too large
Load Diff
@@ -167,6 +167,7 @@ import {
|
||||
JiraServiceManagementBlock,
|
||||
JiraServiceManagementBlockMeta,
|
||||
} from '@/blocks/blocks/jira_service_management'
|
||||
import { JotformBlock, JotformBlockMeta } from '@/blocks/blocks/jotform'
|
||||
import { JupyterBlock, JupyterBlockMeta } from '@/blocks/blocks/jupyter'
|
||||
import {
|
||||
KalshiBlock,
|
||||
@@ -513,6 +514,7 @@ export const BLOCK_REGISTRY: Record<string, BlockConfig> = {
|
||||
jina: JinaBlock,
|
||||
jira: JiraBlock,
|
||||
jira_service_management: JiraServiceManagementBlock,
|
||||
jotform: JotformBlock,
|
||||
jupyter: JupyterBlock,
|
||||
kalshi: KalshiBlock,
|
||||
kalshi_v2: KalshiV2Block,
|
||||
@@ -830,6 +832,7 @@ export const BLOCK_META_REGISTRY: Record<string, BlockMeta> = {
|
||||
jina: JinaBlockMeta,
|
||||
jira: JiraBlockMeta,
|
||||
jira_service_management: JiraServiceManagementBlockMeta,
|
||||
jotform: JotformBlockMeta,
|
||||
jupyter: JupyterBlockMeta,
|
||||
kalshi: KalshiBlockMeta,
|
||||
kalshi_v2: KalshiV2BlockMeta,
|
||||
|
||||
@@ -9140,6 +9140,35 @@ export function FlowiseIcon(props: SVGProps<SVGSVGElement>) {
|
||||
)
|
||||
}
|
||||
|
||||
export function JotformIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
viewBox='147 132 306 336'
|
||||
fill='none'
|
||||
role='img'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path
|
||||
fill='#0A1551'
|
||||
d='M231.287 450.612C237.601 456.733 233.139 467.221 224.173 467.221H168.06C156.989 467.221 147.98 458.488 147.98 447.756V393.358C147.98 384.666 158.799 380.341 165.113 386.462L231.287 450.612Z'
|
||||
/>
|
||||
<path
|
||||
fill='#FFB629'
|
||||
d='M319.003 454.845C302.393 438.343 302.394 411.589 319.003 395.088L378.947 335.535C395.557 319.033 422.486 319.033 439.096 335.535C455.705 352.036 455.705 378.79 439.096 395.292L379.152 454.845C362.542 471.346 335.613 471.346 319.003 454.845Z'
|
||||
/>
|
||||
<path
|
||||
fill='#0099FF'
|
||||
d='M160.64 305.204C144.031 288.703 144.031 261.949 160.64 245.447L261.52 145.155C278.129 128.653 305.059 128.653 321.669 145.155C338.278 161.656 338.278 188.41 321.669 204.912L220.789 305.204C204.179 321.705 177.25 321.705 160.64 305.204Z'
|
||||
/>
|
||||
<path
|
||||
fill='#FF6100'
|
||||
d='M243.108 376.686C226.498 360.185 226.498 333.43 243.108 316.929L379.414 181.511C396.024 165.009 422.953 165.009 439.563 181.511C456.173 198.012 456.173 224.766 439.563 241.268L303.256 376.686C286.647 393.187 259.717 393.187 243.108 376.686Z'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function JupyterIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg {...props} viewBox='0 0 44 51' xmlns='http://www.w3.org/2000/svg'>
|
||||
|
||||
@@ -123,6 +123,7 @@ import {
|
||||
JinaAIIcon,
|
||||
JiraIcon,
|
||||
JiraServiceManagementIcon,
|
||||
JotformIcon,
|
||||
JupyterIcon,
|
||||
KalshiIcon,
|
||||
KetchIcon,
|
||||
@@ -391,6 +392,7 @@ export const blockTypeToIconMap: Record<string, IconComponent> = {
|
||||
jina: JinaAIIcon,
|
||||
jira: JiraIcon,
|
||||
jira_service_management: JiraServiceManagementIcon,
|
||||
jotform: JotformIcon,
|
||||
jsm: JiraServiceManagementIcon,
|
||||
jupyter: JupyterIcon,
|
||||
kalshi_v2: KalshiIcon,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"updatedAt": "2026-08-16",
|
||||
"updatedAt": "2026-08-17",
|
||||
"integrations": [
|
||||
{
|
||||
"type": "onepassword",
|
||||
@@ -11411,6 +11411,197 @@
|
||||
"integrationType": "support",
|
||||
"tags": ["customer-support", "ticketing", "incident-management"]
|
||||
},
|
||||
{
|
||||
"type": "jotform",
|
||||
"slug": "jotform",
|
||||
"name": "Jotform",
|
||||
"description": "Read submissions, manage forms, and wire up webhooks in Jotform",
|
||||
"longDescription": "Integrate Jotform into your workflow to list and read form submissions with their answers resolved to question labels, build and edit forms and their questions, create shareable reports, register submission webhooks, and check account usage.",
|
||||
"bgColor": "#FFFFFF",
|
||||
"iconName": "JotformIcon",
|
||||
"docsUrl": "https://docs.sim.ai/integrations/jotform",
|
||||
"operations": [
|
||||
{
|
||||
"name": "List Forms",
|
||||
"description": "List the forms on a Jotform account with their titles, status, and submission counts."
|
||||
},
|
||||
{
|
||||
"name": "Get Form",
|
||||
"description": "Get the details of a single Jotform form, including its status, URL, and submission counts."
|
||||
},
|
||||
{
|
||||
"name": "Create Form",
|
||||
"description": "Create a new Jotform form from a list of questions, plus optional form properties and notification emails."
|
||||
},
|
||||
{
|
||||
"name": "Clone Form",
|
||||
"description": "Clone an existing Jotform form and return the copy."
|
||||
},
|
||||
{
|
||||
"name": "Delete Form",
|
||||
"description": "Delete a Jotform form. The API returns the form with status DELETED."
|
||||
},
|
||||
{
|
||||
"name": "Get Form Properties",
|
||||
"description": "Get the settings of a form: layout, limits, redirect behavior, notification emails, and validation strings. Supply a property key to read just one."
|
||||
},
|
||||
{
|
||||
"name": "Update Form Properties",
|
||||
"description": "Update form settings such as the thank-you redirect, submission limit, width, or styles. Only the supplied keys change."
|
||||
},
|
||||
{
|
||||
"name": "List Form Files",
|
||||
"description": "List every file uploaded through a form, with its download URL, size, type, and the submission it came from."
|
||||
},
|
||||
{
|
||||
"name": "List Questions",
|
||||
"description": "List every question on a form with its question ID, label, and field type. Question IDs are what submissions are keyed by."
|
||||
},
|
||||
{
|
||||
"name": "Get Question",
|
||||
"description": "Get every property of a single form question, including its validation rules and field-specific settings."
|
||||
},
|
||||
{
|
||||
"name": "Create Question",
|
||||
"description": "Add a question to an existing Jotform form."
|
||||
},
|
||||
{
|
||||
"name": "Create Questions",
|
||||
"description": "Add several questions to an existing Jotform form in one call."
|
||||
},
|
||||
{
|
||||
"name": "Update Question",
|
||||
"description": "Edit the properties of a form question, such as its label, order, or validation. Only the supplied properties change."
|
||||
},
|
||||
{
|
||||
"name": "Delete Question",
|
||||
"description": "Delete a question from a Jotform form."
|
||||
},
|
||||
{
|
||||
"name": "List Form Submissions",
|
||||
"description": "List the submissions received by one form, with each answer available both by question ID and by question label."
|
||||
},
|
||||
{
|
||||
"name": "List Submissions",
|
||||
"description": "List submissions across every form on the account, optionally narrowed to specific forms or a date range."
|
||||
},
|
||||
{
|
||||
"name": "Get Submission",
|
||||
"description": "Get a single Jotform submission, with its answers available both by question ID and by question label."
|
||||
},
|
||||
{
|
||||
"name": "Create Submission",
|
||||
"description": "Submit an entry to a Jotform form. Answers are keyed by question ID, which the List Questions operation returns."
|
||||
},
|
||||
{
|
||||
"name": "Create Submissions",
|
||||
"description": "Submit several entries to a Jotform form in one call. Each entry is keyed by question ID."
|
||||
},
|
||||
{
|
||||
"name": "Update Submission",
|
||||
"description": "Edit an existing Jotform submission. Only the question IDs supplied are changed; the rest keep their stored answers."
|
||||
},
|
||||
{
|
||||
"name": "Delete Submission",
|
||||
"description": "Delete a single Jotform submission."
|
||||
},
|
||||
{
|
||||
"name": "List Reports",
|
||||
"description": "List every report on the account, across all forms, with the shareable URL for each Excel, CSV, grid, table, calendar, RSS, or visual report."
|
||||
},
|
||||
{
|
||||
"name": "List Form Reports",
|
||||
"description": "List the reports built from one form, each with its shareable URL."
|
||||
},
|
||||
{
|
||||
"name": "Create Report",
|
||||
"description": "Create a shareable report of a form, choosing the report type and which submission fields it shows."
|
||||
},
|
||||
{
|
||||
"name": "Get Report",
|
||||
"description": "Get the details and shareable URL of a single Jotform report."
|
||||
},
|
||||
{
|
||||
"name": "Delete Report",
|
||||
"description": "Delete an existing Jotform report."
|
||||
},
|
||||
{
|
||||
"name": "List Webhooks",
|
||||
"description": "List the webhooks registered on a form. The returned IDs are what the Delete Webhook operation takes."
|
||||
},
|
||||
{
|
||||
"name": "Create Webhook",
|
||||
"description": "Register a webhook on a form so every new submission is posted to the given URL. Returns the full webhook list for the form."
|
||||
},
|
||||
{
|
||||
"name": "Delete Webhook",
|
||||
"description": "Remove a webhook from a form. Returns the webhooks that remain registered on the form."
|
||||
},
|
||||
{
|
||||
"name": "List Labels",
|
||||
"description": "List the labels on the account as a tree. Labels are how Jotform groups forms, workflows, sheets, and apps, replacing the older folder endpoints."
|
||||
},
|
||||
{
|
||||
"name": "Get Label",
|
||||
"description": "Get the name, color, and owner of a single Jotform label."
|
||||
},
|
||||
{
|
||||
"name": "Create Label",
|
||||
"description": "Create a Jotform label for grouping forms and other assets, optionally nested under a parent label."
|
||||
},
|
||||
{
|
||||
"name": "Update Label",
|
||||
"description": "Rename a Jotform label or change its color."
|
||||
},
|
||||
{
|
||||
"name": "Delete Label",
|
||||
"description": "Delete a Jotform label along with all of its sublabels."
|
||||
},
|
||||
{
|
||||
"name": "List Label Resources",
|
||||
"description": "List the assets assigned to a label — forms, workflows, sheets, and apps — with their status and titles."
|
||||
},
|
||||
{
|
||||
"name": "Add Label Resources",
|
||||
"description": "Assign forms, workflows, sheets, or apps to a Jotform label."
|
||||
},
|
||||
{
|
||||
"name": "Remove Label Resources",
|
||||
"description": "Unassign forms, workflows, sheets, or apps from a Jotform label."
|
||||
},
|
||||
{
|
||||
"name": "List Sub-Users",
|
||||
"description": "List the sub-users on the account with the forms and folders each one can reach, and at what access level."
|
||||
},
|
||||
{
|
||||
"name": "Get Settings",
|
||||
"description": "Read the account settings behind the API key, including time zone, language, and contact details."
|
||||
},
|
||||
{
|
||||
"name": "Update Settings",
|
||||
"description": "Update account settings such as name, email, website, company, industry, or time zone. Only the supplied fields change."
|
||||
},
|
||||
{
|
||||
"name": "Get Account",
|
||||
"description": "Get the Jotform account behind the API key, including its plan, status, time zone, and contact details."
|
||||
},
|
||||
{
|
||||
"name": "Get Usage",
|
||||
"description": "Get this month usage for the account: submissions received, payments, form views, upload space, and API calls made today."
|
||||
},
|
||||
{
|
||||
"name": "Get History",
|
||||
"description": "Read the account activity log: forms created, updated, deleted or purged, and account logins."
|
||||
}
|
||||
],
|
||||
"operationCount": 43,
|
||||
"triggers": [],
|
||||
"triggerCount": 0,
|
||||
"authType": "api-key",
|
||||
"category": "tools",
|
||||
"integrationType": "productivity",
|
||||
"tags": ["forms", "automation", "webhooks"]
|
||||
},
|
||||
{
|
||||
"type": "jupyter",
|
||||
"slug": "jupyter",
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,91 @@
|
||||
import { normalizeLabelResourceRefs, toLabelResourcePayload } from '@/tools/jotform/normalize'
|
||||
import type {
|
||||
JotformLabelResourceRefsResponse,
|
||||
JotformLabelResourcesParams,
|
||||
} from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const addLabelResourcesTool: ToolConfig<
|
||||
JotformLabelResourcesParams,
|
||||
JotformLabelResourceRefsResponse
|
||||
> = {
|
||||
id: 'jotform_add_label_resources',
|
||||
name: 'Jotform Add Label Resources',
|
||||
description: 'Assign forms, workflows, sheets, or apps to a Jotform label.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
labelId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the label to assign the assets to',
|
||||
},
|
||||
resources: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Assets to add, each with an id and a type of form, workflow, sheet, or portal, e.g. [{"id":"251464995493876","type":"form"}]',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildJotformUrl(
|
||||
params,
|
||||
`label/${encodeURIComponent(requireValue(params.labelId, 'labelId'))}/add-resources`
|
||||
).toString(),
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
...buildJotformHeaders(params.apiKey),
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({ resources: toLabelResourcePayload(params.resources) }),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Add Label Resources')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { resources: normalizeLabelResourceRefs(envelope.content) },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
resources: {
|
||||
type: 'array',
|
||||
description: 'The assets now assigned to the label',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Asset ID' },
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'Asset kind, echoed uppercase: FORM, WORKFLOW, SHEET, or PORTAL',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { normalizeForm, unwrapSingle } from '@/tools/jotform/normalize'
|
||||
import type { JotformCloneFormParams, JotformFormResponse } from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const cloneFormTool: ToolConfig<JotformCloneFormParams, JotformFormResponse> = {
|
||||
id: 'jotform_clone_form',
|
||||
name: 'Jotform Clone Form',
|
||||
description: 'Clone an existing Jotform form and return the copy.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
formId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the form to clone',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildJotformUrl(
|
||||
params,
|
||||
`form/${encodeURIComponent(requireValue(params.formId, 'formId'))}/clone`
|
||||
).toString(),
|
||||
method: 'POST',
|
||||
headers: (params) => buildJotformHeaders(params.apiKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Clone Form')
|
||||
const raw = unwrapSingle(envelope.content)
|
||||
if (!raw) throw new Error('Jotform Clone Form returned no form.')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { form: normalizeForm(raw) },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
form: {
|
||||
type: 'object',
|
||||
description: 'The newly created copy',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'ID of the cloned form' },
|
||||
username: { type: 'string', description: 'Account that owns the clone' },
|
||||
title: { type: 'string', description: 'Form title' },
|
||||
height: { type: 'string', description: 'Form height in pixels' },
|
||||
status: { type: 'string', description: 'ENABLED, DISABLED, or DELETED' },
|
||||
created_at: { type: 'string', description: 'Creation time, YYYY-MM-DD HH:MM:SS' },
|
||||
updated_at: { type: 'string', description: 'Last update time, YYYY-MM-DD HH:MM:SS' },
|
||||
last_submission: { type: 'string', description: 'Time of the most recent submission' },
|
||||
new: { type: 'string', description: 'Unread submission count' },
|
||||
count: { type: 'string', description: 'Total submission count' },
|
||||
type: { type: 'string', description: 'LEGACY or CARD' },
|
||||
favorite: { type: 'string', description: '1 when the form is favorited, otherwise 0' },
|
||||
archived: { type: 'string', description: '1 when the form is archived, otherwise 0' },
|
||||
url: { type: 'string', description: 'Public URL of the cloned form' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { normalizeForm, unwrapSingle } from '@/tools/jotform/normalize'
|
||||
import type { JotformCreateFormParams, JotformFormResponse } from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
parseJotformResponse,
|
||||
toJsonArray,
|
||||
toJsonObject,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const createFormTool: ToolConfig<JotformCreateFormParams, JotformFormResponse> = {
|
||||
id: 'jotform_create_form',
|
||||
name: 'Jotform Create Form',
|
||||
description:
|
||||
'Create a new Jotform form from a list of questions, plus optional form properties and notification emails.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
questions: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Array of question objects, each with type, text, order, and name, e.g. [{"type":"control_email","text":"Email","order":"1","name":"email"}]',
|
||||
},
|
||||
properties: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Form properties object, e.g. {"title":"Contact Us","height":"600"}',
|
||||
},
|
||||
emails: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Array of email objects, each with type (notification or autorespond), from, to, subject, html, and body',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => buildJotformUrl(params, 'form').toString(),
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
...buildJotformHeaders(params.apiKey),
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const questions = toJsonArray(params.questions, 'questions')
|
||||
if (questions.length === 0) {
|
||||
throw new Error('questions must contain at least one question object.')
|
||||
}
|
||||
|
||||
const body: Record<string, unknown> = { questions }
|
||||
const properties = toJsonObject(params.properties, 'properties')
|
||||
if (Object.keys(properties).length > 0) body.properties = properties
|
||||
const emails = toJsonArray(params.emails, 'emails')
|
||||
if (emails.length > 0) body.emails = emails
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Create Form')
|
||||
const raw = unwrapSingle(envelope.content)
|
||||
if (!raw) throw new Error('Jotform Create Form returned no form.')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { form: normalizeForm(raw) },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
form: {
|
||||
type: 'object',
|
||||
description: 'The created form',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'ID of the created form' },
|
||||
username: { type: 'string', description: 'Account that owns the form' },
|
||||
title: { type: 'string', description: 'Form title' },
|
||||
height: { type: 'string', description: 'Form height in pixels' },
|
||||
status: { type: 'string', description: 'ENABLED, DISABLED, or DELETED' },
|
||||
created_at: { type: 'string', description: 'Creation time, YYYY-MM-DD HH:MM:SS' },
|
||||
updated_at: { type: 'string', description: 'Last update time, YYYY-MM-DD HH:MM:SS' },
|
||||
new: { type: 'string', description: 'Unread submission count' },
|
||||
count: { type: 'string', description: 'Total submission count' },
|
||||
url: { type: 'string', description: 'Public form URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { normalizeLabel, unwrapSingle } from '@/tools/jotform/normalize'
|
||||
import type { JotformCreateLabelParams, JotformLabelResponse } from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformUrl,
|
||||
jotformFormHeaders,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
toFormBody,
|
||||
trimOrUndefined,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const createLabelTool: ToolConfig<JotformCreateLabelParams, JotformLabelResponse> = {
|
||||
id: 'jotform_create_label',
|
||||
name: 'Jotform Create Label',
|
||||
description:
|
||||
'Create a Jotform label for grouping forms and other assets, optionally nested under a parent label.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
labelName: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Name of the label, e.g. "IT Operations"',
|
||||
},
|
||||
color: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Label color as a hex code, e.g. "#FFDC7B"',
|
||||
},
|
||||
parent: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the parent label to nest this label under',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => buildJotformUrl(params, 'label').toString(),
|
||||
method: 'POST',
|
||||
headers: (params) => jotformFormHeaders(params.apiKey),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {
|
||||
name: requireValue(params.labelName, 'labelName'),
|
||||
}
|
||||
const color = trimOrUndefined(params.color)
|
||||
const parent = trimOrUndefined(params.parent)
|
||||
if (color) body.color = color
|
||||
if (parent) body.parent = parent
|
||||
return toFormBody(body)
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Create Label')
|
||||
const raw = unwrapSingle(envelope.content)
|
||||
if (!raw) throw new Error('Jotform Create Label returned no label.')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { label: normalizeLabel(raw) },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
label: {
|
||||
type: 'object',
|
||||
description: 'The created label',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'ID assigned to the new label' },
|
||||
name: { type: 'string', description: 'Label name' },
|
||||
color: { type: 'string', description: 'Label color, as a hex code' },
|
||||
owner: { type: 'string', description: 'Account that owns the label' },
|
||||
created_at: { type: 'string', description: 'Creation time, YYYY-MM-DD HH:MM:SS' },
|
||||
updated_at: { type: 'string', description: 'Last update time, YYYY-MM-DD HH:MM:SS' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { normalizeQuestion, unwrapSingle } from '@/tools/jotform/normalize'
|
||||
import type { JotformCreateQuestionParams, JotformQuestionResponse } from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformUrl,
|
||||
jotformFormHeaders,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
toFormBody,
|
||||
toJsonObject,
|
||||
trimOrUndefined,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const createQuestionTool: ToolConfig<JotformCreateQuestionParams, JotformQuestionResponse> =
|
||||
{
|
||||
id: 'jotform_create_question',
|
||||
name: 'Jotform Create Question',
|
||||
description: 'Add a question to an existing Jotform form.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
formId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the form to add the question to',
|
||||
},
|
||||
questionType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Field type, e.g. control_textbox, control_textarea, control_dropdown, control_radio, control_checkbox, control_fileupload, control_fullname, control_email, or control_datetime',
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Question label shown on the form',
|
||||
},
|
||||
order: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Position of the question on the form',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Slug for the question label',
|
||||
},
|
||||
questionProperties: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Additional type-specific properties merged into the question, e.g. {"required":"Yes","validation":"Numeric"}',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildJotformUrl(
|
||||
params,
|
||||
`form/${encodeURIComponent(requireValue(params.formId, 'formId'))}/questions`
|
||||
).toString(),
|
||||
method: 'POST',
|
||||
headers: (params) => jotformFormHeaders(params.apiKey),
|
||||
body: (params) => {
|
||||
const question: Record<string, unknown> = {
|
||||
...toJsonObject(params.questionProperties, 'questionProperties'),
|
||||
type: requireValue(params.questionType, 'questionType'),
|
||||
}
|
||||
|
||||
const text = trimOrUndefined(params.text)
|
||||
const order = trimOrUndefined(params.order)
|
||||
const name = trimOrUndefined(params.name)
|
||||
if (text) question.text = text
|
||||
if (order) question.order = order
|
||||
if (name) question.name = name
|
||||
|
||||
return toFormBody({ question })
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Create Question')
|
||||
const raw = unwrapSingle(envelope.content)
|
||||
if (!raw) throw new Error('Jotform Create Question returned no question.')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { question: normalizeQuestion(raw) },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
question: {
|
||||
type: 'object',
|
||||
description: 'The created question, as stored on the form',
|
||||
properties: {
|
||||
qid: { type: 'string', description: 'ID assigned to the new question' },
|
||||
name: { type: 'string', description: 'Slug of the question label' },
|
||||
order: { type: 'string', description: 'Position of the question on the form' },
|
||||
text: { type: 'string', description: 'Question label' },
|
||||
type: { type: 'string', description: 'Field type of the question' },
|
||||
required: { type: 'string', description: 'Yes when the question is required' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { normalizeQuestion, toList } from '@/tools/jotform/normalize'
|
||||
import type {
|
||||
JotformCreateQuestionsParams,
|
||||
JotformListQuestionsResponse,
|
||||
} from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
isRecord,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
toJsonArray,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const createQuestionsTool: ToolConfig<
|
||||
JotformCreateQuestionsParams,
|
||||
JotformListQuestionsResponse
|
||||
> = {
|
||||
id: 'jotform_create_questions',
|
||||
name: 'Jotform Create Questions',
|
||||
description: 'Add several questions to an existing Jotform form in one call.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
formId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the form to add the questions to',
|
||||
},
|
||||
questions: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Array of question objects, each with type, text, order, and name, e.g. [{"type":"control_head","text":"Text 1","order":"1","name":"Header1"}]',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildJotformUrl(
|
||||
params,
|
||||
`form/${encodeURIComponent(requireValue(params.formId, 'formId'))}/questions`
|
||||
).toString(),
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
...buildJotformHeaders(params.apiKey),
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
/**
|
||||
* The endpoint reads a `questions` envelope whose value is keyed by position
|
||||
* rather than a bare array — `{"questions":{"1":{...},"2":{...}}}` — so an array
|
||||
* of question objects is indexed from 1 on the way out.
|
||||
*/
|
||||
body: (params) => {
|
||||
const questions = toJsonArray(params.questions, 'questions')
|
||||
if (questions.length === 0) {
|
||||
throw new Error('questions must contain at least one question object.')
|
||||
}
|
||||
|
||||
const indexed: Record<string, unknown> = {}
|
||||
questions.forEach((question, index) => {
|
||||
if (!isRecord(question)) {
|
||||
throw new Error('Every entry in questions must be a JSON object.')
|
||||
}
|
||||
indexed[String(index + 1)] = question
|
||||
})
|
||||
|
||||
return { questions: indexed }
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Create Questions')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
questions: toList(envelope.content).map(normalizeQuestion),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
questions: {
|
||||
type: 'array',
|
||||
description: 'The questions that were added, as stored on the form',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
qid: { type: 'string', description: 'ID assigned to the question' },
|
||||
name: { type: 'string', description: 'Slug of the question label' },
|
||||
order: { type: 'string', description: 'Position of the question on the form' },
|
||||
text: { type: 'string', description: 'Question label' },
|
||||
type: { type: 'string', description: 'Field type of the question' },
|
||||
required: { type: 'string', description: 'Yes when the question is required' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { normalizeReport, unwrapSingle } from '@/tools/jotform/normalize'
|
||||
import type { JotformCreateReportParams, JotformReportResponse } from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformUrl,
|
||||
jotformFormHeaders,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
toFormBody,
|
||||
trimOrUndefined,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const createReportTool: ToolConfig<JotformCreateReportParams, JotformReportResponse> = {
|
||||
id: 'jotform_create_report',
|
||||
name: 'Jotform Create Report',
|
||||
description:
|
||||
'Create a shareable report of a form, choosing the report type and which submission fields it shows.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
formId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the form to build the report from',
|
||||
},
|
||||
title: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Title of the report',
|
||||
},
|
||||
listType: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Report type: excel, csv, grid, table, calendar, rss, or visual',
|
||||
},
|
||||
fields: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Comma-separated fields to include: ip, dt (submission date), and question IDs, e.g. "ip,dt,3,4"',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildJotformUrl(
|
||||
params,
|
||||
`form/${encodeURIComponent(requireValue(params.formId, 'formId'))}/reports`
|
||||
).toString(),
|
||||
method: 'POST',
|
||||
headers: (params) => jotformFormHeaders(params.apiKey),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {
|
||||
title: requireValue(params.title, 'title'),
|
||||
list_type: requireValue(params.listType, 'listType'),
|
||||
}
|
||||
const fields = trimOrUndefined(params.fields)
|
||||
if (fields) body.fields = fields
|
||||
return toFormBody(body)
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Create Report')
|
||||
const raw = unwrapSingle(envelope.content)
|
||||
if (!raw) throw new Error('Jotform Create Report returned no report.')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { report: normalizeReport(raw) },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
report: {
|
||||
type: 'object',
|
||||
description: 'The created report',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Report ID' },
|
||||
form_id: { type: 'string', description: 'Form the report is built from' },
|
||||
title: { type: 'string', description: 'Report title' },
|
||||
fields: { type: 'string', description: 'Comma-separated fields included in the report' },
|
||||
list_type: { type: 'string', description: 'Report type that was created' },
|
||||
status: { type: 'string', description: 'ENABLED or DELETED' },
|
||||
url: { type: 'string', description: 'Shareable URL of the report' },
|
||||
isProtected: { type: 'boolean', description: 'True when the report is password protected' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { unwrapSingle } from '@/tools/jotform/normalize'
|
||||
import type {
|
||||
JotformCreateSubmissionParams,
|
||||
JotformSubmissionRefResponse,
|
||||
} from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformUrl,
|
||||
jotformFormHeaders,
|
||||
normalizeSubmissionAnswers,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
toFormBody,
|
||||
toJsonObject,
|
||||
toStringOrNull,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const createSubmissionTool: ToolConfig<
|
||||
JotformCreateSubmissionParams,
|
||||
JotformSubmissionRefResponse
|
||||
> = {
|
||||
id: 'jotform_create_submission',
|
||||
name: 'Jotform Create Submission',
|
||||
description:
|
||||
'Submit an entry to a Jotform form. Answers are keyed by question ID, which the List Questions operation returns.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
formId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the form to submit to',
|
||||
},
|
||||
answers: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Answers keyed by question ID. Multi-field questions take either a nested object or the documented shorthand, e.g. {"3":{"first":"Bart","last":"Simpson"},"4":"Hello"} or {"3_first":"Bart","3_last":"Simpson"}',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildJotformUrl(
|
||||
params,
|
||||
`form/${encodeURIComponent(requireValue(params.formId, 'formId'))}/submissions`
|
||||
).toString(),
|
||||
method: 'POST',
|
||||
headers: (params) => jotformFormHeaders(params.apiKey),
|
||||
body: (params) => {
|
||||
const answers = toJsonObject(params.answers, 'answers')
|
||||
if (Object.keys(answers).length === 0) {
|
||||
throw new Error('answers must contain at least one question ID.')
|
||||
}
|
||||
return toFormBody({ submission: normalizeSubmissionAnswers(answers) })
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Create Submission')
|
||||
const raw = unwrapSingle(envelope.content) ?? {}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
submissionId: toStringOrNull(raw.submissionID),
|
||||
url: toStringOrNull(raw.URL),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
submissionId: {
|
||||
type: 'string',
|
||||
description: 'ID of the submission that was created',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'API URL of the new submission',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import type {
|
||||
JotformCreateSubmissionsParams,
|
||||
JotformCreateSubmissionsResponse,
|
||||
} from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
isRecord,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
toJsonArray,
|
||||
toStringOrNull,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const createSubmissionsTool: ToolConfig<
|
||||
JotformCreateSubmissionsParams,
|
||||
JotformCreateSubmissionsResponse
|
||||
> = {
|
||||
id: 'jotform_create_submissions',
|
||||
name: 'Jotform Create Submissions',
|
||||
description:
|
||||
'Submit several entries to a Jotform form in one call. Each entry is keyed by question ID.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
formId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the form to submit to',
|
||||
},
|
||||
submissions: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Array of submissions. Each entry maps a question ID to an object holding its answer, e.g. [{"1":{"text":"Answer 1"},"2":{"text":"Answer 2"}}]',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildJotformUrl(
|
||||
params,
|
||||
`form/${encodeURIComponent(requireValue(params.formId, 'formId'))}/submissions`
|
||||
).toString(),
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
...buildJotformHeaders(params.apiKey),
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
/**
|
||||
* Unlike the single-submission POST, the bulk endpoint takes a bare JSON array
|
||||
* rather than form-encoded `submission[...]` keys or a named envelope.
|
||||
*/
|
||||
body: (params) => {
|
||||
const submissions = toJsonArray(params.submissions, 'submissions')
|
||||
if (submissions.length === 0) {
|
||||
throw new Error('submissions must contain at least one entry.')
|
||||
}
|
||||
return submissions
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Create Submissions')
|
||||
const entries = Array.isArray(envelope.content) ? envelope.content : []
|
||||
|
||||
const created = entries.filter(isRecord).map((entry) => ({
|
||||
submissionId: toStringOrNull(entry.submissionID),
|
||||
url: toStringOrNull(entry.URL),
|
||||
}))
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
submissions: created,
|
||||
count: created.length,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
submissions: {
|
||||
type: 'array',
|
||||
description: 'The submissions that were created',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
submissionId: { type: 'string', description: 'ID of the created submission' },
|
||||
url: { type: 'string', description: 'API URL of the created submission' },
|
||||
},
|
||||
},
|
||||
},
|
||||
count: {
|
||||
type: 'number',
|
||||
description: 'Number of submissions created',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { normalizeWebhooks } from '@/tools/jotform/normalize'
|
||||
import type { JotformCreateWebhookParams, JotformWebhooksResponse } from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformUrl,
|
||||
jotformFormHeaders,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
toFormBody,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const createWebhookTool: ToolConfig<JotformCreateWebhookParams, JotformWebhooksResponse> = {
|
||||
id: 'jotform_create_webhook',
|
||||
name: 'Jotform Create Webhook',
|
||||
description:
|
||||
'Register a webhook on a form so every new submission is posted to the given URL. Returns the full webhook list for the form.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
formId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the form to add the webhook to',
|
||||
},
|
||||
webhookUrl: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'URL that Jotform posts submission data to',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildJotformUrl(
|
||||
params,
|
||||
`form/${encodeURIComponent(requireValue(params.formId, 'formId'))}/webhooks`
|
||||
).toString(),
|
||||
method: 'POST',
|
||||
headers: (params) => jotformFormHeaders(params.apiKey),
|
||||
body: (params) => toFormBody({ webhookURL: requireValue(params.webhookUrl, 'webhookUrl') }),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Create Webhook')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { webhooks: normalizeWebhooks(envelope.content) },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
webhooks: {
|
||||
type: 'array',
|
||||
description: 'Webhooks registered on the form after the addition',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Webhook ID, used when deleting the webhook' },
|
||||
url: { type: 'string', description: 'URL that receives submission notifications' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { normalizeForm, unwrapSingle } from '@/tools/jotform/normalize'
|
||||
import type { JotformDeleteFormParams, JotformDeleteFormResponse } from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const deleteFormTool: ToolConfig<JotformDeleteFormParams, JotformDeleteFormResponse> = {
|
||||
id: 'jotform_delete_form',
|
||||
name: 'Jotform Delete Form',
|
||||
description: 'Delete a Jotform form. The API returns the form with status DELETED.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
formId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the form to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildJotformUrl(
|
||||
params,
|
||||
`form/${encodeURIComponent(requireValue(params.formId, 'formId'))}`
|
||||
).toString(),
|
||||
method: 'DELETE',
|
||||
headers: (params) => buildJotformHeaders(params.apiKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Delete Form')
|
||||
const raw = unwrapSingle(envelope.content)
|
||||
if (!raw) throw new Error('Jotform Delete Form returned no form.')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { form: normalizeForm(raw) },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
form: {
|
||||
type: 'object',
|
||||
description: 'The deleted form, as the API reports it after deletion',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Form ID' },
|
||||
username: { type: 'string', description: 'Account that owned the form' },
|
||||
title: { type: 'string', description: 'Form title' },
|
||||
height: { type: 'string', description: 'Form height in pixels' },
|
||||
status: { type: 'string', description: 'DELETED after a successful delete' },
|
||||
created_at: { type: 'string', description: 'Creation time, YYYY-MM-DD HH:MM:SS' },
|
||||
updated_at: { type: 'string', description: 'Deletion time, YYYY-MM-DD HH:MM:SS' },
|
||||
new: { type: 'string', description: 'Unread submission count' },
|
||||
count: { type: 'string', description: 'Total submission count' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { JotformDeleteLabelParams, JotformMessageResponse } from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
toStringOrNull,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const deleteLabelTool: ToolConfig<JotformDeleteLabelParams, JotformMessageResponse> = {
|
||||
id: 'jotform_delete_label',
|
||||
name: 'Jotform Delete Label',
|
||||
description: 'Delete a Jotform label along with all of its sublabels.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
labelId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the label to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildJotformUrl(
|
||||
params,
|
||||
`label/${encodeURIComponent(requireValue(params.labelId, 'labelId'))}`
|
||||
).toString(),
|
||||
method: 'DELETE',
|
||||
headers: (params) => buildJotformHeaders(params.apiKey),
|
||||
},
|
||||
|
||||
/**
|
||||
* Jotform publishes no response sample for this endpoint, so nothing is read out
|
||||
* of `content` beyond rendering it as text. The envelope itself still decides
|
||||
* success, which is what `parseJotformResponse` checks.
|
||||
*/
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Delete Label')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
message: toStringOrNull(envelope.content) ?? envelope.message,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: {
|
||||
type: 'boolean',
|
||||
description: 'True when Jotform accepted the deletion',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Confirmation text returned by Jotform',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { JotformDeleteQuestionParams, JotformMessageResponse } from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
toStringOrNull,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const deleteQuestionTool: ToolConfig<JotformDeleteQuestionParams, JotformMessageResponse> = {
|
||||
id: 'jotform_delete_question',
|
||||
name: 'Jotform Delete Question',
|
||||
description: 'Delete a question from a Jotform form.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
formId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the form the question belongs to',
|
||||
},
|
||||
questionId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the question to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildJotformUrl(
|
||||
params,
|
||||
`form/${encodeURIComponent(requireValue(params.formId, 'formId'))}/question/${encodeURIComponent(
|
||||
requireValue(params.questionId, 'questionId')
|
||||
)}`
|
||||
).toString(),
|
||||
method: 'DELETE',
|
||||
headers: (params) => buildJotformHeaders(params.apiKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Delete Question')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
message: toStringOrNull(envelope.content) ?? envelope.message,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: {
|
||||
type: 'boolean',
|
||||
description: 'True when Jotform accepted the deletion',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Confirmation text returned by Jotform',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { JotformDeleteReportParams, JotformMessageResponse } from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
toStringOrNull,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const deleteReportTool: ToolConfig<JotformDeleteReportParams, JotformMessageResponse> = {
|
||||
id: 'jotform_delete_report',
|
||||
name: 'Jotform Delete Report',
|
||||
description: 'Delete an existing Jotform report.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
reportId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the report to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildJotformUrl(
|
||||
params,
|
||||
`report/${encodeURIComponent(requireValue(params.reportId, 'reportId'))}`
|
||||
).toString(),
|
||||
method: 'DELETE',
|
||||
headers: (params) => buildJotformHeaders(params.apiKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Delete Report')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
message: toStringOrNull(envelope.content) ?? envelope.message,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: {
|
||||
type: 'boolean',
|
||||
description: 'True when Jotform accepted the deletion',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Confirmation text returned by Jotform',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { JotformDeleteSubmissionParams, JotformMessageResponse } from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
toStringOrNull,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const deleteSubmissionTool: ToolConfig<
|
||||
JotformDeleteSubmissionParams,
|
||||
JotformMessageResponse
|
||||
> = {
|
||||
id: 'jotform_delete_submission',
|
||||
name: 'Jotform Delete Submission',
|
||||
description: 'Delete a single Jotform submission.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
submissionId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the submission to delete',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildJotformUrl(
|
||||
params,
|
||||
`submission/${encodeURIComponent(requireValue(params.submissionId, 'submissionId'))}`
|
||||
).toString(),
|
||||
method: 'DELETE',
|
||||
headers: (params) => buildJotformHeaders(params.apiKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Delete Submission')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
deleted: true,
|
||||
message: toStringOrNull(envelope.content) ?? envelope.message,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
deleted: {
|
||||
type: 'boolean',
|
||||
description: 'True when Jotform accepted the deletion',
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: 'Confirmation text returned by Jotform',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { normalizeWebhooks } from '@/tools/jotform/normalize'
|
||||
import type { JotformDeleteWebhookParams, JotformWebhooksResponse } from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const deleteWebhookTool: ToolConfig<JotformDeleteWebhookParams, JotformWebhooksResponse> = {
|
||||
id: 'jotform_delete_webhook',
|
||||
name: 'Jotform Delete Webhook',
|
||||
description:
|
||||
'Remove a webhook from a form. Returns the webhooks that remain registered on the form.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
formId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the form the webhook belongs to',
|
||||
},
|
||||
webhookId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Webhook ID, available from the List Webhooks operation',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildJotformUrl(
|
||||
params,
|
||||
`form/${encodeURIComponent(requireValue(params.formId, 'formId'))}/webhooks/${encodeURIComponent(
|
||||
requireValue(params.webhookId, 'webhookId')
|
||||
)}`
|
||||
).toString(),
|
||||
method: 'DELETE',
|
||||
headers: (params) => buildJotformHeaders(params.apiKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Delete Webhook')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { webhooks: normalizeWebhooks(envelope.content) },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
webhooks: {
|
||||
type: 'array',
|
||||
description: 'Webhooks still registered on the form after the deletion',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Webhook ID' },
|
||||
url: { type: 'string', description: 'URL that receives submission notifications' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { normalizeForm, unwrapSingle } from '@/tools/jotform/normalize'
|
||||
import type { JotformFormResponse, JotformGetFormParams } from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getFormTool: ToolConfig<JotformGetFormParams, JotformFormResponse> = {
|
||||
id: 'jotform_get_form',
|
||||
name: 'Jotform Get Form',
|
||||
description:
|
||||
'Get the details of a single Jotform form, including its status, URL, and submission counts.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
formId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Form ID, the numeric segment of the form URL',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildJotformUrl(
|
||||
params,
|
||||
`form/${encodeURIComponent(requireValue(params.formId, 'formId'))}`
|
||||
).toString(),
|
||||
method: 'GET',
|
||||
headers: (params) => buildJotformHeaders(params.apiKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Get Form')
|
||||
const raw = unwrapSingle(envelope.content)
|
||||
if (!raw) throw new Error('Jotform Get Form returned no form.')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { form: normalizeForm(raw) },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
form: {
|
||||
type: 'object',
|
||||
description: 'The requested form',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Form ID' },
|
||||
username: { type: 'string', description: 'Account that owns the form' },
|
||||
title: { type: 'string', description: 'Form title' },
|
||||
height: { type: 'string', description: 'Form height in pixels' },
|
||||
status: { type: 'string', description: 'ENABLED, DISABLED, or DELETED' },
|
||||
created_at: { type: 'string', description: 'Creation time, YYYY-MM-DD HH:MM:SS' },
|
||||
updated_at: { type: 'string', description: 'Last update time, YYYY-MM-DD HH:MM:SS' },
|
||||
last_submission: { type: 'string', description: 'Time of the most recent submission' },
|
||||
new: { type: 'string', description: 'Unread submission count' },
|
||||
count: { type: 'string', description: 'Total submission count' },
|
||||
type: { type: 'string', description: 'LEGACY or CARD' },
|
||||
favorite: { type: 'string', description: '1 when the form is favorited, otherwise 0' },
|
||||
archived: { type: 'string', description: '1 when the form is archived, otherwise 0' },
|
||||
url: { type: 'string', description: 'Public form URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import type {
|
||||
JotformFormPropertiesResponse,
|
||||
JotformGetFormPropertiesParams,
|
||||
} from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
isRecord,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
trimOrUndefined,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getFormPropertiesTool: ToolConfig<
|
||||
JotformGetFormPropertiesParams,
|
||||
JotformFormPropertiesResponse
|
||||
> = {
|
||||
id: 'jotform_get_form_properties',
|
||||
name: 'Jotform Get Form Properties',
|
||||
description:
|
||||
'Get the settings of a form: layout, limits, redirect behavior, notification emails, and validation strings. Supply a property key to read just one.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
formId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the form to read properties from',
|
||||
},
|
||||
propertyKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Single property to read instead of the whole set, e.g. formWidth, thankurl, or activeRedirect',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const formId = encodeURIComponent(requireValue(params.formId, 'formId'))
|
||||
const key = trimOrUndefined(params.propertyKey)
|
||||
const path = key
|
||||
? `form/${formId}/properties/${encodeURIComponent(key)}`
|
||||
: `form/${formId}/properties`
|
||||
return buildJotformUrl(params, path).toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => buildJotformHeaders(params.apiKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Get Form Properties')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
properties: isRecord(envelope.content) ? envelope.content : {},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
properties: {
|
||||
type: 'json',
|
||||
description:
|
||||
'Form properties keyed by property name. The set varies by form; documented keys include formWidth, labelWidth, activeRedirect, thankurl, expireDate, limitSubmission, styles, emails, and formStrings.',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
import { toList } from '@/tools/jotform/normalize'
|
||||
import type { JotformGetHistoryParams, JotformGetHistoryResponse } from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
parseJotformResponse,
|
||||
toStringOrNull,
|
||||
trimOrUndefined,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getHistoryTool: ToolConfig<JotformGetHistoryParams, JotformGetHistoryResponse> = {
|
||||
id: 'jotform_get_history',
|
||||
name: 'Jotform Get History',
|
||||
description:
|
||||
'Read the account activity log: forms created, updated, deleted or purged, and account logins.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
action: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Activity to filter by: all (default), userCreation, userLogin, formCreation, formUpdate, formDelete, or formPurge',
|
||||
},
|
||||
date: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Named date range to limit results to, e.g. lastWeek. Use startDate and endDate for an explicit range instead',
|
||||
},
|
||||
sortBy: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Sort order: ASC or DESC',
|
||||
},
|
||||
startDate: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Only return activity after this date. Format MM/DD/YYYY',
|
||||
},
|
||||
endDate: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Only return activity before this date. Format MM/DD/YYYY',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = buildJotformUrl(params, 'user/history')
|
||||
const action = trimOrUndefined(params.action)
|
||||
const date = trimOrUndefined(params.date)
|
||||
const sortBy = trimOrUndefined(params.sortBy)
|
||||
const startDate = trimOrUndefined(params.startDate)
|
||||
const endDate = trimOrUndefined(params.endDate)
|
||||
|
||||
if (action) url.searchParams.set('action', action)
|
||||
if (date) url.searchParams.set('date', date)
|
||||
if (sortBy) url.searchParams.set('sortBy', sortBy)
|
||||
if (startDate) url.searchParams.set('startDate', startDate)
|
||||
if (endDate) url.searchParams.set('endDate', endDate)
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => buildJotformHeaders(params.apiKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Get History')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
history: toList(envelope.content).map((entry) => ({
|
||||
type: toStringOrNull(entry.type),
|
||||
formID: toStringOrNull(entry.formID),
|
||||
username: toStringOrNull(entry.username),
|
||||
formTitle: toStringOrNull(entry.formTitle),
|
||||
formStatus: toStringOrNull(entry.formStatus),
|
||||
ip: toStringOrNull(entry.ip),
|
||||
timestamp: toStringOrNull(entry.timestamp),
|
||||
})),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
history: {
|
||||
type: 'array',
|
||||
description: 'Account activity entries',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
type: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Activity type: userCreation, userLogin, formCreation, formUpdate, formDelete, or formPurge',
|
||||
},
|
||||
formID: { type: 'string', description: 'Form the activity applied to' },
|
||||
username: { type: 'string', description: 'Account that performed the activity' },
|
||||
formTitle: { type: 'string', description: 'Title of the affected form' },
|
||||
formStatus: { type: 'string', description: 'Status of the form after the activity' },
|
||||
ip: { type: 'string', description: 'IP address the activity came from' },
|
||||
timestamp: { type: 'string', description: 'Unix timestamp of the activity, in seconds' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { normalizeLabel, unwrapSingle } from '@/tools/jotform/normalize'
|
||||
import type { JotformGetLabelParams, JotformLabelResponse } from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getLabelTool: ToolConfig<JotformGetLabelParams, JotformLabelResponse> = {
|
||||
id: 'jotform_get_label',
|
||||
name: 'Jotform Get Label',
|
||||
description: 'Get the name, color, and owner of a single Jotform label.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
labelId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the label to read, available from the List Labels operation',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildJotformUrl(
|
||||
params,
|
||||
`label/${encodeURIComponent(requireValue(params.labelId, 'labelId'))}`
|
||||
).toString(),
|
||||
method: 'GET',
|
||||
headers: (params) => buildJotformHeaders(params.apiKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Get Label')
|
||||
const raw = unwrapSingle(envelope.content)
|
||||
if (!raw) throw new Error('Jotform Get Label returned no label.')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { label: normalizeLabel(raw) },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
label: {
|
||||
type: 'object',
|
||||
description: 'The requested label',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Label ID' },
|
||||
name: { type: 'string', description: 'Label name' },
|
||||
order: { type: 'string', description: 'Position among its siblings' },
|
||||
color: { type: 'string', description: 'Label color, as a hex code' },
|
||||
owner: { type: 'string', description: 'Account that owns the label' },
|
||||
ownerType: { type: 'string', description: 'Owner kind, e.g. USER' },
|
||||
created_at: { type: 'string', description: 'Creation time, YYYY-MM-DD HH:MM:SS' },
|
||||
updated_at: { type: 'string', description: 'Last update time, YYYY-MM-DD HH:MM:SS' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { normalizeQuestion, unwrapSingle } from '@/tools/jotform/normalize'
|
||||
import type { JotformGetQuestionParams, JotformQuestionResponse } from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getQuestionTool: ToolConfig<JotformGetQuestionParams, JotformQuestionResponse> = {
|
||||
id: 'jotform_get_question',
|
||||
name: 'Jotform Get Question',
|
||||
description:
|
||||
'Get every property of a single form question, including its validation rules and field-specific settings.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
formId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the form the question belongs to',
|
||||
},
|
||||
questionId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Question ID, available from the List Questions operation',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildJotformUrl(
|
||||
params,
|
||||
`form/${encodeURIComponent(requireValue(params.formId, 'formId'))}/question/${encodeURIComponent(
|
||||
requireValue(params.questionId, 'questionId')
|
||||
)}`
|
||||
).toString(),
|
||||
method: 'GET',
|
||||
headers: (params) => buildJotformHeaders(params.apiKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Get Question')
|
||||
const raw = unwrapSingle(envelope.content)
|
||||
if (!raw) throw new Error('Jotform Get Question returned no question.')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { question: normalizeQuestion(raw) },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
question: {
|
||||
type: 'object',
|
||||
description:
|
||||
'The requested question. Also carries the type-specific properties Jotform stores for that field.',
|
||||
properties: {
|
||||
qid: { type: 'string', description: 'Question ID' },
|
||||
name: { type: 'string', description: 'Slug of the question label' },
|
||||
order: { type: 'string', description: 'Position of the question on the form' },
|
||||
text: { type: 'string', description: 'Question label' },
|
||||
type: { type: 'string', description: 'Field type, e.g. control_head or control_textbox' },
|
||||
required: { type: 'string', description: 'Yes when the question is required' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { normalizeReport, unwrapSingle } from '@/tools/jotform/normalize'
|
||||
import type { JotformGetReportParams, JotformReportResponse } from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getReportTool: ToolConfig<JotformGetReportParams, JotformReportResponse> = {
|
||||
id: 'jotform_get_report',
|
||||
name: 'Jotform Get Report',
|
||||
description: 'Get the details and shareable URL of a single Jotform report.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
reportId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the report to read',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildJotformUrl(
|
||||
params,
|
||||
`report/${encodeURIComponent(requireValue(params.reportId, 'reportId'))}`
|
||||
).toString(),
|
||||
method: 'GET',
|
||||
headers: (params) => buildJotformHeaders(params.apiKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Get Report')
|
||||
const raw = unwrapSingle(envelope.content)
|
||||
if (!raw) throw new Error('Jotform Get Report returned no report.')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { report: normalizeReport(raw) },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
report: {
|
||||
type: 'object',
|
||||
description: 'The requested report',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Report ID' },
|
||||
form_id: { type: 'string', description: 'Form the report is built from' },
|
||||
title: { type: 'string', description: 'Report title' },
|
||||
fields: { type: 'string', description: 'Comma-separated fields included in the report' },
|
||||
list_type: {
|
||||
type: 'string',
|
||||
description: 'Report type: excel, csv, grid, table, calendar, rss, or visual',
|
||||
},
|
||||
status: { type: 'string', description: 'ENABLED or DELETED' },
|
||||
url: { type: 'string', description: 'Shareable URL of the report' },
|
||||
isProtected: { type: 'boolean', description: 'True when the report is password protected' },
|
||||
settings: { type: 'string', description: 'Report display settings, as a JSON string' },
|
||||
created_at: { type: 'string', description: 'Creation time, YYYY-MM-DD HH:MM:SS' },
|
||||
updated_at: { type: 'string', description: 'Last update time, YYYY-MM-DD HH:MM:SS' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { normalizeUser, unwrapSingle } from '@/tools/jotform/normalize'
|
||||
import type { JotformGetSettingsParams, JotformUserResponse } from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
parseJotformResponse,
|
||||
trimOrUndefined,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getSettingsTool: ToolConfig<JotformGetSettingsParams, JotformUserResponse> = {
|
||||
id: 'jotform_get_settings',
|
||||
name: 'Jotform Get Settings',
|
||||
description:
|
||||
'Read the account settings behind the API key, including time zone, language, and contact details.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
settingsKey: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Single setting to read instead of the whole set, e.g. time_zone',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const key = trimOrUndefined(params.settingsKey)
|
||||
const path = key ? `user/settings/${encodeURIComponent(key)}` : 'user/settings'
|
||||
return buildJotformUrl(params, path).toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => buildJotformHeaders(params.apiKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Get Settings')
|
||||
const raw = unwrapSingle(envelope.content) ?? {}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { user: normalizeUser(raw) },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
user: {
|
||||
type: 'object',
|
||||
description:
|
||||
'Account settings. Reading a single setting fills only that field and leaves the rest null.',
|
||||
properties: {
|
||||
username: { type: 'string', description: 'Jotform username' },
|
||||
name: { type: 'string', description: 'Display name on the account' },
|
||||
email: { type: 'string', description: 'Account email address' },
|
||||
website: { type: 'string', description: 'Website recorded on the account' },
|
||||
time_zone: { type: 'string', description: 'Account time zone, in IANA format' },
|
||||
account_type: { type: 'string', description: 'URL of the plan the account is on' },
|
||||
status: { type: 'string', description: 'ACTIVE, DELETED, or SUSPENDED' },
|
||||
created_at: { type: 'string', description: 'Account creation time, YYYY-MM-DD HH:MM:SS' },
|
||||
updated_at: { type: 'string', description: 'Last update time, YYYY-MM-DD HH:MM:SS' },
|
||||
is_verified: { type: 'string', description: '1 when the account email is verified' },
|
||||
industry: { type: 'string', description: 'Industry recorded on the account' },
|
||||
company: { type: 'string', description: 'Company recorded on the account' },
|
||||
language: { type: 'string', description: 'Account interface language, e.g. en-US' },
|
||||
avatarUrl: { type: 'string', description: 'Avatar image URL' },
|
||||
usage: { type: 'string', description: 'URL of the monthly usage endpoint' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { normalizeSubmission, unwrapSingle } from '@/tools/jotform/normalize'
|
||||
import type { JotformGetSubmissionParams, JotformSubmissionResponse } from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getSubmissionTool: ToolConfig<JotformGetSubmissionParams, JotformSubmissionResponse> =
|
||||
{
|
||||
id: 'jotform_get_submission',
|
||||
name: 'Jotform Get Submission',
|
||||
description:
|
||||
'Get a single Jotform submission, with its answers available both by question ID and by question label.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
submissionId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the submission to read',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildJotformUrl(
|
||||
params,
|
||||
`submission/${encodeURIComponent(requireValue(params.submissionId, 'submissionId'))}`
|
||||
).toString(),
|
||||
method: 'GET',
|
||||
headers: (params) => buildJotformHeaders(params.apiKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Get Submission')
|
||||
const raw = unwrapSingle(envelope.content)
|
||||
if (!raw) throw new Error('Jotform Get Submission returned no submission.')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { submission: normalizeSubmission(raw) },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
submission: {
|
||||
type: 'object',
|
||||
description: 'The requested submission',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Submission ID' },
|
||||
form_id: { type: 'string', description: 'Form the submission belongs to' },
|
||||
ip: { type: 'string', description: 'IP address of the submitter' },
|
||||
created_at: { type: 'string', description: 'Submission time, YYYY-MM-DD HH:MM:SS' },
|
||||
updated_at: { type: 'string', description: 'Last edit time, YYYY-MM-DD HH:MM:SS' },
|
||||
status: { type: 'string', description: 'ACTIVE or OVERQUOTA' },
|
||||
new: { type: 'string', description: '1 when the submission is unread' },
|
||||
workflowStatus: {
|
||||
type: 'string',
|
||||
description: 'Approval state, present only when the form feeds a workflow',
|
||||
},
|
||||
answers: {
|
||||
type: 'json',
|
||||
description:
|
||||
'Answers keyed by question ID. Each holds text (the question label), type, answer, and prettyFormat when Jotform renders one.',
|
||||
},
|
||||
values: {
|
||||
type: 'json',
|
||||
description:
|
||||
'The same answers re-keyed by question label, each rendered as a single string. A label shared by more than one question is suffixed with its question ID on every occurrence, so no answer is lost.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { unwrapSingle } from '@/tools/jotform/normalize'
|
||||
import type { JotformGetUsageParams, JotformGetUsageResponse } from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
parseJotformResponse,
|
||||
toStringOrNull,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getUsageTool: ToolConfig<JotformGetUsageParams, JotformGetUsageResponse> = {
|
||||
id: 'jotform_get_usage',
|
||||
name: 'Jotform Get Usage',
|
||||
description:
|
||||
'Get this month usage for the account: submissions received, payments, form views, upload space, and API calls made today.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => buildJotformUrl(params, 'user/usage').toString(),
|
||||
method: 'GET',
|
||||
headers: (params) => buildJotformHeaders(params.apiKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Get Usage')
|
||||
const raw = unwrapSingle(envelope.content) ?? {}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
usage: {
|
||||
submissions: toStringOrNull(raw.submissions),
|
||||
ssl_submissions: toStringOrNull(raw.ssl_submissions),
|
||||
payments: toStringOrNull(raw.payments),
|
||||
uploads: toStringOrNull(raw.uploads),
|
||||
mobile_submissions: toStringOrNull(raw.mobile_submissions),
|
||||
views: toStringOrNull(raw.views),
|
||||
api: toStringOrNull(raw.api),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
usage: {
|
||||
type: 'object',
|
||||
description: 'Usage counters for the current month',
|
||||
properties: {
|
||||
submissions: { type: 'string', description: 'Submissions received this month' },
|
||||
ssl_submissions: { type: 'string', description: 'Secure submissions received this month' },
|
||||
payments: { type: 'string', description: 'Payment submissions received this month' },
|
||||
uploads: { type: 'string', description: 'Disk space used by uploaded files, in bytes' },
|
||||
mobile_submissions: {
|
||||
type: 'string',
|
||||
description: 'Mobile submissions received this month',
|
||||
},
|
||||
views: { type: 'string', description: 'Form views received this month' },
|
||||
api: { type: 'string', description: 'API calls made today' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { unwrapSingle } from '@/tools/jotform/normalize'
|
||||
import type { JotformGetUserParams, JotformGetUserResponse } from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
parseJotformResponse,
|
||||
toStringOrNull,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getUserTool: ToolConfig<JotformGetUserParams, JotformGetUserResponse> = {
|
||||
id: 'jotform_get_user',
|
||||
name: 'Jotform Get Account',
|
||||
description:
|
||||
'Get the Jotform account behind the API key, including its plan, status, time zone, and contact details.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => buildJotformUrl(params, 'user').toString(),
|
||||
method: 'GET',
|
||||
headers: (params) => buildJotformHeaders(params.apiKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Get Account')
|
||||
const raw = unwrapSingle(envelope.content)
|
||||
if (!raw) throw new Error('Jotform Get Account returned no user.')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
user: {
|
||||
username: toStringOrNull(raw.username),
|
||||
name: toStringOrNull(raw.name),
|
||||
email: toStringOrNull(raw.email),
|
||||
website: toStringOrNull(raw.website),
|
||||
time_zone: toStringOrNull(raw.time_zone),
|
||||
account_type: toStringOrNull(raw.account_type),
|
||||
status: toStringOrNull(raw.status),
|
||||
created_at: toStringOrNull(raw.created_at),
|
||||
updated_at: toStringOrNull(raw.updated_at),
|
||||
is_verified: toStringOrNull(raw.is_verified),
|
||||
industry: toStringOrNull(raw.industry),
|
||||
company: toStringOrNull(raw.company),
|
||||
language: toStringOrNull(raw.language),
|
||||
avatarUrl: toStringOrNull(raw.avatarUrl),
|
||||
usage: toStringOrNull(raw.usage),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
user: {
|
||||
type: 'object',
|
||||
description: 'The account the API key belongs to',
|
||||
properties: {
|
||||
username: { type: 'string', description: 'Jotform username' },
|
||||
name: { type: 'string', description: 'Display name on the account' },
|
||||
email: { type: 'string', description: 'Account email address' },
|
||||
website: { type: 'string', description: 'Website recorded on the account' },
|
||||
time_zone: { type: 'string', description: 'Account time zone, in IANA format' },
|
||||
account_type: { type: 'string', description: 'URL of the plan the account is on' },
|
||||
status: { type: 'string', description: 'ACTIVE, DELETED, or SUSPENDED' },
|
||||
created_at: { type: 'string', description: 'Account creation time, YYYY-MM-DD HH:MM:SS' },
|
||||
updated_at: { type: 'string', description: 'Last update time, YYYY-MM-DD HH:MM:SS' },
|
||||
is_verified: { type: 'string', description: '1 when the account email is verified' },
|
||||
industry: { type: 'string', description: 'Industry recorded on the account' },
|
||||
company: { type: 'string', description: 'Company recorded on the account' },
|
||||
language: { type: 'string', description: 'Account interface language, e.g. en-US' },
|
||||
avatarUrl: { type: 'string', description: 'Avatar image URL' },
|
||||
usage: { type: 'string', description: 'URL of the monthly usage endpoint' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { addLabelResourcesTool } from '@/tools/jotform/add_label_resources'
|
||||
import { cloneFormTool } from '@/tools/jotform/clone_form'
|
||||
import { createFormTool } from '@/tools/jotform/create_form'
|
||||
import { createLabelTool } from '@/tools/jotform/create_label'
|
||||
import { createQuestionTool } from '@/tools/jotform/create_question'
|
||||
import { createQuestionsTool } from '@/tools/jotform/create_questions'
|
||||
import { createReportTool } from '@/tools/jotform/create_report'
|
||||
import { createSubmissionTool } from '@/tools/jotform/create_submission'
|
||||
import { createSubmissionsTool } from '@/tools/jotform/create_submissions'
|
||||
import { createWebhookTool } from '@/tools/jotform/create_webhook'
|
||||
import { deleteFormTool } from '@/tools/jotform/delete_form'
|
||||
import { deleteLabelTool } from '@/tools/jotform/delete_label'
|
||||
import { deleteQuestionTool } from '@/tools/jotform/delete_question'
|
||||
import { deleteReportTool } from '@/tools/jotform/delete_report'
|
||||
import { deleteSubmissionTool } from '@/tools/jotform/delete_submission'
|
||||
import { deleteWebhookTool } from '@/tools/jotform/delete_webhook'
|
||||
import { getFormTool } from '@/tools/jotform/get_form'
|
||||
import { getFormPropertiesTool } from '@/tools/jotform/get_form_properties'
|
||||
import { getHistoryTool } from '@/tools/jotform/get_history'
|
||||
import { getLabelTool } from '@/tools/jotform/get_label'
|
||||
import { getQuestionTool } from '@/tools/jotform/get_question'
|
||||
import { getReportTool } from '@/tools/jotform/get_report'
|
||||
import { getSettingsTool } from '@/tools/jotform/get_settings'
|
||||
import { getSubmissionTool } from '@/tools/jotform/get_submission'
|
||||
import { getUsageTool } from '@/tools/jotform/get_usage'
|
||||
import { getUserTool } from '@/tools/jotform/get_user'
|
||||
import { listFormFilesTool } from '@/tools/jotform/list_form_files'
|
||||
import { listFormReportsTool } from '@/tools/jotform/list_form_reports'
|
||||
import { listFormSubmissionsTool } from '@/tools/jotform/list_form_submissions'
|
||||
import { listFormsTool } from '@/tools/jotform/list_forms'
|
||||
import { listLabelResourcesTool } from '@/tools/jotform/list_label_resources'
|
||||
import { listLabelsTool } from '@/tools/jotform/list_labels'
|
||||
import { listQuestionsTool } from '@/tools/jotform/list_questions'
|
||||
import { listReportsTool } from '@/tools/jotform/list_reports'
|
||||
import { listSubmissionsTool } from '@/tools/jotform/list_submissions'
|
||||
import { listSubUsersTool } from '@/tools/jotform/list_subusers'
|
||||
import { listWebhooksTool } from '@/tools/jotform/list_webhooks'
|
||||
import { removeLabelResourcesTool } from '@/tools/jotform/remove_label_resources'
|
||||
import { updateFormPropertiesTool } from '@/tools/jotform/update_form_properties'
|
||||
import { updateLabelTool } from '@/tools/jotform/update_label'
|
||||
import { updateQuestionTool } from '@/tools/jotform/update_question'
|
||||
import { updateSettingsTool } from '@/tools/jotform/update_settings'
|
||||
import { updateSubmissionTool } from '@/tools/jotform/update_submission'
|
||||
|
||||
export const jotformListFormsTool = listFormsTool
|
||||
export const jotformGetFormTool = getFormTool
|
||||
export const jotformCreateFormTool = createFormTool
|
||||
export const jotformCloneFormTool = cloneFormTool
|
||||
export const jotformDeleteFormTool = deleteFormTool
|
||||
export const jotformGetFormPropertiesTool = getFormPropertiesTool
|
||||
export const jotformUpdateFormPropertiesTool = updateFormPropertiesTool
|
||||
export const jotformListFormFilesTool = listFormFilesTool
|
||||
export const jotformListQuestionsTool = listQuestionsTool
|
||||
export const jotformGetQuestionTool = getQuestionTool
|
||||
export const jotformCreateQuestionTool = createQuestionTool
|
||||
export const jotformUpdateQuestionTool = updateQuestionTool
|
||||
export const jotformDeleteQuestionTool = deleteQuestionTool
|
||||
export const jotformListFormSubmissionsTool = listFormSubmissionsTool
|
||||
export const jotformListSubmissionsTool = listSubmissionsTool
|
||||
export const jotformGetSubmissionTool = getSubmissionTool
|
||||
export const jotformCreateSubmissionTool = createSubmissionTool
|
||||
export const jotformUpdateSubmissionTool = updateSubmissionTool
|
||||
export const jotformDeleteSubmissionTool = deleteSubmissionTool
|
||||
export const jotformListReportsTool = listReportsTool
|
||||
export const jotformListFormReportsTool = listFormReportsTool
|
||||
export const jotformCreateReportTool = createReportTool
|
||||
export const jotformGetReportTool = getReportTool
|
||||
export const jotformDeleteReportTool = deleteReportTool
|
||||
export const jotformListWebhooksTool = listWebhooksTool
|
||||
export const jotformCreateWebhookTool = createWebhookTool
|
||||
export const jotformDeleteWebhookTool = deleteWebhookTool
|
||||
export const jotformGetUserTool = getUserTool
|
||||
export const jotformGetUsageTool = getUsageTool
|
||||
export const jotformGetHistoryTool = getHistoryTool
|
||||
export const jotformAddLabelResourcesTool = addLabelResourcesTool
|
||||
export const jotformCreateLabelTool = createLabelTool
|
||||
export const jotformCreateQuestionsTool = createQuestionsTool
|
||||
export const jotformCreateSubmissionsTool = createSubmissionsTool
|
||||
export const jotformDeleteLabelTool = deleteLabelTool
|
||||
export const jotformGetLabelTool = getLabelTool
|
||||
export const jotformGetSettingsTool = getSettingsTool
|
||||
export const jotformListLabelResourcesTool = listLabelResourcesTool
|
||||
export const jotformListLabelsTool = listLabelsTool
|
||||
export const jotformListSubUsersTool = listSubUsersTool
|
||||
export const jotformRemoveLabelResourcesTool = removeLabelResourcesTool
|
||||
export const jotformUpdateLabelTool = updateLabelTool
|
||||
export const jotformUpdateSettingsTool = updateSettingsTool
|
||||
@@ -0,0 +1,550 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { addLabelResourcesTool } from '@/tools/jotform/add_label_resources'
|
||||
import { createFormTool } from '@/tools/jotform/create_form'
|
||||
import { createQuestionsTool } from '@/tools/jotform/create_questions'
|
||||
import { createSubmissionTool } from '@/tools/jotform/create_submission'
|
||||
import { createSubmissionsTool } from '@/tools/jotform/create_submissions'
|
||||
import { createWebhookTool } from '@/tools/jotform/create_webhook'
|
||||
import { getFormTool } from '@/tools/jotform/get_form'
|
||||
import { listFormSubmissionsTool } from '@/tools/jotform/list_form_submissions'
|
||||
import { listLabelsTool } from '@/tools/jotform/list_labels'
|
||||
import { listWebhooksTool } from '@/tools/jotform/list_webhooks'
|
||||
import { normalizeSubmission } from '@/tools/jotform/normalize'
|
||||
import { updateFormPropertiesTool } from '@/tools/jotform/update_form_properties'
|
||||
import { normalizeSubmissionAnswers, parseJotformResponse, toFormBody } from '@/tools/jotform/utils'
|
||||
|
||||
function jsonResponse(body: unknown, status = 200): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
}
|
||||
|
||||
const auth = { apiKey: 'key-123' }
|
||||
|
||||
describe('jotform request building', () => {
|
||||
it('routes each region to its own host', () => {
|
||||
const url = (region?: string) =>
|
||||
(getFormTool.request.url as (p: Record<string, unknown>) => string)({
|
||||
...auth,
|
||||
region,
|
||||
formId: '2315',
|
||||
})
|
||||
|
||||
expect(url()).toBe('https://api.jotform.com/form/2315')
|
||||
expect(url('us')).toBe('https://api.jotform.com/form/2315')
|
||||
expect(url('eu')).toBe('https://eu-api.jotform.com/form/2315')
|
||||
expect(url('hipaa')).toBe('https://hipaa-api.jotform.com/form/2315')
|
||||
expect(() => url('apac')).toThrow(/Unknown Jotform region/)
|
||||
})
|
||||
|
||||
it('sends the API key as a header rather than a query parameter', () => {
|
||||
const headers = (getFormTool.request.headers as (p: Record<string, unknown>) => object)({
|
||||
...auth,
|
||||
formId: '2315',
|
||||
})
|
||||
|
||||
expect(headers).toEqual({ APIKEY: 'key-123' })
|
||||
})
|
||||
|
||||
/**
|
||||
* Untouched subblocks serialize as empty strings and are merged into the tool
|
||||
* params underneath the block mapper, so every optional query value has to
|
||||
* survive that as "absent" rather than as `?limit=`.
|
||||
*/
|
||||
it('drops blank pagination values instead of sending empty query parameters', () => {
|
||||
const url = (listFormSubmissionsTool.request.url as (p: Record<string, unknown>) => string)({
|
||||
...auth,
|
||||
formId: '2315',
|
||||
limit: ' ',
|
||||
offset: '',
|
||||
orderby: '',
|
||||
direction: '',
|
||||
filter: '',
|
||||
})
|
||||
|
||||
expect(url).toBe('https://api.jotform.com/form/2315/submissions')
|
||||
})
|
||||
|
||||
it('serializes filters as JSON and normalizes the sort direction', () => {
|
||||
const url = (listFormSubmissionsTool.request.url as (p: Record<string, unknown>) => string)({
|
||||
...auth,
|
||||
formId: '2315',
|
||||
limit: '50',
|
||||
direction: 'desc',
|
||||
filter: { new: '1' },
|
||||
})
|
||||
const parsed = new URL(url)
|
||||
|
||||
expect(parsed.searchParams.get('limit')).toBe('50')
|
||||
expect(parsed.searchParams.get('direction')).toBe('DESC')
|
||||
expect(parsed.searchParams.get('filter')).toBe('{"new":"1"}')
|
||||
})
|
||||
|
||||
it('refuses to build a path with a missing identifier', () => {
|
||||
expect(() =>
|
||||
(getFormTool.request.url as (p: Record<string, unknown>) => string)({ ...auth, formId: '' })
|
||||
).toThrow(/formId is required/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('toFormBody', () => {
|
||||
/**
|
||||
* Jotform reads PHP bracket notation, not JSON, so a nested answer has to become
|
||||
* `submission[3][first]` — a JSON body is accepted with a 200 and silently stores
|
||||
* nothing.
|
||||
*/
|
||||
it('flattens nested objects into bracketed keys', () => {
|
||||
const body = toFormBody({ submission: { 3: { first: 'Bart', last: 'Simpson' }, 4: 'Hello' } })
|
||||
|
||||
expect(body.split('&').sort()).toEqual([
|
||||
'submission%5B3%5D%5Bfirst%5D=Bart',
|
||||
'submission%5B3%5D%5Blast%5D=Simpson',
|
||||
'submission%5B4%5D=Hello',
|
||||
])
|
||||
})
|
||||
|
||||
it('indexes arrays and skips null values', () => {
|
||||
expect(toFormBody({ q: ['a', 'b'], skipped: null })).toBe('q%5B0%5D=a&q%5B1%5D=b')
|
||||
})
|
||||
})
|
||||
|
||||
describe('create submission body', () => {
|
||||
it('declares the form content type so the transport passes the string through', () => {
|
||||
const headers = (
|
||||
createSubmissionTool.request.headers as (p: Record<string, unknown>) => object
|
||||
)(auth)
|
||||
|
||||
expect(headers).toEqual({
|
||||
APIKEY: 'key-123',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
})
|
||||
})
|
||||
|
||||
it('accepts answers as a JSON string, the shape a direct tool call sends', () => {
|
||||
const body = (createSubmissionTool.request.body as (p: Record<string, unknown>) => string)({
|
||||
...auth,
|
||||
formId: '2315',
|
||||
answers: '{"4":"Hello"}',
|
||||
})
|
||||
|
||||
expect(body).toBe('submission%5B4%5D=Hello')
|
||||
})
|
||||
|
||||
it('rejects an empty answer set rather than posting a blank submission', () => {
|
||||
expect(() =>
|
||||
(createSubmissionTool.request.body as (p: Record<string, unknown>) => string)({
|
||||
...auth,
|
||||
formId: '2315',
|
||||
answers: {},
|
||||
})
|
||||
).toThrow(/at least one question ID/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('parseJotformResponse', () => {
|
||||
/**
|
||||
* Jotform reports many failures with HTTP 200 and a non-2xx `responseCode` in the
|
||||
* body, so a check on `response.ok` alone would surface an error payload as a
|
||||
* successful tool result.
|
||||
*/
|
||||
it('throws on a non-2xx responseCode carried inside a 200 response', async () => {
|
||||
await expect(
|
||||
parseJotformResponse(
|
||||
jsonResponse({ responseCode: 401, message: 'Invalid API Key' }),
|
||||
'Jotform Get Form'
|
||||
)
|
||||
).rejects.toThrow('Jotform Get Form error (401): Invalid API Key')
|
||||
})
|
||||
|
||||
it('throws on an HTTP error status', async () => {
|
||||
await expect(
|
||||
parseJotformResponse(jsonResponse({ message: 'Not found' }, 404), 'Jotform Get Form')
|
||||
).rejects.toThrow('Jotform Get Form error (404): Not found')
|
||||
})
|
||||
|
||||
it('reads the result window alongside the content', async () => {
|
||||
const envelope = await parseJotformResponse(
|
||||
jsonResponse({
|
||||
responseCode: 200,
|
||||
message: 'success',
|
||||
content: [],
|
||||
resultSet: { offset: 0, limit: 20, count: 3 },
|
||||
'limit-left': 4986,
|
||||
}),
|
||||
'Jotform List Forms'
|
||||
)
|
||||
|
||||
expect(envelope.resultSet).toEqual({ offset: 0, limit: 20, count: 3 })
|
||||
expect(envelope.limitLeft).toBe(4986)
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeSubmission', () => {
|
||||
it('re-keys answers by question label, preferring prettyFormat', () => {
|
||||
const submission = normalizeSubmission({
|
||||
id: '237955080346633702',
|
||||
form_id: '31751954731962',
|
||||
created_at: '2013-06-25 03:38:00',
|
||||
status: 'ACTIVE',
|
||||
new: '1',
|
||||
answers: {
|
||||
'3': {
|
||||
text: 'Name',
|
||||
type: 'control_fullname',
|
||||
answer: { first: 'Bart', last: 'Simpson' },
|
||||
prettyFormat: 'Bart Simpson',
|
||||
},
|
||||
'4': { text: 'Your Message', type: 'control_textarea', answer: 'Hi there' },
|
||||
'5': { text: 'Skipped', type: 'control_textbox' },
|
||||
},
|
||||
})
|
||||
|
||||
expect(submission.values).toEqual({ Name: 'Bart Simpson', 'Your Message': 'Hi there' })
|
||||
expect(submission.answers['3'].answer).toEqual({ first: 'Bart', last: 'Simpson' })
|
||||
expect(submission.workflowStatus).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('webhook responses', () => {
|
||||
/**
|
||||
* The webhook list arrives as an id-keyed map, and that key is the ID the delete
|
||||
* endpoint takes — flattening to a bare URL list would throw it away.
|
||||
*/
|
||||
it('keeps the map key as the webhook ID', async () => {
|
||||
const result = await listWebhooksTool.transformResponse!(
|
||||
jsonResponse({
|
||||
responseCode: 200,
|
||||
message: 'success',
|
||||
content: { '0': 'https://a.example/hook', '1': 'https://b.example/hook' },
|
||||
}),
|
||||
{} as never
|
||||
)
|
||||
|
||||
expect(result.output.webhooks).toEqual([
|
||||
{ id: '0', url: 'https://a.example/hook' },
|
||||
{ id: '1', url: 'https://b.example/hook' },
|
||||
])
|
||||
})
|
||||
|
||||
it('reads the array form the create endpoint documents', async () => {
|
||||
const result = await createWebhookTool.transformResponse!(
|
||||
jsonResponse({ responseCode: 200, message: 'success', content: ['https://a.example/hook'] }),
|
||||
{} as never
|
||||
)
|
||||
|
||||
expect(result.output.webhooks).toEqual([{ id: '0', url: 'https://a.example/hook' }])
|
||||
})
|
||||
})
|
||||
|
||||
describe('single-resource unwrapping', () => {
|
||||
/**
|
||||
* `GET /form/{id}` is documented returning its single form inside an array, while
|
||||
* sibling endpoints return a bare object. Both shapes have to land on one form.
|
||||
*/
|
||||
it('accepts the form both as an object and wrapped in an array', async () => {
|
||||
const form = { id: '2315', title: 'Contact Us', status: 'ENABLED', count: '755' }
|
||||
|
||||
const fromArray = await getFormTool.transformResponse!(
|
||||
jsonResponse({ responseCode: 200, message: 'success', content: [form] }),
|
||||
{} as never
|
||||
)
|
||||
const fromObject = await getFormTool.transformResponse!(
|
||||
jsonResponse({ responseCode: 200, message: 'success', content: form }),
|
||||
{} as never
|
||||
)
|
||||
|
||||
expect(fromArray.output.form.id).toBe('2315')
|
||||
expect(fromArray.output.form).toEqual(fromObject.output.form)
|
||||
expect(fromArray.output.form.title).toBe('Contact Us')
|
||||
expect(fromArray.output.form.url).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('envelope-wrapped PUT bodies', () => {
|
||||
/**
|
||||
* `PUT /form/{id}/properties` and `PUT /form/{id}/questions` each read a named
|
||||
* envelope, while `PUT /form` and the bulk-submission PUT read their payload bare.
|
||||
* Sending the wrong one is accepted with a 200 and changes nothing, so each shape
|
||||
* is pinned against the request sample it came from.
|
||||
*/
|
||||
it('wraps form properties in a properties envelope', () => {
|
||||
const body = (updateFormPropertiesTool.request.body as (p: Record<string, unknown>) => object)({
|
||||
...auth,
|
||||
formId: '2315',
|
||||
properties: { formWidth: '650' },
|
||||
})
|
||||
|
||||
expect(body).toEqual({ properties: { formWidth: '650' } })
|
||||
})
|
||||
|
||||
it('wraps bulk questions in a questions envelope keyed from 1', () => {
|
||||
const body = (createQuestionsTool.request.body as (p: Record<string, unknown>) => object)({
|
||||
...auth,
|
||||
formId: '2315',
|
||||
questions: [
|
||||
{ type: 'control_head', text: 'Text 1' },
|
||||
{ type: 'control_head', text: 'Text 2' },
|
||||
],
|
||||
})
|
||||
|
||||
expect(body).toEqual({
|
||||
questions: {
|
||||
'1': { type: 'control_head', text: 'Text 1' },
|
||||
'2': { type: 'control_head', text: 'Text 2' },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('sends bulk submissions as a bare array', () => {
|
||||
const body = (createSubmissionsTool.request.body as (p: Record<string, unknown>) => unknown)({
|
||||
...auth,
|
||||
formId: '2315',
|
||||
submissions: [{ '1': { text: 'Answer 1' } }],
|
||||
})
|
||||
|
||||
expect(body).toEqual([{ '1': { text: 'Answer 1' } }])
|
||||
})
|
||||
|
||||
it('sends a new form without an envelope', () => {
|
||||
const body = (createFormTool.request.body as (p: Record<string, unknown>) => object)({
|
||||
...auth,
|
||||
questions: [{ type: 'control_email', text: 'Email', order: '1', name: 'email' }],
|
||||
properties: { title: 'Contact Us' },
|
||||
})
|
||||
|
||||
expect(body).toEqual({
|
||||
questions: [{ type: 'control_email', text: 'Email', order: '1', name: 'email' }],
|
||||
properties: { title: 'Contact Us' },
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeSubmissionAnswers', () => {
|
||||
/**
|
||||
* Jotform documents answers to multi-field questions in a `{qid}_{subfield}`
|
||||
* shorthand, and both official SDKs expand it to the nested wire form before
|
||||
* sending. Users copy the shorthand straight out of the docs.
|
||||
*/
|
||||
it('expands the documented qid_subfield shorthand into nested answers', () => {
|
||||
expect(normalizeSubmissionAnswers({ '1_first': 'Johny', '1_last': 'Doe', '4': 'Hi' })).toEqual({
|
||||
'1': { first: 'Johny', last: 'Doe' },
|
||||
'4': 'Hi',
|
||||
})
|
||||
})
|
||||
|
||||
it('leaves already-nested answers untouched', () => {
|
||||
expect(normalizeSubmissionAnswers({ '3': { first: 'Bart' } })).toEqual({
|
||||
'3': { first: 'Bart' },
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* `created_at` looks exactly like the shorthand and is not — splitting it would
|
||||
* post `submission[created][at]`. The official PHP SDK special-cases it by name.
|
||||
*/
|
||||
it('keeps submission control keys whole', () => {
|
||||
expect(
|
||||
normalizeSubmissionAnswers({ created_at: '2026-01-01 00:00:00', new: '1', flag: '0' })
|
||||
).toEqual({ created_at: '2026-01-01 00:00:00', new: '1', flag: '0' })
|
||||
})
|
||||
|
||||
it('produces the bracket form the API reads once encoded', () => {
|
||||
const body = (createSubmissionTool.request.body as (p: Record<string, unknown>) => string)({
|
||||
...auth,
|
||||
formId: '2315',
|
||||
answers: { '1_first': 'Johny' },
|
||||
})
|
||||
|
||||
expect(body).toBe('submission%5B1%5D%5Bfirst%5D=Johny')
|
||||
})
|
||||
})
|
||||
|
||||
describe('label resources', () => {
|
||||
it('normalizes the asset type to the lowercase form the request takes', () => {
|
||||
const body = (addLabelResourcesTool.request.body as (p: Record<string, unknown>) => object)({
|
||||
...auth,
|
||||
labelId: 'lbl-1',
|
||||
resources: [{ id: '251464995493876', type: 'FORM' }],
|
||||
})
|
||||
|
||||
expect(body).toEqual({ resources: [{ id: '251464995493876', type: 'form' }] })
|
||||
})
|
||||
|
||||
it('rejects an asset missing its type rather than sending a partial reference', () => {
|
||||
expect(() =>
|
||||
(addLabelResourcesTool.request.body as (p: Record<string, unknown>) => object)({
|
||||
...auth,
|
||||
labelId: 'lbl-1',
|
||||
resources: [{ id: '251464995493876' }],
|
||||
})
|
||||
).toThrow(/needs both an id and a type/)
|
||||
})
|
||||
|
||||
/**
|
||||
* `GET /user/labels` returns a root label whose `sublabels` nest without a
|
||||
* documented depth bound, so the walk is capped rather than trusting the payload.
|
||||
*/
|
||||
it('preserves the label tree and caps runaway nesting', async () => {
|
||||
const result = await listLabelsTool.transformResponse!(
|
||||
jsonResponse({
|
||||
responseCode: 200,
|
||||
message: 'success',
|
||||
content: {
|
||||
id: 'root',
|
||||
sublabels: [{ id: 'child', name: 'Finance', color: '#59BED2', sublabels: [] }],
|
||||
},
|
||||
}),
|
||||
{} as never
|
||||
)
|
||||
|
||||
expect(result.output.labels).toHaveLength(1)
|
||||
expect(result.output.labels[0].sublabels[0]).toMatchObject({ id: 'child', name: 'Finance' })
|
||||
|
||||
let deep: Record<string, unknown> = { id: 'leaf', sublabels: [] }
|
||||
for (let i = 0; i < 60; i++) deep = { id: `n${i}`, sublabels: [deep] }
|
||||
const capped = await listLabelsTool.transformResponse!(
|
||||
jsonResponse({ responseCode: 200, message: 'success', content: [deep] }),
|
||||
{} as never
|
||||
)
|
||||
|
||||
let depth = 0
|
||||
let node = capped.output.labels[0]
|
||||
while (node?.sublabels?.length) {
|
||||
node = node.sublabels[0]
|
||||
depth++
|
||||
}
|
||||
expect(depth).toBeLessThanOrEqual(32)
|
||||
})
|
||||
})
|
||||
|
||||
describe('error envelope robustness', () => {
|
||||
/**
|
||||
* Jotform quotes `responseCode` on some endpoints and not others. A
|
||||
* `typeof === 'number'` test silently skips the check on the quoted ones, turning
|
||||
* an auth failure into a successful tool result with empty output.
|
||||
*/
|
||||
it('throws on a quoted non-2xx responseCode', async () => {
|
||||
await expect(
|
||||
parseJotformResponse(
|
||||
jsonResponse({ responseCode: '401', message: 'Invalid API Key' }),
|
||||
'Jotform Get Form'
|
||||
)
|
||||
).rejects.toThrow('Jotform Get Form error (401): Invalid API Key')
|
||||
})
|
||||
|
||||
/** An upstream gateway can answer with an HTML page instead of the JSON envelope. */
|
||||
it('caps a non-JSON error body instead of inlining the whole page', async () => {
|
||||
const page = `<html>${'x'.repeat(5000)}</html>`
|
||||
await expect(
|
||||
parseJotformResponse(new Response(page, { status: 502 }), 'Jotform Get Form')
|
||||
).rejects.toThrow(/^Jotform Get Form error \(502\): .{1,320}$/s)
|
||||
})
|
||||
})
|
||||
|
||||
describe('duplicate question labels', () => {
|
||||
/**
|
||||
* Question labels are not unique — a form can carry two questions both labelled
|
||||
* "Email". Keying `values` on the label alone silently dropped all but the last,
|
||||
* handing downstream workflows a confidently wrong answer.
|
||||
*/
|
||||
it('disambiguates every occurrence of a repeated label with its question ID', () => {
|
||||
const submission = normalizeSubmission({
|
||||
id: '1',
|
||||
answers: {
|
||||
'3': { text: 'Email', type: 'control_email', answer: 'first@example.com' },
|
||||
'7': { text: 'Email', type: 'control_email', answer: 'second@example.com' },
|
||||
'9': { text: 'Message', type: 'control_textarea', answer: 'Hello' },
|
||||
},
|
||||
})
|
||||
|
||||
expect(submission.values).toEqual({
|
||||
'Email (3)': 'first@example.com',
|
||||
'Email (7)': 'second@example.com',
|
||||
Message: 'Hello',
|
||||
})
|
||||
/* Never an arbitrary winner under the bare key. */
|
||||
expect(submission.values.Email).toBeUndefined()
|
||||
})
|
||||
|
||||
it('keeps a unique label bare', () => {
|
||||
const submission = normalizeSubmission({
|
||||
id: '1',
|
||||
answers: { '3': { text: 'Email', type: 'control_email', answer: 'only@example.com' } },
|
||||
})
|
||||
|
||||
expect(submission.values).toEqual({ Email: 'only@example.com' })
|
||||
})
|
||||
|
||||
/** The id-keyed record stays complete regardless of how labels collide. */
|
||||
it('never loses an answer from the id-keyed record', () => {
|
||||
const submission = normalizeSubmission({
|
||||
id: '1',
|
||||
answers: {
|
||||
'3': { text: 'Email', type: 'control_email', answer: 'a@example.com' },
|
||||
'7': { text: 'Email', type: 'control_email', answer: 'b@example.com' },
|
||||
},
|
||||
})
|
||||
|
||||
expect(Object.keys(submission.answers)).toEqual(['3', '7'])
|
||||
expect(submission.answers['3'].answer).toBe('a@example.com')
|
||||
expect(submission.answers['7'].answer).toBe('b@example.com')
|
||||
})
|
||||
})
|
||||
|
||||
describe('duplicate label key collisions', () => {
|
||||
/**
|
||||
* Labels are free text, so the disambiguation key is not automatically safe either:
|
||||
* a question literally labelled "Email (3)" lands on the key generated for a
|
||||
* duplicate "Email" at qid 3.
|
||||
*/
|
||||
it('widens a generated key that collides with a literal label', () => {
|
||||
const submission = normalizeSubmission({
|
||||
id: '1',
|
||||
answers: {
|
||||
'3': { text: 'Email', type: 'control_email', answer: 'dup-a@example.com' },
|
||||
'5': { text: 'Email', type: 'control_email', answer: 'dup-b@example.com' },
|
||||
'9': { text: 'Email (3)', type: 'control_textbox', answer: 'literal' },
|
||||
},
|
||||
})
|
||||
|
||||
expect(Object.keys(submission.values)).toHaveLength(3)
|
||||
expect(new Set(Object.values(submission.values))).toEqual(
|
||||
new Set(['dup-a@example.com', 'dup-b@example.com', 'literal'])
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Assigning `__proto__` onto an object literal sets the prototype rather than an
|
||||
* own property, so the answer would vanish from the map entirely.
|
||||
*/
|
||||
it('keeps an answer labelled __proto__ as a real own property', () => {
|
||||
const submission = normalizeSubmission({
|
||||
id: '1',
|
||||
answers: { '3': { text: '__proto__', type: 'control_textbox', answer: 'kept' } },
|
||||
})
|
||||
|
||||
expect(Object.hasOwn(submission.values, '__proto__')).toBe(true)
|
||||
expect(Object.values(submission.values)).toContain('kept')
|
||||
})
|
||||
|
||||
/** The guarantee is a count: every rendered answer reaches the map. */
|
||||
it('never drops a rendered answer, whatever the labels are', () => {
|
||||
const submission = normalizeSubmission({
|
||||
id: '1',
|
||||
answers: {
|
||||
'1': { text: 'X', type: 'control_textbox', answer: 'a' },
|
||||
'2': { text: 'X', type: 'control_textbox', answer: 'b' },
|
||||
'3': { text: 'X (1)', type: 'control_textbox', answer: 'c' },
|
||||
'4': { text: 'X (2)', type: 'control_textbox', answer: 'd' },
|
||||
'5': { text: 'X (2) (2)', type: 'control_textbox', answer: 'e' },
|
||||
},
|
||||
})
|
||||
|
||||
expect(Object.keys(submission.values)).toHaveLength(5)
|
||||
expect(new Set(Object.values(submission.values))).toEqual(new Set(['a', 'b', 'c', 'd', 'e']))
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,81 @@
|
||||
import { normalizeFile, toList } from '@/tools/jotform/normalize'
|
||||
import type { JotformGetFormFilesParams, JotformGetFormFilesResponse } from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listFormFilesTool: ToolConfig<JotformGetFormFilesParams, JotformGetFormFilesResponse> =
|
||||
{
|
||||
id: 'jotform_list_form_files',
|
||||
name: 'Jotform List Form Files',
|
||||
description:
|
||||
'List every file uploaded through a form, with its download URL, size, type, and the submission it came from.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
formId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the form whose uploads to list',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildJotformUrl(
|
||||
params,
|
||||
`form/${encodeURIComponent(requireValue(params.formId, 'formId'))}/files`
|
||||
).toString(),
|
||||
method: 'GET',
|
||||
headers: (params) => buildJotformHeaders(params.apiKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform List Form Files')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
files: toList(envelope.content).map(normalizeFile),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
files: {
|
||||
type: 'array',
|
||||
description: 'Files uploaded through the form',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string', description: 'File name' },
|
||||
type: { type: 'string', description: 'MIME type, e.g. image/png' },
|
||||
size: { type: 'string', description: 'File size in bytes' },
|
||||
username: { type: 'string', description: 'Account that owns the form' },
|
||||
form_id: { type: 'string', description: 'Form the file was uploaded through' },
|
||||
submission_id: { type: 'string', description: 'Submission the file belongs to' },
|
||||
date: { type: 'string', description: 'Upload time, YYYY-MM-DD HH:MM:SS' },
|
||||
url: { type: 'string', description: 'Download URL for the file' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { normalizeReport, toList } from '@/tools/jotform/normalize'
|
||||
import type {
|
||||
JotformListFormReportsParams,
|
||||
JotformListReportsResponse,
|
||||
} from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listFormReportsTool: ToolConfig<
|
||||
JotformListFormReportsParams,
|
||||
JotformListReportsResponse
|
||||
> = {
|
||||
id: 'jotform_list_form_reports',
|
||||
name: 'Jotform List Form Reports',
|
||||
description: 'List the reports built from one form, each with its shareable URL.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
formId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the form whose reports to list',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildJotformUrl(
|
||||
params,
|
||||
`form/${encodeURIComponent(requireValue(params.formId, 'formId'))}/reports`
|
||||
).toString(),
|
||||
method: 'GET',
|
||||
headers: (params) => buildJotformHeaders(params.apiKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform List Form Reports')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
reports: toList(envelope.content).map(normalizeReport),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
reports: {
|
||||
type: 'array',
|
||||
description: 'Reports built from the form',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Report ID' },
|
||||
form_id: { type: 'string', description: 'Form the report is built from' },
|
||||
title: { type: 'string', description: 'Report title' },
|
||||
fields: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Comma-separated fields included in the report: ip, dt (submission date), and question IDs',
|
||||
},
|
||||
list_type: {
|
||||
type: 'string',
|
||||
description: 'Report type: excel, csv, grid, table, calendar, rss, or visual',
|
||||
},
|
||||
status: { type: 'string', description: 'ENABLED or DELETED' },
|
||||
url: { type: 'string', description: 'Shareable URL of the report' },
|
||||
isProtected: {
|
||||
type: 'boolean',
|
||||
description: 'True when the report is password protected',
|
||||
},
|
||||
settings: { type: 'string', description: 'Report display settings, as a JSON string' },
|
||||
created_at: { type: 'string', description: 'Creation time, YYYY-MM-DD HH:MM:SS' },
|
||||
updated_at: { type: 'string', description: 'Last update time, YYYY-MM-DD HH:MM:SS' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
import { normalizeSubmission, toList } from '@/tools/jotform/normalize'
|
||||
import type {
|
||||
JotformListFormSubmissionsParams,
|
||||
JotformListSubmissionsResponse,
|
||||
} from '@/tools/jotform/types'
|
||||
import {
|
||||
applyListQuery,
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listFormSubmissionsTool: ToolConfig<
|
||||
JotformListFormSubmissionsParams,
|
||||
JotformListSubmissionsResponse
|
||||
> = {
|
||||
id: 'jotform_list_form_submissions',
|
||||
name: 'Jotform List Form Submissions',
|
||||
description:
|
||||
'List the submissions received by one form, with each answer available both by question ID and by question label.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
formId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the form whose submissions to list',
|
||||
},
|
||||
offset: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Index of the first result to return. Default 0',
|
||||
},
|
||||
limit: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of submissions to return. Default 20, maximum 1000',
|
||||
},
|
||||
orderby: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Field to order by: id, form_id, IP, created_at, status, new, flag, or updated_at',
|
||||
},
|
||||
direction: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Sort direction: ASC or DESC',
|
||||
},
|
||||
filter: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Filter object, e.g. {"created_at:gt":"2024-01-01 00:00:00"}, {"new":"1"}, or {"fullText":"John Brown"}',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = buildJotformUrl(
|
||||
params,
|
||||
`form/${encodeURIComponent(requireValue(params.formId, 'formId'))}/submissions`
|
||||
)
|
||||
applyListQuery(url, params)
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => buildJotformHeaders(params.apiKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform List Form Submissions')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
submissions: toList(envelope.content).map(normalizeSubmission),
|
||||
pagination: envelope.resultSet,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
submissions: {
|
||||
type: 'array',
|
||||
description: 'Submissions received by the form',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Submission ID' },
|
||||
form_id: { type: 'string', description: 'Form the submission belongs to' },
|
||||
ip: { type: 'string', description: 'IP address of the submitter' },
|
||||
created_at: { type: 'string', description: 'Submission time, YYYY-MM-DD HH:MM:SS' },
|
||||
updated_at: { type: 'string', description: 'Last edit time, YYYY-MM-DD HH:MM:SS' },
|
||||
status: { type: 'string', description: 'ACTIVE or OVERQUOTA' },
|
||||
new: { type: 'string', description: '1 when the submission is unread' },
|
||||
workflowStatus: {
|
||||
type: 'string',
|
||||
description: 'Approval state, present only when the form feeds a workflow',
|
||||
},
|
||||
answers: {
|
||||
type: 'json',
|
||||
description:
|
||||
'Answers keyed by question ID. Each holds text (the question label), type, answer, and prettyFormat when Jotform renders one.',
|
||||
},
|
||||
values: {
|
||||
type: 'json',
|
||||
description:
|
||||
'The same answers re-keyed by question label, each rendered as a single string. A label shared by more than one question is suffixed with its question ID on every occurrence, so no answer is lost.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pagination: {
|
||||
type: 'object',
|
||||
description: 'Result window reported by the API',
|
||||
optional: true,
|
||||
properties: {
|
||||
offset: { type: 'number', description: 'Index of the first returned submission' },
|
||||
limit: { type: 'number', description: 'Page size applied' },
|
||||
count: { type: 'number', description: 'Number of submissions returned' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
import { normalizeForm, toList } from '@/tools/jotform/normalize'
|
||||
import type { JotformListFormsParams, JotformListFormsResponse } from '@/tools/jotform/types'
|
||||
import {
|
||||
applyListQuery,
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
parseJotformResponse,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listFormsTool: ToolConfig<JotformListFormsParams, JotformListFormsResponse> = {
|
||||
id: 'jotform_list_forms',
|
||||
name: 'Jotform List Forms',
|
||||
description:
|
||||
'List the forms on a Jotform account with their titles, status, and submission counts.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
offset: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Index of the first result to return. Default 0',
|
||||
},
|
||||
limit: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of forms to return. Default 20, maximum 1000',
|
||||
},
|
||||
orderby: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Field to order by: id, username, title, status, created_at, updated_at, new, count, or slug',
|
||||
},
|
||||
direction: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Sort direction: ASC or DESC',
|
||||
},
|
||||
filter: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Filter object, e.g. {"status":"ENABLED"} or {"created_at:gt":"2024-01-01 00:00:00"}',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = buildJotformUrl(params, 'user/forms')
|
||||
applyListQuery(url, params)
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => buildJotformHeaders(params.apiKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform List Forms')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
forms: toList(envelope.content).map(normalizeForm),
|
||||
pagination: envelope.resultSet,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
forms: {
|
||||
type: 'array',
|
||||
description: 'Forms on the account',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Form ID' },
|
||||
username: { type: 'string', description: 'Account that owns the form' },
|
||||
title: { type: 'string', description: 'Form title' },
|
||||
height: { type: 'string', description: 'Form height in pixels' },
|
||||
status: { type: 'string', description: 'ENABLED, DISABLED, or DELETED' },
|
||||
created_at: { type: 'string', description: 'Creation time, YYYY-MM-DD HH:MM:SS' },
|
||||
updated_at: { type: 'string', description: 'Last update time, YYYY-MM-DD HH:MM:SS' },
|
||||
last_submission: { type: 'string', description: 'Time of the most recent submission' },
|
||||
new: { type: 'string', description: 'Unread submission count' },
|
||||
count: { type: 'string', description: 'Total submission count' },
|
||||
type: { type: 'string', description: 'LEGACY or CARD' },
|
||||
favorite: { type: 'string', description: '1 when the form is favorited, otherwise 0' },
|
||||
archived: { type: 'string', description: '1 when the form is archived, otherwise 0' },
|
||||
url: { type: 'string', description: 'Public form URL' },
|
||||
},
|
||||
},
|
||||
},
|
||||
pagination: {
|
||||
type: 'object',
|
||||
description: 'Result window reported by the API',
|
||||
optional: true,
|
||||
properties: {
|
||||
offset: { type: 'number', description: 'Index of the first returned form' },
|
||||
limit: { type: 'number', description: 'Page size applied' },
|
||||
count: { type: 'number', description: 'Number of forms returned' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { normalizeLabelResource, toList } from '@/tools/jotform/normalize'
|
||||
import type {
|
||||
JotformListLabelResourcesParams,
|
||||
JotformListLabelResourcesResponse,
|
||||
} from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
trimOrUndefined,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listLabelResourcesTool: ToolConfig<
|
||||
JotformListLabelResourcesParams,
|
||||
JotformListLabelResourcesResponse
|
||||
> = {
|
||||
id: 'jotform_list_label_resources',
|
||||
name: 'Jotform List Label Resources',
|
||||
description:
|
||||
'List the assets assigned to a label — forms, workflows, sheets, and apps — with their status and titles.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
labelId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the label whose assets to list',
|
||||
},
|
||||
offset: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Index of the first result to return. Default 0',
|
||||
},
|
||||
limit: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of assets to return',
|
||||
},
|
||||
orderby: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Field to order by, e.g. created_at',
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Filter by asset status, e.g. active',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = buildJotformUrl(
|
||||
params,
|
||||
`label/${encodeURIComponent(requireValue(params.labelId, 'labelId'))}/resources`
|
||||
)
|
||||
const offset = trimOrUndefined(params.offset)
|
||||
const limit = trimOrUndefined(params.limit)
|
||||
const orderby = trimOrUndefined(params.orderby)
|
||||
const status = trimOrUndefined(params.status)
|
||||
|
||||
if (offset) url.searchParams.set('offset', offset)
|
||||
if (limit) url.searchParams.set('limit', limit)
|
||||
if (orderby) url.searchParams.set('orderby', orderby)
|
||||
if (status) url.searchParams.set('status', status)
|
||||
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => buildJotformHeaders(params.apiKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform List Label Resources')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
resources: toList(envelope.content).map(normalizeLabelResource),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
resources: {
|
||||
type: 'array',
|
||||
description: 'Assets assigned to the label',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Asset ID' },
|
||||
username: { type: 'string', description: 'Account that owns the asset' },
|
||||
title: { type: 'string', description: 'Asset title' },
|
||||
status: { type: 'string', description: 'Asset status, e.g. ENABLED or AUTODISABLED' },
|
||||
assetType: {
|
||||
type: 'string',
|
||||
description: 'Kind of asset: form, workflow, sheet, or portal',
|
||||
},
|
||||
type: { type: 'string', description: 'Asset subtype, e.g. LEGACY, APP, or default' },
|
||||
labels: { type: 'string', description: 'Label IDs the asset belongs to' },
|
||||
created_at: { type: 'string', description: 'Creation time, YYYY-MM-DD HH:MM:SS' },
|
||||
updated_at: { type: 'string', description: 'Last update time, YYYY-MM-DD HH:MM:SS' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { normalizeLabelTree } from '@/tools/jotform/normalize'
|
||||
import type { JotformListLabelsParams, JotformListLabelsResponse } from '@/tools/jotform/types'
|
||||
import { buildJotformHeaders, buildJotformUrl, parseJotformResponse } from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listLabelsTool: ToolConfig<JotformListLabelsParams, JotformListLabelsResponse> = {
|
||||
id: 'jotform_list_labels',
|
||||
name: 'Jotform List Labels',
|
||||
description:
|
||||
'List the labels on the account as a tree. Labels are how Jotform groups forms, workflows, sheets, and apps, replacing the older folder endpoints.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
addResources: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Set to "true" to include the resources assigned to each label',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = buildJotformUrl(params, 'user/labels')
|
||||
const addResources = params.addResources?.trim()
|
||||
if (addResources) url.searchParams.set('addResources', addResources)
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => buildJotformHeaders(params.apiKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform List Labels')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { labels: normalizeLabelTree(envelope.content) },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
labels: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Labels on the account. The API returns a root entry whose sublabels hold the user-visible labels, nested to arbitrary depth.',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Label ID' },
|
||||
name: { type: 'string', description: 'Label name' },
|
||||
order: { type: 'string', description: 'Position among its siblings' },
|
||||
color: { type: 'string', description: 'Label color, as a hex code' },
|
||||
owner: { type: 'string', description: 'Account that owns the label' },
|
||||
parent_label_id: { type: 'string', description: 'ID of the parent label' },
|
||||
created_at: { type: 'string', description: 'Creation time, YYYY-MM-DD HH:MM:SS' },
|
||||
updated_at: { type: 'string', description: 'Last update time, YYYY-MM-DD HH:MM:SS' },
|
||||
sublabels: { type: 'json', description: 'Nested labels beneath this one' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { normalizeQuestion, toList } from '@/tools/jotform/normalize'
|
||||
import type {
|
||||
JotformListQuestionsParams,
|
||||
JotformListQuestionsResponse,
|
||||
} from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listQuestionsTool: ToolConfig<
|
||||
JotformListQuestionsParams,
|
||||
JotformListQuestionsResponse
|
||||
> = {
|
||||
id: 'jotform_list_questions',
|
||||
name: 'Jotform List Questions',
|
||||
description:
|
||||
'List every question on a form with its question ID, label, and field type. Question IDs are what submissions are keyed by.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
formId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the form whose questions to list',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildJotformUrl(
|
||||
params,
|
||||
`form/${encodeURIComponent(requireValue(params.formId, 'formId'))}/questions`
|
||||
).toString(),
|
||||
method: 'GET',
|
||||
headers: (params) => buildJotformHeaders(params.apiKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform List Questions')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
questions: toList(envelope.content).map(normalizeQuestion),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
questions: {
|
||||
type: 'array',
|
||||
description:
|
||||
'Questions on the form. Each entry also carries the type-specific properties Jotform stores for that field, such as validation, sublabels, or options.',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
qid: { type: 'string', description: 'Question ID, used to key submission answers' },
|
||||
name: { type: 'string', description: 'Slug of the question label' },
|
||||
order: { type: 'string', description: 'Position of the question on the form' },
|
||||
text: { type: 'string', description: 'Question label' },
|
||||
type: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Field type, e.g. control_textbox, control_textarea, control_dropdown, control_fullname, control_email, control_fileupload',
|
||||
},
|
||||
required: { type: 'string', description: 'Yes when the question is required' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { normalizeReport, toList } from '@/tools/jotform/normalize'
|
||||
import type { JotformListReportsParams, JotformListReportsResponse } from '@/tools/jotform/types'
|
||||
import { buildJotformHeaders, buildJotformUrl, parseJotformResponse } from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listReportsTool: ToolConfig<JotformListReportsParams, JotformListReportsResponse> = {
|
||||
id: 'jotform_list_reports',
|
||||
name: 'Jotform List Reports',
|
||||
description:
|
||||
'List every report on the account, across all forms, with the shareable URL for each Excel, CSV, grid, table, calendar, RSS, or visual report.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => buildJotformUrl(params, 'user/reports').toString(),
|
||||
method: 'GET',
|
||||
headers: (params) => buildJotformHeaders(params.apiKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform List Reports')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
reports: toList(envelope.content).map(normalizeReport),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
reports: {
|
||||
type: 'array',
|
||||
description: 'Reports across every form on the account',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Report ID' },
|
||||
form_id: { type: 'string', description: 'Form the report is built from' },
|
||||
title: { type: 'string', description: 'Report title' },
|
||||
fields: {
|
||||
type: 'string',
|
||||
description:
|
||||
'Comma-separated fields included in the report: ip, dt (submission date), and question IDs',
|
||||
},
|
||||
list_type: {
|
||||
type: 'string',
|
||||
description: 'Report type: excel, csv, grid, table, calendar, rss, or visual',
|
||||
},
|
||||
status: { type: 'string', description: 'ENABLED or DELETED' },
|
||||
url: { type: 'string', description: 'Shareable URL of the report' },
|
||||
isProtected: {
|
||||
type: 'boolean',
|
||||
description: 'True when the report is password protected',
|
||||
},
|
||||
settings: { type: 'string', description: 'Report display settings, as a JSON string' },
|
||||
created_at: { type: 'string', description: 'Creation time, YYYY-MM-DD HH:MM:SS' },
|
||||
updated_at: { type: 'string', description: 'Last update time, YYYY-MM-DD HH:MM:SS' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { normalizeSubmission, toList } from '@/tools/jotform/normalize'
|
||||
import type { JotformListParams, JotformListSubmissionsResponse } from '@/tools/jotform/types'
|
||||
import {
|
||||
applyListQuery,
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
parseJotformResponse,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listSubmissionsTool: ToolConfig<JotformListParams, JotformListSubmissionsResponse> = {
|
||||
id: 'jotform_list_submissions',
|
||||
name: 'Jotform List Submissions',
|
||||
description:
|
||||
'List submissions across every form on the account, optionally narrowed to specific forms or a date range.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
offset: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Index of the first result to return. Default 0',
|
||||
},
|
||||
limit: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Number of submissions to return. Default 20, maximum 1000',
|
||||
},
|
||||
orderby: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Field to order by: id, form_id, IP, created_at, status, new, flag, or updated_at',
|
||||
},
|
||||
direction: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'Sort direction: ASC or DESC',
|
||||
},
|
||||
filter: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Filter object, e.g. {"formIDs":["231234567890"]}, {"created_at:gt":"2024-01-01 00:00:00"}, or {"fullText":"John Brown"}',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
const url = buildJotformUrl(params, 'user/submissions')
|
||||
applyListQuery(url, params)
|
||||
return url.toString()
|
||||
},
|
||||
method: 'GET',
|
||||
headers: (params) => buildJotformHeaders(params.apiKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform List Submissions')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
submissions: toList(envelope.content).map(normalizeSubmission),
|
||||
pagination: envelope.resultSet,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
submissions: {
|
||||
type: 'array',
|
||||
description: 'Submissions across every form on the account',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Submission ID' },
|
||||
form_id: { type: 'string', description: 'Form the submission belongs to' },
|
||||
ip: { type: 'string', description: 'IP address of the submitter' },
|
||||
created_at: { type: 'string', description: 'Submission time, YYYY-MM-DD HH:MM:SS' },
|
||||
updated_at: { type: 'string', description: 'Last edit time, YYYY-MM-DD HH:MM:SS' },
|
||||
status: { type: 'string', description: 'ACTIVE or OVERQUOTA' },
|
||||
new: { type: 'string', description: '1 when the submission is unread' },
|
||||
workflowStatus: {
|
||||
type: 'string',
|
||||
description: 'Approval state, present only when the form feeds a workflow',
|
||||
},
|
||||
answers: {
|
||||
type: 'json',
|
||||
description:
|
||||
'Answers keyed by question ID. Each holds text (the question label), type, answer, and prettyFormat when Jotform renders one.',
|
||||
},
|
||||
values: {
|
||||
type: 'json',
|
||||
description:
|
||||
'The same answers re-keyed by question label, each rendered as a single string. A label shared by more than one question is suffixed with its question ID on every occurrence, so no answer is lost.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
pagination: {
|
||||
type: 'object',
|
||||
description: 'Result window reported by the API',
|
||||
optional: true,
|
||||
properties: {
|
||||
offset: { type: 'number', description: 'Index of the first returned submission' },
|
||||
limit: { type: 'number', description: 'Page size applied' },
|
||||
count: { type: 'number', description: 'Number of submissions returned' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { normalizeSubUser, toList } from '@/tools/jotform/normalize'
|
||||
import type { JotformListSubUsersParams, JotformListSubUsersResponse } from '@/tools/jotform/types'
|
||||
import { buildJotformHeaders, buildJotformUrl, parseJotformResponse } from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listSubUsersTool: ToolConfig<JotformListSubUsersParams, JotformListSubUsersResponse> =
|
||||
{
|
||||
id: 'jotform_list_subusers',
|
||||
name: 'Jotform List Sub-Users',
|
||||
description:
|
||||
'List the sub-users on the account with the forms and folders each one can reach, and at what access level.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => buildJotformUrl(params, 'user/subusers').toString(),
|
||||
method: 'GET',
|
||||
headers: (params) => buildJotformHeaders(params.apiKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform List Sub-Users')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
subusers: toList(envelope.content).map(normalizeSubUser),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
subusers: {
|
||||
type: 'array',
|
||||
description: 'Sub-users on the account',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
username: { type: 'string', description: 'Sub-user username' },
|
||||
email: { type: 'string', description: 'Sub-user email address' },
|
||||
owner: { type: 'string', description: 'Parent account that created the sub-user' },
|
||||
status: {
|
||||
type: 'string',
|
||||
description: 'LIVE, DELETED, or PENDING while an invitation is unaccepted',
|
||||
},
|
||||
created_at: { type: 'string', description: 'Creation time, YYYY-MM-DD HH:MM:SS' },
|
||||
permissions: {
|
||||
type: 'json',
|
||||
description:
|
||||
'What the sub-user can reach: each entry has type (FORM, FOLDER, or ALL), resource_id, access_type (full or readOnly), and title',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { normalizeWebhooks } from '@/tools/jotform/normalize'
|
||||
import type { JotformListWebhooksParams, JotformWebhooksResponse } from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const listWebhooksTool: ToolConfig<JotformListWebhooksParams, JotformWebhooksResponse> = {
|
||||
id: 'jotform_list_webhooks',
|
||||
name: 'Jotform List Webhooks',
|
||||
description:
|
||||
'List the webhooks registered on a form. The returned IDs are what the Delete Webhook operation takes.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
formId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the form whose webhooks to list',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildJotformUrl(
|
||||
params,
|
||||
`form/${encodeURIComponent(requireValue(params.formId, 'formId'))}/webhooks`
|
||||
).toString(),
|
||||
method: 'GET',
|
||||
headers: (params) => buildJotformHeaders(params.apiKey),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform List Webhooks')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { webhooks: normalizeWebhooks(envelope.content) },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
webhooks: {
|
||||
type: 'array',
|
||||
description: 'Webhooks registered on the form',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Webhook ID, used when deleting the webhook' },
|
||||
url: { type: 'string', description: 'URL that receives submission notifications' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
import type {
|
||||
JotformFile,
|
||||
JotformForm,
|
||||
JotformLabel,
|
||||
JotformLabelNode,
|
||||
JotformLabelResource,
|
||||
JotformQuestion,
|
||||
JotformReport,
|
||||
JotformSubmission,
|
||||
JotformSubmissionAnswer,
|
||||
JotformSubUser,
|
||||
JotformUser,
|
||||
} from '@/tools/jotform/types'
|
||||
import { isRecord, toJsonArray, toStringOrNull } from '@/tools/jotform/utils'
|
||||
|
||||
/**
|
||||
* Jotform returns every scalar as a string and documents `content` as either an
|
||||
* object or a single-element array depending on the endpoint, so each resource is
|
||||
* projected through one normalizer rather than mapped inline per tool.
|
||||
*/
|
||||
|
||||
/** Unwraps the single-element array form some endpoints document for one resource. */
|
||||
export function unwrapSingle(content: unknown): Record<string, unknown> | null {
|
||||
if (Array.isArray(content)) return isRecord(content[0]) ? content[0] : null
|
||||
return isRecord(content) ? content : null
|
||||
}
|
||||
|
||||
export function toList(content: unknown): Record<string, unknown>[] {
|
||||
if (Array.isArray(content)) return content.filter(isRecord)
|
||||
/* Folder `forms` and question maps come back keyed by id rather than as arrays. */
|
||||
if (isRecord(content)) return Object.values(content).filter(isRecord)
|
||||
return []
|
||||
}
|
||||
|
||||
export function normalizeForm(raw: Record<string, unknown>): JotformForm {
|
||||
return {
|
||||
id: toStringOrNull(raw.id),
|
||||
username: toStringOrNull(raw.username),
|
||||
title: toStringOrNull(raw.title),
|
||||
height: toStringOrNull(raw.height),
|
||||
status: toStringOrNull(raw.status),
|
||||
created_at: toStringOrNull(raw.created_at),
|
||||
updated_at: toStringOrNull(raw.updated_at),
|
||||
last_submission: toStringOrNull(raw.last_submission),
|
||||
new: toStringOrNull(raw.new),
|
||||
count: toStringOrNull(raw.count),
|
||||
type: toStringOrNull(raw.type),
|
||||
favorite: toStringOrNull(raw.favorite),
|
||||
archived: toStringOrNull(raw.archived),
|
||||
url: toStringOrNull(raw.url),
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeQuestion(raw: Record<string, unknown>): JotformQuestion {
|
||||
return {
|
||||
...raw,
|
||||
qid: toStringOrNull(raw.qid),
|
||||
name: toStringOrNull(raw.name),
|
||||
order: toStringOrNull(raw.order),
|
||||
text: toStringOrNull(raw.text),
|
||||
type: toStringOrNull(raw.type),
|
||||
required: toStringOrNull(raw.required),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeAnswer(raw: Record<string, unknown>): JotformSubmissionAnswer {
|
||||
return {
|
||||
text: toStringOrNull(raw.text),
|
||||
type: toStringOrNull(raw.type),
|
||||
answer: raw.answer ?? null,
|
||||
prettyFormat: toStringOrNull(raw.prettyFormat),
|
||||
}
|
||||
}
|
||||
|
||||
/** Renders one answer as the single string `values` holds. */
|
||||
function renderAnswer(answer: JotformSubmissionAnswer): string | null {
|
||||
if (answer.prettyFormat !== null) return answer.prettyFormat
|
||||
|
||||
const raw = answer.answer
|
||||
if (raw === null || raw === undefined) return null
|
||||
if (typeof raw === 'string' || typeof raw === 'number' || typeof raw === 'boolean') {
|
||||
return String(raw)
|
||||
}
|
||||
return JSON.stringify(raw)
|
||||
}
|
||||
|
||||
/**
|
||||
* Answers are keyed by question id, which is useless downstream without the form's
|
||||
* question list. `values` re-keys them by question label with the answer rendered as
|
||||
* text — `prettyFormat` when Jotform supplies one, the raw scalar otherwise — so a
|
||||
* submission can be read without a second call.
|
||||
*
|
||||
* Labels are not unique: a form can carry two questions both labelled "Email", and
|
||||
* keying on the label alone would silently drop all but the last. So a label is used
|
||||
* bare only when it appears once on the submission; every occurrence of a repeated
|
||||
* label is suffixed with its question id instead. Disambiguating *every* occurrence
|
||||
* rather than only the later ones keeps the result independent of answer order — and
|
||||
* makes a newly duplicated label read as absent rather than as an arbitrary winner.
|
||||
*
|
||||
* Labels are also free text, so a generated key is not automatically safe either: a
|
||||
* question literally labelled "Email (3)" collides with the key generated for a
|
||||
* duplicate "Email" at qid 3. Any key already taken is therefore widened again until
|
||||
* it is free, which makes "no answer is dropped" hold for every input rather than for
|
||||
* the labels that happen to be well behaved. `answers` remains the complete, id-keyed
|
||||
* record regardless.
|
||||
*/
|
||||
function buildValues(answers: Record<string, JotformSubmissionAnswer>): Record<string, string> {
|
||||
const labelCounts = new Map<string, number>()
|
||||
for (const answer of Object.values(answers)) {
|
||||
if (!answer.text) continue
|
||||
labelCounts.set(answer.text, (labelCounts.get(answer.text) ?? 0) + 1)
|
||||
}
|
||||
|
||||
/* Accumulated in a Map, not an object literal: a question labelled `__proto__`
|
||||
assigned onto `{}` sets the prototype instead of an own property and vanishes.
|
||||
`Object.fromEntries` defines it as an own property. */
|
||||
const values = new Map<string, string>()
|
||||
for (const [qid, answer] of Object.entries(answers)) {
|
||||
if (!answer.text) continue
|
||||
const rendered = renderAnswer(answer)
|
||||
if (rendered === null) continue
|
||||
|
||||
let key = (labelCounts.get(answer.text) ?? 0) > 1 ? `${answer.text} (${qid})` : answer.text
|
||||
while (values.has(key)) key = `${key} (${qid})`
|
||||
values.set(key, rendered)
|
||||
}
|
||||
return Object.fromEntries(values)
|
||||
}
|
||||
|
||||
export function normalizeSubmission(raw: Record<string, unknown>): JotformSubmission {
|
||||
const answers: Record<string, JotformSubmissionAnswer> = {}
|
||||
if (isRecord(raw.answers)) {
|
||||
for (const [qid, answer] of Object.entries(raw.answers)) {
|
||||
if (isRecord(answer)) answers[qid] = normalizeAnswer(answer)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: toStringOrNull(raw.id),
|
||||
form_id: toStringOrNull(raw.form_id),
|
||||
ip: toStringOrNull(raw.ip),
|
||||
created_at: toStringOrNull(raw.created_at),
|
||||
updated_at: toStringOrNull(raw.updated_at),
|
||||
status: toStringOrNull(raw.status),
|
||||
new: toStringOrNull(raw.new),
|
||||
workflowStatus: toStringOrNull(raw.workflowStatus),
|
||||
answers,
|
||||
values: buildValues(answers),
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeReport(raw: Record<string, unknown>): JotformReport {
|
||||
return {
|
||||
id: toStringOrNull(raw.id),
|
||||
form_id: toStringOrNull(raw.form_id),
|
||||
title: toStringOrNull(raw.title),
|
||||
fields: toStringOrNull(raw.fields),
|
||||
list_type: toStringOrNull(raw.list_type),
|
||||
status: toStringOrNull(raw.status),
|
||||
url: toStringOrNull(raw.url),
|
||||
isProtected: typeof raw.isProtected === 'boolean' ? raw.isProtected : null,
|
||||
settings: toStringOrNull(raw.settings),
|
||||
created_at: toStringOrNull(raw.created_at),
|
||||
updated_at: toStringOrNull(raw.updated_at),
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeFile(raw: Record<string, unknown>): JotformFile {
|
||||
return {
|
||||
name: toStringOrNull(raw.name),
|
||||
type: toStringOrNull(raw.type),
|
||||
size: toStringOrNull(raw.size),
|
||||
username: toStringOrNull(raw.username),
|
||||
form_id: toStringOrNull(raw.form_id),
|
||||
submission_id: toStringOrNull(raw.submission_id),
|
||||
date: toStringOrNull(raw.date),
|
||||
url: toStringOrNull(raw.url),
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeUser(raw: Record<string, unknown>): JotformUser {
|
||||
return {
|
||||
username: toStringOrNull(raw.username),
|
||||
name: toStringOrNull(raw.name),
|
||||
email: toStringOrNull(raw.email),
|
||||
website: toStringOrNull(raw.website),
|
||||
time_zone: toStringOrNull(raw.time_zone),
|
||||
account_type: toStringOrNull(raw.account_type),
|
||||
status: toStringOrNull(raw.status),
|
||||
created_at: toStringOrNull(raw.created_at),
|
||||
updated_at: toStringOrNull(raw.updated_at),
|
||||
is_verified: toStringOrNull(raw.is_verified),
|
||||
industry: toStringOrNull(raw.industry),
|
||||
company: toStringOrNull(raw.company),
|
||||
language: toStringOrNull(raw.language),
|
||||
avatarUrl: toStringOrNull(raw.avatarUrl),
|
||||
usage: toStringOrNull(raw.usage),
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeSubUser(raw: Record<string, unknown>): JotformSubUser {
|
||||
const permissions = Array.isArray(raw.permissions)
|
||||
? raw.permissions.filter(isRecord).map((permission) => ({
|
||||
type: toStringOrNull(permission.type),
|
||||
resource_id: toStringOrNull(permission.resource_id),
|
||||
access_type: toStringOrNull(permission.access_type),
|
||||
title: toStringOrNull(permission.title),
|
||||
}))
|
||||
: []
|
||||
|
||||
return {
|
||||
username: toStringOrNull(raw.username),
|
||||
email: toStringOrNull(raw.email),
|
||||
owner: toStringOrNull(raw.owner),
|
||||
status: toStringOrNull(raw.status),
|
||||
created_at: toStringOrNull(raw.created_at),
|
||||
permissions,
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeLabel(raw: Record<string, unknown>): JotformLabel {
|
||||
return {
|
||||
id: toStringOrNull(raw.id),
|
||||
name: toStringOrNull(raw.name),
|
||||
order: toStringOrNull(raw.order),
|
||||
color: toStringOrNull(raw.color),
|
||||
owner: toStringOrNull(raw.owner),
|
||||
ownerType: toStringOrNull(raw.ownerType) ?? toStringOrNull(raw.owner_type),
|
||||
parent_label_id: toStringOrNull(raw.parent_label_id),
|
||||
created_at: toStringOrNull(raw.created_at),
|
||||
updated_at: toStringOrNull(raw.updated_at),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `GET /user/labels` answers with a root label whose `sublabels` nest to arbitrary
|
||||
* depth. The tree is preserved rather than flattened, because a label reads
|
||||
* differently depending on where it sits, but the recursion is depth-capped so a
|
||||
* cyclic or pathological payload cannot exhaust the stack.
|
||||
*/
|
||||
const MAX_LABEL_DEPTH = 32
|
||||
|
||||
/**
|
||||
* A label payload is one node when it is an object and a sibling list when it is an
|
||||
* array. `toList` cannot serve here: it reads a bare object as an id-keyed map, which
|
||||
* turns the documented single root label into an empty list.
|
||||
*/
|
||||
function toLabelNodes(content: unknown): Record<string, unknown>[] {
|
||||
if (Array.isArray(content)) return content.filter(isRecord)
|
||||
return isRecord(content) ? [content] : []
|
||||
}
|
||||
|
||||
export function normalizeLabelTree(content: unknown, depth = 0): JotformLabelNode[] {
|
||||
if (depth >= MAX_LABEL_DEPTH) return []
|
||||
|
||||
return toLabelNodes(content).map((raw) => ({
|
||||
...normalizeLabel(raw),
|
||||
sublabels: normalizeLabelTree(raw.sublabels, depth + 1),
|
||||
}))
|
||||
}
|
||||
|
||||
export function normalizeLabelResource(raw: Record<string, unknown>): JotformLabelResource {
|
||||
return {
|
||||
id: toStringOrNull(raw.id),
|
||||
username: toStringOrNull(raw.username),
|
||||
title: toStringOrNull(raw.title),
|
||||
status: toStringOrNull(raw.status),
|
||||
assetType: toStringOrNull(raw.assetType),
|
||||
type: toStringOrNull(raw.type),
|
||||
labels: toStringOrNull(raw.labels),
|
||||
created_at: toStringOrNull(raw.created_at),
|
||||
updated_at: toStringOrNull(raw.updated_at),
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeLabelResourceRefs(
|
||||
content: unknown
|
||||
): Array<{ id: string | null; type: string | null }> {
|
||||
return toList(content).map((raw) => ({
|
||||
id: toStringOrNull(raw.id),
|
||||
type: toStringOrNull(raw.type),
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* The add/remove endpoints take `[{id, type}]` with a lowercase type, while the
|
||||
* response echoes the type uppercase. Callers copy either casing out of the docs, so
|
||||
* the request side is normalized down rather than passed through.
|
||||
*/
|
||||
export function toLabelResourcePayload(
|
||||
value: unknown[] | string | undefined
|
||||
): Array<{ id: string; type: string }> {
|
||||
const entries = toJsonArray(value, 'resources')
|
||||
if (entries.length === 0) {
|
||||
throw new Error('resources must contain at least one asset.')
|
||||
}
|
||||
|
||||
return entries.map((entry) => {
|
||||
if (!isRecord(entry)) {
|
||||
throw new Error('Every entry in resources must be a JSON object with an id and a type.')
|
||||
}
|
||||
const id = toStringOrNull(entry.id)
|
||||
const type = toStringOrNull(entry.type)
|
||||
if (!id || !type) {
|
||||
throw new Error('Every entry in resources needs both an id and a type.')
|
||||
}
|
||||
return { id, type: type.toLowerCase() }
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Webhook lists come back as an id-keyed map, and the key IS the webhook id used by
|
||||
* `DELETE /form/{id}/webhooks/{whid}` — a bare URL array would throw that away.
|
||||
*/
|
||||
export function normalizeWebhooks(content: unknown): Array<{ id: string; url: string }> {
|
||||
if (Array.isArray(content)) {
|
||||
return content
|
||||
.map((url, index) => ({ id: String(index), url: toStringOrNull(url) }))
|
||||
.filter((entry): entry is { id: string; url: string } => entry.url !== null)
|
||||
}
|
||||
if (!isRecord(content)) return []
|
||||
return Object.entries(content)
|
||||
.map(([id, url]) => ({ id, url: toStringOrNull(url) }))
|
||||
.filter((entry): entry is { id: string; url: string } => entry.url !== null)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { normalizeLabelResourceRefs, toLabelResourcePayload } from '@/tools/jotform/normalize'
|
||||
import type {
|
||||
JotformLabelResourceRefsResponse,
|
||||
JotformLabelResourcesParams,
|
||||
} from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const removeLabelResourcesTool: ToolConfig<
|
||||
JotformLabelResourcesParams,
|
||||
JotformLabelResourceRefsResponse
|
||||
> = {
|
||||
id: 'jotform_remove_label_resources',
|
||||
name: 'Jotform Remove Label Resources',
|
||||
description: 'Unassign forms, workflows, sheets, or apps from a Jotform label.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
labelId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the label to remove the assets from',
|
||||
},
|
||||
resources: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Assets to remove, each with an id and a type of form, workflow, sheet, or portal, e.g. [{"id":"251464995493876","type":"form"}]',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildJotformUrl(
|
||||
params,
|
||||
`label/${encodeURIComponent(requireValue(params.labelId, 'labelId'))}/remove-resources`
|
||||
).toString(),
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
...buildJotformHeaders(params.apiKey),
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => ({ resources: toLabelResourcePayload(params.resources) }),
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Remove Label Resources')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { resources: normalizeLabelResourceRefs(envelope.content) },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
resources: {
|
||||
type: 'array',
|
||||
description: 'The assets reported by the API after the removal',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string', description: 'Asset ID' },
|
||||
type: {
|
||||
type: 'string',
|
||||
description: 'Asset kind, echoed uppercase: FORM, WORKFLOW, SHEET, or PORTAL',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
/**
|
||||
* Jotform returns every scalar as a string, including counts and boolean-ish flags
|
||||
* such as `favorite` and `archived`, so the projected shapes keep them as strings
|
||||
* rather than coercing to types the API never promised.
|
||||
*/
|
||||
|
||||
/** Credentials and host selection every Jotform tool takes. */
|
||||
export interface JotformAuthParams {
|
||||
apiKey: string
|
||||
region?: string
|
||||
}
|
||||
|
||||
export interface JotformForm {
|
||||
id: string | null
|
||||
username: string | null
|
||||
title: string | null
|
||||
height: string | null
|
||||
status: string | null
|
||||
created_at: string | null
|
||||
updated_at: string | null
|
||||
last_submission: string | null
|
||||
new: string | null
|
||||
count: string | null
|
||||
type: string | null
|
||||
favorite: string | null
|
||||
archived: string | null
|
||||
url: string | null
|
||||
}
|
||||
|
||||
/** Question properties vary by field type, so the documented keys are widened. */
|
||||
export interface JotformQuestion extends Record<string, unknown> {
|
||||
qid: string | null
|
||||
name: string | null
|
||||
order: string | null
|
||||
text: string | null
|
||||
type: string | null
|
||||
required: string | null
|
||||
}
|
||||
|
||||
export interface JotformSubmissionAnswer {
|
||||
text: string | null
|
||||
type: string | null
|
||||
answer: unknown
|
||||
prettyFormat: string | null
|
||||
}
|
||||
|
||||
export interface JotformSubmission {
|
||||
id: string | null
|
||||
form_id: string | null
|
||||
ip: string | null
|
||||
created_at: string | null
|
||||
updated_at: string | null
|
||||
status: string | null
|
||||
new: string | null
|
||||
workflowStatus: string | null
|
||||
answers: Record<string, JotformSubmissionAnswer>
|
||||
/**
|
||||
* Answers re-keyed by question label with each value rendered as text. Labels are
|
||||
* not unique, so every occurrence of a repeated label is suffixed with its question
|
||||
* id rather than overwriting.
|
||||
*/
|
||||
values: Record<string, string>
|
||||
}
|
||||
|
||||
export interface JotformReport {
|
||||
id: string | null
|
||||
form_id: string | null
|
||||
title: string | null
|
||||
fields: string | null
|
||||
list_type: string | null
|
||||
status: string | null
|
||||
url: string | null
|
||||
isProtected: boolean | null
|
||||
settings: string | null
|
||||
created_at: string | null
|
||||
updated_at: string | null
|
||||
}
|
||||
|
||||
export interface JotformFile {
|
||||
name: string | null
|
||||
type: string | null
|
||||
size: string | null
|
||||
username: string | null
|
||||
form_id: string | null
|
||||
submission_id: string | null
|
||||
date: string | null
|
||||
url: string | null
|
||||
}
|
||||
|
||||
export interface JotformPagination {
|
||||
offset: number | null
|
||||
limit: number | null
|
||||
count: number | null
|
||||
}
|
||||
|
||||
export interface JotformListParams extends JotformAuthParams {
|
||||
offset?: string
|
||||
limit?: string
|
||||
orderby?: string
|
||||
direction?: string
|
||||
filter?: Record<string, unknown> | string
|
||||
}
|
||||
|
||||
export type JotformListFormsParams = JotformListParams
|
||||
|
||||
export interface JotformListFormsResponse extends ToolResponse {
|
||||
output: {
|
||||
forms: JotformForm[]
|
||||
pagination: JotformPagination | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface JotformGetFormParams extends JotformAuthParams {
|
||||
formId: string
|
||||
}
|
||||
|
||||
export interface JotformFormResponse extends ToolResponse {
|
||||
output: {
|
||||
form: JotformForm
|
||||
}
|
||||
}
|
||||
|
||||
export interface JotformCreateFormParams extends JotformAuthParams {
|
||||
questions?: unknown[] | string
|
||||
properties?: Record<string, unknown> | string
|
||||
emails?: unknown[] | string
|
||||
}
|
||||
|
||||
export interface JotformDeleteFormParams extends JotformAuthParams {
|
||||
formId: string
|
||||
}
|
||||
|
||||
export interface JotformDeleteFormResponse extends ToolResponse {
|
||||
output: {
|
||||
form: JotformForm
|
||||
}
|
||||
}
|
||||
|
||||
export interface JotformCloneFormParams extends JotformAuthParams {
|
||||
formId: string
|
||||
}
|
||||
|
||||
export interface JotformGetFormPropertiesParams extends JotformAuthParams {
|
||||
formId: string
|
||||
propertyKey?: string
|
||||
}
|
||||
|
||||
export interface JotformFormPropertiesResponse extends ToolResponse {
|
||||
output: {
|
||||
properties: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
export interface JotformUpdateFormPropertiesParams extends JotformAuthParams {
|
||||
formId: string
|
||||
properties: Record<string, unknown> | string
|
||||
}
|
||||
|
||||
export interface JotformGetFormFilesParams extends JotformAuthParams {
|
||||
formId: string
|
||||
}
|
||||
|
||||
export interface JotformGetFormFilesResponse extends ToolResponse {
|
||||
output: {
|
||||
files: JotformFile[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface JotformListQuestionsParams extends JotformAuthParams {
|
||||
formId: string
|
||||
}
|
||||
|
||||
export interface JotformListQuestionsResponse extends ToolResponse {
|
||||
output: {
|
||||
questions: JotformQuestion[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface JotformGetQuestionParams extends JotformAuthParams {
|
||||
formId: string
|
||||
questionId: string
|
||||
}
|
||||
|
||||
export interface JotformQuestionResponse extends ToolResponse {
|
||||
output: {
|
||||
question: JotformQuestion
|
||||
}
|
||||
}
|
||||
|
||||
export interface JotformCreateQuestionParams extends JotformAuthParams {
|
||||
formId: string
|
||||
questionType: string
|
||||
text?: string
|
||||
order?: string
|
||||
name?: string
|
||||
questionProperties?: Record<string, unknown> | string
|
||||
}
|
||||
|
||||
export interface JotformUpdateQuestionParams extends JotformAuthParams {
|
||||
formId: string
|
||||
questionId: string
|
||||
text?: string
|
||||
order?: string
|
||||
name?: string
|
||||
questionProperties?: Record<string, unknown> | string
|
||||
}
|
||||
|
||||
export interface JotformUpdateQuestionResponse extends ToolResponse {
|
||||
output: {
|
||||
question: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
export interface JotformDeleteQuestionParams extends JotformAuthParams {
|
||||
formId: string
|
||||
questionId: string
|
||||
}
|
||||
|
||||
export interface JotformMessageResponse extends ToolResponse {
|
||||
output: {
|
||||
deleted: boolean
|
||||
message: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface JotformListFormSubmissionsParams extends JotformListParams {
|
||||
formId: string
|
||||
}
|
||||
|
||||
export interface JotformListSubmissionsResponse extends ToolResponse {
|
||||
output: {
|
||||
submissions: JotformSubmission[]
|
||||
pagination: JotformPagination | null
|
||||
}
|
||||
}
|
||||
|
||||
export interface JotformGetSubmissionParams extends JotformAuthParams {
|
||||
submissionId: string
|
||||
}
|
||||
|
||||
export interface JotformDeleteSubmissionParams extends JotformAuthParams {
|
||||
submissionId: string
|
||||
}
|
||||
|
||||
export interface JotformSubmissionResponse extends ToolResponse {
|
||||
output: {
|
||||
submission: JotformSubmission
|
||||
}
|
||||
}
|
||||
|
||||
export interface JotformCreateSubmissionParams extends JotformAuthParams {
|
||||
formId: string
|
||||
answers: Record<string, unknown> | string
|
||||
}
|
||||
|
||||
export interface JotformUpdateSubmissionParams extends JotformAuthParams {
|
||||
submissionId: string
|
||||
answers: Record<string, unknown> | string
|
||||
}
|
||||
|
||||
export interface JotformSubmissionRefResponse extends ToolResponse {
|
||||
output: {
|
||||
submissionId: string | null
|
||||
url: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export type JotformListReportsParams = JotformAuthParams
|
||||
|
||||
export interface JotformListFormReportsParams extends JotformAuthParams {
|
||||
formId: string
|
||||
}
|
||||
|
||||
export interface JotformListReportsResponse extends ToolResponse {
|
||||
output: {
|
||||
reports: JotformReport[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface JotformCreateReportParams extends JotformAuthParams {
|
||||
formId: string
|
||||
title: string
|
||||
listType: string
|
||||
fields?: string
|
||||
}
|
||||
|
||||
export interface JotformGetReportParams extends JotformAuthParams {
|
||||
reportId: string
|
||||
}
|
||||
|
||||
export interface JotformReportResponse extends ToolResponse {
|
||||
output: {
|
||||
report: JotformReport
|
||||
}
|
||||
}
|
||||
|
||||
export interface JotformDeleteReportParams extends JotformAuthParams {
|
||||
reportId: string
|
||||
}
|
||||
|
||||
export interface JotformListWebhooksParams extends JotformAuthParams {
|
||||
formId: string
|
||||
}
|
||||
|
||||
export interface JotformWebhooksResponse extends ToolResponse {
|
||||
output: {
|
||||
webhooks: Array<{ id: string; url: string }>
|
||||
}
|
||||
}
|
||||
|
||||
export interface JotformCreateWebhookParams extends JotformAuthParams {
|
||||
formId: string
|
||||
webhookUrl: string
|
||||
}
|
||||
|
||||
export interface JotformDeleteWebhookParams extends JotformAuthParams {
|
||||
formId: string
|
||||
webhookId: string
|
||||
}
|
||||
|
||||
export type JotformGetUserParams = JotformAuthParams
|
||||
|
||||
export interface JotformUser {
|
||||
username: string | null
|
||||
name: string | null
|
||||
email: string | null
|
||||
website: string | null
|
||||
time_zone: string | null
|
||||
account_type: string | null
|
||||
status: string | null
|
||||
created_at: string | null
|
||||
updated_at: string | null
|
||||
is_verified: string | null
|
||||
industry: string | null
|
||||
company: string | null
|
||||
language: string | null
|
||||
avatarUrl: string | null
|
||||
usage: string | null
|
||||
}
|
||||
|
||||
export interface JotformGetUserResponse extends ToolResponse {
|
||||
output: {
|
||||
user: JotformUser
|
||||
}
|
||||
}
|
||||
|
||||
export type JotformGetUsageParams = JotformAuthParams
|
||||
|
||||
export interface JotformGetUsageResponse extends ToolResponse {
|
||||
output: {
|
||||
usage: {
|
||||
submissions: string | null
|
||||
ssl_submissions: string | null
|
||||
payments: string | null
|
||||
uploads: string | null
|
||||
mobile_submissions: string | null
|
||||
views: string | null
|
||||
api: string | null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface JotformGetHistoryParams extends JotformAuthParams {
|
||||
action?: string
|
||||
date?: string
|
||||
sortBy?: string
|
||||
startDate?: string
|
||||
endDate?: string
|
||||
}
|
||||
|
||||
export interface JotformHistoryEntry {
|
||||
type: string | null
|
||||
formID: string | null
|
||||
username: string | null
|
||||
formTitle: string | null
|
||||
formStatus: string | null
|
||||
ip: string | null
|
||||
timestamp: string | null
|
||||
}
|
||||
|
||||
export interface JotformGetHistoryResponse extends ToolResponse {
|
||||
output: {
|
||||
history: JotformHistoryEntry[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface JotformCreateSubmissionsParams extends JotformAuthParams {
|
||||
formId: string
|
||||
submissions: unknown[] | string
|
||||
}
|
||||
|
||||
export interface JotformCreateSubmissionsResponse extends ToolResponse {
|
||||
output: {
|
||||
submissions: Array<{ submissionId: string | null; url: string | null }>
|
||||
count: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface JotformCreateQuestionsParams extends JotformAuthParams {
|
||||
formId: string
|
||||
questions: unknown[] | string
|
||||
}
|
||||
|
||||
export interface JotformSubUserPermission {
|
||||
type: string | null
|
||||
resource_id: string | null
|
||||
access_type: string | null
|
||||
title: string | null
|
||||
}
|
||||
|
||||
export interface JotformSubUser {
|
||||
username: string | null
|
||||
email: string | null
|
||||
owner: string | null
|
||||
status: string | null
|
||||
created_at: string | null
|
||||
permissions: JotformSubUserPermission[]
|
||||
}
|
||||
|
||||
export type JotformListSubUsersParams = JotformAuthParams
|
||||
|
||||
export interface JotformListSubUsersResponse extends ToolResponse {
|
||||
output: {
|
||||
subusers: JotformSubUser[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface JotformGetSettingsParams extends JotformAuthParams {
|
||||
settingsKey?: string
|
||||
}
|
||||
|
||||
export interface JotformUpdateSettingsParams extends JotformAuthParams {
|
||||
settingsName?: string
|
||||
email?: string
|
||||
website?: string
|
||||
timeZone?: string
|
||||
company?: string
|
||||
industry?: string
|
||||
}
|
||||
|
||||
export interface JotformUserResponse extends ToolResponse {
|
||||
output: {
|
||||
user: JotformUser
|
||||
}
|
||||
}
|
||||
|
||||
export interface JotformLabel {
|
||||
id: string | null
|
||||
name: string | null
|
||||
order: string | null
|
||||
color: string | null
|
||||
owner: string | null
|
||||
ownerType: string | null
|
||||
parent_label_id: string | null
|
||||
created_at: string | null
|
||||
updated_at: string | null
|
||||
}
|
||||
|
||||
/** A label plus the labels nested beneath it, as `GET /user/labels` returns them. */
|
||||
export interface JotformLabelNode extends JotformLabel {
|
||||
sublabels: JotformLabelNode[]
|
||||
}
|
||||
|
||||
export interface JotformLabelResource {
|
||||
id: string | null
|
||||
username: string | null
|
||||
title: string | null
|
||||
status: string | null
|
||||
assetType: string | null
|
||||
type: string | null
|
||||
labels: string | null
|
||||
created_at: string | null
|
||||
updated_at: string | null
|
||||
}
|
||||
|
||||
export interface JotformListLabelsParams extends JotformAuthParams {
|
||||
addResources?: string
|
||||
}
|
||||
|
||||
export interface JotformListLabelsResponse extends ToolResponse {
|
||||
output: {
|
||||
labels: JotformLabelNode[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface JotformGetLabelParams extends JotformAuthParams {
|
||||
labelId: string
|
||||
}
|
||||
|
||||
export interface JotformLabelResponse extends ToolResponse {
|
||||
output: {
|
||||
label: JotformLabel
|
||||
}
|
||||
}
|
||||
|
||||
export interface JotformCreateLabelParams extends JotformAuthParams {
|
||||
labelName: string
|
||||
color?: string
|
||||
parent?: string
|
||||
}
|
||||
|
||||
export interface JotformUpdateLabelParams extends JotformAuthParams {
|
||||
labelId: string
|
||||
labelName?: string
|
||||
color?: string
|
||||
}
|
||||
|
||||
export interface JotformUpdateLabelResponse extends ToolResponse {
|
||||
output: {
|
||||
label: Record<string, unknown>
|
||||
}
|
||||
}
|
||||
|
||||
export interface JotformDeleteLabelParams extends JotformAuthParams {
|
||||
labelId: string
|
||||
}
|
||||
|
||||
export interface JotformListLabelResourcesParams extends JotformAuthParams {
|
||||
labelId: string
|
||||
offset?: string
|
||||
limit?: string
|
||||
orderby?: string
|
||||
status?: string
|
||||
}
|
||||
|
||||
export interface JotformListLabelResourcesResponse extends ToolResponse {
|
||||
output: {
|
||||
resources: JotformLabelResource[]
|
||||
}
|
||||
}
|
||||
|
||||
export interface JotformLabelResourcesParams extends JotformAuthParams {
|
||||
labelId: string
|
||||
resources: unknown[] | string
|
||||
}
|
||||
|
||||
export interface JotformLabelResourceRefsResponse extends ToolResponse {
|
||||
output: {
|
||||
resources: Array<{ id: string | null; type: string | null }>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import type {
|
||||
JotformFormPropertiesResponse,
|
||||
JotformUpdateFormPropertiesParams,
|
||||
} from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
isRecord,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
toJsonObject,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const updateFormPropertiesTool: ToolConfig<
|
||||
JotformUpdateFormPropertiesParams,
|
||||
JotformFormPropertiesResponse
|
||||
> = {
|
||||
id: 'jotform_update_form_properties',
|
||||
name: 'Jotform Update Form Properties',
|
||||
description:
|
||||
'Update form settings such as the thank-you redirect, submission limit, width, or styles. Only the supplied keys change.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
formId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the form to update',
|
||||
},
|
||||
properties: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Properties to set, e.g. {"thankurl":"https://example.com/thanks","activeRedirect":"thankurl","formWidth":"650"}',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildJotformUrl(
|
||||
params,
|
||||
`form/${encodeURIComponent(requireValue(params.formId, 'formId'))}/properties`
|
||||
).toString(),
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
...buildJotformHeaders(params.apiKey),
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
/**
|
||||
* The PUT endpoint reads a `properties` envelope, not the bare property map —
|
||||
* `{"properties":{"formWidth":"650"}}`. Posting the map unwrapped is accepted
|
||||
* with a 200 and changes nothing.
|
||||
*/
|
||||
body: (params) => {
|
||||
const properties = toJsonObject(params.properties, 'properties')
|
||||
if (Object.keys(properties).length === 0) {
|
||||
throw new Error('properties must contain at least one key to update.')
|
||||
}
|
||||
return { properties }
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Update Form Properties')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
properties: isRecord(envelope.content) ? envelope.content : {},
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
properties: {
|
||||
type: 'json',
|
||||
description: 'The property keys that were edited, with their new values',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { unwrapSingle } from '@/tools/jotform/normalize'
|
||||
import type { JotformUpdateLabelParams, JotformUpdateLabelResponse } from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformHeaders,
|
||||
buildJotformUrl,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
trimOrUndefined,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const updateLabelTool: ToolConfig<JotformUpdateLabelParams, JotformUpdateLabelResponse> = {
|
||||
id: 'jotform_update_label',
|
||||
name: 'Jotform Update Label',
|
||||
description: 'Rename a Jotform label or change its color.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
labelId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the label to update',
|
||||
},
|
||||
labelName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New name for the label',
|
||||
},
|
||||
color: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New label color as a hex code, e.g. "#23FFDD"',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildJotformUrl(
|
||||
params,
|
||||
`label/${encodeURIComponent(requireValue(params.labelId, 'labelId'))}`
|
||||
).toString(),
|
||||
method: 'PUT',
|
||||
headers: (params) => ({
|
||||
...buildJotformHeaders(params.apiKey),
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {}
|
||||
const name = trimOrUndefined(params.labelName)
|
||||
const color = trimOrUndefined(params.color)
|
||||
if (name) body.name = name
|
||||
if (color) body.color = color
|
||||
|
||||
if (Object.keys(body).length === 0) {
|
||||
throw new Error('Supply a name or a color to update.')
|
||||
}
|
||||
|
||||
return body
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Update Label')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { label: unwrapSingle(envelope.content) ?? {} },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
label: {
|
||||
type: 'json',
|
||||
description:
|
||||
'The label fields that were edited, with their new values. Jotform echoes only the changed keys, such as name and color.',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { unwrapSingle } from '@/tools/jotform/normalize'
|
||||
import type {
|
||||
JotformUpdateQuestionParams,
|
||||
JotformUpdateQuestionResponse,
|
||||
} from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformUrl,
|
||||
jotformFormHeaders,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
toFormBody,
|
||||
toJsonObject,
|
||||
trimOrUndefined,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const updateQuestionTool: ToolConfig<
|
||||
JotformUpdateQuestionParams,
|
||||
JotformUpdateQuestionResponse
|
||||
> = {
|
||||
id: 'jotform_update_question',
|
||||
name: 'Jotform Update Question',
|
||||
description:
|
||||
'Edit the properties of a form question, such as its label, order, or validation. Only the supplied properties change.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
formId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the form the question belongs to',
|
||||
},
|
||||
questionId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the question to edit',
|
||||
},
|
||||
text: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New question label',
|
||||
},
|
||||
order: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New position of the question on the form',
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New slug for the question label',
|
||||
},
|
||||
questionProperties: {
|
||||
type: 'json',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Additional type-specific properties to set, e.g. {"required":"Yes","validation":"Email"}',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildJotformUrl(
|
||||
params,
|
||||
`form/${encodeURIComponent(requireValue(params.formId, 'formId'))}/question/${encodeURIComponent(
|
||||
requireValue(params.questionId, 'questionId')
|
||||
)}`
|
||||
).toString(),
|
||||
method: 'POST',
|
||||
headers: (params) => jotformFormHeaders(params.apiKey),
|
||||
body: (params) => {
|
||||
const question: Record<string, unknown> = {
|
||||
...toJsonObject(params.questionProperties, 'questionProperties'),
|
||||
}
|
||||
|
||||
const text = trimOrUndefined(params.text)
|
||||
const order = trimOrUndefined(params.order)
|
||||
const name = trimOrUndefined(params.name)
|
||||
if (text) question.text = text
|
||||
if (order) question.order = order
|
||||
if (name) question.name = name
|
||||
|
||||
if (Object.keys(question).length === 0) {
|
||||
throw new Error('Supply at least one question property to update.')
|
||||
}
|
||||
|
||||
return toFormBody({ question })
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Update Question')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { question: unwrapSingle(envelope.content) ?? {} },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
question: {
|
||||
type: 'json',
|
||||
description:
|
||||
'The question properties that were edited, with their new values. Jotform echoes only the changed keys, such as text, order, and type.',
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import { normalizeUser, unwrapSingle } from '@/tools/jotform/normalize'
|
||||
import type { JotformUpdateSettingsParams, JotformUserResponse } from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformUrl,
|
||||
jotformFormHeaders,
|
||||
parseJotformResponse,
|
||||
toFormBody,
|
||||
trimOrUndefined,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const updateSettingsTool: ToolConfig<JotformUpdateSettingsParams, JotformUserResponse> = {
|
||||
id: 'jotform_update_settings',
|
||||
name: 'Jotform Update Settings',
|
||||
description:
|
||||
'Update account settings such as name, email, website, company, industry, or time zone. Only the supplied fields change.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
settingsName: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New display name on the account',
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New account email address',
|
||||
},
|
||||
website: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New website recorded on the account',
|
||||
},
|
||||
timeZone: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New account time zone in IANA format, e.g. America/New_York',
|
||||
},
|
||||
company: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New company recorded on the account',
|
||||
},
|
||||
industry: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'New industry recorded on the account',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) => buildJotformUrl(params, 'user/settings').toString(),
|
||||
method: 'POST',
|
||||
headers: (params) => jotformFormHeaders(params.apiKey),
|
||||
body: (params) => {
|
||||
const body: Record<string, unknown> = {}
|
||||
const name = trimOrUndefined(params.settingsName)
|
||||
const email = trimOrUndefined(params.email)
|
||||
const website = trimOrUndefined(params.website)
|
||||
const timeZone = trimOrUndefined(params.timeZone)
|
||||
const company = trimOrUndefined(params.company)
|
||||
const industry = trimOrUndefined(params.industry)
|
||||
|
||||
if (name) body.name = name
|
||||
if (email) body.email = email
|
||||
if (website) body.website = website
|
||||
if (timeZone) body.time_zone = timeZone
|
||||
if (company) body.company = company
|
||||
if (industry) body.industry = industry
|
||||
|
||||
if (Object.keys(body).length === 0) {
|
||||
throw new Error('Supply at least one account setting to update.')
|
||||
}
|
||||
|
||||
return toFormBody(body)
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Update Settings')
|
||||
const raw = unwrapSingle(envelope.content) ?? {}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: { user: normalizeUser(raw) },
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
user: {
|
||||
type: 'object',
|
||||
description:
|
||||
'The account after the update. Jotform returns null for the fields it did not echo back.',
|
||||
properties: {
|
||||
username: { type: 'string', description: 'Jotform username' },
|
||||
name: { type: 'string', description: 'Display name on the account' },
|
||||
email: { type: 'string', description: 'Account email address' },
|
||||
website: { type: 'string', description: 'Website recorded on the account' },
|
||||
time_zone: { type: 'string', description: 'Account time zone, in IANA format' },
|
||||
account_type: { type: 'string', description: 'URL of the plan the account is on' },
|
||||
status: { type: 'string', description: 'ACTIVE, DELETED, or SUSPENDED' },
|
||||
created_at: { type: 'string', description: 'Account creation time, YYYY-MM-DD HH:MM:SS' },
|
||||
updated_at: { type: 'string', description: 'Last update time, YYYY-MM-DD HH:MM:SS' },
|
||||
is_verified: { type: 'string', description: '1 when the account email is verified' },
|
||||
industry: { type: 'string', description: 'Industry recorded on the account' },
|
||||
company: { type: 'string', description: 'Company recorded on the account' },
|
||||
language: { type: 'string', description: 'Account interface language, e.g. en-US' },
|
||||
avatarUrl: { type: 'string', description: 'Avatar image URL' },
|
||||
usage: { type: 'string', description: 'URL of the monthly usage endpoint' },
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { unwrapSingle } from '@/tools/jotform/normalize'
|
||||
import type {
|
||||
JotformSubmissionRefResponse,
|
||||
JotformUpdateSubmissionParams,
|
||||
} from '@/tools/jotform/types'
|
||||
import {
|
||||
buildJotformUrl,
|
||||
jotformFormHeaders,
|
||||
normalizeSubmissionAnswers,
|
||||
parseJotformResponse,
|
||||
requireValue,
|
||||
toFormBody,
|
||||
toJsonObject,
|
||||
toStringOrNull,
|
||||
} from '@/tools/jotform/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const updateSubmissionTool: ToolConfig<
|
||||
JotformUpdateSubmissionParams,
|
||||
JotformSubmissionRefResponse
|
||||
> = {
|
||||
id: 'jotform_update_submission',
|
||||
name: 'Jotform Update Submission',
|
||||
description:
|
||||
'Edit an existing Jotform submission. Only the question IDs supplied are changed; the rest keep their stored answers.',
|
||||
version: '1.0.0',
|
||||
|
||||
params: {
|
||||
apiKey: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-only',
|
||||
description: 'Jotform API key',
|
||||
},
|
||||
region: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
visibility: 'user-only',
|
||||
description:
|
||||
'Jotform data residency region the API key belongs to: "us" (default), "eu", or "hipaa"',
|
||||
},
|
||||
submissionId: {
|
||||
type: 'string',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description: 'ID of the submission to edit',
|
||||
},
|
||||
answers: {
|
||||
type: 'json',
|
||||
required: true,
|
||||
visibility: 'user-or-llm',
|
||||
description:
|
||||
'Answers to overwrite, keyed by question ID, e.g. {"4":"Updated message","3":{"first":"Lisa"}} or the shorthand {"3_first":"Lisa"}. The control keys new, flag, and status are passed through unchanged.',
|
||||
},
|
||||
},
|
||||
|
||||
request: {
|
||||
url: (params) =>
|
||||
buildJotformUrl(
|
||||
params,
|
||||
`submission/${encodeURIComponent(requireValue(params.submissionId, 'submissionId'))}`
|
||||
).toString(),
|
||||
method: 'POST',
|
||||
headers: (params) => jotformFormHeaders(params.apiKey),
|
||||
body: (params) => {
|
||||
const answers = toJsonObject(params.answers, 'answers')
|
||||
if (Object.keys(answers).length === 0) {
|
||||
throw new Error('answers must contain at least one question ID.')
|
||||
}
|
||||
return toFormBody({ submission: normalizeSubmissionAnswers(answers) })
|
||||
},
|
||||
},
|
||||
|
||||
transformResponse: async (response: Response) => {
|
||||
const envelope = await parseJotformResponse(response, 'Jotform Update Submission')
|
||||
const raw = unwrapSingle(envelope.content) ?? {}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
output: {
|
||||
submissionId: toStringOrNull(raw.submissionID),
|
||||
url: toStringOrNull(raw.URL),
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
outputs: {
|
||||
submissionId: {
|
||||
type: 'string',
|
||||
description: 'ID of the submission that was edited',
|
||||
},
|
||||
url: {
|
||||
type: 'string',
|
||||
description: 'API URL of the submission',
|
||||
optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { truncate } from '@sim/utils/string'
|
||||
|
||||
/**
|
||||
* Jotform serves the same REST surface from three regional hosts, and an API key
|
||||
* is only valid on the host that issued it. See https://api.jotform.com/docs/
|
||||
*/
|
||||
const JOTFORM_REGION_HOSTS: Record<string, string> = {
|
||||
us: 'https://api.jotform.com',
|
||||
eu: 'https://eu-api.jotform.com',
|
||||
hipaa: 'https://hipaa-api.jotform.com',
|
||||
}
|
||||
|
||||
const DEFAULT_REGION = 'us'
|
||||
|
||||
export interface JotformScope {
|
||||
apiKey: string
|
||||
region?: string
|
||||
}
|
||||
|
||||
/** The envelope every Jotform endpoint wraps its payload in. */
|
||||
export interface JotformEnvelope {
|
||||
content: unknown
|
||||
resultSet: JotformResultSet | null
|
||||
limitLeft: number | null
|
||||
message: string | null
|
||||
}
|
||||
|
||||
export interface JotformResultSet {
|
||||
offset: number | null
|
||||
limit: number | null
|
||||
count: number | null
|
||||
}
|
||||
|
||||
/** Empty strings arrive from untouched subblocks and must not reach the API as values. */
|
||||
export function trimOrUndefined(value: unknown): string | undefined {
|
||||
if (typeof value === 'number') return String(value)
|
||||
if (typeof value !== 'string') return undefined
|
||||
const trimmed = value.trim()
|
||||
return trimmed.length > 0 ? trimmed : undefined
|
||||
}
|
||||
|
||||
/** Reads a required path segment, failing loudly rather than building `/form//questions`. */
|
||||
export function requireValue(value: unknown, field: string): string {
|
||||
const trimmed = trimOrUndefined(value)
|
||||
if (!trimmed) throw new Error(`${field} is required.`)
|
||||
return trimmed
|
||||
}
|
||||
|
||||
export function buildJotformUrl(params: JotformScope, path: string): URL {
|
||||
const host = JOTFORM_REGION_HOSTS[(params.region || DEFAULT_REGION).trim().toLowerCase()]
|
||||
if (!host) {
|
||||
throw new Error(`Unknown Jotform region "${params.region}". Use "us", "eu", or "hipaa".`)
|
||||
}
|
||||
return new URL(`${host}/${path.replace(/^\/+/, '')}`)
|
||||
}
|
||||
|
||||
export function buildJotformHeaders(apiKey: string): Record<string, string> {
|
||||
return { APIKEY: apiKey.trim() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Jotform answers with HTTP 200 and a `responseCode` in the body for many failures,
|
||||
* so the body has to be inspected even when `response.ok` is true.
|
||||
*/
|
||||
export async function parseJotformResponse(
|
||||
response: Response,
|
||||
label: string
|
||||
): Promise<JotformEnvelope> {
|
||||
const text = await response.text()
|
||||
let data: Record<string, unknown> | null = null
|
||||
try {
|
||||
data = text ? (JSON.parse(text) as Record<string, unknown>) : null
|
||||
} catch {
|
||||
data = null
|
||||
}
|
||||
|
||||
const message = typeof data?.message === 'string' ? data.message : null
|
||||
/* Jotform types `responseCode` inconsistently across endpoints, quoting it on some,
|
||||
so a `typeof === 'number'` test would skip the check on the quoted ones. */
|
||||
const responseCode = toNumberOrNull(data?.responseCode)
|
||||
|
||||
if (!response.ok || (responseCode !== null && (responseCode < 200 || responseCode >= 300))) {
|
||||
const status = responseCode ?? response.status
|
||||
/* An error body is not always the documented JSON envelope — an upstream gateway
|
||||
can return an HTML page — so the raw fallback is capped before it becomes the
|
||||
error message. */
|
||||
const detail = message || truncate(text, 300) || response.statusText
|
||||
throw new Error(`${label} error (${status}): ${detail}`)
|
||||
}
|
||||
|
||||
const rawResultSet = data?.resultSet as Record<string, unknown> | undefined
|
||||
const limitLeft = data?.['limit-left']
|
||||
|
||||
return {
|
||||
content: data?.content ?? null,
|
||||
resultSet: rawResultSet
|
||||
? {
|
||||
offset: toNumberOrNull(rawResultSet.offset),
|
||||
limit: toNumberOrNull(rawResultSet.limit),
|
||||
count: toNumberOrNull(rawResultSet.count),
|
||||
}
|
||||
: null,
|
||||
limitLeft: toNumberOrNull(limitLeft),
|
||||
message,
|
||||
}
|
||||
}
|
||||
|
||||
function toNumberOrNull(value: unknown): number | null {
|
||||
if (value === null || value === undefined || value === '') return null
|
||||
const parsed = Number(value)
|
||||
return Number.isFinite(parsed) ? parsed : null
|
||||
}
|
||||
|
||||
export function toStringOrNull(value: unknown): string | null {
|
||||
if (value === null || value === undefined) return null
|
||||
if (typeof value === 'string') return value
|
||||
if (typeof value === 'number' || typeof value === 'boolean') return String(value)
|
||||
return null
|
||||
}
|
||||
|
||||
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* `json` params arrive parsed from the block but as a raw JSON string when a tool is
|
||||
* called straight from the registry, so object bodies normalize both shapes.
|
||||
*/
|
||||
export function toJsonObject(
|
||||
value: Record<string, unknown> | string | undefined,
|
||||
field: string
|
||||
): Record<string, unknown> {
|
||||
if (value === undefined || value === null || value === '') return {}
|
||||
if (isRecord(value)) return value
|
||||
|
||||
let parsed: unknown
|
||||
try {
|
||||
parsed = JSON.parse(value as string)
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid JSON input for ${field}: ${getErrorMessage(error)}`)
|
||||
}
|
||||
|
||||
if (!isRecord(parsed)) {
|
||||
throw new Error(`Expected ${field} to be a JSON object.`)
|
||||
}
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
/** Same as `toJsonObject`, but for params documented as arrays. */
|
||||
export function toJsonArray(value: unknown[] | string | undefined, field: string): unknown[] {
|
||||
if (value === undefined || value === null || value === '') return []
|
||||
if (Array.isArray(value)) return value
|
||||
|
||||
let parsed: unknown
|
||||
try {
|
||||
parsed = JSON.parse(value as string)
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid JSON input for ${field}: ${getErrorMessage(error)}`)
|
||||
}
|
||||
|
||||
if (!Array.isArray(parsed)) {
|
||||
throw new Error(`Expected ${field} to be a JSON array.`)
|
||||
}
|
||||
|
||||
return parsed
|
||||
}
|
||||
|
||||
/**
|
||||
* Jotform's POST endpoints read PHP-style bracket notation — `submission[3][first]`,
|
||||
* `question[text]`, `properties[formWidth]` — rather than a JSON body, so nested
|
||||
* objects are flattened into bracketed keys before form encoding.
|
||||
*/
|
||||
export function toFormBody(fields: Record<string, unknown>): string {
|
||||
const pairs: string[] = []
|
||||
for (const [key, value] of Object.entries(fields)) {
|
||||
appendFormPairs(pairs, key, value)
|
||||
}
|
||||
return pairs.join('&')
|
||||
}
|
||||
|
||||
function appendFormPairs(pairs: string[], key: string, value: unknown): void {
|
||||
if (value === undefined || value === null) return
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((entry, index) => appendFormPairs(pairs, `${key}[${index}]`, entry))
|
||||
return
|
||||
}
|
||||
|
||||
if (isRecord(value)) {
|
||||
for (const [childKey, childValue] of Object.entries(value)) {
|
||||
appendFormPairs(pairs, `${key}[${childKey}]`, childValue)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Form-encoded bodies must declare their content type so the shared transport passes
|
||||
* the preformatted string through instead of re-serializing it as JSON.
|
||||
*/
|
||||
export function jotformFormHeaders(apiKey: string): Record<string, string> {
|
||||
return {
|
||||
...buildJotformHeaders(apiKey),
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Control keys a submission body carries alongside its answers. `created_at` is the
|
||||
* one that looks like a `{qid}_{subfield}` pair and is not one, which is why the
|
||||
* official PHP SDK special-cases it by name rather than by shape.
|
||||
*/
|
||||
const SUBMISSION_CONTROL_KEYS = new Set(['created_at', 'new', 'flag', 'status', 'ip'])
|
||||
|
||||
/**
|
||||
* Expands the `{qid}_{subfield}` shorthand Jotform documents (`1_first`) into the
|
||||
* nested form its API actually reads (`submission[1][first]`). Both official SDKs
|
||||
* perform this same split, and users copy the shorthand straight out of the docs, so
|
||||
* an already-nested object and the flat shorthand have to mean the same thing.
|
||||
*/
|
||||
export function normalizeSubmissionAnswers(
|
||||
answers: Record<string, unknown>
|
||||
): Record<string, unknown> {
|
||||
const normalized: Record<string, unknown> = {}
|
||||
|
||||
for (const [key, value] of Object.entries(answers)) {
|
||||
const separator = key.indexOf('_')
|
||||
if (separator <= 0 || SUBMISSION_CONTROL_KEYS.has(key)) {
|
||||
normalized[key] = value
|
||||
continue
|
||||
}
|
||||
|
||||
const qid = key.slice(0, separator)
|
||||
const subField = key.slice(separator + 1)
|
||||
const existing = normalized[qid]
|
||||
const target = isRecord(existing) ? existing : {}
|
||||
target[subField] = value
|
||||
normalized[qid] = target
|
||||
}
|
||||
|
||||
return normalized
|
||||
}
|
||||
|
||||
/** Applies the pagination and filter query shared by every Jotform list endpoint. */
|
||||
export function applyListQuery(
|
||||
url: URL,
|
||||
params: {
|
||||
offset?: string
|
||||
limit?: string
|
||||
orderby?: string
|
||||
direction?: string
|
||||
filter?: Record<string, unknown> | string
|
||||
}
|
||||
): void {
|
||||
const offset = trimOrUndefined(params.offset)
|
||||
const limit = trimOrUndefined(params.limit)
|
||||
const orderby = trimOrUndefined(params.orderby)
|
||||
const direction = trimOrUndefined(params.direction)
|
||||
|
||||
if (offset) url.searchParams.set('offset', offset)
|
||||
if (limit) url.searchParams.set('limit', limit)
|
||||
if (orderby) url.searchParams.set('orderby', orderby)
|
||||
if (direction) url.searchParams.set('direction', direction.toUpperCase())
|
||||
|
||||
const filter = toJsonObject(params.filter, 'filter')
|
||||
if (Object.keys(filter).length > 0) url.searchParams.set('filter', JSON.stringify(filter))
|
||||
}
|
||||
@@ -2263,6 +2263,51 @@ import {
|
||||
jiraUpdateWorklogTool,
|
||||
jiraWriteTool,
|
||||
} from '@/tools/jira'
|
||||
import {
|
||||
jotformAddLabelResourcesTool,
|
||||
jotformCloneFormTool,
|
||||
jotformCreateFormTool,
|
||||
jotformCreateLabelTool,
|
||||
jotformCreateQuestionsTool,
|
||||
jotformCreateQuestionTool,
|
||||
jotformCreateReportTool,
|
||||
jotformCreateSubmissionsTool,
|
||||
jotformCreateSubmissionTool,
|
||||
jotformCreateWebhookTool,
|
||||
jotformDeleteFormTool,
|
||||
jotformDeleteLabelTool,
|
||||
jotformDeleteQuestionTool,
|
||||
jotformDeleteReportTool,
|
||||
jotformDeleteSubmissionTool,
|
||||
jotformDeleteWebhookTool,
|
||||
jotformGetFormPropertiesTool,
|
||||
jotformGetFormTool,
|
||||
jotformGetHistoryTool,
|
||||
jotformGetLabelTool,
|
||||
jotformGetQuestionTool,
|
||||
jotformGetReportTool,
|
||||
jotformGetSettingsTool,
|
||||
jotformGetSubmissionTool,
|
||||
jotformGetUsageTool,
|
||||
jotformGetUserTool,
|
||||
jotformListFormFilesTool,
|
||||
jotformListFormReportsTool,
|
||||
jotformListFormSubmissionsTool,
|
||||
jotformListFormsTool,
|
||||
jotformListLabelResourcesTool,
|
||||
jotformListLabelsTool,
|
||||
jotformListQuestionsTool,
|
||||
jotformListReportsTool,
|
||||
jotformListSubmissionsTool,
|
||||
jotformListSubUsersTool,
|
||||
jotformListWebhooksTool,
|
||||
jotformRemoveLabelResourcesTool,
|
||||
jotformUpdateFormPropertiesTool,
|
||||
jotformUpdateLabelTool,
|
||||
jotformUpdateQuestionTool,
|
||||
jotformUpdateSettingsTool,
|
||||
jotformUpdateSubmissionTool,
|
||||
} from '@/tools/jotform'
|
||||
import {
|
||||
jsmAddCommentTool,
|
||||
jsmAddCustomerTool,
|
||||
@@ -6148,6 +6193,49 @@ export const tools: Record<string, ToolConfig> = {
|
||||
jira_get_transitions: jiraGetTransitionsTool,
|
||||
jira_list_issue_types: jiraListIssueTypesTool,
|
||||
jira_get_fields: jiraGetFieldsTool,
|
||||
jotform_list_forms: jotformListFormsTool,
|
||||
jotform_get_form: jotformGetFormTool,
|
||||
jotform_create_form: jotformCreateFormTool,
|
||||
jotform_clone_form: jotformCloneFormTool,
|
||||
jotform_delete_form: jotformDeleteFormTool,
|
||||
jotform_get_form_properties: jotformGetFormPropertiesTool,
|
||||
jotform_update_form_properties: jotformUpdateFormPropertiesTool,
|
||||
jotform_list_form_files: jotformListFormFilesTool,
|
||||
jotform_list_questions: jotformListQuestionsTool,
|
||||
jotform_get_question: jotformGetQuestionTool,
|
||||
jotform_create_question: jotformCreateQuestionTool,
|
||||
jotform_update_question: jotformUpdateQuestionTool,
|
||||
jotform_delete_question: jotformDeleteQuestionTool,
|
||||
jotform_list_form_submissions: jotformListFormSubmissionsTool,
|
||||
jotform_list_submissions: jotformListSubmissionsTool,
|
||||
jotform_get_submission: jotformGetSubmissionTool,
|
||||
jotform_create_submission: jotformCreateSubmissionTool,
|
||||
jotform_update_submission: jotformUpdateSubmissionTool,
|
||||
jotform_delete_submission: jotformDeleteSubmissionTool,
|
||||
jotform_list_reports: jotformListReportsTool,
|
||||
jotform_list_form_reports: jotformListFormReportsTool,
|
||||
jotform_create_report: jotformCreateReportTool,
|
||||
jotform_get_report: jotformGetReportTool,
|
||||
jotform_delete_report: jotformDeleteReportTool,
|
||||
jotform_list_webhooks: jotformListWebhooksTool,
|
||||
jotform_create_webhook: jotformCreateWebhookTool,
|
||||
jotform_delete_webhook: jotformDeleteWebhookTool,
|
||||
jotform_get_user: jotformGetUserTool,
|
||||
jotform_get_usage: jotformGetUsageTool,
|
||||
jotform_get_history: jotformGetHistoryTool,
|
||||
jotform_add_label_resources: jotformAddLabelResourcesTool,
|
||||
jotform_create_label: jotformCreateLabelTool,
|
||||
jotform_create_questions: jotformCreateQuestionsTool,
|
||||
jotform_create_submissions: jotformCreateSubmissionsTool,
|
||||
jotform_delete_label: jotformDeleteLabelTool,
|
||||
jotform_get_label: jotformGetLabelTool,
|
||||
jotform_get_settings: jotformGetSettingsTool,
|
||||
jotform_list_label_resources: jotformListLabelResourcesTool,
|
||||
jotform_list_labels: jotformListLabelsTool,
|
||||
jotform_list_subusers: jotformListSubUsersTool,
|
||||
jotform_remove_label_resources: jotformRemoveLabelResourcesTool,
|
||||
jotform_update_label: jotformUpdateLabelTool,
|
||||
jotform_update_settings: jotformUpdateSettingsTool,
|
||||
jsm_get_service_desks: jsmGetServiceDesksTool,
|
||||
jsm_get_request_types: jsmGetRequestTypesTool,
|
||||
jsm_get_request_type_fields: jsmGetRequestTypeFieldsTool,
|
||||
|
||||
Reference in New Issue
Block a user