Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/data-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ enabled. Migration `20261022010000_lets_talk_classification` seeds the Let's tal
classify and label flows disabled; forms-processor-v6 ADR 0001 and its
`examples/lets-talk-shadow-mode.sql` / `lets-talk-cutover.sql` describe the rollout.

Migration `20261102010000_space_tech_challenge_email` seeds `space-tech-challenge-email`,
which emails every `space_tech_challenge` submission to a fixed list, with the submitter's
`email` answer as reply-to. It is disabled with empty settings: forms-processor-v6 supplies
the recipients and template from `SPACE_TECH_CHALLENGE_RECIPIENTS` and
`SPACE_TECH_CHALLENGE_TEMPLATE_ID`. Enable it in SQL after both are set and the processor
is redeployed.

Processor events contain answers and must be included in personal-data retention
and erasure procedures separately from `Submission`. Deleting a ProcessorEvent
cascades its delivery records and removes deduplication protection, so retain it
Expand Down
36 changes: 36 additions & 0 deletions examples/space-tech-challenge.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"title": "Share your space-data challenge",
"description": "",
"successMessage": "Thanks for sharing your challenge. We'll get back to you within 48 hours on whether it's a fit.",
"access": "ANONYMOUS",
"fields": [
{
"key": "full_name",
"label": "Full Name",
"type": "TEXT",
"required": true,
"maxLength": 200
},
{
"key": "company",
"label": "Company",
"type": "TEXT",
"required": true,
"maxLength": 200
},
{
"key": "email",
"label": "Work Email",
"type": "EMAIL",
"required": true,
"maxLength": 254
},
{
"key": "challenge",
"label": "Which challenge are you trying to solve?",
"type": "TEXTAREA",
"required": true,
"maxLength": 5000
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- Internal notification for SpaceTech landing page challenges (form space_tech_challenge).
-- Every submission goes to a fixed recipient list, with the submitter as reply-to.
-- forms-processor-v6 supplies the settings from SPACE_TECH_CHALLENGE_RECIPIENTS and
-- SPACE_TECH_CHALLENGE_TEMPLATE_ID (SSM); they replace these database settings.
-- Seeded disabled: enable it after both parameters are set and the processor is redeployed.
INSERT INTO forms."ProcessorFlow" (id, "formKey", action, rules)
VALUES ('space-tech-challenge-email', 'space_tech_challenge', 'sendgrid-email', '{"all":[]}')
ON CONFLICT (id) DO NOTHING;
56 changes: 56 additions & 0 deletions scripts/publish-space-tech-challenge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { readFile } from 'node:fs/promises';

/**
* Idempotently publishes the SpaceTech landing page definition to the selected Forms API (development by default).
* @param No arguments; reads FORMS_API_BASE_URL and FORMS_ADMIN_TOKEN from the environment.
* @returns Completion after registration, immutable version save, and publication; creates no submissions.
* @throws Error for a missing token, unsupported API base, conflicting revision, or failed API request.
*/
async function publishSpaceTechChallenge(): Promise<void> {
const apiBase = (
process.env.FORMS_API_BASE_URL ?? 'https://api.topcoder-dev.com/v6'
).replace(/\/+$/, '');
if (
![
'https://api.topcoder-dev.com/v6',
'https://api.topcoder.com/v6',
].includes(apiBase)
)
throw new Error(
'FORMS_API_BASE_URL must select the dev or production Topcoder API.',
);
const token = process.env.FORMS_ADMIN_TOKEN;
if (!token)
throw new Error(
'FORMS_ADMIN_TOKEN with manage:forms or Administrator access is required.',
);
const definition: unknown = JSON.parse(
await readFile('examples/space-tech-challenge.json', 'utf8'),
);
const base = `${apiBase}/forms`;
for (const [method, path, body] of [
['POST', '', { key: 'space_tech_challenge' }],
['PUT', '/space_tech_challenge/versions/1', definition],
['POST', '/space_tech_challenge/versions/1/publish', undefined],
] as const) {
const response = await fetch(`${base}${path}`, {
method,
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
...(body ? { body: JSON.stringify(body) } : {}),
signal: AbortSignal.timeout(30000),
});
if (!response.ok)
throw new Error(
`${method} ${path} failed (${response.status}); no credentials or response data logged.`,
);
}
console.log(`Published space_tech_challenge version 1 to ${apiBase}.`);
}

void publishSpaceTechChallenge().catch((error: unknown) => {
console.error(error instanceof Error ? error.message : 'Publication failed.');
process.exitCode = 1;
});
14 changes: 14 additions & 0 deletions test/forms.integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,20 @@ describe('forms API with real PostgreSQL', () => {
);
});

it('seeds the SpaceTech challenge email disabled, with settings left to the processor environment', async () => {
const flows = await db.processorFlow.findMany({
where: { formKey: 'space_tech_challenge' },
});
expect(flows).toHaveLength(1);
expect(flows[0]).toMatchObject({
id: 'space-tech-challenge-email',
enabled: false,
action: 'sendgrid-email',
rules: { all: [] },
settings: {},
});
});

it('reports AI classification columns and filters by label only for forms with a classify flow', async () => {
const key = await create({
...definition(),
Expand Down
Loading