690503:0135 Update workflow #01
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
# Quickstart: Unified Workflow Engine — Production Hardening
|
||||
|
||||
**Phase 1 Output** | Generated: 2026-05-02
|
||||
**For**: Developers implementing tasks from `tasks.md` (generated by `/speckit-tasks`)
|
||||
|
||||
---
|
||||
|
||||
## Pre-flight Checklist
|
||||
|
||||
Before writing any code:
|
||||
|
||||
- [ ] Apply Delta 09: `specs/03-Data-and-Storage/deltas/09-add-version-no-to-workflow-instances.sql`
|
||||
- [ ] Apply Delta 10: `specs/03-Data-and-Storage/deltas/10-add-action-by-user-uuid-to-workflow-histories.sql`
|
||||
- [ ] Confirm `workflow_instances` has `version_no` column: `DESCRIBE workflow_instances;`
|
||||
- [ ] Confirm `workflow_histories` has `action_by_user_uuid` column: `DESCRIBE workflow_histories;`
|
||||
- [ ] Verify existing tests pass: `pnpm test --testPathPattern=workflow-engine`
|
||||
|
||||
---
|
||||
|
||||
## Implementation Order
|
||||
|
||||
Tasks MUST be implemented in this order to avoid breaking existing functionality:
|
||||
|
||||
```
|
||||
[B1] Schema Deltas (DB)
|
||||
↓
|
||||
[B2] Entity + DTO updates
|
||||
↓
|
||||
[B3] processTransition() — optimistic lock
|
||||
↓
|
||||
[B4] WorkflowTransitionGuard — CASL role mapping
|
||||
↓
|
||||
[B5] Observability — metrics + structured log
|
||||
↓
|
||||
[B6] DSL Redis cache invalidation
|
||||
↓
|
||||
[B7] BullMQ DLQ + n8n webhook
|
||||
↓
|
||||
[F1] FilePreviewModal component
|
||||
↓
|
||||
[F2] Step-attachment upload zone in IntegratedBanner
|
||||
↓
|
||||
[F3] Module gap-fill (all 4 modules)
|
||||
↓
|
||||
[F4] Admin DSL editor UI
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Files Reference
|
||||
|
||||
| Task | File | Action |
|
||||
|------|------|--------|
|
||||
| B1 | `specs/03-Data-and-Storage/deltas/09-*.sql` | CREATE |
|
||||
| B1 | `specs/03-Data-and-Storage/deltas/10-*.sql` | CREATE |
|
||||
| B2 | `backend/src/modules/workflow-engine/entities/workflow-instance.entity.ts` | EDIT — add `versionNo` |
|
||||
| B2 | `backend/src/modules/workflow-engine/entities/workflow-history.entity.ts` | EDIT — add `actionByUserUuid` |
|
||||
| B2 | `backend/src/modules/workflow-engine/dto/workflow-history-item.dto.ts` | EDIT — add `actorUuid` |
|
||||
| B3 | `backend/src/modules/workflow-engine/workflow-engine.service.ts` | EDIT — optimistic lock, rollback, metrics |
|
||||
| B4 | `backend/src/modules/workflow-engine/guards/workflow-transition.guard.ts` | EDIT — DSL role → CASL |
|
||||
| B5 | `backend/src/modules/workflow-engine/workflow-engine.module.ts` | EDIT — register metrics providers |
|
||||
| B6 | `backend/src/modules/workflow-engine/workflow-engine.service.ts` | EDIT — cache set/del in createDefinition/update |
|
||||
| B7 | `backend/src/modules/workflow-engine/workflow-event.service.ts` | EDIT — DLQ + n8n webhook |
|
||||
| F1 | `frontend/components/workflow/file-preview-modal.tsx` | CREATE |
|
||||
| F2 | `frontend/components/workflow/integrated-banner.tsx` | EDIT — upload zone |
|
||||
| F2 | `frontend/components/workflow/workflow-lifecycle.tsx` | EDIT — attachment chips |
|
||||
| F3 | `frontend/app/(admin)/admin/doc-control/correspondence/[uuid]/page.tsx` | EDIT — banner wiring |
|
||||
| F3 | `frontend/app/(admin)/admin/doc-control/rfa/[uuid]/page.tsx` | EDIT — step-attach gap |
|
||||
| F3 | `frontend/app/(admin)/admin/doc-control/transmittals/[uuid]/page.tsx` | EDIT — step-attach gap |
|
||||
| F3 | `frontend/app/(admin)/admin/doc-control/circulation/[uuid]/page.tsx` | EDIT — step-attach gap |
|
||||
| F4 | `frontend/app/(admin)/admin/workflows/definitions/page.tsx` | CREATE |
|
||||
| F4 | `frontend/app/(admin)/admin/workflows/definitions/[id]/page.tsx` | CREATE |
|
||||
|
||||
---
|
||||
|
||||
## Critical Patterns
|
||||
|
||||
### Optimistic Lock — Client Side
|
||||
|
||||
```typescript
|
||||
// Frontend: store versionNo from GET /workflow-engine/instances/:id
|
||||
const { data: instance } = useWorkflowInstance(instanceId);
|
||||
|
||||
// On transition: pass versionNo in body
|
||||
await triggerTransition({
|
||||
action: 'APPROVE',
|
||||
versionNo: instance.versionNo, // ← MUST include
|
||||
attachmentPublicIds: pendingFiles,
|
||||
comment,
|
||||
});
|
||||
|
||||
// On 409 → show toast "เอกสารถูกอนุมัติโดยผู้อื่นแล้ว กรุณารีเฟรช"
|
||||
// Invalidate query cache → user sees updated state
|
||||
```
|
||||
|
||||
### DSL Role Mapping — Guard
|
||||
|
||||
```typescript
|
||||
// backend/src/modules/workflow-engine/guards/workflow-transition.guard.ts
|
||||
const DSL_ROLE_TO_CASL: Record<string, string> = {
|
||||
'Superadmin': 'system.manage_all',
|
||||
'OrgAdmin': 'organization.manage_users',
|
||||
'ContractMember': 'contract.view',
|
||||
'AssignedHandler': '__assigned__',
|
||||
};
|
||||
|
||||
// In canActivate: extract require.role from DSL compiled state
|
||||
const stepConfig = compiled?.states?.[instance.currentState];
|
||||
const requiredRoles: string[] = stepConfig?.require?.role ?? [];
|
||||
|
||||
for (const dslRole of requiredRoles) {
|
||||
const caslAbility = DSL_ROLE_TO_CASL[dslRole];
|
||||
if (!caslAbility) continue;
|
||||
if (caslAbility === '__assigned__') continue; // handled by Level 3 check
|
||||
if (userPermissions.includes(caslAbility)) return true;
|
||||
}
|
||||
// Fall through to Level 3 (assignedUserId) check as before
|
||||
```
|
||||
|
||||
### File Preview Modal — Usage
|
||||
|
||||
```tsx
|
||||
// In workflow-lifecycle.tsx
|
||||
import { FilePreviewModal } from './file-preview-modal';
|
||||
|
||||
const [preview, setPreview] = useState<WorkflowAttachmentSummary | null>(null);
|
||||
|
||||
// In attachment chip onClick:
|
||||
<button onClick={() => setPreview(attachment)}>{attachment.originalFilename}</button>
|
||||
|
||||
<FilePreviewModal attachment={preview} onClose={() => setPreview(null)} />
|
||||
```
|
||||
|
||||
### Admin DSL Editor — Monaco Setup
|
||||
|
||||
```tsx
|
||||
// In definitions/[id]/page.tsx
|
||||
import dynamic from 'next/dynamic';
|
||||
|
||||
const MonacoEditor = dynamic(() => import('@monaco-editor/react'), { ssr: false });
|
||||
|
||||
// Validate on change (debounced 800ms)
|
||||
const handleEditorChange = useCallback(
|
||||
debounce(async (value: string) => {
|
||||
try {
|
||||
const parsed = JSON.parse(value);
|
||||
const result = await validateDsl(parsed);
|
||||
setValidationErrors(result.errors);
|
||||
} catch {
|
||||
setValidationErrors([{ path: 'root', message: 'Invalid JSON' }]);
|
||||
}
|
||||
}, 800),
|
||||
[]
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Verification Commands
|
||||
|
||||
```bash
|
||||
# Backend unit tests for workflow engine
|
||||
cd backend
|
||||
pnpm test --testPathPattern=workflow-engine --coverage
|
||||
|
||||
# Frontend typecheck
|
||||
cd frontend
|
||||
pnpm tsc --noEmit
|
||||
|
||||
# Frontend component tests
|
||||
cd frontend
|
||||
pnpm vitest run components/workflow
|
||||
|
||||
# Full backend test suite
|
||||
cd backend
|
||||
pnpm test --coverage
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `N8N_WEBHOOK_URL` | Prod only | URL for dead-letter job ops notifications |
|
||||
| `REDIS_URL` | All | Redis connection for BullMQ + cache |
|
||||
|
||||
Both must be set in `docker-compose.yml` — never hardcoded.
|
||||
|
||||
---
|
||||
|
||||
## Commit Message Convention
|
||||
|
||||
```
|
||||
feat(workflow-engine): add optimistic lock version_no (FR-002, ADR-001 v1.1)
|
||||
feat(workflow-engine): add CASL DSL role mapping to guard (FR-002a)
|
||||
feat(workflow-engine): structured transition log + metrics (FR-022/023)
|
||||
feat(workflow-engine): DSL cache invalidation on activate (FR-007)
|
||||
feat(workflow-engine): BullMQ DLQ + n8n webhook (FR-005/006)
|
||||
feat(workflow-ui): FilePreviewModal component (FR-020)
|
||||
feat(workflow-ui): step-attachment upload zone in IntegratedBanner (FR-014-019)
|
||||
feat(workflow-ui): Admin DSL editor page (FR-024/025)
|
||||
feat(correspondence): IntegratedBanner gap-fill wiring (FR-011)
|
||||
chore(schema): delta-09 version_no, delta-10 action_by_user_uuid (ADR-009)
|
||||
```
|
||||
Reference in New Issue
Block a user