@topogram/template-todo 0.1.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -0
- package/implementation/README.md +9 -0
- package/implementation/backend/reference.js +206 -0
- package/implementation/backend/repository-reference.js +74 -0
- package/implementation/backend/repository-renderers.js +442 -0
- package/implementation/index.js +53 -0
- package/implementation/runtime/check-renderers.js +215 -0
- package/implementation/runtime/checks.js +120 -0
- package/implementation/runtime/reference.js +92 -0
- package/implementation/web/reference.js +51 -0
- package/implementation/web/renderers.js +1223 -0
- package/implementation/web/screens-reference.js +15 -0
- package/package.json +31 -0
- package/topogram/actors/actor-user.tg +6 -0
- package/topogram/capabilities/cap-complete-task.tg +12 -0
- package/topogram/capabilities/cap-create-project.tg +11 -0
- package/topogram/capabilities/cap-create-task.tg +14 -0
- package/topogram/capabilities/cap-create-user.tg +11 -0
- package/topogram/capabilities/cap-delete-task.tg +12 -0
- package/topogram/capabilities/cap-download-task-export.tg +10 -0
- package/topogram/capabilities/cap-export-tasks.tg +11 -0
- package/topogram/capabilities/cap-get-project.tg +11 -0
- package/topogram/capabilities/cap-get-task-export-job.tg +11 -0
- package/topogram/capabilities/cap-get-task.tg +11 -0
- package/topogram/capabilities/cap-get-user.tg +11 -0
- package/topogram/capabilities/cap-list-projects.tg +11 -0
- package/topogram/capabilities/cap-list-tasks.tg +11 -0
- package/topogram/capabilities/cap-list-users.tg +11 -0
- package/topogram/capabilities/cap-update-project.tg +12 -0
- package/topogram/capabilities/cap-update-task.tg +12 -0
- package/topogram/capabilities/cap-update-user.tg +12 -0
- package/topogram/components/component-ui-task-board.tg +33 -0
- package/topogram/components/component-ui-task-calendar.tg +30 -0
- package/topogram/components/component-ui-task-summary.tg +23 -0
- package/topogram/components/component-ui-task-table.tg +34 -0
- package/topogram/decisions/decision-task-ownership.tg +9 -0
- package/topogram/docs/glossary/user.md +22 -0
- package/topogram/docs/journeys/task-creation-and-ownership.md +57 -0
- package/topogram/entities/entity-project.tg +28 -0
- package/topogram/entities/entity-task.tg +38 -0
- package/topogram/entities/entity-user.tg +24 -0
- package/topogram/enums/enum-export-job-status.tg +3 -0
- package/topogram/enums/enum-project-status.tg +3 -0
- package/topogram/enums/enum-task-priority.tg +3 -0
- package/topogram/enums/enum-task-status.tg +3 -0
- package/topogram/operations/operation-task-creation-monitoring.tg +10 -0
- package/topogram/projections/proj-api.tg +177 -0
- package/topogram/projections/proj-db-postgres.tg +55 -0
- package/topogram/projections/proj-db-sqlite.tg +47 -0
- package/topogram/projections/proj-ui-shared.tg +133 -0
- package/topogram/projections/proj-ui-web-react.tg +92 -0
- package/topogram/projections/proj-ui-web.tg +92 -0
- package/topogram/rules/rule-no-task-creation-in-archived-project.tg +10 -0
- package/topogram/rules/rule-only-active-users-may-own-tasks.tg +10 -0
- package/topogram/shapes/shape-input-complete-task.tg +11 -0
- package/topogram/shapes/shape-input-create-project.tg +6 -0
- package/topogram/shapes/shape-input-create-task.tg +6 -0
- package/topogram/shapes/shape-input-create-user.tg +6 -0
- package/topogram/shapes/shape-input-delete-task.tg +10 -0
- package/topogram/shapes/shape-input-export-tasks.tg +13 -0
- package/topogram/shapes/shape-input-get-project.tg +10 -0
- package/topogram/shapes/shape-input-get-task-export-job.tg +10 -0
- package/topogram/shapes/shape-input-get-task.tg +10 -0
- package/topogram/shapes/shape-input-get-user.tg +10 -0
- package/topogram/shapes/shape-input-list-projects.tg +11 -0
- package/topogram/shapes/shape-input-list-tasks.tg +14 -0
- package/topogram/shapes/shape-input-list-users.tg +11 -0
- package/topogram/shapes/shape-input-update-project.tg +14 -0
- package/topogram/shapes/shape-input-update-task.tg +16 -0
- package/topogram/shapes/shape-input-update-user.tg +13 -0
- package/topogram/shapes/shape-output-project-card.tg +6 -0
- package/topogram/shapes/shape-output-project-detail.tg +6 -0
- package/topogram/shapes/shape-output-task-card.tg +19 -0
- package/topogram/shapes/shape-output-task-detail.tg +6 -0
- package/topogram/shapes/shape-output-task-export-callback.tg +14 -0
- package/topogram/shapes/shape-output-task-export-job.tg +13 -0
- package/topogram/shapes/shape-output-task-export-status.tg +17 -0
- package/topogram/shapes/shape-output-user-card.tg +6 -0
- package/topogram/shapes/shape-output-user-detail.tg +6 -0
- package/topogram/terms/term-user.tg +5 -0
- package/topogram/verifications/verification-create-task-policy.tg +15 -0
- package/topogram/verifications/verification-runtime-smoke.tg +16 -0
- package/topogram/verifications/verification-task-runtime-flow.tg +31 -0
- package/topogram-template.json +11 -0
- package/topogram.project.json +53 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
export function renderTodoRuntimeCheckState() {
|
|
2
|
+
return `const state = {
|
|
3
|
+
createdPrimary: null,
|
|
4
|
+
latestPrimary: null,
|
|
5
|
+
exportJob: null
|
|
6
|
+
};`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function renderTodoRuntimeCheckCreatePayload() {
|
|
10
|
+
return `function createPayload(overrides = {}) {
|
|
11
|
+
return {
|
|
12
|
+
title: "Runtime Check Task",
|
|
13
|
+
description: "Generated by the richer runtime check bundle.",
|
|
14
|
+
priority: "high",
|
|
15
|
+
project_id: envValue(plan.env.demoContainerId),
|
|
16
|
+
...(envValue(plan.env.demoUserId) ? { owner_id: envValue(plan.env.demoUserId) } : {}),
|
|
17
|
+
...overrides
|
|
18
|
+
};
|
|
19
|
+
}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function renderTodoRuntimeCheckHelpers() {
|
|
23
|
+
return `async function waitForExportJob(jobId, { attempts = 10, delayMs = 25 } = {}) {
|
|
24
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
25
|
+
const { response, responseBody } = await requestContract("cap_get_task_export_job", {
|
|
26
|
+
pathParams: {
|
|
27
|
+
job_id: jobId
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
assertCondition(response.status === 200, \`get export job expected 200 during polling, got \${response.status}\`);
|
|
31
|
+
if (responseBody?.status === "completed" && responseBody?.download_url) {
|
|
32
|
+
return responseBody;
|
|
33
|
+
}
|
|
34
|
+
await sleep(delayMs);
|
|
35
|
+
}
|
|
36
|
+
throw new Error("export job did not reach completed status in time");
|
|
37
|
+
}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function renderTodoRuntimeCheckCases() {
|
|
41
|
+
return ` } else if (definition.id === "api_seed_task_ready") {
|
|
42
|
+
const { contract, response, responseBody } = await requestContract(definition.capabilityId, {
|
|
43
|
+
pathParams: inferPathParams(contractFor(definition.capabilityId), definition.pathParams)
|
|
44
|
+
});
|
|
45
|
+
assertCondition(response.status === contract.endpoint.successStatus, \`seed task readiness expected \${contract.endpoint.successStatus}, got \${response.status}\`);
|
|
46
|
+
assertCondition(responseBody?.id === envValue(plan.env.demoPrimaryId), "seed task readiness did not return the expected demo task");
|
|
47
|
+
} else if (definition.id === "create_task") {
|
|
48
|
+
const { contract, response, responseBody } = await requestContract(definition.capabilityId, {
|
|
49
|
+
payload: createPayload(),
|
|
50
|
+
headers: {
|
|
51
|
+
"Idempotency-Key": crypto.randomUUID()
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
assertCondition(response.status === contract.endpoint.successStatus, \`create task expected \${contract.endpoint.successStatus}, got \${response.status}\`);
|
|
55
|
+
assertCondition(responseBody?.id, "create task response did not include id");
|
|
56
|
+
assertCondition(responseBody?.project_id === envValue(plan.env.demoContainerId), "create task response used the wrong project");
|
|
57
|
+
assertCondition(responseBody?.priority === "high", "create task did not persist priority");
|
|
58
|
+
assertCondition(responseBody?.updated_at, "create task response did not include updated_at");
|
|
59
|
+
state.createdPrimary = responseBody;
|
|
60
|
+
state.latestPrimary = responseBody;
|
|
61
|
+
result.resources.primaryId = responseBody.id;
|
|
62
|
+
} else if (definition.id === "get_created_task") {
|
|
63
|
+
const { contract, response, responseBody } = await requestContract(definition.capabilityId);
|
|
64
|
+
assertCondition(response.status === contract.endpoint.successStatus, \`get task expected \${contract.endpoint.successStatus}, got \${response.status}\`);
|
|
65
|
+
assertCondition(responseBody?.id === state.latestPrimary?.id, "get task did not return the created task");
|
|
66
|
+
assertCondition(responseBody?.title, "get task did not include title");
|
|
67
|
+
assertCondition(responseBody?.priority, "get task did not include priority");
|
|
68
|
+
} else if (definition.id === "list_tasks") {
|
|
69
|
+
const { contract, response, responseBody } = await requestContract(definition.capabilityId);
|
|
70
|
+
assertCondition(response.status === contract.endpoint.successStatus, \`list tasks expected \${contract.endpoint.successStatus}, got \${response.status}\`);
|
|
71
|
+
assertCondition(Array.isArray(responseBody?.items), "list tasks did not return an items array");
|
|
72
|
+
assertCondition(responseBody.items.some((item) => item.id === state.latestPrimary?.id), "list tasks did not include the created task");
|
|
73
|
+
} else if (definition.id === "export_tasks") {
|
|
74
|
+
const { contract, response, responseBody } = await requestContract(definition.capabilityId, {
|
|
75
|
+
payload: {
|
|
76
|
+
project_id: envValue(plan.env.demoContainerId)
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
assertCondition(response.status === contract.endpoint.successStatus, \`export tasks expected \${contract.endpoint.successStatus}, got \${response.status}\`);
|
|
80
|
+
assertCondition(responseBody?.job_id, "export tasks did not return a job_id");
|
|
81
|
+
assertCondition(responseBody?.status_url, "export tasks did not return a status_url");
|
|
82
|
+
state.exportJob = responseBody;
|
|
83
|
+
result.resources.jobId = responseBody.job_id;
|
|
84
|
+
} else if (definition.id === "get_task_export_job") {
|
|
85
|
+
assertCondition(state.exportJob?.job_id, "get task export job requires a previously created export job");
|
|
86
|
+
const responseBody = await waitForExportJob(state.exportJob.job_id);
|
|
87
|
+
assertCondition(responseBody.job_id === state.exportJob.job_id, "get task export job returned the wrong job");
|
|
88
|
+
assertCondition(responseBody.status === "completed", "get task export job did not complete");
|
|
89
|
+
assertCondition(responseBody.download_url, "get task export job did not return a download_url");
|
|
90
|
+
state.exportJob = responseBody;
|
|
91
|
+
result.resources.jobId = responseBody.job_id;
|
|
92
|
+
} else if (definition.id === "download_task_export") {
|
|
93
|
+
assertCondition(state.exportJob?.job_id, "download task export requires a previously created export job");
|
|
94
|
+
const contract = contractFor(definition.capabilityId);
|
|
95
|
+
const requestHeaders = new Headers();
|
|
96
|
+
if ((contract.endpoint.authz || []).length > 0 && authToken()) {
|
|
97
|
+
requestHeaders.set("Authorization", \`Bearer \${authToken()}\`);
|
|
98
|
+
}
|
|
99
|
+
const response = await fetchWithStackHint(buildUrl(apiBase(), contract.endpoint.path, {
|
|
100
|
+
job_id: state.exportJob.job_id
|
|
101
|
+
}), { method: contract.endpoint.method, headers: requestHeaders }, "api service");
|
|
102
|
+
const responseBuffer = await response.arrayBuffer();
|
|
103
|
+
assertCondition(response.status === contract.endpoint.successStatus, \`download task export expected \${contract.endpoint.successStatus}, got \${response.status}\`);
|
|
104
|
+
assertCondition(response.headers.get("content-type")?.includes("application/zip"), "download task export did not return zip content");
|
|
105
|
+
assertCondition(response.headers.get("content-disposition")?.includes("task-export.zip"), "download task export did not include the expected filename");
|
|
106
|
+
assertCondition(responseBuffer.byteLength > 0, "download task export returned an empty body");
|
|
107
|
+
result.resources.jobId = state.exportJob.job_id;
|
|
108
|
+
} else if (definition.id === "project_lookup_ready" || definition.id === "user_lookup_ready") {
|
|
109
|
+
const response = await fetch(new URL(definition.path, apiBase()));
|
|
110
|
+
const responseBody = await parseResponseBody(response);
|
|
111
|
+
assertCondition(response.status === 200, \`\${definition.id} expected 200, got \${response.status}\`);
|
|
112
|
+
assertCondition(Array.isArray(responseBody), \`\${definition.id} did not return an array\`);
|
|
113
|
+
assertCondition(responseBody.length > 0, \`\${definition.id} returned no options\`);
|
|
114
|
+
assertCondition(responseBody.every((item) => item && typeof item.value === "string" && typeof item.label === "string"), \`\${definition.id} returned malformed lookup options\`);
|
|
115
|
+
} else if (definition.id === "update_without_precondition") {
|
|
116
|
+
const contract = contractFor(definition.capabilityId);
|
|
117
|
+
const { response, responseBody } = await requestContract(definition.capabilityId, {
|
|
118
|
+
payload: {
|
|
119
|
+
title: "Runtime Check Task Updated"
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
const expected = preconditionStatusFor(contract);
|
|
123
|
+
assertCondition(response.status === expected, \`update without precondition expected \${expected}, got \${response.status}\`);
|
|
124
|
+
assertErrorResponse(responseBody, contract.endpoint.preconditions?.[0]?.code, "update without precondition");
|
|
125
|
+
} else if (definition.id === "update_with_stale_precondition") {
|
|
126
|
+
const { response, responseBody } = await requestContract(definition.capabilityId, {
|
|
127
|
+
payload: {
|
|
128
|
+
title: "Runtime Check Task Stale Update"
|
|
129
|
+
},
|
|
130
|
+
headers: {
|
|
131
|
+
"If-Match": "1970-01-01T00:00:00.000Z"
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
assertCondition(response.status === definition.expectStatus, \`update with stale precondition expected \${definition.expectStatus}, got \${response.status}\`);
|
|
135
|
+
assertErrorResponse(responseBody, definition.expectErrorCode, "update with stale precondition");
|
|
136
|
+
} else if (definition.id === "update_task") {
|
|
137
|
+
const { contract, response, responseBody } = await requestContract(definition.capabilityId, {
|
|
138
|
+
payload: {
|
|
139
|
+
title: "Runtime Check Task Updated",
|
|
140
|
+
priority: "low"
|
|
141
|
+
},
|
|
142
|
+
headers: {
|
|
143
|
+
"If-Match": state.latestPrimary?.updated_at || ""
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
assertCondition(response.status === contract.endpoint.successStatus, \`update task expected \${contract.endpoint.successStatus}, got \${response.status}\`);
|
|
147
|
+
assertCondition(responseBody?.title === "Runtime Check Task Updated", "update task did not persist the updated title");
|
|
148
|
+
assertCondition(responseBody?.priority === "low", "update task did not persist the updated priority");
|
|
149
|
+
assertCondition(responseBody?.updated_at, "update task did not return updated_at");
|
|
150
|
+
state.latestPrimary = responseBody;
|
|
151
|
+
result.resources.primaryId = responseBody.id;
|
|
152
|
+
} else if (definition.id === "complete_without_precondition") {
|
|
153
|
+
const contract = contractFor(definition.capabilityId);
|
|
154
|
+
const { response, responseBody } = await requestContract(definition.capabilityId, {
|
|
155
|
+
payload: {
|
|
156
|
+
completed_at: new Date().toISOString()
|
|
157
|
+
},
|
|
158
|
+
headers: {
|
|
159
|
+
"Idempotency-Key": crypto.randomUUID()
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
const expected = preconditionStatusFor(contract);
|
|
163
|
+
assertCondition(response.status === expected, \`complete without precondition expected \${expected}, got \${response.status}\`);
|
|
164
|
+
assertErrorResponse(responseBody, contract.endpoint.preconditions?.[0]?.code, "complete without precondition");
|
|
165
|
+
} else if (definition.id === "complete_task") {
|
|
166
|
+
const { contract, response, responseBody } = await requestContract(definition.capabilityId, {
|
|
167
|
+
payload: {
|
|
168
|
+
completed_at: new Date().toISOString()
|
|
169
|
+
},
|
|
170
|
+
headers: {
|
|
171
|
+
"If-Match": state.latestPrimary?.updated_at || "",
|
|
172
|
+
"Idempotency-Key": crypto.randomUUID()
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
assertCondition(response.status === contract.endpoint.successStatus, \`complete task expected \${contract.endpoint.successStatus}, got \${response.status}\`);
|
|
176
|
+
assertCondition(responseBody?.status === "completed", "complete task did not set status to completed");
|
|
177
|
+
assertCondition(responseBody?.updated_at, "complete task did not return updated_at");
|
|
178
|
+
state.latestPrimary = responseBody;
|
|
179
|
+
result.resources.primaryId = responseBody.id;
|
|
180
|
+
} else if (definition.id === "delete_without_precondition") {
|
|
181
|
+
const contract = contractFor(definition.capabilityId);
|
|
182
|
+
const { response, responseBody } = await requestContract(definition.capabilityId);
|
|
183
|
+
const expected = preconditionStatusFor(contract);
|
|
184
|
+
assertCondition(response.status === expected, \`delete without precondition expected \${expected}, got \${response.status}\`);
|
|
185
|
+
assertErrorResponse(responseBody, contract.endpoint.preconditions?.[0]?.code, "delete without precondition");
|
|
186
|
+
} else if (definition.id === "delete_task") {
|
|
187
|
+
const { contract, response, responseBody } = await requestContract(definition.capabilityId, {
|
|
188
|
+
headers: {
|
|
189
|
+
"If-Match": state.latestPrimary?.updated_at || ""
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
assertCondition(response.status === contract.endpoint.successStatus, \`delete task expected \${contract.endpoint.successStatus}, got \${response.status}\`);
|
|
193
|
+
assertCondition(responseBody?.status === "archived", "delete task did not archive the task");
|
|
194
|
+
state.latestPrimary = responseBody;
|
|
195
|
+
result.resources.primaryId = responseBody.id;
|
|
196
|
+
} else if (definition.id === "invalid_create_returns_4xx") {
|
|
197
|
+
const { response, responseBody } = await requestContract(definition.capabilityId, {
|
|
198
|
+
payload: {
|
|
199
|
+
description: "Missing required fields"
|
|
200
|
+
},
|
|
201
|
+
headers: {
|
|
202
|
+
"Idempotency-Key": crypto.randomUUID()
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
assertCondition(Math.floor(response.status / 100) === definition.expectStatusClass, \`invalid create expected \${definition.expectStatusClass}xx, got \${response.status}\`);
|
|
206
|
+
assertErrorResponse(responseBody, definition.expectErrorCode, "invalid create");
|
|
207
|
+
} else if (definition.id === "get_unknown_task_not_found") {
|
|
208
|
+
const { response, responseBody } = await requestContract(definition.capabilityId, {
|
|
209
|
+
pathParams: {
|
|
210
|
+
id: crypto.randomUUID()
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
assertCondition(response.status === definition.expectStatus, \`get unknown task expected \${definition.expectStatus}, got \${response.status}\`);
|
|
214
|
+
assertErrorResponse(responseBody, definition.expectErrorCode, "get unknown task");`;
|
|
215
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export const TODO_RUNTIME_CHECKS = {
|
|
2
|
+
environmentStage: {
|
|
3
|
+
id: "environment",
|
|
4
|
+
name: "Environment Readiness",
|
|
5
|
+
failFast: true,
|
|
6
|
+
checks: [
|
|
7
|
+
{
|
|
8
|
+
id: "required_env",
|
|
9
|
+
kind: "env_required",
|
|
10
|
+
mandatory: true,
|
|
11
|
+
mutating: false
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: "web_tasks_page_ready",
|
|
15
|
+
kind: "web_contract",
|
|
16
|
+
path: "/tasks",
|
|
17
|
+
expectStatus: 200,
|
|
18
|
+
expectText: "Tasks",
|
|
19
|
+
mandatory: true,
|
|
20
|
+
mutating: false
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "web_projects_page_ready",
|
|
24
|
+
kind: "web_contract",
|
|
25
|
+
path: "/projects",
|
|
26
|
+
expectStatus: 200,
|
|
27
|
+
expectText: "Projects",
|
|
28
|
+
mandatory: true,
|
|
29
|
+
mutating: false
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "web_users_page_ready",
|
|
33
|
+
kind: "web_contract",
|
|
34
|
+
path: "/users",
|
|
35
|
+
expectStatus: 200,
|
|
36
|
+
expectText: "Users",
|
|
37
|
+
mandatory: true,
|
|
38
|
+
mutating: false
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "api_health_ready",
|
|
42
|
+
kind: "api_health",
|
|
43
|
+
path: "/health",
|
|
44
|
+
expectStatus: 200,
|
|
45
|
+
expectOk: true,
|
|
46
|
+
mandatory: true,
|
|
47
|
+
mutating: false
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "api_ready",
|
|
51
|
+
kind: "api_ready",
|
|
52
|
+
path: "/ready",
|
|
53
|
+
expectStatus: 200,
|
|
54
|
+
expectReady: true,
|
|
55
|
+
mandatory: true,
|
|
56
|
+
mutating: false
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "api_seed_task_ready",
|
|
60
|
+
kind: "api_contract",
|
|
61
|
+
capabilityId: "cap_get_task",
|
|
62
|
+
pathParams: {
|
|
63
|
+
task_id: "$env:TOPOGRAM_DEMO_PRIMARY_ID"
|
|
64
|
+
},
|
|
65
|
+
expectShape: "task_detail",
|
|
66
|
+
mandatory: true,
|
|
67
|
+
mutating: false
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "web_task_detail_owner_edit_visible",
|
|
71
|
+
kind: "web_contract",
|
|
72
|
+
path: "/tasks/$env:TOPOGRAM_DEMO_PRIMARY_ID",
|
|
73
|
+
expectStatus: 200,
|
|
74
|
+
expectText: "Edit Task",
|
|
75
|
+
mandatory: true,
|
|
76
|
+
mutating: false
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: "web_task_detail_non_owner_edit_hidden",
|
|
80
|
+
kind: "web_contract",
|
|
81
|
+
path: "/tasks/$env:TOPOGRAM_DEMO_PRIMARY_ID?topogram_auth_user_id=99999999-9999-4999-8999-999999999999",
|
|
82
|
+
expectStatus: 200,
|
|
83
|
+
expectText: "Back to Tasks",
|
|
84
|
+
expectNotText: "Edit Task",
|
|
85
|
+
mandatory: true,
|
|
86
|
+
mutating: false
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
},
|
|
90
|
+
apiStage: {
|
|
91
|
+
id: "api",
|
|
92
|
+
name: "API Runtime Flows",
|
|
93
|
+
failFast: false,
|
|
94
|
+
checks: [
|
|
95
|
+
{ id: "create_task", kind: "api_contract", capabilityId: "cap_create_task", mutating: true, mandatory: true },
|
|
96
|
+
{ id: "get_created_task", kind: "api_contract", capabilityId: "cap_get_task", mutating: false, mandatory: true },
|
|
97
|
+
{ id: "list_tasks", kind: "api_contract", capabilityId: "cap_list_tasks", mutating: false, mandatory: true },
|
|
98
|
+
{ id: "export_tasks", kind: "api_contract", capabilityId: "cap_export_tasks", mutating: true, mandatory: true },
|
|
99
|
+
{ id: "get_task_export_job", kind: "api_contract", capabilityId: "cap_get_task_export_job", mutating: false, mandatory: true },
|
|
100
|
+
{ id: "download_task_export", kind: "api_contract", capabilityId: "cap_download_task_export", mutating: false, mandatory: true },
|
|
101
|
+
{ id: "project_lookup_ready", kind: "lookup_contract", lookupKey: "project", mandatory: true, mutating: false },
|
|
102
|
+
{ id: "user_lookup_ready", kind: "lookup_contract", lookupKey: "user", mandatory: true, mutating: false },
|
|
103
|
+
{ id: "update_without_precondition", kind: "api_negative", capabilityId: "cap_update_task", expectStatusFrom: "precondition", expectErrorCodeFrom: "precondition", mutating: false, mandatory: true },
|
|
104
|
+
{ id: "update_with_stale_precondition", kind: "api_negative", capabilityId: "cap_update_task", expectStatus: 412, expectErrorCode: "stale_precondition", stalePrecondition: true, mutating: false, mandatory: true },
|
|
105
|
+
{ id: "update_task", kind: "api_contract", capabilityId: "cap_update_task", mutating: true, mandatory: true },
|
|
106
|
+
{ id: "complete_without_precondition", kind: "api_negative", capabilityId: "cap_complete_task", expectStatusFrom: "precondition", expectErrorCodeFrom: "precondition", mutating: false, mandatory: true },
|
|
107
|
+
{ id: "complete_task", kind: "api_contract", capabilityId: "cap_complete_task", mutating: true, mandatory: true },
|
|
108
|
+
{ id: "delete_without_precondition", kind: "api_negative", capabilityId: "cap_delete_task", expectStatusFrom: "precondition", expectErrorCodeFrom: "precondition", mutating: false, mandatory: true },
|
|
109
|
+
{ id: "delete_task", kind: "api_contract", capabilityId: "cap_delete_task", mutating: true, mandatory: true },
|
|
110
|
+
{ id: "invalid_create_returns_4xx", kind: "api_negative", capabilityId: "cap_create_task", expectStatusClass: 4, expectErrorCode: "cap_create_task_invalid_request", mutating: false, mandatory: true },
|
|
111
|
+
{ id: "get_unknown_task_not_found", kind: "api_negative", capabilityId: "cap_get_task", expectStatus: 404, expectErrorCode: "cap_get_task_not_found", mutating: false, mandatory: true }
|
|
112
|
+
]
|
|
113
|
+
},
|
|
114
|
+
smokeChecks: [
|
|
115
|
+
{ id: "web_tasks_page", type: "web_get", path: "/tasks", expectStatus: 200, expectText: "Tasks" },
|
|
116
|
+
{ id: "create_task", type: "api_post", path: "/tasks", expectStatus: 201, capabilityId: "cap_create_task" },
|
|
117
|
+
{ id: "get_task", type: "api_get", path: "/tasks/:id", expectStatus: 200, capabilityId: "cap_get_task" },
|
|
118
|
+
{ id: "list_tasks", type: "api_get", path: "/tasks", expectStatus: 200, capabilityId: "cap_list_tasks" }
|
|
119
|
+
]
|
|
120
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { TODO_BACKEND_REFERENCE } from "../backend/reference.js";
|
|
2
|
+
|
|
3
|
+
export const TODO_RUNTIME_REFERENCE = {
|
|
4
|
+
localDbProjectionId: "proj_db_postgres",
|
|
5
|
+
serviceName: TODO_BACKEND_REFERENCE.serviceName,
|
|
6
|
+
ports: {
|
|
7
|
+
server: 3000,
|
|
8
|
+
web: 5173
|
|
9
|
+
},
|
|
10
|
+
environment: {
|
|
11
|
+
name: "Todo Local Runtime Stack",
|
|
12
|
+
databaseName: "topogram_todo",
|
|
13
|
+
envExample: `TOPOGRAM_AUTH_PROFILE=bearer_demo
|
|
14
|
+
TOPOGRAM_AUTH_TOKEN=topogram-todo-demo-token
|
|
15
|
+
TOPOGRAM_AUTH_USER_ID=${TODO_BACKEND_REFERENCE.demo.userId}
|
|
16
|
+
TOPOGRAM_AUTH_PERMISSIONS=*
|
|
17
|
+
TOPOGRAM_AUTH_ROLES=manager
|
|
18
|
+
TOPOGRAM_AUTH_ADMIN=true
|
|
19
|
+
PUBLIC_TOPOGRAM_AUTH_TOKEN=topogram-todo-demo-token
|
|
20
|
+
PUBLIC_TOPOGRAM_AUTH_USER_ID=${TODO_BACKEND_REFERENCE.demo.userId}
|
|
21
|
+
PUBLIC_TOPOGRAM_AUTH_PERMISSIONS=*
|
|
22
|
+
PUBLIC_TOPOGRAM_DEMO_PRIMARY_ID=${TODO_BACKEND_REFERENCE.demo.taskId}
|
|
23
|
+
PUBLIC_TOPOGRAM_DEMO_CONTAINER_ID=${TODO_BACKEND_REFERENCE.demo.projectId}
|
|
24
|
+
TOPOGRAM_DEMO_PRIMARY_ID=${TODO_BACKEND_REFERENCE.demo.taskId}
|
|
25
|
+
TOPOGRAM_DEMO_CONTAINER_ID=${TODO_BACKEND_REFERENCE.demo.projectId}`
|
|
26
|
+
},
|
|
27
|
+
compileCheck: {
|
|
28
|
+
name: "Todo Generated Compile Checks"
|
|
29
|
+
},
|
|
30
|
+
smoke: {
|
|
31
|
+
name: "Todo Runtime Smoke Plan",
|
|
32
|
+
bundleTitle: "Todo Runtime Smoke Bundle",
|
|
33
|
+
defaultContainerEnvVar: "TOPOGRAM_DEMO_CONTAINER_ID",
|
|
34
|
+
webPath: "/tasks",
|
|
35
|
+
expectText: "Tasks",
|
|
36
|
+
createPath: "/tasks",
|
|
37
|
+
getPathPrefix: "/tasks/",
|
|
38
|
+
listPath: "/tasks",
|
|
39
|
+
createPayload: {
|
|
40
|
+
title: "Smoke Test Task",
|
|
41
|
+
containerField: "project_id",
|
|
42
|
+
extraFields: {
|
|
43
|
+
owner_id: "__DEMO_USER_ID__"
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
runtimeCheck: {
|
|
48
|
+
name: "Todo Runtime Check Plan",
|
|
49
|
+
bundleTitle: "Todo Runtime Check Bundle",
|
|
50
|
+
requiredEnv: [
|
|
51
|
+
"TOPOGRAM_API_BASE_URL",
|
|
52
|
+
"TOPOGRAM_WEB_BASE_URL",
|
|
53
|
+
"TOPOGRAM_DEMO_CONTAINER_ID",
|
|
54
|
+
"TOPOGRAM_DEMO_PRIMARY_ID"
|
|
55
|
+
],
|
|
56
|
+
demoContainerEnvVar: "TOPOGRAM_DEMO_CONTAINER_ID",
|
|
57
|
+
demoPrimaryEnvVar: "TOPOGRAM_DEMO_PRIMARY_ID",
|
|
58
|
+
lookupPaths: {
|
|
59
|
+
project: "/lookups/projects",
|
|
60
|
+
user: "/lookups/users"
|
|
61
|
+
},
|
|
62
|
+
stageNotes: [
|
|
63
|
+
{
|
|
64
|
+
id: "environment",
|
|
65
|
+
summary: "required env, web readiness, API health, API readiness, and DB-backed seeded task lookup"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: "api",
|
|
69
|
+
summary: "core task happy paths, export/job flows, generated lookup endpoints, and important negative cases"
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
notes: [
|
|
73
|
+
"Mutating checks create, update, complete, and archive a runtime-check task.",
|
|
74
|
+
"Export checks submit a task export job, verify job status, and verify the download endpoint.",
|
|
75
|
+
"Runtime checks also verify the generated project and user lookup endpoints.",
|
|
76
|
+
"Later stages are skipped if environment readiness fails.",
|
|
77
|
+
"The generated server exposes both `/health` and `/ready`.",
|
|
78
|
+
"Use the smoke bundle for a faster minimal confidence check.",
|
|
79
|
+
"Use this runtime-check bundle for richer staged verification and JSON reporting."
|
|
80
|
+
]
|
|
81
|
+
},
|
|
82
|
+
appBundle: {
|
|
83
|
+
name: "Topogram Todo App Bundle",
|
|
84
|
+
demoContainerName: TODO_BACKEND_REFERENCE.demo.project.name,
|
|
85
|
+
demoPrimaryTitle: TODO_BACKEND_REFERENCE.demo.tasks[0].title
|
|
86
|
+
},
|
|
87
|
+
demoEnv: {
|
|
88
|
+
userId: TODO_BACKEND_REFERENCE.demo.userId,
|
|
89
|
+
containerId: TODO_BACKEND_REFERENCE.demo.projectId,
|
|
90
|
+
primaryId: TODO_BACKEND_REFERENCE.demo.taskId
|
|
91
|
+
}
|
|
92
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const TODO_WEB_REFERENCE = {
|
|
2
|
+
brandName: "Topogram Todo",
|
|
3
|
+
client: {
|
|
4
|
+
primaryParam: "task_id",
|
|
5
|
+
functionNames: {
|
|
6
|
+
list: "listTasks",
|
|
7
|
+
get: "getTask",
|
|
8
|
+
create: "createTask",
|
|
9
|
+
update: "updateTask",
|
|
10
|
+
terminal: "completeTask"
|
|
11
|
+
},
|
|
12
|
+
capabilityIds: {
|
|
13
|
+
list: "cap_list_tasks",
|
|
14
|
+
get: "cap_get_task",
|
|
15
|
+
create: "cap_create_task",
|
|
16
|
+
update: "cap_update_task",
|
|
17
|
+
terminal: "cap_complete_task"
|
|
18
|
+
},
|
|
19
|
+
extraFunctions: [
|
|
20
|
+
{ name: "deleteTask", capabilityId: "cap_delete_task", primaryParam: "task_id" },
|
|
21
|
+
{ name: "exportTasks", capabilityId: "cap_export_tasks" },
|
|
22
|
+
{ name: "getTaskExportJob", capabilityId: "cap_get_task_export_job", primaryParam: "job_id" }
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
nav: {
|
|
26
|
+
browseLabel: "Tasks",
|
|
27
|
+
browseRoute: "/tasks",
|
|
28
|
+
createLabel: "Create Task",
|
|
29
|
+
createRoute: "/tasks/new",
|
|
30
|
+
links: [
|
|
31
|
+
{ label: "Tasks", route: "/tasks" },
|
|
32
|
+
{ label: "Projects", route: "/projects" },
|
|
33
|
+
{ label: "Users", route: "/users" }
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
home: {
|
|
37
|
+
demoPrimaryEnvVar: "PUBLIC_TOPOGRAM_DEMO_PRIMARY_ID",
|
|
38
|
+
demoTaskLabel: "Open Demo Task",
|
|
39
|
+
heroDescriptionTemplate: "Generated from Topogram via the PROFILE profile and wired to a multi-resource workspace for tasks, projects, and users.",
|
|
40
|
+
dynamicRouteText: "This screen uses a dynamic route.",
|
|
41
|
+
noRouteText: "No direct route is exposed for this screen."
|
|
42
|
+
},
|
|
43
|
+
createPrimary: {
|
|
44
|
+
defaultAssigneeEnvVar: "PUBLIC_TOPOGRAM_DEMO_USER_ID",
|
|
45
|
+
defaultContainerEnvVar: "PUBLIC_TOPOGRAM_DEMO_CONTAINER_ID",
|
|
46
|
+
helperText: "A project is required to create a task. Owner is optional.",
|
|
47
|
+
projectPlaceholder: "Select a project",
|
|
48
|
+
cancelLabel: "Cancel",
|
|
49
|
+
submitLabel: "Create Task"
|
|
50
|
+
}
|
|
51
|
+
};
|